Merge pull request #468 from s-Joshua-s/fix/thinking-block-modification-error
fix(api): 修复 thinking 块被意外修改导致的 400 错误
This commit is contained in:
@@ -1168,6 +1168,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -603,12 +603,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
|
||||||
@@ -805,12 +811,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) {
|
||||||
@@ -818,6 +833,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 {
|
||||||
@@ -835,6 +851,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -845,6 +862,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -860,16 +878,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
|
||||||
}
|
}
|
||||||
@@ -898,11 +919,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)
|
||||||
@@ -913,6 +938,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 {
|
||||||
@@ -925,6 +968,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -934,6 +978,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,12 +990,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
|
||||||
@@ -3672,6 +3751,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