diff --git a/backend/internal/handler/gemini_v1beta_handler.go b/backend/internal/handler/gemini_v1beta_handler.go index fc01d51725..db2f5dd380 100644 --- a/backend/internal/handler/gemini_v1beta_handler.go +++ b/backend/internal/handler/gemini_v1beta_handler.go @@ -52,6 +52,11 @@ func (h *GatewayHandler) GeminiV1BetaListModels(c *gin.Context) { return } + if models, ok := customGeminiModelsList(apiKey.Group); ok { + c.JSON(http.StatusOK, models) + return + } + account, err := h.geminiCompatService.SelectAccountForAIStudioEndpoints(c.Request.Context(), apiKey.GroupID) if err != nil { // 没有 gemini 账户,检查是否有 antigravity 账户可用 @@ -78,6 +83,17 @@ func (h *GatewayHandler) GeminiV1BetaListModels(c *gin.Context) { writeUpstreamResponse(c, res) } +func customGeminiModelsList(group *service.Group) (gemini.ModelsListResponse, bool) { + if group == nil || !group.CustomModelsListEnabled() { + return gemini.ModelsListResponse{}, false + } + models := make([]gemini.Model, 0, len(group.ModelsListConfig.Models)) + for _, modelID := range group.ModelsListConfig.Models { + models = append(models, gemini.FallbackModel(modelID)) + } + return gemini.ModelsListResponse{Models: models}, true +} + // GeminiV1BetaGetModel proxies: // GET /v1beta/models/{model} func (h *GatewayHandler) GeminiV1BetaGetModel(c *gin.Context) { diff --git a/backend/internal/handler/gemini_v1beta_handler_test.go b/backend/internal/handler/gemini_v1beta_handler_test.go index 29d7cc4169..9d86731686 100644 --- a/backend/internal/handler/gemini_v1beta_handler_test.go +++ b/backend/internal/handler/gemini_v1beta_handler_test.go @@ -3,13 +3,81 @@ package handler import ( + "encoding/json" "net/http" + "net/http/httptest" "testing" + "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity" + "github.com/Wei-Shaw/sub2api/internal/pkg/gemini" + "github.com/Wei-Shaw/sub2api/internal/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) +func TestGeminiV1BetaListModels_CustomGroupListUsesNativeResponse(t *testing.T) { + gin.SetMode(gin.TestMode) + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodGet, "/v1beta/models", nil) + c.Set(string(middleware.ContextKeyAPIKey), &service.APIKey{ + Group: &service.Group{ + Platform: service.PlatformGemini, + ModelsListConfig: service.GroupModelsListConfig{ + Enabled: true, + Models: []string{"gemini-2.5-pro", "models/gemini-custom"}, + }, + }, + }) + + (&GatewayHandler{}).GeminiV1BetaListModels(c) + + require.Equal(t, http.StatusOK, rec.Code) + var got gemini.ModelsListResponse + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got)) + require.Equal(t, []gemini.Model{ + gemini.FallbackModel("gemini-2.5-pro"), + gemini.FallbackModel("models/gemini-custom"), + }, got.Models) +} + +func TestGeminiV1BetaListModels_ForcedAntigravityIgnoresCustomGroupList(t *testing.T) { + gin.SetMode(gin.TestMode) + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodGet, "/antigravity/v1beta/models", nil) + c.Set(string(middleware.ContextKeyAPIKey), &service.APIKey{ + Group: &service.Group{ + Platform: service.PlatformGemini, + ModelsListConfig: service.GroupModelsListConfig{ + Enabled: true, + Models: []string{"gemini-custom"}, + }, + }, + }) + c.Set(string(middleware.ContextKeyForcePlatform), service.PlatformAntigravity) + + (&GatewayHandler{}).GeminiV1BetaListModels(c) + + require.Equal(t, http.StatusOK, rec.Code) + var got antigravity.GeminiModelsListResponse + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got)) + require.Equal(t, antigravity.FallbackGeminiModelsList(), got) +} + +func TestCustomGeminiModelsList_DisabledKeepsExistingFlow(t *testing.T) { + group := &service.Group{ + ModelsListConfig: service.GroupModelsListConfig{ + Enabled: false, + Models: []string{"gemini-2.5-pro"}, + }, + } + + _, ok := customGeminiModelsList(group) + require.False(t, ok) +} + // TestGeminiV1BetaHandler_PlatformRoutingInvariant 文档化并验证 Handler 层的平台路由逻辑不变量 // 该测试确保 gemini 和 antigravity 平台的路由逻辑符合预期 func TestGeminiV1BetaHandler_PlatformRoutingInvariant(t *testing.T) { diff --git a/frontend/src/i18n/locales/en/admin/overview.ts b/frontend/src/i18n/locales/en/admin/overview.ts index 80be8c74e9..124122b034 100644 --- a/frontend/src/i18n/locales/en/admin/overview.ts +++ b/frontend/src/i18n/locales/en/admin/overview.ts @@ -1070,8 +1070,8 @@ export default { sumTooHigh: 'Min gross margin plus safety buffer must be less than 100%, otherwise every account would be excluded' }, modelsList: { - title: 'Custom /v1/models Model List', - hint: 'Only changes the /v1/models response. Whitelist model calls and account routing are unchanged.', + title: 'Custom {endpoint} Model List', + hint: 'Only changes the {endpoint} response. Whitelist model calls and account routing are unchanged.', loading: 'Loading model list...', empty: 'No displayable models', selectedSummary: 'Selected {selected} / {total}', diff --git a/frontend/src/i18n/locales/zh/admin/overview.ts b/frontend/src/i18n/locales/zh/admin/overview.ts index 72e2c56a96..e2a0e805e7 100644 --- a/frontend/src/i18n/locales/zh/admin/overview.ts +++ b/frontend/src/i18n/locales/zh/admin/overview.ts @@ -1067,8 +1067,8 @@ export default { sumTooHigh: '最低毛利率与安全缓冲之和必须小于 100%,否则将排除全部账号' }, modelsList: { - title: '自定义 /v1/models 模型列表', - hint: '仅影响 /v1/models 展示结果,不影响白名单模型调用和账号调度。', + title: '自定义 {endpoint} 模型列表', + hint: '仅影响 {endpoint} 展示结果,不影响白名单模型调用和账号调度。', loading: '正在加载模型列表...', empty: '暂无可展示模型', selectedSummary: '已选 {selected} / {total}', diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index adf0e4cf12..04ec052583 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -775,10 +775,10 @@
- {{ t("admin.groups.modelsList.hint") }} + {{ t("admin.groups.modelsList.hint", { endpoint: modelsListEndpoint(createForm.platform) }) }}