diff --git a/backend/internal/integration/e2e_gateway_test.go b/backend/internal/integration/e2e_gateway_test.go index 38505418..70343a3d 100644 --- a/backend/internal/integration/e2e_gateway_test.go +++ b/backend/internal/integration/e2e_gateway_test.go @@ -630,3 +630,102 @@ func testClaudeThinkingWithToolHistory(t *testing.T, model string) { } t.Logf("✅ thinking 模式工具调用测试通过, id=%v", result["id"]) } + +// TestClaudeMessagesWithInvalidThinkingSignature 测试历史 thinking block 带有无效 signature 的场景 +// 验证:系统应使用 dummy signature 替换历史的无效 signature +func TestClaudeMessagesWithInvalidThinkingSignature(t *testing.T) { + models := []string{ + "claude-haiku-4-5-20251001", // gemini-3-flash + } + for i, model := range models { + if i > 0 { + time.Sleep(testInterval) + } + t.Run(model+"_无效thinking签名", func(t *testing.T) { + testClaudeWithInvalidThinkingSignature(t, model) + }) + } +} + +func testClaudeWithInvalidThinkingSignature(t *testing.T, model string) { + url := baseURL + "/v1/messages" + + // 模拟历史对话包含 thinking block 带有无效/过期的 signature + payload := map[string]any{ + "model": model, + "max_tokens": 200, + "stream": false, + // 开启 thinking 模式 + "thinking": map[string]any{ + "type": "enabled", + "budget_tokens": 1024, + }, + "messages": []any{ + map[string]any{ + "role": "user", + "content": "What is 2+2?", + }, + // assistant 消息包含 thinking block 和无效 signature + map[string]any{ + "role": "assistant", + "content": []map[string]any{ + { + "type": "thinking", + "thinking": "Let me calculate 2+2...", + "signature": "invalid_expired_signature_abc123", // 模拟过期的 signature + }, + { + "type": "text", + "text": "2+2 equals 4.", + }, + }, + }, + map[string]any{ + "role": "user", + "content": "What is 3+3?", + }, + }, + } + body, _ := json.Marshal(payload) + + req, _ := http.NewRequest("POST", url, bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+claudeAPIKey) + req.Header.Set("anthropic-version", "2023-06-01") + + client := &http.Client{Timeout: 60 * time.Second} + resp, err := client.Do(req) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + + // 400 错误说明 signature 处理失败 + if resp.StatusCode == 400 { + t.Fatalf("无效 thinking signature 处理失败,收到 400 错误: %s", string(respBody)) + } + + if resp.StatusCode == 503 { + t.Skipf("账号暂时不可用 (503): %s", string(respBody)) + } + + if resp.StatusCode == 429 { + t.Skipf("请求被限流 (429): %s", string(respBody)) + } + + if resp.StatusCode != 200 { + t.Fatalf("HTTP %d: %s", resp.StatusCode, string(respBody)) + } + + var result map[string]any + if err := json.Unmarshal(respBody, &result); err != nil { + t.Fatalf("解析响应失败: %v", err) + } + + if result["type"] != "message" { + t.Errorf("期望 type=message, 得到 %v", result["type"]) + } + t.Logf("✅ 无效 thinking signature 处理测试通过, id=%v", result["id"]) +} diff --git a/backend/internal/pkg/antigravity/request_transformer.go b/backend/internal/pkg/antigravity/request_transformer.go index a41168ed..25d54714 100644 --- a/backend/internal/pkg/antigravity/request_transformer.go +++ b/backend/internal/pkg/antigravity/request_transformer.go @@ -139,8 +139,12 @@ func buildContents(messages []ClaudeMessage, toolIDToName map[string]string, isT } } if !hasThoughtPart && len(parts) > 0 { - // 在开头添加 dummy thinking block - parts = append([]GeminiPart{{Text: "Thinking...", Thought: true}}, parts...) + // 在开头添加 dummy thinking block(需要 signature) + parts = append([]GeminiPart{{ + Text: "Thinking...", + Thought: true, + ThoughtSignature: dummyThoughtSignature, + }}, parts...) } } @@ -192,8 +196,10 @@ func buildParts(content json.RawMessage, toolIDToName map[string]string, isThink Text: block.Thinking, Thought: true, } - if block.Signature != "" { - part.ThoughtSignature = block.Signature + // 历史 thinking block 的 signature 可能已过期,统一使用 dummy signature + // 参考: https://ai.google.dev/gemini-api/docs/thought-signatures + if isThinkingEnabled { + part.ThoughtSignature = dummyThoughtSignature } parts = append(parts, part) @@ -213,18 +219,14 @@ func buildParts(content json.RawMessage, toolIDToName map[string]string, isThink toolIDToName[block.ID] = block.Name } + // 与 proxycast 保持一致:function_call 无条件添加 thought_signature part := GeminiPart{ FunctionCall: &GeminiFunctionCall{ Name: block.Name, Args: block.Input, ID: block.ID, }, - } - // Gemini 3 要求 thinking 模式下 functionCall 必须有 thought_signature - if block.Signature != "" { - part.ThoughtSignature = block.Signature - } else if isThinkingEnabled { - part.ThoughtSignature = dummyThoughtSignature + ThoughtSignature: dummyThoughtSignature, } parts = append(parts, part)