feat: add gemini model mapping whitelist for apikey and bulk edit

This commit is contained in:
liuxiongfeng
2026-02-11 22:31:07 +08:00
parent 807d0018ef
commit a03c361b04
6 changed files with 116 additions and 84 deletions

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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<number[]>([])
// 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<string, string> | null => {
const mapping: Record<string, string> = {}
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<string, unknown> | null => {

View File

@@ -862,8 +862,8 @@
<p class="input-hint">{{ t('admin.accounts.gemini.tier.aiStudioHint') }}</p>
</div>
<!-- Model Restriction Section (不适用于 GeminiAntigravity 已在上层条件排除) -->
<div v-if="form.platform !== 'gemini'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
<!-- Model Restriction Section (Antigravity 已在上层条件排除) -->
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
<!-- Mode Toggle -->
@@ -1135,34 +1135,6 @@
</div>
</div>
<!-- Gemini 模型说明 -->
<div v-if="form.platform === 'gemini'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20">
<div class="flex items-start gap-3">
<svg
class="h-5 w-5 flex-shrink-0 text-blue-600 dark:text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div>
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">
{{ t('admin.accounts.gemini.modelPassthrough') }}
</p>
<p class="mt-1 text-xs text-blue-700 dark:text-blue-400">
{{ t('admin.accounts.gemini.modelPassthroughDesc') }}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Temp Unschedulable Rules -->

View File

@@ -65,8 +65,8 @@
<p class="input-hint">{{ t('admin.accounts.leaveEmptyToKeep') }}</p>
</div>
<!-- Model Restriction Section (不适用于 Gemini Antigravity) -->
<div v-if="account.platform !== 'gemini' && account.platform !== 'antigravity'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
<!-- Model Restriction Section (不适用于 Antigravity) -->
<div v-if="account.platform !== 'antigravity'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
<!-- Mode Toggle -->
@@ -338,34 +338,6 @@
</div>
</div>
<!-- Gemini 模型说明 -->
<div v-if="account.platform === 'gemini'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20">
<div class="flex items-start gap-3">
<svg
class="h-5 w-5 flex-shrink-0 text-blue-600 dark:text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div>
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">
{{ t('admin.accounts.gemini.modelPassthrough') }}
</p>
<p class="mt-1 text-xs text-blue-700 dark:text-blue-400">
{{ t('admin.accounts.gemini.modelPassthroughDesc') }}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Upstream fields (only for upstream type) -->

View File

@@ -515,6 +515,7 @@ export interface ProxyAccountSummary {
export interface GeminiCredentials {
// API Key authentication
api_key?: string
model_mapping?: Record<string, string>
// OAuth authentication
access_token?: string