添加 /antigravity/v1/* 和 /antigravity/v1beta/* 路由: - 通过 ForcePlatform 中间件强制使用 antigravity 平台 - 跳过混合调度逻辑,仅调度 antigravity 账户 - 支持按分组优先查找,找不到时回退查询全部 antigravity 账户 修复 context key 类型不匹配问题: - middleware 和 service 统一使用字符串常量 "ctx_force_platform" - 解决 Go context.Value() 类型+值匹配导致的读取失败 其他改动: - 嵌入式前端中间件白名单添加 /antigravity/ 路径 - e2e 测试 Gemini 端点 URL 添加 endpointPrefix 支持
741 lines
19 KiB
Go
741 lines
19 KiB
Go
//go:build e2e
|
||
|
||
package integration
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
var (
|
||
baseURL = getEnv("BASE_URL", "http://localhost:8080")
|
||
// ENDPOINT_PREFIX: 端点前缀,支持混合模式和非混合模式测试
|
||
// - "" (默认): 使用 /v1/messages, /v1beta/models(混合模式,可调度 antigravity 账户)
|
||
// - "/antigravity": 使用 /antigravity/v1/messages, /antigravity/v1beta/models(非混合模式,仅 antigravity 账户)
|
||
endpointPrefix = getEnv("ENDPOINT_PREFIX", "")
|
||
claudeAPIKey = "sk-8e572bc3b3de92ace4f41f4256c28600ca11805732a7b693b5c44741346bbbb3"
|
||
geminiAPIKey = "sk-5950197a2085b38bbe5a1b229cc02b8ece914963fc44cacc06d497ae8b87410f"
|
||
testInterval = 1 * time.Second // 测试间隔,防止限流
|
||
)
|
||
|
||
func getEnv(key, defaultVal string) string {
|
||
if v := os.Getenv(key); v != "" {
|
||
return v
|
||
}
|
||
return defaultVal
|
||
}
|
||
|
||
// Claude 模型列表
|
||
var claudeModels = []string{
|
||
// Opus 系列
|
||
"claude-opus-4-5-thinking", // 直接支持
|
||
"claude-opus-4", // 映射到 claude-opus-4-5-thinking
|
||
"claude-opus-4-5-20251101", // 映射到 claude-opus-4-5-thinking
|
||
// Sonnet 系列
|
||
"claude-sonnet-4-5", // 直接支持
|
||
"claude-sonnet-4-5-thinking", // 直接支持
|
||
"claude-sonnet-4-5-20250929", // 映射到 claude-sonnet-4-5-thinking
|
||
"claude-3-5-sonnet-20241022", // 映射到 claude-sonnet-4-5
|
||
// Haiku 系列(映射到 gemini-3-flash)
|
||
"claude-haiku-4",
|
||
"claude-haiku-4-5",
|
||
"claude-haiku-4-5-20251001",
|
||
"claude-3-haiku-20240307",
|
||
}
|
||
|
||
// Gemini 模型列表
|
||
var geminiModels = []string{
|
||
"gemini-2.5-flash",
|
||
"gemini-2.5-flash-lite",
|
||
"gemini-3-flash",
|
||
"gemini-3-pro-low",
|
||
}
|
||
|
||
func TestMain(m *testing.M) {
|
||
mode := "混合模式"
|
||
if endpointPrefix != "" {
|
||
mode = "Antigravity 模式"
|
||
}
|
||
fmt.Printf("\n🚀 E2E Gateway Tests - %s (prefix=%q, %s)\n\n", baseURL, endpointPrefix, mode)
|
||
os.Exit(m.Run())
|
||
}
|
||
|
||
// TestClaudeModelsList 测试 GET /v1/models
|
||
func TestClaudeModelsList(t *testing.T) {
|
||
url := baseURL + endpointPrefix + "/v1/models"
|
||
|
||
req, _ := http.NewRequest("GET", url, nil)
|
||
req.Header.Set("Authorization", "Bearer "+claudeAPIKey)
|
||
|
||
client := &http.Client{Timeout: 30 * time.Second}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
t.Fatalf("请求失败: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
t.Fatalf("HTTP %d: %s", resp.StatusCode, string(body))
|
||
}
|
||
|
||
var result map[string]any
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
t.Fatalf("解析响应失败: %v", err)
|
||
}
|
||
|
||
if result["object"] != "list" {
|
||
t.Errorf("期望 object=list, 得到 %v", result["object"])
|
||
}
|
||
|
||
data, ok := result["data"].([]any)
|
||
if !ok {
|
||
t.Fatal("响应缺少 data 数组")
|
||
}
|
||
t.Logf("✅ 返回 %d 个模型", len(data))
|
||
}
|
||
|
||
// TestGeminiModelsList 测试 GET /v1beta/models
|
||
func TestGeminiModelsList(t *testing.T) {
|
||
url := baseURL + endpointPrefix + "/v1beta/models"
|
||
|
||
req, _ := http.NewRequest("GET", url, nil)
|
||
req.Header.Set("Authorization", "Bearer "+geminiAPIKey)
|
||
|
||
client := &http.Client{Timeout: 30 * time.Second}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
t.Fatalf("请求失败: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
t.Fatalf("HTTP %d: %s", resp.StatusCode, string(body))
|
||
}
|
||
|
||
var result map[string]any
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
t.Fatalf("解析响应失败: %v", err)
|
||
}
|
||
|
||
models, ok := result["models"].([]any)
|
||
if !ok {
|
||
t.Fatal("响应缺少 models 数组")
|
||
}
|
||
t.Logf("✅ 返回 %d 个模型", len(models))
|
||
}
|
||
|
||
// TestClaudeMessages 测试 Claude /v1/messages 接口
|
||
func TestClaudeMessages(t *testing.T) {
|
||
for i, model := range claudeModels {
|
||
if i > 0 {
|
||
time.Sleep(testInterval)
|
||
}
|
||
t.Run(model+"_非流式", func(t *testing.T) {
|
||
testClaudeMessage(t, model, false)
|
||
})
|
||
time.Sleep(testInterval)
|
||
t.Run(model+"_流式", func(t *testing.T) {
|
||
testClaudeMessage(t, model, true)
|
||
})
|
||
}
|
||
}
|
||
|
||
func testClaudeMessage(t *testing.T, model string, stream bool) {
|
||
url := baseURL + endpointPrefix + "/v1/messages"
|
||
|
||
payload := map[string]any{
|
||
"model": model,
|
||
"max_tokens": 50,
|
||
"stream": stream,
|
||
"messages": []map[string]string{
|
||
{"role": "user", "content": "Say 'hello' in one word."},
|
||
},
|
||
}
|
||
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()
|
||
|
||
if resp.StatusCode != 200 {
|
||
respBody, _ := io.ReadAll(resp.Body)
|
||
t.Fatalf("HTTP %d: %s", resp.StatusCode, string(respBody))
|
||
}
|
||
|
||
if stream {
|
||
// 流式:读取 SSE 事件
|
||
scanner := bufio.NewScanner(resp.Body)
|
||
eventCount := 0
|
||
for scanner.Scan() {
|
||
line := scanner.Text()
|
||
if strings.HasPrefix(line, "data:") {
|
||
eventCount++
|
||
if eventCount >= 3 {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if eventCount == 0 {
|
||
t.Fatal("未收到任何 SSE 事件")
|
||
}
|
||
t.Logf("✅ 收到 %d+ 个 SSE 事件", eventCount)
|
||
} else {
|
||
// 非流式:解析 JSON 响应
|
||
var result map[string]any
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
t.Fatalf("解析响应失败: %v", err)
|
||
}
|
||
if result["type"] != "message" {
|
||
t.Errorf("期望 type=message, 得到 %v", result["type"])
|
||
}
|
||
t.Logf("✅ 收到消息响应 id=%v", result["id"])
|
||
}
|
||
}
|
||
|
||
// TestGeminiGenerateContent 测试 Gemini /v1beta/models/:model 接口
|
||
func TestGeminiGenerateContent(t *testing.T) {
|
||
for i, model := range geminiModels {
|
||
if i > 0 {
|
||
time.Sleep(testInterval)
|
||
}
|
||
t.Run(model+"_非流式", func(t *testing.T) {
|
||
testGeminiGenerate(t, model, false)
|
||
})
|
||
time.Sleep(testInterval)
|
||
t.Run(model+"_流式", func(t *testing.T) {
|
||
testGeminiGenerate(t, model, true)
|
||
})
|
||
}
|
||
}
|
||
|
||
func testGeminiGenerate(t *testing.T, model string, stream bool) {
|
||
action := "generateContent"
|
||
if stream {
|
||
action = "streamGenerateContent"
|
||
}
|
||
url := fmt.Sprintf("%s%s/v1beta/models/%s:%s", baseURL, endpointPrefix, model, action)
|
||
if stream {
|
||
url += "?alt=sse"
|
||
}
|
||
|
||
payload := map[string]any{
|
||
"contents": []map[string]any{
|
||
{
|
||
"role": "user",
|
||
"parts": []map[string]string{
|
||
{"text": "Say 'hello' in one word."},
|
||
},
|
||
},
|
||
},
|
||
"generationConfig": map[string]int{
|
||
"maxOutputTokens": 50,
|
||
},
|
||
}
|
||
body, _ := json.Marshal(payload)
|
||
|
||
req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
|
||
req.Header.Set("Content-Type", "application/json")
|
||
req.Header.Set("Authorization", "Bearer "+geminiAPIKey)
|
||
|
||
client := &http.Client{Timeout: 60 * time.Second}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
t.Fatalf("请求失败: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != 200 {
|
||
respBody, _ := io.ReadAll(resp.Body)
|
||
t.Fatalf("HTTP %d: %s", resp.StatusCode, string(respBody))
|
||
}
|
||
|
||
if stream {
|
||
// 流式:读取 SSE 事件
|
||
scanner := bufio.NewScanner(resp.Body)
|
||
eventCount := 0
|
||
for scanner.Scan() {
|
||
line := scanner.Text()
|
||
if strings.HasPrefix(line, "data:") {
|
||
eventCount++
|
||
if eventCount >= 3 {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if eventCount == 0 {
|
||
t.Fatal("未收到任何 SSE 事件")
|
||
}
|
||
t.Logf("✅ 收到 %d+ 个 SSE 事件", eventCount)
|
||
} else {
|
||
// 非流式:解析 JSON 响应
|
||
var result map[string]any
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
t.Fatalf("解析响应失败: %v", err)
|
||
}
|
||
if _, ok := result["candidates"]; !ok {
|
||
t.Error("响应缺少 candidates 字段")
|
||
}
|
||
t.Log("✅ 收到 candidates 响应")
|
||
}
|
||
}
|
||
|
||
// TestClaudeMessagesWithComplexTools 测试带复杂工具 schema 的请求
|
||
// 模拟 Claude Code 发送的请求,包含需要清理的 JSON Schema 字段
|
||
func TestClaudeMessagesWithComplexTools(t *testing.T) {
|
||
// 测试模型列表(只测试几个代表性模型)
|
||
models := []string{
|
||
"claude-opus-4-5-20251101", // Claude 模型
|
||
"claude-haiku-4-5-20251001", // 映射到 Gemini
|
||
}
|
||
|
||
for i, model := range models {
|
||
if i > 0 {
|
||
time.Sleep(testInterval)
|
||
}
|
||
t.Run(model+"_复杂工具", func(t *testing.T) {
|
||
testClaudeMessageWithTools(t, model)
|
||
})
|
||
}
|
||
}
|
||
|
||
func testClaudeMessageWithTools(t *testing.T, model string) {
|
||
url := baseURL + endpointPrefix + "/v1/messages"
|
||
|
||
// 构造包含复杂 schema 的工具定义(模拟 Claude Code 的工具)
|
||
// 这些字段需要被 cleanJSONSchema 清理
|
||
tools := []map[string]any{
|
||
{
|
||
"name": "read_file",
|
||
"description": "Read file contents",
|
||
"input_schema": map[string]any{
|
||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"path": map[string]any{
|
||
"type": "string",
|
||
"description": "File path",
|
||
"minLength": 1,
|
||
"maxLength": 4096,
|
||
"pattern": "^[^\\x00]+$",
|
||
},
|
||
"encoding": map[string]any{
|
||
"type": []string{"string", "null"},
|
||
"default": "utf-8",
|
||
"enum": []string{"utf-8", "ascii", "latin-1"},
|
||
},
|
||
},
|
||
"required": []string{"path"},
|
||
"additionalProperties": false,
|
||
},
|
||
},
|
||
{
|
||
"name": "write_file",
|
||
"description": "Write content to file",
|
||
"input_schema": map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"path": map[string]any{
|
||
"type": "string",
|
||
"minLength": 1,
|
||
},
|
||
"content": map[string]any{
|
||
"type": "string",
|
||
"maxLength": 1048576,
|
||
},
|
||
},
|
||
"required": []string{"path", "content"},
|
||
"additionalProperties": false,
|
||
"strict": true,
|
||
},
|
||
},
|
||
{
|
||
"name": "list_files",
|
||
"description": "List files in directory",
|
||
"input_schema": map[string]any{
|
||
"$id": "https://example.com/list-files.schema.json",
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"directory": map[string]any{
|
||
"type": "string",
|
||
},
|
||
"patterns": map[string]any{
|
||
"type": "array",
|
||
"items": map[string]any{
|
||
"type": "string",
|
||
"minLength": 1,
|
||
},
|
||
"minItems": 1,
|
||
"maxItems": 100,
|
||
"uniqueItems": true,
|
||
},
|
||
"recursive": map[string]any{
|
||
"type": "boolean",
|
||
"default": false,
|
||
},
|
||
},
|
||
"required": []string{"directory"},
|
||
"additionalProperties": false,
|
||
},
|
||
},
|
||
{
|
||
"name": "search_code",
|
||
"description": "Search code in files",
|
||
"input_schema": map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"query": map[string]any{
|
||
"type": "string",
|
||
"minLength": 1,
|
||
"format": "regex",
|
||
},
|
||
"max_results": map[string]any{
|
||
"type": "integer",
|
||
"minimum": 1,
|
||
"maximum": 1000,
|
||
"exclusiveMinimum": 0,
|
||
"default": 100,
|
||
},
|
||
},
|
||
"required": []string{"query"},
|
||
"additionalProperties": false,
|
||
"examples": []map[string]any{
|
||
{"query": "function.*test", "max_results": 50},
|
||
},
|
||
},
|
||
},
|
||
// 测试 required 引用不存在的属性(应被自动过滤)
|
||
{
|
||
"name": "invalid_required_tool",
|
||
"description": "Tool with invalid required field",
|
||
"input_schema": map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"name": map[string]any{
|
||
"type": "string",
|
||
},
|
||
},
|
||
// "nonexistent_field" 不存在于 properties 中,应被过滤掉
|
||
"required": []string{"name", "nonexistent_field"},
|
||
},
|
||
},
|
||
// 测试没有 properties 的 schema(应自动添加空 properties)
|
||
{
|
||
"name": "no_properties_tool",
|
||
"description": "Tool without properties",
|
||
"input_schema": map[string]any{
|
||
"type": "object",
|
||
"required": []string{"should_be_removed"},
|
||
},
|
||
},
|
||
// 测试没有 type 的 schema(应自动添加 type: OBJECT)
|
||
{
|
||
"name": "no_type_tool",
|
||
"description": "Tool without type",
|
||
"input_schema": map[string]any{
|
||
"properties": map[string]any{
|
||
"value": map[string]any{
|
||
"type": "string",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
payload := map[string]any{
|
||
"model": model,
|
||
"max_tokens": 100,
|
||
"stream": false,
|
||
"messages": []map[string]string{
|
||
{"role": "user", "content": "List files in the current directory"},
|
||
},
|
||
"tools": tools,
|
||
}
|
||
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 错误说明 schema 清理不完整
|
||
if resp.StatusCode == 400 {
|
||
t.Fatalf("Schema 清理失败,收到 400 错误: %s", string(respBody))
|
||
}
|
||
|
||
// 503 可能是账号限流,不算测试失败
|
||
if resp.StatusCode == 503 {
|
||
t.Skipf("账号暂时不可用 (503): %s", string(respBody))
|
||
}
|
||
|
||
// 429 是限流
|
||
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("✅ 复杂工具 schema 测试通过, id=%v", result["id"])
|
||
}
|
||
|
||
// TestClaudeMessagesWithThinkingAndTools 测试 thinking 模式下带工具调用的场景
|
||
// 验证:当历史 assistant 消息包含 tool_use 但没有 signature 时,
|
||
// 系统应自动添加 dummy thought_signature 避免 Gemini 400 错误
|
||
func TestClaudeMessagesWithThinkingAndTools(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) {
|
||
testClaudeThinkingWithToolHistory(t, model)
|
||
})
|
||
}
|
||
}
|
||
|
||
func testClaudeThinkingWithToolHistory(t *testing.T, model string) {
|
||
url := baseURL + endpointPrefix + "/v1/messages"
|
||
|
||
// 模拟历史对话:用户请求 → assistant 调用工具 → 工具返回 → 继续对话
|
||
// 注意:tool_use 块故意不包含 signature,测试系统是否能正确添加 dummy 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": "List files in the current directory",
|
||
},
|
||
// assistant 消息包含 tool_use 但没有 signature
|
||
map[string]any{
|
||
"role": "assistant",
|
||
"content": []map[string]any{
|
||
{
|
||
"type": "text",
|
||
"text": "I'll list the files for you.",
|
||
},
|
||
{
|
||
"type": "tool_use",
|
||
"id": "toolu_01XGmNv",
|
||
"name": "Bash",
|
||
"input": map[string]any{"command": "ls -la"},
|
||
// 故意不包含 signature
|
||
},
|
||
},
|
||
},
|
||
// 工具结果
|
||
map[string]any{
|
||
"role": "user",
|
||
"content": []map[string]any{
|
||
{
|
||
"type": "tool_result",
|
||
"tool_use_id": "toolu_01XGmNv",
|
||
"content": "file1.txt\nfile2.txt\ndir1/",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
"tools": []map[string]any{
|
||
{
|
||
"name": "Bash",
|
||
"description": "Execute bash commands",
|
||
"input_schema": map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"command": map[string]any{
|
||
"type": "string",
|
||
},
|
||
},
|
||
"required": []string{"command"},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
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 错误说明 thought_signature 处理失败
|
||
if resp.StatusCode == 400 {
|
||
t.Fatalf("thought_signature 处理失败,收到 400 错误: %s", string(respBody))
|
||
}
|
||
|
||
// 503 可能是账号限流,不算测试失败
|
||
if resp.StatusCode == 503 {
|
||
t.Skipf("账号暂时不可用 (503): %s", string(respBody))
|
||
}
|
||
|
||
// 429 是限流
|
||
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 模式工具调用测试通过, id=%v", result["id"])
|
||
}
|
||
|
||
// TestClaudeMessagesWithNoSignature 测试历史 thinking block 不带 signature 的场景
|
||
// 验证:Gemini 模型接受没有 signature 的 thinking block
|
||
func TestClaudeMessagesWithNoSignature(t *testing.T) {
|
||
models := []string{
|
||
"claude-haiku-4-5-20251001", // gemini-3-flash - 支持无 signature
|
||
}
|
||
for i, model := range models {
|
||
if i > 0 {
|
||
time.Sleep(testInterval)
|
||
}
|
||
t.Run(model+"_无signature", func(t *testing.T) {
|
||
testClaudeWithNoSignature(t, model)
|
||
})
|
||
}
|
||
}
|
||
|
||
func testClaudeWithNoSignature(t *testing.T, model string) {
|
||
url := baseURL + endpointPrefix + "/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
|
||
},
|
||
{
|
||
"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)
|
||
|
||
if resp.StatusCode == 400 {
|
||
t.Fatalf("无 signature thinking 处理失败,收到 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("✅ 无 signature thinking 处理测试通过, id=%v", result["id"])
|
||
}
|