Spaces:
Paused
Paused
Upload 8 files
Browse files- Dockerfile +15 -0
- README.md +370 -10
- config.py +63 -0
- cookie_manager.py +151 -0
- main.py +140 -0
- models.py +66 -0
- proxy_handler.py +291 -0
- requirements.txt +13 -0
Dockerfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
VOLUME ["/data"]
|
| 9 |
+
|
| 10 |
+
COPY . .
|
| 11 |
+
|
| 12 |
+
ENV PORT=7860
|
| 13 |
+
EXPOSE 7860
|
| 14 |
+
|
| 15 |
+
CMD ["python", "main.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,370 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Z2API
|
| 2 |
+
|
| 3 |
+
一个为Z.AI API提供OpenAI兼容接口的代理服务器,支持cookie池管理、智能内容过滤和灵活的响应模式控制。
|
| 4 |
+
|
| 5 |
+
> **💡 核心特性:** 支持流式和非流式两种响应模式,非流式模式下可选择性隐藏AI思考过程,提供更简洁的API响应。
|
| 6 |
+
|
| 7 |
+
## ✨ 特性
|
| 8 |
+
|
| 9 |
+
- 🔌 **OpenAI SDK完全兼容** - 无缝替换OpenAI API
|
| 10 |
+
- 🍪 **智能Cookie池管理** - 多token轮换,自动故障转移
|
| 11 |
+
- 🧠 **智能内容过滤** - 非流式响应可选择隐藏AI思考过程
|
| 12 |
+
- 🌊 **灵活响应模式** - 支持流式和非流式响应,可配置默认模式
|
| 13 |
+
- 🛡️ **安全认证** - 固定API Key验证
|
| 14 |
+
- 📊 **健康检查** - 自动监控和恢复
|
| 15 |
+
- 📝 **详细日志** - 完善的调试和监控信息
|
| 16 |
+
|
| 17 |
+
## 🚀 快速开始
|
| 18 |
+
|
| 19 |
+
### 环境要求
|
| 20 |
+
|
| 21 |
+
- Python 3.8+
|
| 22 |
+
- pip
|
| 23 |
+
|
| 24 |
+
### 安装步骤
|
| 25 |
+
|
| 26 |
+
1. **克隆项目**
|
| 27 |
+
```bash
|
| 28 |
+
git clone https://github.com/LargeCupPanda/Z2API.git
|
| 29 |
+
cd Z2API
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
2. **安装依赖**
|
| 33 |
+
```bash
|
| 34 |
+
pip install -r requirements.txt
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
3. **配置环境变量**
|
| 38 |
+
```bash
|
| 39 |
+
cp .env.example .env
|
| 40 |
+
# 编辑 .env 文件,配置你的参数
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
4. **启动服务器**
|
| 44 |
+
```bash
|
| 45 |
+
python main.py
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
服务器将在 `http://localhost:8000` 启动
|
| 49 |
+
|
| 50 |
+
## ⚙️ 配置说明
|
| 51 |
+
|
| 52 |
+
在 `.env` 文件中配置以下参数:
|
| 53 |
+
|
| 54 |
+
```env
|
| 55 |
+
# 服务器设置
|
| 56 |
+
HOST=0.0.0.0
|
| 57 |
+
PORT=8000
|
| 58 |
+
|
| 59 |
+
# API Key (用于外部认证)
|
| 60 |
+
API_KEY=sk-z2api-key-2024
|
| 61 |
+
|
| 62 |
+
# 内容过滤设置 (仅适用于非流式响应)
|
| 63 |
+
# 是否显示AI思考过程 (true/false)
|
| 64 |
+
SHOW_THINK_TAGS=false
|
| 65 |
+
|
| 66 |
+
# 响应模式设置
|
| 67 |
+
# 默认是否使用流式响应 (true/false)
|
| 68 |
+
DEFAULT_STREAM=false
|
| 69 |
+
|
| 70 |
+
# Z.AI Token配置
|
| 71 |
+
# 从 https://chat.z.ai 获取的JWT token (不包含"Bearer "前缀),多个用`,`分隔,比如:token1,token2
|
| 72 |
+
Z_AI_COOKIES=eyJ9...
|
| 73 |
+
|
| 74 |
+
# 速率限制
|
| 75 |
+
MAX_REQUESTS_PER_MINUTE=60
|
| 76 |
+
|
| 77 |
+
# 日志级别 (DEBUG, INFO, WARNING, ERROR)
|
| 78 |
+
LOG_LEVEL=INFO
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
### 🔑 获取Z.AI Token
|
| 82 |
+
|
| 83 |
+
1. 访问 [https://chat.z.ai](https://chat.z.ai) 并登录
|
| 84 |
+
2. 打开浏览器开发者工具 (F12)
|
| 85 |
+
3. 切换到 **Network** 标签
|
| 86 |
+
4. 发送一条消息给AI
|
| 87 |
+
5. 找到对 `chat/completions` 的请求
|
| 88 |
+
6. 复制请求头中 `Authorization: Bearer xxx` 的token部分
|
| 89 |
+
7. 将token值(不包括"Bearer "前缀)配置到 `Z_AI_COOKIES`
|
| 90 |
+
|
| 91 |
+
## 📖 使用方法
|
| 92 |
+
|
| 93 |
+
### OpenAI SDK (推荐)
|
| 94 |
+
|
| 95 |
+
```python
|
| 96 |
+
import openai
|
| 97 |
+
|
| 98 |
+
# 配置客户端
|
| 99 |
+
client = openai.OpenAI(
|
| 100 |
+
base_url="http://localhost:8000/v1",
|
| 101 |
+
api_key="sk-z2api-key-2024" # 使用配置的API Key
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
# 发送请求
|
| 105 |
+
response = client.chat.completions.create(
|
| 106 |
+
model="GLM-4.5", # 固定模型名称
|
| 107 |
+
messages=[
|
| 108 |
+
{"role": "user", "content": "你好,请介绍一下自己"}
|
| 109 |
+
],
|
| 110 |
+
max_tokens=1000,
|
| 111 |
+
temperature=0.7
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
print(response.choices[0].message.content)
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
### cURL
|
| 118 |
+
|
| 119 |
+
```bash
|
| 120 |
+
curl -X POST http://localhost:8000/v1/chat/completions \
|
| 121 |
+
-H "Content-Type: application/json" \
|
| 122 |
+
-H "Authorization: Bearer sk-z2api-key-2024" \
|
| 123 |
+
-d '{
|
| 124 |
+
"model": "GLM-4.5",
|
| 125 |
+
"messages": [
|
| 126 |
+
{"role": "user", "content": "Hello, how are you?"}
|
| 127 |
+
],
|
| 128 |
+
"max_tokens": 500
|
| 129 |
+
}'
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
### 不同响应模式示例
|
| 133 |
+
|
| 134 |
+
#### 非流式响应(默认,支持思考内容过滤)
|
| 135 |
+
|
| 136 |
+
```python
|
| 137 |
+
import openai
|
| 138 |
+
|
| 139 |
+
client = openai.OpenAI(
|
| 140 |
+
base_url="http://localhost:8000/v1",
|
| 141 |
+
api_key="sk-z2api-key-2024"
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
# 非流式响应,会根据SHOW_THINK_TAGS设置过滤内容
|
| 145 |
+
response = client.chat.completions.create(
|
| 146 |
+
model="GLM-4.5",
|
| 147 |
+
messages=[{"role": "user", "content": "解释一下量子计算"}],
|
| 148 |
+
stream=False # 或者不设置此参数(使用DEFAULT_STREAM默认值)
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
print(response.choices[0].message.content)
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
#### 流式响应(包含完整内容)
|
| 155 |
+
|
| 156 |
+
```python
|
| 157 |
+
# 流式响应,始终包含完整内容(忽略SHOW_THINK_TAGS设置)
|
| 158 |
+
stream = client.chat.completions.create(
|
| 159 |
+
model="GLM-4.5",
|
| 160 |
+
messages=[{"role": "user", "content": "写一首关于春天的诗"}],
|
| 161 |
+
stream=True
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
for chunk in stream:
|
| 165 |
+
if chunk.choices[0].delta.content is not None:
|
| 166 |
+
print(chunk.choices[0].delta.content, end="")
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
## 🎛️ 高级配置
|
| 170 |
+
|
| 171 |
+
### 响应模式控制
|
| 172 |
+
|
| 173 |
+
系统支持两种响应模式,通过以下参数控制:
|
| 174 |
+
|
| 175 |
+
```env
|
| 176 |
+
# 默认响应模式 (推荐设置为false,即非流式)
|
| 177 |
+
DEFAULT_STREAM=false
|
| 178 |
+
|
| 179 |
+
# 思考内容过滤 (仅对非流式响应生效)
|
| 180 |
+
SHOW_THINK_TAGS=false
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
**响应模式说明:**
|
| 184 |
+
|
| 185 |
+
| 模式 | 参数设置 | 思考内容过滤 | 适用场景 |
|
| 186 |
+
|------|----------|--------------|----------|
|
| 187 |
+
| **非流式** | `stream=false` 或默认 | ✅ 支持 `SHOW_THINK_TAGS` | 简洁回答,API集成 |
|
| 188 |
+
| **流式** | `stream=true` | ❌ 忽略 `SHOW_THINK_TAGS` | 实时交互,聊天界面 |
|
| 189 |
+
|
| 190 |
+
**效果对比:**
|
| 191 |
+
- **非流式 + `SHOW_THINK_TAGS=false`**: 只返回答案(~80字符),简洁明了
|
| 192 |
+
- **非���式 + `SHOW_THINK_TAGS=true`**: 完整内容(~1300字符),包含思考过程
|
| 193 |
+
- **流式响应**: 始终包含完整内容,实时输出
|
| 194 |
+
|
| 195 |
+
**推荐配置:**
|
| 196 |
+
```env
|
| 197 |
+
# 推荐配置:默认非流式,隐藏思考过程
|
| 198 |
+
DEFAULT_STREAM=false
|
| 199 |
+
SHOW_THINK_TAGS=false
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
这样配置可以:
|
| 203 |
+
- 提供简洁的API响应(适合大多数应用场景)
|
| 204 |
+
- 需要完整内容时可通过 `stream=true` 获取
|
| 205 |
+
- 需要思考过程时可通过 `SHOW_THINK_TAGS=true` 开启
|
| 206 |
+
|
| 207 |
+
### Cookie池管理
|
| 208 |
+
|
| 209 |
+
支持配置多个token以提高并发性和可靠性:
|
| 210 |
+
|
| 211 |
+
```env
|
| 212 |
+
# 单个token
|
| 213 |
+
Z_AI_COOKIES=token1
|
| 214 |
+
|
| 215 |
+
# 多个token (逗号分隔)
|
| 216 |
+
Z_AI_COOKIES=token1,token2,token3
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
系统会自动:
|
| 220 |
+
- 轮换使用不同的token
|
| 221 |
+
- 检测失效的token并自动切换
|
| 222 |
+
- 定期进行健康检查和恢复
|
| 223 |
+
|
| 224 |
+
## 🔍 API端点
|
| 225 |
+
|
| 226 |
+
| 端点 | 方法 | 描述 |
|
| 227 |
+
|------|------|------|
|
| 228 |
+
| `/v1/chat/completions` | POST | 聊天完成接口 (OpenAI兼容) |
|
| 229 |
+
| `/health` | GET | 健康检查 |
|
| 230 |
+
| `/` | GET | 服务状态 |
|
| 231 |
+
|
| 232 |
+
## 🧪 测试
|
| 233 |
+
|
| 234 |
+
### 基本测试
|
| 235 |
+
|
| 236 |
+
```bash
|
| 237 |
+
# 运行示例测试
|
| 238 |
+
python example_usage.py
|
| 239 |
+
|
| 240 |
+
# 测试健康检查
|
| 241 |
+
curl http://localhost:8000/health
|
| 242 |
+
```
|
| 243 |
+
|
| 244 |
+
### API测试
|
| 245 |
+
|
| 246 |
+
```bash
|
| 247 |
+
# 测试非流式响应
|
| 248 |
+
curl -X POST http://localhost:8000/v1/chat/completions \
|
| 249 |
+
-H "Content-Type: application/json" \
|
| 250 |
+
-H "Authorization: Bearer sk-z2api-key-2024" \
|
| 251 |
+
-d '{
|
| 252 |
+
"model": "GLM-4.5",
|
| 253 |
+
"messages": [{"role": "user", "content": "Hello"}],
|
| 254 |
+
"stream": false
|
| 255 |
+
}'
|
| 256 |
+
|
| 257 |
+
# 测试流式响应
|
| 258 |
+
curl -X POST http://localhost:8000/v1/chat/completions \
|
| 259 |
+
-H "Content-Type: application/json" \
|
| 260 |
+
-H "Authorization: Bearer sk-z2api-key-2024" \
|
| 261 |
+
-d '{
|
| 262 |
+
"model": "GLM-4.5",
|
| 263 |
+
"messages": [{"role": "user", "content": "Hello"}],
|
| 264 |
+
"stream": true
|
| 265 |
+
}'
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
## 📊 监控和日志
|
| 269 |
+
|
| 270 |
+
### 日志级别
|
| 271 |
+
|
| 272 |
+
```env
|
| 273 |
+
LOG_LEVEL=DEBUG # 详细调试信息
|
| 274 |
+
LOG_LEVEL=INFO # 一般信息 (推荐)
|
| 275 |
+
LOG_LEVEL=WARNING # 警告信息
|
| 276 |
+
LOG_LEVEL=ERROR # 仅错误信息
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
### 健康检查
|
| 280 |
+
|
| 281 |
+
访问 `http://localhost:8000/health` 查看服务状态:
|
| 282 |
+
|
| 283 |
+
```json
|
| 284 |
+
{
|
| 285 |
+
"status": "healthy",
|
| 286 |
+
"timestamp": "2025-08-04T17:30:00Z",
|
| 287 |
+
"version": "1.0.0"
|
| 288 |
+
}
|
| 289 |
+
```
|
| 290 |
+
|
| 291 |
+
## 🔧 故障排除
|
| 292 |
+
|
| 293 |
+
### 常见问题
|
| 294 |
+
|
| 295 |
+
1. **401 Unauthorized**
|
| 296 |
+
- 检查API Key是否正确配置
|
| 297 |
+
- 确认使用的是 `sk-z2api-key-2024`
|
| 298 |
+
|
| 299 |
+
2. **Token失效**
|
| 300 |
+
- 重新从Z.AI网站获取新的token
|
| 301 |
+
- 更新 `.env` 文件中的 `Z_AI_COOKIES`
|
| 302 |
+
|
| 303 |
+
3. **连接超时**
|
| 304 |
+
- 检查网络连接
|
| 305 |
+
- 确认Z.AI服务可访问
|
| 306 |
+
|
| 307 |
+
4. **内容为空或不符合预期**
|
| 308 |
+
- 检查 `SHOW_THINK_TAGS` 和 `DEFAULT_STREAM` 设置
|
| 309 |
+
- 确认响应模式(流式 vs 非流式)
|
| 310 |
+
- 查看服务器日志获取详细信息
|
| 311 |
+
|
| 312 |
+
5. **思考内容过滤不生效**
|
| 313 |
+
- 确认使用的是非流式响应(`stream=false`)
|
| 314 |
+
- 流式响应会忽略 `SHOW_THINK_TAGS` 设置
|
| 315 |
+
|
| 316 |
+
6. **服务启动失败**
|
| 317 |
+
- 检查端口是否被占用:`netstat -tlnp | grep :8000`
|
| 318 |
+
- 查看详细错误:直接运行 `python main.py`
|
| 319 |
+
- 检查依赖是否安装:`pip list | grep fastapi`
|
| 320 |
+
|
| 321 |
+
### 调试模式
|
| 322 |
+
|
| 323 |
+
```bash
|
| 324 |
+
# 启用详细日志
|
| 325 |
+
export LOG_LEVEL=DEBUG
|
| 326 |
+
python main.py
|
| 327 |
+
|
| 328 |
+
# 或者直接在.env文件中设置
|
| 329 |
+
echo "LOG_LEVEL=DEBUG" >> .env
|
| 330 |
+
```
|
| 331 |
+
|
| 332 |
+
## 📋 配置参数
|
| 333 |
+
|
| 334 |
+
| 参数 | 描述 | 默认值 | 必需 |
|
| 335 |
+
|------|------|--------|------|
|
| 336 |
+
| `HOST` | 服务器监听地址 | `0.0.0.0` | 否 |
|
| 337 |
+
| `PORT` | 服务器端口 | `8000` | 否 |
|
| 338 |
+
| `API_KEY` | 外部认证密钥 | `sk-z2api-key-2024` | 否 |
|
| 339 |
+
| `SHOW_THINK_TAGS` | 显示思考内容 | `false` | 否 |
|
| 340 |
+
| `DEFAULT_STREAM` | 默认流式模式 | `false` | 否 |
|
| 341 |
+
| `Z_AI_COOKIES` | Z.AI JWT tokens | - | 是 |
|
| 342 |
+
| `LOG_LEVEL` | 日志级别 | `INFO` | 否 |
|
| 343 |
+
|
| 344 |
+
## 🛠️ 服务管理
|
| 345 |
+
|
| 346 |
+
### 基本操作
|
| 347 |
+
|
| 348 |
+
```bash
|
| 349 |
+
# 启动服务(前台运行)
|
| 350 |
+
python main.py
|
| 351 |
+
|
| 352 |
+
# 后台运行
|
| 353 |
+
nohup python main.py > z2api.log 2>&1 &
|
| 354 |
+
|
| 355 |
+
# 查看日志
|
| 356 |
+
tail -f z2api.log
|
| 357 |
+
|
| 358 |
+
# 停止服务
|
| 359 |
+
# 找到进程ID并终止
|
| 360 |
+
ps aux | grep "python main.py"
|
| 361 |
+
kill <PID>
|
| 362 |
+
```
|
| 363 |
+
|
| 364 |
+
## 🤝 贡献
|
| 365 |
+
|
| 366 |
+
**特别说明:** 作者为非编程人士,此项目全程由AI开发,AI代码100%,人类代码0%。由于这种开发模式,更新维护起来非常费劲,所以特别欢迎大家提交Issue和Pull Request来帮助改进项目!
|
| 367 |
+
|
| 368 |
+
## 📄 许可证
|
| 369 |
+
|
| 370 |
+
MIT License
|
config.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration settings for Z.AI Proxy
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
from typing import List
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
class Settings:
|
| 11 |
+
# Server settings
|
| 12 |
+
HOST: str = os.getenv("HOST", "0.0.0.0")
|
| 13 |
+
PORT: int = int(os.getenv("PORT", "7860"))
|
| 14 |
+
|
| 15 |
+
# Z.AI settings
|
| 16 |
+
UPSTREAM_URL: str = "https://chat.z.ai/api/chat/completions"
|
| 17 |
+
UPSTREAM_MODEL: str = "0727-360B-API"
|
| 18 |
+
|
| 19 |
+
# Model settings (OpenAI SDK compatible)
|
| 20 |
+
MODEL_NAME: str = "GLM-4.5"
|
| 21 |
+
MODEL_ID: str = "GLM-4.5"
|
| 22 |
+
|
| 23 |
+
# API Key for external authentication
|
| 24 |
+
API_KEY: str = os.getenv("API_KEY", "sk-z2api-key-2024")
|
| 25 |
+
|
| 26 |
+
# Content filtering settings (only applies to non-streaming responses)
|
| 27 |
+
SHOW_THINK_TAGS: bool = os.getenv("SHOW_THINK_TAGS", "false").lower() in ("true", "1", "yes")
|
| 28 |
+
|
| 29 |
+
# Response mode settings
|
| 30 |
+
DEFAULT_STREAM: bool = os.getenv("DEFAULT_STREAM", "false").lower() in ("true", "1", "yes")
|
| 31 |
+
|
| 32 |
+
# Cookie settings
|
| 33 |
+
COOKIES: List[str] = []
|
| 34 |
+
|
| 35 |
+
# Auto refresh settings
|
| 36 |
+
AUTO_REFRESH_TOKENS: bool = os.getenv("AUTO_REFRESH_TOKENS", "false").lower() in ("true", "1", "yes")
|
| 37 |
+
REFRESH_CHECK_INTERVAL: int = int(os.getenv("REFRESH_CHECK_INTERVAL", "3600")) # 1 hour
|
| 38 |
+
|
| 39 |
+
def __init__(self):
|
| 40 |
+
# Load cookies from environment variable
|
| 41 |
+
cookies_str = os.getenv("Z_AI_COOKIES", "")
|
| 42 |
+
if cookies_str and cookies_str != "your_z_ai_cookie_here":
|
| 43 |
+
self.COOKIES = [cookie.strip() for cookie in cookies_str.split(",") if cookie.strip()]
|
| 44 |
+
|
| 45 |
+
# Don't raise error immediately, let the application handle it
|
| 46 |
+
if not self.COOKIES:
|
| 47 |
+
print("⚠️ Warning: No valid Z.AI cookies configured!")
|
| 48 |
+
print("Please set Z_AI_COOKIES environment variable with comma-separated cookie values.")
|
| 49 |
+
print("Example: Z_AI_COOKIES=cookie1,cookie2,cookie3")
|
| 50 |
+
print("The server will start but API calls will fail until cookies are configured.")
|
| 51 |
+
|
| 52 |
+
# Rate limiting
|
| 53 |
+
MAX_REQUESTS_PER_MINUTE: int = int(os.getenv("MAX_REQUESTS_PER_MINUTE", "60"))
|
| 54 |
+
|
| 55 |
+
# Logging
|
| 56 |
+
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
|
| 57 |
+
|
| 58 |
+
# Create settings instance
|
| 59 |
+
try:
|
| 60 |
+
settings = Settings()
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"❌ Configuration error: {e}")
|
| 63 |
+
settings = None
|
cookie_manager.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Cookie pool manager for Z.AI tokens with round-robin rotation
|
| 3 |
+
"""
|
| 4 |
+
import asyncio
|
| 5 |
+
import logging
|
| 6 |
+
from typing import List, Optional
|
| 7 |
+
from asyncio import Lock
|
| 8 |
+
import httpx
|
| 9 |
+
from config import settings
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
class CookieManager:
|
| 14 |
+
def __init__(self, cookies: List[str]):
|
| 15 |
+
self.cookies = cookies or []
|
| 16 |
+
self.current_index = 0
|
| 17 |
+
self.lock = Lock()
|
| 18 |
+
self.failed_cookies = set()
|
| 19 |
+
|
| 20 |
+
if self.cookies:
|
| 21 |
+
logger.info(f"Initialized CookieManager with {len(cookies)} cookies")
|
| 22 |
+
else:
|
| 23 |
+
logger.warning("CookieManager initialized with no cookies")
|
| 24 |
+
|
| 25 |
+
async def get_next_cookie(self) -> Optional[str]:
|
| 26 |
+
"""Get the next available cookie using round-robin"""
|
| 27 |
+
if not self.cookies:
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
async with self.lock:
|
| 31 |
+
attempts = 0
|
| 32 |
+
while attempts < len(self.cookies):
|
| 33 |
+
cookie = self.cookies[self.current_index]
|
| 34 |
+
self.current_index = (self.current_index + 1) % len(self.cookies)
|
| 35 |
+
|
| 36 |
+
# Skip failed cookies
|
| 37 |
+
if cookie not in self.failed_cookies:
|
| 38 |
+
return cookie
|
| 39 |
+
|
| 40 |
+
attempts += 1
|
| 41 |
+
|
| 42 |
+
# All cookies failed, reset failed set and try again
|
| 43 |
+
if self.failed_cookies:
|
| 44 |
+
logger.warning(f"All {len(self.cookies)} cookies failed, resetting failed set and retrying")
|
| 45 |
+
self.failed_cookies.clear()
|
| 46 |
+
return self.cookies[0]
|
| 47 |
+
|
| 48 |
+
return None
|
| 49 |
+
|
| 50 |
+
async def mark_cookie_failed(self, cookie: str):
|
| 51 |
+
"""Mark a cookie as failed"""
|
| 52 |
+
async with self.lock:
|
| 53 |
+
self.failed_cookies.add(cookie)
|
| 54 |
+
logger.warning(f"Marked cookie as failed: {cookie[:20]}...")
|
| 55 |
+
|
| 56 |
+
async def mark_cookie_success(self, cookie: str):
|
| 57 |
+
"""Mark a cookie as working (remove from failed set)"""
|
| 58 |
+
async with self.lock:
|
| 59 |
+
if cookie in self.failed_cookies:
|
| 60 |
+
self.failed_cookies.discard(cookie)
|
| 61 |
+
logger.info(f"Cookie recovered: {cookie[:20]}...")
|
| 62 |
+
|
| 63 |
+
async def health_check(self, cookie: str) -> bool:
|
| 64 |
+
"""Check if a cookie is still valid"""
|
| 65 |
+
try:
|
| 66 |
+
async with httpx.AsyncClient() as client:
|
| 67 |
+
# Use the same payload format as actual requests
|
| 68 |
+
import uuid
|
| 69 |
+
test_payload = {
|
| 70 |
+
"stream": True,
|
| 71 |
+
"model": "0727-360B-API",
|
| 72 |
+
"messages": [{"role": "user", "content": "hi"}],
|
| 73 |
+
"background_tasks": {
|
| 74 |
+
"title_generation": False,
|
| 75 |
+
"tags_generation": False
|
| 76 |
+
},
|
| 77 |
+
"chat_id": str(uuid.uuid4()),
|
| 78 |
+
"features": {
|
| 79 |
+
"image_generation": False,
|
| 80 |
+
"code_interpreter": False,
|
| 81 |
+
"web_search": False,
|
| 82 |
+
"auto_web_search": False
|
| 83 |
+
},
|
| 84 |
+
"id": str(uuid.uuid4()),
|
| 85 |
+
"mcp_servers": [],
|
| 86 |
+
"model_item": {
|
| 87 |
+
"id": "0727-360B-API",
|
| 88 |
+
"name": "GLM-4.5",
|
| 89 |
+
"owned_by": "openai"
|
| 90 |
+
},
|
| 91 |
+
"params": {},
|
| 92 |
+
"tool_servers": [],
|
| 93 |
+
"variables": {
|
| 94 |
+
"{{USER_NAME}}": "User",
|
| 95 |
+
"{{USER_LOCATION}}": "Unknown",
|
| 96 |
+
"{{CURRENT_DATETIME}}": "2025-08-04 16:46:56"
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
response = await client.post(
|
| 100 |
+
"https://chat.z.ai/api/chat/completions",
|
| 101 |
+
headers={
|
| 102 |
+
"Authorization": f"Bearer {cookie}",
|
| 103 |
+
"Content-Type": "application/json",
|
| 104 |
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
| 105 |
+
"Accept": "application/json, text/event-stream",
|
| 106 |
+
"Accept-Language": "zh-CN",
|
| 107 |
+
"sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
|
| 108 |
+
"sec-ch-ua-mobile": "?0",
|
| 109 |
+
"sec-ch-ua-platform": '"macOS"',
|
| 110 |
+
"x-fe-version": "prod-fe-1.0.53",
|
| 111 |
+
"Origin": "https://chat.z.ai",
|
| 112 |
+
"Referer": "https://chat.z.ai/c/069723d5-060b-404f-992c-4705f1554c4c"
|
| 113 |
+
},
|
| 114 |
+
json=test_payload,
|
| 115 |
+
timeout=10.0
|
| 116 |
+
)
|
| 117 |
+
# Consider 200 as success
|
| 118 |
+
is_healthy = response.status_code == 200
|
| 119 |
+
if not is_healthy:
|
| 120 |
+
logger.debug(f"Health check failed for cookie {cookie[:20]}...: HTTP {response.status_code}")
|
| 121 |
+
else:
|
| 122 |
+
logger.debug(f"Health check passed for cookie {cookie[:20]}...")
|
| 123 |
+
|
| 124 |
+
return is_healthy
|
| 125 |
+
except Exception as e:
|
| 126 |
+
logger.debug(f"Health check failed for cookie {cookie[:20]}...: {e}")
|
| 127 |
+
return False
|
| 128 |
+
|
| 129 |
+
async def periodic_health_check(self):
|
| 130 |
+
"""Periodically check all cookies health"""
|
| 131 |
+
while True:
|
| 132 |
+
try:
|
| 133 |
+
# Only check if we have cookies and some are marked as failed
|
| 134 |
+
if self.cookies and self.failed_cookies:
|
| 135 |
+
logger.info(f"Running health check for {len(self.failed_cookies)} failed cookies")
|
| 136 |
+
|
| 137 |
+
for cookie in list(self.failed_cookies): # Create a copy to avoid modification during iteration
|
| 138 |
+
if await self.health_check(cookie):
|
| 139 |
+
await self.mark_cookie_success(cookie)
|
| 140 |
+
logger.info(f"Cookie recovered: {cookie[:20]}...")
|
| 141 |
+
else:
|
| 142 |
+
logger.debug(f"Cookie still failed: {cookie[:20]}...")
|
| 143 |
+
|
| 144 |
+
# Wait 10 minutes before next check (reduced frequency)
|
| 145 |
+
await asyncio.sleep(600)
|
| 146 |
+
except Exception as e:
|
| 147 |
+
logger.error(f"Error in periodic health check: {e}")
|
| 148 |
+
await asyncio.sleep(300) # Wait 5 minutes on error
|
| 149 |
+
|
| 150 |
+
# Global cookie manager instance
|
| 151 |
+
cookie_manager = CookieManager(settings.COOKIES if settings else [])
|
main.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Z.AI Proxy - OpenAI-compatible API for Z.AI
|
| 3 |
+
"""
|
| 4 |
+
import asyncio
|
| 5 |
+
import logging
|
| 6 |
+
from contextlib import asynccontextmanager
|
| 7 |
+
from fastapi import FastAPI, HTTPException, Depends, Request
|
| 8 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 10 |
+
|
| 11 |
+
from config import settings
|
| 12 |
+
from models import ChatCompletionRequest, ModelsResponse, ModelInfo, ErrorResponse
|
| 13 |
+
from proxy_handler import ProxyHandler
|
| 14 |
+
from cookie_manager import cookie_manager
|
| 15 |
+
|
| 16 |
+
# Configure logging
|
| 17 |
+
logging.basicConfig(
|
| 18 |
+
level=getattr(logging, settings.LOG_LEVEL),
|
| 19 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 20 |
+
)
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
# Security
|
| 24 |
+
security = HTTPBearer(auto_error=False)
|
| 25 |
+
|
| 26 |
+
@asynccontextmanager
|
| 27 |
+
async def lifespan(app: FastAPI):
|
| 28 |
+
"""Application lifespan manager"""
|
| 29 |
+
# Start background tasks
|
| 30 |
+
health_check_task = asyncio.create_task(cookie_manager.periodic_health_check())
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
yield
|
| 34 |
+
finally:
|
| 35 |
+
# Cleanup
|
| 36 |
+
health_check_task.cancel()
|
| 37 |
+
try:
|
| 38 |
+
await health_check_task
|
| 39 |
+
except asyncio.CancelledError:
|
| 40 |
+
pass
|
| 41 |
+
|
| 42 |
+
# Create FastAPI app
|
| 43 |
+
app = FastAPI(
|
| 44 |
+
title="Z.AI Proxy",
|
| 45 |
+
description="OpenAI-compatible API proxy for Z.AI",
|
| 46 |
+
version="1.0.0",
|
| 47 |
+
lifespan=lifespan
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# Add CORS middleware
|
| 51 |
+
app.add_middleware(
|
| 52 |
+
CORSMiddleware,
|
| 53 |
+
allow_origins=["*"],
|
| 54 |
+
allow_credentials=True,
|
| 55 |
+
allow_methods=["*"],
|
| 56 |
+
allow_headers=["*"],
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
async def verify_auth(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
| 60 |
+
"""Verify authentication with fixed API key"""
|
| 61 |
+
if not credentials:
|
| 62 |
+
raise HTTPException(status_code=401, detail="Authorization header required")
|
| 63 |
+
|
| 64 |
+
# Verify the API key matches our configured key
|
| 65 |
+
if credentials.credentials != settings.API_KEY:
|
| 66 |
+
raise HTTPException(status_code=401, detail="Invalid API key")
|
| 67 |
+
|
| 68 |
+
return credentials.credentials
|
| 69 |
+
|
| 70 |
+
@app.get("/v1/models", response_model=ModelsResponse)
|
| 71 |
+
async def list_models():
|
| 72 |
+
"""List available models"""
|
| 73 |
+
models = [
|
| 74 |
+
ModelInfo(
|
| 75 |
+
id=settings.MODEL_ID,
|
| 76 |
+
object="model",
|
| 77 |
+
owned_by="z-ai"
|
| 78 |
+
)
|
| 79 |
+
]
|
| 80 |
+
return ModelsResponse(data=models)
|
| 81 |
+
|
| 82 |
+
@app.post("/v1/chat/completions")
|
| 83 |
+
async def chat_completions(
|
| 84 |
+
request: ChatCompletionRequest,
|
| 85 |
+
auth_token: str = Depends(verify_auth)
|
| 86 |
+
):
|
| 87 |
+
"""Create chat completion"""
|
| 88 |
+
try:
|
| 89 |
+
# Check if cookies are configured
|
| 90 |
+
if not settings or not settings.COOKIES:
|
| 91 |
+
raise HTTPException(
|
| 92 |
+
status_code=503,
|
| 93 |
+
detail="Service unavailable: No Z.AI cookies configured. Please set Z_AI_COOKIES environment variable."
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# Validate model
|
| 97 |
+
if request.model != settings.MODEL_NAME:
|
| 98 |
+
raise HTTPException(
|
| 99 |
+
status_code=400,
|
| 100 |
+
detail=f"Model '{request.model}' not supported. Use '{settings.MODEL_NAME}'"
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
async with ProxyHandler() as handler:
|
| 104 |
+
return await handler.handle_chat_completion(request)
|
| 105 |
+
|
| 106 |
+
except HTTPException:
|
| 107 |
+
raise
|
| 108 |
+
except Exception as e:
|
| 109 |
+
logger.error(f"Unexpected error: {e}")
|
| 110 |
+
raise HTTPException(status_code=500, detail="Internal server error")
|
| 111 |
+
|
| 112 |
+
@app.get("/health")
|
| 113 |
+
async def health_check():
|
| 114 |
+
"""Health check endpoint"""
|
| 115 |
+
return {"status": "healthy", "model": settings.MODEL_NAME}
|
| 116 |
+
|
| 117 |
+
@app.exception_handler(HTTPException)
|
| 118 |
+
async def http_exception_handler(request: Request, exc: HTTPException):
|
| 119 |
+
"""Custom HTTP exception handler"""
|
| 120 |
+
from fastapi.responses import JSONResponse
|
| 121 |
+
return JSONResponse(
|
| 122 |
+
status_code=exc.status_code,
|
| 123 |
+
content={
|
| 124 |
+
"error": {
|
| 125 |
+
"message": exc.detail,
|
| 126 |
+
"type": "invalid_request_error",
|
| 127 |
+
"code": exc.status_code
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
if __name__ == "__main__":
|
| 133 |
+
import uvicorn
|
| 134 |
+
uvicorn.run(
|
| 135 |
+
"main:app",
|
| 136 |
+
host=settings.HOST,
|
| 137 |
+
port=settings.PORT,
|
| 138 |
+
reload=False,
|
| 139 |
+
log_level=settings.LOG_LEVEL.lower()
|
| 140 |
+
)
|
models.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pydantic models for OpenAI API compatibility
|
| 3 |
+
"""
|
| 4 |
+
from typing import List, Optional, Dict, Any, Union, Literal
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
|
| 7 |
+
class ChatMessage(BaseModel):
|
| 8 |
+
role: Literal["system", "user", "assistant"]
|
| 9 |
+
content: str
|
| 10 |
+
|
| 11 |
+
class ChatCompletionRequest(BaseModel):
|
| 12 |
+
model: str
|
| 13 |
+
messages: List[ChatMessage]
|
| 14 |
+
temperature: Optional[float] = 1.0
|
| 15 |
+
top_p: Optional[float] = 1.0
|
| 16 |
+
n: Optional[int] = 1
|
| 17 |
+
stream: Optional[bool] = False
|
| 18 |
+
stop: Optional[Union[str, List[str]]] = None
|
| 19 |
+
max_tokens: Optional[int] = None
|
| 20 |
+
presence_penalty: Optional[float] = 0.0
|
| 21 |
+
frequency_penalty: Optional[float] = 0.0
|
| 22 |
+
logit_bias: Optional[Dict[str, float]] = None
|
| 23 |
+
user: Optional[str] = None
|
| 24 |
+
|
| 25 |
+
class ChatCompletionChoice(BaseModel):
|
| 26 |
+
index: int
|
| 27 |
+
message: ChatMessage
|
| 28 |
+
finish_reason: Optional[str] = None
|
| 29 |
+
|
| 30 |
+
class ChatCompletionUsage(BaseModel):
|
| 31 |
+
prompt_tokens: int
|
| 32 |
+
completion_tokens: int
|
| 33 |
+
total_tokens: int
|
| 34 |
+
|
| 35 |
+
class ChatCompletionResponse(BaseModel):
|
| 36 |
+
id: str
|
| 37 |
+
object: str = "chat.completion"
|
| 38 |
+
created: int
|
| 39 |
+
model: str
|
| 40 |
+
choices: List[ChatCompletionChoice]
|
| 41 |
+
usage: Optional[ChatCompletionUsage] = None
|
| 42 |
+
|
| 43 |
+
class ChatCompletionStreamChoice(BaseModel):
|
| 44 |
+
index: int
|
| 45 |
+
delta: Dict[str, Any]
|
| 46 |
+
finish_reason: Optional[str] = None
|
| 47 |
+
|
| 48 |
+
class ChatCompletionStreamResponse(BaseModel):
|
| 49 |
+
id: str
|
| 50 |
+
object: str = "chat.completion.chunk"
|
| 51 |
+
created: int
|
| 52 |
+
model: str
|
| 53 |
+
choices: List[ChatCompletionStreamChoice]
|
| 54 |
+
|
| 55 |
+
class ModelInfo(BaseModel):
|
| 56 |
+
id: str
|
| 57 |
+
object: str = "model"
|
| 58 |
+
owned_by: str
|
| 59 |
+
permission: List[Any] = []
|
| 60 |
+
|
| 61 |
+
class ModelsResponse(BaseModel):
|
| 62 |
+
object: str = "list"
|
| 63 |
+
data: List[ModelInfo]
|
| 64 |
+
|
| 65 |
+
class ErrorResponse(BaseModel):
|
| 66 |
+
error: Dict[str, Any]
|
proxy_handler.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Proxy handler for Z.AI API requests
|
| 3 |
+
"""
|
| 4 |
+
import json
|
| 5 |
+
import logging
|
| 6 |
+
import re
|
| 7 |
+
import time
|
| 8 |
+
from typing import AsyncGenerator, Dict, Any, Optional
|
| 9 |
+
import httpx
|
| 10 |
+
from fastapi import HTTPException
|
| 11 |
+
from fastapi.responses import StreamingResponse
|
| 12 |
+
|
| 13 |
+
from config import settings
|
| 14 |
+
from cookie_manager import cookie_manager
|
| 15 |
+
from models import ChatCompletionRequest, ChatCompletionResponse, ChatCompletionStreamResponse
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
class ProxyHandler:
|
| 20 |
+
def __init__(self):
|
| 21 |
+
self.client = httpx.AsyncClient(timeout=60.0)
|
| 22 |
+
|
| 23 |
+
async def __aenter__(self):
|
| 24 |
+
return self
|
| 25 |
+
|
| 26 |
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
| 27 |
+
await self.client.aclose()
|
| 28 |
+
|
| 29 |
+
def transform_content(self, content: str) -> str:
|
| 30 |
+
"""Transform content by replacing HTML tags and optionally removing think tags"""
|
| 31 |
+
if not content:
|
| 32 |
+
return content
|
| 33 |
+
|
| 34 |
+
logger.debug(f"SHOW_THINK_TAGS setting: {settings.SHOW_THINK_TAGS}")
|
| 35 |
+
|
| 36 |
+
# Optionally remove thinking content based on configuration
|
| 37 |
+
if not settings.SHOW_THINK_TAGS:
|
| 38 |
+
logger.debug("Removing thinking content from response")
|
| 39 |
+
original_length = len(content)
|
| 40 |
+
|
| 41 |
+
# Remove <details> blocks (thinking content) - handle both closed and unclosed tags
|
| 42 |
+
# First try to remove complete <details>...</details> blocks
|
| 43 |
+
content = re.sub(r'<details[^>]*>.*?</details>', '', content, flags=re.DOTALL)
|
| 44 |
+
|
| 45 |
+
# Then remove any remaining <details> opening tags and everything after them until we hit answer content
|
| 46 |
+
# Look for pattern: <details...><summary>...</summary>...content... and remove the thinking part
|
| 47 |
+
content = re.sub(r'<details[^>]*>.*?(?=\s*[A-Z]|\s*\d|\s*$)', '', content, flags=re.DOTALL)
|
| 48 |
+
|
| 49 |
+
content = content.strip()
|
| 50 |
+
|
| 51 |
+
logger.debug(f"Content length after removing thinking content: {original_length} -> {len(content)}")
|
| 52 |
+
else:
|
| 53 |
+
logger.debug("Keeping thinking content, converting to <think> tags")
|
| 54 |
+
|
| 55 |
+
# Replace <details> with <think>
|
| 56 |
+
content = re.sub(r'<details[^>]*>', '<think>', content)
|
| 57 |
+
content = content.replace('</details>', '</think>')
|
| 58 |
+
|
| 59 |
+
# Remove <summary> tags and their content
|
| 60 |
+
content = re.sub(r'<summary>.*?</summary>', '', content, flags=re.DOTALL)
|
| 61 |
+
|
| 62 |
+
# If there's no closing </think>, add it at the end of thinking content
|
| 63 |
+
if '<think>' in content and '</think>' not in content:
|
| 64 |
+
# Find where thinking ends and answer begins
|
| 65 |
+
think_start = content.find('<think>')
|
| 66 |
+
if think_start != -1:
|
| 67 |
+
# Look for the start of the actual answer (usually starts with a capital letter or number)
|
| 68 |
+
answer_match = re.search(r'\n\s*[A-Z0-9]', content[think_start:])
|
| 69 |
+
if answer_match:
|
| 70 |
+
insert_pos = think_start + answer_match.start()
|
| 71 |
+
content = content[:insert_pos] + '</think>\n' + content[insert_pos:]
|
| 72 |
+
else:
|
| 73 |
+
content += '</think>'
|
| 74 |
+
|
| 75 |
+
return content.strip()
|
| 76 |
+
|
| 77 |
+
async def proxy_request(self, request: ChatCompletionRequest) -> Dict[str, Any]:
|
| 78 |
+
"""Proxy request to Z.AI API"""
|
| 79 |
+
cookie = await cookie_manager.get_next_cookie()
|
| 80 |
+
if not cookie:
|
| 81 |
+
raise HTTPException(status_code=503, detail="No available cookies")
|
| 82 |
+
|
| 83 |
+
# Transform model name
|
| 84 |
+
target_model = settings.UPSTREAM_MODEL if request.model == settings.MODEL_NAME else request.model
|
| 85 |
+
|
| 86 |
+
# Determine if this should be a streaming response
|
| 87 |
+
is_streaming = request.stream if request.stream is not None else settings.DEFAULT_STREAM
|
| 88 |
+
|
| 89 |
+
# Validate parameter compatibility
|
| 90 |
+
if is_streaming and not settings.SHOW_THINK_TAGS:
|
| 91 |
+
logger.warning("SHOW_THINK_TAGS=false is ignored for streaming responses")
|
| 92 |
+
|
| 93 |
+
# Prepare request data
|
| 94 |
+
request_data = request.model_dump(exclude_none=True)
|
| 95 |
+
request_data["model"] = target_model
|
| 96 |
+
|
| 97 |
+
# Build request data based on actual Z.AI format from zai-messages.md
|
| 98 |
+
import uuid
|
| 99 |
+
|
| 100 |
+
request_data = {
|
| 101 |
+
"stream": True, # Always request streaming from Z.AI for processing
|
| 102 |
+
"model": target_model,
|
| 103 |
+
"messages": request_data["messages"],
|
| 104 |
+
"background_tasks": {
|
| 105 |
+
"title_generation": True,
|
| 106 |
+
"tags_generation": True
|
| 107 |
+
},
|
| 108 |
+
"chat_id": str(uuid.uuid4()),
|
| 109 |
+
"features": {
|
| 110 |
+
"image_generation": False,
|
| 111 |
+
"code_interpreter": False,
|
| 112 |
+
"web_search": False,
|
| 113 |
+
"auto_web_search": False
|
| 114 |
+
},
|
| 115 |
+
"id": str(uuid.uuid4()),
|
| 116 |
+
"mcp_servers": ["deep-web-search"],
|
| 117 |
+
"model_item": {
|
| 118 |
+
"id": target_model,
|
| 119 |
+
"name": "GLM-4.5",
|
| 120 |
+
"owned_by": "openai"
|
| 121 |
+
},
|
| 122 |
+
"params": {},
|
| 123 |
+
"tool_servers": [],
|
| 124 |
+
"variables": {
|
| 125 |
+
"{{USER_NAME}}": "User",
|
| 126 |
+
"{{USER_LOCATION}}": "Unknown",
|
| 127 |
+
"{{CURRENT_DATETIME}}": "2025-08-04 16:46:56"
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
logger.debug(f"Sending request data: {request_data}")
|
| 134 |
+
|
| 135 |
+
headers = {
|
| 136 |
+
"Content-Type": "application/json",
|
| 137 |
+
"Authorization": f"Bearer {cookie}",
|
| 138 |
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
| 139 |
+
"Accept": "application/json, text/event-stream",
|
| 140 |
+
"Accept-Language": "zh-CN",
|
| 141 |
+
"sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
|
| 142 |
+
"sec-ch-ua-mobile": "?0",
|
| 143 |
+
"sec-ch-ua-platform": '"macOS"',
|
| 144 |
+
"x-fe-version": "prod-fe-1.0.53",
|
| 145 |
+
"Origin": "https://chat.z.ai",
|
| 146 |
+
"Referer": "https://chat.z.ai/c/069723d5-060b-404f-992c-4705f1554c4c"
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
try:
|
| 150 |
+
response = await self.client.post(
|
| 151 |
+
settings.UPSTREAM_URL,
|
| 152 |
+
json=request_data,
|
| 153 |
+
headers=headers
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
if response.status_code == 401:
|
| 157 |
+
await cookie_manager.mark_cookie_failed(cookie)
|
| 158 |
+
raise HTTPException(status_code=401, detail="Invalid authentication")
|
| 159 |
+
|
| 160 |
+
if response.status_code != 200:
|
| 161 |
+
raise HTTPException(status_code=response.status_code, detail=f"Upstream error: {response.text}")
|
| 162 |
+
|
| 163 |
+
await cookie_manager.mark_cookie_success(cookie)
|
| 164 |
+
return {"response": response, "cookie": cookie}
|
| 165 |
+
|
| 166 |
+
except httpx.RequestError as e:
|
| 167 |
+
logger.error(f"Request error: {e}")
|
| 168 |
+
await cookie_manager.mark_cookie_failed(cookie)
|
| 169 |
+
raise HTTPException(status_code=503, detail="Upstream service unavailable")
|
| 170 |
+
|
| 171 |
+
async def process_streaming_response(self, response: httpx.Response) -> AsyncGenerator[Dict[str, Any], None]:
|
| 172 |
+
"""Process streaming response from Z.AI"""
|
| 173 |
+
buffer = ""
|
| 174 |
+
|
| 175 |
+
async for chunk in response.aiter_text():
|
| 176 |
+
buffer += chunk
|
| 177 |
+
lines = buffer.split('\n')
|
| 178 |
+
buffer = lines[-1] # Keep incomplete line in buffer
|
| 179 |
+
|
| 180 |
+
for line in lines[:-1]:
|
| 181 |
+
line = line.strip()
|
| 182 |
+
if not line.startswith("data: "):
|
| 183 |
+
continue
|
| 184 |
+
|
| 185 |
+
payload = line[6:].strip()
|
| 186 |
+
if payload == "[DONE]":
|
| 187 |
+
return
|
| 188 |
+
|
| 189 |
+
try:
|
| 190 |
+
parsed = json.loads(payload)
|
| 191 |
+
|
| 192 |
+
# Check for errors first
|
| 193 |
+
if parsed.get("error") or (parsed.get("data", {}).get("error")):
|
| 194 |
+
error_detail = (parsed.get("error", {}).get("detail") or
|
| 195 |
+
parsed.get("data", {}).get("error", {}).get("detail") or
|
| 196 |
+
"Unknown error from upstream")
|
| 197 |
+
logger.error(f"Upstream error: {error_detail}")
|
| 198 |
+
raise HTTPException(status_code=400, detail=f"Upstream error: {error_detail}")
|
| 199 |
+
|
| 200 |
+
# Transform the response
|
| 201 |
+
if parsed.get("data"):
|
| 202 |
+
# Remove unwanted fields
|
| 203 |
+
parsed["data"].pop("edit_index", None)
|
| 204 |
+
parsed["data"].pop("edit_content", None)
|
| 205 |
+
|
| 206 |
+
# Note: We don't transform delta_content here because <think> tags
|
| 207 |
+
# might span multiple chunks. We'll transform the final aggregated content.
|
| 208 |
+
|
| 209 |
+
yield parsed
|
| 210 |
+
|
| 211 |
+
except json.JSONDecodeError:
|
| 212 |
+
continue # Skip non-JSON lines
|
| 213 |
+
|
| 214 |
+
async def handle_chat_completion(self, request: ChatCompletionRequest):
|
| 215 |
+
"""Handle chat completion request"""
|
| 216 |
+
proxy_result = await self.proxy_request(request)
|
| 217 |
+
response = proxy_result["response"]
|
| 218 |
+
|
| 219 |
+
# Determine final streaming mode
|
| 220 |
+
is_streaming = request.stream if request.stream is not None else settings.DEFAULT_STREAM
|
| 221 |
+
|
| 222 |
+
if is_streaming:
|
| 223 |
+
# For streaming responses, SHOW_THINK_TAGS setting is ignored
|
| 224 |
+
return StreamingResponse(
|
| 225 |
+
self.stream_response(response, request.model),
|
| 226 |
+
media_type="text/event-stream",
|
| 227 |
+
headers={
|
| 228 |
+
"Cache-Control": "no-cache",
|
| 229 |
+
"Connection": "keep-alive",
|
| 230 |
+
}
|
| 231 |
+
)
|
| 232 |
+
else:
|
| 233 |
+
# For non-streaming responses, SHOW_THINK_TAGS setting applies
|
| 234 |
+
return await self.non_stream_response(response, request.model)
|
| 235 |
+
|
| 236 |
+
async def stream_response(self, response: httpx.Response, model: str) -> AsyncGenerator[str, None]:
|
| 237 |
+
"""Generate streaming response"""
|
| 238 |
+
async for parsed in self.process_streaming_response(response):
|
| 239 |
+
yield f"data: {json.dumps(parsed)}\n\n"
|
| 240 |
+
yield "data: [DONE]\n\n"
|
| 241 |
+
|
| 242 |
+
async def non_stream_response(self, response: httpx.Response, model: str) -> ChatCompletionResponse:
|
| 243 |
+
"""Generate non-streaming response"""
|
| 244 |
+
chunks = []
|
| 245 |
+
async for parsed in self.process_streaming_response(response):
|
| 246 |
+
chunks.append(parsed)
|
| 247 |
+
logger.debug(f"Received chunk: {parsed}") # Debug log
|
| 248 |
+
|
| 249 |
+
if not chunks:
|
| 250 |
+
raise HTTPException(status_code=500, detail="No response from upstream")
|
| 251 |
+
|
| 252 |
+
logger.info(f"Total chunks received: {len(chunks)}")
|
| 253 |
+
logger.debug(f"First chunk structure: {chunks[0] if chunks else 'None'}")
|
| 254 |
+
|
| 255 |
+
# Aggregate content based on SHOW_THINK_TAGS setting
|
| 256 |
+
if settings.SHOW_THINK_TAGS:
|
| 257 |
+
# Include all content
|
| 258 |
+
full_content = "".join(
|
| 259 |
+
chunk.get("data", {}).get("delta_content", "") for chunk in chunks
|
| 260 |
+
)
|
| 261 |
+
else:
|
| 262 |
+
# Only include answer phase content
|
| 263 |
+
full_content = "".join(
|
| 264 |
+
chunk.get("data", {}).get("delta_content", "")
|
| 265 |
+
for chunk in chunks
|
| 266 |
+
if chunk.get("data", {}).get("phase") == "answer"
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
logger.info(f"Aggregated content length: {len(full_content)}")
|
| 270 |
+
logger.debug(f"Full aggregated content: {full_content}") # Show full content for debugging
|
| 271 |
+
|
| 272 |
+
# Apply content transformation (including think tag filtering)
|
| 273 |
+
transformed_content = self.transform_content(full_content)
|
| 274 |
+
|
| 275 |
+
logger.info(f"Transformed content length: {len(transformed_content)}")
|
| 276 |
+
logger.debug(f"Transformed content: {transformed_content[:200]}...")
|
| 277 |
+
|
| 278 |
+
# Create OpenAI-compatible response
|
| 279 |
+
return ChatCompletionResponse(
|
| 280 |
+
id=chunks[0].get("data", {}).get("id", "chatcmpl-unknown"),
|
| 281 |
+
created=int(time.time()),
|
| 282 |
+
model=model,
|
| 283 |
+
choices=[{
|
| 284 |
+
"index": 0,
|
| 285 |
+
"message": {
|
| 286 |
+
"role": "assistant",
|
| 287 |
+
"content": transformed_content
|
| 288 |
+
},
|
| 289 |
+
"finish_reason": "stop"
|
| 290 |
+
}]
|
| 291 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core server dependencies
|
| 2 |
+
fastapi==0.104.1
|
| 3 |
+
uvicorn[standard]==0.24.0
|
| 4 |
+
|
| 5 |
+
# HTTP client for upstream requests
|
| 6 |
+
httpx==0.25.2
|
| 7 |
+
|
| 8 |
+
# Data validation and settings
|
| 9 |
+
pydantic==2.5.0
|
| 10 |
+
python-dotenv==1.0.0
|
| 11 |
+
|
| 12 |
+
# OpenAI SDK for testing and examples
|
| 13 |
+
openai>=1.0.0
|