NtGdi commited on
Commit
106464e
·
1 Parent(s): c9a9c69

feat: support GLM-4.6-V

Browse files
Files changed (3) hide show
  1. README.md +1 -0
  2. internal/chat.go +271 -97
  3. internal/models.go +144 -10
README.md CHANGED
@@ -77,6 +77,7 @@ curl http://localhost:8000/v1/chat/completions \
77
  | GLM-4.5 | 0727-360B-API |
78
  | GLM-4.6 | GLM-4-6-API-V1 |
79
  | GLM-4.5-V | glm-4.5v |
 
80
  | GLM-4.5-Air | 0727-106B-API |
81
 
82
  ### 模型标签
 
77
  | GLM-4.5 | 0727-360B-API |
78
  | GLM-4.6 | GLM-4-6-API-V1 |
79
  | GLM-4.5-V | glm-4.5v |
80
+ | GLM-4.6-V | glm-4.6v |
81
  | GLM-4.5-Air | 0727-106B-API |
82
 
83
  ### 模型标签
internal/chat.go CHANGED
@@ -24,7 +24,6 @@ func extractLatestUserContent(messages []Message) string {
24
  return ""
25
  }
26
 
27
- // 提取所有消息中的图片URL
28
  func extractAllImageURLs(messages []Message) []string {
29
  var allImageURLs []string
30
  for _, msg := range messages {
@@ -60,12 +59,15 @@ func makeUpstreamRequest(token string, messages []Message, model string) (*http.
60
 
61
  enableThinking := IsThinkingModel(model)
62
  autoWebSearch := IsSearchModel(model)
63
- // GLM-4.5-V 不支持 auto_web_search
64
- if targetModel == "glm-4.5v" {
65
  autoWebSearch = false
66
  }
67
 
68
- // 上传图片并建立URL→FileID映射
 
 
 
 
69
  urlToFileID := make(map[string]string)
70
  var filesData []map[string]interface{}
71
  if len(imageURLs) > 0 {
@@ -90,7 +92,6 @@ func makeUpstreamRequest(token string, messages []Message, model string) (*http.
90
  }
91
  }
92
 
93
- // 转换消息为上游格式
94
  var upstreamMessages []map[string]interface{}
95
  for _, msg := range messages {
96
  upstreamMessages = append(upstreamMessages, msg.ToUpstreamMessage(urlToFileID))
@@ -113,7 +114,10 @@ func makeUpstreamRequest(token string, messages []Message, model string) (*http.
113
  "id": uuid.New().String(),
114
  }
115
 
116
- // 添加files字段
 
 
 
117
  if len(filesData) > 0 {
118
  body["files"] = filesData
119
  body["current_user_message_id"] = userMsgID
@@ -135,9 +139,6 @@ func makeUpstreamRequest(token string, messages []Message, model string) (*http.
135
  req.Header.Set("Referer", fmt.Sprintf("https://chat.z.ai/c/%s", uuid.New().String()))
136
  req.Header.Set("User-Agent", uarand.GetRandom())
137
 
138
- // LogDebug("[Request] URL: %s", url)
139
- // LogDebug("[Request] Headers: %v", req.Header)
140
-
141
  client := &http.Client{}
142
  resp, err := client.Do(req)
143
  if err != nil {
@@ -157,19 +158,34 @@ type UpstreamData struct {
157
  } `json:"data"`
158
  }
159
 
160
- // 思考内容过滤器状态
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  type ThinkingFilter struct {
162
  hasSeenFirstThinking bool
163
  buffer string
 
 
 
164
  }
165
 
166
- // 处理思考阶段的内容
167
- // 第一个 delta_content 包含 <details...>\n<summary>Thinking…</summary>\n> 前缀,需要过滤
168
- // 后续 delta_content 需要替换 "\n> " 为 "\n"(跨块累积处理)
169
  func (f *ThinkingFilter) ProcessThinking(deltaContent string) string {
170
  if !f.hasSeenFirstThinking {
171
  f.hasSeenFirstThinking = true
172
- // 第一个 thinking 内容,查找 "> " 之后的内容
173
  if idx := strings.Index(deltaContent, "> "); idx != -1 {
174
  deltaContent = deltaContent[idx+2:]
175
  } else {
@@ -177,15 +193,11 @@ func (f *ThinkingFilter) ProcessThinking(deltaContent string) string {
177
  }
178
  }
179
 
180
- // 合并缓冲区内容
181
  content := f.buffer + deltaContent
182
  f.buffer = ""
183
 
184
- // 替换完整的 "\n> " 为 "\n"
185
  content = strings.ReplaceAll(content, "\n> ", "\n")
186
 
187
- // 检查末尾是否有可能是 "\n> " 的前缀
188
- // 可能的前缀:"\n", "\n>"
189
  if strings.HasSuffix(content, "\n>") {
190
  f.buffer = "\n>"
191
  return content[:len(content)-2]
@@ -198,17 +210,13 @@ func (f *ThinkingFilter) ProcessThinking(deltaContent string) string {
198
  return content
199
  }
200
 
201
- // Flush 返回缓冲区中剩余的内容
202
  func (f *ThinkingFilter) Flush() string {
203
  result := f.buffer
204
  f.buffer = ""
205
  return result
206
  }
207
 
208
- // 从 answer 阶段的 edit_content 中提取完整思考内容
209
- // 格式:true" duration="0" ...>\n<summary>Thought for 0 seconds</summary>\n> 完整思考内容\n</details>\n你好
210
  func (f *ThinkingFilter) ExtractCompleteThinking(editContent string) string {
211
- // 查找 "> " 到 "</details>" 之间的内容
212
  startIdx := strings.Index(editContent, "> ")
213
  if startIdx == -1 {
214
  return ""
@@ -221,11 +229,34 @@ func (f *ThinkingFilter) ExtractCompleteThinking(editContent string) string {
221
  }
222
 
223
  content := editContent[startIdx:endIdx]
224
- // 替换 "\n> " 为 "\n"
225
  content = strings.ReplaceAll(content, "\n> ", "\n")
226
  return content
227
  }
228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  func HandleChatCompletions(w http.ResponseWriter, r *http.Request) {
230
  token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
231
  if token == "" {
@@ -233,7 +264,6 @@ func HandleChatCompletions(w http.ResponseWriter, r *http.Request) {
233
  return
234
  }
235
 
236
- // 如果 token 是 "free",获取匿名 token
237
  if token == "free" {
238
  anonymousToken, err := GetAnonymousToken()
239
  if err != nil {
@@ -299,6 +329,8 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
299
  searchRefFilter := NewSearchRefFilter()
300
  thinkingFilter := &ThinkingFilter{}
301
  pendingSourcesMarkdown := ""
 
 
302
 
303
  for scanner.Scan() {
304
  line := scanner.Text()
@@ -322,64 +354,113 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
322
  break
323
  }
324
 
325
- // 处理思考阶段的增量内容
326
  if upstream.Data.Phase == "thinking" && upstream.Data.DeltaContent != "" {
327
- // 如果有待输出的搜索结果,先输出到 reasoning
328
- if pendingSourcesMarkdown != "" {
329
- hasContent = true
330
- chunk := ChatCompletionChunk{
331
- ID: completionID,
332
- Object: "chat.completion.chunk",
333
- Created: time.Now().Unix(),
334
- Model: modelName,
335
- Choices: []Choice{{
336
- Index: 0,
337
- Delta: Delta{ReasoningContent: pendingSourcesMarkdown},
338
- FinishReason: nil,
339
- }},
340
- }
341
- data, _ := json.Marshal(chunk)
342
- fmt.Fprintf(w, "data: %s\n\n", data)
343
- flusher.Flush()
344
- pendingSourcesMarkdown = ""
345
  }
 
346
 
347
  reasoningContent := thinkingFilter.ProcessThinking(upstream.Data.DeltaContent)
348
- reasoningContent = searchRefFilter.Process(reasoningContent)
 
 
 
 
349
  if reasoningContent != "" {
350
- hasContent = true
351
- chunk := ChatCompletionChunk{
352
- ID: completionID,
353
- Object: "chat.completion.chunk",
354
- Created: time.Now().Unix(),
355
- Model: modelName,
356
- Choices: []Choice{{
357
- Index: 0,
358
- Delta: Delta{ReasoningContent: reasoningContent},
359
- FinishReason: nil,
360
- }},
 
 
 
 
 
 
 
 
361
  }
362
- data, _ := json.Marshal(chunk)
363
- fmt.Fprintf(w, "data: %s\n\n", data)
364
- flusher.Flush()
365
  }
366
  continue
367
  }
368
 
369
- // 解析搜索结果,暂存等待下一个流决定放在哪里
370
- if upstream.Data.EditContent != "" && IsSearchResultContent(upstream.Data.EditContent) {
371
- if results := ParseSearchResults(upstream.Data.EditContent); len(results) > 0 {
 
 
 
 
372
  searchRefFilter.AddSearchResults(results)
373
  pendingSourcesMarkdown = searchRefFilter.GetSearchResultsMarkdown()
374
  }
375
  continue
376
  }
377
- // 跳过搜索工具调用
378
- if upstream.Data.EditContent != "" && IsSearchToolCall(upstream.Data.EditContent, upstream.Data.Phase) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  continue
380
  }
381
 
382
- // 进入 answer 阶段,如果有待输出的搜索结果,先输出到 content
383
  if pendingSourcesMarkdown != "" {
384
  hasContent = true
385
  chunk := ChatCompletionChunk{
@@ -398,14 +479,32 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
398
  flusher.Flush()
399
  pendingSourcesMarkdown = ""
400
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
 
402
  content := ""
403
  reasoningContent := ""
404
 
405
- // 先输出 thinking 缓冲区剩余内容
406
  if thinkingRemaining := thinkingFilter.Flush(); thinkingRemaining != "" {
407
- thinkingRemaining = searchRefFilter.Process(thinkingRemaining) + searchRefFilter.Flush()
408
- if thinkingRemaining != "" {
 
409
  hasContent = true
410
  chunk := ChatCompletionChunk{
411
  ID: completionID,
@@ -414,7 +513,7 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
414
  Model: modelName,
415
  Choices: []Choice{{
416
  Index: 0,
417
- Delta: Delta{ReasoningContent: thinkingRemaining},
418
  FinishReason: nil,
419
  }},
420
  }
@@ -424,22 +523,53 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
424
  }
425
  }
426
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  if upstream.Data.Phase == "answer" && upstream.Data.DeltaContent != "" {
428
  content = upstream.Data.DeltaContent
429
- } else if upstream.Data.Phase == "answer" && upstream.Data.EditContent != "" {
430
- // 思考模型首次 answer:提取完整思考内容 + 正常回复开头
431
- if strings.Contains(upstream.Data.EditContent, "</details>") {
432
- reasoningContent = thinkingFilter.ExtractCompleteThinking(upstream.Data.EditContent)
433
- if idx := strings.Index(upstream.Data.EditContent, "</details>\n"); idx != -1 {
434
- content = upstream.Data.EditContent[idx+len("</details>\n"):]
 
 
 
 
 
 
435
  }
436
  }
437
- } else if (upstream.Data.Phase == "other" || upstream.Data.Phase == "tool_call") && upstream.Data.EditContent != "" {
438
- // other: 普通最后一个 token; tool_call: 搜索模式最后一个 token
439
- content = upstream.Data.EditContent
 
 
 
 
 
 
 
440
  }
441
 
442
- // 输出完整思考内容(如果有)
443
  if reasoningContent != "" {
444
  reasoningContent = searchRefFilter.Process(reasoningContent) + searchRefFilter.Flush()
445
  }
@@ -465,13 +595,16 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
465
  continue
466
  }
467
 
468
- // 过滤搜索引用标记(跨流累积处理)
469
  content = searchRefFilter.Process(content)
470
  if content == "" {
471
  continue
472
  }
473
 
474
  hasContent = true
 
 
 
 
475
  chunk := ChatCompletionChunk{
476
  ID: completionID,
477
  Object: "chat.completion.chunk",
@@ -493,7 +626,6 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
493
  LogError("[Upstream] scanner error: %v", err)
494
  }
495
 
496
- // 输出过滤器中剩余的内容(非引用标记的部分)
497
  if remaining := searchRefFilter.Flush(); remaining != "" {
498
  hasContent = true
499
  chunk := ChatCompletionChunk{
@@ -516,7 +648,6 @@ func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionI
516
  LogError("Stream response 200 but no content received")
517
  }
518
 
519
- // Final chunk
520
  stopReason := "stop"
521
  finalChunk := ChatCompletionChunk{
522
  ID: completionID,
@@ -545,6 +676,7 @@ func handleNonStreamResponse(w http.ResponseWriter, body io.ReadCloser, completi
545
  searchRefFilter := NewSearchRefFilter()
546
  hasThinking := false
547
  pendingSourcesMarkdown := ""
 
548
 
549
  for scanner.Scan() {
550
  line := scanner.Text()
@@ -567,50 +699,92 @@ func handleNonStreamResponse(w http.ResponseWriter, body io.ReadCloser, completi
567
  }
568
 
569
  if upstream.Data.Phase == "thinking" && upstream.Data.DeltaContent != "" {
570
- if pendingSourcesMarkdown != "" {
571
- reasoningChunks = append(reasoningChunks, pendingSourcesMarkdown)
572
- pendingSourcesMarkdown = ""
 
 
 
573
  }
 
 
574
  hasThinking = true
575
  reasoningContent := thinkingFilter.ProcessThinking(upstream.Data.DeltaContent)
576
  if reasoningContent != "" {
 
577
  reasoningChunks = append(reasoningChunks, reasoningContent)
578
  }
579
  continue
580
  }
581
 
582
- if upstream.Data.EditContent != "" && IsSearchResultContent(upstream.Data.EditContent) {
583
- if results := ParseSearchResults(upstream.Data.EditContent); len(results) > 0 {
 
 
 
 
 
584
  searchRefFilter.AddSearchResults(results)
585
  pendingSourcesMarkdown = searchRefFilter.GetSearchResultsMarkdown()
586
  }
587
  continue
588
  }
589
- if upstream.Data.EditContent != "" && IsSearchToolCall(upstream.Data.EditContent, upstream.Data.Phase) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
590
  continue
591
  }
592
 
593
- // 进入 answer 阶段,把待输出的搜索结果放到 content
594
- if pendingSourcesMarkdown != "" && !hasThinking {
595
- chunks = append(chunks, pendingSourcesMarkdown)
 
 
 
596
  pendingSourcesMarkdown = ""
597
  }
 
 
 
 
598
 
599
  content := ""
600
  if upstream.Data.Phase == "answer" && upstream.Data.DeltaContent != "" {
601
  content = upstream.Data.DeltaContent
602
- } else if upstream.Data.Phase == "answer" && upstream.Data.EditContent != "" {
603
- if strings.Contains(upstream.Data.EditContent, "</details>") {
604
- reasoningContent := thinkingFilter.ExtractCompleteThinking(upstream.Data.EditContent)
605
  if reasoningContent != "" {
606
  reasoningChunks = append(reasoningChunks, reasoningContent)
607
  }
608
- if idx := strings.Index(upstream.Data.EditContent, "</details>\n"); idx != -1 {
609
- content = upstream.Data.EditContent[idx+len("</details>\n"):]
 
 
 
 
 
 
610
  }
611
  }
612
- } else if (upstream.Data.Phase == "other" || upstream.Data.Phase == "tool_call") && upstream.Data.EditContent != "" {
613
- content = upstream.Data.EditContent
614
  }
615
 
616
  if content != "" {
 
24
  return ""
25
  }
26
 
 
27
  func extractAllImageURLs(messages []Message) []string {
28
  var allImageURLs []string
29
  for _, msg := range messages {
 
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 {
 
92
  }
93
  }
94
 
 
95
  var upstreamMessages []map[string]interface{}
96
  for _, msg := range messages {
97
  upstreamMessages = append(upstreamMessages, msg.ToUpstreamMessage(urlToFileID))
 
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
 
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 {
 
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 {
 
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]
 
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 ""
 
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 ")
262
  if token == "" {
 
264
  return
265
  }
266
 
 
267
  if token == "free" {
268
  anonymousToken, err := GetAnonymousToken()
269
  if err != nil {
 
329
  searchRefFilter := NewSearchRefFilter()
330
  thinkingFilter := &ThinkingFilter{}
331
  pendingSourcesMarkdown := ""
332
+ pendingImageSearchMarkdown := ""
333
+ totalContentOutputLength := 0 // 记录已输出的 content 字符长度
334
 
335
  for scanner.Scan() {
336
  line := scanner.Text()
 
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
+ }
389
+ data, _ := json.Marshal(chunk)
390
+ fmt.Fprintf(w, "data: %s\n\n", data)
391
+ flusher.Flush()
392
  }
 
 
 
393
  }
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
+ }
426
+ data, _ := json.Marshal(chunk)
427
+ fmt.Fprintf(w, "data: %s\n\n", data)
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
+ }
453
+ data, _ := json.Marshal(chunk)
454
+ fmt.Fprintf(w, "data: %s\n\n", data)
455
+ flusher.Flush()
456
+ }
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{
 
479
  flusher.Flush()
480
  pendingSourcesMarkdown = ""
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
+ }
495
+ data, _ := json.Marshal(chunk)
496
+ fmt.Fprintf(w, "data: %s\n\n", data)
497
+ flusher.Flush()
498
+ pendingImageSearchMarkdown = ""
499
+ }
500
 
501
  content := ""
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,
 
513
  Model: modelName,
514
  Choices: []Choice{{
515
  Index: 0,
516
+ Delta: Delta{ReasoningContent: processedRemaining},
517
  FinishReason: nil,
518
  }},
519
  }
 
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
+ }
539
+ data, _ := json.Marshal(chunk)
540
+ fmt.Fprintf(w, "data: %s\n\n", data)
541
+ flusher.Flush()
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
+
551
+ if idx := strings.Index(editContent, "</details>"); idx != -1 {
552
+ afterDetails := editContent[idx+len("</details>"):]
553
+ if strings.HasPrefix(afterDetails, "\n") {
554
+ content = afterDetails[1:]
555
+ } else {
556
+ content = afterDetails
557
+ }
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
+
565
+ if len(fullContentRunes) > totalContentOutputLength {
566
+ content = string(fullContentRunes[totalContentOutputLength:])
567
+ totalContentOutputLength = len(fullContentRunes)
568
+ } else {
569
+ content = fullContent
570
+ }
571
  }
572
 
 
573
  if reasoningContent != "" {
574
  reasoningContent = searchRefFilter.Process(reasoningContent) + searchRefFilter.Flush()
575
  }
 
595
  continue
596
  }
597
 
 
598
  content = searchRefFilter.Process(content)
599
  if content == "" {
600
  continue
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",
 
626
  LogError("[Upstream] scanner error: %v", err)
627
  }
628
 
 
629
  if remaining := searchRefFilter.Flush(); remaining != "" {
630
  hasContent = true
631
  chunk := ChatCompletionChunk{
 
648
  LogError("Stream response 200 but no content received")
649
  }
650
 
 
651
  stopReason := "stop"
652
  finalChunk := ChatCompletionChunk{
653
  ID: completionID,
 
676
  searchRefFilter := NewSearchRefFilter()
677
  hasThinking := false
678
  pendingSourcesMarkdown := ""
679
+ pendingImageSearchMarkdown := ""
680
 
681
  for scanner.Scan() {
682
  line := scanner.Text()
 
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
 
754
+ if pendingSourcesMarkdown != "" {
755
+ if hasThinking {
756
+ reasoningChunks = append(reasoningChunks, pendingSourcesMarkdown)
757
+ } else {
758
+ chunks = append(chunks, pendingSourcesMarkdown)
759
+ }
760
  pendingSourcesMarkdown = ""
761
  }
762
+ if pendingImageSearchMarkdown != "" {
763
+ chunks = append(chunks, pendingImageSearchMarkdown)
764
+ pendingImageSearchMarkdown = ""
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 != "" {
774
  reasoningChunks = append(reasoningChunks, reasoningContent)
775
  }
776
+
777
+ if idx := strings.Index(editContent, "</details>"); idx != -1 {
778
+ afterDetails := editContent[idx+len("</details>"):]
779
+ if strings.HasPrefix(afterDetails, "\n") {
780
+ content = afterDetails[1:]
781
+ } else {
782
+ content = afterDetails
783
+ }
784
  }
785
  }
786
+ } else if (upstream.Data.Phase == "other" || upstream.Data.Phase == "tool_call") && editContent != "" {
787
+ content = editContent
788
  }
789
 
790
  if content != "" {
internal/models.go CHANGED
@@ -12,6 +12,7 @@ var BaseModelMapping = map[string]string{
12
  "GLM-4.5": "0727-360B-API",
13
  "GLM-4.6": "GLM-4-6-API-V1",
14
  "GLM-4.5-V": "glm-4.5v",
 
15
  "GLM-4.5-Air": "0727-106B-API",
16
  "0808-360B-DR": "0808-360B-DR",
17
  }
@@ -23,6 +24,8 @@ var ModelList = []string{
23
  "GLM-4.5-thinking",
24
  "GLM-4.6-thinking",
25
  "GLM-4.5-V",
 
 
26
  "GLM-4.5-Air",
27
  "0808-360B-DR",
28
  }
@@ -189,8 +192,8 @@ type ChatCompletionResponse struct {
189
  }
190
 
191
  type ModelsResponse struct {
192
- Object string `json:"object"`
193
- Data []ModelInfo `json:"data"`
194
  }
195
 
196
  type ModelInfo struct {
@@ -233,7 +236,6 @@ func escapeMarkdownTitle(title string) string {
233
  return title
234
  }
235
 
236
- // Process 将搜索引用转换为 markdown 链接,末尾可能的不完整引用暂存
237
  func (f *SearchRefFilter) Process(content string) string {
238
  content = f.buffer + content
239
  f.buffer = ""
@@ -313,21 +315,17 @@ func (f *SearchRefFilter) GetSearchResultsMarkdown() string {
313
  return sb.String()
314
  }
315
 
316
- // 检查是否为搜索结果内容(需要跳过)
317
  func IsSearchResultContent(editContent string) bool {
318
  return strings.Contains(editContent, `"search_result"`)
319
  }
320
 
321
- // ParseSearchResults 从 edit_content 中解析搜索结果
322
  func ParseSearchResults(editContent string) []SearchResult {
323
- // 查找 "search_result": 的位置
324
  searchResultKey := `"search_result":`
325
  idx := strings.Index(editContent, searchResultKey)
326
  if idx == -1 {
327
  return nil
328
  }
329
 
330
- // 找到 [ 开始的位置
331
  startIdx := idx + len(searchResultKey)
332
  for startIdx < len(editContent) && editContent[startIdx] != '[' {
333
  startIdx++
@@ -336,7 +334,6 @@ func ParseSearchResults(editContent string) []SearchResult {
336
  return nil
337
  }
338
 
339
- // 找到匹配的 ] 结束位置
340
  bracketCount := 0
341
  endIdx := startIdx
342
  for endIdx < len(editContent) {
@@ -356,7 +353,6 @@ func ParseSearchResults(editContent string) []SearchResult {
356
  return nil
357
  }
358
 
359
- // 解析 JSON 数组
360
  jsonStr := editContent[startIdx:endIdx]
361
  var rawResults []struct {
362
  Title string `json:"title"`
@@ -382,7 +378,6 @@ func ParseSearchResults(editContent string) []SearchResult {
382
  return results
383
  }
384
 
385
- // 检查是否为搜索工具调用内容(需要跳过)
386
  func IsSearchToolCall(editContent string, phase string) bool {
387
  if phase != "tool_call" {
388
  return false
@@ -390,3 +385,142 @@ func IsSearchToolCall(editContent string, phase string) bool {
390
  // tool_call 阶段包含 mcp 相关内容的都跳过
391
  return strings.Contains(editContent, `"mcp"`) || strings.Contains(editContent, `mcp-server`)
392
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  "GLM-4.5": "0727-360B-API",
13
  "GLM-4.6": "GLM-4-6-API-V1",
14
  "GLM-4.5-V": "glm-4.5v",
15
+ "GLM-4.6-V": "glm-4.6v",
16
  "GLM-4.5-Air": "0727-106B-API",
17
  "0808-360B-DR": "0808-360B-DR",
18
  }
 
24
  "GLM-4.5-thinking",
25
  "GLM-4.6-thinking",
26
  "GLM-4.5-V",
27
+ "GLM-4.6-V",
28
+ "GLM-4.6-V-thinking",
29
  "GLM-4.5-Air",
30
  "0808-360B-DR",
31
  }
 
192
  }
193
 
194
  type ModelsResponse struct {
195
+ Object string `json:"object"`
196
+ Data []ModelInfo `json:"data"`
197
  }
198
 
199
  type ModelInfo struct {
 
236
  return title
237
  }
238
 
 
239
  func (f *SearchRefFilter) Process(content string) string {
240
  content = f.buffer + content
241
  f.buffer = ""
 
315
  return sb.String()
316
  }
317
 
 
318
  func IsSearchResultContent(editContent string) bool {
319
  return strings.Contains(editContent, `"search_result"`)
320
  }
321
 
 
322
  func ParseSearchResults(editContent string) []SearchResult {
 
323
  searchResultKey := `"search_result":`
324
  idx := strings.Index(editContent, searchResultKey)
325
  if idx == -1 {
326
  return nil
327
  }
328
 
 
329
  startIdx := idx + len(searchResultKey)
330
  for startIdx < len(editContent) && editContent[startIdx] != '[' {
331
  startIdx++
 
334
  return nil
335
  }
336
 
 
337
  bracketCount := 0
338
  endIdx := startIdx
339
  for endIdx < len(editContent) {
 
353
  return nil
354
  }
355
 
 
356
  jsonStr := editContent[startIdx:endIdx]
357
  var rawResults []struct {
358
  Title string `json:"title"`
 
378
  return results
379
  }
380
 
 
381
  func IsSearchToolCall(editContent string, phase string) bool {
382
  if phase != "tool_call" {
383
  return false
 
385
  // tool_call 阶段包含 mcp 相关内容的都跳过
386
  return strings.Contains(editContent, `"mcp"`) || strings.Contains(editContent, `mcp-server`)
387
  }
388
+
389
+ type ImageSearchResult struct {
390
+ Title string `json:"title"`
391
+ Link string `json:"link"`
392
+ Thumbnail string `json:"thumbnail"`
393
+ }
394
+
395
+ func ParseImageSearchResults(editContent string) []ImageSearchResult {
396
+ resultKey := `"result":`
397
+ idx := strings.Index(editContent, resultKey)
398
+ if idx == -1 {
399
+ return nil
400
+ }
401
+
402
+ startIdx := idx + len(resultKey)
403
+ for startIdx < len(editContent) && editContent[startIdx] != '[' {
404
+ startIdx++
405
+ }
406
+ if startIdx >= len(editContent) {
407
+ return nil
408
+ }
409
+
410
+ bracketCount := 0
411
+ endIdx := startIdx
412
+ inString := false
413
+ escapeNext := false
414
+ for endIdx < len(editContent) {
415
+ ch := editContent[endIdx]
416
+
417
+ if escapeNext {
418
+ escapeNext = false
419
+ endIdx++
420
+ continue
421
+ }
422
+
423
+ if ch == '\\' {
424
+ escapeNext = true
425
+ endIdx++
426
+ continue
427
+ }
428
+
429
+ if ch == '"' {
430
+ inString = !inString
431
+ }
432
+
433
+ if !inString {
434
+ if ch == '[' || ch == '{' {
435
+ bracketCount++
436
+ } else if ch == ']' || ch == '}' {
437
+ bracketCount--
438
+ if bracketCount == 0 && ch == ']' {
439
+ endIdx++
440
+ break
441
+ }
442
+ }
443
+ }
444
+ endIdx++
445
+ }
446
+
447
+ if bracketCount != 0 {
448
+ return nil
449
+ }
450
+
451
+ jsonStr := editContent[startIdx:endIdx]
452
+
453
+ var rawResults []map[string]interface{}
454
+ if err := json.Unmarshal([]byte(jsonStr), &rawResults); err != nil {
455
+ return nil
456
+ }
457
+
458
+ var results []ImageSearchResult
459
+ for _, item := range rawResults {
460
+ if itemType, ok := item["type"].(string); ok && itemType == "text" {
461
+ if text, ok := item["text"].(string); ok {
462
+ result := parseImageSearchText(text)
463
+ if result.Title != "" && result.Link != "" {
464
+ results = append(results, result)
465
+ }
466
+ }
467
+ }
468
+ }
469
+
470
+ return results
471
+ }
472
+
473
+ func parseImageSearchText(text string) ImageSearchResult {
474
+ result := ImageSearchResult{}
475
+
476
+ if titleIdx := strings.Index(text, "Title: "); titleIdx != -1 {
477
+ titleStart := titleIdx + len("Title: ")
478
+ titleEnd := strings.Index(text[titleStart:], ";")
479
+ if titleEnd != -1 {
480
+ result.Title = strings.TrimSpace(text[titleStart : titleStart+titleEnd])
481
+ }
482
+ }
483
+
484
+ if linkIdx := strings.Index(text, "Link: "); linkIdx != -1 {
485
+ linkStart := linkIdx + len("Link: ")
486
+ linkEnd := strings.Index(text[linkStart:], ";")
487
+ if linkEnd != -1 {
488
+ result.Link = strings.TrimSpace(text[linkStart : linkStart+linkEnd])
489
+ } else {
490
+ result.Link = strings.TrimSpace(text[linkStart:])
491
+ }
492
+ }
493
+
494
+ if thumbnailIdx := strings.Index(text, "Thumbnail: "); thumbnailIdx != -1 {
495
+ thumbnailStart := thumbnailIdx + len("Thumbnail: ")
496
+ result.Thumbnail = strings.TrimSpace(text[thumbnailStart:])
497
+ }
498
+
499
+ return result
500
+ }
501
+
502
+ func FormatImageSearchResults(results []ImageSearchResult) string {
503
+ if len(results) == 0 {
504
+ return ""
505
+ }
506
+
507
+ var sb strings.Builder
508
+ for _, r := range results {
509
+ escapedTitle := strings.ReplaceAll(r.Title, `[`, `\[`)
510
+ escapedTitle = strings.ReplaceAll(escapedTitle, `]`, `\]`)
511
+ sb.WriteString(fmt.Sprintf("\n![%s](%s)", escapedTitle, r.Link))
512
+ }
513
+ sb.WriteString("\n")
514
+ return sb.String()
515
+ }
516
+
517
+ func ExtractTextBeforeGlmBlock(editContent string) string {
518
+ if idx := strings.Index(editContent, "<glm_block"); idx != -1 {
519
+ text := editContent[:idx]
520
+ if strings.HasSuffix(text, "\n") {
521
+ text = text[:len(text)-1]
522
+ }
523
+ return text
524
+ }
525
+ return ""
526
+ }