fix(api): 修复 thinking 块被意外修改导致的 400 错误
问题描述: 使用扩展思考功能时,偶现以下错误: "thinking or redacted_thinking blocks in the latest assistant message cannot be modified" 根因分析: 当代理服务修改请求体中的某些字段时(如 metadata.user_id、model), 使用 map[string]any 解析整个 JSON 后重新序列化,导致: 1. 字段顺序改变(Go map 序列化按字母排序) 2. 数字格式变化(如 1.0 → 1) 3. Unicode 转义变化 Claude API 对 thinking 块进行字节级验证,任何变化都会触发错误。 修复内容: 1. identity_service.go - RewriteUserID/RewriteUserIDWithMasking 使用 json.RawMessage 保留其他字段的原始字节 2. gateway_service.go - replaceModelInBody 使用 json.RawMessage 保留其他字段的原始字节 3. gateway_service.go - normalizeClaudeOAuthRequestBody 保留 messages 的原始字节,跳过包含 thinking 块的消息修改 4. gateway_service.go - isThinkingBlockSignatureError 添加 "cannot be modified" 错误检测,触发自动重试 5. antigravity_gateway_service.go - isSignatureRelatedError 添加 "cannot be modified" 错误检测 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1003,6 +1003,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
|
||||
}
|
||||
|
||||
|
||||
@@ -585,12 +585,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
|
||||
@@ -787,12 +793,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) {
|
||||
@@ -800,6 +815,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 {
|
||||
@@ -817,6 +833,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
||||
sanitized := sanitizeSystemText(text)
|
||||
if sanitized != text {
|
||||
block["text"] = sanitized
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -827,6 +844,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
||||
if normalized != rawModel {
|
||||
req["model"] = normalized
|
||||
modelID = normalized
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -842,16 +860,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
|
||||
}
|
||||
@@ -880,11 +901,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)
|
||||
@@ -895,6 +920,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 {
|
||||
@@ -907,6 +950,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
||||
normalized := normalizeToolNameForClaude(name, toolNameMap)
|
||||
if normalized != "" && normalized != name {
|
||||
blockMap["name"] = normalized
|
||||
messagesModified = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -916,6 +960,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
||||
if opts.stripSystemCacheControl {
|
||||
if system, ok := req["system"]; ok {
|
||||
_ = stripCacheControlFromSystemBlocks(system)
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,12 +972,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
|
||||
@@ -3621,6 +3700,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") {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user