主要改动: - request_transformer: thinking block 缺少签名时降级为文本而非丢弃,保留内容并在上层禁用 thinking mode - antigravity_gateway_service: 新增两阶段降级策略,先处理 thinking blocks,如仍失败且涉及 tool 签名错误则进一步降级 tool blocks - gateway_request: 新增 FilterSignatureSensitiveBlocksForRetry 函数,支持将 tool_use/tool_result 降级为文本 - gateway_request: 改进 FilterThinkingBlocksForRetry,禁用顶层 thinking 配置以避免结构约束冲突 - gateway_service: 实现保守的两阶段重试逻辑,优先保留内容,仅在必要时降级工具调用 - 新增 antigravity_gateway_service_test.go 测试签名块剥离逻辑 - 更新相关测试用例以验证降级行为 此修复解决了跨平台/账户切换时历史消息签名失效导致的请求失败问题。
245 lines
7.4 KiB
Go
245 lines
7.4 KiB
Go
package antigravity
|
||
|
||
import (
|
||
"encoding/json"
|
||
"testing"
|
||
)
|
||
|
||
// TestBuildParts_ThinkingBlockWithoutSignature 测试thinking block无signature时的处理
|
||
func TestBuildParts_ThinkingBlockWithoutSignature(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
content string
|
||
allowDummyThought bool
|
||
expectedParts int
|
||
description string
|
||
}{
|
||
{
|
||
name: "Claude model - downgrade thinking to text without signature",
|
||
content: `[
|
||
{"type": "text", "text": "Hello"},
|
||
{"type": "thinking", "thinking": "Let me think...", "signature": ""},
|
||
{"type": "text", "text": "World"}
|
||
]`,
|
||
allowDummyThought: false,
|
||
expectedParts: 3, // thinking 内容降级为普通 text part
|
||
description: "Claude模型缺少signature时应将thinking降级为text,并在上层禁用thinking mode",
|
||
},
|
||
{
|
||
name: "Claude model - preserve thinking block with signature",
|
||
content: `[
|
||
{"type": "text", "text": "Hello"},
|
||
{"type": "thinking", "thinking": "Let me think...", "signature": "sig_real_123"},
|
||
{"type": "text", "text": "World"}
|
||
]`,
|
||
allowDummyThought: false,
|
||
expectedParts: 3,
|
||
description: "Claude模型应透传带 signature 的 thinking block(用于 Vertex 签名链路)",
|
||
},
|
||
{
|
||
name: "Gemini model - use dummy signature",
|
||
content: `[
|
||
{"type": "text", "text": "Hello"},
|
||
{"type": "thinking", "thinking": "Let me think...", "signature": ""},
|
||
{"type": "text", "text": "World"}
|
||
]`,
|
||
allowDummyThought: true,
|
||
expectedParts: 3, // 三个block都保留,thinking使用dummy signature
|
||
description: "Gemini模型应该为无signature的thinking block使用dummy signature",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
toolIDToName := make(map[string]string)
|
||
parts, _, err := buildParts(json.RawMessage(tt.content), toolIDToName, tt.allowDummyThought)
|
||
|
||
if err != nil {
|
||
t.Fatalf("buildParts() error = %v", err)
|
||
}
|
||
|
||
if len(parts) != tt.expectedParts {
|
||
t.Errorf("%s: got %d parts, want %d parts", tt.description, len(parts), tt.expectedParts)
|
||
}
|
||
|
||
switch tt.name {
|
||
case "Claude model - preserve thinking block with signature":
|
||
if len(parts) != 3 {
|
||
t.Fatalf("expected 3 parts, got %d", len(parts))
|
||
}
|
||
if !parts[1].Thought || parts[1].ThoughtSignature != "sig_real_123" {
|
||
t.Fatalf("expected thought part with signature sig_real_123, got thought=%v signature=%q",
|
||
parts[1].Thought, parts[1].ThoughtSignature)
|
||
}
|
||
case "Claude model - downgrade thinking to text without signature":
|
||
if len(parts) != 3 {
|
||
t.Fatalf("expected 3 parts, got %d", len(parts))
|
||
}
|
||
if parts[1].Thought {
|
||
t.Fatalf("expected downgraded text part, got thought=%v signature=%q",
|
||
parts[1].Thought, parts[1].ThoughtSignature)
|
||
}
|
||
if parts[1].Text != "Let me think..." {
|
||
t.Fatalf("expected downgraded text %q, got %q", "Let me think...", parts[1].Text)
|
||
}
|
||
case "Gemini model - use dummy signature":
|
||
if len(parts) != 3 {
|
||
t.Fatalf("expected 3 parts, got %d", len(parts))
|
||
}
|
||
if !parts[1].Thought || parts[1].ThoughtSignature != dummyThoughtSignature {
|
||
t.Fatalf("expected dummy thought signature, got thought=%v signature=%q",
|
||
parts[1].Thought, parts[1].ThoughtSignature)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestBuildParts_ToolUseSignatureHandling(t *testing.T) {
|
||
content := `[
|
||
{"type": "tool_use", "id": "t1", "name": "Bash", "input": {"command": "ls"}, "signature": "sig_tool_abc"}
|
||
]`
|
||
|
||
t.Run("Gemini uses dummy tool_use signature", func(t *testing.T) {
|
||
toolIDToName := make(map[string]string)
|
||
parts, _, err := buildParts(json.RawMessage(content), toolIDToName, true)
|
||
if err != nil {
|
||
t.Fatalf("buildParts() error = %v", err)
|
||
}
|
||
if len(parts) != 1 || parts[0].FunctionCall == nil {
|
||
t.Fatalf("expected 1 functionCall part, got %+v", parts)
|
||
}
|
||
if parts[0].ThoughtSignature != dummyThoughtSignature {
|
||
t.Fatalf("expected dummy tool signature %q, got %q", dummyThoughtSignature, parts[0].ThoughtSignature)
|
||
}
|
||
})
|
||
|
||
t.Run("Claude model - preserve valid signature for tool_use", func(t *testing.T) {
|
||
toolIDToName := make(map[string]string)
|
||
parts, _, err := buildParts(json.RawMessage(content), toolIDToName, false)
|
||
if err != nil {
|
||
t.Fatalf("buildParts() error = %v", err)
|
||
}
|
||
if len(parts) != 1 || parts[0].FunctionCall == nil {
|
||
t.Fatalf("expected 1 functionCall part, got %+v", parts)
|
||
}
|
||
// Claude 模型应透传有效的 signature(Vertex/Google 需要完整签名链路)
|
||
if parts[0].ThoughtSignature != "sig_tool_abc" {
|
||
t.Fatalf("expected preserved tool signature %q, got %q", "sig_tool_abc", parts[0].ThoughtSignature)
|
||
}
|
||
})
|
||
}
|
||
|
||
// TestBuildTools_CustomTypeTools 测试custom类型工具转换
|
||
func TestBuildTools_CustomTypeTools(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
tools []ClaudeTool
|
||
expectedLen int
|
||
description string
|
||
}{
|
||
{
|
||
name: "Standard tool format",
|
||
tools: []ClaudeTool{
|
||
{
|
||
Name: "get_weather",
|
||
Description: "Get weather information",
|
||
InputSchema: map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"location": map[string]any{"type": "string"},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
expectedLen: 1,
|
||
description: "标准工具格式应该正常转换",
|
||
},
|
||
{
|
||
name: "Custom type tool (MCP format)",
|
||
tools: []ClaudeTool{
|
||
{
|
||
Type: "custom",
|
||
Name: "mcp_tool",
|
||
Custom: &ClaudeCustomToolSpec{
|
||
Description: "MCP tool description",
|
||
InputSchema: map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"param": map[string]any{"type": "string"},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
expectedLen: 1,
|
||
description: "Custom类型工具应该从Custom字段读取description和input_schema",
|
||
},
|
||
{
|
||
name: "Mixed standard and custom tools",
|
||
tools: []ClaudeTool{
|
||
{
|
||
Name: "standard_tool",
|
||
Description: "Standard tool",
|
||
InputSchema: map[string]any{"type": "object"},
|
||
},
|
||
{
|
||
Type: "custom",
|
||
Name: "custom_tool",
|
||
Custom: &ClaudeCustomToolSpec{
|
||
Description: "Custom tool",
|
||
InputSchema: map[string]any{"type": "object"},
|
||
},
|
||
},
|
||
},
|
||
expectedLen: 1, // 返回一个GeminiToolDeclaration,包含2个function declarations
|
||
description: "混合标准和custom工具应该都能正确转换",
|
||
},
|
||
{
|
||
name: "Invalid custom tool - nil Custom field",
|
||
tools: []ClaudeTool{
|
||
{
|
||
Type: "custom",
|
||
Name: "invalid_custom",
|
||
// Custom 为 nil
|
||
},
|
||
},
|
||
expectedLen: 0, // 应该被跳过
|
||
description: "Custom字段为nil的custom工具应该被跳过",
|
||
},
|
||
{
|
||
name: "Invalid custom tool - nil InputSchema",
|
||
tools: []ClaudeTool{
|
||
{
|
||
Type: "custom",
|
||
Name: "invalid_custom",
|
||
Custom: &ClaudeCustomToolSpec{
|
||
Description: "Invalid",
|
||
// InputSchema 为 nil
|
||
},
|
||
},
|
||
},
|
||
expectedLen: 0, // 应该被跳过
|
||
description: "InputSchema为nil的custom工具应该被跳过",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
result := buildTools(tt.tools)
|
||
|
||
if len(result) != tt.expectedLen {
|
||
t.Errorf("%s: got %d tool declarations, want %d", tt.description, len(result), tt.expectedLen)
|
||
}
|
||
|
||
// 验证function declarations存在
|
||
if len(result) > 0 && result[0].FunctionDeclarations != nil {
|
||
if len(result[0].FunctionDeclarations) != len(tt.tools) {
|
||
t.Errorf("%s: got %d function declarations, want %d",
|
||
tt.description, len(result[0].FunctionDeclarations), len(tt.tools))
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|