Spaces:
Paused
Paused
kao0312 commited on
Commit ·
cbe30d3
1
Parent(s): d41adbf
feat: lowercase model ids, add glm-5, socks5 proxy pool, remove arm build
Browse files- .github/workflows/docker-image.yml +1 -1
- .gitignore +2 -0
- Dockerfile +3 -0
- README.md +52 -27
- go.mod +3 -3
- go.sum +10 -0
- internal/auth/anonymous.go +3 -1
- internal/model/mapping.go +39 -40
- internal/model/mapping_test.go +69 -30
- internal/proxy/pool.go +120 -0
- internal/upstream/client.go +2 -1
- internal/upstream/upload.go +3 -2
- internal/version/version.go +4 -5
- main.go +2 -0
.github/workflows/docker-image.yml
CHANGED
|
@@ -52,4 +52,4 @@ jobs:
|
|
| 52 |
labels: ${{ steps.meta.outputs.labels }}
|
| 53 |
cache-from: type=gha
|
| 54 |
cache-to: type=gha,mode=max
|
| 55 |
-
platforms: linux/amd64
|
|
|
|
| 52 |
labels: ${{ steps.meta.outputs.labels }}
|
| 53 |
cache-from: type=gha
|
| 54 |
cache-to: type=gha,mode=max
|
| 55 |
+
platforms: linux/amd64
|
.gitignore
CHANGED
|
@@ -4,3 +4,5 @@
|
|
| 4 |
PROJECT_STRUCTURE.md
|
| 5 |
hello.txt
|
| 6 |
psychoanalysis.txt
|
|
|
|
|
|
|
|
|
| 4 |
PROJECT_STRUCTURE.md
|
| 5 |
hello.txt
|
| 6 |
psychoanalysis.txt
|
| 7 |
+
proxies.txt
|
| 8 |
+
zai-proxy
|
Dockerfile
CHANGED
|
@@ -14,6 +14,9 @@ WORKDIR /app
|
|
| 14 |
|
| 15 |
COPY --from=builder /app/zai-proxy .
|
| 16 |
|
|
|
|
|
|
|
|
|
|
| 17 |
EXPOSE 8000
|
| 18 |
|
| 19 |
CMD ["./zai-proxy"]
|
|
|
|
| 14 |
|
| 15 |
COPY --from=builder /app/zai-proxy .
|
| 16 |
|
| 17 |
+
# proxies.txt 可通过 docker run -v ./proxies.txt:/app/proxies.txt 挂载
|
| 18 |
+
VOLUME ["/app/proxies.txt"]
|
| 19 |
+
|
| 20 |
EXPOSE 8000
|
| 21 |
|
| 22 |
CMD ["./zai-proxy"]
|
README.md
CHANGED
|
@@ -53,7 +53,16 @@ docker run -d --name zai-proxy \
|
|
| 53 |
ghcr.io/yurika0211/zai-proxy:latest
|
| 54 |
```
|
| 55 |
|
| 56 |
-
> 镜像支持 `linux/amd64`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
#### 可用镜像标签
|
| 59 |
|
|
@@ -96,6 +105,17 @@ docker compose up -d
|
|
| 96 |
|
| 97 |
支持 `.env` 文件自动加载。
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
## 获取 z.ai Token
|
| 100 |
|
| 101 |
### 方式一:使用匿名 Token(免登录)
|
|
@@ -106,7 +126,7 @@ docker compose up -d
|
|
| 106 |
curl http://localhost:8000/v1/chat/completions \
|
| 107 |
-H "Authorization: Bearer free" \
|
| 108 |
-H "Content-Type: application/json" \
|
| 109 |
-
-d '{"model": "
|
| 110 |
```
|
| 111 |
|
| 112 |
### 方式二:使用个人 Token
|
|
@@ -134,7 +154,7 @@ curl http://localhost:8000/v1/chat/completions \
|
|
| 134 |
-H "Authorization: Bearer YOUR_ZAI_TOKEN" \
|
| 135 |
-H "Content-Type: application/json" \
|
| 136 |
-d '{
|
| 137 |
-
"model": "
|
| 138 |
"messages": [{"role": "user", "content": "hello"}],
|
| 139 |
"stream": true
|
| 140 |
}'
|
|
@@ -171,12 +191,13 @@ curl http://localhost:8000/v1/messages \
|
|
| 171 |
|
| 172 |
| 模型名称 | 上游模型 | 说明 |
|
| 173 |
|----------|----------|------|
|
| 174 |
-
| `
|
| 175 |
-
| `
|
| 176 |
-
| `
|
| 177 |
-
| `
|
| 178 |
-
| `
|
| 179 |
-
| `
|
|
|
|
| 180 |
|
| 181 |
### Claude 模型名映射
|
| 182 |
|
|
@@ -184,16 +205,16 @@ curl http://localhost:8000/v1/messages \
|
|
| 184 |
|
| 185 |
| Claude 模型 | 映射到 | 备注 |
|
| 186 |
|-------------|--------|------|
|
| 187 |
-
| `claude-opus-4-6` |
|
| 188 |
-
| `claude-opus-4-5-20250514` |
|
| 189 |
-
| `claude-sonnet-4-6` |
|
| 190 |
-
| `claude-sonnet-4-5-20241022` |
|
| 191 |
-
| `claude-haiku-4-5` |
|
| 192 |
-
| `claude-haiku-4-5-20251001` |
|
| 193 |
-
| `claude-3-5-sonnet-20241022` |
|
| 194 |
-
| `claude-3-5-haiku-20241022` |
|
| 195 |
|
| 196 |
-
> Opus 系列模型始终自动启用 thinking 模式。未识别的模型名会回退到
|
| 197 |
|
| 198 |
### 模型标签
|
| 199 |
|
|
@@ -206,11 +227,13 @@ curl http://localhost:8000/v1/messages \
|
|
| 206 |
| `-tools` | 自动注入内置工具定义,模型可进行函数调用 |
|
| 207 |
|
| 208 |
组合示例:
|
| 209 |
-
- `
|
| 210 |
-
- `
|
| 211 |
-
- `
|
| 212 |
-
- `
|
| 213 |
-
- `
|
|
|
|
|
|
|
| 214 |
|
| 215 |
## 使用示例
|
| 216 |
|
|
@@ -218,7 +241,7 @@ curl http://localhost:8000/v1/messages \
|
|
| 218 |
|
| 219 |
```json
|
| 220 |
{
|
| 221 |
-
"model": "
|
| 222 |
"messages": [
|
| 223 |
{
|
| 224 |
"role": "user",
|
|
@@ -292,7 +315,7 @@ curl http://localhost:8000/v1/chat/completions \
|
|
| 292 |
-H "Authorization: Bearer YOUR_ZAI_TOKEN" \
|
| 293 |
-H "Content-Type: application/json" \
|
| 294 |
-d '{
|
| 295 |
-
"model": "
|
| 296 |
"messages": [{"role": "user", "content": "现在几点了?"}],
|
| 297 |
"stream": true
|
| 298 |
}'
|
|
@@ -312,7 +335,7 @@ curl http://localhost:8000/v1/chat/completions \
|
|
| 312 |
-H "Authorization: Bearer YOUR_ZAI_TOKEN" \
|
| 313 |
-H "Content-Type: application/json" \
|
| 314 |
-d '{
|
| 315 |
-
"model": "
|
| 316 |
"messages": [
|
| 317 |
{"role": "user", "content": "现在几点了?"},
|
| 318 |
{"role": "assistant", "content": "", "tool_calls": [
|
|
@@ -330,7 +353,7 @@ curl http://localhost:8000/v1/chat/completions \
|
|
| 330 |
|
| 331 |
```json
|
| 332 |
{
|
| 333 |
-
"model": "
|
| 334 |
"messages": [{"role": "user", "content": "北京天气怎么样?"}],
|
| 335 |
"tools": [{
|
| 336 |
"type": "function",
|
|
@@ -418,6 +441,8 @@ zai-proxy/
|
|
| 418 |
│ │ ├── types.go # OpenAI 兼容类型
|
| 419 |
│ │ ├── anthropic.go # Anthropic API 类型
|
| 420 |
│ │ └── mapping.go # 模型名映射与解析
|
|
|
|
|
|
|
| 421 |
│ ├── tools/ # 内置工具定义
|
| 422 |
│ │ ├── builtin.go # 6 个内置工具
|
| 423 |
│ │ └── prompt.go # 工具系统提示词构建
|
|
|
|
| 53 |
ghcr.io/yurika0211/zai-proxy:latest
|
| 54 |
```
|
| 55 |
|
| 56 |
+
> 镜像仅支持 `linux/amd64` 平台。
|
| 57 |
+
|
| 58 |
+
#### 挂载代理文件(可选)
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
docker run -d --name zai-proxy \
|
| 62 |
+
-p 8000:8000 \
|
| 63 |
+
-v ./proxies.txt:/app/proxies.txt \
|
| 64 |
+
ghcr.io/yurika0211/zai-proxy:latest
|
| 65 |
+
```
|
| 66 |
|
| 67 |
#### 可用镜像标签
|
| 68 |
|
|
|
|
| 105 |
|
| 106 |
支持 `.env` 文件自动加载。
|
| 107 |
|
| 108 |
+
## 代理池(可选)
|
| 109 |
+
|
| 110 |
+
在项目根目录放置 `proxies.txt` 文件,每行一个 SOCKS5 代理,格式:
|
| 111 |
+
|
| 112 |
+
```
|
| 113 |
+
ip:port:username:password
|
| 114 |
+
ip:port
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
启动时自动加载,每次请求随机选择一个代理。不存在该文件则直连。
|
| 118 |
+
|
| 119 |
## 获取 z.ai Token
|
| 120 |
|
| 121 |
### 方式一:使用匿名 Token(免登录)
|
|
|
|
| 126 |
curl http://localhost:8000/v1/chat/completions \
|
| 127 |
-H "Authorization: Bearer free" \
|
| 128 |
-H "Content-Type: application/json" \
|
| 129 |
+
-d '{"model": "glm-4.7", "messages": [{"role": "user", "content": "hello"}]}'
|
| 130 |
```
|
| 131 |
|
| 132 |
### 方式二:使用个人 Token
|
|
|
|
| 154 |
-H "Authorization: Bearer YOUR_ZAI_TOKEN" \
|
| 155 |
-H "Content-Type: application/json" \
|
| 156 |
-d '{
|
| 157 |
+
"model": "glm-4.7",
|
| 158 |
"messages": [{"role": "user", "content": "hello"}],
|
| 159 |
"stream": true
|
| 160 |
}'
|
|
|
|
| 191 |
|
| 192 |
| 模型名称 | 上游模型 | 说明 |
|
| 193 |
|----------|----------|------|
|
| 194 |
+
| `glm-4.5` | 0727-360B-API | |
|
| 195 |
+
| `glm-4.6` | GLM-4-6-API-V1 | |
|
| 196 |
+
| `glm-4.7` | glm-4.7 | |
|
| 197 |
+
| `glm-5` | glm-5 | 最新 |
|
| 198 |
+
| `glm-4.5-v` | glm-4.5v | 视觉 |
|
| 199 |
+
| `glm-4.6-v` | glm-4.6v | 视觉(最新) |
|
| 200 |
+
| `glm-4.5-air` | 0727-106B-API | 轻量 |
|
| 201 |
|
| 202 |
### Claude 模型名映射
|
| 203 |
|
|
|
|
| 205 |
|
| 206 |
| Claude 模型 | 映射到 | 备注 |
|
| 207 |
|-------------|--------|------|
|
| 208 |
+
| `claude-opus-4-6` | glm-4.7 | 自动启用 thinking |
|
| 209 |
+
| `claude-opus-4-5-20250514` | glm-4.7 | 自动启用 thinking |
|
| 210 |
+
| `claude-sonnet-4-6` | glm-4.7 | |
|
| 211 |
+
| `claude-sonnet-4-5-20241022` | glm-4.7 | |
|
| 212 |
+
| `claude-haiku-4-5` | glm-4.5-air | |
|
| 213 |
+
| `claude-haiku-4-5-20251001` | glm-4.5-air | |
|
| 214 |
+
| `claude-3-5-sonnet-20241022` | glm-4.7 | |
|
| 215 |
+
| `claude-3-5-haiku-20241022` | glm-4.5-air | |
|
| 216 |
|
| 217 |
+
> Opus 系列模型始终自动启用 thinking 模式。未识别的模型名会回退到 glm-4.7。
|
| 218 |
|
| 219 |
### 模型标签
|
| 220 |
|
|
|
|
| 227 |
| `-tools` | 自动注入内置工具定义,模型可进行函数调用 |
|
| 228 |
|
| 229 |
组合示例:
|
| 230 |
+
- `glm-4.7-thinking`
|
| 231 |
+
- `glm-4.7-search`
|
| 232 |
+
- `glm-4.7-thinking-search`
|
| 233 |
+
- `glm-4.7-tools`
|
| 234 |
+
- `glm-4.7-tools-thinking`
|
| 235 |
+
- `glm-5-thinking`
|
| 236 |
+
- `glm-5-tools`
|
| 237 |
|
| 238 |
## 使用示例
|
| 239 |
|
|
|
|
| 241 |
|
| 242 |
```json
|
| 243 |
{
|
| 244 |
+
"model": "glm-4.6-v",
|
| 245 |
"messages": [
|
| 246 |
{
|
| 247 |
"role": "user",
|
|
|
|
| 315 |
-H "Authorization: Bearer YOUR_ZAI_TOKEN" \
|
| 316 |
-H "Content-Type: application/json" \
|
| 317 |
-d '{
|
| 318 |
+
"model": "glm-4.7-tools",
|
| 319 |
"messages": [{"role": "user", "content": "现在几点了?"}],
|
| 320 |
"stream": true
|
| 321 |
}'
|
|
|
|
| 335 |
-H "Authorization: Bearer YOUR_ZAI_TOKEN" \
|
| 336 |
-H "Content-Type: application/json" \
|
| 337 |
-d '{
|
| 338 |
+
"model": "glm-4.7-tools",
|
| 339 |
"messages": [
|
| 340 |
{"role": "user", "content": "现在几点了?"},
|
| 341 |
{"role": "assistant", "content": "", "tool_calls": [
|
|
|
|
| 353 |
|
| 354 |
```json
|
| 355 |
{
|
| 356 |
+
"model": "glm-4.7",
|
| 357 |
"messages": [{"role": "user", "content": "北京天气怎么样?"}],
|
| 358 |
"tools": [{
|
| 359 |
"type": "function",
|
|
|
|
| 441 |
│ │ ├── types.go # OpenAI 兼容类型
|
| 442 |
│ │ ├── anthropic.go # Anthropic API 类型
|
| 443 |
│ │ └── mapping.go # 模型名映射与解析
|
| 444 |
+
│ ├── proxy/ # SOCKS5 代理池
|
| 445 |
+
│ │ └── pool.go # 代理加载与随机选择
|
| 446 |
│ ├── tools/ # 内置工具定义
|
| 447 |
│ │ ├── builtin.go # 6 个内置工具
|
| 448 |
│ │ └── prompt.go # 工具系统提示词构建
|
go.mod
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
module zai-proxy
|
| 2 |
|
| 3 |
-
go 1.
|
| 4 |
|
| 5 |
require (
|
|
|
|
| 6 |
github.com/google/uuid v1.6.0
|
| 7 |
github.com/joho/godotenv v1.5.1
|
|
|
|
| 8 |
)
|
| 9 |
-
|
| 10 |
-
require github.com/corpix/uarand v0.2.0 // indirect
|
|
|
|
| 1 |
module zai-proxy
|
| 2 |
|
| 3 |
+
go 1.25.0
|
| 4 |
|
| 5 |
require (
|
| 6 |
+
github.com/corpix/uarand v0.2.0
|
| 7 |
github.com/google/uuid v1.6.0
|
| 8 |
github.com/joho/godotenv v1.5.1
|
| 9 |
+
golang.org/x/net v0.52.0
|
| 10 |
)
|
|
|
|
|
|
go.sum
CHANGED
|
@@ -1,6 +1,16 @@
|
|
| 1 |
github.com/corpix/uarand v0.2.0 h1:U98xXwud/AVuCpkpgfPF7J5TQgr7R5tqT8VZP5KWbzE=
|
| 2 |
github.com/corpix/uarand v0.2.0/go.mod h1:/3Z1QIqWkDIhf6XWn/08/uMHoQ8JUoTIKc2iPchBOmM=
|
|
|
|
|
|
|
| 3 |
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 4 |
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 5 |
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
| 6 |
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
github.com/corpix/uarand v0.2.0 h1:U98xXwud/AVuCpkpgfPF7J5TQgr7R5tqT8VZP5KWbzE=
|
| 2 |
github.com/corpix/uarand v0.2.0/go.mod h1:/3Z1QIqWkDIhf6XWn/08/uMHoQ8JUoTIKc2iPchBOmM=
|
| 3 |
+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
| 4 |
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 5 |
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 6 |
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 7 |
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
| 8 |
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
| 9 |
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
| 10 |
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 11 |
+
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
| 12 |
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 13 |
+
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
| 14 |
+
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
| 15 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
| 16 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
internal/auth/anonymous.go
CHANGED
|
@@ -4,6 +4,8 @@ import (
|
|
| 4 |
"encoding/json"
|
| 5 |
"fmt"
|
| 6 |
"net/http"
|
|
|
|
|
|
|
| 7 |
)
|
| 8 |
|
| 9 |
type AnonymousAuthResponse struct {
|
|
@@ -12,7 +14,7 @@ type AnonymousAuthResponse struct {
|
|
| 12 |
|
| 13 |
// GetAnonymousToken 从 z.ai 获取匿名 token
|
| 14 |
func GetAnonymousToken() (string, error) {
|
| 15 |
-
resp, err :=
|
| 16 |
if err != nil {
|
| 17 |
return "", err
|
| 18 |
}
|
|
|
|
| 4 |
"encoding/json"
|
| 5 |
"fmt"
|
| 6 |
"net/http"
|
| 7 |
+
|
| 8 |
+
"zai-proxy/internal/proxy"
|
| 9 |
)
|
| 10 |
|
| 11 |
type AnonymousAuthResponse struct {
|
|
|
|
| 14 |
|
| 15 |
// GetAnonymousToken 从 z.ai 获取匿名 token
|
| 16 |
func GetAnonymousToken() (string, error) {
|
| 17 |
+
resp, err := proxy.GetHTTPClient().Get("https://chat.z.ai/api/v1/auths/")
|
| 18 |
if err != nil {
|
| 19 |
return "", err
|
| 20 |
}
|
internal/model/mapping.go
CHANGED
|
@@ -2,41 +2,39 @@ package model
|
|
| 2 |
|
| 3 |
import "strings"
|
| 4 |
|
| 5 |
-
// 基础模型映射(不包含标签后缀)
|
| 6 |
var BaseModelMapping = map[string]string{
|
| 7 |
-
"
|
| 8 |
-
"
|
| 9 |
-
"
|
| 10 |
-
"
|
| 11 |
-
"
|
| 12 |
-
"
|
| 13 |
-
"
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
// Claude 模型名到 GLM 基础模型名的映射
|
| 17 |
var ClaudeModelMapping = map[string]string{
|
| 18 |
-
"claude-opus-4-6":
|
| 19 |
-
"claude-opus-4-5-20250514":
|
| 20 |
-
"claude-sonnet-4-6":
|
| 21 |
-
"claude-sonnet-4-5-20241022": "
|
| 22 |
-
"claude-haiku-4-5":
|
| 23 |
-
"claude-haiku-4-5-20251001":
|
| 24 |
-
"claude-3-5-sonnet-20241022": "
|
| 25 |
-
"claude-3-5-haiku-20241022": "
|
| 26 |
}
|
| 27 |
|
| 28 |
// ResolveClaudeModel maps a Claude model name to a GLM model name with appropriate tags.
|
| 29 |
-
// Returns the resolved model name and whether thinking should be enabled.
|
| 30 |
func ResolveClaudeModel(model string, thinkingEnabled bool) (resolvedModel string, enableThinking bool) {
|
| 31 |
-
base, ok := ClaudeModelMapping[model]
|
| 32 |
if !ok {
|
| 33 |
-
|
| 34 |
-
base = "GLM-4.7"
|
| 35 |
}
|
| 36 |
|
| 37 |
enableThinking = thinkingEnabled
|
| 38 |
-
|
| 39 |
-
if strings.Contains(model, "opus") {
|
| 40 |
enableThinking = true
|
| 41 |
}
|
| 42 |
|
|
@@ -44,36 +42,38 @@ func ResolveClaudeModel(model string, thinkingEnabled bool) (resolvedModel strin
|
|
| 44 |
if enableThinking {
|
| 45 |
resolvedModel += "-thinking"
|
| 46 |
}
|
| 47 |
-
// Always enable tools for Claude requests (handled via prompt injection)
|
| 48 |
resolvedModel += "-tools"
|
| 49 |
return resolvedModel, enableThinking
|
| 50 |
}
|
| 51 |
|
| 52 |
-
// v1/models 返回的模型列表(
|
| 53 |
var ModelList = []string{
|
| 54 |
-
"
|
| 55 |
-
"
|
| 56 |
-
"
|
| 57 |
-
"
|
| 58 |
-
"
|
| 59 |
-
"
|
| 60 |
-
"
|
| 61 |
-
"
|
| 62 |
-
"
|
| 63 |
-
"
|
| 64 |
-
"
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
}
|
| 67 |
|
| 68 |
// 解析模型名称,提取基础模型名和标签
|
| 69 |
-
//
|
| 70 |
func ParseModelName(model string) (baseModel string, enableThinking bool, enableSearch bool, enableTools bool) {
|
| 71 |
enableThinking = false
|
| 72 |
enableSearch = false
|
| 73 |
enableTools = false
|
| 74 |
-
baseModel = model
|
| 75 |
|
| 76 |
-
// 检查并移除 -thinking、-search 和 -tools 标签(任意顺序)
|
| 77 |
for {
|
| 78 |
if strings.HasSuffix(baseModel, "-thinking") {
|
| 79 |
enableThinking = true
|
|
@@ -112,6 +112,5 @@ func GetTargetModel(model string) string {
|
|
| 112 |
if target, ok := BaseModelMapping[baseModel]; ok {
|
| 113 |
return target
|
| 114 |
}
|
| 115 |
-
// Fallback: unknown models default to glm-4.7
|
| 116 |
return "glm-4.7"
|
| 117 |
}
|
|
|
|
| 2 |
|
| 3 |
import "strings"
|
| 4 |
|
| 5 |
+
// 基础模型映射(key 全部小写,不包含标签后缀)
|
| 6 |
var BaseModelMapping = map[string]string{
|
| 7 |
+
"glm-4.5": "0727-360B-API",
|
| 8 |
+
"glm-4.6": "GLM-4-6-API-V1",
|
| 9 |
+
"glm-4.7": "glm-4.7",
|
| 10 |
+
"glm-5": "glm-5",
|
| 11 |
+
"glm-4.5-v": "glm-4.5v",
|
| 12 |
+
"glm-4.6-v": "glm-4.6v",
|
| 13 |
+
"glm-4.5-air": "0727-106B-API",
|
| 14 |
+
"0808-360b-dr": "0808-360B-DR",
|
| 15 |
}
|
| 16 |
|
| 17 |
// Claude 模型名到 GLM 基础模型名的映射
|
| 18 |
var ClaudeModelMapping = map[string]string{
|
| 19 |
+
"claude-opus-4-6": "glm-4.7",
|
| 20 |
+
"claude-opus-4-5-20250514": "glm-4.7",
|
| 21 |
+
"claude-sonnet-4-6": "glm-4.7",
|
| 22 |
+
"claude-sonnet-4-5-20241022": "glm-4.7",
|
| 23 |
+
"claude-haiku-4-5": "glm-4.5-air",
|
| 24 |
+
"claude-haiku-4-5-20251001": "glm-4.5-air",
|
| 25 |
+
"claude-3-5-sonnet-20241022": "glm-4.7",
|
| 26 |
+
"claude-3-5-haiku-20241022": "glm-4.5-air",
|
| 27 |
}
|
| 28 |
|
| 29 |
// ResolveClaudeModel maps a Claude model name to a GLM model name with appropriate tags.
|
|
|
|
| 30 |
func ResolveClaudeModel(model string, thinkingEnabled bool) (resolvedModel string, enableThinking bool) {
|
| 31 |
+
base, ok := ClaudeModelMapping[strings.ToLower(model)]
|
| 32 |
if !ok {
|
| 33 |
+
base = "glm-4.7"
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
enableThinking = thinkingEnabled
|
| 37 |
+
if strings.Contains(strings.ToLower(model), "opus") {
|
|
|
|
| 38 |
enableThinking = true
|
| 39 |
}
|
| 40 |
|
|
|
|
| 42 |
if enableThinking {
|
| 43 |
resolvedModel += "-thinking"
|
| 44 |
}
|
|
|
|
| 45 |
resolvedModel += "-tools"
|
| 46 |
return resolvedModel, enableThinking
|
| 47 |
}
|
| 48 |
|
| 49 |
+
// v1/models 返回的模型列表(全部小写)
|
| 50 |
var ModelList = []string{
|
| 51 |
+
"glm-4.5",
|
| 52 |
+
"glm-4.6",
|
| 53 |
+
"glm-4.7",
|
| 54 |
+
"glm-4.7-thinking",
|
| 55 |
+
"glm-4.7-thinking-search",
|
| 56 |
+
"glm-4.7-tools",
|
| 57 |
+
"glm-4.7-tools-thinking",
|
| 58 |
+
"glm-5",
|
| 59 |
+
"glm-5-thinking",
|
| 60 |
+
"glm-5-thinking-search",
|
| 61 |
+
"glm-5-tools",
|
| 62 |
+
"glm-5-tools-thinking",
|
| 63 |
+
"glm-4.5-v",
|
| 64 |
+
"glm-4.6-v",
|
| 65 |
+
"glm-4.6-v-thinking",
|
| 66 |
+
"glm-4.5-air",
|
| 67 |
}
|
| 68 |
|
| 69 |
// 解析模型名称,提取基础模型名和标签
|
| 70 |
+
// 输入大小写不敏感,输出的 baseModel 为小写
|
| 71 |
func ParseModelName(model string) (baseModel string, enableThinking bool, enableSearch bool, enableTools bool) {
|
| 72 |
enableThinking = false
|
| 73 |
enableSearch = false
|
| 74 |
enableTools = false
|
| 75 |
+
baseModel = strings.ToLower(model)
|
| 76 |
|
|
|
|
| 77 |
for {
|
| 78 |
if strings.HasSuffix(baseModel, "-thinking") {
|
| 79 |
enableThinking = true
|
|
|
|
| 112 |
if target, ok := BaseModelMapping[baseModel]; ok {
|
| 113 |
return target
|
| 114 |
}
|
|
|
|
| 115 |
return "glm-4.7"
|
| 116 |
}
|
internal/model/mapping_test.go
CHANGED
|
@@ -6,17 +6,27 @@ import "testing"
|
|
| 6 |
|
| 7 |
func TestParseModelName_Plain(t *testing.T) {
|
| 8 |
base, thinking, search, tools := ParseModelName("GLM-4.7")
|
| 9 |
-
if base != "
|
| 10 |
-
t.Errorf("base = %q, want %q", base, "
|
| 11 |
}
|
| 12 |
if thinking || search || tools {
|
| 13 |
t.Errorf("flags = (%v, %v, %v), want all false", thinking, search, tools)
|
| 14 |
}
|
| 15 |
}
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
func TestParseModelName_Thinking(t *testing.T) {
|
| 18 |
base, thinking, search, tools := ParseModelName("GLM-4.7-thinking")
|
| 19 |
-
if base != "
|
| 20 |
t.Errorf("base = %q", base)
|
| 21 |
}
|
| 22 |
if !thinking {
|
|
@@ -29,7 +39,7 @@ func TestParseModelName_Thinking(t *testing.T) {
|
|
| 29 |
|
| 30 |
func TestParseModelName_Search(t *testing.T) {
|
| 31 |
base, thinking, search, tools := ParseModelName("GLM-4.7-search")
|
| 32 |
-
if base != "
|
| 33 |
t.Errorf("base = %q", base)
|
| 34 |
}
|
| 35 |
if !search {
|
|
@@ -42,7 +52,7 @@ func TestParseModelName_Search(t *testing.T) {
|
|
| 42 |
|
| 43 |
func TestParseModelName_Tools(t *testing.T) {
|
| 44 |
base, thinking, search, tools := ParseModelName("GLM-4.7-tools")
|
| 45 |
-
if base != "
|
| 46 |
t.Errorf("base = %q", base)
|
| 47 |
}
|
| 48 |
if !tools {
|
|
@@ -55,7 +65,7 @@ func TestParseModelName_Tools(t *testing.T) {
|
|
| 55 |
|
| 56 |
func TestParseModelName_ThinkingSearch(t *testing.T) {
|
| 57 |
base, thinking, search, tools := ParseModelName("GLM-4.7-thinking-search")
|
| 58 |
-
if base != "
|
| 59 |
t.Errorf("base = %q", base)
|
| 60 |
}
|
| 61 |
if !thinking || !search {
|
|
@@ -68,7 +78,7 @@ func TestParseModelName_ThinkingSearch(t *testing.T) {
|
|
| 68 |
|
| 69 |
func TestParseModelName_ToolsThinking(t *testing.T) {
|
| 70 |
base, thinking, search, tools := ParseModelName("GLM-4.7-tools-thinking")
|
| 71 |
-
if base != "
|
| 72 |
t.Errorf("base = %q", base)
|
| 73 |
}
|
| 74 |
if !tools || !thinking {
|
|
@@ -81,7 +91,7 @@ func TestParseModelName_ToolsThinking(t *testing.T) {
|
|
| 81 |
|
| 82 |
func TestParseModelName_ToolsSearch(t *testing.T) {
|
| 83 |
base, thinking, search, tools := ParseModelName("GLM-4.7-tools-search")
|
| 84 |
-
if base != "
|
| 85 |
t.Errorf("base = %q", base)
|
| 86 |
}
|
| 87 |
if !tools || !search {
|
|
@@ -94,7 +104,7 @@ func TestParseModelName_ToolsSearch(t *testing.T) {
|
|
| 94 |
|
| 95 |
func TestParseModelName_AllTags(t *testing.T) {
|
| 96 |
base, thinking, search, tools := ParseModelName("GLM-4.7-tools-thinking-search")
|
| 97 |
-
if base != "
|
| 98 |
t.Errorf("base = %q", base)
|
| 99 |
}
|
| 100 |
if !thinking || !search || !tools {
|
|
@@ -104,7 +114,7 @@ func TestParseModelName_AllTags(t *testing.T) {
|
|
| 104 |
|
| 105 |
func TestParseModelName_ReverseOrder(t *testing.T) {
|
| 106 |
base, thinking, search, tools := ParseModelName("GLM-4.7-search-thinking-tools")
|
| 107 |
-
if base != "
|
| 108 |
t.Errorf("base = %q", base)
|
| 109 |
}
|
| 110 |
if !thinking || !search || !tools {
|
|
@@ -112,15 +122,42 @@ func TestParseModelName_ReverseOrder(t *testing.T) {
|
|
| 112 |
}
|
| 113 |
}
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
// ===== IsToolsModel =====
|
| 116 |
|
| 117 |
func TestIsToolsModel_True(t *testing.T) {
|
| 118 |
tests := []string{
|
| 119 |
-
"
|
| 120 |
"GLM-4.7-tools-thinking",
|
| 121 |
-
"
|
| 122 |
"GLM-4.7-thinking-tools",
|
| 123 |
-
"
|
|
|
|
| 124 |
}
|
| 125 |
for _, m := range tests {
|
| 126 |
if !IsToolsModel(m) {
|
|
@@ -131,9 +168,9 @@ func TestIsToolsModel_True(t *testing.T) {
|
|
| 131 |
|
| 132 |
func TestIsToolsModel_False(t *testing.T) {
|
| 133 |
tests := []string{
|
| 134 |
-
"
|
| 135 |
"GLM-4.7-thinking",
|
| 136 |
-
"
|
| 137 |
"GLM-4.7-thinking-search",
|
| 138 |
}
|
| 139 |
for _, m := range tests {
|
|
@@ -143,32 +180,32 @@ func TestIsToolsModel_False(t *testing.T) {
|
|
| 143 |
}
|
| 144 |
}
|
| 145 |
|
| 146 |
-
// ===== IsThinkingModel / IsSearchModel
|
| 147 |
|
| 148 |
func TestIsThinkingModel_WithTools(t *testing.T) {
|
| 149 |
-
if !IsThinkingModel("
|
| 150 |
-
t.Error("IsThinkingModel should be true for
|
| 151 |
}
|
| 152 |
-
if IsThinkingModel("
|
| 153 |
-
t.Error("IsThinkingModel should be false for
|
| 154 |
}
|
| 155 |
}
|
| 156 |
|
| 157 |
func TestIsSearchModel_WithTools(t *testing.T) {
|
| 158 |
-
if !IsSearchModel("
|
| 159 |
-
t.Error("IsSearchModel should be true for
|
| 160 |
}
|
| 161 |
-
if IsSearchModel("
|
| 162 |
-
t.Error("IsSearchModel should be false for
|
| 163 |
}
|
| 164 |
}
|
| 165 |
|
| 166 |
-
// ===== GetTargetModel
|
| 167 |
|
| 168 |
func TestGetTargetModel_WithTools(t *testing.T) {
|
| 169 |
-
target := GetTargetModel("
|
| 170 |
if target != "glm-4.7" {
|
| 171 |
-
t.Errorf("GetTargetModel(
|
| 172 |
}
|
| 173 |
}
|
| 174 |
|
|
@@ -179,12 +216,14 @@ func TestGetTargetModel_WithToolsThinking(t *testing.T) {
|
|
| 179 |
}
|
| 180 |
}
|
| 181 |
|
| 182 |
-
// ===== ModelList
|
| 183 |
|
| 184 |
func TestModelList_ContainsToolsVariants(t *testing.T) {
|
| 185 |
expected := map[string]bool{
|
| 186 |
-
"
|
| 187 |
-
"
|
|
|
|
|
|
|
| 188 |
}
|
| 189 |
|
| 190 |
for _, m := range ModelList {
|
|
|
|
| 6 |
|
| 7 |
func TestParseModelName_Plain(t *testing.T) {
|
| 8 |
base, thinking, search, tools := ParseModelName("GLM-4.7")
|
| 9 |
+
if base != "glm-4.7" {
|
| 10 |
+
t.Errorf("base = %q, want %q", base, "glm-4.7")
|
| 11 |
}
|
| 12 |
if thinking || search || tools {
|
| 13 |
t.Errorf("flags = (%v, %v, %v), want all false", thinking, search, tools)
|
| 14 |
}
|
| 15 |
}
|
| 16 |
|
| 17 |
+
func TestParseModelName_Lowercase(t *testing.T) {
|
| 18 |
+
base, thinking, _, _ := ParseModelName("glm-4.7-thinking")
|
| 19 |
+
if base != "glm-4.7" {
|
| 20 |
+
t.Errorf("base = %q, want %q", base, "glm-4.7")
|
| 21 |
+
}
|
| 22 |
+
if !thinking {
|
| 23 |
+
t.Error("thinking should be true")
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
func TestParseModelName_Thinking(t *testing.T) {
|
| 28 |
base, thinking, search, tools := ParseModelName("GLM-4.7-thinking")
|
| 29 |
+
if base != "glm-4.7" {
|
| 30 |
t.Errorf("base = %q", base)
|
| 31 |
}
|
| 32 |
if !thinking {
|
|
|
|
| 39 |
|
| 40 |
func TestParseModelName_Search(t *testing.T) {
|
| 41 |
base, thinking, search, tools := ParseModelName("GLM-4.7-search")
|
| 42 |
+
if base != "glm-4.7" {
|
| 43 |
t.Errorf("base = %q", base)
|
| 44 |
}
|
| 45 |
if !search {
|
|
|
|
| 52 |
|
| 53 |
func TestParseModelName_Tools(t *testing.T) {
|
| 54 |
base, thinking, search, tools := ParseModelName("GLM-4.7-tools")
|
| 55 |
+
if base != "glm-4.7" {
|
| 56 |
t.Errorf("base = %q", base)
|
| 57 |
}
|
| 58 |
if !tools {
|
|
|
|
| 65 |
|
| 66 |
func TestParseModelName_ThinkingSearch(t *testing.T) {
|
| 67 |
base, thinking, search, tools := ParseModelName("GLM-4.7-thinking-search")
|
| 68 |
+
if base != "glm-4.7" {
|
| 69 |
t.Errorf("base = %q", base)
|
| 70 |
}
|
| 71 |
if !thinking || !search {
|
|
|
|
| 78 |
|
| 79 |
func TestParseModelName_ToolsThinking(t *testing.T) {
|
| 80 |
base, thinking, search, tools := ParseModelName("GLM-4.7-tools-thinking")
|
| 81 |
+
if base != "glm-4.7" {
|
| 82 |
t.Errorf("base = %q", base)
|
| 83 |
}
|
| 84 |
if !tools || !thinking {
|
|
|
|
| 91 |
|
| 92 |
func TestParseModelName_ToolsSearch(t *testing.T) {
|
| 93 |
base, thinking, search, tools := ParseModelName("GLM-4.7-tools-search")
|
| 94 |
+
if base != "glm-4.7" {
|
| 95 |
t.Errorf("base = %q", base)
|
| 96 |
}
|
| 97 |
if !tools || !search {
|
|
|
|
| 104 |
|
| 105 |
func TestParseModelName_AllTags(t *testing.T) {
|
| 106 |
base, thinking, search, tools := ParseModelName("GLM-4.7-tools-thinking-search")
|
| 107 |
+
if base != "glm-4.7" {
|
| 108 |
t.Errorf("base = %q", base)
|
| 109 |
}
|
| 110 |
if !thinking || !search || !tools {
|
|
|
|
| 114 |
|
| 115 |
func TestParseModelName_ReverseOrder(t *testing.T) {
|
| 116 |
base, thinking, search, tools := ParseModelName("GLM-4.7-search-thinking-tools")
|
| 117 |
+
if base != "glm-4.7" {
|
| 118 |
t.Errorf("base = %q", base)
|
| 119 |
}
|
| 120 |
if !thinking || !search || !tools {
|
|
|
|
| 122 |
}
|
| 123 |
}
|
| 124 |
|
| 125 |
+
// ===== GLM-5 =====
|
| 126 |
+
|
| 127 |
+
func TestParseModelName_GLM5(t *testing.T) {
|
| 128 |
+
base, thinking, _, _ := ParseModelName("glm-5")
|
| 129 |
+
if base != "glm-5" {
|
| 130 |
+
t.Errorf("base = %q, want %q", base, "glm-5")
|
| 131 |
+
}
|
| 132 |
+
if thinking {
|
| 133 |
+
t.Error("thinking should be false")
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
func TestGetTargetModel_GLM5(t *testing.T) {
|
| 138 |
+
target := GetTargetModel("glm-5")
|
| 139 |
+
if target != "glm-5" {
|
| 140 |
+
t.Errorf("GetTargetModel(glm-5) = %q, want %q", target, "glm-5")
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
func TestGetTargetModel_GLM5Thinking(t *testing.T) {
|
| 145 |
+
target := GetTargetModel("glm-5-thinking")
|
| 146 |
+
if target != "glm-5" {
|
| 147 |
+
t.Errorf("GetTargetModel(glm-5-thinking) = %q, want %q", target, "glm-5")
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
// ===== IsToolsModel =====
|
| 152 |
|
| 153 |
func TestIsToolsModel_True(t *testing.T) {
|
| 154 |
tests := []string{
|
| 155 |
+
"glm-4.7-tools",
|
| 156 |
"GLM-4.7-tools-thinking",
|
| 157 |
+
"glm-4.7-tools-search",
|
| 158 |
"GLM-4.7-thinking-tools",
|
| 159 |
+
"glm-4.5-tools",
|
| 160 |
+
"glm-5-tools",
|
| 161 |
}
|
| 162 |
for _, m := range tests {
|
| 163 |
if !IsToolsModel(m) {
|
|
|
|
| 168 |
|
| 169 |
func TestIsToolsModel_False(t *testing.T) {
|
| 170 |
tests := []string{
|
| 171 |
+
"glm-4.7",
|
| 172 |
"GLM-4.7-thinking",
|
| 173 |
+
"glm-4.7-search",
|
| 174 |
"GLM-4.7-thinking-search",
|
| 175 |
}
|
| 176 |
for _, m := range tests {
|
|
|
|
| 180 |
}
|
| 181 |
}
|
| 182 |
|
| 183 |
+
// ===== IsThinkingModel / IsSearchModel =====
|
| 184 |
|
| 185 |
func TestIsThinkingModel_WithTools(t *testing.T) {
|
| 186 |
+
if !IsThinkingModel("glm-4.7-tools-thinking") {
|
| 187 |
+
t.Error("IsThinkingModel should be true for glm-4.7-tools-thinking")
|
| 188 |
}
|
| 189 |
+
if IsThinkingModel("glm-4.7-tools") {
|
| 190 |
+
t.Error("IsThinkingModel should be false for glm-4.7-tools")
|
| 191 |
}
|
| 192 |
}
|
| 193 |
|
| 194 |
func TestIsSearchModel_WithTools(t *testing.T) {
|
| 195 |
+
if !IsSearchModel("glm-4.7-tools-search") {
|
| 196 |
+
t.Error("IsSearchModel should be true for glm-4.7-tools-search")
|
| 197 |
}
|
| 198 |
+
if IsSearchModel("glm-4.7-tools") {
|
| 199 |
+
t.Error("IsSearchModel should be false for glm-4.7-tools")
|
| 200 |
}
|
| 201 |
}
|
| 202 |
|
| 203 |
+
// ===== GetTargetModel =====
|
| 204 |
|
| 205 |
func TestGetTargetModel_WithTools(t *testing.T) {
|
| 206 |
+
target := GetTargetModel("glm-4.7-tools")
|
| 207 |
if target != "glm-4.7" {
|
| 208 |
+
t.Errorf("GetTargetModel(glm-4.7-tools) = %q, want %q", target, "glm-4.7")
|
| 209 |
}
|
| 210 |
}
|
| 211 |
|
|
|
|
| 216 |
}
|
| 217 |
}
|
| 218 |
|
| 219 |
+
// ===== ModelList =====
|
| 220 |
|
| 221 |
func TestModelList_ContainsToolsVariants(t *testing.T) {
|
| 222 |
expected := map[string]bool{
|
| 223 |
+
"glm-4.7-tools": false,
|
| 224 |
+
"glm-4.7-tools-thinking": false,
|
| 225 |
+
"glm-5": false,
|
| 226 |
+
"glm-5-tools": false,
|
| 227 |
}
|
| 228 |
|
| 229 |
for _, m := range ModelList {
|
internal/proxy/pool.go
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package proxy
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"context"
|
| 6 |
+
"fmt"
|
| 7 |
+
"math/rand"
|
| 8 |
+
"net"
|
| 9 |
+
"net/http"
|
| 10 |
+
"os"
|
| 11 |
+
"strings"
|
| 12 |
+
"sync"
|
| 13 |
+
|
| 14 |
+
"golang.org/x/net/proxy"
|
| 15 |
+
|
| 16 |
+
"zai-proxy/internal/logger"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
var (
|
| 20 |
+
proxies []string
|
| 21 |
+
mu sync.RWMutex
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
// LoadProxies 从 proxies.txt 文件加载代理列表
|
| 25 |
+
// 格式: ip:port:username:password 或 ip:port
|
| 26 |
+
func LoadProxies(path string) {
|
| 27 |
+
file, err := os.Open(path)
|
| 28 |
+
if err != nil {
|
| 29 |
+
logger.LogInfo("No proxies.txt found, running without proxy")
|
| 30 |
+
return
|
| 31 |
+
}
|
| 32 |
+
defer file.Close()
|
| 33 |
+
|
| 34 |
+
var loaded []string
|
| 35 |
+
scanner := bufio.NewScanner(file)
|
| 36 |
+
for scanner.Scan() {
|
| 37 |
+
line := strings.TrimSpace(scanner.Text())
|
| 38 |
+
if line == "" || strings.HasPrefix(line, "#") {
|
| 39 |
+
continue
|
| 40 |
+
}
|
| 41 |
+
loaded = append(loaded, line)
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
mu.Lock()
|
| 45 |
+
proxies = loaded
|
| 46 |
+
mu.Unlock()
|
| 47 |
+
|
| 48 |
+
logger.LogInfo("Loaded %d proxies from %s", len(loaded), path)
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
type proxyInfo struct {
|
| 52 |
+
addr string
|
| 53 |
+
username string
|
| 54 |
+
password string
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// getRandomProxyInfo 随机返回一个代理信息
|
| 58 |
+
func getRandomProxyInfo() *proxyInfo {
|
| 59 |
+
mu.RLock()
|
| 60 |
+
defer mu.RUnlock()
|
| 61 |
+
|
| 62 |
+
if len(proxies) == 0 {
|
| 63 |
+
return nil
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
line := proxies[rand.Intn(len(proxies))]
|
| 67 |
+
parts := strings.Split(line, ":")
|
| 68 |
+
|
| 69 |
+
switch len(parts) {
|
| 70 |
+
case 2:
|
| 71 |
+
return &proxyInfo{addr: fmt.Sprintf("%s:%s", parts[0], parts[1])}
|
| 72 |
+
case 4:
|
| 73 |
+
return &proxyInfo{
|
| 74 |
+
addr: fmt.Sprintf("%s:%s", parts[0], parts[1]),
|
| 75 |
+
username: parts[2],
|
| 76 |
+
password: parts[3],
|
| 77 |
+
}
|
| 78 |
+
default:
|
| 79 |
+
logger.LogWarn("Invalid proxy format: %s", line)
|
| 80 |
+
return nil
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// GetHTTPClient 返回一个配置了随机 SOCKS5 代理的 http.Client
|
| 85 |
+
func GetHTTPClient() *http.Client {
|
| 86 |
+
info := getRandomProxyInfo()
|
| 87 |
+
if info == nil {
|
| 88 |
+
return &http.Client{}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
logger.LogDebug("Using SOCKS5 proxy: %s", info.addr)
|
| 92 |
+
|
| 93 |
+
var auth *proxy.Auth
|
| 94 |
+
if info.username != "" {
|
| 95 |
+
auth = &proxy.Auth{
|
| 96 |
+
User: info.username,
|
| 97 |
+
Password: info.password,
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
dialer, err := proxy.SOCKS5("tcp", info.addr, auth, proxy.Direct)
|
| 102 |
+
if err != nil {
|
| 103 |
+
logger.LogWarn("Failed to create SOCKS5 dialer: %v", err)
|
| 104 |
+
return &http.Client{}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
contextDialer, ok := dialer.(proxy.ContextDialer)
|
| 108 |
+
if !ok {
|
| 109 |
+
logger.LogWarn("SOCKS5 dialer does not support ContextDialer")
|
| 110 |
+
return &http.Client{}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
return &http.Client{
|
| 114 |
+
Transport: &http.Transport{
|
| 115 |
+
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
| 116 |
+
return contextDialer.DialContext(ctx, network, addr)
|
| 117 |
+
},
|
| 118 |
+
},
|
| 119 |
+
}
|
| 120 |
+
}
|
internal/upstream/client.go
CHANGED
|
@@ -14,6 +14,7 @@ import (
|
|
| 14 |
"zai-proxy/internal/auth"
|
| 15 |
"zai-proxy/internal/logger"
|
| 16 |
"zai-proxy/internal/model"
|
|
|
|
| 17 |
builtintools "zai-proxy/internal/tools"
|
| 18 |
"zai-proxy/internal/version"
|
| 19 |
)
|
|
@@ -243,7 +244,7 @@ func MakeUpstreamRequest(token string, messages []model.Message, modelName strin
|
|
| 243 |
req.Header.Set("Referer", fmt.Sprintf("https://chat.z.ai/c/%s", uuid.New().String()))
|
| 244 |
req.Header.Set("User-Agent", uarand.GetRandom())
|
| 245 |
|
| 246 |
-
client :=
|
| 247 |
resp, err := client.Do(req)
|
| 248 |
if err != nil {
|
| 249 |
return nil, "", err
|
|
|
|
| 14 |
"zai-proxy/internal/auth"
|
| 15 |
"zai-proxy/internal/logger"
|
| 16 |
"zai-proxy/internal/model"
|
| 17 |
+
"zai-proxy/internal/proxy"
|
| 18 |
builtintools "zai-proxy/internal/tools"
|
| 19 |
"zai-proxy/internal/version"
|
| 20 |
)
|
|
|
|
| 244 |
req.Header.Set("Referer", fmt.Sprintf("https://chat.z.ai/c/%s", uuid.New().String()))
|
| 245 |
req.Header.Set("User-Agent", uarand.GetRandom())
|
| 246 |
|
| 247 |
+
client := proxy.GetHTTPClient()
|
| 248 |
resp, err := client.Do(req)
|
| 249 |
if err != nil {
|
| 250 |
return nil, "", err
|
internal/upstream/upload.go
CHANGED
|
@@ -14,6 +14,7 @@ import (
|
|
| 14 |
"github.com/google/uuid"
|
| 15 |
|
| 16 |
"zai-proxy/internal/logger"
|
|
|
|
| 17 |
)
|
| 18 |
|
| 19 |
// FileUploadResponse z.ai 文件上传响应
|
|
@@ -87,7 +88,7 @@ func UploadImageFromURL(token string, imageURL string) (*UpstreamFile, error) {
|
|
| 87 |
filename = uuid.New().String()[:12] + ext
|
| 88 |
} else {
|
| 89 |
// 从 URL 下载图片
|
| 90 |
-
resp, err :=
|
| 91 |
if err != nil {
|
| 92 |
return nil, fmt.Errorf("failed to download image: %v", err)
|
| 93 |
}
|
|
@@ -144,7 +145,7 @@ func UploadImageFromURL(token string, imageURL string) (*UpstreamFile, error) {
|
|
| 144 |
req.Header.Set("Origin", "https://chat.z.ai")
|
| 145 |
req.Header.Set("Referer", "https://chat.z.ai/")
|
| 146 |
|
| 147 |
-
client :=
|
| 148 |
resp, err := client.Do(req)
|
| 149 |
if err != nil {
|
| 150 |
return nil, fmt.Errorf("failed to upload image: %v", err)
|
|
|
|
| 14 |
"github.com/google/uuid"
|
| 15 |
|
| 16 |
"zai-proxy/internal/logger"
|
| 17 |
+
"zai-proxy/internal/proxy"
|
| 18 |
)
|
| 19 |
|
| 20 |
// FileUploadResponse z.ai 文件上传响应
|
|
|
|
| 88 |
filename = uuid.New().String()[:12] + ext
|
| 89 |
} else {
|
| 90 |
// 从 URL 下载图片
|
| 91 |
+
resp, err := proxy.GetHTTPClient().Get(imageURL)
|
| 92 |
if err != nil {
|
| 93 |
return nil, fmt.Errorf("failed to download image: %v", err)
|
| 94 |
}
|
|
|
|
| 145 |
req.Header.Set("Origin", "https://chat.z.ai")
|
| 146 |
req.Header.Set("Referer", "https://chat.z.ai/")
|
| 147 |
|
| 148 |
+
client := proxy.GetHTTPClient()
|
| 149 |
resp, err := client.Do(req)
|
| 150 |
if err != nil {
|
| 151 |
return nil, fmt.Errorf("failed to upload image: %v", err)
|
internal/version/version.go
CHANGED
|
@@ -2,12 +2,12 @@ package version
|
|
| 2 |
|
| 3 |
import (
|
| 4 |
"io"
|
| 5 |
-
"net/http"
|
| 6 |
"regexp"
|
| 7 |
"sync"
|
| 8 |
"time"
|
| 9 |
|
| 10 |
"zai-proxy/internal/logger"
|
|
|
|
| 11 |
)
|
| 12 |
|
| 13 |
var (
|
|
@@ -22,7 +22,7 @@ func GetFeVersion() string {
|
|
| 22 |
}
|
| 23 |
|
| 24 |
func fetchFeVersion() {
|
| 25 |
-
resp, err :=
|
| 26 |
if err != nil {
|
| 27 |
logger.LogError("Failed to fetch fe version: %v", err)
|
| 28 |
return
|
|
@@ -46,10 +46,9 @@ func fetchFeVersion() {
|
|
| 46 |
}
|
| 47 |
|
| 48 |
func StartVersionUpdater() {
|
| 49 |
-
fetchFeVersion()
|
| 50 |
-
|
| 51 |
-
ticker := time.NewTicker(1 * time.Hour)
|
| 52 |
go func() {
|
|
|
|
|
|
|
| 53 |
for range ticker.C {
|
| 54 |
fetchFeVersion()
|
| 55 |
}
|
|
|
|
| 2 |
|
| 3 |
import (
|
| 4 |
"io"
|
|
|
|
| 5 |
"regexp"
|
| 6 |
"sync"
|
| 7 |
"time"
|
| 8 |
|
| 9 |
"zai-proxy/internal/logger"
|
| 10 |
+
"zai-proxy/internal/proxy"
|
| 11 |
)
|
| 12 |
|
| 13 |
var (
|
|
|
|
| 22 |
}
|
| 23 |
|
| 24 |
func fetchFeVersion() {
|
| 25 |
+
resp, err := proxy.GetHTTPClient().Get("https://chat.z.ai/")
|
| 26 |
if err != nil {
|
| 27 |
logger.LogError("Failed to fetch fe version: %v", err)
|
| 28 |
return
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
func StartVersionUpdater() {
|
|
|
|
|
|
|
|
|
|
| 49 |
go func() {
|
| 50 |
+
fetchFeVersion()
|
| 51 |
+
ticker := time.NewTicker(1 * time.Hour)
|
| 52 |
for range ticker.C {
|
| 53 |
fetchFeVersion()
|
| 54 |
}
|
main.go
CHANGED
|
@@ -6,12 +6,14 @@ import (
|
|
| 6 |
"zai-proxy/internal/config"
|
| 7 |
"zai-proxy/internal/handler"
|
| 8 |
"zai-proxy/internal/logger"
|
|
|
|
| 9 |
"zai-proxy/internal/version"
|
| 10 |
)
|
| 11 |
|
| 12 |
func main() {
|
| 13 |
config.LoadConfig()
|
| 14 |
logger.InitLogger()
|
|
|
|
| 15 |
version.StartVersionUpdater()
|
| 16 |
|
| 17 |
http.HandleFunc("/v1/models", handler.HandleModels)
|
|
|
|
| 6 |
"zai-proxy/internal/config"
|
| 7 |
"zai-proxy/internal/handler"
|
| 8 |
"zai-proxy/internal/logger"
|
| 9 |
+
"zai-proxy/internal/proxy"
|
| 10 |
"zai-proxy/internal/version"
|
| 11 |
)
|
| 12 |
|
| 13 |
func main() {
|
| 14 |
config.LoadConfig()
|
| 15 |
logger.InitLogger()
|
| 16 |
+
proxy.LoadProxies("proxies.txt")
|
| 17 |
version.StartVersionUpdater()
|
| 18 |
|
| 19 |
http.HandleFunc("/v1/models", handler.HandleModels)
|