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
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -585,12 +585,18 @@ func (s *GatewayService) hashContent(content string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// replaceModelInBody 替换请求体中的model字段
|
// replaceModelInBody 替换请求体中的model字段
|
||||||
|
// 使用 json.RawMessage 保留其他字段的原始字节,避免 thinking 块等内容被修改
|
||||||
func (s *GatewayService) replaceModelInBody(body []byte, newModel string) []byte {
|
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 {
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
req["model"] = newModel
|
// 只序列化 model 字段
|
||||||
|
modelBytes, err := json.Marshal(newModel)
|
||||||
|
if err != nil {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
req["model"] = modelBytes
|
||||||
newBody, err := json.Marshal(req)
|
newBody, err := json.Marshal(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return body
|
return body
|
||||||
@@ -787,12 +793,21 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
|||||||
if len(body) == 0 {
|
if len(body) == 0 {
|
||||||
return body, modelID, nil
|
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
|
var req map[string]any
|
||||||
if err := json.Unmarshal(body, &req); err != nil {
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
return body, modelID, nil
|
return body, modelID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
toolNameMap := make(map[string]string)
|
toolNameMap := make(map[string]string)
|
||||||
|
modified := false
|
||||||
|
|
||||||
if system, ok := req["system"]; ok {
|
if system, ok := req["system"]; ok {
|
||||||
switch v := system.(type) {
|
switch v := system.(type) {
|
||||||
@@ -800,6 +815,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
|||||||
sanitized := sanitizeSystemText(v)
|
sanitized := sanitizeSystemText(v)
|
||||||
if sanitized != v {
|
if sanitized != v {
|
||||||
req["system"] = sanitized
|
req["system"] = sanitized
|
||||||
|
modified = true
|
||||||
}
|
}
|
||||||
case []any:
|
case []any:
|
||||||
for _, item := range v {
|
for _, item := range v {
|
||||||
@@ -817,6 +833,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
|||||||
sanitized := sanitizeSystemText(text)
|
sanitized := sanitizeSystemText(text)
|
||||||
if sanitized != text {
|
if sanitized != text {
|
||||||
block["text"] = sanitized
|
block["text"] = sanitized
|
||||||
|
modified = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -827,6 +844,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
|||||||
if normalized != rawModel {
|
if normalized != rawModel {
|
||||||
req["model"] = normalized
|
req["model"] = normalized
|
||||||
modelID = normalized
|
modelID = normalized
|
||||||
|
modified = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,16 +860,19 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
|||||||
normalized := normalizeToolNameForClaude(name, toolNameMap)
|
normalized := normalizeToolNameForClaude(name, toolNameMap)
|
||||||
if normalized != "" && normalized != name {
|
if normalized != "" && normalized != name {
|
||||||
toolMap["name"] = normalized
|
toolMap["name"] = normalized
|
||||||
|
modified = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if desc, ok := toolMap["description"].(string); ok {
|
if desc, ok := toolMap["description"].(string); ok {
|
||||||
sanitized := sanitizeToolDescription(desc)
|
sanitized := sanitizeToolDescription(desc)
|
||||||
if sanitized != desc {
|
if sanitized != desc {
|
||||||
toolMap["description"] = sanitized
|
toolMap["description"] = sanitized
|
||||||
|
modified = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if schema, ok := toolMap["input_schema"]; ok {
|
if schema, ok := toolMap["input_schema"]; ok {
|
||||||
normalizeToolInputSchema(schema, toolNameMap)
|
normalizeToolInputSchema(schema, toolNameMap)
|
||||||
|
modified = true
|
||||||
}
|
}
|
||||||
tools[idx] = toolMap
|
tools[idx] = toolMap
|
||||||
}
|
}
|
||||||
@@ -880,11 +901,15 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
|||||||
normalizedTools[normalized] = value
|
normalizedTools[normalized] = value
|
||||||
}
|
}
|
||||||
req["tools"] = normalizedTools
|
req["tools"] = normalizedTools
|
||||||
|
modified = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
req["tools"] = []any{}
|
req["tools"] = []any{}
|
||||||
|
modified = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理 messages 中的 tool_use 块,但保留包含 thinking 块的消息的原始字节
|
||||||
|
messagesModified := false
|
||||||
if messages, ok := req["messages"].([]any); ok {
|
if messages, ok := req["messages"].([]any); ok {
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
msgMap, ok := msg.(map[string]any)
|
msgMap, ok := msg.(map[string]any)
|
||||||
@@ -895,6 +920,24 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
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 {
|
for _, block := range content {
|
||||||
blockMap, ok := block.(map[string]any)
|
blockMap, ok := block.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -907,6 +950,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
|||||||
normalized := normalizeToolNameForClaude(name, toolNameMap)
|
normalized := normalizeToolNameForClaude(name, toolNameMap)
|
||||||
if normalized != "" && normalized != name {
|
if normalized != "" && normalized != name {
|
||||||
blockMap["name"] = normalized
|
blockMap["name"] = normalized
|
||||||
|
messagesModified = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -916,6 +960,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
|||||||
if opts.stripSystemCacheControl {
|
if opts.stripSystemCacheControl {
|
||||||
if system, ok := req["system"]; ok {
|
if system, ok := req["system"]; ok {
|
||||||
_ = stripCacheControlFromSystemBlocks(system)
|
_ = 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 == "" {
|
if existing, ok := metadata["user_id"].(string); !ok || existing == "" {
|
||||||
metadata["user_id"] = opts.metadataUserID
|
metadata["user_id"] = opts.metadataUserID
|
||||||
|
modified = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(req, "temperature")
|
if _, hasTemp := req["temperature"]; hasTemp {
|
||||||
delete(req, "tool_choice")
|
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)
|
newBody, err := json.Marshal(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return body, modelID, toolNameMap
|
return body, modelID, toolNameMap
|
||||||
@@ -3621,6 +3700,13 @@ func (s *GatewayService) isThinkingBlockSignatureError(respBody []byte) bool {
|
|||||||
return true
|
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 后导致的)
|
// 检测空消息内容错误(可能是过滤 thinking blocks 后导致的)
|
||||||
// 例如: "all messages must have non-empty content"
|
// 例如: "all messages must have non-empty content"
|
||||||
if strings.Contains(msg, "non-empty content") || strings.Contains(msg, "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
|
// RewriteUserID 重写body中的metadata.user_id
|
||||||
// 输入格式:user_{clientId}_account__session_{sessionUUID}
|
// 输入格式:user_{clientId}_account__session_{sessionUUID}
|
||||||
// 输出格式:user_{cachedClientID}_account_{accountUUID}_session_{newHash}
|
// 输出格式:user_{cachedClientID}_account_{accountUUID}_session_{newHash}
|
||||||
|
//
|
||||||
|
// 重要:此函数使用 json.RawMessage 保留其他字段的原始字节,
|
||||||
|
// 避免重新序列化导致 thinking 块等内容被修改。
|
||||||
func (s *IdentityService) RewriteUserID(body []byte, accountID int64, accountUUID, cachedClientID string) ([]byte, error) {
|
func (s *IdentityService) RewriteUserID(body []byte, accountID int64, accountUUID, cachedClientID string) ([]byte, error) {
|
||||||
if len(body) == 0 || accountUUID == "" || cachedClientID == "" {
|
if len(body) == 0 || accountUUID == "" || cachedClientID == "" {
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析JSON
|
// 使用 RawMessage 保留其他字段的原始字节
|
||||||
var reqMap map[string]any
|
var reqMap map[string]json.RawMessage
|
||||||
if err := json.Unmarshal(body, &reqMap); err != nil {
|
if err := json.Unmarshal(body, &reqMap); err != nil {
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata, ok := reqMap["metadata"].(map[string]any)
|
// 解析 metadata 字段
|
||||||
|
metadataRaw, ok := reqMap["metadata"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return body, nil
|
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)
|
userID, ok := metadata["user_id"].(string)
|
||||||
if !ok || userID == "" {
|
if !ok || userID == "" {
|
||||||
return body, nil
|
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)
|
newUserID := fmt.Sprintf("user_%s_account_%s_session_%s", cachedClientID, accountUUID, newSessionHash)
|
||||||
|
|
||||||
metadata["user_id"] = newUserID
|
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)
|
return json.Marshal(reqMap)
|
||||||
}
|
}
|
||||||
@@ -215,6 +230,9 @@ func (s *IdentityService) RewriteUserID(body []byte, accountID int64, accountUUI
|
|||||||
// RewriteUserIDWithMasking 重写body中的metadata.user_id,支持会话ID伪装
|
// RewriteUserIDWithMasking 重写body中的metadata.user_id,支持会话ID伪装
|
||||||
// 如果账号启用了会话ID伪装(session_id_masking_enabled),
|
// 如果账号启用了会话ID伪装(session_id_masking_enabled),
|
||||||
// 则在完成常规重写后,将 session 部分替换为固定的伪装ID(15分钟内保持不变)
|
// 则在完成常规重写后,将 session 部分替换为固定的伪装ID(15分钟内保持不变)
|
||||||
|
//
|
||||||
|
// 重要:此函数使用 json.RawMessage 保留其他字段的原始字节,
|
||||||
|
// 避免重新序列化导致 thinking 块等内容被修改。
|
||||||
func (s *IdentityService) RewriteUserIDWithMasking(ctx context.Context, body []byte, account *Account, accountUUID, cachedClientID string) ([]byte, error) {
|
func (s *IdentityService) RewriteUserIDWithMasking(ctx context.Context, body []byte, account *Account, accountUUID, cachedClientID string) ([]byte, error) {
|
||||||
// 先执行常规的 RewriteUserID 逻辑
|
// 先执行常规的 RewriteUserID 逻辑
|
||||||
newBody, err := s.RewriteUserID(body, account.ID, accountUUID, cachedClientID)
|
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
|
return newBody, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析重写后的 body,提取 user_id
|
// 使用 RawMessage 保留其他字段的原始字节
|
||||||
var reqMap map[string]any
|
var reqMap map[string]json.RawMessage
|
||||||
if err := json.Unmarshal(newBody, &reqMap); err != nil {
|
if err := json.Unmarshal(newBody, &reqMap); err != nil {
|
||||||
return newBody, nil
|
return newBody, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata, ok := reqMap["metadata"].(map[string]any)
|
// 解析 metadata 字段
|
||||||
|
metadataRaw, ok := reqMap["metadata"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return newBody, nil
|
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)
|
userID, ok := metadata["user_id"].(string)
|
||||||
if !ok || userID == "" {
|
if !ok || userID == "" {
|
||||||
return newBody, nil
|
return newBody, nil
|
||||||
@@ -278,7 +302,13 @@ func (s *IdentityService) RewriteUserIDWithMasking(ctx context.Context, body []b
|
|||||||
)
|
)
|
||||||
|
|
||||||
metadata["user_id"] = newUserID
|
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)
|
return json.Marshal(reqMap)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user