Spaces:
Paused
Paused
NtGdi commited on
Commit ·
106464e
1
Parent(s): c9a9c69
feat: support GLM-4.6-V
Browse files- README.md +1 -0
- internal/chat.go +271 -97
- 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 |
-
|
| 64 |
-
if targetModel == "glm-4.5v" {
|
| 65 |
autoWebSearch = false
|
| 66 |
}
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 328 |
-
if
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
if reasoningContent != "" {
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 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 |
-
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
searchRefFilter.AddSearchResults(results)
|
| 373 |
pendingSourcesMarkdown = searchRefFilter.GetSearchResultsMarkdown()
|
| 374 |
}
|
| 375 |
continue
|
| 376 |
}
|
| 377 |
-
|
| 378 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 408 |
-
|
|
|
|
| 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:
|
| 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" &&
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
if idx := strings.Index(
|
| 434 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
}
|
| 436 |
}
|
| 437 |
-
} else if (upstream.Data.Phase == "other" || upstream.Data.Phase == "tool_call") &&
|
| 438 |
-
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 571 |
-
|
| 572 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 583 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
searchRefFilter.AddSearchResults(results)
|
| 585 |
pendingSourcesMarkdown = searchRefFilter.GetSearchResultsMarkdown()
|
| 586 |
}
|
| 587 |
continue
|
| 588 |
}
|
| 589 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 590 |
continue
|
| 591 |
}
|
| 592 |
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
|
|
|
|
|
|
|
|
|
| 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" &&
|
| 603 |
-
if strings.Contains(
|
| 604 |
-
reasoningContent := thinkingFilter.
|
| 605 |
if reasoningContent != "" {
|
| 606 |
reasoningChunks = append(reasoningChunks, reasoningContent)
|
| 607 |
}
|
| 608 |
-
|
| 609 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 610 |
}
|
| 611 |
}
|
| 612 |
-
} else if (upstream.Data.Phase == "other" || upstream.Data.Phase == "tool_call") &&
|
| 613 |
-
content =
|
| 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
|
| 193 |
-
Data []ModelInfo
|
| 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", 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 |
+
}
|