package internal import ( "bufio" "bytes" "encoding/json" "fmt" "io" "math/rand" "net/http" "net/url" "strconv" "strings" "time" fhttp "github.com/bogdanfinn/fhttp" "github.com/google/uuid" ) // generateRandomIP 生成随机 IP 地址用于 X-Forwarded-For func generateRandomIP() string { // 生成看起来合理的公网 IP // 避免保留地址段:10.x, 172.16-31.x, 192.168.x, 127.x firstOctet := []int{36, 42, 58, 60, 61, 101, 106, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 139, 140, 144, 150, 153, 157, 163, 171, 175, 180, 182, 183, 202, 210, 211, 218, 219, 220, 221, 222, 223} first := firstOctet[rand.Intn(len(firstOctet))] return fmt.Sprintf("%d.%d.%d.%d", first, rand.Intn(256), rand.Intn(256), rand.Intn(254)+1) } // APIError OpenAI 兼容的错误格式 type APIError struct { Message string `json:"message"` Type string `json:"type"` Code string `json:"code,omitempty"` } // ErrorResponse 错误响应结构 type ErrorResponse struct { Error APIError `json:"error"` } // 错误类型常量 const ( ErrTypeInvalidRequest = "invalid_request_error" ErrTypeAuthentication = "authentication_error" ErrTypeNotFound = "not_found_error" ErrTypeServer = "server_error" ErrTypeUpstream = "upstream_error" ) // writeError 写入错误响应 func writeError(w http.ResponseWriter, statusCode int, errType, message, code string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(ErrorResponse{ Error: APIError{ Message: message, Type: errType, Code: code, }, }) } // writeErrorResponse 统一错误响应(通用请求失败) func writeErrorResponse(w http.ResponseWriter, statusCode int) { writeError(w, statusCode, ErrTypeServer, "请求失败", "") } // writeInvalidRequestError 无效请求错误 func writeInvalidRequestError(w http.ResponseWriter, message string) { writeError(w, http.StatusBadRequest, ErrTypeInvalidRequest, message, "invalid_request") } // writeModelNotFoundError 模型不存在错误 func writeModelNotFoundError(w http.ResponseWriter, model string) { writeError(w, http.StatusNotFound, ErrTypeNotFound, fmt.Sprintf("模型 '%s' 不存在", model), "model_not_found") } // writeUpstreamError 上游错误(透传) func writeUpstreamError(w http.ResponseWriter, statusCode int, upstreamBody []byte) { // 尝试解析上游错误 var upstreamErr struct { Error struct { Message string `json:"message"` Type string `json:"type"` Code string `json:"code"` } `json:"error"` Message string `json:"message"` Msg string `json:"msg"` } message := "请求失败" if err := json.Unmarshal(upstreamBody, &upstreamErr); err == nil { if upstreamErr.Error.Message != "" { message = upstreamErr.Error.Message } else if upstreamErr.Message != "" { message = upstreamErr.Message } else if upstreamErr.Msg != "" { message = upstreamErr.Msg } } writeError(w, statusCode, ErrTypeUpstream, message, "upstream_error") } func extractLatestUserContent(messages []Message) string { for i := len(messages) - 1; i >= 0; i-- { if messages[i].Role == "user" { text, _ := messages[i].ParseContent() return text } } return "" } func extractAllMediaURLs(messages []Message) (imageURLs, videoURLs []string) { for _, msg := range messages { _, imgs, vids := msg.ParseContentFull() imageURLs = append(imageURLs, imgs...) videoURLs = append(videoURLs, vids...) } return imageURLs, videoURLs } func buildBrowserFingerprintQuery(timestamp int64, requestID, userID, token, chatID string) string { currentURL := fmt.Sprintf("https://chat.z.ai/c/%s", chatID) pathname := fmt.Sprintf("/c/%s", chatID) localTime := time.UnixMilli(timestamp).UTC().Format("2006-01-02T15:04:05.000Z") utcTime := time.UnixMilli(timestamp).UTC().Format(time.RFC1123) q := url.Values{} q.Set("timestamp", strconv.FormatInt(timestamp, 10)) q.Set("requestId", requestID) q.Set("user_id", userID) q.Set("version", "0.0.1") q.Set("platform", "web") q.Set("token", token) q.Set("user_agent", BrowserUserAgent) q.Set("language", "en-US") q.Set("languages", "en-US,en") q.Set("timezone", "Asia/Shanghai") q.Set("cookie_enabled", "true") q.Set("screen_width", "1920") q.Set("screen_height", "1080") q.Set("screen_resolution", "1920x1080") q.Set("viewport_height", "929") q.Set("viewport_width", "1920") q.Set("viewport_size", "1920x929") q.Set("color_depth", "24") q.Set("pixel_ratio", "1") q.Set("current_url", currentURL) q.Set("pathname", pathname) q.Set("search", "") q.Set("hash", "") q.Set("host", "chat.z.ai") q.Set("hostname", "chat.z.ai") q.Set("protocol", "https:") q.Set("referrer", "") q.Set("title", "Z.ai - Free AI Chatbot & Agent powered by GLM-5.1 & GLM-5") q.Set("timezone_offset", "-480") q.Set("local_time", localTime) q.Set("utc_time", utcTime) q.Set("is_mobile", "false") q.Set("is_touch", "false") q.Set("max_touch_points", "0") q.Set("browser_name", "Chrome") q.Set("os_name", "Windows") q.Set("signature_timestamp", strconv.FormatInt(timestamp, 10)) return q.Encode() } type bridgeUpstreamResponse struct { OK bool `json:"ok"` Status int `json:"status"` StatusText string `json:"statusText"` Headers map[string]string `json:"headers"` Body string `json:"body"` Error string `json:"error"` } func makeBridgeRequest(upstreamURL, token, signature string, bodyBytes []byte, randomIP string) (*fhttp.Response, error) { bridgeURL := strings.TrimRight(Cfg.BrowserBridgeURL, "/") + "/v1/upstream" payload := map[string]interface{}{ "method": "POST", "url": upstreamURL, "headers": map[string]string{ "Authorization": "Bearer " + token, "X-FE-Version": GetFeVersion(), "X-Signature": signature, "Content-Type": "application/json", "X-Forwarded-For": randomIP, "X-Real-IP": randomIP, }, "body": string(bodyBytes), } payloadBytes, err := json.Marshal(payload) if err != nil { return nil, err } req, err := fhttp.NewRequest("POST", bridgeURL, bytes.NewReader(payloadBytes)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") if Cfg.BrowserBridgeSecret != "" { req.Header.Set("X-Bridge-Secret", Cfg.BrowserBridgeSecret) } client, err := TLSHTTPClient(300 * time.Second) if err != nil { return nil, err } bridgeResp, err := client.Do(req) if err != nil { return nil, err } defer bridgeResp.Body.Close() bridgeBody, err := io.ReadAll(bridgeResp.Body) if err != nil { return nil, err } if bridgeResp.StatusCode != http.StatusOK { return nil, fmt.Errorf("browser bridge status %d: %s", bridgeResp.StatusCode, string(bridgeBody)[:min(500, len(bridgeBody))]) } var decoded bridgeUpstreamResponse if err := json.Unmarshal(bridgeBody, &decoded); err != nil { return nil, fmt.Errorf("decode browser bridge response: %w", err) } if !decoded.OK { if decoded.Error == "" { decoded.Error = "unknown browser bridge error" } return nil, fmt.Errorf("browser bridge error: %s", decoded.Error) } if decoded.Status == 0 { return nil, fmt.Errorf("browser bridge returned empty upstream status") } resp := &fhttp.Response{ StatusCode: decoded.Status, Status: fmt.Sprintf("%d %s", decoded.Status, decoded.StatusText), Header: make(fhttp.Header), Body: io.NopCloser(strings.NewReader(decoded.Body)), } for k, v := range decoded.Headers { resp.Header.Set(k, v) } return resp, nil } func makeUpstreamRequest(token string, messages []Message, model string, imageURLs, videoURLs []string, hasTools bool, tools []Tool) (*fhttp.Response, string, error) { payload, err := DecodeJWTPayload(token) if err != nil || payload == nil { return nil, "", fmt.Errorf("invalid token") } userID := payload.ID chatID := uuid.New().String() timestamp := time.Now().UnixMilli() requestID := uuid.New().String() userMsgID := uuid.New().String() // 使用新的模型映射系统 mapping := GetUpstreamConfig(model) var targetModel string var enableThinking, autoWebSearch bool var mcpServers []string if mapping != nil { targetModel = mapping.UpstreamModelID enableThinking = mapping.EnableThinking autoWebSearch = mapping.AutoWebSearch mcpServers = mapping.MCPServers LogDebug("Model mapping: %s -> %s (thinking=%v, search=%v)", model, targetModel, enableThinking, autoWebSearch) } else { // 回退到老的逻辑 targetModel = GetTargetModel(model) enableThinking = IsThinkingModel(model) autoWebSearch = IsSearchModel(model) LogDebug("Using fallback model mapping: %s -> %s", model, targetModel) } if targetModel == "glm-4.5v" || targetModel == "glm-4.6v" { autoWebSearch = false } if hasTools { autoWebSearch = false LogDebug("[Upstream] Disabled auto web search because custom tools were provided") } if len(imageURLs) > 0 || len(videoURLs) > 0 { vlmServers := []string{"vlm-image-search", "vlm-image-recognition", "vlm-image-processing"} existingSet := make(map[string]bool) for _, s := range mcpServers { existingSet[s] = true } for _, s := range vlmServers { if !existingSet[s] { mcpServers = append(mcpServers, s) } } } latestUserContent := extractLatestUserContent(messages) signature := GenerateSignature(userID, requestID, latestUserContent, timestamp) url := Cfg.APIEndpoint + "?" + buildBrowserFingerprintQuery(timestamp, requestID, userID, token, chatID) urlToFileID := make(map[string]string) var filesData []map[string]interface{} // 上传图片 if len(imageURLs) > 0 { LogDebug("[Upstream] Uploading %d images...", len(imageURLs)) imageFiles, _ := UploadImages(token, imageURLs) LogDebug("[Upstream] Image upload result: %d files", len(imageFiles)) for i, f := range imageFiles { if i < len(imageURLs) { urlToFileID[imageURLs[i]] = f.ID } filesData = append(filesData, map[string]interface{}{ "type": f.Type, "file": f.File, "id": f.ID, "url": f.URL, "name": f.Name, "status": f.Status, "size": f.Size, "error": f.Error, "itemId": f.ItemID, "media": f.Media, "ref_user_msg_id": userMsgID, }) } } // 上传视频 if len(videoURLs) > 0 { LogDebug("[Upstream] Uploading %d videos...", len(videoURLs)) videoFiles, _ := UploadVideos(token, videoURLs) LogDebug("[Upstream] Video upload result: %d files", len(videoFiles)) for i, f := range videoFiles { if i < len(videoURLs) { urlToFileID[videoURLs[i]] = f.ID } filesData = append(filesData, map[string]interface{}{ "type": f.Type, "file": f.File, "id": f.ID, "url": f.URL, "name": f.Name, "status": f.Status, "size": f.Size, "error": f.Error, "itemId": f.ItemID, "media": f.Media, "ref_user_msg_id": userMsgID, }) } } var upstreamMessages []map[string]interface{} for _, msg := range messages { upstreamMessages = append(upstreamMessages, msg.ToUpstreamMessage(urlToFileID)) } // 普通 chat 模式(不使用 z.ai 内部 agent)。 // 工具调用通过 prompt injection 实现,模型输出 XML 工具调用 → 解析为 OpenAI tool_calls。 // z.ai agent 模式虽然能调用内置工具但不返回 tool_calls 字段,对外部客户端无用,已废弃。 flags := []string{} previewMode := false imageGen := true webSearch := true body := map[string]interface{}{ "stream": true, "model": targetModel, "messages": upstreamMessages, "signature_prompt": latestUserContent, "params": map[string]interface{}{}, "extra": map[string]interface{}{}, "features": map[string]interface{}{ "image_generation": imageGen, "web_search": webSearch, "auto_web_search": autoWebSearch && !hasTools, "preview_mode": previewMode, "flags": flags, "vlm_tools_enable": false, "vlm_web_search_enable": false, "vlm_website_mode": false, "enable_thinking": enableThinking, }, "chat_id": chatID, "id": uuid.New().String(), } // (Agent 模式相关字段已移除 — z.ai agent 不返回 tool_calls,对外部客户端无用) // 原生 function calling:把 tools 传给上游,设置 params.function_calling = "native" // 注意:z.ai 的原生 function calling 格式可能和 OpenAI 不同,暂时禁用 // if hasTools && len(tools) > 0 { // body["tools"] = tools // body["params"] = map[string]interface{}{ // "function_calling": "native", // } // } if len(mcpServers) > 0 { body["mcp_servers"] = mcpServers } // 注入 captcha_verify_param(如果配置了 captcha provider) if Cfg.CaptchaProviderURL != "" { captchaToken, err := fetchCaptchaToken() if err != nil { LogWarn("[Captcha] Failed to get captcha token: %v", err) } else if captchaToken != "" { body["captcha_verify_param"] = captchaToken LogDebug("[Captcha] Injected captcha_verify_param") } } if len(filesData) > 0 { body["files"] = filesData body["current_user_message_id"] = userMsgID LogDebug("[Upstream] Attaching %d files to request, userMsgID=%s", len(filesData), userMsgID) for i, fd := range filesData { LogDebug("[Upstream] File %d: id=%v, type=%v, name=%v, status=%v", i+1, fd["id"], fd["type"], fd["name"], fd["status"]) } } bodyBytes, _ := json.Marshal(body) req, err := fhttp.NewRequest("POST", url, bytes.NewReader(bodyBytes)) if err != nil { return nil, "", err } randomIP := generateRandomIP() req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("X-FE-Version", GetFeVersion()) req.Header.Set("X-Signature", signature) req.Header.Set("Content-Type", "application/json") req.Header.Set("Connection", "keep-alive") req.Header.Set("Origin", "https://chat.z.ai") req.Header.Set("Referer", fmt.Sprintf("https://chat.z.ai/c/%s", chatID)) ApplyBrowserFingerprintHeaders(req.Header) req.Header.Set("X-Forwarded-For", randomIP) req.Header.Set("X-Real-IP", randomIP) LogDebug("Upstream request: model=%s, messages=%d, XFF=%s", targetModel, len(messages), randomIP) var resp *fhttp.Response if Cfg.BrowserBridgeURL != "" { LogDebug("[BrowserBridge] Forwarding upstream request via %s", Cfg.BrowserBridgeURL) resp, err = makeBridgeRequest(url, token, signature, bodyBytes, randomIP) } else { client, clientErr := TLSHTTPClient(300 * time.Second) if clientErr != nil { return nil, "", clientErr } resp, err = client.Do(req) } if err != nil { return nil, "", err } LogDebug("Upstream response: status=%d, XFF=%s", resp.StatusCode, randomIP) return resp, targetModel, nil } type UpstreamData struct { Type string `json:"type"` Data struct { DeltaContent string `json:"delta_content"` EditContent string `json:"edit_content"` Phase string `json:"phase"` Done bool `json:"done"` Error *struct { Code string `json:"code"` Detail string `json:"detail"` } `json:"error,omitempty"` } `json:"data"` } // HasError 检查上游响应是否包含错误 func (u *UpstreamData) HasError() bool { return u.Data.Error != nil && u.Data.Error.Code != "" } // GetErrorMessage 获取错误信息 func (u *UpstreamData) GetErrorMessage() string { if u.Data.Error == nil { return "" } if u.Data.Error.Detail != "" { return u.Data.Error.Detail } return u.Data.Error.Code } // UpstreamResult 上游请求结果 type UpstreamResult struct { Success bool HasContent bool ResponseStarted bool ErrorMessage string OutputTokens int64 } const RetryableErr = "INTERNAL_ERROR" func (u *UpstreamData) GetEditContent() string { editContent := u.Data.EditContent if editContent == "" { return "" } if len(editContent) > 0 && editContent[0] == '"' { var unescaped string if err := json.Unmarshal([]byte(editContent), &unescaped); err == nil { LogDebug("[GetEditContent] Unescaped edit_content from JSON string") return unescaped } } return editContent } type ThinkingFilter struct { hasSeenFirstThinking bool buffer string lastOutputChunk string lastPhase string thinkingRoundCount int } func (f *ThinkingFilter) ProcessThinking(deltaContent string) string { if !f.hasSeenFirstThinking { // 合并缓存和当前内容,查找 "> " 作为思考内容的开始标记 combined := f.buffer + deltaContent if idx := strings.Index(combined, "> "); idx != -1 { f.hasSeenFirstThinking = true f.buffer = "" deltaContent = combined[idx+2:] } else { // 没找到开始标记,缓存当前内容继续等待 f.buffer = combined return "" } } content := f.buffer + deltaContent f.buffer = "" content = strings.ReplaceAll(content, "\n> ", "\n") if strings.HasSuffix(content, "\n>") { f.buffer = "\n>" return content[:len(content)-2] } if strings.HasSuffix(content, "\n") { f.buffer = "\n" return content[:len(content)-1] } return content } func (f *ThinkingFilter) Flush() string { result := f.buffer f.buffer = "" return result } func (f *ThinkingFilter) ExtractCompleteThinking(editContent string) string { startIdx := strings.Index(editContent, "> ") if startIdx == -1 { return "" } startIdx += 2 endIdx := strings.Index(editContent, "\n") if endIdx == -1 { return "" } content := editContent[startIdx:endIdx] content = strings.ReplaceAll(content, "\n> ", "\n") return content } func (f *ThinkingFilter) ExtractIncrementalThinking(editContent string) string { completeThinking := f.ExtractCompleteThinking(editContent) if completeThinking == "" { return "" } if f.lastOutputChunk == "" { return completeThinking } idx := strings.Index(completeThinking, f.lastOutputChunk) if idx == -1 { return completeThinking } incrementalPart := completeThinking[idx+len(f.lastOutputChunk):] return incrementalPart } func (f *ThinkingFilter) ResetForNewRound() { f.lastOutputChunk = "" f.hasSeenFirstThinking = false } func getUpstreamToken() (string, error) { // 优先使用 TokenManager 中的用户 token,其次 BACKUP_TOKEN,最后匿名 token。 if tmToken := GetTokenManager().GetToken(); tmToken != "" { LogDebug("Using token from TokenManager") return tmToken, nil } if backupToken := GetBackupToken(); backupToken != "" { LogDebug("Using backup token") return backupToken, nil } anonymousToken, err := GetAnonymousToken() if err != nil { return "", err } LogDebug("Using anonymous token: %s...", anonymousToken[:min(10, len(anonymousToken))]) return anonymousToken, nil } func HandleChatCompletions(w http.ResponseWriter, r *http.Request) { // 只接受 POST 请求 if r.Method != http.MethodPost { writeInvalidRequestError(w, "Only POST method is allowed") return } apiKey := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") // API Key 认证 if !Cfg.SkipAuthToken { if apiKey == "" { LogDebug("Missing Authorization header") writeError(w, http.StatusUnauthorized, ErrTypeAuthentication, "Missing or invalid Authorization header", "invalid_api_key") return } // 验证 API Key if !ValidateAuthToken(apiKey) { LogDebug("Invalid API key: %s...", apiKey[:min(8, len(apiKey))]) writeError(w, http.StatusUnauthorized, ErrTypeAuthentication, "Invalid API key", "invalid_api_key") return } LogDebug("API key validated: %s...", apiKey[:min(8, len(apiKey))]) } else { LogDebug("SKIP_AUTH_TOKEN enabled, skipping API key validation") } // 先解析请求,避免在无效请求上消耗/轮询上游 token var req ChatRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeInvalidRequestError(w, "无效的请求格式") return } if req.Model == "" { req.Model = "GLM-4.6" } // 验证模型是否存在 if !IsValidModel(req.Model) { writeModelNotFoundError(w, req.Model) return } clientIP := GetClientIP(r) isMultimodal := false // 检测多模态 reqImageURLs, reqVideoURLs := extractAllMediaURLs(req.Messages) if len(reqImageURLs) > 0 || len(reqVideoURLs) > 0 { isMultimodal = true LogDebug("[Request] Multimodal detected: images=%d, videos=%d", len(reqImageURLs), len(reqVideoURLs)) for i, url := range reqImageURLs { urlPreview := url if len(urlPreview) > 80 { urlPreview = urlPreview[:80] + "..." } LogDebug("[Request] Image %d: %s", i+1, urlPreview) } for i, url := range reqVideoURLs { urlPreview := url if len(urlPreview) > 80 { urlPreview = urlPreview[:80] + "..." } LogDebug("[Request] Video %d: %s", i+1, urlPreview) } } // 处理工具调用 messages := req.Messages // Agent 模式由 z.ai 上游原生支持工具调用,不需要 prompt injection // 只在传统 chat 模式(无 agent flag)时才注入工具 prompt // 工具调用:通过 prompt injection 让模型输出 XML 工具调用, // 我们在响应里解析回 OpenAI 格式 tool_calls。 if len(req.Tools) > 0 { // FORCE_TOOL_CHOICE_REQUIRED:把没指定 tool_choice / auto 的请求升级为 required toolChoice := req.ToolChoice if Cfg.ForceToolChoiceRequired { if tc, ok := toolChoice.(string); !ok || tc == "" || tc == "auto" { toolChoice = "required" LogDebug("[Tools] Forced tool_choice to 'required'") } } messages = ProcessMessagesWithTools(messages, req.Tools, toolChoice) } inputTokens := CountRequestTokens(messages, req.Tools) LogDebug("Chat request: model=%s, messages=%d, stream=%v, input_tokens=%d, ip=%s, multimodal=%v, tools=%d", req.Model, len(messages), req.Stream, inputTokens, clientIP, isMultimodal, len(req.Tools)) completionID := fmt.Sprintf("chatcmpl-%s", uuid.New().String()[:29]) includeUsage := req.StreamOptions != nil && req.StreamOptions.IncludeUsage var outputTokens int64 var lastError string success := false var token string // 重试循环 maxRetries := max(Cfg.RetryCount, 0) for attempt := 0; attempt <= maxRetries; attempt++ { if attempt > 0 { LogInfo("Retry %d/%d with next available token", attempt, maxRetries) } var tokenErr error token, tokenErr = getUpstreamToken() if tokenErr != nil { LogError("Failed to get upstream token (attempt %d): %v", attempt+1, tokenErr) lastError = tokenErr.Error() break } resp, modelName, err := makeUpstreamRequest(token, messages, req.Model, reqImageURLs, reqVideoURLs, len(req.Tools) > 0, req.Tools) if err != nil { LogError("Upstream request failed (attempt %d): %v", attempt+1, err) lastError = err.Error() continue } if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) resp.Body.Close() LogError("Upstream error (attempt %d): status=%d, body=%s", attempt+1, resp.StatusCode, string(body)[:min(500, len(body))]) lastError = fmt.Sprintf("status %d", resp.StatusCode) // 非 5xx 错误不重试 if resp.StatusCode < 500 { GetTokenManager().RecordCall(false, isMultimodal) writeUpstreamError(w, resp.StatusCode, body) return } continue } var result UpstreamResult if req.Stream { result = handleStreamResponseWithRetry(w, resp.Body, completionID, modelName, inputTokens, includeUsage, req.Tools, attempt == 0) } else { result = handleNonStreamResponseWithRetry(w, resp.Body, completionID, modelName, inputTokens, req.Tools) } resp.Body.Close() outputTokens = result.OutputTokens if result.Success && result.HasContent { success = true break } // 检查是否需要重试 if result.ErrorMessage != "" { lastError = result.ErrorMessage LogWarn("Upstream returned error (attempt %d): %s", attempt+1, result.ErrorMessage) } else if !result.HasContent { lastError = "empty response" LogWarn("Upstream returned empty content (attempt %d)", attempt+1) } // 流式请求已开始写入,无法重试 if req.Stream && result.ResponseStarted { LogDebug("Stream response already started, cannot retry") break } } if !success && !req.Stream { // 非流式请求失败,返回错误 GetTokenManager().RecordCall(false, isMultimodal) writeError(w, http.StatusBadGateway, ErrTypeUpstream, fmt.Sprintf("请求失败: %s", lastError), "upstream_error") return } // 记录遥测数据 RecordRequest(inputTokens, outputTokens, req.Model) GetTokenManager().RecordCall(success, isMultimodal) LogDebug("Chat completed: model=%s, input_tokens=%d, output_tokens=%d, ip=%s, success=%v", req.Model, inputTokens, outputTokens, clientIP, success) } func handleStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionID, modelName string, inputTokens int64, includeUsage bool, tools []Tool) int64 { var outputTokens int64 var fullContent strings.Builder var fullReasoning strings.Builder // 累积思考链内容,工具调用回退解析用 w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "Streaming not supported", http.StatusInternalServerError) return 0 } // 发送第一个 chunk 带 role firstChunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Role: "assistant"}, FinishReason: nil, }}, } data, _ := json.Marshal(firstChunk) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() scanner := bufio.NewScanner(body) scanner.Buffer(make([]byte, 1024*1024), 1024*1024) hasContent := false searchRefFilter := NewSearchRefFilter() thinkingFilter := &ThinkingFilter{} pendingSourcesMarkdown := "" pendingImageSearchMarkdown := "" totalContentOutputLength := 0 // 记录已输出的 content 字符长度 hasTools := len(tools) > 0 for scanner.Scan() { line := scanner.Text() LogDebug("[Upstream] %s", line) if !strings.HasPrefix(line, "data: ") { continue } payload := strings.TrimPrefix(line, "data: ") if payload == "[DONE]" { break } var upstream UpstreamData if err := json.Unmarshal([]byte(payload), &upstream); err != nil { continue } // 检测上游错误 if upstream.HasError() { LogError("Upstream error: %s", upstream.GetErrorMessage()) errContent := fmt.Sprintf("[上游服务错误: %s]", upstream.GetErrorMessage()) errChunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: errContent}, FinishReason: nil, }}, } errData, _ := json.Marshal(errChunk) fmt.Fprintf(w, "data: %s\n\n", errData) flusher.Flush() hasContent = true break } if upstream.Data.Phase == "done" { break } if upstream.Data.Phase == "thinking" && upstream.Data.DeltaContent != "" { isNewThinkingRound := false if thinkingFilter.lastPhase != "" && thinkingFilter.lastPhase != "thinking" { thinkingFilter.ResetForNewRound() thinkingFilter.thinkingRoundCount++ isNewThinkingRound = true } thinkingFilter.lastPhase = "thinking" reasoningContent := thinkingFilter.ProcessThinking(upstream.Data.DeltaContent) if isNewThinkingRound && thinkingFilter.thinkingRoundCount > 1 && reasoningContent != "" { reasoningContent = "\n\n" + reasoningContent } if reasoningContent != "" { thinkingFilter.lastOutputChunk = reasoningContent reasoningContent = searchRefFilter.Process(reasoningContent) if reasoningContent != "" { hasContent = true fullReasoning.WriteString(reasoningContent) // 累积用于工具调用回退解析 chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{ReasoningContent: reasoningContent}, FinishReason: nil, }}, } data, _ := json.Marshal(chunk) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() } } continue } if upstream.Data.Phase != "" { thinkingFilter.lastPhase = upstream.Data.Phase } editContent := upstream.GetEditContent() if editContent != "" && IsSearchResultContent(editContent) { if results := ParseSearchResults(editContent); len(results) > 0 { searchRefFilter.AddSearchResults(results) pendingSourcesMarkdown = searchRefFilter.GetSearchResultsMarkdown() } continue } if editContent != "" && strings.Contains(editContent, `"search_image"`) { textBeforeBlock := ExtractTextBeforeGlmBlock(editContent) if textBeforeBlock != "" { textBeforeBlock = searchRefFilter.Process(textBeforeBlock) if textBeforeBlock != "" { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: textBeforeBlock}, FinishReason: nil, }}, } data, _ := json.Marshal(chunk) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() } } if results := ParseImageSearchResults(editContent); len(results) > 0 { pendingImageSearchMarkdown = FormatImageSearchResults(results) } continue } if editContent != "" && strings.Contains(editContent, `"mcp"`) { textBeforeBlock := ExtractTextBeforeGlmBlock(editContent) if textBeforeBlock != "" { textBeforeBlock = searchRefFilter.Process(textBeforeBlock) if textBeforeBlock != "" { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: textBeforeBlock}, FinishReason: nil, }}, } data, _ := json.Marshal(chunk) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() } } continue } if editContent != "" && IsSearchToolCall(editContent, upstream.Data.Phase) { continue } if pendingSourcesMarkdown != "" { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: pendingSourcesMarkdown}, FinishReason: nil, }}, } data, _ := json.Marshal(chunk) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() pendingSourcesMarkdown = "" } if pendingImageSearchMarkdown != "" { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: pendingImageSearchMarkdown}, FinishReason: nil, }}, } data, _ := json.Marshal(chunk) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() pendingImageSearchMarkdown = "" } content := "" reasoningContent := "" if thinkingRemaining := thinkingFilter.Flush(); thinkingRemaining != "" { thinkingFilter.lastOutputChunk = thinkingRemaining processedRemaining := searchRefFilter.Process(thinkingRemaining) if processedRemaining != "" { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{ReasoningContent: processedRemaining}, FinishReason: nil, }}, } data, _ := json.Marshal(chunk) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() } } if pendingSourcesMarkdown != "" && thinkingFilter.hasSeenFirstThinking { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{ReasoningContent: pendingSourcesMarkdown}, FinishReason: nil, }}, } data, _ := json.Marshal(chunk) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() pendingSourcesMarkdown = "" } if upstream.Data.Phase == "answer" && upstream.Data.DeltaContent != "" { content = upstream.Data.DeltaContent } else if upstream.Data.Phase == "answer" && editContent != "" { if strings.Contains(editContent, "") { reasoningContent = thinkingFilter.ExtractIncrementalThinking(editContent) if idx := strings.Index(editContent, ""); idx != -1 { afterDetails := editContent[idx+len(""):] if strings.HasPrefix(afterDetails, "\n") { content = afterDetails[1:] } else { content = afterDetails } totalContentOutputLength = len([]rune(content)) } } } else if (upstream.Data.Phase == "other" || upstream.Data.Phase == "tool_call") && editContent != "" { fullContent := editContent fullContentRunes := []rune(fullContent) if len(fullContentRunes) > totalContentOutputLength { content = string(fullContentRunes[totalContentOutputLength:]) totalContentOutputLength = len(fullContentRunes) } else { content = fullContent } } if reasoningContent != "" { reasoningContent = searchRefFilter.Process(reasoningContent) + searchRefFilter.Flush() } if reasoningContent != "" { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{ReasoningContent: reasoningContent}, FinishReason: nil, }}, } data, _ := json.Marshal(chunk) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() } if content == "" { continue } content = searchRefFilter.Process(content) if content == "" { continue } hasContent = true if upstream.Data.Phase == "answer" && upstream.Data.DeltaContent != "" { totalContentOutputLength += len([]rune(content)) } fullContent.WriteString(content) outputTokens += CountTokens(content) if hasTools { continue } chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: content}, FinishReason: nil, }}, } chunkData, _ := json.Marshal(chunk) fmt.Fprintf(w, "data: %s\n\n", chunkData) flusher.Flush() } if err := scanner.Err(); err != nil { LogError("[Upstream] scanner error: %v", err) } if remaining := searchRefFilter.Flush(); remaining != "" { hasContent = true fullContent.WriteString(remaining) if !hasTools { chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: remaining}, FinishReason: nil, }}, } chunkData, _ := json.Marshal(chunk) fmt.Fprintf(w, "data: %s\n\n", chunkData) flusher.Flush() } } if !hasContent { LogError("Stream response 200 but no content received") } stopReason := "stop" var toolCalls []ToolCall if len(tools) > 0 { rawContent := fullContent.String() toolCalls = ExtractToolInvocationsWithFallback(rawContent, fullReasoning.String()) if len(toolCalls) > 0 { stopReason = "tool_calls" LogDebug("[Stream] Detected %d tool calls, sending tool_calls chunks", len(toolCalls)) for i, tc := range toolCalls { if tc.ID == "" { tc.ID = generateCallID() } toolChunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{ ToolCalls: []ToolCall{{ Index: i, ID: tc.ID, Type: tc.Type, Function: tc.Function, }}, }, FinishReason: nil, }}, } toolData, _ := json.Marshal(toolChunk) fmt.Fprintf(w, "data: %s\n\n", toolData) flusher.Flush() } } else { bufferedContent := RemoveToolJSONContent(rawContent) if bufferedContent != "" { chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: bufferedContent}, FinishReason: nil, }}, } chunkData, _ := json.Marshal(chunk) fmt.Fprintf(w, "data: %s\n\n", chunkData) flusher.Flush() } } } finalChunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{}, FinishReason: &stopReason, }}, } finalData, _ := json.Marshal(finalChunk) fmt.Fprintf(w, "data: %s\n\n", finalData) if includeUsage { usageChunk := ChatCompletionChunkResponse{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{}, Usage: &Usage{ PromptTokens: inputTokens, CompletionTokens: outputTokens, TotalTokens: inputTokens + outputTokens, }, } usageData, _ := json.Marshal(usageChunk) fmt.Fprintf(w, "data: %s\n\n", usageData) } fmt.Fprintf(w, "data: [DONE]\n\n") flusher.Flush() return outputTokens } func handleNonStreamResponse(w http.ResponseWriter, body io.ReadCloser, completionID, modelName string, inputTokens int64, tools []Tool) int64 { w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Request-Id", completionID) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } var outputTokens int64 scanner := bufio.NewScanner(body) scanner.Buffer(make([]byte, 1024*1024), 1024*1024) var chunks []string var reasoningChunks []string thinkingFilter := &ThinkingFilter{} searchRefFilter := NewSearchRefFilter() hasThinking := false pendingSourcesMarkdown := "" pendingImageSearchMarkdown := "" for scanner.Scan() { line := scanner.Text() LogDebug("[Upstream] %s", line) if !strings.HasPrefix(line, "data: ") { continue } payload := strings.TrimPrefix(line, "data: ") if payload == "[DONE]" { break } var upstream UpstreamData if err := json.Unmarshal([]byte(payload), &upstream); err != nil { continue } if upstream.HasError() { LogError("Upstream error: %s", upstream.GetErrorMessage()) chunks = append(chunks, fmt.Sprintf("[上游服务错误: %s]", upstream.GetErrorMessage())) break } if upstream.Data.Phase == "done" { break } if upstream.Data.Phase == "thinking" && upstream.Data.DeltaContent != "" { if thinkingFilter.lastPhase != "" && thinkingFilter.lastPhase != "thinking" { thinkingFilter.ResetForNewRound() thinkingFilter.thinkingRoundCount++ if thinkingFilter.thinkingRoundCount > 1 { reasoningChunks = append(reasoningChunks, "\n\n") } } thinkingFilter.lastPhase = "thinking" hasThinking = true reasoningContent := thinkingFilter.ProcessThinking(upstream.Data.DeltaContent) if reasoningContent != "" { thinkingFilter.lastOutputChunk = reasoningContent reasoningChunks = append(reasoningChunks, reasoningContent) } continue } if upstream.Data.Phase != "" { thinkingFilter.lastPhase = upstream.Data.Phase } editContent := upstream.GetEditContent() if editContent != "" && IsSearchResultContent(editContent) { if results := ParseSearchResults(editContent); len(results) > 0 { searchRefFilter.AddSearchResults(results) pendingSourcesMarkdown = searchRefFilter.GetSearchResultsMarkdown() } continue } if editContent != "" && strings.Contains(editContent, `"search_image"`) { textBeforeBlock := ExtractTextBeforeGlmBlock(editContent) if textBeforeBlock != "" { chunks = append(chunks, textBeforeBlock) } if results := ParseImageSearchResults(editContent); len(results) > 0 { pendingImageSearchMarkdown = FormatImageSearchResults(results) } continue } if editContent != "" && strings.Contains(editContent, `"mcp"`) { textBeforeBlock := ExtractTextBeforeGlmBlock(editContent) if textBeforeBlock != "" { chunks = append(chunks, textBeforeBlock) } continue } if editContent != "" && IsSearchToolCall(editContent, upstream.Data.Phase) { continue } if pendingSourcesMarkdown != "" { if hasThinking { reasoningChunks = append(reasoningChunks, pendingSourcesMarkdown) } else { chunks = append(chunks, pendingSourcesMarkdown) } pendingSourcesMarkdown = "" } if pendingImageSearchMarkdown != "" { chunks = append(chunks, pendingImageSearchMarkdown) pendingImageSearchMarkdown = "" } content := "" if upstream.Data.Phase == "answer" && upstream.Data.DeltaContent != "" { content = upstream.Data.DeltaContent } else if upstream.Data.Phase == "answer" && editContent != "" { if strings.Contains(editContent, "") { reasoningContent := thinkingFilter.ExtractIncrementalThinking(editContent) if reasoningContent != "" { reasoningChunks = append(reasoningChunks, reasoningContent) } if idx := strings.Index(editContent, ""); idx != -1 { afterDetails := editContent[idx+len(""):] if strings.HasPrefix(afterDetails, "\n") { content = afterDetails[1:] } else { content = afterDetails } } } } else if (upstream.Data.Phase == "other" || upstream.Data.Phase == "tool_call") && editContent != "" { content = editContent } if content != "" { chunks = append(chunks, content) } } fullContent := strings.Join(chunks, "") fullContent = searchRefFilter.Process(fullContent) + searchRefFilter.Flush() fullReasoning := strings.Join(reasoningChunks, "") fullReasoning = searchRefFilter.Process(fullReasoning) + searchRefFilter.Flush() if fullContent == "" && fullReasoning == "" { LogError("Non-stream response 200 but no content received") } stopReason := "stop" var toolCalls []ToolCall if len(tools) > 0 { toolCalls = ExtractToolInvocationsWithFallback(fullContent, fullReasoning) fullContent = RemoveToolJSONContent(fullContent) if len(toolCalls) > 0 { stopReason = "tool_calls" } } outputTokens = CountTokens(fullContent) + CountTokens(fullReasoning) response := ChatCompletionResponse{ ID: completionID, Object: "chat.completion", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Message: &MessageResp{ Role: "assistant", Content: fullContent, ReasoningContent: fullReasoning, ToolCalls: toolCalls, }, FinishReason: &stopReason, }}, Usage: &Usage{ PromptTokens: inputTokens, CompletionTokens: outputTokens, TotalTokens: inputTokens + outputTokens, }, SystemFingerprint: "openai", } json.NewEncoder(w).Encode(response) return outputTokens } func handleStreamResponseWithRetry(w http.ResponseWriter, body io.ReadCloser, completionID, modelName string, inputTokens int64, includeUsage bool, tools []Tool, isFirstAttempt bool) UpstreamResult { result := UpstreamResult{Success: true, HasContent: false} var outputTokens int64 var fullContent strings.Builder var fullReasoning strings.Builder // 累积思考链内容,工具调用回退解析用 var upstreamError string flusher, ok := w.(http.Flusher) if !ok { result.Success = false result.ErrorMessage = "streaming not supported" return result } startStream := func() error { if result.ResponseStarted { return nil } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") firstChunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Role: "assistant"}, FinishReason: nil, }}, } data, _ := json.Marshal(firstChunk) if _, err := fmt.Fprintf(w, "data: %s\n\n", data); err != nil { return err } flusher.Flush() result.ResponseStarted = true return nil } sendChunk := func(chunk interface{}) bool { if err := startStream(); err != nil { result.Success = false result.ErrorMessage = err.Error() return false } chunkData, _ := json.Marshal(chunk) if _, err := fmt.Fprintf(w, "data: %s\n\n", chunkData); err != nil { result.Success = false result.ErrorMessage = err.Error() return false } flusher.Flush() return true } scanner := bufio.NewScanner(body) scanner.Buffer(make([]byte, 1024*1024), 1024*1024) hasContent := false searchRefFilter := NewSearchRefFilter() thinkingFilter := &ThinkingFilter{} pendingSourcesMarkdown := "" pendingImageSearchMarkdown := "" totalContentOutputLength := 0 hasTools := len(tools) > 0 for scanner.Scan() { line := scanner.Text() LogDebug("[Upstream] %s", line) if !strings.HasPrefix(line, "data: ") { continue } payload := strings.TrimPrefix(line, "data: ") if payload == "[DONE]" { break } var upstream UpstreamData if err := json.Unmarshal([]byte(payload), &upstream); err != nil { continue } if upstream.HasError() { upstreamError = upstream.GetErrorMessage() LogError("Upstream error: %s", upstreamError) result.Success = false result.ErrorMessage = upstreamError if result.ResponseStarted { errContent := fmt.Sprintf("[上游服务错误: %s]", upstreamError) errChunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: errContent}, FinishReason: nil, }}, } if !sendChunk(errChunk) { return result } hasContent = true } break } if upstream.Data.Phase == "done" { break } if upstream.Data.Phase == "thinking" && upstream.Data.DeltaContent != "" { isNewThinkingRound := false if thinkingFilter.lastPhase != "" && thinkingFilter.lastPhase != "thinking" { thinkingFilter.ResetForNewRound() thinkingFilter.thinkingRoundCount++ isNewThinkingRound = true } thinkingFilter.lastPhase = "thinking" reasoningContent := thinkingFilter.ProcessThinking(upstream.Data.DeltaContent) if isNewThinkingRound && thinkingFilter.thinkingRoundCount > 1 && reasoningContent != "" { reasoningContent = "\n\n" + reasoningContent } if reasoningContent != "" { thinkingFilter.lastOutputChunk = reasoningContent reasoningContent = searchRefFilter.Process(reasoningContent) if reasoningContent != "" { hasContent = true fullReasoning.WriteString(reasoningContent) // 累积用于工具调用回退解析 chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{ReasoningContent: reasoningContent}, FinishReason: nil, }}, } if !sendChunk(chunk) { return result } } } continue } if upstream.Data.Phase != "" { thinkingFilter.lastPhase = upstream.Data.Phase } editContent := upstream.GetEditContent() if editContent != "" && IsSearchResultContent(editContent) { if results := ParseSearchResults(editContent); len(results) > 0 { searchRefFilter.AddSearchResults(results) pendingSourcesMarkdown = searchRefFilter.GetSearchResultsMarkdown() } continue } if editContent != "" && strings.Contains(editContent, `"search_image"`) { textBeforeBlock := ExtractTextBeforeGlmBlock(editContent) if textBeforeBlock != "" { textBeforeBlock = searchRefFilter.Process(textBeforeBlock) if textBeforeBlock != "" { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: textBeforeBlock}, FinishReason: nil, }}, } if !sendChunk(chunk) { return result } } } if results := ParseImageSearchResults(editContent); len(results) > 0 { pendingImageSearchMarkdown = FormatImageSearchResults(results) } continue } if editContent != "" && strings.Contains(editContent, `"mcp"`) { textBeforeBlock := ExtractTextBeforeGlmBlock(editContent) if textBeforeBlock != "" { textBeforeBlock = searchRefFilter.Process(textBeforeBlock) if textBeforeBlock != "" { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: textBeforeBlock}, FinishReason: nil, }}, } if !sendChunk(chunk) { return result } } } continue } if editContent != "" && IsSearchToolCall(editContent, upstream.Data.Phase) { continue } if pendingSourcesMarkdown != "" { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: pendingSourcesMarkdown}, FinishReason: nil, }}, } if !sendChunk(chunk) { return result } pendingSourcesMarkdown = "" } if pendingImageSearchMarkdown != "" { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: pendingImageSearchMarkdown}, FinishReason: nil, }}, } if !sendChunk(chunk) { return result } pendingImageSearchMarkdown = "" } content := "" reasoningContent := "" if thinkingRemaining := thinkingFilter.Flush(); thinkingRemaining != "" { thinkingFilter.lastOutputChunk = thinkingRemaining processedRemaining := searchRefFilter.Process(thinkingRemaining) if processedRemaining != "" { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{ReasoningContent: processedRemaining}, FinishReason: nil, }}, } if !sendChunk(chunk) { return result } } } if pendingSourcesMarkdown != "" && thinkingFilter.hasSeenFirstThinking { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{ReasoningContent: pendingSourcesMarkdown}, FinishReason: nil, }}, } if !sendChunk(chunk) { return result } pendingSourcesMarkdown = "" } if upstream.Data.Phase == "answer" && upstream.Data.DeltaContent != "" { content = upstream.Data.DeltaContent } else if upstream.Data.Phase == "answer" && editContent != "" { if strings.Contains(editContent, "") { reasoningContent = thinkingFilter.ExtractIncrementalThinking(editContent) if idx := strings.Index(editContent, ""); idx != -1 { afterDetails := editContent[idx+len(""):] if strings.HasPrefix(afterDetails, "\n") { content = afterDetails[1:] } else { content = afterDetails } totalContentOutputLength = len([]rune(content)) } } } else if (upstream.Data.Phase == "other" || upstream.Data.Phase == "tool_call") && editContent != "" { fullEditContent := editContent fullContentRunes := []rune(fullEditContent) if len(fullContentRunes) > totalContentOutputLength { content = string(fullContentRunes[totalContentOutputLength:]) totalContentOutputLength = len(fullContentRunes) } else { content = fullEditContent } } if reasoningContent != "" { reasoningContent = searchRefFilter.Process(reasoningContent) + searchRefFilter.Flush() } if reasoningContent != "" { hasContent = true chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{ReasoningContent: reasoningContent}, FinishReason: nil, }}, } if !sendChunk(chunk) { return result } } if content == "" { continue } content = searchRefFilter.Process(content) if content == "" { continue } hasContent = true if upstream.Data.Phase == "answer" && upstream.Data.DeltaContent != "" { totalContentOutputLength += len([]rune(content)) } fullContent.WriteString(content) if hasTools { outputTokens += CountTokens(content) continue } chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: content}, FinishReason: nil, }}, } outputTokens += CountTokens(content) if !sendChunk(chunk) { return result } } if remaining := searchRefFilter.Flush(); remaining != "" { hasContent = true fullContent.WriteString(remaining) if !hasTools { chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: remaining}, FinishReason: nil, }}, } if !sendChunk(chunk) { return result } } } stopReason := "stop" var toolCalls []ToolCall if hasTools { toolCalls = ExtractToolInvocationsWithFallback(fullContent.String(), fullReasoning.String()) if len(toolCalls) > 0 { stopReason = "tool_calls" for i, tc := range toolCalls { toolChunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{ ToolCalls: []ToolCall{{ Index: i, ID: tc.ID, Type: tc.Type, Function: tc.Function, }}, }, FinishReason: nil, }}, } if !sendChunk(toolChunk) { return result } } } else { // 未检测到工具调用,将缓冲的 content 作为普通内容发送 bufferedContent := RemoveToolJSONContent(fullContent.String()) if bufferedContent != "" { chunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{Content: bufferedContent}, FinishReason: nil, }}, } if !sendChunk(chunk) { return result } } } } if !hasContent { result.OutputTokens = outputTokens result.ErrorMessage = "empty response" return result } finalChunk := ChatCompletionChunk{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Delta: &Delta{}, FinishReason: &stopReason, }}, } if !sendChunk(finalChunk) { return result } if includeUsage { usageChunk := ChatCompletionChunkResponse{ ID: completionID, Object: "chat.completion.chunk", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{}, Usage: &Usage{ PromptTokens: inputTokens, CompletionTokens: outputTokens, TotalTokens: inputTokens + outputTokens, }, } if !sendChunk(usageChunk) { return result } } if _, err := fmt.Fprintf(w, "data: [DONE]\n\n"); err != nil { result.Success = false result.ErrorMessage = err.Error() return result } flusher.Flush() result.HasContent = hasContent result.OutputTokens = outputTokens return result } // handleNonStreamResponseWithRetry 非流式响应处理(带重试支持,不立即写入响应) func handleNonStreamResponseWithRetry(w http.ResponseWriter, body io.ReadCloser, completionID, modelName string, inputTokens int64, tools []Tool) UpstreamResult { result := UpstreamResult{Success: true, HasContent: false} var outputTokens int64 var upstreamError string scanner := bufio.NewScanner(body) scanner.Buffer(make([]byte, 1024*1024), 1024*1024) var chunks []string var reasoningChunks []string thinkingFilter := &ThinkingFilter{} searchRefFilter := NewSearchRefFilter() hasThinking := false pendingSourcesMarkdown := "" pendingImageSearchMarkdown := "" for scanner.Scan() { line := scanner.Text() LogDebug("[Upstream] %s", line) if !strings.HasPrefix(line, "data: ") { continue } payload := strings.TrimPrefix(line, "data: ") if payload == "[DONE]" { break } var upstream UpstreamData if err := json.Unmarshal([]byte(payload), &upstream); err != nil { continue } // 检测上游错误 if upstream.HasError() { upstreamError = upstream.GetErrorMessage() LogError("Upstream error: %s", upstreamError) result.Success = false result.ErrorMessage = upstreamError return result } if upstream.Data.Phase == "done" { break } if upstream.Data.Phase == "thinking" && upstream.Data.DeltaContent != "" { if thinkingFilter.lastPhase != "" && thinkingFilter.lastPhase != "thinking" { thinkingFilter.ResetForNewRound() thinkingFilter.thinkingRoundCount++ if thinkingFilter.thinkingRoundCount > 1 { reasoningChunks = append(reasoningChunks, "\n\n") } } thinkingFilter.lastPhase = "thinking" hasThinking = true reasoningContent := thinkingFilter.ProcessThinking(upstream.Data.DeltaContent) if reasoningContent != "" { thinkingFilter.lastOutputChunk = reasoningContent reasoningChunks = append(reasoningChunks, reasoningContent) } continue } if upstream.Data.Phase != "" { thinkingFilter.lastPhase = upstream.Data.Phase } editContent := upstream.GetEditContent() if editContent != "" && IsSearchResultContent(editContent) { if results := ParseSearchResults(editContent); len(results) > 0 { searchRefFilter.AddSearchResults(results) pendingSourcesMarkdown = searchRefFilter.GetSearchResultsMarkdown() } continue } if editContent != "" && strings.Contains(editContent, `"search_image"`) { textBeforeBlock := ExtractTextBeforeGlmBlock(editContent) if textBeforeBlock != "" { chunks = append(chunks, textBeforeBlock) } if results := ParseImageSearchResults(editContent); len(results) > 0 { pendingImageSearchMarkdown = FormatImageSearchResults(results) } continue } if editContent != "" && strings.Contains(editContent, `"mcp"`) { textBeforeBlock := ExtractTextBeforeGlmBlock(editContent) if textBeforeBlock != "" { chunks = append(chunks, textBeforeBlock) } continue } if editContent != "" && IsSearchToolCall(editContent, upstream.Data.Phase) { continue } if pendingSourcesMarkdown != "" { if hasThinking { reasoningChunks = append(reasoningChunks, pendingSourcesMarkdown) } else { chunks = append(chunks, pendingSourcesMarkdown) } pendingSourcesMarkdown = "" } if pendingImageSearchMarkdown != "" { chunks = append(chunks, pendingImageSearchMarkdown) pendingImageSearchMarkdown = "" } content := "" if upstream.Data.Phase == "answer" && upstream.Data.DeltaContent != "" { content = upstream.Data.DeltaContent } else if upstream.Data.Phase == "answer" && editContent != "" { if strings.Contains(editContent, "") { reasoningContent := thinkingFilter.ExtractIncrementalThinking(editContent) if reasoningContent != "" { reasoningChunks = append(reasoningChunks, reasoningContent) } if idx := strings.Index(editContent, ""); idx != -1 { afterDetails := editContent[idx+len(""):] if strings.HasPrefix(afterDetails, "\n") { content = afterDetails[1:] } else { content = afterDetails } } } } else if (upstream.Data.Phase == "other" || upstream.Data.Phase == "tool_call") && editContent != "" { content = editContent } if content != "" { chunks = append(chunks, content) } } fullContent := strings.Join(chunks, "") fullContent = searchRefFilter.Process(fullContent) + searchRefFilter.Flush() fullReasoning := strings.Join(reasoningChunks, "") fullReasoning = searchRefFilter.Process(fullReasoning) + searchRefFilter.Flush() // 检查是否有内容 if fullContent == "" && fullReasoning == "" { result.HasContent = false result.ErrorMessage = "empty response" return result } result.HasContent = true // 检测工具调用 stopReason := "stop" var toolCalls []ToolCall if len(tools) > 0 { toolCalls = ExtractToolInvocationsWithFallback(fullContent, fullReasoning) fullContent = RemoveToolJSONContent(fullContent) if len(toolCalls) > 0 { stopReason = "tool_calls" } } // 计算输出 token outputTokens = CountTokens(fullContent) + CountTokens(fullReasoning) result.OutputTokens = outputTokens // 写入响应 w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Request-Id", completionID) response := ChatCompletionResponse{ ID: completionID, Object: "chat.completion", Created: time.Now().Unix(), Model: modelName, Choices: []Choice{{ Index: 0, Message: &MessageResp{ Role: "assistant", Content: fullContent, ReasoningContent: fullReasoning, ToolCalls: toolCalls, }, FinishReason: &stopReason, }}, Usage: &Usage{ PromptTokens: inputTokens, CompletionTokens: outputTokens, TotalTokens: inputTokens + outputTokens, }, SystemFingerprint: "openai", } json.NewEncoder(w).Encode(response) return result } func HandleModels(w http.ResponseWriter, r *http.Request) { models := GetAvailableModels() response := ModelsResponse{ Object: "list", Data: models, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) }