fix(thinking): 优化 thinking block 签名错误重试逻辑

- FilterThinkingBlocksForRetry: 将 thinking block 转换为 text block 而非直接删除
- stripThinkingFromClaudeRequest: Antigravity 网关同步采用转换策略
- 统一处理 thinking/redacted_thinking/无 type 字段的 thinking block
- 保留 thinking 内容,避免上下文丢失
This commit is contained in:
ianshaw
2026-01-03 17:07:54 -08:00
parent 519b0b245a
commit b0d41823bd
2 changed files with 337 additions and 26 deletions

View File

@@ -81,6 +81,115 @@ func ParseGatewayRequest(body []byte) (*ParsedRequest, error) {
// - When thinking.type == "enabled": Only remove thinking blocks without valid signatures
// (blocks with missing/empty/dummy signatures that would cause 400 errors)
func FilterThinkingBlocks(body []byte) []byte {
return filterThinkingBlocksInternal(body, false)
}
// FilterThinkingBlocksForRetry converts thinking blocks to text blocks for retry scenarios
// This preserves the thinking content while avoiding signature validation errors.
// Note: redacted_thinking blocks are removed because they cannot be converted to text.
func FilterThinkingBlocksForRetry(body []byte) []byte {
// Fast path: check for presence of thinking-related keys
if !bytes.Contains(body, []byte(`"type":"thinking"`)) &&
!bytes.Contains(body, []byte(`"type": "thinking"`)) &&
!bytes.Contains(body, []byte(`"type":"redacted_thinking"`)) &&
!bytes.Contains(body, []byte(`"type": "redacted_thinking"`)) &&
!bytes.Contains(body, []byte(`"thinking":`)) &&
!bytes.Contains(body, []byte(`"thinking" :`)) {
return body
}
var req map[string]any
if err := json.Unmarshal(body, &req); err != nil {
return body
}
messages, ok := req["messages"].([]any)
if !ok {
return body
}
modified := false
for _, msg := range messages {
msgMap, ok := msg.(map[string]any)
if !ok {
continue
}
content, ok := msgMap["content"].([]any)
if !ok {
continue
}
newContent := make([]any, 0, len(content))
modifiedThisMsg := false
for _, block := range content {
blockMap, ok := block.(map[string]any)
if !ok {
newContent = append(newContent, block)
continue
}
blockType, _ := blockMap["type"].(string)
// Case 1: Standard thinking block - convert to text
if blockType == "thinking" {
thinkingText, _ := blockMap["thinking"].(string)
if thinkingText != "" {
newContent = append(newContent, map[string]any{
"type": "text",
"text": thinkingText,
})
}
modifiedThisMsg = true
continue
}
// Case 2: Redacted thinking block - remove (cannot convert encrypted content)
if blockType == "redacted_thinking" {
modifiedThisMsg = true
continue
}
// Case 3: Untyped block with "thinking" field - convert to text
if blockType == "" {
if thinkingText, hasThinking := blockMap["thinking"].(string); hasThinking {
if thinkingText != "" {
newContent = append(newContent, map[string]any{
"type": "text",
"text": thinkingText,
})
}
modifiedThisMsg = true
continue
}
}
newContent = append(newContent, block)
}
if modifiedThisMsg {
msgMap["content"] = newContent
modified = true
}
}
if !modified {
return body
}
newBody, err := json.Marshal(req)
if err != nil {
return body
}
return newBody
}
// filterThinkingBlocksInternal removes invalid thinking blocks from request
// Strategy:
// - When thinking.type != "enabled": Remove all thinking blocks
// - When thinking.type == "enabled": Only remove thinking blocks without valid signatures
func filterThinkingBlocksInternal(body []byte, _ bool) []byte {
// Fast path: if body doesn't contain "thinking", skip parsing
if !bytes.Contains(body, []byte(`"type":"thinking"`)) &&
!bytes.Contains(body, []byte(`"type": "thinking"`)) &&
@@ -93,7 +202,7 @@ func FilterThinkingBlocks(body []byte) []byte {
var req map[string]any
if err := json.Unmarshal(body, &req); err != nil {
return body // Return original on parse error
return body
}
// Check if thinking is enabled
@@ -106,7 +215,7 @@ func FilterThinkingBlocks(body []byte) []byte {
messages, ok := req["messages"].([]any)
if !ok {
return body // No messages array
return body
}
filtered := false
@@ -122,7 +231,6 @@ func FilterThinkingBlocks(body []byte) []byte {
continue
}
// Filter thinking blocks from content array
newContent := make([]any, 0, len(content))
filteredThisMessage := false
@@ -135,30 +243,24 @@ func FilterThinkingBlocks(body []byte) []byte {
blockType, _ := blockMap["type"].(string)
// Handle thinking/redacted_thinking blocks
if blockType == "thinking" || blockType == "redacted_thinking" {
// When thinking is enabled and this is an assistant message,
// only keep thinking blocks with valid (non-empty, non-dummy) signatures
// only keep thinking blocks with valid signatures
if thinkingEnabled && role == "assistant" {
signature, _ := blockMap["signature"].(string)
// Keep blocks with valid signatures, remove those without
if signature != "" && signature != "skip_thought_signature_validator" {
newContent = append(newContent, block)
continue
}
}
filtered = true
filteredThisMessage = true
continue
}
// Some clients send the "thinking" object without a "type" discriminator.
// We intentionally do not drop other typed blocks (e.g. tool_use) that might
// legitimately contain a "thinking" key inside their payload.
// Handle blocks without type discriminator but with "thinking" key
if blockType == "" {
if thinkingContent, hasThinking := blockMap["thinking"]; hasThinking {
_ = thinkingContent
if _, hasThinking := blockMap["thinking"]; hasThinking {
filtered = true
filteredThisMessage = true
continue
@@ -174,14 +276,12 @@ func FilterThinkingBlocks(body []byte) []byte {
}
if !filtered {
return body // No changes needed
return body
}
// Re-serialize
newBody, err := json.Marshal(req)
if err != nil {
return body // Return original on marshal error
return body
}
return newBody
}