feat(antigravity): 添加混合调度可选功能
- 后端:账户模型添加 IsMixedSchedulingEnabled() 方法,读取 extra.mixed_scheduling - 后端:gateway_service 和 gemini_messages_compat_service 支持混合调度逻辑 - 后端:分组创建支持指定 platform 参数 - 前端:账户创建/编辑弹窗添加混合调度开关(仅 antigravity 账户显示) - 前端:混合调度开关添加问号图标和 tooltip 说明 - 前端:GroupSelector 支持根据 mixedScheduling 属性过滤分组 - 前端:分组创建支持选择 platform - 测试:e2e 测试添加 ENDPOINT_PREFIX 环境变量支持混合/隔离模式测试 - 测试:删除过时的 Claude signature 测试用例
This commit is contained in:
@@ -16,10 +16,14 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
baseURL = getEnv("BASE_URL", "http://localhost:8080")
|
||||
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 = 3 * time.Second // 测试间隔,防止限流
|
||||
testInterval = 1 * time.Second // 测试间隔,防止限流
|
||||
)
|
||||
|
||||
func getEnv(key, defaultVal string) string {
|
||||
@@ -32,9 +36,9 @@ func getEnv(key, defaultVal string) string {
|
||||
// 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
|
||||
"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", // 直接支持
|
||||
@@ -56,13 +60,17 @@ var geminiModels = []string{
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
fmt.Printf("\n🚀 E2E Gateway Tests - %s\n\n", baseURL)
|
||||
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 + "/v1/models"
|
||||
url := baseURL + endpointPrefix + "/v1/models"
|
||||
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+claudeAPIKey)
|
||||
@@ -97,7 +105,7 @@ func TestClaudeModelsList(t *testing.T) {
|
||||
|
||||
// TestGeminiModelsList 测试 GET /v1beta/models
|
||||
func TestGeminiModelsList(t *testing.T) {
|
||||
url := baseURL + "/v1beta/models"
|
||||
url := baseURL + endpointPrefix + "/v1beta/models"
|
||||
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+geminiAPIKey)
|
||||
@@ -143,7 +151,7 @@ func TestClaudeMessages(t *testing.T) {
|
||||
}
|
||||
|
||||
func testClaudeMessage(t *testing.T, model string, stream bool) {
|
||||
url := baseURL + "/v1/messages"
|
||||
url := baseURL + endpointPrefix + "/v1/messages"
|
||||
|
||||
payload := map[string]any{
|
||||
"model": model,
|
||||
@@ -294,8 +302,8 @@ func testGeminiGenerate(t *testing.T, model string, stream bool) {
|
||||
func TestClaudeMessagesWithComplexTools(t *testing.T) {
|
||||
// 测试模型列表(只测试几个代表性模型)
|
||||
models := []string{
|
||||
"claude-opus-4-5-20251101", // Claude 模型
|
||||
"claude-haiku-4-5-20251001", // 映射到 Gemini
|
||||
"claude-opus-4-5-20251101", // Claude 模型
|
||||
"claude-haiku-4-5-20251001", // 映射到 Gemini
|
||||
}
|
||||
|
||||
for i, model := range models {
|
||||
@@ -309,7 +317,7 @@ func TestClaudeMessagesWithComplexTools(t *testing.T) {
|
||||
}
|
||||
|
||||
func testClaudeMessageWithTools(t *testing.T, model string) {
|
||||
url := baseURL + "/v1/messages"
|
||||
url := baseURL + endpointPrefix + "/v1/messages"
|
||||
|
||||
// 构造包含复杂 schema 的工具定义(模拟 Claude Code 的工具)
|
||||
// 这些字段需要被 cleanJSONSchema 清理
|
||||
@@ -524,7 +532,7 @@ func TestClaudeMessagesWithThinkingAndTools(t *testing.T) {
|
||||
}
|
||||
|
||||
func testClaudeThinkingWithToolHistory(t *testing.T, model string) {
|
||||
url := baseURL + "/v1/messages"
|
||||
url := baseURL + endpointPrefix + "/v1/messages"
|
||||
|
||||
// 模拟历史对话:用户请求 → assistant 调用工具 → 工具返回 → 继续对话
|
||||
// 注意:tool_use 块故意不包含 signature,测试系统是否能正确添加 dummy signature
|
||||
@@ -650,7 +658,7 @@ func TestClaudeMessagesWithNoSignature(t *testing.T) {
|
||||
}
|
||||
|
||||
func testClaudeWithNoSignature(t *testing.T, model string) {
|
||||
url := baseURL + "/v1/messages"
|
||||
url := baseURL + endpointPrefix + "/v1/messages"
|
||||
|
||||
// 模拟历史对话包含 thinking block 但没有 signature
|
||||
payload := map[string]any{
|
||||
@@ -730,104 +738,3 @@ func testClaudeWithNoSignature(t *testing.T, model string) {
|
||||
}
|
||||
t.Logf("✅ 无 signature thinking 处理测试通过, id=%v", result["id"])
|
||||
}
|
||||
|
||||
// TestClaudeMessagesWithClaudeSignature 测试历史 thinking block 带有 Claude signature 的场景
|
||||
// 验证:Claude 的 signature 格式与 Gemini 不兼容,发送到 Gemini 模型时应忽略(不传递)
|
||||
func TestClaudeMessagesWithClaudeSignature(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+"_带Claude_signature", func(t *testing.T) {
|
||||
testClaudeWithClaudeSignature(t, model)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testClaudeWithClaudeSignature(t *testing.T, model string) {
|
||||
url := baseURL + "/v1/messages"
|
||||
|
||||
// 模拟历史对话包含 thinking block 且带有 Claude 格式的 signature
|
||||
// 这个 signature 是 Claude API 返回的格式,对 Gemini 无效
|
||||
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 和 Claude 格式的 signature
|
||||
map[string]any{
|
||||
"role": "assistant",
|
||||
"content": []map[string]any{
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": "Let me calculate 2+2. This is a simple arithmetic problem.",
|
||||
// Claude API 返回的 signature 格式(base64 编码的加密数据)
|
||||
"signature": "zbbJDG5qqgNXD/BVLwwxxT3gVaAY2hQ6CcB+hVLZWPi8r6vvlRBQKMfFPE3x5...",
|
||||
},
|
||||
{
|
||||
"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("Claude 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("✅ Claude signature 忽略测试通过, id=%v", result["id"])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user