Spaces:
Running
Running
sanbo
commited on
Commit
·
f118c0c
1
Parent(s):
509a621
update sth. at 2025-01-26 00:51:18
Browse files- .dockerignore +128 -0
- .gitignore +79 -0
- Dockerfile +48 -1
- base_chat_format.py +30 -0
- base_get_channel.py +39 -0
- duckai_service.py +543 -0
- more_core.py +327 -0
- requirements.txt +13 -0
.dockerignore
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Version Control Systems
|
2 |
+
.git/
|
3 |
+
.gitignore
|
4 |
+
.gitattributes
|
5 |
+
.svn/
|
6 |
+
.hg/
|
7 |
+
.github/
|
8 |
+
|
9 |
+
# Environment and Configuration
|
10 |
+
.env
|
11 |
+
.env.*
|
12 |
+
*.env
|
13 |
+
.venv/
|
14 |
+
venv/
|
15 |
+
env/
|
16 |
+
ENV/
|
17 |
+
.python-version
|
18 |
+
pip-selfcheck.json
|
19 |
+
requirements.txt.bak
|
20 |
+
|
21 |
+
# Python Specific
|
22 |
+
__pycache__/
|
23 |
+
*.py[cod]
|
24 |
+
*$py.class
|
25 |
+
*.so
|
26 |
+
.Python
|
27 |
+
build/
|
28 |
+
develop-eggs/
|
29 |
+
dist/
|
30 |
+
downloads/
|
31 |
+
eggs/
|
32 |
+
.eggs/
|
33 |
+
lib/
|
34 |
+
lib64/
|
35 |
+
parts/
|
36 |
+
sdist/
|
37 |
+
var/
|
38 |
+
wheels/
|
39 |
+
*.egg-info/
|
40 |
+
.installed.cfg
|
41 |
+
*.egg
|
42 |
+
MANIFEST
|
43 |
+
|
44 |
+
# IDE and Editors
|
45 |
+
.idea/
|
46 |
+
.vscode/
|
47 |
+
*.swp
|
48 |
+
*.swo
|
49 |
+
*~
|
50 |
+
.DS_Store
|
51 |
+
.settings/
|
52 |
+
.project
|
53 |
+
.pydevproject
|
54 |
+
.classpath
|
55 |
+
*.sublime-project
|
56 |
+
*.sublime-workspace
|
57 |
+
.editorconfig
|
58 |
+
|
59 |
+
# Testing and Documentation
|
60 |
+
.tox/
|
61 |
+
.coverage
|
62 |
+
.coverage.*
|
63 |
+
htmlcov/
|
64 |
+
.pytest_cache/
|
65 |
+
nosetests.xml
|
66 |
+
coverage.xml
|
67 |
+
*.cover
|
68 |
+
.hypothesis/
|
69 |
+
docs/
|
70 |
+
*.md
|
71 |
+
LICENSE*
|
72 |
+
README*
|
73 |
+
|
74 |
+
# Logs and Data
|
75 |
+
*.log
|
76 |
+
*.sql
|
77 |
+
*.sqlite
|
78 |
+
*.db
|
79 |
+
logs/
|
80 |
+
log/
|
81 |
+
data/
|
82 |
+
tmp/
|
83 |
+
|
84 |
+
# Cache and Temporary Files
|
85 |
+
.cache/
|
86 |
+
.mypy_cache/
|
87 |
+
.dmypy.json
|
88 |
+
dmypy.json
|
89 |
+
*.bak
|
90 |
+
*.tmp
|
91 |
+
*.temp
|
92 |
+
.*.swp
|
93 |
+
*.out
|
94 |
+
|
95 |
+
# Build and Deployment
|
96 |
+
.dockerignore
|
97 |
+
Dockerfile*
|
98 |
+
docker-compose*
|
99 |
+
*.yml
|
100 |
+
*.yaml
|
101 |
+
.gitlab-ci.yml
|
102 |
+
.travis.yml
|
103 |
+
.circleci/
|
104 |
+
|
105 |
+
# Dependencies and Packages
|
106 |
+
node_modules/
|
107 |
+
package-lock.json
|
108 |
+
yarn.lock
|
109 |
+
*.pyc
|
110 |
+
*.pyo
|
111 |
+
*.pyd
|
112 |
+
*.so
|
113 |
+
*.dylib
|
114 |
+
*.dll
|
115 |
+
|
116 |
+
# System Files
|
117 |
+
.DS_Store
|
118 |
+
Thumbs.db
|
119 |
+
desktop.ini
|
120 |
+
*.swp
|
121 |
+
*~
|
122 |
+
|
123 |
+
# Project Specific Backups
|
124 |
+
*_backup/
|
125 |
+
*_bak/
|
126 |
+
*.old
|
127 |
+
*.orig
|
128 |
+
*.rej
|
.gitignore
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.externalNativeBuild
|
2 |
+
import-summary.txt
|
3 |
+
|
4 |
+
#java files
|
5 |
+
*.class
|
6 |
+
*.dex
|
7 |
+
.sync/
|
8 |
+
|
9 |
+
#for idea temp file
|
10 |
+
*.iws
|
11 |
+
*.ipr
|
12 |
+
*.iml
|
13 |
+
target/
|
14 |
+
.idea/
|
15 |
+
.idea
|
16 |
+
.gradle/
|
17 |
+
release/
|
18 |
+
build/
|
19 |
+
spoon/
|
20 |
+
releasebak/
|
21 |
+
|
22 |
+
#mac temp file
|
23 |
+
__MACOSX
|
24 |
+
.DS_Store
|
25 |
+
._.DS_Store
|
26 |
+
|
27 |
+
#for eclipse
|
28 |
+
.settings/
|
29 |
+
local.properties
|
30 |
+
*gen/
|
31 |
+
*.classpath
|
32 |
+
*/bin/
|
33 |
+
bin/
|
34 |
+
.project
|
35 |
+
|
36 |
+
#temp file
|
37 |
+
*.bak
|
38 |
+
|
39 |
+
*.pmd
|
40 |
+
sh.exe.stackdump
|
41 |
+
|
42 |
+
.vs/
|
43 |
+
.vscode/
|
44 |
+
|
45 |
+
*.log
|
46 |
+
*.ctxt
|
47 |
+
.mtj.tmp/
|
48 |
+
|
49 |
+
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
50 |
+
hs_err_pid*
|
51 |
+
|
52 |
+
# Package Files #
|
53 |
+
# *.jar
|
54 |
+
*.war
|
55 |
+
*.nar
|
56 |
+
*.ear
|
57 |
+
*.zip
|
58 |
+
*.tar.gz
|
59 |
+
*.rar
|
60 |
+
*.cxx
|
61 |
+
*.cfg
|
62 |
+
# for nodejs
|
63 |
+
node_modules/
|
64 |
+
# for python
|
65 |
+
package-lock.json
|
66 |
+
.$*
|
67 |
+
*.drawio.bkp
|
68 |
+
__pycache__
|
69 |
+
*.pyc
|
70 |
+
*.pyo
|
71 |
+
*.pyd
|
72 |
+
.Python
|
73 |
+
env
|
74 |
+
.env
|
75 |
+
.venv
|
76 |
+
pip-log.txt
|
77 |
+
dev.md
|
78 |
+
|
79 |
+
|
Dockerfile
CHANGED
@@ -1 +1,48 @@
|
|
1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 使用官方Python镜像
|
2 |
+
FROM python:3.11-slim
|
3 |
+
|
4 |
+
# 设置工作目录
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# 安装系统依赖和字体
|
8 |
+
RUN apt-get update && apt-get install -y \
|
9 |
+
fonts-ipafont-gothic \
|
10 |
+
fonts-wqy-zenhei \
|
11 |
+
fonts-thai-tlwg \
|
12 |
+
fonts-kacst \
|
13 |
+
fonts-freefont-ttf \
|
14 |
+
libxss1 \
|
15 |
+
libnss3 \
|
16 |
+
libnspr4 \
|
17 |
+
libatk1.0-0 \
|
18 |
+
libatk-bridge2.0-0 \
|
19 |
+
libcups2 \
|
20 |
+
libdrm2 \
|
21 |
+
libxkbcommon0 \
|
22 |
+
libxcomposite1 \
|
23 |
+
libxdamage1 \
|
24 |
+
libxfixes3 \
|
25 |
+
libxrandr2 \
|
26 |
+
libgbm1 \
|
27 |
+
libasound2 \
|
28 |
+
&& rm -rf /var/lib/apt/lists/*
|
29 |
+
|
30 |
+
# 复制项目文件
|
31 |
+
COPY . .
|
32 |
+
|
33 |
+
# 安装Python依赖
|
34 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
35 |
+
|
36 |
+
# 安装Playwright及其依赖
|
37 |
+
RUN playwright install chromium --with-deps
|
38 |
+
|
39 |
+
# 环境变量配置
|
40 |
+
ENV PYTHONUNBUFFERED=1
|
41 |
+
ENV DEBUG=false
|
42 |
+
ENV PORT=7860
|
43 |
+
|
44 |
+
# 暴露端口
|
45 |
+
EXPOSE 7860
|
46 |
+
|
47 |
+
# 启动命令
|
48 |
+
CMD ["python", "more_core.py"]
|
base_chat_format.py
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import random
|
3 |
+
import string
|
4 |
+
def is_chatgpt_format(data):
|
5 |
+
"""Check if the data is in the expected ChatGPT format"""
|
6 |
+
try:
|
7 |
+
# If the data is a string, try to parse it as JSON
|
8 |
+
if isinstance(data, str):
|
9 |
+
try:
|
10 |
+
data = json.loads(data)
|
11 |
+
except json.JSONDecodeError:
|
12 |
+
return False # If the string can't be parsed, it's not in the expected format
|
13 |
+
|
14 |
+
# Now check if data is a dictionary and contains the necessary structure
|
15 |
+
if isinstance(data, dict):
|
16 |
+
# Ensure 'choices' is a list and the first item has a 'message' field
|
17 |
+
if "choices" in data and isinstance(data["choices"], list) and len(data["choices"]) > 0:
|
18 |
+
if "message" in data["choices"][0]:
|
19 |
+
return True
|
20 |
+
except Exception as e:
|
21 |
+
print(f"Error checking ChatGPT format: {e}")
|
22 |
+
|
23 |
+
return False
|
24 |
+
|
25 |
+
|
26 |
+
def _generate_id(letters: int = 4, numbers: int = 6) -> str:
|
27 |
+
"""Generate unique chat completion ID"""
|
28 |
+
letters_str = ''.join(random.choices(string.ascii_lowercase, k=letters))
|
29 |
+
numbers_str = ''.join(random.choices(string.digits, k=numbers))
|
30 |
+
return f"chatcmpl-{letters_str}{numbers_str}"
|
base_get_channel.py
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import re
|
2 |
+
|
3 |
+
|
4 |
+
def get_channel_company(value: str, description: str = "") -> str:
|
5 |
+
"""
|
6 |
+
提取模型所属公司,优先级:value > description > 截取value首段
|
7 |
+
"""
|
8 |
+
|
9 |
+
# 匹配规则表(可扩展)
|
10 |
+
RULES = [
|
11 |
+
(r"claude", "Anthropic"),
|
12 |
+
(r"gemini|palm", "Google"),
|
13 |
+
(r"llama2?|llama3?|meta-llama", "Meta"),
|
14 |
+
(r"gpt-|dall·e|o1|o2|o3|o4", "OpenAI"),
|
15 |
+
(r"deepseek", "DeepSeek"),
|
16 |
+
(r"abab|minimax", "MiniMax"),
|
17 |
+
(r"mistral", "Mistral"),
|
18 |
+
(r"ernie|文心一言", "Baidu"),
|
19 |
+
(r"chatglm|智谱", "Zhipu")
|
20 |
+
]
|
21 |
+
|
22 |
+
def _match(text: str) -> str | None:
|
23 |
+
"""从文本中匹配公司(兼容空值)"""
|
24 |
+
if not text: return None
|
25 |
+
return next((owner for pattern, owner in RULES if re.search(pattern, text.lower())), None)
|
26 |
+
|
27 |
+
# 优先级逻辑
|
28 |
+
owner = _match(value) or _match(description)
|
29 |
+
if owner: return owner
|
30 |
+
|
31 |
+
# 截取首段逻辑优化(过滤空字符)
|
32 |
+
if value:
|
33 |
+
# 分割并过滤空字符串
|
34 |
+
#parts = [p for p in re.split(r'[-\s/]+', value) if p.strip()]
|
35 |
+
parts = [p for p in re.split(r'[-\s/._]+', value) if p.strip()]
|
36 |
+
if parts:
|
37 |
+
return parts[0].capitalize() # 首字母大写
|
38 |
+
|
39 |
+
return "unknown" # 全空或无有效内容
|
duckai_service.py
ADDED
@@ -0,0 +1,543 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
from playwright.sync_api import sync_playwright
|
3 |
+
import json
|
4 |
+
import base_get_channel as channel
|
5 |
+
from typing import Optional, Dict
|
6 |
+
import time
|
7 |
+
from datetime import datetime, timedelta
|
8 |
+
# 禁用 SSL 警告
|
9 |
+
import urllib3
|
10 |
+
|
11 |
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
12 |
+
|
13 |
+
debug = True
|
14 |
+
last_request_time = 0 # 上次请求的时间戳
|
15 |
+
cache_duration = 14400 # 缓存有效期,单位:秒 (4小时)
|
16 |
+
'''用于存储缓存的模型数据'''
|
17 |
+
cached_models = {
|
18 |
+
"object": "list",
|
19 |
+
"data": [],
|
20 |
+
"version": "1.0.0",
|
21 |
+
"provider": "DuckAI",
|
22 |
+
"name": "DuckAI",
|
23 |
+
"default_locale": "zh-CN",
|
24 |
+
"status": True,
|
25 |
+
"time": 0
|
26 |
+
}
|
27 |
+
|
28 |
+
'''基础模型'''
|
29 |
+
base_model = "gpt-4o-mini"
|
30 |
+
# 全局变量:存储所有模型的统计信息
|
31 |
+
# 格式:{model_name: {"calls": 调用次数, "fails": 失败次数, "last_fail": 最后失败时间}}
|
32 |
+
MODEL_STATS: Dict[str, Dict] = {}
|
33 |
+
|
34 |
+
|
35 |
+
def record_call(model_name: str, success: bool = True) -> None:
|
36 |
+
"""
|
37 |
+
记录模型调用情况
|
38 |
+
Args:
|
39 |
+
model_name: 模型名称
|
40 |
+
success: 调用是否成功
|
41 |
+
"""
|
42 |
+
global MODEL_STATS
|
43 |
+
if model_name not in MODEL_STATS:
|
44 |
+
MODEL_STATS[model_name] = {"calls": 0, "fails": 0, "last_fail": None}
|
45 |
+
|
46 |
+
stats = MODEL_STATS[model_name]
|
47 |
+
stats["calls"] += 1
|
48 |
+
if not success:
|
49 |
+
stats["fails"] += 1
|
50 |
+
stats["last_fail"] = datetime.now()
|
51 |
+
|
52 |
+
|
53 |
+
def get_auto_model(cooldown_seconds: int = 300) -> str:
|
54 |
+
"""异步获取最优模型"""
|
55 |
+
try:
|
56 |
+
if not MODEL_STATS:
|
57 |
+
get_models()
|
58 |
+
|
59 |
+
best_model = None
|
60 |
+
best_rate = -1.0
|
61 |
+
now = datetime.now()
|
62 |
+
|
63 |
+
for name, stats in MODEL_STATS.items():
|
64 |
+
if stats.get("last_fail") and (now - stats["last_fail"]) < timedelta(seconds=cooldown_seconds):
|
65 |
+
continue
|
66 |
+
|
67 |
+
total_calls = stats["calls"]
|
68 |
+
if total_calls > 0:
|
69 |
+
success_rate = (total_calls - stats["fails"]) / total_calls
|
70 |
+
if success_rate > best_rate:
|
71 |
+
best_rate = success_rate
|
72 |
+
best_model = name
|
73 |
+
|
74 |
+
default_model = best_model or base_model
|
75 |
+
if debug:
|
76 |
+
print(f"选择模型: {default_model}")
|
77 |
+
return default_model
|
78 |
+
except Exception as e:
|
79 |
+
if debug:
|
80 |
+
print(f"模型选择错误: {e}")
|
81 |
+
return base_model
|
82 |
+
|
83 |
+
|
84 |
+
def get_models():
|
85 |
+
"""model data retrieval with thread safety"""
|
86 |
+
global cached_models, last_request_time
|
87 |
+
current_time = time.time()
|
88 |
+
if (current_time - last_request_time) > cache_duration:
|
89 |
+
try:
|
90 |
+
if debug:
|
91 |
+
print(f"will get model ")
|
92 |
+
# Update timestamp before awaiting to prevent concurrent updates
|
93 |
+
get_model_impl_by_playwright()
|
94 |
+
last_request_time = current_time
|
95 |
+
if debug:
|
96 |
+
print(f"success get model ")
|
97 |
+
except Exception as e:
|
98 |
+
print(f"000000---{e}")
|
99 |
+
|
100 |
+
return json.dumps(cached_models)
|
101 |
+
|
102 |
+
|
103 |
+
def get_model_impl_by_playwright():
|
104 |
+
global cached_models
|
105 |
+
"""
|
106 |
+
从网页获取获取模型
|
107 |
+
"""
|
108 |
+
with sync_playwright() as p:
|
109 |
+
browser = p.chromium.launch(headless=True)
|
110 |
+
context = browser.new_context()
|
111 |
+
page = context.new_page()
|
112 |
+
|
113 |
+
try:
|
114 |
+
# 访问页面
|
115 |
+
page.goto("https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=1")
|
116 |
+
|
117 |
+
# 点击 Get Started 按钮
|
118 |
+
get_started_button_xpath = '//*[@id="react-layout"]/div/div[2]/main/div/div/div[2]/div/button'
|
119 |
+
page.wait_for_selector(get_started_button_xpath)
|
120 |
+
page.click(get_started_button_xpath)
|
121 |
+
|
122 |
+
# 等待所有模型加载完成
|
123 |
+
page.wait_for_function(
|
124 |
+
"document.querySelectorAll('ul[role=\"radiogroup\"] > li').length > 0"
|
125 |
+
)
|
126 |
+
|
127 |
+
# 解析模型信息
|
128 |
+
parser_models_info_form_page(page)
|
129 |
+
|
130 |
+
except Exception as e:
|
131 |
+
print(f"发生错误: {e}")
|
132 |
+
finally:
|
133 |
+
browser.close()
|
134 |
+
|
135 |
+
return cached_models
|
136 |
+
|
137 |
+
|
138 |
+
def parser_models_info_form_page(page):
|
139 |
+
global cached_models
|
140 |
+
models = page.query_selector_all('ul[role="radiogroup"] > li')
|
141 |
+
|
142 |
+
# 创建现有模型的 ID 集合用于快速查找
|
143 |
+
existing_ids = {item["id"] for item in cached_models['data']}
|
144 |
+
result = []
|
145 |
+
# 确保有内容时更新
|
146 |
+
is_update = False
|
147 |
+
for model in models:
|
148 |
+
# 使用更精确的选择器
|
149 |
+
name_element = model.query_selector('.J58ouJfofMIxA2Ukt6lA')
|
150 |
+
description_element = model.query_selector('.tDjqHxDUIeGL37tpvoSI')
|
151 |
+
# 模型真实名字
|
152 |
+
value = model.query_selector('input').get_attribute('value')
|
153 |
+
# 确保有效
|
154 |
+
if not name_element or not value:
|
155 |
+
continue
|
156 |
+
|
157 |
+
# 模型描述
|
158 |
+
name = name_element.inner_text()
|
159 |
+
description = description_element.inner_text() if description_element else ""
|
160 |
+
# 确定描述供应商
|
161 |
+
owned_by = channel.get_channel_company(value, description)
|
162 |
+
# 生成新模型数据
|
163 |
+
new_model = {
|
164 |
+
"id": value,
|
165 |
+
"object": "model",
|
166 |
+
"_type": "text",
|
167 |
+
"created": int(time.time() * 1000), # 使用当前时间戳
|
168 |
+
"owned_by": owned_by,
|
169 |
+
"description": name
|
170 |
+
}
|
171 |
+
# 记录成功
|
172 |
+
record_call(value)
|
173 |
+
# 检查是否已存在相同 ID 的模型
|
174 |
+
if new_model['id'] in existing_ids:
|
175 |
+
# 更新已存在的模型数据
|
176 |
+
for idx, item in enumerate(cached_models['data']):
|
177 |
+
if item['id'] == new_model['id']:
|
178 |
+
cached_models['data'][idx] = new_model # 完全替换旧数据
|
179 |
+
break
|
180 |
+
else:
|
181 |
+
# 添加新模型到缓存
|
182 |
+
cached_models['data'].append(new_model)
|
183 |
+
|
184 |
+
is_updated = True
|
185 |
+
|
186 |
+
# 仅在检测到更新时刷新时间戳
|
187 |
+
if is_updated:
|
188 |
+
cached_models['time'] = int(time.time() * 1000)
|
189 |
+
return json.dumps(cached_models, ensure_ascii=False)
|
190 |
+
|
191 |
+
|
192 |
+
def is_model_available(model_id: str, cooldown_seconds: int = 300) -> bool:
|
193 |
+
"""
|
194 |
+
判断模型是否在模型列表中且非最近失败的模型
|
195 |
+
|
196 |
+
Args:
|
197 |
+
model_id: 模型ID,需要检查的模型标识符
|
198 |
+
cooldown_seconds: 失败冷却时间(秒),默认300秒
|
199 |
+
|
200 |
+
Returns:
|
201 |
+
bool: 如果模型可用返回True,否则返回False
|
202 |
+
|
203 |
+
Note:
|
204 |
+
- 当MODEL_STATS为空时会自动调用get_models()更新数据
|
205 |
+
- 检查模型是否在冷却期内,如果在冷却期则返回False
|
206 |
+
"""
|
207 |
+
global MODEL_STATS
|
208 |
+
|
209 |
+
# 如果MODEL_STATS为空,加载模型数据
|
210 |
+
if not MODEL_STATS:
|
211 |
+
get_models()
|
212 |
+
|
213 |
+
# 检查模型是否在统计信息中
|
214 |
+
if model_id not in MODEL_STATS:
|
215 |
+
return False
|
216 |
+
|
217 |
+
# 检查是否在冷却期内
|
218 |
+
stats = MODEL_STATS[model_id]
|
219 |
+
if stats["last_fail"]:
|
220 |
+
time_since_failure = datetime.now() - stats["last_fail"]
|
221 |
+
if time_since_failure < timedelta(seconds=cooldown_seconds):
|
222 |
+
return False
|
223 |
+
|
224 |
+
return True
|
225 |
+
|
226 |
+
|
227 |
+
def get_model_by_autoupdate(model_id: Optional[str] = None, cooldown_seconds: int = 300) -> Optional[str]:
|
228 |
+
"""
|
229 |
+
检查提供的model_id是否可用,如果不可用则返回成功率最高的模型
|
230 |
+
|
231 |
+
Args:
|
232 |
+
model_id: 指定的模型ID,可选参数
|
233 |
+
cooldown_seconds: 失败冷却时间(秒),默认300秒
|
234 |
+
|
235 |
+
Returns:
|
236 |
+
str | None: 返回可用的模型ID,如果没有可用模型则返回None
|
237 |
+
|
238 |
+
Note:
|
239 |
+
- 当MODEL_STATS为空时会自动调用get_models()更新数据
|
240 |
+
- 如果指定的model_id可用,则直接返回
|
241 |
+
- 如果指定的model_id不可用,则返回成功率最高的模型
|
242 |
+
"""
|
243 |
+
global MODEL_STATS
|
244 |
+
|
245 |
+
# 如果MODEL_STATS为空,加载模型数据
|
246 |
+
if not MODEL_STATS:
|
247 |
+
get_models()
|
248 |
+
|
249 |
+
# 如果提供了model_id且可用,直接返回
|
250 |
+
if model_id and is_model_available(model_id, cooldown_seconds):
|
251 |
+
return model_id
|
252 |
+
|
253 |
+
# 否则返回成功率最高的可用模型
|
254 |
+
return get_auto_model(cooldown_seconds=cooldown_seconds)
|
255 |
+
|
256 |
+
|
257 |
+
################################################################################################
|
258 |
+
|
259 |
+
|
260 |
+
# 元素存放变量及时间
|
261 |
+
vqd4_time = ("", 0)
|
262 |
+
|
263 |
+
def extract_x_vqd_4(default_host='duckduckgo.com', max_retries=3, retry_delay=1):
|
264 |
+
"""
|
265 |
+
获取 x-vqd-4 token
|
266 |
+
Args:
|
267 |
+
default_host: 请求的主机地址
|
268 |
+
max_retries: 最大重试次数
|
269 |
+
retry_delay: 重试延迟时间(秒)
|
270 |
+
Returns:
|
271 |
+
str: 成功返回token,失败返回空字符串
|
272 |
+
"""
|
273 |
+
url = f"https://{default_host}/duckchat/v1/status"
|
274 |
+
global vqd4_time
|
275 |
+
headers = {
|
276 |
+
'Accept': '*/*', # 修正 Accept 头
|
277 |
+
'Accept-Encoding': 'gzip, deflate, br',
|
278 |
+
'Accept-Language': 'zh-CN,zh-Hans;q=0.9',
|
279 |
+
'Cache-Control': 'no-store', # 修正缓存控制
|
280 |
+
'Connection': 'keep-alive',
|
281 |
+
'Host': default_host,
|
282 |
+
'Pragma': 'no-cache',
|
283 |
+
'Referer': f"https://{default_host}/",
|
284 |
+
'Sec-Fetch-Dest': 'empty',
|
285 |
+
'Sec-Fetch-Mode': 'cors',
|
286 |
+
'Sec-Fetch-Site': 'same-origin',
|
287 |
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15 Ddg/17.4.1',
|
288 |
+
'X-DuckDuckGo-Client': 'macOS',
|
289 |
+
'x-vqd-accept': '1'
|
290 |
+
}
|
291 |
+
|
292 |
+
for attempt in range(max_retries):
|
293 |
+
try:
|
294 |
+
response = requests.get(
|
295 |
+
url,
|
296 |
+
headers=headers,
|
297 |
+
timeout=10, # 添加超时设置
|
298 |
+
verify=False # 忽略 SSL 验证
|
299 |
+
)
|
300 |
+
response.encoding = 'utf-8'
|
301 |
+
|
302 |
+
if response.status_code == 200:
|
303 |
+
if 'x-vqd-4' in response.headers:
|
304 |
+
vqd4 = response.headers['x-vqd-4']
|
305 |
+
if vqd4.strip(): # 验证token不为空
|
306 |
+
vqd4_time = (vqd4, int(time.time() * 1000))
|
307 |
+
if debug:
|
308 |
+
print(f"成功获取token: {vqd4}")
|
309 |
+
return vqd4
|
310 |
+
|
311 |
+
if debug:
|
312 |
+
print(f"Token为空,尝试次数: {attempt + 1}/{max_retries}")
|
313 |
+
else:
|
314 |
+
if debug:
|
315 |
+
print(f"请求失败,状态码: {response.status_code},尝试次数: {attempt + 1}/{max_retries}")
|
316 |
+
|
317 |
+
# 如果不是最后一次尝试,则等待后重试
|
318 |
+
if attempt < max_retries - 1:
|
319 |
+
time.sleep(retry_delay)
|
320 |
+
|
321 |
+
except requests.RequestException as e:
|
322 |
+
if debug:
|
323 |
+
print(f"请求异常: {e},尝试次数: {attempt + 1}/{max_retries}")
|
324 |
+
if attempt < max_retries - 1:
|
325 |
+
time.sleep(retry_delay)
|
326 |
+
continue
|
327 |
+
|
328 |
+
if debug:
|
329 |
+
print("获取token失败,已达到最大重试次数")
|
330 |
+
return "" # 所有重试都失败后返回空字符串
|
331 |
+
|
332 |
+
# def makesure_token():
|
333 |
+
# """
|
334 |
+
# 确保获取token:
|
335 |
+
# 1. 如内存没有,则新建
|
336 |
+
# 2. 内存键值对有,若失效,更新
|
337 |
+
# 2. 内存键值对有,且无失效,直接使用
|
338 |
+
# """
|
339 |
+
# global vqd4_time
|
340 |
+
# x_vqd_4_value = ''
|
341 |
+
# if not vqd4_time[0].strip():
|
342 |
+
# # 字符串为空或仅包含空格
|
343 |
+
# x_vqd_4_value = extract_x_vqd_4()
|
344 |
+
# else:
|
345 |
+
# # 字符串非空
|
346 |
+
# print("字符串非空")
|
347 |
+
# t = vqd4_time[1]
|
348 |
+
# # 获取当前的 UNIX 时间戳(以毫秒为单位)
|
349 |
+
# current_time = int(time.time() * 1000)
|
350 |
+
# # 计算时间差(绝对值)
|
351 |
+
# time_difference = abs(current_time - t)
|
352 |
+
|
353 |
+
# # 检查时间差是否大于 2 分钟 (120,000 毫秒)
|
354 |
+
# if time_difference > 2 * 60 * 1000:
|
355 |
+
# print("时间差大于 2 分钟")
|
356 |
+
# x_vqd_4_value = extract_x_vqd_4()
|
357 |
+
# else:
|
358 |
+
# print("时间差在 2 分钟以内")
|
359 |
+
# x_vqd_4_value = vqd4_time[0] # 直接使用缓存的 x_vqd_4
|
360 |
+
|
361 |
+
# # 确保有值
|
362 |
+
# if not x_vqd_4_value.strip():
|
363 |
+
# x_vqd_4_value = extract_x_vqd_4()
|
364 |
+
# return x_vqd_4_value
|
365 |
+
|
366 |
+
|
367 |
+
def parse_response(response_text):
|
368 |
+
"""
|
369 |
+
逐行解析
|
370 |
+
"""
|
371 |
+
lines = response_text.split('\n')
|
372 |
+
result = ""
|
373 |
+
for line in lines:
|
374 |
+
if line.startswith("data:"):
|
375 |
+
data = json.loads(line[len("data:"):])
|
376 |
+
if "message" in data:
|
377 |
+
result += data["message"]
|
378 |
+
print(result)
|
379 |
+
|
380 |
+
|
381 |
+
def chat_completion_message(user_prompt, x_vqd_4='', model=base_model,
|
382 |
+
system_message='You are a helpful assistant.',
|
383 |
+
user_id: str = None, session_id: str = None, default_host="duckduckgo.com"
|
384 |
+
, stream=False, temperature=0.3, max_tokens=1024, top_p=0.5, frequency_penalty=0,
|
385 |
+
presence_penalty=0):
|
386 |
+
"""
|
387 |
+
单条消息请求: https://duckduckgo.com/duckchat/v1/chat
|
388 |
+
"""
|
389 |
+
|
390 |
+
messages = [
|
391 |
+
# 需要 system-> user
|
392 |
+
{"role": "user", "content": system_message},
|
393 |
+
{"role": "user", "content": user_prompt}
|
394 |
+
]
|
395 |
+
return chat_completion_messages(messages=messages, x_vqd_4=x_vqd_4, model=model, default_host=default_host
|
396 |
+
, user_id=user_id
|
397 |
+
, session_id=session_id
|
398 |
+
, stream=stream
|
399 |
+
, temperature=temperature
|
400 |
+
, max_tokens=max_tokens
|
401 |
+
, top_p=top_p
|
402 |
+
, frequency_penalty=frequency_penalty
|
403 |
+
, presence_penalty=presence_penalty
|
404 |
+
)
|
405 |
+
|
406 |
+
|
407 |
+
def chat_completion_messages(
|
408 |
+
messages,
|
409 |
+
x_vqd_4='',
|
410 |
+
model=base_model,
|
411 |
+
user_id: str = None,
|
412 |
+
session_id: str = None,
|
413 |
+
default_host="duckduckgo.com",
|
414 |
+
stream=False, temperature=0.3, max_tokens=1024, top_p=0.5,
|
415 |
+
frequency_penalty=0, presence_penalty=0):
|
416 |
+
try:
|
417 |
+
# 确保model有效
|
418 |
+
if not model or model == "auto":
|
419 |
+
model = get_auto_model()
|
420 |
+
else:
|
421 |
+
model = get_model_by_autoupdate(model)
|
422 |
+
if debug:
|
423 |
+
print(f"校准后的model: {model}")
|
424 |
+
|
425 |
+
# 处理 token
|
426 |
+
if x_vqd_4 is None or not x_vqd_4.strip():
|
427 |
+
x_vqd_4 = extract_x_vqd_4() # 使用 makesure_token 确保获取有效的 token
|
428 |
+
|
429 |
+
# # 验证 token 格式
|
430 |
+
# if not x_vqd_4.strip() or x_vqd_4.startswith("x-vqd-4 parameter not found"):
|
431 |
+
# x_vqd_4 = makesure_token() # token 无效时重新获取
|
432 |
+
|
433 |
+
print(f"send_request 获取的token: {x_vqd_4}")
|
434 |
+
|
435 |
+
headers = {
|
436 |
+
'Accept': 'text/event-stream',
|
437 |
+
'Accept-Encoding': 'gzip, deflate, br',
|
438 |
+
'Accept-Language': 'zh-CN,zh-Hans;q=0.9',
|
439 |
+
'Cache-Control': 'no-cache',
|
440 |
+
'Connection': 'keep-alive',
|
441 |
+
# 'Content-Length': '',
|
442 |
+
'Content-Type': 'application/json',
|
443 |
+
# 'Cookie': 'av=1; bf=1; dcm=3; dcs=1; n=1',
|
444 |
+
'Host': default_host,
|
445 |
+
'Origin': f"https://{default_host}",
|
446 |
+
'Pragma': 'no-cache',
|
447 |
+
'Referer': f"https://{default_host}/",
|
448 |
+
'Sec-Fetch-Dest': 'empty',
|
449 |
+
'Sec-Fetch-Mode': 'cors',
|
450 |
+
'Sec-Fetch-Site': 'same-origin',
|
451 |
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15 Ddg/17.4.1',
|
452 |
+
'X-DuckDuckGo-Client': 'macOS'
|
453 |
+
}
|
454 |
+
# chat 独有请求头
|
455 |
+
# header.Set("accept", "text/event-stream")
|
456 |
+
# header.Set("x-vqd-4", token)
|
457 |
+
|
458 |
+
# 检查模型,如果messages 包含 system那么修改为user
|
459 |
+
for message in messages:
|
460 |
+
if message.get("role") == "system":
|
461 |
+
message["role"] = "user"
|
462 |
+
|
463 |
+
if x_vqd_4:
|
464 |
+
headers['x-vqd-4'] = x_vqd_4
|
465 |
+
data = {
|
466 |
+
"model": model,
|
467 |
+
"messages": messages
|
468 |
+
}
|
469 |
+
return chat_completion(default_host=default_host, model=model, headers=headers, payload=data)
|
470 |
+
|
471 |
+
except Exception as e:
|
472 |
+
print(f"使用模型[{model}]发生了异常:", e)
|
473 |
+
return ""
|
474 |
+
|
475 |
+
|
476 |
+
def chat_completion(default_host, model, headers, payload):
|
477 |
+
global vqd4_time
|
478 |
+
try:
|
479 |
+
response = requests.post(f'https://{default_host}/duckchat/v1/chat', headers=headers, json=payload)
|
480 |
+
response.encoding = 'utf-8' # 明确设置字符编码
|
481 |
+
response.raise_for_status()
|
482 |
+
|
483 |
+
# print("Status Code:", response.status_code)
|
484 |
+
# print("Content-Type:", response.headers.get('Content-Type'))
|
485 |
+
# print(response.text)
|
486 |
+
# print(response.headers)
|
487 |
+
|
488 |
+
final_content = ""
|
489 |
+
if response.status_code == 200:
|
490 |
+
# # 将 headers 转换为普通字典
|
491 |
+
# headers_dict = dict(response.headers)
|
492 |
+
# # 将字典转换为 JSON 字符串
|
493 |
+
# headers_json = json.dumps(headers_dict, ensure_ascii=False, indent=4)
|
494 |
+
# print(headers_json)
|
495 |
+
# 解析请求头
|
496 |
+
if 'x-vqd-4' in response.headers:
|
497 |
+
vqd4 = response.headers['x-vqd-4']
|
498 |
+
vqd4_time = (vqd4, int(time.time() * 1000))
|
499 |
+
# 解析响应内容
|
500 |
+
for line in response.iter_lines(decode_unicode=True):
|
501 |
+
# print(line)
|
502 |
+
# 检查 if 'data: [DONE]'在行中进行下一步动作
|
503 |
+
if 'data: [DONE]' in line:
|
504 |
+
# 如果找到结束信号,退出循环
|
505 |
+
break
|
506 |
+
elif line.startswith('data: '): # 确保行以'data: '开头
|
507 |
+
data_json = line[6:] # 删除行前缀'data: '
|
508 |
+
datax = json.loads(data_json) # 解析JSON字符串为字典
|
509 |
+
if 'message' in datax:
|
510 |
+
final_content += datax['message']
|
511 |
+
# print( final_content)
|
512 |
+
# 保存最终的content结果
|
513 |
+
final_result = final_content
|
514 |
+
return final_result
|
515 |
+
except Exception as e:
|
516 |
+
print(f"使用模型[{model}]发生了异常:", e)
|
517 |
+
|
518 |
+
|
519 |
+
# 测试代码
|
520 |
+
if __name__ == "__main__":
|
521 |
+
time1 = time.time() * 1000
|
522 |
+
result_json = get_models()
|
523 |
+
time2 = time.time() * 1000
|
524 |
+
print(f"耗时: {time2 - time1}")
|
525 |
+
print(result_json)
|
526 |
+
result_json2 = get_models()
|
527 |
+
time3 = time.time() * 1000
|
528 |
+
print(f"耗时2: {time3 - time2}")
|
529 |
+
print(result_json2)
|
530 |
+
|
531 |
+
print(f"获取自动模型1:{get_model_by_autoupdate('hello')}")
|
532 |
+
print(f"获取自动模型2:{get_auto_model()}")
|
533 |
+
|
534 |
+
|
535 |
+
t1 = time.time()
|
536 |
+
res = chat_completion_message("你是谁?你使用的是什么模型?你的知识库截止到什么时间? ")
|
537 |
+
t2 = time.time()
|
538 |
+
print(
|
539 |
+
f"====================================={base_model} --->测试结果【{t2 - t1}】=====================================\r\n{res}\r\n")
|
540 |
+
res2 = chat_completion_message("你比较擅长什么技能? ")
|
541 |
+
t3 = time.time()
|
542 |
+
print(
|
543 |
+
f"====================================={base_model} --->测试结果【{t3 - t2}】=====================================\r\n{res2}\r\n")
|
more_core.py
ADDED
@@ -0,0 +1,327 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import multiprocessing
|
3 |
+
import os
|
4 |
+
import time
|
5 |
+
from typing import Dict, Any, List
|
6 |
+
|
7 |
+
import tiktoken
|
8 |
+
import uvicorn
|
9 |
+
from apscheduler.schedulers.background import BackgroundScheduler
|
10 |
+
from fastapi import FastAPI, Request, HTTPException
|
11 |
+
from fastapi.responses import JSONResponse
|
12 |
+
from starlette.responses import HTMLResponse
|
13 |
+
# 禁用 SSL 警告
|
14 |
+
import urllib3
|
15 |
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
16 |
+
|
17 |
+
import duckai_service as dg
|
18 |
+
import base_chat_format as forts
|
19 |
+
|
20 |
+
|
21 |
+
# debug for Log
|
22 |
+
debug = True
|
23 |
+
|
24 |
+
app = FastAPI(
|
25 |
+
title="pekingduck",
|
26 |
+
description="Pekingduck is delious~",
|
27 |
+
version="1.0.0|2025.1.26"
|
28 |
+
)
|
29 |
+
|
30 |
+
|
31 |
+
class APIServer:
|
32 |
+
"""High-performance API server implementation"""
|
33 |
+
|
34 |
+
def __init__(self, app: FastAPI):
|
35 |
+
self.app = app
|
36 |
+
self.encoding = tiktoken.get_encoding("cl100k_base")
|
37 |
+
self._setup_routes()
|
38 |
+
self._setup_scheduler()
|
39 |
+
dg.get_models()
|
40 |
+
def _setup_scheduler(self):
|
41 |
+
""" Schedule tasks to check and reload routes and models at regular intervals. """
|
42 |
+
self.scheduler = BackgroundScheduler()
|
43 |
+
# Scheduled Task 1: Check and reload routes every 30 seconds. Calls _reload_routes_if_needed method to check if routes need to be updated
|
44 |
+
self.scheduler.add_job(self._reload_routes_if_needed, 'interval', seconds=30)
|
45 |
+
|
46 |
+
# Scheduled Task 2: Reload models every 30 minutes (1800 seconds). This task will check and update the model data periodically
|
47 |
+
self.scheduler.add_job(self._reload_check, 'interval', seconds=60 * 30)
|
48 |
+
self.scheduler.start()
|
49 |
+
def _setup_routes(self) -> None:
|
50 |
+
"""Initialize API routes"""
|
51 |
+
self.routes = """Initialize API routes"""
|
52 |
+
|
53 |
+
# Static routes with names for filtering
|
54 |
+
@self.app.get("/", name="root", include_in_schema=False)
|
55 |
+
def root():
|
56 |
+
return HTMLResponse(content="<h1>hello. It's home page.</h1>")
|
57 |
+
|
58 |
+
@self.app.get("/web", name="web")
|
59 |
+
def web():
|
60 |
+
return HTMLResponse(content="<h1>hello. It's web page.</h1>")
|
61 |
+
|
62 |
+
@self.app.get("/health", name="health")
|
63 |
+
def health():
|
64 |
+
return JSONResponse(content={"status": "working"})
|
65 |
+
|
66 |
+
|
67 |
+
@self.app.get("/v1/models", name="models")
|
68 |
+
def models():
|
69 |
+
if debug:
|
70 |
+
print("Fetching models...")
|
71 |
+
models_str = dg.get_models()
|
72 |
+
try:
|
73 |
+
models_json = json.loads(models_str)
|
74 |
+
return JSONResponse(content=models_json)
|
75 |
+
except json.JSONDecodeError as e:
|
76 |
+
raise HTTPException(status_code=500,
|
77 |
+
detail=f"Invalid models data: {str(e)}")
|
78 |
+
|
79 |
+
# Register dynamic chat completion routes
|
80 |
+
routes = self._get_routes()
|
81 |
+
if debug:
|
82 |
+
print(f"Registering routes: {routes}")
|
83 |
+
for path in routes:
|
84 |
+
self._register_route(path)
|
85 |
+
existing_routes = [route.path for route in self.app.routes if hasattr(route, 'path')]
|
86 |
+
if debug:
|
87 |
+
print(f"All routes now: {existing_routes}")
|
88 |
+
|
89 |
+
def _get_routes(self) -> List[str]:
|
90 |
+
"""Get configured API routes"""
|
91 |
+
default_path = "/api/v1/chat/completions"
|
92 |
+
replace_chat = os.getenv("REPLACE_CHAT", "")
|
93 |
+
prefix_chat = os.getenv("PREFIX_CHAT", "")
|
94 |
+
append_chat = os.getenv("APPEND_CHAT", "")
|
95 |
+
|
96 |
+
if replace_chat:
|
97 |
+
return [path.strip() for path in replace_chat.split(",") if path.strip()]
|
98 |
+
|
99 |
+
routes = []
|
100 |
+
if prefix_chat:
|
101 |
+
routes.extend(f"{prefix.rstrip('/')}{default_path}"
|
102 |
+
for prefix in prefix_chat.split(","))
|
103 |
+
return routes
|
104 |
+
|
105 |
+
if append_chat:
|
106 |
+
append_paths = [path.strip() for path in append_chat.split(",") if path.strip()]
|
107 |
+
routes = [default_path] + append_paths
|
108 |
+
return routes
|
109 |
+
|
110 |
+
return [default_path]
|
111 |
+
|
112 |
+
def _register_route(self, path: str) -> None:
|
113 |
+
"""Register a single API route"""
|
114 |
+
global debug
|
115 |
+
|
116 |
+
async def chat_endpoint(request: Request) -> Dict[str, Any]:
|
117 |
+
try:
|
118 |
+
if debug:
|
119 |
+
print(f"Request chat_endpoint...")
|
120 |
+
headers = dict(request.headers)
|
121 |
+
data = await request.json()
|
122 |
+
if debug:
|
123 |
+
print(f"Request received...\r\n\tHeaders: {headers},\r\n\tData: {data}")
|
124 |
+
return self._generate_response(headers, data)
|
125 |
+
except Exception as e:
|
126 |
+
if debug:
|
127 |
+
print(f"Request processing error: {e}")
|
128 |
+
raise HTTPException(status_code=500, detail="Internal server error") from e
|
129 |
+
|
130 |
+
self.app.post(path)(chat_endpoint)
|
131 |
+
|
132 |
+
def _calculate_tokens(self, text: str) -> int:
|
133 |
+
"""Calculate token count for text"""
|
134 |
+
return len(self.encoding.encode(text))
|
135 |
+
|
136 |
+
|
137 |
+
def process_result(self, result, model):
|
138 |
+
# 如果result是字符串,尝试将其转换为JSON
|
139 |
+
if isinstance(result, str):
|
140 |
+
try:
|
141 |
+
result = json.loads(result) # 转换为JSON
|
142 |
+
except json.JSONDecodeError:
|
143 |
+
return result
|
144 |
+
|
145 |
+
# 确保result是一个字典(JSON对象)
|
146 |
+
if isinstance(result, dict):
|
147 |
+
# 设置新的id和object值
|
148 |
+
result['id'] = forts._generate_id() # 根据需要设置新的ID值
|
149 |
+
result['object'] = "chat.completion" # 根据需要设置新的object值
|
150 |
+
|
151 |
+
# 添加model值
|
152 |
+
result['model'] = model # 根据需要设置model值
|
153 |
+
return result
|
154 |
+
|
155 |
+
def _generate_response(self, headers: Dict[str, str], data: Dict[str, Any]) -> Dict[str, Any]:
|
156 |
+
"""Generate API response"""
|
157 |
+
global debug
|
158 |
+
if debug:
|
159 |
+
print("inside _generate_response")
|
160 |
+
try:
|
161 |
+
# check model
|
162 |
+
model = data.get("model")
|
163 |
+
# print(f"model: {model}")
|
164 |
+
# just auto will check
|
165 |
+
if "auto" == model:
|
166 |
+
model = dg.get_auto_model()
|
167 |
+
# else:
|
168 |
+
# if not dg.is_model_available(model):
|
169 |
+
# raise HTTPException(status_code=400, detail="Invalid Model")
|
170 |
+
# ## kuan
|
171 |
+
# model = dg.get_model_by_autoupdate(model)
|
172 |
+
# must has token ? token check
|
173 |
+
authorization = headers.get('Authorization')
|
174 |
+
token = os.getenv("TOKEN", "")
|
175 |
+
if token and token not in authorization:
|
176 |
+
raise HTTPException(status_code=401, detail="无效的Token")
|
177 |
+
|
178 |
+
# call ai
|
179 |
+
msgs = data.get("messages")
|
180 |
+
if not msgs:
|
181 |
+
raise HTTPException(status_code=400, detail="消息不能为空")
|
182 |
+
|
183 |
+
if debug:
|
184 |
+
print(f"request model: {model}")
|
185 |
+
if token:
|
186 |
+
print(f"request token: {token}")
|
187 |
+
print(f"request messages: {msgs}")
|
188 |
+
|
189 |
+
result = dg.chat_completion_messages(
|
190 |
+
messages=msgs,
|
191 |
+
model=model
|
192 |
+
)
|
193 |
+
if debug:
|
194 |
+
print(f"result: {result}---- {forts.is_chatgpt_format(result)}")
|
195 |
+
|
196 |
+
# If the request body data already matches ChatGPT format, return it directly
|
197 |
+
if forts.is_chatgpt_format(result):
|
198 |
+
# If data already follows ChatGPT format, use it directly
|
199 |
+
response_data = self.process_result(result, model)
|
200 |
+
else:
|
201 |
+
# Calculate the current timestamp
|
202 |
+
current_timestamp = int(time.time() * 1000)
|
203 |
+
# Otherwise, calculate the tokens and return a structured response
|
204 |
+
prompt_tokens = self._calculate_tokens(str(data))
|
205 |
+
completion_tokens = self._calculate_tokens(result)
|
206 |
+
total_tokens = prompt_tokens + completion_tokens
|
207 |
+
|
208 |
+
response_data = {
|
209 |
+
"id": forts._generate_id(),
|
210 |
+
"object": "chat.completion",
|
211 |
+
"created": current_timestamp,
|
212 |
+
"model": data.get("model", "gpt-4o"),
|
213 |
+
"usage": {
|
214 |
+
"prompt_tokens": prompt_tokens,
|
215 |
+
"completion_tokens": completion_tokens,
|
216 |
+
"total_tokens": total_tokens
|
217 |
+
},
|
218 |
+
"choices": [{
|
219 |
+
"message": {
|
220 |
+
"role": "assistant",
|
221 |
+
"content": result
|
222 |
+
},
|
223 |
+
"finish_reason": "stop",
|
224 |
+
"index": 0
|
225 |
+
}]
|
226 |
+
}
|
227 |
+
|
228 |
+
# Print the response for debugging (you may remove this in production)
|
229 |
+
if debug:
|
230 |
+
print(f"Response Data: {response_data}")
|
231 |
+
|
232 |
+
return response_data
|
233 |
+
except Exception as e:
|
234 |
+
dg.record_call(model,False)
|
235 |
+
if debug:
|
236 |
+
print(f"Response generation error: {e}")
|
237 |
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
238 |
+
|
239 |
+
def _get_workers_count(self) -> int:
|
240 |
+
"""Calculate optimal worker count"""
|
241 |
+
try:
|
242 |
+
cpu_cores = multiprocessing.cpu_count()
|
243 |
+
recommended_workers = (2 * cpu_cores) + 1
|
244 |
+
return min(max(4, recommended_workers), 8)
|
245 |
+
except Exception as e:
|
246 |
+
if debug:
|
247 |
+
print(f"Worker count calculation failed: {e}, using default 4")
|
248 |
+
return 4
|
249 |
+
|
250 |
+
def get_server_config(self, host: str = "0.0.0.0", port: int = 7860) -> uvicorn.Config:
|
251 |
+
"""Get server configuration"""
|
252 |
+
workers = self._get_workers_count()
|
253 |
+
if debug:
|
254 |
+
print(f"Configuring server with {workers} workers")
|
255 |
+
|
256 |
+
return uvicorn.Config(
|
257 |
+
app=self.app,
|
258 |
+
host=host,
|
259 |
+
port=port,
|
260 |
+
workers=workers,
|
261 |
+
loop="uvloop",
|
262 |
+
limit_concurrency=1000,
|
263 |
+
timeout_keep_alive=30,
|
264 |
+
access_log=True,
|
265 |
+
log_level="info",
|
266 |
+
http="httptools"
|
267 |
+
)
|
268 |
+
|
269 |
+
def run(self, host: str = "0.0.0.0", port: int = 7860) -> None:
|
270 |
+
"""Run the API server"""
|
271 |
+
config = self.get_server_config(host, port)
|
272 |
+
server = uvicorn.Server(config)
|
273 |
+
server.run()
|
274 |
+
|
275 |
+
def _reload_check(self) -> None:
|
276 |
+
dg.reload_check()
|
277 |
+
|
278 |
+
|
279 |
+
def _reload_routes_if_needed(self) -> None:
|
280 |
+
"""Check if routes need to be reloaded based on environment variables"""
|
281 |
+
# reload Debug
|
282 |
+
global debug
|
283 |
+
debug = os.getenv("DEBUG", "False").lower() in ["true", "1", "t"]
|
284 |
+
# relaod routes
|
285 |
+
new_routes = self._get_routes()
|
286 |
+
current_routes = [route for route in self.app.routes if hasattr(route, 'path')]
|
287 |
+
|
288 |
+
# Check if the current routes are different from the new routes
|
289 |
+
if [route.path for route in current_routes] != new_routes:
|
290 |
+
if debug:
|
291 |
+
print("Routes changed, reloading...")
|
292 |
+
self._reload_routes(new_routes)
|
293 |
+
|
294 |
+
# def _reload_routes(self, new_routes: List[str]) -> None:
|
295 |
+
# """Reload the routes based on the updated configuration"""
|
296 |
+
# # Clear existing routes
|
297 |
+
# self.app.routes.clear()
|
298 |
+
# # Register new routes
|
299 |
+
# for path in new_routes:
|
300 |
+
# self._register_route(path)
|
301 |
+
|
302 |
+
def _reload_routes(self, new_routes: List[str]) -> None:
|
303 |
+
"""Reload only dynamic routes while preserving static ones"""
|
304 |
+
# Define static route names
|
305 |
+
static_routes = {"root", "web", "health", "models"}
|
306 |
+
|
307 |
+
# Remove only dynamic routes
|
308 |
+
self.app.routes[:] = [
|
309 |
+
route for route in self.app.routes
|
310 |
+
if not hasattr(route, 'name') or route.name in static_routes
|
311 |
+
]
|
312 |
+
|
313 |
+
# Register new dynamic routes
|
314 |
+
for path in new_routes:
|
315 |
+
self._register_route(path)
|
316 |
+
|
317 |
+
|
318 |
+
|
319 |
+
def create_server() -> APIServer:
|
320 |
+
"""Factory function to create server instance"""
|
321 |
+
return APIServer(app)
|
322 |
+
|
323 |
+
|
324 |
+
if __name__ == "__main__":
|
325 |
+
port = int(os.getenv("PORT", "7860"))
|
326 |
+
server = create_server()
|
327 |
+
server.run(port=port)
|
requirements.txt
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
playwright
|
2 |
+
aiohttp
|
3 |
+
apscheduler
|
4 |
+
beautifulsoup4
|
5 |
+
fastapi
|
6 |
+
requests
|
7 |
+
starlette
|
8 |
+
tiktoken
|
9 |
+
urllib3
|
10 |
+
uvicorn
|
11 |
+
uvloop
|
12 |
+
httptools
|
13 |
+
|