diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 6a0ef4fdf4..97ebd9ed35 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -2894,10 +2894,9 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) { return } - // Handle Antigravity accounts: return Claude + Gemini models + // Explicit account mappings expose their request-side names to connectivity tests. if account.Platform == service.PlatformAntigravity { - // 直接复用 antigravity.DefaultModels(),与 /v1/models 端点保持同步 - response.Success(c, antigravity.DefaultModels()) + response.Success(c, antigravityAccountTestModels(account.Credentials["model_mapping"])) return } @@ -2998,6 +2997,48 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) { response.Success(c, models) } +// antigravityAccountTestModels uses the stored mapping rather than GetModelMapping, +// which supplies defaults and compatibility aliases that the administrator did not configure. +func antigravityAccountTestModels(rawMapping any) []antigravity.ClaudeModel { + var mappedIDs []string + switch mapping := rawMapping.(type) { + case map[string]any: + for id := range mapping { + if strings.TrimSpace(id) != "" { + mappedIDs = append(mappedIDs, id) + } + } + case map[string]string: + for id := range mapping { + if strings.TrimSpace(id) != "" { + mappedIDs = append(mappedIDs, id) + } + } + } + if len(mappedIDs) == 0 { + return antigravity.DefaultModels() + } + + sort.Strings(mappedIDs) + defaultByID := make(map[string]antigravity.ClaudeModel) + for _, model := range antigravity.DefaultModels() { + defaultByID[model.ID] = model + } + models := make([]antigravity.ClaudeModel, 0, len(mappedIDs)) + for _, id := range mappedIDs { + if model, ok := defaultByID[id]; ok { + models = append(models, model) + continue + } + models = append(models, antigravity.ClaudeModel{ + ID: id, + Type: "model", + DisplayName: id, + }) + } + return models +} + // SyncUpstreamModels handles syncing live supported models from an account's upstream. // POST /api/v1/admin/accounts/:id/models/sync-upstream func (h *AccountHandler) SyncUpstreamModels(c *gin.Context) { diff --git a/backend/internal/handler/admin/account_handler_available_models_test.go b/backend/internal/handler/admin/account_handler_available_models_test.go index 3bcfce11d0..b8f091bccc 100644 --- a/backend/internal/handler/admin/account_handler_available_models_test.go +++ b/backend/internal/handler/admin/account_handler_available_models_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity" "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" @@ -330,6 +331,49 @@ func TestAccountHandlerGetAvailableModels_GeminiGoogleOneUsesConservativeCatalog require.NotContains(t, ids, "gemini-2.5-flash-image") } +func TestAccountHandlerGetAvailableModels_Antigravity(t *testing.T) { + defaults := antigravity.DefaultModels() + known := defaults[0] + custom := antigravity.ClaudeModel{ID: "claude-custom-alias", Type: "model", DisplayName: "claude-custom-alias"} + cases := []struct { + name string + credentials map[string]any + want []antigravity.ClaudeModel + }{ + {name: "nil credentials", want: defaults}, + {name: "missing mapping", credentials: map[string]any{}, want: defaults}, + {name: "empty JSON mapping", credentials: map[string]any{"model_mapping": map[string]any{}}, want: defaults}, + {name: "empty string mapping", credentials: map[string]any{"model_mapping": map[string]string{}}, want: defaults}, + {name: "blank key", credentials: map[string]any{"model_mapping": map[string]any{" ": "upstream-model"}}, want: defaults}, + {name: "JSON mapping keys and metadata", credentials: map[string]any{"model_mapping": map[string]any{ + known.ID: known.ID, custom.ID: "upstream-model", + }}, want: []antigravity.ClaudeModel{custom, known}}, + {name: "string mapping keys and metadata", credentials: map[string]any{"model_mapping": map[string]string{ + known.ID: known.ID, custom.ID: "upstream-model", + }}, want: []antigravity.ClaudeModel{custom, known}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + for _, accountType := range []string{service.AccountTypeOAuth, service.AccountTypeAPIKey} { + t.Run(accountType, func(t *testing.T) { + svc := &availableModelsAdminService{ + stubAdminService: newStubAdminService(), + account: service.Account{ID: 51, Platform: service.PlatformAntigravity, Type: accountType, Credentials: tc.credentials}, + } + rec := httptest.NewRecorder() + setupAvailableModelsRouter(svc).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/51/models", nil)) + require.Equal(t, http.StatusOK, rec.Code) + var resp struct { + Data []antigravity.ClaudeModel `json:"data"` + } + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) + require.Equal(t, tc.want, resp.Data) + }) + } + }) + } +} + func TestAccountHandlerSyncUpstreamModels_ConfigErrorReturnsBadRequest(t *testing.T) { svc := &availableModelsAdminService{ stubAdminService: newStubAdminService(),