mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-18 22:04:45 +08:00
feat: add gemini model mapping whitelist for apikey and bulk edit
This commit is contained in:
@@ -890,6 +890,55 @@ func TestGatewayService_SelectAccountForModelWithPlatform_GeminiPreferOAuth(t *t
|
|||||||
require.Equal(t, int64(2), acc.ID)
|
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) {
|
func TestGatewayService_SelectAccountForModelWithPlatform_StickyInGroup(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
groupID := int64(50)
|
groupID := int64(50)
|
||||||
@@ -1065,6 +1114,36 @@ func TestGatewayService_isModelSupportedByAccount(t *testing.T) {
|
|||||||
model: "claude-3-5-sonnet-20241022",
|
model: "claude-3-5-sonnet-20241022",
|
||||||
expected: true,
|
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 {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -2547,10 +2547,6 @@ func (s *GatewayService) isModelSupportedByAccount(account *Account, requestedMo
|
|||||||
if account.Platform == PlatformAnthropic && account.Type != AccountTypeAPIKey {
|
if account.Platform == PlatformAnthropic && account.Type != AccountTypeAPIKey {
|
||||||
requestedModel = claude.NormalizeModelID(requestedModel)
|
requestedModel = claude.NormalizeModelID(requestedModel)
|
||||||
}
|
}
|
||||||
// Gemini API Key 账户直接透传,由上游判断模型是否支持
|
|
||||||
if account.Platform == PlatformGemini && account.Type == AccountTypeAPIKey {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// 其他平台使用账户的模型支持检查
|
// 其他平台使用账户的模型支持检查
|
||||||
return account.IsModelSupported(requestedModel)
|
return account.IsModelSupported(requestedModel)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -654,6 +654,7 @@ import Select from '@/components/common/Select.vue'
|
|||||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||||
import Icon from '@/components/icons/Icon.vue'
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
|
import { buildModelMappingObject as buildModelMappingPayload } from '@/composables/useModelWhitelist'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
show: boolean
|
show: boolean
|
||||||
@@ -705,7 +706,7 @@ const rateMultiplier = ref(1)
|
|||||||
const status = ref<'active' | 'inactive'>('active')
|
const status = ref<'active' | 'inactive'>('active')
|
||||||
const groupIds = ref<number[]>([])
|
const groupIds = ref<number[]>([])
|
||||||
|
|
||||||
// All models list (combined Anthropic + OpenAI)
|
// All models list (combined Anthropic + OpenAI + Gemini)
|
||||||
const allModels = [
|
const allModels = [
|
||||||
{ value: 'claude-opus-4-6', label: 'Claude Opus 4.6' },
|
{ value: 'claude-opus-4-6', label: 'Claude Opus 4.6' },
|
||||||
{ value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' },
|
{ 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-codex', label: 'GPT-5.1 Codex' },
|
||||||
{ value: 'gpt-5.1-2025-11-13', label: 'GPT-5.1' },
|
{ 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.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 = [
|
const presetMappings = [
|
||||||
{
|
{
|
||||||
label: 'Sonnet 4',
|
label: 'Sonnet 4',
|
||||||
@@ -777,6 +783,24 @@ const presetMappings = [
|
|||||||
from: 'gpt-5.1-codex-max',
|
from: 'gpt-5.1-codex-max',
|
||||||
to: 'gpt-5.1-codex',
|
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'
|
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 buildModelMappingObject = (): Record<string, string> | null => {
|
||||||
const mapping: Record<string, string> = {}
|
return buildModelMappingPayload(
|
||||||
|
modelRestrictionMode.value,
|
||||||
if (modelRestrictionMode.value === 'whitelist') {
|
allowedModels.value,
|
||||||
for (const model of allowedModels.value) {
|
modelMappings.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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildUpdatePayload = (): Record<string, unknown> | null => {
|
const buildUpdatePayload = (): Record<string, unknown> | null => {
|
||||||
|
|||||||
@@ -862,8 +862,8 @@
|
|||||||
<p class="input-hint">{{ t('admin.accounts.gemini.tier.aiStudioHint') }}</p>
|
<p class="input-hint">{{ t('admin.accounts.gemini.tier.aiStudioHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Model Restriction Section (不适用于 Gemini,Antigravity 已在上层条件排除) -->
|
<!-- Model Restriction Section (Antigravity 已在上层条件排除) -->
|
||||||
<div v-if="form.platform !== 'gemini'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
||||||
|
|
||||||
<!-- Mode Toggle -->
|
<!-- Mode Toggle -->
|
||||||
@@ -1135,34 +1135,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Temp Unschedulable Rules -->
|
<!-- Temp Unschedulable Rules -->
|
||||||
|
|||||||
@@ -65,8 +65,8 @@
|
|||||||
<p class="input-hint">{{ t('admin.accounts.leaveEmptyToKeep') }}</p>
|
<p class="input-hint">{{ t('admin.accounts.leaveEmptyToKeep') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Model Restriction Section (不适用于 Gemini 和 Antigravity) -->
|
<!-- Model Restriction Section (不适用于 Antigravity) -->
|
||||||
<div v-if="account.platform !== 'gemini' && account.platform !== 'antigravity'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
<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>
|
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
||||||
|
|
||||||
<!-- Mode Toggle -->
|
<!-- Mode Toggle -->
|
||||||
@@ -338,34 +338,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Upstream fields (only for upstream type) -->
|
<!-- Upstream fields (only for upstream type) -->
|
||||||
|
|||||||
@@ -515,6 +515,7 @@ export interface ProxyAccountSummary {
|
|||||||
export interface GeminiCredentials {
|
export interface GeminiCredentials {
|
||||||
// API Key authentication
|
// API Key authentication
|
||||||
api_key?: string
|
api_key?: string
|
||||||
|
model_mapping?: Record<string, string>
|
||||||
|
|
||||||
// OAuth authentication
|
// OAuth authentication
|
||||||
access_token?: string
|
access_token?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user