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 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,linux/arm64
 
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` 和 `linux/arm64` 双平台。
 
 
 
 
 
 
 
 
 
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": "GLM-4.7", "messages": [{"role": "user", "content": "hello"}]}'
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": "GLM-4.7",
138
  "messages": [{"role": "user", "content": "hello"}],
139
  "stream": true
140
  }'
@@ -171,12 +191,13 @@ curl http://localhost:8000/v1/messages \
171
 
172
  | 模型名称 | 上游模型 | 说明 |
173
  |----------|----------|------|
174
- | `GLM-4.5` | 0727-360B-API | |
175
- | `GLM-4.6` | GLM-4-6-API-V1 | |
176
- | `GLM-4.7` | glm-4.7 | 最新 |
177
- | `GLM-4.5-V` | glm-4.5v | 视觉 |
178
- | `GLM-4.6-V` | glm-4.6v | 视觉(最新) |
179
- | `GLM-4.5-Air` | 0727-106B-API | 轻量 |
 
180
 
181
  ### Claude 模型名映射
182
 
@@ -184,16 +205,16 @@ curl http://localhost:8000/v1/messages \
184
 
185
  | Claude 模型 | 映射到 | 备注 |
186
  |-------------|--------|------|
187
- | `claude-opus-4-6` | GLM-4.7 | 自动启用 thinking |
188
- | `claude-opus-4-5-20250514` | GLM-4.7 | 自动启用 thinking |
189
- | `claude-sonnet-4-6` | GLM-4.7 | |
190
- | `claude-sonnet-4-5-20241022` | GLM-4.7 | |
191
- | `claude-haiku-4-5` | GLM-4.5-Air | |
192
- | `claude-haiku-4-5-20251001` | GLM-4.5-Air | |
193
- | `claude-3-5-sonnet-20241022` | GLM-4.7 | |
194
- | `claude-3-5-haiku-20241022` | GLM-4.5-Air | |
195
 
196
- > Opus 系列模型始终自动启用 thinking 模式。未识别的模型名会回退到 GLM-4.7。
197
 
198
  ### 模型标签
199
 
@@ -206,11 +227,13 @@ curl http://localhost:8000/v1/messages \
206
  | `-tools` | 自动注入内置工具定义,模型可进行函数调用 |
207
 
208
  组合示例:
209
- - `GLM-4.7-thinking`
210
- - `GLM-4.7-search`
211
- - `GLM-4.7-thinking-search`
212
- - `GLM-4.7-tools`
213
- - `GLM-4.7-tools-thinking`
 
 
214
 
215
  ## 使用示例
216
 
@@ -218,7 +241,7 @@ curl http://localhost:8000/v1/messages \
218
 
219
  ```json
220
  {
221
- "model": "GLM-4.6-V",
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": "GLM-4.7-tools",
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": "GLM-4.7-tools",
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": "GLM-4.7",
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.21
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 := http.Get("https://chat.z.ai/api/v1/auths/")
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
- "GLM-4.5": "0727-360B-API",
8
- "GLM-4.6": "GLM-4-6-API-V1",
9
- "GLM-4.7": "glm-4.7",
10
- "GLM-4.5-V": "glm-4.5v",
11
- "GLM-4.6-V": "glm-4.6v",
12
- "GLM-4.5-Air": "0727-106B-API",
13
- "0808-360B-DR": "0808-360B-DR",
 
14
  }
15
 
16
  // Claude 模型名到 GLM 基础模型名的映射
17
  var ClaudeModelMapping = map[string]string{
18
- "claude-opus-4-6": "GLM-4.7",
19
- "claude-opus-4-5-20250514": "GLM-4.7",
20
- "claude-sonnet-4-6": "GLM-4.7",
21
- "claude-sonnet-4-5-20241022": "GLM-4.7",
22
- "claude-haiku-4-5": "GLM-4.5-Air",
23
- "claude-haiku-4-5-20251001": "GLM-4.5-Air",
24
- "claude-3-5-sonnet-20241022": "GLM-4.7",
25
- "claude-3-5-haiku-20241022": "GLM-4.5-Air",
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
- // Unknown model: fallback to GLM-4.7
34
- base = "GLM-4.7"
35
  }
36
 
37
  enableThinking = thinkingEnabled
38
- // Opus models always enable thinking
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
- "GLM-4.5",
55
- "GLM-4.6",
56
- "GLM-4.7",
57
- "GLM-4.7-thinking",
58
- "GLM-4.7-thinking-search",
59
- "GLM-4.7-tools",
60
- "GLM-4.7-tools-thinking",
61
- "GLM-4.5-V",
62
- "GLM-4.6-V",
63
- "GLM-4.6-V-thinking",
64
- "GLM-4.5-Air",
65
- // "0808-360B-DR",
 
 
 
 
66
  }
67
 
68
  // 解析模型名称,提取基础模型名和标签
69
- // 支持 -thinking、-search 和 -tools 标签的任意排列组合
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 != "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_Thinking(t *testing.T) {
18
  base, thinking, search, tools := ParseModelName("GLM-4.7-thinking")
19
- if base != "GLM-4.7" {
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 != "GLM-4.7" {
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 != "GLM-4.7" {
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 != "GLM-4.7" {
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 != "GLM-4.7" {
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 != "GLM-4.7" {
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 != "GLM-4.7" {
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 != "GLM-4.7" {
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
- "GLM-4.7-tools",
120
  "GLM-4.7-tools-thinking",
121
- "GLM-4.7-tools-search",
122
  "GLM-4.7-thinking-tools",
123
- "GLM-4.5-tools",
 
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
- "GLM-4.7",
135
  "GLM-4.7-thinking",
136
- "GLM-4.7-search",
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 不受 -tools 影响 =====
147
 
148
  func TestIsThinkingModel_WithTools(t *testing.T) {
149
- if !IsThinkingModel("GLM-4.7-tools-thinking") {
150
- t.Error("IsThinkingModel should be true for GLM-4.7-tools-thinking")
151
  }
152
- if IsThinkingModel("GLM-4.7-tools") {
153
- t.Error("IsThinkingModel should be false for GLM-4.7-tools")
154
  }
155
  }
156
 
157
  func TestIsSearchModel_WithTools(t *testing.T) {
158
- if !IsSearchModel("GLM-4.7-tools-search") {
159
- t.Error("IsSearchModel should be true for GLM-4.7-tools-search")
160
  }
161
- if IsSearchModel("GLM-4.7-tools") {
162
- t.Error("IsSearchModel should be false for GLM-4.7-tools")
163
  }
164
  }
165
 
166
- // ===== GetTargetModel with -tools =====
167
 
168
  func TestGetTargetModel_WithTools(t *testing.T) {
169
- target := GetTargetModel("GLM-4.7-tools")
170
  if target != "glm-4.7" {
171
- t.Errorf("GetTargetModel(GLM-4.7-tools) = %q, want %q", target, "glm-4.7")
172
  }
173
  }
174
 
@@ -179,12 +216,14 @@ func TestGetTargetModel_WithToolsThinking(t *testing.T) {
179
  }
180
  }
181
 
182
- // ===== ModelList 包含 -tools 变体 =====
183
 
184
  func TestModelList_ContainsToolsVariants(t *testing.T) {
185
  expected := map[string]bool{
186
- "GLM-4.7-tools": false,
187
- "GLM-4.7-tools-thinking": false,
 
 
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 := &http.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 := http.Get(imageURL)
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 := &http.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 := http.Get("https://chat.z.ai/")
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)