diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index b65dedf9cc..b20d9ef652 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -1006,7 +1006,8 @@ func (h *GatewayHandler) Models(c *gin.Context) { // Get available models from account configurations for the selected group platform. availableModels := h.gatewayService.GetAvailableModels(c.Request.Context(), groupID, platform) if apiKey != nil && apiKey.Group != nil && apiKey.Group.CustomModelsListEnabled() { - availableModels = filterModelsByCustomList(availableModels, defaultModelIDsForPlatform(platform), apiKey.Group.ModelsListConfig.Models) + fallbackModels := defaultModelIDsForPlatform(platform) + availableModels = filterModelsByCustomList(customModelsListSource(platform, availableModels, fallbackModels), fallbackModels, apiKey.Group.ModelsListConfig.Models) writeCustomModelsList(c, platform, availableModels) return } @@ -1090,6 +1091,13 @@ func writeOpenAIModelsList(c *gin.Context, modelIDs []string) { }) } +func customModelsListSource(platform string, availableModels, fallbackModels []string) []string { + if platform == service.PlatformAnthropic && len(availableModels) > 0 { + return mergeModelIDs(availableModels, fallbackModels) + } + return availableModels +} + func filterModelsByCustomList(availableModels, fallbackModels, selectedModels []string) []string { if len(selectedModels) == 0 { return availableModels @@ -1158,6 +1166,15 @@ func defaultModelIDsForPlatform(platform string) []string { ids = append(ids, model.ID) } return ids + case service.PlatformAnthropic: + ids := make([]string, 0, len(claude.DefaultModels)+len(antigravity.DefaultModels())) + for _, model := range claude.DefaultModels { + ids = append(ids, model.ID) + } + for _, model := range antigravity.DefaultModels() { + ids = append(ids, model.ID) + } + return mergeModelIDs(ids, nil) case service.PlatformGrok: return xai.DefaultModelIDs() default: @@ -1169,6 +1186,25 @@ func defaultModelIDsForPlatform(platform string) []string { } } +func mergeModelIDs(primary, secondary []string) []string { + seen := make(map[string]struct{}, len(primary)+len(secondary)) + merged := make([]string, 0, len(primary)+len(secondary)) + for _, models := range [][]string{primary, secondary} { + for _, model := range models { + model = strings.TrimSpace(model) + if model == "" { + continue + } + if _, ok := seen[model]; ok { + continue + } + seen[model] = struct{}{} + merged = append(merged, model) + } + } + return merged +} + // AntigravityModels 返回 Antigravity 支持的全部模型 // GET /antigravity/models func (h *GatewayHandler) AntigravityModels(c *gin.Context) { diff --git a/backend/internal/handler/gateway_models_test.go b/backend/internal/handler/gateway_models_test.go index c5238f2a2f..6011e13027 100644 --- a/backend/internal/handler/gateway_models_test.go +++ b/backend/internal/handler/gateway_models_test.go @@ -269,6 +269,149 @@ func TestGatewayModels_CustomModelsListKeepsConcreteModelAllowedByWildcardMappin require.Equal(t, []string{"claude-sonnet-4-6"}, modelIDsForTest(got.Data)) } +func TestGatewayModels_AnthropicCustomModelsListIncludesOAuthClaudeAndMappedDeepSeek(t *testing.T) { + gin.SetMode(gin.TestMode) + + groupID := int64(28) + h := newGatewayModelsHandlerForTest( + &gatewayModelsAccountRepoStub{ + byGroup: map[int64][]service.Account{ + groupID: { + { + ID: 1, + Platform: service.PlatformAnthropic, + Type: service.AccountTypeOAuth, + }, + { + ID: 2, + Platform: service.PlatformAnthropic, + Type: service.AccountTypeAPIKey, + Credentials: map[string]any{ + "model_mapping": map[string]any{ + "deepseek-v4-pro": "deepseek-v4-pro", + }, + }, + }, + }, + }, + }, + ) + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodGet, "/v1/models", nil) + c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{ + Group: &service.Group{ + ID: groupID, + Platform: service.PlatformAnthropic, + ModelsListConfig: service.GroupModelsListConfig{ + Enabled: true, + Models: []string{"claude-fable-5", "claude-opus-4-8", "deepseek-v4-pro"}, + }, + }, + }) + + h.Models(c) + + require.Equal(t, http.StatusOK, rec.Code) + + var got gatewayModelsResponseForTest + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got)) + require.Equal(t, []string{"claude-fable-5", "claude-opus-4-8", "deepseek-v4-pro"}, modelIDsForTest(got.Data)) +} + +func TestGatewayModels_AnthropicCustomModelsListDisabledKeepsMappedModelList(t *testing.T) { + gin.SetMode(gin.TestMode) + + groupID := int64(29) + h := newGatewayModelsHandlerForTest( + &gatewayModelsAccountRepoStub{ + byGroup: map[int64][]service.Account{ + groupID: { + { + ID: 1, + Platform: service.PlatformAnthropic, + Type: service.AccountTypeOAuth, + }, + { + ID: 2, + Platform: service.PlatformAnthropic, + Type: service.AccountTypeAPIKey, + Credentials: map[string]any{ + "model_mapping": map[string]any{ + "deepseek-v4-pro": "deepseek-v4-pro", + }, + }, + }, + }, + }, + }, + ) + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodGet, "/v1/models", nil) + c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{ + Group: &service.Group{ + ID: groupID, + Platform: service.PlatformAnthropic, + ModelsListConfig: service.GroupModelsListConfig{ + Enabled: false, + Models: []string{"claude-fable-5", "deepseek-v4-pro"}, + }, + }, + }) + + h.Models(c) + + require.Equal(t, http.StatusOK, rec.Code) + + var got gatewayModelsResponseForTest + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got)) + require.Equal(t, []string{"deepseek-v4-pro"}, modelIDsForTest(got.Data)) +} + +func TestGatewayModels_AnthropicCustomModelsListIncludesOAuthClaudeWithoutMappings(t *testing.T) { + gin.SetMode(gin.TestMode) + + groupID := int64(30) + h := newGatewayModelsHandlerForTest( + &gatewayModelsAccountRepoStub{ + byGroup: map[int64][]service.Account{ + groupID: { + { + ID: 1, + Platform: service.PlatformAnthropic, + Type: service.AccountTypeOAuth, + }, + }, + }, + }, + ) + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodGet, "/v1/models", nil) + c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{ + Group: &service.Group{ + ID: groupID, + Platform: service.PlatformAnthropic, + ModelsListConfig: service.GroupModelsListConfig{ + Enabled: true, + Models: []string{"claude-opus-4-6-thinking", "claude-sonnet-4-5"}, + }, + }, + }) + + h.Models(c) + + require.Equal(t, http.StatusOK, rec.Code) + + var got gatewayModelsResponseForTest + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got)) + require.Equal(t, []string{"claude-opus-4-6-thinking", "claude-sonnet-4-5"}, modelIDsForTest(got.Data)) +} + func TestGatewayModels_CustomModelsListCanReturnEmptyWhenSelectionsUnavailable(t *testing.T) { gin.SetMode(gin.TestMode)