From a747c63b8e2181c857d9c37cf8f309b66d5f38d4 Mon Sep 17 00:00:00 2001 From: liuxiongfeng Date: Wed, 11 Feb 2026 22:31:07 +0800 Subject: [PATCH] feat: add gemini model mapping whitelist for apikey and bulk edit --- .../service/gateway_multiplatform_test.go | 79 +++++++++++++++++++ backend/internal/service/gateway_service.go | 4 - .../account/BulkEditAccountModal.vue | 52 +++++++----- .../components/account/CreateAccountModal.vue | 32 +------- .../components/account/EditAccountModal.vue | 32 +------- frontend/src/types/index.ts | 1 + 6 files changed, 116 insertions(+), 84 deletions(-) diff --git a/backend/internal/service/gateway_multiplatform_test.go b/backend/internal/service/gateway_multiplatform_test.go index b4b93ace..e257b1a5 100644 --- a/backend/internal/service/gateway_multiplatform_test.go +++ b/backend/internal/service/gateway_multiplatform_test.go @@ -890,6 +890,55 @@ func TestGatewayService_SelectAccountForModelWithPlatform_GeminiPreferOAuth(t *t require.Equal(t, int64(2), acc.ID) } +func TestGatewayService_SelectAccountForModelWithPlatform_GeminiAPIKeyModelMappingFilter(t *testing.T) { + ctx := context.Background() + + repo := &mockAccountRepoForPlatform{ + accounts: []Account{ + { + ID: 1, + Platform: PlatformGemini, + Type: AccountTypeAPIKey, + Priority: 1, + Status: StatusActive, + Schedulable: true, + Credentials: map[string]any{"model_mapping": map[string]any{"gemini-2.5-pro": "gemini-2.5-pro"}}, + }, + { + ID: 2, + Platform: PlatformGemini, + Type: AccountTypeAPIKey, + Priority: 2, + Status: StatusActive, + Schedulable: true, + Credentials: map[string]any{"model_mapping": map[string]any{"gemini-2.5-flash": "gemini-2.5-flash"}}, + }, + }, + accountsByID: map[int64]*Account{}, + } + for i := range repo.accounts { + repo.accountsByID[repo.accounts[i].ID] = &repo.accounts[i] + } + + cache := &mockGatewayCacheForPlatform{} + + svc := &GatewayService{ + accountRepo: repo, + cache: cache, + cfg: testConfig(), + } + + acc, err := svc.selectAccountForModelWithPlatform(ctx, nil, "", "gemini-2.5-flash", nil, PlatformGemini) + require.NoError(t, err) + require.NotNil(t, acc) + require.Equal(t, int64(2), acc.ID, "应过滤不支持请求模型的 APIKey 账号") + + acc, err = svc.selectAccountForModelWithPlatform(ctx, nil, "", "gemini-3-pro-preview", nil, PlatformGemini) + require.Error(t, err) + require.Nil(t, acc) + require.Contains(t, err.Error(), "supporting model") +} + func TestGatewayService_SelectAccountForModelWithPlatform_StickyInGroup(t *testing.T) { ctx := context.Background() groupID := int64(50) @@ -1065,6 +1114,36 @@ func TestGatewayService_isModelSupportedByAccount(t *testing.T) { model: "claude-3-5-sonnet-20241022", expected: true, }, + { + name: "Gemini平台-无映射配置-支持所有模型", + account: &Account{Platform: PlatformGemini, Type: AccountTypeAPIKey}, + model: "gemini-2.5-flash", + expected: true, + }, + { + name: "Gemini平台-有映射配置-只支持配置的模型", + account: &Account{ + Platform: PlatformGemini, + Type: AccountTypeAPIKey, + Credentials: map[string]any{ + "model_mapping": map[string]any{"gemini-2.5-pro": "gemini-2.5-pro"}, + }, + }, + model: "gemini-2.5-flash", + expected: false, + }, + { + name: "Gemini平台-有映射配置-支持配置的模型", + account: &Account{ + Platform: PlatformGemini, + Type: AccountTypeAPIKey, + Credentials: map[string]any{ + "model_mapping": map[string]any{"gemini-2.5-pro": "gemini-2.5-pro"}, + }, + }, + model: "gemini-2.5-pro", + expected: true, + }, } for _, tt := range tests { diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 56af4610..3141f71d 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -2547,10 +2547,6 @@ func (s *GatewayService) isModelSupportedByAccount(account *Account, requestedMo if account.Platform == PlatformAnthropic && account.Type != AccountTypeAPIKey { requestedModel = claude.NormalizeModelID(requestedModel) } - // Gemini API Key 账户直接透传,由上游判断模型是否支持 - if account.Platform == PlatformGemini && account.Type == AccountTypeAPIKey { - return true - } // 其他平台使用账户的模型支持检查 return account.IsModelSupported(requestedModel) } diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index 912eabb3..879a255c 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -654,6 +654,7 @@ import Select from '@/components/common/Select.vue' import ProxySelector from '@/components/common/ProxySelector.vue' import GroupSelector from '@/components/common/GroupSelector.vue' import Icon from '@/components/icons/Icon.vue' +import { buildModelMappingObject as buildModelMappingPayload } from '@/composables/useModelWhitelist' interface Props { show: boolean @@ -705,7 +706,7 @@ const rateMultiplier = ref(1) const status = ref<'active' | 'inactive'>('active') const groupIds = ref([]) -// All models list (combined Anthropic + OpenAI) +// All models list (combined Anthropic + OpenAI + Gemini) const allModels = [ { value: 'claude-opus-4-6', label: 'Claude Opus 4.6' }, { value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' }, @@ -722,10 +723,15 @@ const allModels = [ { value: 'gpt-5.1-codex', label: 'GPT-5.1 Codex' }, { value: 'gpt-5.1-2025-11-13', label: 'GPT-5.1' }, { value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' }, - { value: 'gpt-5-2025-08-07', label: 'GPT-5' } + { value: 'gpt-5-2025-08-07', label: 'GPT-5' }, + { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }, + { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' }, + { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' }, + { value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash Preview' }, + { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' } ] -// Preset mappings (combined Anthropic + OpenAI) +// Preset mappings (combined Anthropic + OpenAI + Gemini) const presetMappings = [ { label: 'Sonnet 4', @@ -777,6 +783,24 @@ const presetMappings = [ from: 'gpt-5.1-codex-max', to: 'gpt-5.1-codex', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' + }, + { + label: 'Gemini Flash 2.0', + from: 'gemini-2.0-flash', + to: 'gemini-2.0-flash', + color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' + }, + { + label: 'Gemini 2.5 Flash', + from: 'gemini-2.5-flash', + to: 'gemini-2.5-flash', + color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' + }, + { + label: 'Gemini 2.5 Pro', + from: 'gemini-2.5-pro', + to: 'gemini-2.5-pro', + color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' } ] @@ -866,23 +890,11 @@ const removeErrorCode = (code: number) => { } const buildModelMappingObject = (): Record | null => { - const mapping: Record = {} - - if (modelRestrictionMode.value === 'whitelist') { - for (const model of allowedModels.value) { - mapping[model] = model - } - } else { - for (const m of modelMappings.value) { - const from = m.from.trim() - const to = m.to.trim() - if (from && to) { - mapping[from] = to - } - } - } - - return Object.keys(mapping).length > 0 ? mapping : null + return buildModelMappingPayload( + modelRestrictionMode.value, + allowedModels.value, + modelMappings.value + ) } const buildUpdatePayload = (): Record | null => { diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index af06abca..2d7d745c 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -862,8 +862,8 @@

{{ t('admin.accounts.gemini.tier.aiStudioHint') }}

- -
+ +
@@ -1135,34 +1135,6 @@
- -
-
-
- - - -
-

- {{ t('admin.accounts.gemini.modelPassthrough') }} -

-

- {{ t('admin.accounts.gemini.modelPassthroughDesc') }} -

-
-
-
-
diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 60575f56..3395e82e 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -65,8 +65,8 @@

{{ t('admin.accounts.leaveEmptyToKeep') }}

- -
+ +
@@ -338,34 +338,6 @@
- -
-
-
- - - -
-

- {{ t('admin.accounts.gemini.modelPassthrough') }} -

-

- {{ t('admin.accounts.gemini.modelPassthroughDesc') }} -

-
-
-
-
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 189d8af1..a250820b 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -515,6 +515,7 @@ export interface ProxyAccountSummary { export interface GeminiCredentials { // API Key authentication api_key?: string + model_mapping?: Record // OAuth authentication access_token?: string