ss22345 commited on
Commit
48d903a
·
1 Parent(s): 04438fd

refactor: split internal package into domain-based sub-packages

Browse files

Split the flat internal/ package into auth, config, filter, handler,
logger, model, upstream, and version sub-packages by responsibility.

internal/{anonymous.go → auth/anonymous.go} RENAMED
@@ -1,4 +1,4 @@
1
- package internal
2
 
3
  import (
4
  "encoding/json"
 
1
+ package auth
2
 
3
  import (
4
  "encoding/json"
internal/{jwt.go → auth/jwt.go} RENAMED
@@ -1,4 +1,4 @@
1
- package internal
2
 
3
  import (
4
  "encoding/base64"
 
1
+ package auth
2
 
3
  import (
4
  "encoding/base64"
internal/{signature.go → auth/signature.go} RENAMED
@@ -1,4 +1,4 @@
1
- package internal
2
 
3
  import (
4
  "crypto/hmac"
@@ -20,14 +20,8 @@ func GenerateSignature(userID, requestID, userContent string, timestamp int64) s
20
  signData := fmt.Sprintf("%s|%s|%d", requestInfo, contentBase64, timestamp)
21
 
22
  period := timestamp / (5 * 60 * 1000)
23
- // 两次加密均返回 hex 字符串
24
  firstHmac := hmacSha256Hex([]byte("key-@@@@)))()((9))-xxxx&&&%%%%%"), fmt.Sprintf("%d", period))
25
  signature := hmacSha256Hex([]byte(firstHmac), signData)
26
 
27
- // LogDebug("[Signature] requestInfo=%s", requestInfo)
28
- // LogDebug("[Signature] userContent=%s", userContent)
29
- // LogDebug("[Signature] timestamp=%d", timestamp)
30
- // LogDebug("[Signature] signature=%s", signature)
31
-
32
  return signature
33
  }
 
1
+ package auth
2
 
3
  import (
4
  "crypto/hmac"
 
20
  signData := fmt.Sprintf("%s|%s|%d", requestInfo, contentBase64, timestamp)
21
 
22
  period := timestamp / (5 * 60 * 1000)
 
23
  firstHmac := hmacSha256Hex([]byte("key-@@@@)))()((9))-xxxx&&&%%%%%"), fmt.Sprintf("%d", period))
24
  signature := hmacSha256Hex([]byte(firstHmac), signData)
25
 
 
 
 
 
 
26
  return signature
27
  }
internal/{config.go → config/config.go} RENAMED
@@ -1,4 +1,4 @@
1
- package internal
2
 
3
  import (
4
  "os"
 
1
+ package config
2
 
3
  import (
4
  "os"
internal/{models.go → filter/search.go} RENAMED
@@ -1,231 +1,29 @@
1
- package internal
2
 
3
  import (
4
  "encoding/json"
5
  "fmt"
6
  "regexp"
7
  "strings"
8
- )
9
-
10
- // 基础模型映射(不包含标签后缀)
11
- var BaseModelMapping = map[string]string{
12
- "GLM-4.5": "0727-360B-API",
13
- "GLM-4.6": "GLM-4-6-API-V1",
14
- "GLM-4.7": "glm-4.7",
15
- "GLM-4.5-V": "glm-4.5v",
16
- "GLM-4.6-V": "glm-4.6v",
17
- "GLM-4.5-Air": "0727-106B-API",
18
- "0808-360B-DR": "0808-360B-DR",
19
- }
20
-
21
- // v1/models 返回的模型列表(不包含所有标签组合)
22
- var ModelList = []string{
23
- "GLM-4.5",
24
- "GLM-4.6",
25
- "GLM-4.7",
26
- "GLM-4.7-thinking",
27
- "GLM-4.7-thinking-search",
28
- "GLM-4.5-V",
29
- "GLM-4.6-V",
30
- "GLM-4.6-V-thinking",
31
- "GLM-4.5-Air",
32
- // "0808-360B-DR",
33
- }
34
-
35
- // 解析模型名称,提取基础模型名和标签
36
- // 支持 -thinking 和 -search 标签的任意排列组合
37
- func ParseModelName(model string) (baseModel string, enableThinking bool, enableSearch bool) {
38
- enableThinking = false
39
- enableSearch = false
40
- baseModel = model
41
-
42
- // 检查并移除 -thinking 和 -search 标签(任意顺序)
43
- for {
44
- if strings.HasSuffix(baseModel, "-thinking") {
45
- enableThinking = true
46
- baseModel = strings.TrimSuffix(baseModel, "-thinking")
47
- } else if strings.HasSuffix(baseModel, "-search") {
48
- enableSearch = true
49
- baseModel = strings.TrimSuffix(baseModel, "-search")
50
- } else {
51
- break
52
- }
53
- }
54
-
55
- return baseModel, enableThinking, enableSearch
56
- }
57
-
58
- func IsThinkingModel(model string) bool {
59
- _, enableThinking, _ := ParseModelName(model)
60
- return enableThinking
61
- }
62
-
63
- func IsSearchModel(model string) bool {
64
- _, _, enableSearch := ParseModelName(model)
65
- return enableSearch
66
- }
67
-
68
- func GetTargetModel(model string) string {
69
- baseModel, _, _ := ParseModelName(model)
70
- if target, ok := BaseModelMapping[baseModel]; ok {
71
- return target
72
- }
73
- return model
74
- }
75
-
76
- // OpenAI 格式的消息内容项
77
- type ContentPart struct {
78
- Type string `json:"type"`
79
- Text string `json:"text,omitempty"`
80
- ImageURL *ImageURL `json:"image_url,omitempty"`
81
- }
82
-
83
- type ImageURL struct {
84
- URL string `json:"url"`
85
- }
86
-
87
- // Message 支持纯文本和多模态内容
88
- type Message struct {
89
- Role string `json:"role"`
90
- Content interface{} `json:"content"` // string 或 []ContentPart
91
- }
92
-
93
- // 解析消息内容,返回文本和图片URL列表
94
- func (m *Message) ParseContent() (text string, imageURLs []string) {
95
- switch content := m.Content.(type) {
96
- case string:
97
- return content, nil
98
- case []interface{}:
99
- for _, item := range content {
100
- if part, ok := item.(map[string]interface{}); ok {
101
- partType, _ := part["type"].(string)
102
- if partType == "text" {
103
- if t, ok := part["text"].(string); ok {
104
- text += t
105
- }
106
- } else if partType == "image_url" {
107
- if imgURL, ok := part["image_url"].(map[string]interface{}); ok {
108
- if url, ok := imgURL["url"].(string); ok {
109
- imageURLs = append(imageURLs, url)
110
- }
111
- }
112
- }
113
- }
114
- }
115
- }
116
- return text, imageURLs
117
- }
118
-
119
- // 转换为上游消息格式,支持多模态
120
- func (m *Message) ToUpstreamMessage(urlToFileID map[string]string) map[string]interface{} {
121
- text, imageURLs := m.ParseContent()
122
 
123
- // 无图片,返回纯文本
124
- if len(imageURLs) == 0 {
125
- return map[string]interface{}{
126
- "role": m.Role,
127
- "content": text,
128
- }
129
- }
130
-
131
- // 有图片,构建多模态内容
132
- var content []interface{}
133
- if text != "" {
134
- content = append(content, map[string]interface{}{
135
- "type": "text",
136
- "text": text,
137
- })
138
- }
139
- for _, imgURL := range imageURLs {
140
- if fileID, ok := urlToFileID[imgURL]; ok {
141
- content = append(content, map[string]interface{}{
142
- "type": "image_url",
143
- "image_url": map[string]interface{}{
144
- "url": fileID,
145
- },
146
- })
147
- }
148
- }
149
-
150
- return map[string]interface{}{
151
- "role": m.Role,
152
- "content": content,
153
- }
154
- }
155
-
156
- type ChatRequest struct {
157
- Model string `json:"model"`
158
- Messages []Message `json:"messages"`
159
- Stream bool `json:"stream"`
160
- }
161
-
162
- type ChatCompletionChunk struct {
163
- ID string `json:"id"`
164
- Object string `json:"object"`
165
- Created int64 `json:"created"`
166
- Model string `json:"model"`
167
- Choices []Choice `json:"choices"`
168
- }
169
-
170
- type Choice struct {
171
- Index int `json:"index"`
172
- Delta Delta `json:"delta,omitempty"`
173
- Message *MessageResp `json:"message,omitempty"`
174
- FinishReason *string `json:"finish_reason"`
175
- }
176
-
177
- type Delta struct {
178
- Content string `json:"content,omitempty"`
179
- ReasoningContent string `json:"reasoning_content,omitempty"`
180
- }
181
-
182
- type MessageResp struct {
183
- Role string `json:"role"`
184
- Content string `json:"content"`
185
- ReasoningContent string `json:"reasoning_content,omitempty"`
186
- }
187
-
188
- type ChatCompletionResponse struct {
189
- ID string `json:"id"`
190
- Object string `json:"object"`
191
- Created int64 `json:"created"`
192
- Model string `json:"model"`
193
- Choices []Choice `json:"choices"`
194
- }
195
-
196
- type ModelsResponse struct {
197
- Object string `json:"object"`
198
- Data []ModelInfo `json:"data"`
199
- }
200
-
201
- type ModelInfo struct {
202
- ID string `json:"id"`
203
- Object string `json:"object"`
204
- OwnedBy string `json:"owned_by"`
205
- }
206
 
207
  var searchRefPattern = regexp.MustCompile(`【turn\d+search(\d+)】`)
208
  var searchRefPrefixPattern = regexp.MustCompile(`【(t(u(r(n(\d+(s(e(a(r(c(h(\d+)?)?)?)?)?)?)?)?)?)?)?)?$`)
209
 
210
- type SearchResult struct {
211
- Title string `json:"title"`
212
- URL string `json:"url"`
213
- Index int `json:"index"`
214
- RefID string `json:"ref_id"`
215
- }
216
-
217
  type SearchRefFilter struct {
218
  buffer string
219
- searchResults map[string]SearchResult
220
  }
221
 
222
  func NewSearchRefFilter() *SearchRefFilter {
223
  return &SearchRefFilter{
224
- searchResults: make(map[string]SearchResult),
225
  }
226
  }
227
 
228
- func (f *SearchRefFilter) AddSearchResults(results []SearchResult) {
229
  for _, r := range results {
230
  f.searchResults[r.RefID] = r
231
  }
@@ -296,7 +94,7 @@ func (f *SearchRefFilter) GetSearchResultsMarkdown() string {
296
  return ""
297
  }
298
 
299
- var results []SearchResult
300
  for _, r := range f.searchResults {
301
  results = append(results, r)
302
  }
@@ -321,7 +119,7 @@ func IsSearchResultContent(editContent string) bool {
321
  return strings.Contains(editContent, `"search_result"`)
322
  }
323
 
324
- func ParseSearchResults(editContent string) []SearchResult {
325
  searchResultKey := `"search_result":`
326
  idx := strings.Index(editContent, searchResultKey)
327
  if idx == -1 {
@@ -367,9 +165,9 @@ func ParseSearchResults(editContent string) []SearchResult {
367
  return nil
368
  }
369
 
370
- var results []SearchResult
371
  for _, r := range rawResults {
372
- results = append(results, SearchResult{
373
  Title: r.Title,
374
  URL: r.URL,
375
  Index: r.Index,
@@ -384,17 +182,10 @@ func IsSearchToolCall(editContent string, phase string) bool {
384
  if phase != "tool_call" {
385
  return false
386
  }
387
- // tool_call 阶段包含 mcp 相关内容的都跳过
388
  return strings.Contains(editContent, `"mcp"`) || strings.Contains(editContent, `mcp-server`)
389
  }
390
 
391
- type ImageSearchResult struct {
392
- Title string `json:"title"`
393
- Link string `json:"link"`
394
- Thumbnail string `json:"thumbnail"`
395
- }
396
-
397
- func ParseImageSearchResults(editContent string) []ImageSearchResult {
398
  resultKey := `"result":`
399
  idx := strings.Index(editContent, resultKey)
400
  if idx == -1 {
@@ -457,7 +248,7 @@ func ParseImageSearchResults(editContent string) []ImageSearchResult {
457
  return nil
458
  }
459
 
460
- var results []ImageSearchResult
461
  for _, item := range rawResults {
462
  if itemType, ok := item["type"].(string); ok && itemType == "text" {
463
  if text, ok := item["text"].(string); ok {
@@ -472,8 +263,8 @@ func ParseImageSearchResults(editContent string) []ImageSearchResult {
472
  return results
473
  }
474
 
475
- func parseImageSearchText(text string) ImageSearchResult {
476
- result := ImageSearchResult{}
477
 
478
  if titleIdx := strings.Index(text, "Title: "); titleIdx != -1 {
479
  titleStart := titleIdx + len("Title: ")
@@ -501,7 +292,7 @@ func parseImageSearchText(text string) ImageSearchResult {
501
  return result
502
  }
503
 
504
- func FormatImageSearchResults(results []ImageSearchResult) string {
505
  if len(results) == 0 {
506
  return ""
507
  }
 
1
+ package filter
2
 
3
  import (
4
  "encoding/json"
5
  "fmt"
6
  "regexp"
7
  "strings"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
+ "zai-proxy/internal/model"
10
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  var searchRefPattern = regexp.MustCompile(`【turn\d+search(\d+)】`)
13
  var searchRefPrefixPattern = regexp.MustCompile(`【(t(u(r(n(\d+(s(e(a(r(c(h(\d+)?)?)?)?)?)?)?)?)?)?)?)?$`)
14
 
 
 
 
 
 
 
 
15
  type SearchRefFilter struct {
16
  buffer string
17
+ searchResults map[string]model.SearchResult
18
  }
19
 
20
  func NewSearchRefFilter() *SearchRefFilter {
21
  return &SearchRefFilter{
22
+ searchResults: make(map[string]model.SearchResult),
23
  }
24
  }
25
 
26
+ func (f *SearchRefFilter) AddSearchResults(results []model.SearchResult) {
27
  for _, r := range results {
28
  f.searchResults[r.RefID] = r
29
  }
 
94
  return ""
95
  }
96
 
97
+ var results []model.SearchResult
98
  for _, r := range f.searchResults {
99
  results = append(results, r)
100
  }
 
119
  return strings.Contains(editContent, `"search_result"`)
120
  }
121
 
122
+ func ParseSearchResults(editContent string) []model.SearchResult {
123
  searchResultKey := `"search_result":`
124
  idx := strings.Index(editContent, searchResultKey)
125
  if idx == -1 {
 
165
  return nil
166
  }
167
 
168
+ var results []model.SearchResult
169
  for _, r := range rawResults {
170
+ results = append(results, model.SearchResult{
171
  Title: r.Title,
172
  URL: r.URL,
173
  Index: r.Index,
 
182
  if phase != "tool_call" {
183
  return false
184
  }
 
185
  return strings.Contains(editContent, `"mcp"`) || strings.Contains(editContent, `mcp-server`)
186
  }
187
 
188
+ func ParseImageSearchResults(editContent string) []model.ImageSearchResult {
 
 
 
 
 
 
189
  resultKey := `"result":`
190
  idx := strings.Index(editContent, resultKey)
191
  if idx == -1 {
 
248
  return nil
249
  }
250
 
251
+ var results []model.ImageSearchResult
252
  for _, item := range rawResults {
253
  if itemType, ok := item["type"].(string); ok && itemType == "text" {
254
  if text, ok := item["text"].(string); ok {
 
263
  return results
264
  }
265
 
266
+ func parseImageSearchText(text string) model.ImageSearchResult {
267
+ result := model.ImageSearchResult{}
268
 
269
  if titleIdx := strings.Index(text, "Title: "); titleIdx != -1 {
270
  titleStart := titleIdx + len("Title: ")
 
292
  return result
293
  }
294
 
295
+ func FormatImageSearchResults(results []model.ImageSearchResult) string {
296
  if len(results) == 0 {
297
  return ""
298
  }
internal/filter/thinking.go ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package filter
2
+
3
+ import "strings"
4
+
5
+ type ThinkingFilter struct {
6
+ HasSeenFirstThinking bool
7
+ Buffer string
8
+ LastOutputChunk string
9
+ LastPhase string
10
+ ThinkingRoundCount int
11
+ }
12
+
13
+ func (f *ThinkingFilter) ProcessThinking(deltaContent string) string {
14
+ if !f.HasSeenFirstThinking {
15
+ f.HasSeenFirstThinking = true
16
+ if idx := strings.Index(deltaContent, "> "); idx != -1 {
17
+ deltaContent = deltaContent[idx+2:]
18
+ } else {
19
+ return ""
20
+ }
21
+ }
22
+
23
+ content := f.Buffer + deltaContent
24
+ f.Buffer = ""
25
+
26
+ content = strings.ReplaceAll(content, "\n> ", "\n")
27
+
28
+ if strings.HasSuffix(content, "\n>") {
29
+ f.Buffer = "\n>"
30
+ return content[:len(content)-2]
31
+ }
32
+ if strings.HasSuffix(content, "\n") {
33
+ f.Buffer = "\n"
34
+ return content[:len(content)-1]
35
+ }
36
+
37
+ return content
38
+ }
39
+
40
+ func (f *ThinkingFilter) Flush() string {
41
+ result := f.Buffer
42
+ f.Buffer = ""
43
+ return result
44
+ }
45
+
46
+ func (f *ThinkingFilter) ExtractCompleteThinking(editContent string) string {
47
+ startIdx := strings.Index(editContent, "> ")
48
+ if startIdx == -1 {
49
+ return ""
50
+ }
51
+ startIdx += 2
52
+
53
+ endIdx := strings.Index(editContent, "\n</details>")
54
+ if endIdx == -1 {
55
+ return ""
56
+ }
57
+
58
+ content := editContent[startIdx:endIdx]
59
+ content = strings.ReplaceAll(content, "\n> ", "\n")
60
+ return content
61
+ }
62
+
63
+ func (f *ThinkingFilter) ExtractIncrementalThinking(editContent string) string {
64
+ completeThinking := f.ExtractCompleteThinking(editContent)
65
+ if completeThinking == "" {
66
+ return ""
67
+ }
68
+
69
+ if f.LastOutputChunk == "" {
70
+ return completeThinking
71
+ }
72
+
73
+ idx := strings.Index(completeThinking, f.LastOutputChunk)
74
+ if idx == -1 {
75
+ return completeThinking
76
+ }
77
+
78
+ incrementalPart := completeThinking[idx+len(f.LastOutputChunk):]
79
+ return incrementalPart
80
+ }
81
+
82
+ func (f *ThinkingFilter) ResetForNewRound() {
83
+ f.LastOutputChunk = ""
84
+ f.HasSeenFirstThinking = false
85
+ }
internal/{chat.go → handler/chat.go} RENAMED
@@ -1,8 +1,7 @@
1
- package internal
2
 
3
  import (
4
  "bufio"
5
- "bytes"
6
  "encoding/json"
7
  "fmt"
8
  "io"
@@ -10,252 +9,14 @@ import (
10
  "strings"
11
  "time"
12
 
13
- "github.com/corpix/uarand"
14
  "github.com/google/uuid"
15
- )
16
-
17
- func extractLatestUserContent(messages []Message) string {
18
- for i := len(messages) - 1; i >= 0; i-- {
19
- if messages[i].Role == "user" {
20
- text, _ := messages[i].ParseContent()
21
- return text
22
- }
23
- }
24
- return ""
25
- }
26
-
27
- func extractAllImageURLs(messages []Message) []string {
28
- var allImageURLs []string
29
- for _, msg := range messages {
30
- _, imageURLs := msg.ParseContent()
31
- allImageURLs = append(allImageURLs, imageURLs...)
32
- }
33
- return allImageURLs
34
- }
35
-
36
- func makeUpstreamRequest(token string, messages []Message, model string) (*http.Response, string, error) {
37
- payload, err := DecodeJWTPayload(token)
38
- if err != nil || payload == nil {
39
- return nil, "", fmt.Errorf("invalid token")
40
- }
41
-
42
- userID := payload.ID
43
- chatID := uuid.New().String()
44
- timestamp := time.Now().UnixMilli()
45
- requestID := uuid.New().String()
46
- userMsgID := uuid.New().String()
47
-
48
- targetModel := GetTargetModel(model)
49
- latestUserContent := extractLatestUserContent(messages)
50
- imageURLs := extractAllImageURLs(messages)
51
-
52
- signature := GenerateSignature(userID, requestID, latestUserContent, timestamp)
53
-
54
- url := fmt.Sprintf("https://chat.z.ai/api/v2/chat/completions?timestamp=%d&requestId=%s&user_id=%s&version=0.0.1&platform=web&token=%s&current_url=%s&pathname=%s&signature_timestamp=%d",
55
- timestamp, requestID, userID, token,
56
- fmt.Sprintf("https://chat.z.ai/c/%s", chatID),
57
- fmt.Sprintf("/c/%s", chatID),
58
- timestamp)
59
-
60
- enableThinking := IsThinkingModel(model)
61
- autoWebSearch := IsSearchModel(model)
62
- if targetModel == "glm-4.5v" || targetModel == "glm-4.6v" {
63
- autoWebSearch = false
64
- }
65
-
66
- var mcpServers []string
67
- if targetModel == "glm-4.6v" {
68
- mcpServers = []string{"vlm-image-search", "vlm-image-recognition", "vlm-image-processing"}
69
- }
70
-
71
- urlToFileID := make(map[string]string)
72
- var filesData []map[string]interface{}
73
- if len(imageURLs) > 0 {
74
- files, _ := UploadImages(token, imageURLs)
75
- for i, f := range files {
76
- if i < len(imageURLs) {
77
- urlToFileID[imageURLs[i]] = f.ID
78
- }
79
- filesData = append(filesData, map[string]interface{}{
80
- "type": f.Type,
81
- "file": f.File,
82
- "id": f.ID,
83
- "url": f.URL,
84
- "name": f.Name,
85
- "status": f.Status,
86
- "size": f.Size,
87
- "error": f.Error,
88
- "itemId": f.ItemID,
89
- "media": f.Media,
90
- "ref_user_msg_id": userMsgID,
91
- })
92
- }
93
- }
94
-
95
- var upstreamMessages []map[string]interface{}
96
- for _, msg := range messages {
97
- upstreamMessages = append(upstreamMessages, msg.ToUpstreamMessage(urlToFileID))
98
- }
99
-
100
- body := map[string]interface{}{
101
- "stream": true,
102
- "model": targetModel,
103
- "messages": upstreamMessages,
104
- "signature_prompt": latestUserContent,
105
- "params": map[string]interface{}{},
106
- "features": map[string]interface{}{
107
- "image_generation": false,
108
- "web_search": false,
109
- "auto_web_search": autoWebSearch,
110
- "preview_mode": true,
111
- "enable_thinking": enableThinking,
112
- },
113
- "chat_id": chatID,
114
- "id": uuid.New().String(),
115
- }
116
-
117
- if len(mcpServers) > 0 {
118
- body["mcp_servers"] = mcpServers
119
- }
120
-
121
- if len(filesData) > 0 {
122
- body["files"] = filesData
123
- body["current_user_message_id"] = userMsgID
124
- }
125
-
126
- bodyBytes, _ := json.Marshal(body)
127
-
128
- req, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
129
- if err != nil {
130
- return nil, "", err
131
- }
132
-
133
- req.Header.Set("Authorization", "Bearer "+token)
134
- req.Header.Set("X-FE-Version", GetFeVersion())
135
- req.Header.Set("X-Signature", signature)
136
- req.Header.Set("Content-Type", "application/json")
137
- req.Header.Set("Connection", "keep-alive")
138
- req.Header.Set("Origin", "https://chat.z.ai")
139
- req.Header.Set("Referer", fmt.Sprintf("https://chat.z.ai/c/%s", uuid.New().String()))
140
- req.Header.Set("User-Agent", uarand.GetRandom())
141
-
142
- client := &http.Client{}
143
- resp, err := client.Do(req)
144
- if err != nil {
145
- return nil, "", err
146
- }
147
-
148
- return resp, targetModel, nil
149
- }
150
-
151
- type UpstreamData struct {
152
- Type string `json:"type"`
153
- Data struct {
154
- DeltaContent string `json:"delta_content"`
155
- EditContent string `json:"edit_content"`
156
- Phase string `json:"phase"`
157
- Done bool `json:"done"`
158
- } `json:"data"`
159
- }
160
-
161
- func (u *UpstreamData) GetEditContent() string {
162
- editContent := u.Data.EditContent
163
- if editContent == "" {
164
- return ""
165
- }
166
 
167
- if len(editContent) > 0 && editContent[0] == '"' {
168
- var unescaped string
169
- if err := json.Unmarshal([]byte(editContent), &unescaped); err == nil {
170
- LogDebug("[GetEditContent] Unescaped edit_content from JSON string")
171
- return unescaped
172
- }
173
- }
174
-
175
- return editContent
176
- }
177
-
178
- type ThinkingFilter struct {
179
- hasSeenFirstThinking bool
180
- buffer string
181
- lastOutputChunk string
182
- lastPhase string
183
- thinkingRoundCount int
184
- }
185
-
186
- func (f *ThinkingFilter) ProcessThinking(deltaContent string) string {
187
- if !f.hasSeenFirstThinking {
188
- f.hasSeenFirstThinking = true
189
- if idx := strings.Index(deltaContent, "> "); idx != -1 {
190
- deltaContent = deltaContent[idx+2:]
191
- } else {
192
- return ""
193
- }
194
- }
195
-
196
- content := f.buffer + deltaContent
197
- f.buffer = ""
198
-
199
- content = strings.ReplaceAll(content, "\n> ", "\n")
200
-
201
- if strings.HasSuffix(content, "\n>") {
202
- f.buffer = "\n>"
203
- return content[:len(content)-2]
204
- }
205
- if strings.HasSuffix(content, "\n") {
206
- f.buffer = "\n"
207
- return content[:len(content)-1]
208
- }
209
-
210
- return content
211
- }
212
-
213
- func (f *ThinkingFilter) Flush() string {
214
- result := f.buffer
215
- f.buffer = ""
216
- return result
217
- }
218
-
219
- func (f *ThinkingFilter) ExtractCompleteThinking(editContent string) string {
220
- startIdx := strings.Index(editContent, "> ")
221
- if startIdx == -1 {
222
- return ""
223
- }
224
- startIdx += 2
225
-
226
- endIdx := strings.Index(editContent, "\n</details>")
227
- if endIdx == -1 {
228
- return ""
229
- }
230
-
231
- content := editContent[startIdx:endIdx]
232
- content = strings.ReplaceAll(content, "\n> ", "\n")
233
- return content
234
- }
235
-
236
- func (f *ThinkingFilter) ExtractIncrementalThinking(editContent string) string {
237
- completeThinking := f.ExtractCompleteThinking(editContent)
238
- if completeThinking == "" {
239
- return ""
240
- }
241
-
242
- if f.lastOutputChunk == "" {
243
- return completeThinking
244
- }
245
-
246
- idx := strings.Index(completeThinking, f.lastOutputChunk)
247
- if idx == -1 {
248
- return completeThinking
249
- }
250
-
251
- incrementalPart := completeThinking[idx+len(f.lastOutputChunk):]
252
- return incrementalPart
253
- }
254
-
255
- func (f *ThinkingFilter) ResetForNewRound() {
256
- f.lastOutputChunk = ""
257
- f.hasSeenFirstThinking = false
258
- }
259
 
260
  func HandleChatCompletions(w http.ResponseWriter, r *http.Request) {
261
  token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
@@ -265,16 +26,16 @@ func HandleChatCompletions(w http.ResponseWriter, r *http.Request) {
265
  }
266
 
267
  if token == "free" {
268
- anonymousToken, err := GetAnonymousToken()
269
  if err != nil {
270
- LogError("Failed to get anonymous token: %v", err)
271
  http.Error(w, "Failed to get anonymous token", http.StatusInternalServerError)
272
  return
273
  }
274
  token = anonymousToken
275
  }
276
 
277
- var req ChatRequest
278
  if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
279
  http.Error(w, "Invalid request", http.StatusBadRequest)
280
  return
@@ -284,9 +45,9 @@ func HandleChatCompletions(w http.ResponseWriter, r *http.Request) {
284
  req.Model = "GLM-4.6"
285
  }
286
 
287
- resp, modelName, err := makeUpstreamRequest(token, req.Messages, req.Model)
288
  if err != nil {
289
- LogError("Upstream request failed: %v", err)
290
  http.Error(w, "Upstream error", http.StatusBadGateway)
291
  return
292
  }
@@ -298,7 +59,7 @@ func HandleChatCompletions(w http.ResponseWriter, r *http.Request) {
298
  if len(bodyStr) > 500 {
299
  bodyStr = bodyStr[:500]
300
  }
301
- LogError("Upstream error: status=%d, body=%s", resp.StatusCode, bodyStr)
302
  http.Error(w, "Upstream error", resp.StatusCode)
303
  return
304
  }
@@ -326,15 +87,15 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
326
  scanner := bufio.NewScanner(body)
327
  scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
328
  hasContent := false
329
- searchRefFilter := NewSearchRefFilter()
330
- thinkingFilter := &ThinkingFilter{}
331
  pendingSourcesMarkdown := ""
332
  pendingImageSearchMarkdown := ""
333
- totalContentOutputLength := 0 // 记录已输出的 content 字符长度
334
 
335
  for scanner.Scan() {
336
  line := scanner.Text()
337
- LogDebug("[Upstream] %s", line)
338
 
339
  if !strings.HasPrefix(line, "data: ") {
340
  continue
@@ -345,44 +106,44 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
345
  break
346
  }
347
 
348
- var upstream UpstreamData
349
- if err := json.Unmarshal([]byte(payload), &upstream); err != nil {
350
  continue
351
  }
352
 
353
- if upstream.Data.Phase == "done" {
354
  break
355
  }
356
 
357
- if upstream.Data.Phase == "thinking" && upstream.Data.DeltaContent != "" {
358
  isNewThinkingRound := false
359
- if thinkingFilter.lastPhase != "" && thinkingFilter.lastPhase != "thinking" {
360
  thinkingFilter.ResetForNewRound()
361
- thinkingFilter.thinkingRoundCount++
362
  isNewThinkingRound = true
363
  }
364
- thinkingFilter.lastPhase = "thinking"
365
 
366
- reasoningContent := thinkingFilter.ProcessThinking(upstream.Data.DeltaContent)
367
 
368
- if isNewThinkingRound && thinkingFilter.thinkingRoundCount > 1 && reasoningContent != "" {
369
  reasoningContent = "\n\n" + reasoningContent
370
  }
371
 
372
  if reasoningContent != "" {
373
- thinkingFilter.lastOutputChunk = reasoningContent
374
  reasoningContent = searchRefFilter.Process(reasoningContent)
375
 
376
  if reasoningContent != "" {
377
  hasContent = true
378
- chunk := ChatCompletionChunk{
379
  ID: completionID,
380
  Object: "chat.completion.chunk",
381
  Created: time.Now().Unix(),
382
  Model: modelName,
383
- Choices: []Choice{{
384
  Index: 0,
385
- Delta: Delta{ReasoningContent: reasoningContent},
386
  FinishReason: nil,
387
  }},
388
  }
@@ -394,32 +155,32 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
394
  continue
395
  }
396
 
397
- if upstream.Data.Phase != "" {
398
- thinkingFilter.lastPhase = upstream.Data.Phase
399
  }
400
 
401
- editContent := upstream.GetEditContent()
402
- if editContent != "" && IsSearchResultContent(editContent) {
403
- if results := ParseSearchResults(editContent); len(results) > 0 {
404
  searchRefFilter.AddSearchResults(results)
405
  pendingSourcesMarkdown = searchRefFilter.GetSearchResultsMarkdown()
406
  }
407
  continue
408
  }
409
  if editContent != "" && strings.Contains(editContent, `"search_image"`) {
410
- textBeforeBlock := ExtractTextBeforeGlmBlock(editContent)
411
  if textBeforeBlock != "" {
412
  textBeforeBlock = searchRefFilter.Process(textBeforeBlock)
413
  if textBeforeBlock != "" {
414
  hasContent = true
415
- chunk := ChatCompletionChunk{
416
  ID: completionID,
417
  Object: "chat.completion.chunk",
418
  Created: time.Now().Unix(),
419
  Model: modelName,
420
- Choices: []Choice{{
421
  Index: 0,
422
- Delta: Delta{Content: textBeforeBlock},
423
  FinishReason: nil,
424
  }},
425
  }
@@ -428,25 +189,25 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
428
  flusher.Flush()
429
  }
430
  }
431
- if results := ParseImageSearchResults(editContent); len(results) > 0 {
432
- pendingImageSearchMarkdown = FormatImageSearchResults(results)
433
  }
434
  continue
435
  }
436
  if editContent != "" && strings.Contains(editContent, `"mcp"`) {
437
- textBeforeBlock := ExtractTextBeforeGlmBlock(editContent)
438
  if textBeforeBlock != "" {
439
  textBeforeBlock = searchRefFilter.Process(textBeforeBlock)
440
  if textBeforeBlock != "" {
441
  hasContent = true
442
- chunk := ChatCompletionChunk{
443
  ID: completionID,
444
  Object: "chat.completion.chunk",
445
  Created: time.Now().Unix(),
446
  Model: modelName,
447
- Choices: []Choice{{
448
  Index: 0,
449
- Delta: Delta{Content: textBeforeBlock},
450
  FinishReason: nil,
451
  }},
452
  }
@@ -457,20 +218,20 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
457
  }
458
  continue
459
  }
460
- if editContent != "" && IsSearchToolCall(editContent, upstream.Data.Phase) {
461
  continue
462
  }
463
 
464
  if pendingSourcesMarkdown != "" {
465
  hasContent = true
466
- chunk := ChatCompletionChunk{
467
  ID: completionID,
468
  Object: "chat.completion.chunk",
469
  Created: time.Now().Unix(),
470
  Model: modelName,
471
- Choices: []Choice{{
472
  Index: 0,
473
- Delta: Delta{Content: pendingSourcesMarkdown},
474
  FinishReason: nil,
475
  }},
476
  }
@@ -481,14 +242,14 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
481
  }
482
  if pendingImageSearchMarkdown != "" {
483
  hasContent = true
484
- chunk := ChatCompletionChunk{
485
  ID: completionID,
486
  Object: "chat.completion.chunk",
487
  Created: time.Now().Unix(),
488
  Model: modelName,
489
- Choices: []Choice{{
490
  Index: 0,
491
- Delta: Delta{Content: pendingImageSearchMarkdown},
492
  FinishReason: nil,
493
  }},
494
  }
@@ -502,18 +263,18 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
502
  reasoningContent := ""
503
 
504
  if thinkingRemaining := thinkingFilter.Flush(); thinkingRemaining != "" {
505
- thinkingFilter.lastOutputChunk = thinkingRemaining
506
  processedRemaining := searchRefFilter.Process(thinkingRemaining)
507
  if processedRemaining != "" {
508
  hasContent = true
509
- chunk := ChatCompletionChunk{
510
  ID: completionID,
511
  Object: "chat.completion.chunk",
512
  Created: time.Now().Unix(),
513
  Model: modelName,
514
- Choices: []Choice{{
515
  Index: 0,
516
- Delta: Delta{ReasoningContent: processedRemaining},
517
  FinishReason: nil,
518
  }},
519
  }
@@ -523,16 +284,16 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
523
  }
524
  }
525
 
526
- if pendingSourcesMarkdown != "" && thinkingFilter.hasSeenFirstThinking {
527
  hasContent = true
528
- chunk := ChatCompletionChunk{
529
  ID: completionID,
530
  Object: "chat.completion.chunk",
531
  Created: time.Now().Unix(),
532
  Model: modelName,
533
- Choices: []Choice{{
534
  Index: 0,
535
- Delta: Delta{ReasoningContent: pendingSourcesMarkdown},
536
  FinishReason: nil,
537
  }},
538
  }
@@ -542,9 +303,9 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
542
  pendingSourcesMarkdown = ""
543
  }
544
 
545
- if upstream.Data.Phase == "answer" && upstream.Data.DeltaContent != "" {
546
- content = upstream.Data.DeltaContent
547
- } else if upstream.Data.Phase == "answer" && editContent != "" {
548
  if strings.Contains(editContent, "</details>") {
549
  reasoningContent = thinkingFilter.ExtractIncrementalThinking(editContent)
550
 
@@ -558,7 +319,7 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
558
  totalContentOutputLength = len([]rune(content))
559
  }
560
  }
561
- } else if (upstream.Data.Phase == "other" || upstream.Data.Phase == "tool_call") && editContent != "" {
562
  fullContent := editContent
563
  fullContentRunes := []rune(fullContent)
564
 
@@ -575,14 +336,14 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
575
  }
576
  if reasoningContent != "" {
577
  hasContent = true
578
- chunk := ChatCompletionChunk{
579
  ID: completionID,
580
  Object: "chat.completion.chunk",
581
  Created: time.Now().Unix(),
582
  Model: modelName,
583
- Choices: []Choice{{
584
  Index: 0,
585
- Delta: Delta{ReasoningContent: reasoningContent},
586
  FinishReason: nil,
587
  }},
588
  }
@@ -601,18 +362,18 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
601
  }
602
 
603
  hasContent = true
604
- if upstream.Data.Phase == "answer" && upstream.Data.DeltaContent != "" {
605
  totalContentOutputLength += len([]rune(content))
606
  }
607
 
608
- chunk := ChatCompletionChunk{
609
  ID: completionID,
610
  Object: "chat.completion.chunk",
611
  Created: time.Now().Unix(),
612
  Model: modelName,
613
- Choices: []Choice{{
614
  Index: 0,
615
- Delta: Delta{Content: content},
616
  FinishReason: nil,
617
  }},
618
  }
@@ -623,19 +384,19 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
623
  }
624
 
625
  if err := scanner.Err(); err != nil {
626
- LogError("[Upstream] scanner error: %v", err)
627
  }
628
 
629
  if remaining := searchRefFilter.Flush(); remaining != "" {
630
  hasContent = true
631
- chunk := ChatCompletionChunk{
632
  ID: completionID,
633
  Object: "chat.completion.chunk",
634
  Created: time.Now().Unix(),
635
  Model: modelName,
636
- Choices: []Choice{{
637
  Index: 0,
638
- Delta: Delta{Content: remaining},
639
  FinishReason: nil,
640
  }},
641
  }
@@ -645,18 +406,18 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
645
  }
646
 
647
  if !hasContent {
648
- LogError("Stream response 200 but no content received")
649
  }
650
 
651
  stopReason := "stop"
652
- finalChunk := ChatCompletionChunk{
653
  ID: completionID,
654
  Object: "chat.completion.chunk",
655
  Created: time.Now().Unix(),
656
  Model: modelName,
657
- Choices: []Choice{{
658
  Index: 0,
659
- Delta: Delta{},
660
  FinishReason: &stopReason,
661
  }},
662
  }
@@ -672,8 +433,8 @@ func handleNonStreamResponse(w http.ResponseWriter, body io.ReadCloser, completi
672
  scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
673
  var chunks []string
674
  var reasoningChunks []string
675
- thinkingFilter := &ThinkingFilter{}
676
- searchRefFilter := NewSearchRefFilter()
677
  hasThinking := false
678
  pendingSourcesMarkdown := ""
679
  pendingImageSearchMarkdown := ""
@@ -689,65 +450,64 @@ func handleNonStreamResponse(w http.ResponseWriter, body io.ReadCloser, completi
689
  break
690
  }
691
 
692
- var upstream UpstreamData
693
- if err := json.Unmarshal([]byte(payload), &upstream); err != nil {
694
  continue
695
  }
696
 
697
- if upstream.Data.Phase == "done" {
698
  break
699
  }
700
 
701
- if upstream.Data.Phase == "thinking" && upstream.Data.DeltaContent != "" {
702
- if thinkingFilter.lastPhase != "" && thinkingFilter.lastPhase != "thinking" {
703
  thinkingFilter.ResetForNewRound()
704
- thinkingFilter.thinkingRoundCount++
705
- if thinkingFilter.thinkingRoundCount > 1 {
706
  reasoningChunks = append(reasoningChunks, "\n\n")
707
  }
708
  }
709
- thinkingFilter.lastPhase = "thinking"
710
 
711
  hasThinking = true
712
- reasoningContent := thinkingFilter.ProcessThinking(upstream.Data.DeltaContent)
713
  if reasoningContent != "" {
714
- thinkingFilter.lastOutputChunk = reasoningContent
715
  reasoningChunks = append(reasoningChunks, reasoningContent)
716
  }
717
  continue
718
  }
719
 
720
- if upstream.Data.Phase != "" {
721
- thinkingFilter.lastPhase = upstream.Data.Phase
722
  }
723
 
724
- editContent := upstream.GetEditContent()
725
- if editContent != "" && IsSearchResultContent(editContent) {
726
- if results := ParseSearchResults(editContent); len(results) > 0 {
727
  searchRefFilter.AddSearchResults(results)
728
  pendingSourcesMarkdown = searchRefFilter.GetSearchResultsMarkdown()
729
  }
730
  continue
731
  }
732
  if editContent != "" && strings.Contains(editContent, `"search_image"`) {
733
- textBeforeBlock := ExtractTextBeforeGlmBlock(editContent)
734
  if textBeforeBlock != "" {
735
  chunks = append(chunks, textBeforeBlock)
736
  }
737
- // 解析图片搜索结果
738
- if results := ParseImageSearchResults(editContent); len(results) > 0 {
739
- pendingImageSearchMarkdown = FormatImageSearchResults(results)
740
  }
741
  continue
742
  }
743
  if editContent != "" && strings.Contains(editContent, `"mcp"`) {
744
- textBeforeBlock := ExtractTextBeforeGlmBlock(editContent)
745
  if textBeforeBlock != "" {
746
  chunks = append(chunks, textBeforeBlock)
747
  }
748
  continue
749
  }
750
- if editContent != "" && IsSearchToolCall(editContent, upstream.Data.Phase) {
751
  continue
752
  }
753
 
@@ -765,9 +525,9 @@ func handleNonStreamResponse(w http.ResponseWriter, body io.ReadCloser, completi
765
  }
766
 
767
  content := ""
768
- if upstream.Data.Phase == "answer" && upstream.Data.DeltaContent != "" {
769
- content = upstream.Data.DeltaContent
770
- } else if upstream.Data.Phase == "answer" && editContent != "" {
771
  if strings.Contains(editContent, "</details>") {
772
  reasoningContent := thinkingFilter.ExtractIncrementalThinking(editContent)
773
  if reasoningContent != "" {
@@ -783,7 +543,7 @@ func handleNonStreamResponse(w http.ResponseWriter, body io.ReadCloser, completi
783
  }
784
  }
785
  }
786
- } else if (upstream.Data.Phase == "other" || upstream.Data.Phase == "tool_call") && editContent != "" {
787
  content = editContent
788
  }
789
 
@@ -798,18 +558,18 @@ func handleNonStreamResponse(w http.ResponseWriter, body io.ReadCloser, completi
798
  fullReasoning = searchRefFilter.Process(fullReasoning) + searchRefFilter.Flush()
799
 
800
  if fullContent == "" {
801
- LogError("Non-stream response 200 but no content received")
802
  }
803
 
804
  stopReason := "stop"
805
- response := ChatCompletionResponse{
806
  ID: completionID,
807
  Object: "chat.completion",
808
  Created: time.Now().Unix(),
809
  Model: modelName,
810
- Choices: []Choice{{
811
  Index: 0,
812
- Message: &MessageResp{
813
  Role: "assistant",
814
  Content: fullContent,
815
  ReasoningContent: fullReasoning,
@@ -821,22 +581,3 @@ func handleNonStreamResponse(w http.ResponseWriter, body io.ReadCloser, completi
821
  w.Header().Set("Content-Type", "application/json")
822
  json.NewEncoder(w).Encode(response)
823
  }
824
-
825
- func HandleModels(w http.ResponseWriter, r *http.Request) {
826
- var models []ModelInfo
827
- for _, id := range ModelList {
828
- models = append(models, ModelInfo{
829
- ID: id,
830
- Object: "model",
831
- OwnedBy: "z.ai",
832
- })
833
- }
834
-
835
- response := ModelsResponse{
836
- Object: "list",
837
- Data: models,
838
- }
839
-
840
- w.Header().Set("Content-Type", "application/json")
841
- json.NewEncoder(w).Encode(response)
842
- }
 
1
+ package handler
2
 
3
  import (
4
  "bufio"
 
5
  "encoding/json"
6
  "fmt"
7
  "io"
 
9
  "strings"
10
  "time"
11
 
 
12
  "github.com/google/uuid"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ "zai-proxy/internal/auth"
15
+ "zai-proxy/internal/filter"
16
+ "zai-proxy/internal/logger"
17
+ "zai-proxy/internal/model"
18
+ "zai-proxy/internal/upstream"
19
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  func HandleChatCompletions(w http.ResponseWriter, r *http.Request) {
22
  token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
 
26
  }
27
 
28
  if token == "free" {
29
+ anonymousToken, err := auth.GetAnonymousToken()
30
  if err != nil {
31
+ logger.LogError("Failed to get anonymous token: %v", err)
32
  http.Error(w, "Failed to get anonymous token", http.StatusInternalServerError)
33
  return
34
  }
35
  token = anonymousToken
36
  }
37
 
38
+ var req model.ChatRequest
39
  if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
40
  http.Error(w, "Invalid request", http.StatusBadRequest)
41
  return
 
45
  req.Model = "GLM-4.6"
46
  }
47
 
48
+ resp, modelName, err := upstream.MakeUpstreamRequest(token, req.Messages, req.Model)
49
  if err != nil {
50
+ logger.LogError("Upstream request failed: %v", err)
51
  http.Error(w, "Upstream error", http.StatusBadGateway)
52
  return
53
  }
 
59
  if len(bodyStr) > 500 {
60
  bodyStr = bodyStr[:500]
61
  }
62
+ logger.LogError("Upstream error: status=%d, body=%s", resp.StatusCode, bodyStr)
63
  http.Error(w, "Upstream error", resp.StatusCode)
64
  return
65
  }
 
87
  scanner := bufio.NewScanner(body)
88
  scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
89
  hasContent := false
90
+ searchRefFilter := filter.NewSearchRefFilter()
91
+ thinkingFilter := &filter.ThinkingFilter{}
92
  pendingSourcesMarkdown := ""
93
  pendingImageSearchMarkdown := ""
94
+ totalContentOutputLength := 0
95
 
96
  for scanner.Scan() {
97
  line := scanner.Text()
98
+ logger.LogDebug("[Upstream] %s", line)
99
 
100
  if !strings.HasPrefix(line, "data: ") {
101
  continue
 
106
  break
107
  }
108
 
109
+ var upstreamData model.UpstreamData
110
+ if err := json.Unmarshal([]byte(payload), &upstreamData); err != nil {
111
  continue
112
  }
113
 
114
+ if upstreamData.Data.Phase == "done" {
115
  break
116
  }
117
 
118
+ if upstreamData.Data.Phase == "thinking" && upstreamData.Data.DeltaContent != "" {
119
  isNewThinkingRound := false
120
+ if thinkingFilter.LastPhase != "" && thinkingFilter.LastPhase != "thinking" {
121
  thinkingFilter.ResetForNewRound()
122
+ thinkingFilter.ThinkingRoundCount++
123
  isNewThinkingRound = true
124
  }
125
+ thinkingFilter.LastPhase = "thinking"
126
 
127
+ reasoningContent := thinkingFilter.ProcessThinking(upstreamData.Data.DeltaContent)
128
 
129
+ if isNewThinkingRound && thinkingFilter.ThinkingRoundCount > 1 && reasoningContent != "" {
130
  reasoningContent = "\n\n" + reasoningContent
131
  }
132
 
133
  if reasoningContent != "" {
134
+ thinkingFilter.LastOutputChunk = reasoningContent
135
  reasoningContent = searchRefFilter.Process(reasoningContent)
136
 
137
  if reasoningContent != "" {
138
  hasContent = true
139
+ chunk := model.ChatCompletionChunk{
140
  ID: completionID,
141
  Object: "chat.completion.chunk",
142
  Created: time.Now().Unix(),
143
  Model: modelName,
144
+ Choices: []model.Choice{{
145
  Index: 0,
146
+ Delta: model.Delta{ReasoningContent: reasoningContent},
147
  FinishReason: nil,
148
  }},
149
  }
 
155
  continue
156
  }
157
 
158
+ if upstreamData.Data.Phase != "" {
159
+ thinkingFilter.LastPhase = upstreamData.Data.Phase
160
  }
161
 
162
+ editContent := upstreamData.GetEditContent()
163
+ if editContent != "" && filter.IsSearchResultContent(editContent) {
164
+ if results := filter.ParseSearchResults(editContent); len(results) > 0 {
165
  searchRefFilter.AddSearchResults(results)
166
  pendingSourcesMarkdown = searchRefFilter.GetSearchResultsMarkdown()
167
  }
168
  continue
169
  }
170
  if editContent != "" && strings.Contains(editContent, `"search_image"`) {
171
+ textBeforeBlock := filter.ExtractTextBeforeGlmBlock(editContent)
172
  if textBeforeBlock != "" {
173
  textBeforeBlock = searchRefFilter.Process(textBeforeBlock)
174
  if textBeforeBlock != "" {
175
  hasContent = true
176
+ chunk := model.ChatCompletionChunk{
177
  ID: completionID,
178
  Object: "chat.completion.chunk",
179
  Created: time.Now().Unix(),
180
  Model: modelName,
181
+ Choices: []model.Choice{{
182
  Index: 0,
183
+ Delta: model.Delta{Content: textBeforeBlock},
184
  FinishReason: nil,
185
  }},
186
  }
 
189
  flusher.Flush()
190
  }
191
  }
192
+ if results := filter.ParseImageSearchResults(editContent); len(results) > 0 {
193
+ pendingImageSearchMarkdown = filter.FormatImageSearchResults(results)
194
  }
195
  continue
196
  }
197
  if editContent != "" && strings.Contains(editContent, `"mcp"`) {
198
+ textBeforeBlock := filter.ExtractTextBeforeGlmBlock(editContent)
199
  if textBeforeBlock != "" {
200
  textBeforeBlock = searchRefFilter.Process(textBeforeBlock)
201
  if textBeforeBlock != "" {
202
  hasContent = true
203
+ chunk := model.ChatCompletionChunk{
204
  ID: completionID,
205
  Object: "chat.completion.chunk",
206
  Created: time.Now().Unix(),
207
  Model: modelName,
208
+ Choices: []model.Choice{{
209
  Index: 0,
210
+ Delta: model.Delta{Content: textBeforeBlock},
211
  FinishReason: nil,
212
  }},
213
  }
 
218
  }
219
  continue
220
  }
221
+ if editContent != "" && filter.IsSearchToolCall(editContent, upstreamData.Data.Phase) {
222
  continue
223
  }
224
 
225
  if pendingSourcesMarkdown != "" {
226
  hasContent = true
227
+ chunk := model.ChatCompletionChunk{
228
  ID: completionID,
229
  Object: "chat.completion.chunk",
230
  Created: time.Now().Unix(),
231
  Model: modelName,
232
+ Choices: []model.Choice{{
233
  Index: 0,
234
+ Delta: model.Delta{Content: pendingSourcesMarkdown},
235
  FinishReason: nil,
236
  }},
237
  }
 
242
  }
243
  if pendingImageSearchMarkdown != "" {
244
  hasContent = true
245
+ chunk := model.ChatCompletionChunk{
246
  ID: completionID,
247
  Object: "chat.completion.chunk",
248
  Created: time.Now().Unix(),
249
  Model: modelName,
250
+ Choices: []model.Choice{{
251
  Index: 0,
252
+ Delta: model.Delta{Content: pendingImageSearchMarkdown},
253
  FinishReason: nil,
254
  }},
255
  }
 
263
  reasoningContent := ""
264
 
265
  if thinkingRemaining := thinkingFilter.Flush(); thinkingRemaining != "" {
266
+ thinkingFilter.LastOutputChunk = thinkingRemaining
267
  processedRemaining := searchRefFilter.Process(thinkingRemaining)
268
  if processedRemaining != "" {
269
  hasContent = true
270
+ chunk := model.ChatCompletionChunk{
271
  ID: completionID,
272
  Object: "chat.completion.chunk",
273
  Created: time.Now().Unix(),
274
  Model: modelName,
275
+ Choices: []model.Choice{{
276
  Index: 0,
277
+ Delta: model.Delta{ReasoningContent: processedRemaining},
278
  FinishReason: nil,
279
  }},
280
  }
 
284
  }
285
  }
286
 
287
+ if pendingSourcesMarkdown != "" && thinkingFilter.HasSeenFirstThinking {
288
  hasContent = true
289
+ chunk := model.ChatCompletionChunk{
290
  ID: completionID,
291
  Object: "chat.completion.chunk",
292
  Created: time.Now().Unix(),
293
  Model: modelName,
294
+ Choices: []model.Choice{{
295
  Index: 0,
296
+ Delta: model.Delta{ReasoningContent: pendingSourcesMarkdown},
297
  FinishReason: nil,
298
  }},
299
  }
 
303
  pendingSourcesMarkdown = ""
304
  }
305
 
306
+ if upstreamData.Data.Phase == "answer" && upstreamData.Data.DeltaContent != "" {
307
+ content = upstreamData.Data.DeltaContent
308
+ } else if upstreamData.Data.Phase == "answer" && editContent != "" {
309
  if strings.Contains(editContent, "</details>") {
310
  reasoningContent = thinkingFilter.ExtractIncrementalThinking(editContent)
311
 
 
319
  totalContentOutputLength = len([]rune(content))
320
  }
321
  }
322
+ } else if (upstreamData.Data.Phase == "other" || upstreamData.Data.Phase == "tool_call") && editContent != "" {
323
  fullContent := editContent
324
  fullContentRunes := []rune(fullContent)
325
 
 
336
  }
337
  if reasoningContent != "" {
338
  hasContent = true
339
+ chunk := model.ChatCompletionChunk{
340
  ID: completionID,
341
  Object: "chat.completion.chunk",
342
  Created: time.Now().Unix(),
343
  Model: modelName,
344
+ Choices: []model.Choice{{
345
  Index: 0,
346
+ Delta: model.Delta{ReasoningContent: reasoningContent},
347
  FinishReason: nil,
348
  }},
349
  }
 
362
  }
363
 
364
  hasContent = true
365
+ if upstreamData.Data.Phase == "answer" && upstreamData.Data.DeltaContent != "" {
366
  totalContentOutputLength += len([]rune(content))
367
  }
368
 
369
+ chunk := model.ChatCompletionChunk{
370
  ID: completionID,
371
  Object: "chat.completion.chunk",
372
  Created: time.Now().Unix(),
373
  Model: modelName,
374
+ Choices: []model.Choice{{
375
  Index: 0,
376
+ Delta: model.Delta{Content: content},
377
  FinishReason: nil,
378
  }},
379
  }
 
384
  }
385
 
386
  if err := scanner.Err(); err != nil {
387
+ logger.LogError("[Upstream] scanner error: %v", err)
388
  }
389
 
390
  if remaining := searchRefFilter.Flush(); remaining != "" {
391
  hasContent = true
392
+ chunk := model.ChatCompletionChunk{
393
  ID: completionID,
394
  Object: "chat.completion.chunk",
395
  Created: time.Now().Unix(),
396
  Model: modelName,
397
+ Choices: []model.Choice{{
398
  Index: 0,
399
+ Delta: model.Delta{Content: remaining},
400
  FinishReason: nil,
401
  }},
402
  }
 
406
  }
407
 
408
  if !hasContent {
409
+ logger.LogError("Stream response 200 but no content received")
410
  }
411
 
412
  stopReason := "stop"
413
+ finalChunk := model.ChatCompletionChunk{
414
  ID: completionID,
415
  Object: "chat.completion.chunk",
416
  Created: time.Now().Unix(),
417
  Model: modelName,
418
+ Choices: []model.Choice{{
419
  Index: 0,
420
+ Delta: model.Delta{},
421
  FinishReason: &stopReason,
422
  }},
423
  }
 
433
  scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
434
  var chunks []string
435
  var reasoningChunks []string
436
+ thinkingFilter := &filter.ThinkingFilter{}
437
+ searchRefFilter := filter.NewSearchRefFilter()
438
  hasThinking := false
439
  pendingSourcesMarkdown := ""
440
  pendingImageSearchMarkdown := ""
 
450
  break
451
  }
452
 
453
+ var upstreamData model.UpstreamData
454
+ if err := json.Unmarshal([]byte(payload), &upstreamData); err != nil {
455
  continue
456
  }
457
 
458
+ if upstreamData.Data.Phase == "done" {
459
  break
460
  }
461
 
462
+ if upstreamData.Data.Phase == "thinking" && upstreamData.Data.DeltaContent != "" {
463
+ if thinkingFilter.LastPhase != "" && thinkingFilter.LastPhase != "thinking" {
464
  thinkingFilter.ResetForNewRound()
465
+ thinkingFilter.ThinkingRoundCount++
466
+ if thinkingFilter.ThinkingRoundCount > 1 {
467
  reasoningChunks = append(reasoningChunks, "\n\n")
468
  }
469
  }
470
+ thinkingFilter.LastPhase = "thinking"
471
 
472
  hasThinking = true
473
+ reasoningContent := thinkingFilter.ProcessThinking(upstreamData.Data.DeltaContent)
474
  if reasoningContent != "" {
475
+ thinkingFilter.LastOutputChunk = reasoningContent
476
  reasoningChunks = append(reasoningChunks, reasoningContent)
477
  }
478
  continue
479
  }
480
 
481
+ if upstreamData.Data.Phase != "" {
482
+ thinkingFilter.LastPhase = upstreamData.Data.Phase
483
  }
484
 
485
+ editContent := upstreamData.GetEditContent()
486
+ if editContent != "" && filter.IsSearchResultContent(editContent) {
487
+ if results := filter.ParseSearchResults(editContent); len(results) > 0 {
488
  searchRefFilter.AddSearchResults(results)
489
  pendingSourcesMarkdown = searchRefFilter.GetSearchResultsMarkdown()
490
  }
491
  continue
492
  }
493
  if editContent != "" && strings.Contains(editContent, `"search_image"`) {
494
+ textBeforeBlock := filter.ExtractTextBeforeGlmBlock(editContent)
495
  if textBeforeBlock != "" {
496
  chunks = append(chunks, textBeforeBlock)
497
  }
498
+ if results := filter.ParseImageSearchResults(editContent); len(results) > 0 {
499
+ pendingImageSearchMarkdown = filter.FormatImageSearchResults(results)
 
500
  }
501
  continue
502
  }
503
  if editContent != "" && strings.Contains(editContent, `"mcp"`) {
504
+ textBeforeBlock := filter.ExtractTextBeforeGlmBlock(editContent)
505
  if textBeforeBlock != "" {
506
  chunks = append(chunks, textBeforeBlock)
507
  }
508
  continue
509
  }
510
+ if editContent != "" && filter.IsSearchToolCall(editContent, upstreamData.Data.Phase) {
511
  continue
512
  }
513
 
 
525
  }
526
 
527
  content := ""
528
+ if upstreamData.Data.Phase == "answer" && upstreamData.Data.DeltaContent != "" {
529
+ content = upstreamData.Data.DeltaContent
530
+ } else if upstreamData.Data.Phase == "answer" && editContent != "" {
531
  if strings.Contains(editContent, "</details>") {
532
  reasoningContent := thinkingFilter.ExtractIncrementalThinking(editContent)
533
  if reasoningContent != "" {
 
543
  }
544
  }
545
  }
546
+ } else if (upstreamData.Data.Phase == "other" || upstreamData.Data.Phase == "tool_call") && editContent != "" {
547
  content = editContent
548
  }
549
 
 
558
  fullReasoning = searchRefFilter.Process(fullReasoning) + searchRefFilter.Flush()
559
 
560
  if fullContent == "" {
561
+ logger.LogError("Non-stream response 200 but no content received")
562
  }
563
 
564
  stopReason := "stop"
565
+ response := model.ChatCompletionResponse{
566
  ID: completionID,
567
  Object: "chat.completion",
568
  Created: time.Now().Unix(),
569
  Model: modelName,
570
+ Choices: []model.Choice{{
571
  Index: 0,
572
+ Message: &model.MessageResp{
573
  Role: "assistant",
574
  Content: fullContent,
575
  ReasoningContent: fullReasoning,
 
581
  w.Header().Set("Content-Type", "application/json")
582
  json.NewEncoder(w).Encode(response)
583
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
internal/handler/models.go ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handler
2
+
3
+ import (
4
+ "encoding/json"
5
+ "net/http"
6
+
7
+ "zai-proxy/internal/model"
8
+ )
9
+
10
+ func HandleModels(w http.ResponseWriter, r *http.Request) {
11
+ var models []model.ModelInfo
12
+ for _, id := range model.ModelList {
13
+ models = append(models, model.ModelInfo{
14
+ ID: id,
15
+ Object: "model",
16
+ OwnedBy: "z.ai",
17
+ })
18
+ }
19
+
20
+ response := model.ModelsResponse{
21
+ Object: "list",
22
+ Data: models,
23
+ }
24
+
25
+ w.Header().Set("Content-Type", "application/json")
26
+ json.NewEncoder(w).Encode(response)
27
+ }
internal/{logger.go → logger/logger.go} RENAMED
@@ -1,4 +1,4 @@
1
- package internal
2
 
3
  import (
4
  "fmt"
 
1
+ package logger
2
 
3
  import (
4
  "fmt"
internal/model/mapping.go ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 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
+ // v1/models 返回的模型列表(不包含所有标签组合)
17
+ var ModelList = []string{
18
+ "GLM-4.5",
19
+ "GLM-4.6",
20
+ "GLM-4.7",
21
+ "GLM-4.7-thinking",
22
+ "GLM-4.7-thinking-search",
23
+ "GLM-4.5-V",
24
+ "GLM-4.6-V",
25
+ "GLM-4.6-V-thinking",
26
+ "GLM-4.5-Air",
27
+ // "0808-360B-DR",
28
+ }
29
+
30
+ // 解析模型名称,提取基础模型名和标签
31
+ // 支持 -thinking 和 -search 标签的任意排列组合
32
+ func ParseModelName(model string) (baseModel string, enableThinking bool, enableSearch bool) {
33
+ enableThinking = false
34
+ enableSearch = false
35
+ baseModel = model
36
+
37
+ // 检查并移除 -thinking 和 -search 标签(任意顺序)
38
+ for {
39
+ if strings.HasSuffix(baseModel, "-thinking") {
40
+ enableThinking = true
41
+ baseModel = strings.TrimSuffix(baseModel, "-thinking")
42
+ } else if strings.HasSuffix(baseModel, "-search") {
43
+ enableSearch = true
44
+ baseModel = strings.TrimSuffix(baseModel, "-search")
45
+ } else {
46
+ break
47
+ }
48
+ }
49
+
50
+ return baseModel, enableThinking, enableSearch
51
+ }
52
+
53
+ func IsThinkingModel(model string) bool {
54
+ _, enableThinking, _ := ParseModelName(model)
55
+ return enableThinking
56
+ }
57
+
58
+ func IsSearchModel(model string) bool {
59
+ _, _, enableSearch := ParseModelName(model)
60
+ return enableSearch
61
+ }
62
+
63
+ func GetTargetModel(model string) string {
64
+ baseModel, _, _ := ParseModelName(model)
65
+ if target, ok := BaseModelMapping[baseModel]; ok {
66
+ return target
67
+ }
68
+ return model
69
+ }
internal/model/types.go ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package model
2
+
3
+ import "encoding/json"
4
+
5
+ // OpenAI 格式的消息内容项
6
+ type ContentPart struct {
7
+ Type string `json:"type"`
8
+ Text string `json:"text,omitempty"`
9
+ ImageURL *ImageURL `json:"image_url,omitempty"`
10
+ }
11
+
12
+ type ImageURL struct {
13
+ URL string `json:"url"`
14
+ }
15
+
16
+ // Message 支持纯文本和多模态内容
17
+ type Message struct {
18
+ Role string `json:"role"`
19
+ Content interface{} `json:"content"` // string 或 []ContentPart
20
+ }
21
+
22
+ // 解析消息内容,返回文本和图片URL列表
23
+ func (m *Message) ParseContent() (text string, imageURLs []string) {
24
+ switch content := m.Content.(type) {
25
+ case string:
26
+ return content, nil
27
+ case []interface{}:
28
+ for _, item := range content {
29
+ if part, ok := item.(map[string]interface{}); ok {
30
+ partType, _ := part["type"].(string)
31
+ if partType == "text" {
32
+ if t, ok := part["text"].(string); ok {
33
+ text += t
34
+ }
35
+ } else if partType == "image_url" {
36
+ if imgURL, ok := part["image_url"].(map[string]interface{}); ok {
37
+ if url, ok := imgURL["url"].(string); ok {
38
+ imageURLs = append(imageURLs, url)
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ return text, imageURLs
46
+ }
47
+
48
+ // 转换为上游消息格式,支持多模态
49
+ func (m *Message) ToUpstreamMessage(urlToFileID map[string]string) map[string]interface{} {
50
+ text, imageURLs := m.ParseContent()
51
+
52
+ // 无图片,返回纯文本
53
+ if len(imageURLs) == 0 {
54
+ return map[string]interface{}{
55
+ "role": m.Role,
56
+ "content": text,
57
+ }
58
+ }
59
+
60
+ // 有图片,构建多模态内容
61
+ var content []interface{}
62
+ if text != "" {
63
+ content = append(content, map[string]interface{}{
64
+ "type": "text",
65
+ "text": text,
66
+ })
67
+ }
68
+ for _, imgURL := range imageURLs {
69
+ if fileID, ok := urlToFileID[imgURL]; ok {
70
+ content = append(content, map[string]interface{}{
71
+ "type": "image_url",
72
+ "image_url": map[string]interface{}{
73
+ "url": fileID,
74
+ },
75
+ })
76
+ }
77
+ }
78
+
79
+ return map[string]interface{}{
80
+ "role": m.Role,
81
+ "content": content,
82
+ }
83
+ }
84
+
85
+ type ChatRequest struct {
86
+ Model string `json:"model"`
87
+ Messages []Message `json:"messages"`
88
+ Stream bool `json:"stream"`
89
+ }
90
+
91
+ type ChatCompletionChunk struct {
92
+ ID string `json:"id"`
93
+ Object string `json:"object"`
94
+ Created int64 `json:"created"`
95
+ Model string `json:"model"`
96
+ Choices []Choice `json:"choices"`
97
+ }
98
+
99
+ type
100
+
101
+ type Choice struct {
102
+ Index int `json:"index"`
103
+ Delta Delta `json:"delta,omitempty"`
104
+ Message *MessageResp `json:"message,omitempty"`
105
+ FinishReason *string `json:"finish_reason"`
106
+ }
107
+
108
+ type Delta struct {
109
+ Content string `json:"content,omitempty"`
110
+ ReasoningContent string `json:"reasoning_content,omitempty"`
111
+ }
112
+
113
+ type MessageResp struct {
114
+ Role string `json:"role"`
115
+ Content string `json:"content"`
116
+ ReasoningContent string `json:"reasoning_content,omitempty"`
117
+ }
118
+
119
+ type ChatCompletionResponse struct {
120
+ ID string `json:"id"`
121
+ Object string `json:"object"`
122
+ Created int64 `json:"created"`
123
+ Model string `json:"model"`
124
+ Choices []Choice `json:"choices"`
125
+ }
126
+
127
+ type ModelsResponse struct {
128
+ Object string `json:"object"`
129
+ Data []ModelInfo `json:"data"`
130
+ }
131
+
132
+ type ModelInfo struct {
133
+ ID string `json:"id"`
134
+ Object string `json:"object"`
135
+ OwnedBy string `json:"owned_by"`
136
+ }
137
+
138
+ // SearchResult 搜索结果
139
+ type SearchResult struct {
140
+ Title string `json:"title"`
141
+ URL string `json:"url"`
142
+ Index int `json:"index"`
143
+ RefID string `json:"ref_id"`
144
+ }
145
+
146
+ // ImageSearchResult 图片搜索结果
147
+ type ImageSearchResult struct {
148
+ Title string `json:"title"`
149
+ Link string `json:"link"`
150
+ Thumbnail string `json:"thumbnail"`
151
+ }
152
+
153
+ // UpstreamData 上游返回的数据结构
154
+ type UpstreamData struct {
155
+ Type string `json:"type"`
156
+ Data struct {
157
+ DeltaContent string `json:"delta_content"`
158
+ EditContent string `json:"edit_content"`
159
+ Phase string `json:"phase"`
160
+ Done bool `json:"done"`
161
+ } `json:"data"`
162
+ }
163
+
164
+ func (u *UpstreamData) GetEditContent() string {
165
+ editContent := u.Data.EditContent
166
+ if editContent == "" {
167
+ return ""
168
+ }
169
+
170
+ if len(editContent) > 0 && editContent[0] == '"' {
171
+ var unescaped string
172
+ if err := json.Unmarshal([]byte(editContent), &unescaped); err == nil {
173
+ return unescaped
174
+ }
175
+ }
176
+
177
+ return editContent
178
+ }
internal/upstream/client.go ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package upstream
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "fmt"
7
+ "net/http"
8
+ "time"
9
+
10
+ "github.com/corpix/uarand"
11
+ "github.com/google/uuid"
12
+
13
+ "zai-proxy/internal/auth"
14
+ "zai-proxy/internal/model"
15
+ "zai-proxy/internal/version"
16
+ )
17
+
18
+ func ExtractLatestUserContent(messages []model.Message) string {
19
+ for i := len(messages) - 1; i >= 0; i-- {
20
+ if messages[i].Role == "user" {
21
+ text, _ := messages[i].ParseContent()
22
+ return text
23
+ }
24
+ }
25
+ return ""
26
+ }
27
+
28
+ func ExtractAllImageURLs(messages []model.Message) []string {
29
+ var allImageURLs []string
30
+ for _, msg := range messages {
31
+ _, imageURLs := msg.ParseContent()
32
+ allImageURLs = append(allImageURLs, imageURLs...)
33
+ }
34
+ return allImageURLs
35
+ }
36
+
37
+ func MakeUpstreamRequest(token string, messages []model.Message, modelName string) (*http.Response, string, error) {
38
+ payload, err := auth.DecodeJWTPayload(token)
39
+ if err != nil || payload == nil {
40
+ return nil, "", fmt.Errorf("invalid token")
41
+ }
42
+
43
+ userID := payload.ID
44
+ chatID := uuid.New().String()
45
+ timestamp := time.Now().UnixMilli()
46
+ requestID := uuid.New().String()
47
+ userMsgID := uuid.New().String()
48
+
49
+ targetModel := model.GetTargetModel(modelName)
50
+ latestUserContent := ExtractLatestUserContent(messages)
51
+ imageURLs := ExtractAllImageURLs(messages)
52
+
53
+ signature := auth.GenerateSignature(userID, requestID, latestUserContent, timestamp)
54
+
55
+ url := fmt.Sprintf("https://chat.z.ai/api/v2/chat/completions?timestamp=%d&requestId=%s&user_id=%s&version=0.0.1&platform=web&token=%s&current_url=%s&pathname=%s&signature_timestamp=%d",
56
+ timestamp, requestID, userID, token,
57
+ fmt.Sprintf("https://chat.z.ai/c/%s", chatID),
58
+ fmt.Sprintf("/c/%s", chatID),
59
+ timestamp)
60
+
61
+ enableThinking := model.IsThinkingModel(modelName)
62
+ autoWebSearch := model.IsSearchModel(modelName)
63
+ if targetModel == "glm-4.5v" || targetModel == "glm-4.6v" {
64
+ autoWebSearch = false
65
+ }
66
+
67
+ var mcpServers []string
68
+ if targetModel == "glm-4.6v" {
69
+ mcpServers = []string{"vlm-image-search", "vlm-image-recognition", "vlm-image-processing"}
70
+ }
71
+
72
+ urlToFileID := make(map[string]string)
73
+ var filesData []map[string]interface{}
74
+ if len(imageURLs) > 0 {
75
+ files, _ := UploadImages(token, imageURLs)
76
+ for i, f := range files {
77
+ if i < len(imageURLs) {
78
+ urlToFileID[imageURLs[i]] = f.ID
79
+ }
80
+ filesData = append(filesData, map[string]interface{}{
81
+ "type": f.Type,
82
+ "file": f.File,
83
+ "id": f.ID,
84
+ "url": f.URL,
85
+ "name": f.Name,
86
+ "status": f.Status,
87
+ "size": f.Size,
88
+ "error": f.Error,
89
+ "itemId": f.ItemID,
90
+ "media": f.Media,
91
+ "ref_user_msg_id": userMsgID,
92
+ })
93
+ }
94
+ }
95
+
96
+ var upstreamMessages []map[string]interface{}
97
+ for _, msg := range messages {
98
+ upstreamMessages = append(upstreamMessages, msg.ToUpstreamMessage(urlToFileID))
99
+ }
100
+
101
+ body := map[string]interface{}{
102
+ "stream": true,
103
+ "model": targetModel,
104
+ "messages": upstreamMessages,
105
+ "signature_prompt": latestUserContent,
106
+ "params": map[string]interface{}{},
107
+ "features": map[string]interface{}{
108
+ "image_generation": false,
109
+ "web_search": false,
110
+ "auto_web_search": autoWebSearch,
111
+ "preview_mode": true,
112
+ "enable_thinking": enableThinking,
113
+ },
114
+ "chat_id": chatID,
115
+ "id": uuid.New().String(),
116
+ }
117
+
118
+ if len(mcpServers) > 0 {
119
+ body["mcp_servers"] = mcpServers
120
+ }
121
+
122
+ if len(filesData) > 0 {
123
+ body["files"] = filesData
124
+ body["current_user_message_id"] = userMsgID
125
+ }
126
+
127
+ bodyBytes, _ := json.Marshal(body)
128
+
129
+ req, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
130
+ if err != nil {
131
+ return nil, "", err
132
+ }
133
+
134
+ req.Header.Set("Authorization", "Bearer "+token)
135
+ req.Header.Set("X-FE-Version", version.GetFeVersion())
136
+ req.Header.Set("X-Signature", signature)
137
+ req.Header.Set("Content-Type", "application/json")
138
+ req.Header.Set("Connection", "keep-alive")
139
+ req.Header.Set("Origin", "https://chat.z.ai")
140
+ req.Header.Set("Referer", fmt.Sprintf("https://chat.z.ai/c/%s", uuid.New().String()))
141
+ req.Header.Set("User-Agent", uarand.GetRandom())
142
+
143
+ client := &http.Client{}
144
+ resp, err := client.Do(req)
145
+ if err != nil {
146
+ return nil, "", err
147
+ }
148
+
149
+ return resp, targetModel, nil
150
+ }
internal/{upload.go → upstream/upload.go} RENAMED
@@ -1,4 +1,4 @@
1
- package internal
2
 
3
  import (
4
  "bytes"
@@ -12,6 +12,8 @@ import (
12
  "strings"
13
 
14
  "github.com/google/uuid"
 
 
15
  )
16
 
17
  // FileUploadResponse z.ai 文件上传响应
@@ -49,14 +51,13 @@ func UploadImageFromURL(token string, imageURL string) (*UpstreamFile, error) {
49
 
50
  if strings.HasPrefix(imageURL, "data:") {
51
  // Base64 编码的图片
52
- // 格式: data:image/jpeg;base64,/9j/4AAQ...
53
  parts := strings.SplitN(imageURL, ",", 2)
54
  if len(parts) != 2 {
55
  return nil, fmt.Errorf("invalid base64 image format")
56
  }
57
 
58
  // 解析 MIME 类型
59
- header := parts[0] // data:image/jpeg;base64
60
  if idx := strings.Index(header, ":"); idx != -1 {
61
  mimeAndEncoding := header[idx+1:]
62
  if semiIdx := strings.Index(mimeAndEncoding, ";"); semiIdx != -1 {
@@ -160,7 +161,6 @@ func UploadImageFromURL(token string, imageURL string) (*UpstreamFile, error) {
160
  return nil, fmt.Errorf("failed to parse upload response: %v", err)
161
  }
162
 
163
- // 构建上游文件格式
164
  return &UpstreamFile{
165
  Type: "image",
166
  File: uploadResp,
@@ -181,7 +181,7 @@ func UploadImages(token string, imageURLs []string) ([]*UpstreamFile, error) {
181
  for _, url := range imageURLs {
182
  file, err := UploadImageFromURL(token, url)
183
  if err != nil {
184
- LogError("Failed to upload image %s: %v", url[:min(50, len(url))], err)
185
  continue
186
  }
187
  files = append(files, file)
 
1
+ package upstream
2
 
3
  import (
4
  "bytes"
 
12
  "strings"
13
 
14
  "github.com/google/uuid"
15
+
16
+ "zai-proxy/internal/logger"
17
  )
18
 
19
  // FileUploadResponse z.ai 文件上传响应
 
51
 
52
  if strings.HasPrefix(imageURL, "data:") {
53
  // Base64 编码的图片
 
54
  parts := strings.SplitN(imageURL, ",", 2)
55
  if len(parts) != 2 {
56
  return nil, fmt.Errorf("invalid base64 image format")
57
  }
58
 
59
  // 解析 MIME 类型
60
+ header := parts[0]
61
  if idx := strings.Index(header, ":"); idx != -1 {
62
  mimeAndEncoding := header[idx+1:]
63
  if semiIdx := strings.Index(mimeAndEncoding, ";"); semiIdx != -1 {
 
161
  return nil, fmt.Errorf("failed to parse upload response: %v", err)
162
  }
163
 
 
164
  return &UpstreamFile{
165
  Type: "image",
166
  File: uploadResp,
 
181
  for _, url := range imageURLs {
182
  file, err := UploadImageFromURL(token, url)
183
  if err != nil {
184
+ logger.LogError("Failed to upload image %s: %v", url[:min(50, len(url))], err)
185
  continue
186
  }
187
  files = append(files, file)
internal/{version.go → version/version.go} RENAMED
@@ -1,4 +1,4 @@
1
- package internal
2
 
3
  import (
4
  "io"
@@ -6,6 +6,8 @@ import (
6
  "regexp"
7
  "sync"
8
  "time"
 
 
9
  )
10
 
11
  var (
@@ -22,14 +24,14 @@ func GetFeVersion() string {
22
  func fetchFeVersion() {
23
  resp, err := http.Get("https://chat.z.ai/")
24
  if err != nil {
25
- LogError("Failed to fetch fe version: %v", err)
26
  return
27
  }
28
  defer resp.Body.Close()
29
 
30
  body, err := io.ReadAll(resp.Body)
31
  if err != nil {
32
- LogError("Failed to read fe version response: %v", err)
33
  return
34
  }
35
 
@@ -39,7 +41,7 @@ func fetchFeVersion() {
39
  versionLock.Lock()
40
  feVersion = match
41
  versionLock.Unlock()
42
- LogInfo("Updated fe version: %s", match)
43
  }
44
  }
45
 
 
1
+ package version
2
 
3
  import (
4
  "io"
 
6
  "regexp"
7
  "sync"
8
  "time"
9
+
10
+ "zai-proxy/internal/logger"
11
  )
12
 
13
  var (
 
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
29
  }
30
  defer resp.Body.Close()
31
 
32
  body, err := io.ReadAll(resp.Body)
33
  if err != nil {
34
+ logger.LogError("Failed to read fe version response: %v", err)
35
  return
36
  }
37
 
 
41
  versionLock.Lock()
42
  feVersion = match
43
  versionLock.Unlock()
44
+ logger.LogInfo("Updated fe version: %s", match)
45
  }
46
  }
47
 
main.go CHANGED
@@ -3,20 +3,23 @@ package main
3
  import (
4
  "net/http"
5
 
6
- "zai-proxy/internal"
 
 
 
7
  )
8
 
9
  func main() {
10
- internal.LoadConfig()
11
- internal.InitLogger()
12
- internal.StartVersionUpdater()
13
 
14
- http.HandleFunc("/v1/models", internal.HandleModels)
15
- http.HandleFunc("/v1/chat/completions", internal.HandleChatCompletions)
16
 
17
- addr := ":" + internal.Cfg.Port
18
- internal.LogInfo("Server starting on %s", addr)
19
  if err := http.ListenAndServe(addr, nil); err != nil {
20
- internal.LogError("Server failed: %v", err)
21
  }
22
  }
 
3
  import (
4
  "net/http"
5
 
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)
18
+ http.HandleFunc("/v1/chat/completions", handler.HandleChatCompletions)
19
 
20
+ addr := ":" + config.Cfg.Port
21
+ logger.LogInfo("Server starting on %s", addr)
22
  if err := http.ListenAndServe(addr, nil); err != nil {
23
+ logger.LogError("Server failed: %v", err)
24
  }
25
  }