diff --git a/backend/internal/domain/constants.go b/backend/internal/domain/constants.go index cd821165a4..6500502014 100644 --- a/backend/internal/domain/constants.go +++ b/backend/internal/domain/constants.go @@ -105,11 +105,11 @@ var DefaultAntigravityModelMapping = map[string]string{ "claude-opus-4-6": "claude-opus-4-6-thinking", // 简称映射 "claude-opus-4-5-thinking": "claude-opus-4-6-thinking", // 迁移旧模型 "claude-sonnet-4-6": "claude-sonnet-4-6", - "claude-sonnet-4-5": "claude-sonnet-4-5", - "claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking", + "claude-sonnet-4-5": "claude-sonnet-4-6", // 迁移旧模型 + "claude-sonnet-4-5-thinking": "claude-sonnet-4-6", // 迁移旧模型 // Claude 详细版本 ID 映射 "claude-opus-4-5-20251101": "claude-opus-4-6-thinking", // 迁移旧模型 - "claude-sonnet-4-5-20250929": "claude-sonnet-4-5", + "claude-sonnet-4-5-20250929": "claude-sonnet-4-6", // 迁移旧模型 // Claude Haiku → Sonnet(无 Haiku 支持) "claude-haiku-4-5": "claude-sonnet-4-6", "claude-haiku-4-5-20251001": "claude-sonnet-4-6", diff --git a/backend/internal/domain/constants_test.go b/backend/internal/domain/constants_test.go index e847e22f30..7cfcd7fa51 100644 --- a/backend/internal/domain/constants_test.go +++ b/backend/internal/domain/constants_test.go @@ -43,6 +43,20 @@ func TestDefaultAntigravityModelMapping_ContainsNewClaudeModels(t *testing.T) { } } +func TestDefaultAntigravityModelMapping_MigratesLegacySonnet45(t *testing.T) { + t.Parallel() + + for _, model := range []string{ + "claude-sonnet-4-5", + "claude-sonnet-4-5-thinking", + "claude-sonnet-4-5-20250929", + } { + if got := DefaultAntigravityModelMapping[model]; got != "claude-sonnet-4-6" { + t.Fatalf("expected legacy model %q to map to claude-sonnet-4-6, got %q", model, got) + } + } +} + func TestDefaultAntigravityModelMapping_Gemini31ProAliases(t *testing.T) { t.Parallel() diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index 451f90db16..e7ce083ff5 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -43,8 +43,9 @@ import ( var sseDataPrefix = regexp.MustCompile(`^data:\s*`) const ( - testClaudeAPIURL = "https://api.anthropic.com/v1/messages?beta=true" - chatgptCodexAPIURL = "https://chatgpt.com/backend-api/codex/responses" + testClaudeAPIURL = "https://api.anthropic.com/v1/messages?beta=true" + chatgptCodexAPIURL = "https://chatgpt.com/backend-api/codex/responses" + defaultAntigravityTestModel = "claude-sonnet-4-6" ) // TestEvent represents a SSE event for account testing @@ -2311,11 +2312,7 @@ func (s *AccountTestService) routeAntigravityTest(c *gin.Context, account *Accou func (s *AccountTestService) testAntigravityAccountConnection(c *gin.Context, account *Account, modelID string) error { ctx := c.Request.Context() - // 默认模型:Claude 使用 claude-sonnet-4-5,Gemini 使用 gemini-3-pro-preview - testModelID := modelID - if testModelID == "" { - testModelID = "claude-sonnet-4-5" - } + testModelID := antigravityConnectionTestModel(modelID) if s.antigravityGatewayService == nil { return s.sendErrorAndEnd(c, "Antigravity gateway service not configured") @@ -2346,6 +2343,13 @@ func (s *AccountTestService) testAntigravityAccountConnection(c *gin.Context, ac return nil } +func antigravityConnectionTestModel(modelID string) string { + if modelID == "" { + return defaultAntigravityTestModel + } + return modelID +} + // buildGeminiAPIKeyRequest builds request for Gemini API Key accounts func (s *AccountTestService) buildGeminiAPIKeyRequest(ctx context.Context, account *Account, modelID string, payload []byte) (*http.Request, error) { apiKey := account.GetCredential("api_key") diff --git a/backend/internal/service/account_test_service_antigravity_test.go b/backend/internal/service/account_test_service_antigravity_test.go new file mode 100644 index 0000000000..b3362f3f69 --- /dev/null +++ b/backend/internal/service/account_test_service_antigravity_test.go @@ -0,0 +1,16 @@ +//go:build unit + +package service + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAntigravityConnectionTestModel(t *testing.T) { + t.Parallel() + + require.Equal(t, "claude-sonnet-4-6", antigravityConnectionTestModel("")) + require.Equal(t, "gemini-3.1-pro-preview", antigravityConnectionTestModel("gemini-3.1-pro-preview")) +} diff --git a/backend/internal/service/antigravity_credits_overages_test.go b/backend/internal/service/antigravity_credits_overages_test.go index 7a5224daf3..bdba5cec7b 100644 --- a/backend/internal/service/antigravity_credits_overages_test.go +++ b/backend/internal/service/antigravity_credits_overages_test.go @@ -239,7 +239,7 @@ func TestAntigravityRetryLoop_ModelRateLimited_InjectsCredits(t *testing.T) { Extra: map[string]any{ "allow_overages": true, modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-5": map[string]any{ + "claude-sonnet-4-6": map[string]any{ "rate_limited_at": time.Now().UTC().Format(time.RFC3339), "rate_limit_reset_at": time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339), }, @@ -290,7 +290,7 @@ func TestAntigravityRetryLoop_CreditsExhausted_DoesNotInject(t *testing.T) { Extra: map[string]any{ "allow_overages": true, modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-5": map[string]any{ + "claude-sonnet-4-6": map[string]any{ "rate_limited_at": time.Now().UTC().Format(time.RFC3339), "rate_limit_reset_at": time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339), }, @@ -355,7 +355,7 @@ func TestAntigravityRetryLoop_CreditErrorMarksExhausted(t *testing.T) { Extra: map[string]any{ "allow_overages": true, modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-5": map[string]any{ + "claude-sonnet-4-6": map[string]any{ "rate_limited_at": time.Now().UTC().Format(time.RFC3339), "rate_limit_reset_at": time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339), }, diff --git a/backend/internal/service/antigravity_model_mapping_test.go b/backend/internal/service/antigravity_model_mapping_test.go index 62ff2e295d..0f86aa4a9c 100644 --- a/backend/internal/service/antigravity_model_mapping_test.go +++ b/backend/internal/service/antigravity_model_mapping_test.go @@ -69,10 +69,10 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) { expected: "claude-sonnet-4-6", }, { - name: "默认映射 - claude-sonnet-4-5-20250929 → claude-sonnet-4-5", + name: "默认映射 - claude-sonnet-4-5-20250929 → claude-sonnet-4-6", requestedModel: "claude-sonnet-4-5-20250929", accountMapping: nil, - expected: "claude-sonnet-4-5", + expected: "claude-sonnet-4-6", }, // 3. 默认映射中的透传(映射到自己) @@ -89,10 +89,10 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) { expected: "claude-sonnet-4-6", }, { - name: "默认映射透传 - claude-sonnet-4-5", + name: "默认映射 - claude-sonnet-4-5 → claude-sonnet-4-6", requestedModel: "claude-sonnet-4-5", accountMapping: nil, - expected: "claude-sonnet-4-5", + expected: "claude-sonnet-4-6", }, { name: "默认映射透传 - claude-opus-4-8", @@ -113,10 +113,10 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) { expected: "claude-opus-4-6-thinking", }, { - name: "默认映射透传 - claude-sonnet-4-5-thinking", + name: "默认映射 - claude-sonnet-4-5-thinking → claude-sonnet-4-6", requestedModel: "claude-sonnet-4-5-thinking", accountMapping: nil, - expected: "claude-sonnet-4-5-thinking", + expected: "claude-sonnet-4-6", }, { name: "默认映射透传 - gemini-2.5-flash", diff --git a/backend/internal/service/antigravity_rate_limit_test.go b/backend/internal/service/antigravity_rate_limit_test.go index c9b4298291..7a06a4e49e 100644 --- a/backend/internal/service/antigravity_rate_limit_test.go +++ b/backend/internal/service/antigravity_rate_limit_test.go @@ -206,7 +206,7 @@ func TestHandleUpstreamError_429_NonModelRateLimit(t *testing.T) { // 但 429 兜底逻辑会使用 requestedModel 设置模型级限流 require.Nil(t, result) require.Len(t, repo.modelRateLimitCalls, 1) - require.Equal(t, "claude-sonnet-4-5", repo.modelRateLimitCalls[0].modelKey) + require.Equal(t, "claude-sonnet-4-6", repo.modelRateLimitCalls[0].modelKey) } // TestHandleUpstreamError_429_NonModelRateLimit_UsesMappedModelKey 测试 429 非模型限流场景 @@ -844,17 +844,28 @@ func TestSetAntigravityModelRateLimits_GeminiWritesFamilyScope(t *testing.T) { require.Equal(t, antigravityGeminiModelRateLimitKey, repo.modelRateLimitCalls[1].modelKey) } -func TestSetAntigravityModelRateLimits_ClaudeDoesNotWriteGeminiScope(t *testing.T) { +func TestSetAntigravityModelRateLimits_DoesNotDoubleMapCustomChain(t *testing.T) { repo := &stubAntigravityAccountRepo{} svc := &AntigravityGatewayService{} - account := &Account{ID: 790, Platform: PlatformAntigravity} + account := &Account{ + ID: 790, + Platform: PlatformAntigravity, + Credentials: map[string]any{ + "model_mapping": map[string]any{ + "custom-sonnet": "claude-sonnet-4-5", + "claude-sonnet-4-5": "claude-sonnet-4-6", + }, + }, + } resetAt := time.Now().Add(30 * time.Second) + canonicalModel := resolveFinalAntigravityModelKey(context.Background(), account, "custom-sonnet") + require.Equal(t, "claude-sonnet-4-5", canonicalModel) success := svc.setAntigravityModelRateLimits( context.Background(), repo, account, - "claude-sonnet-4-5", + canonicalModel, "[test]", 429, resetAt, @@ -866,6 +877,33 @@ func TestSetAntigravityModelRateLimits_ClaudeDoesNotWriteGeminiScope(t *testing. require.Equal(t, "claude-sonnet-4-5", repo.modelRateLimitCalls[0].modelKey) } +func TestSetModelRateLimitAndClearSession_UsesUpstreamReportedModelMetadata(t *testing.T) { + repo := &stubAntigravityAccountRepo{} + svc := &AntigravityGatewayService{accountRepo: repo} + account := &Account{ + ID: 791, + Platform: PlatformAntigravity, + Credentials: map[string]any{ + "model_mapping": map[string]any{ + "claude-sonnet-4-5": "claude-sonnet-4-6", + }, + }, + } + + svc.setModelRateLimitAndClearSession(&handleModelRateLimitParams{ + ctx: context.Background(), + prefix: "[test]", + account: account, + statusCode: 429, + }, &antigravitySmartRetryInfo{ + RetryDelay: 30 * time.Second, + ModelName: "claude-sonnet-4-5", + }) + + require.Len(t, repo.modelRateLimitCalls, 1) + require.Equal(t, "claude-sonnet-4-5", repo.modelRateLimitCalls[0].modelKey) +} + func TestAntigravityRetryLoop_PreCheck_SwitchesWhenRateLimited(t *testing.T) { upstream := &recordingOKUpstream{} account := &Account{ @@ -877,7 +915,7 @@ func TestAntigravityRetryLoop_PreCheck_SwitchesWhenRateLimited(t *testing.T) { Concurrency: 1, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-5": map[string]any{ + "claude-sonnet-4-6": map[string]any{ "rate_limit_reset_at": time.Now().Add(2 * time.Second).Format(time.RFC3339), }, }, @@ -920,7 +958,7 @@ func TestAntigravityRetryLoop_PreCheck_SwitchesWhenRemainingLong(t *testing.T) { Concurrency: 1, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-5": map[string]any{ + "claude-sonnet-4-6": map[string]any{ "rate_limit_reset_at": time.Now().Add(11 * time.Second).Format(time.RFC3339), }, }, diff --git a/backend/internal/service/antigravity_single_account_retry_test.go b/backend/internal/service/antigravity_single_account_retry_test.go index 6e58ab75d2..85677e59c4 100644 --- a/backend/internal/service/antigravity_single_account_retry_test.go +++ b/backend/internal/service/antigravity_single_account_retry_test.go @@ -693,7 +693,7 @@ func TestAntigravityRetryLoop_PreCheck_SingleAccountRetry_SkipsRateLimit(t *test Concurrency: 1, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-5": map[string]any{ + "claude-sonnet-4-6": map[string]any{ "rate_limit_reset_at": time.Now().Add(30 * time.Second).Format(time.RFC3339), }, }, @@ -737,7 +737,7 @@ func TestAntigravityRetryLoop_PreCheck_NoSingleAccountRetry_SwitchesOnRateLimit( Concurrency: 1, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-5": map[string]any{ + "claude-sonnet-4-6": map[string]any{ "rate_limit_reset_at": time.Now().Add(30 * time.Second).Format(time.RFC3339), }, }, diff --git a/backend/internal/service/model_rate_limit_test.go b/backend/internal/service/model_rate_limit_test.go index 430d91baf6..3293853d74 100644 --- a/backend/internal/service/model_rate_limit_test.go +++ b/backend/internal/service/model_rate_limit_test.go @@ -255,7 +255,7 @@ func TestIsModelRateLimited_OpenAIImageGenerationIntentBlocksTextModelImageTool( require.True(t, account.isModelRateLimitedWithContext(WithOpenAIImageGenerationIntent(context.Background()), "gpt-5.4")) } -func TestIsModelRateLimited_Antigravity_ThinkingAffectsModelKey(t *testing.T) { +func TestIsModelRateLimited_Antigravity_LegacySonnetUsesCanonicalModelKey(t *testing.T) { now := time.Now() future := now.Add(10 * time.Minute).Format(time.RFC3339) @@ -263,7 +263,7 @@ func TestIsModelRateLimited_Antigravity_ThinkingAffectsModelKey(t *testing.T) { Platform: PlatformAntigravity, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-5-thinking": map[string]any{ + "claude-sonnet-4-6": map[string]any{ "rate_limit_reset_at": future, }, }, @@ -453,7 +453,7 @@ func TestGetRateLimitRemainingTime(t *testing.T) { Platform: PlatformAntigravity, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-5": map[string]any{ + "claude-sonnet-4-6": map[string]any{ "rate_limit_reset_at": future15m, }, }, @@ -469,7 +469,7 @@ func TestGetRateLimitRemainingTime(t *testing.T) { Platform: PlatformAntigravity, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-5": map[string]any{ + "claude-sonnet-4-6": map[string]any{ "rate_limit_reset_at": future5m, }, },