diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index d16c4259..cf7e35fc 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -1168,6 +1168,12 @@ func isSignatureRelatedError(respBody []byte) bool { return true } + // Detect thinking block modification errors: + // "thinking or redacted_thinking blocks in the latest assistant message cannot be modified" + if strings.Contains(msg, "cannot be modified") && (strings.Contains(msg, "thinking") || strings.Contains(msg, "redacted_thinking")) { + return true + } + return false } diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 9b31a9c6..79fda860 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -603,12 +603,18 @@ func (s *GatewayService) hashContent(content string) string { } // replaceModelInBody 替换请求体中的model字段 +// 使用 json.RawMessage 保留其他字段的原始字节,避免 thinking 块等内容被修改 func (s *GatewayService) replaceModelInBody(body []byte, newModel string) []byte { - var req map[string]any + var req map[string]json.RawMessage if err := json.Unmarshal(body, &req); err != nil { return body } - req["model"] = newModel + // 只序列化 model 字段 + modelBytes, err := json.Marshal(newModel) + if err != nil { + return body + } + req["model"] = modelBytes newBody, err := json.Marshal(req) if err != nil { return body @@ -805,12 +811,21 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu if len(body) == 0 { return body, modelID, nil } + + // 使用 json.RawMessage 保留 messages 的原始字节,避免 thinking 块被修改 + var reqRaw map[string]json.RawMessage + if err := json.Unmarshal(body, &reqRaw); err != nil { + return body, modelID, nil + } + + // 同时解析为 map[string]any 用于修改非 messages 字段 var req map[string]any if err := json.Unmarshal(body, &req); err != nil { return body, modelID, nil } toolNameMap := make(map[string]string) + modified := false if system, ok := req["system"]; ok { switch v := system.(type) { @@ -818,6 +833,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu sanitized := sanitizeSystemText(v) if sanitized != v { req["system"] = sanitized + modified = true } case []any: for _, item := range v { @@ -835,6 +851,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu sanitized := sanitizeSystemText(text) if sanitized != text { block["text"] = sanitized + modified = true } } } @@ -845,6 +862,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu if normalized != rawModel { req["model"] = normalized modelID = normalized + modified = true } } @@ -860,16 +878,19 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu normalized := normalizeToolNameForClaude(name, toolNameMap) if normalized != "" && normalized != name { toolMap["name"] = normalized + modified = true } } if desc, ok := toolMap["description"].(string); ok { sanitized := sanitizeToolDescription(desc) if sanitized != desc { toolMap["description"] = sanitized + modified = true } } if schema, ok := toolMap["input_schema"]; ok { normalizeToolInputSchema(schema, toolNameMap) + modified = true } tools[idx] = toolMap } @@ -898,11 +919,15 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu normalizedTools[normalized] = value } req["tools"] = normalizedTools + modified = true } } else { req["tools"] = []any{} + modified = true } + // 处理 messages 中的 tool_use 块,但保留包含 thinking 块的消息的原始字节 + messagesModified := false if messages, ok := req["messages"].([]any); ok { for _, msg := range messages { msgMap, ok := msg.(map[string]any) @@ -913,6 +938,24 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu if !ok { continue } + // 检查此消息是否包含 thinking 块 + hasThinking := false + for _, block := range content { + blockMap, ok := block.(map[string]any) + if !ok { + continue + } + blockType, _ := blockMap["type"].(string) + if blockType == "thinking" || blockType == "redacted_thinking" { + hasThinking = true + break + } + } + // 如果包含 thinking 块,跳过此消息的修改 + if hasThinking { + continue + } + // 只修改不包含 thinking 块的消息中的 tool_use for _, block := range content { blockMap, ok := block.(map[string]any) if !ok { @@ -925,6 +968,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu normalized := normalizeToolNameForClaude(name, toolNameMap) if normalized != "" && normalized != name { blockMap["name"] = normalized + messagesModified = true } } } @@ -934,6 +978,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu if opts.stripSystemCacheControl { if system, ok := req["system"]; ok { _ = stripCacheControlFromSystemBlocks(system) + modified = true } } @@ -945,12 +990,46 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu } if existing, ok := metadata["user_id"].(string); !ok || existing == "" { metadata["user_id"] = opts.metadataUserID + modified = true } } - delete(req, "temperature") - delete(req, "tool_choice") + if _, hasTemp := req["temperature"]; hasTemp { + delete(req, "temperature") + modified = true + } + if _, hasChoice := req["tool_choice"]; hasChoice { + delete(req, "tool_choice") + modified = true + } + if !modified && !messagesModified { + return body, modelID, toolNameMap + } + + // 如果 messages 没有被修改,保留原始 messages 字节 + if !messagesModified { + // 序列化非 messages 字段 + newBody, err := json.Marshal(req) + if err != nil { + return body, modelID, toolNameMap + } + // 替换回原始的 messages + var newReq map[string]json.RawMessage + if err := json.Unmarshal(newBody, &newReq); err != nil { + return newBody, modelID, toolNameMap + } + if origMessages, ok := reqRaw["messages"]; ok { + newReq["messages"] = origMessages + } + finalBody, err := json.Marshal(newReq) + if err != nil { + return newBody, modelID, toolNameMap + } + return finalBody, modelID, toolNameMap + } + + // messages 被修改了,需要完整序列化 newBody, err := json.Marshal(req) if err != nil { return body, modelID, toolNameMap @@ -3672,6 +3751,13 @@ func (s *GatewayService) isThinkingBlockSignatureError(respBody []byte) bool { return true } + // 检测 thinking block 被修改的错误 + // 例如: "thinking or redacted_thinking blocks in the latest assistant message cannot be modified" + if strings.Contains(msg, "cannot be modified") && (strings.Contains(msg, "thinking") || strings.Contains(msg, "redacted_thinking")) { + log.Printf("[SignatureCheck] Detected thinking block modification error") + return true + } + // 检测空消息内容错误(可能是过滤 thinking blocks 后导致的) // 例如: "all messages must have non-empty content" if strings.Contains(msg, "non-empty content") || strings.Contains(msg, "empty content") { diff --git a/backend/internal/service/identity_service.go b/backend/internal/service/identity_service.go index a620ac4d..261da0ef 100644 --- a/backend/internal/service/identity_service.go +++ b/backend/internal/service/identity_service.go @@ -169,22 +169,31 @@ func (s *IdentityService) ApplyFingerprint(req *http.Request, fp *Fingerprint) { // RewriteUserID 重写body中的metadata.user_id // 输入格式:user_{clientId}_account__session_{sessionUUID} // 输出格式:user_{cachedClientID}_account_{accountUUID}_session_{newHash} +// +// 重要:此函数使用 json.RawMessage 保留其他字段的原始字节, +// 避免重新序列化导致 thinking 块等内容被修改。 func (s *IdentityService) RewriteUserID(body []byte, accountID int64, accountUUID, cachedClientID string) ([]byte, error) { if len(body) == 0 || accountUUID == "" || cachedClientID == "" { return body, nil } - // 解析JSON - var reqMap map[string]any + // 使用 RawMessage 保留其他字段的原始字节 + var reqMap map[string]json.RawMessage if err := json.Unmarshal(body, &reqMap); err != nil { return body, nil } - metadata, ok := reqMap["metadata"].(map[string]any) + // 解析 metadata 字段 + metadataRaw, ok := reqMap["metadata"] if !ok { return body, nil } + var metadata map[string]any + if err := json.Unmarshal(metadataRaw, &metadata); err != nil { + return body, nil + } + userID, ok := metadata["user_id"].(string) if !ok || userID == "" { return body, nil @@ -207,7 +216,13 @@ func (s *IdentityService) RewriteUserID(body []byte, accountID int64, accountUUI newUserID := fmt.Sprintf("user_%s_account_%s_session_%s", cachedClientID, accountUUID, newSessionHash) metadata["user_id"] = newUserID - reqMap["metadata"] = metadata + + // 只重新序列化 metadata 字段 + newMetadataRaw, err := json.Marshal(metadata) + if err != nil { + return body, nil + } + reqMap["metadata"] = newMetadataRaw return json.Marshal(reqMap) } @@ -215,6 +230,9 @@ func (s *IdentityService) RewriteUserID(body []byte, accountID int64, accountUUI // RewriteUserIDWithMasking 重写body中的metadata.user_id,支持会话ID伪装 // 如果账号启用了会话ID伪装(session_id_masking_enabled), // 则在完成常规重写后,将 session 部分替换为固定的伪装ID(15分钟内保持不变) +// +// 重要:此函数使用 json.RawMessage 保留其他字段的原始字节, +// 避免重新序列化导致 thinking 块等内容被修改。 func (s *IdentityService) RewriteUserIDWithMasking(ctx context.Context, body []byte, account *Account, accountUUID, cachedClientID string) ([]byte, error) { // 先执行常规的 RewriteUserID 逻辑 newBody, err := s.RewriteUserID(body, account.ID, accountUUID, cachedClientID) @@ -227,17 +245,23 @@ func (s *IdentityService) RewriteUserIDWithMasking(ctx context.Context, body []b return newBody, nil } - // 解析重写后的 body,提取 user_id - var reqMap map[string]any + // 使用 RawMessage 保留其他字段的原始字节 + var reqMap map[string]json.RawMessage if err := json.Unmarshal(newBody, &reqMap); err != nil { return newBody, nil } - metadata, ok := reqMap["metadata"].(map[string]any) + // 解析 metadata 字段 + metadataRaw, ok := reqMap["metadata"] if !ok { return newBody, nil } + var metadata map[string]any + if err := json.Unmarshal(metadataRaw, &metadata); err != nil { + return newBody, nil + } + userID, ok := metadata["user_id"].(string) if !ok || userID == "" { return newBody, nil @@ -278,7 +302,13 @@ func (s *IdentityService) RewriteUserIDWithMasking(ctx context.Context, body []b ) metadata["user_id"] = newUserID - reqMap["metadata"] = metadata + + // 只重新序列化 metadata 字段 + newMetadataRaw, marshalErr := json.Marshal(metadata) + if marshalErr != nil { + return newBody, nil + } + reqMap["metadata"] = newMetadataRaw return json.Marshal(reqMap) }