Commit 2c71c8b9 authored by shaw's avatar shaw
Browse files

Merge PR #119: 支持自定义模型和优化模型选择

parents 901b03b8 7331220e
......@@ -114,15 +114,15 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
gitHubReleaseClient := repository.NewGitHubReleaseClient()
serviceBuildInfo := provideServiceBuildInfo(buildInfo)
updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo)
systemHandler := handler.ProvideSystemHandler(updateService)
adminSubscriptionHandler := admin.NewSubscriptionHandler(subscriptionService)
adminUsageHandler := admin.NewUsageHandler(usageService, apiKeyService, adminService)
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, settingHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler)
pricingRemoteClient := repository.NewPricingRemoteClient()
pricingService, err := service.ProvidePricingService(configConfig, pricingRemoteClient)
if err != nil {
return nil, err
}
systemHandler := handler.ProvideSystemHandler(updateService)
adminSubscriptionHandler := admin.NewSubscriptionHandler(subscriptionService)
adminUsageHandler := admin.NewUsageHandler(usageService, apiKeyService, adminService)
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, settingHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler)
billingService := service.NewBillingService(configConfig, pricingService)
identityCache := repository.NewIdentityCache(redisClient)
identityService := service.NewIdentityService(identityCache)
......
......@@ -396,12 +396,42 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
// Models handles listing available models
// GET /v1/models
// Returns different model lists based on the API key's group platform
// Returns models based on account configurations (model_mapping whitelist)
// Falls back to default models if no whitelist is configured
func (h *GatewayHandler) Models(c *gin.Context) {
apiKey, _ := middleware2.GetApiKeyFromContext(c)
// Return OpenAI models for OpenAI platform groups
if apiKey != nil && apiKey.Group != nil && apiKey.Group.Platform == "openai" {
var groupID *int64
var platform string
if apiKey != nil && apiKey.Group != nil {
groupID = &apiKey.Group.ID
platform = apiKey.Group.Platform
}
// Get available models from account configurations (without platform filter)
availableModels := h.gatewayService.GetAvailableModels(c.Request.Context(), groupID, "")
if len(availableModels) > 0 {
// Build model list from whitelist
models := make([]claude.Model, 0, len(availableModels))
for _, modelID := range availableModels {
models = append(models, claude.Model{
ID: modelID,
Type: "model",
DisplayName: modelID,
CreatedAt: "2024-01-01T00:00:00Z",
})
}
c.JSON(http.StatusOK, gin.H{
"object": "list",
"data": models,
})
return
}
// Fallback to default models
if platform == "openai" {
c.JSON(http.StatusOK, gin.H{
"object": "list",
"data": openai.DefaultModels,
......@@ -409,7 +439,6 @@ func (h *GatewayHandler) Models(c *gin.Context) {
return
}
// Default: Claude models
c.JSON(http.StatusOK, gin.H{
"object": "list",
"data": claude.DefaultModels,
......
......@@ -1925,3 +1925,58 @@ func (s *GatewayService) countTokensError(c *gin.Context, status int, errType, m
},
})
}
// GetAvailableModels returns the list of models available for a group
// It aggregates model_mapping keys from all schedulable accounts in the group
func (s *GatewayService) GetAvailableModels(ctx context.Context, groupID *int64, platform string) []string {
var accounts []Account
var err error
if groupID != nil {
accounts, err = s.accountRepo.ListSchedulableByGroupID(ctx, *groupID)
} else {
accounts, err = s.accountRepo.ListSchedulable(ctx)
}
if err != nil || len(accounts) == 0 {
return nil
}
// Filter by platform if specified
if platform != "" {
filtered := make([]Account, 0)
for _, acc := range accounts {
if acc.Platform == platform {
filtered = append(filtered, acc)
}
}
accounts = filtered
}
// Collect unique models from all accounts
modelSet := make(map[string]struct{})
hasAnyMapping := false
for _, acc := range accounts {
mapping := acc.GetModelMapping()
if len(mapping) > 0 {
hasAnyMapping = true
for model := range mapping {
modelSet[model] = struct{}{}
}
}
}
// If no account has model_mapping, return nil (use default)
if !hasAnyMapping {
return nil
}
// Convert to slice
models := make([]string, 0, len(modelSet))
for model := range modelSet {
models = append(models, model)
}
return models
}
This diff is collapsed.
......@@ -11,6 +11,7 @@
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"@lobehub/icons": "^4.0.2",
"@vueuse/core": "^10.7.0",
"axios": "^1.6.2",
"chart.js": "^4.4.1",
......@@ -25,6 +26,7 @@
},
"devDependencies": {
"@types/file-saver": "^2.0.7",
"@types/mdx": "^2.0.13",
"@types/node": "^20.10.5",
"@vitejs/plugin-vue": "^5.2.3",
"autoprefixer": "^10.4.16",
......
......@@ -20,6 +20,9 @@ importers:
driver.js:
specifier: ^1.4.0
version: 1.4.0
file-saver:
specifier: ^2.0.5
version: 2.0.5
pinia:
specifier: ^2.1.7
version: 2.3.1(typescript@5.6.3)(vue@3.5.26(typescript@5.6.3))
......@@ -35,7 +38,13 @@ importers:
vue-router:
specifier: ^4.2.5
version: 4.6.4(vue@3.5.26(typescript@5.6.3))
xlsx:
specifier: ^0.18.5
version: 0.18.5
devDependencies:
'@types/file-saver':
specifier: ^2.0.7
version: 2.0.7
'@types/node':
specifier: ^20.10.5
version: 20.19.27
......@@ -303,67 +312,56 @@ packages:
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.54.0':
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.54.0':
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.54.0':
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.54.0':
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.54.0':
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.54.0':
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.54.0':
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.54.0':
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
......@@ -393,6 +391,9 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/file-saver@2.0.7':
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
'@types/node@20.19.27':
resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==}
......@@ -467,6 +468,10 @@ packages:
'@vueuse/shared@10.11.1':
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
adler-32@1.3.1:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
alien-signals@1.0.13:
resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==}
......@@ -531,6 +536,10 @@ packages:
caniuse-lite@1.0.30001761:
resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==}
cfb@1.2.2:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chart.js@4.5.1:
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
engines: {pnpm: '>=8'}
......@@ -543,6 +552,10 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
codepage@1.15.0:
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
engines: {node: '>=0.8'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
......@@ -551,6 +564,11 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
crc-32@1.2.2:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'}
hasBin: true
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
......@@ -630,6 +648,9 @@ packages:
picomatch:
optional: true
file-saver@2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
......@@ -647,6 +668,10 @@ packages:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
frac@1.1.2:
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
engines: {node: '>=0.8'}
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
......@@ -908,6 +933,10 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
ssf@0.11.2:
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
engines: {node: '>=0.8'}
strip-ansi@7.1.2:
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
engines: {node: '>=12'}
......@@ -1078,6 +1107,19 @@ packages:
typescript:
optional: true
wmf@1.0.2:
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
engines: {node: '>=0.8'}
word@0.3.0:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'}
xlsx@0.18.5:
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
engines: {node: '>=0.8'}
hasBin: true
snapshots:
'@alloc/quick-lru@5.2.0': {}
......@@ -1278,6 +1320,8 @@ snapshots:
'@types/estree@1.0.8': {}
'@types/file-saver@2.0.7': {}
'@types/node@20.19.27':
dependencies:
undici-types: 6.21.0
......@@ -1394,6 +1438,8 @@ snapshots:
- '@vue/composition-api'
- vue
adler-32@1.3.1: {}
alien-signals@1.0.13: {}
ansi-regex@6.2.2: {}
......@@ -1457,6 +1503,11 @@ snapshots:
caniuse-lite@1.0.30001761: {}
cfb@1.2.2:
dependencies:
adler-32: 1.3.1
crc-32: 1.2.2
chart.js@4.5.1:
dependencies:
'@kurkle/color': 0.3.4
......@@ -1477,12 +1528,16 @@ snapshots:
dependencies:
readdirp: 4.1.2
codepage@1.15.0: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@4.1.1: {}
crc-32@1.2.2: {}
cssesc@3.0.0: {}
csstype@3.2.3: {}
......@@ -1568,6 +1623,8 @@ snapshots:
optionalDependencies:
picomatch: 4.0.3
file-saver@2.0.5: {}
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
......@@ -1582,6 +1639,8 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
frac@1.1.2: {}
fraction.js@5.3.4: {}
fsevents@2.3.3:
......@@ -1818,6 +1877,10 @@ snapshots:
source-map-js@1.2.1: {}
ssf@0.11.2:
dependencies:
frac: 1.1.2
strip-ansi@7.1.2:
dependencies:
ansi-regex: 6.2.2
......@@ -1960,3 +2023,17 @@ snapshots:
'@vue/shared': 3.5.26
optionalDependencies:
typescript: 5.6.3
wmf@1.0.2: {}
word@0.3.0: {}
xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
cfb: 1.2.2
codepage: 1.15.0
crc-32: 1.2.2
ssf: 0.11.2
wmf: 1.0.2
word: 0.3.0
......@@ -880,47 +880,7 @@
<!-- Whitelist Mode -->
<div v-if="modelRestrictionMode === 'whitelist'">
<div class="mb-3 rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20">
<p class="text-xs text-blue-700 dark:text-blue-400">
<svg
class="mr-1 inline h-4 w-4"
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>
{{ t('admin.accounts.selectAllowedModels') }}
</p>
</div>
<!-- Model Checkbox List -->
<div class="mb-3 grid grid-cols-2 gap-2">
<label
v-for="model in commonModels"
:key="model.value"
class="flex cursor-pointer items-center rounded-lg border p-3 transition-all hover:bg-gray-50 dark:border-dark-600 dark:hover:bg-dark-700"
:class="
allowedModels.includes(model.value)
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-200'
"
>
<input
type="checkbox"
:value="model.value"
v-model="allowedModels"
class="mr-2 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{{ model.label }}</span>
</label>
</div>
<ModelWhitelistSelector v-model="allowedModels" :platform="form.platform" />
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{
......@@ -1549,6 +1509,7 @@
import { ref, reactive, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { claudeModels, getPresetMappingsByPlatform, getModelsByPlatform, commonErrorCodes, buildModelMappingObject } from '@/composables/useModelWhitelist'
import { useAuthStore } from '@/stores/auth'
import { adminAPI } from '@/api/admin'
import {
......@@ -1563,6 +1524,7 @@ import type { Proxy, Group, AccountPlatform, AccountType } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue'
import ProxySelector from '@/components/common/ProxySelector.vue'
import GroupSelector from '@/components/common/GroupSelector.vue'
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
// Type for exposed OAuthAuthorizationFlow component
......@@ -1676,37 +1638,6 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_
const geminiAIStudioOAuthEnabled = ref(false)
const showAdvancedOAuth = ref(false)
// Common models for whitelist - Anthropic
const anthropicModels = [
{ value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' },
{ value: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4' },
{ value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5' },
{ value: 'claude-3-5-haiku-20241022', label: 'Claude 3.5 Haiku' },
{ value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' },
{ value: 'claude-3-opus-20240229', label: 'Claude 3 Opus' },
{ value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet' },
{ value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku' }
]
// Common models for whitelist - OpenAI
const openaiModels = [
{ value: 'gpt-5.2-2025-12-11', label: 'GPT-5.2' },
{ value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex' },
{ value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' },
{ 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' }
]
// Common models for whitelist - Gemini
const geminiModels = [
{ value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' },
{ value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash Lite' },
{ value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' },
{ value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash' }
]
const geminiQuotaDocs = {
codeAssist: 'https://developers.google.com/gemini-code-assist/resources/quotas',
aiStudio: 'https://ai.google.dev/pricing',
......@@ -1721,147 +1652,8 @@ const geminiHelpLinks = {
countryCheck: 'https://policies.google.com/country-association-form'
}
// Computed: current models based on platform
const commonModels = computed(() => {
if (form.platform === 'openai') return openaiModels
if (form.platform === 'gemini') return geminiModels
return anthropicModels
})
// Preset mappings for quick add - Anthropic
const anthropicPresetMappings = [
{
label: 'Sonnet 4',
from: 'claude-sonnet-4-20250514',
to: 'claude-sonnet-4-20250514',
color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400'
},
{
label: 'Sonnet 4.5',
from: 'claude-sonnet-4-5-20250929',
to: 'claude-sonnet-4-5-20250929',
color:
'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400'
},
{
label: 'Opus 4.5',
from: 'claude-opus-4-5-20251101',
to: 'claude-opus-4-5-20251101',
color:
'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
},
{
label: 'Haiku 3.5',
from: 'claude-3-5-haiku-20241022',
to: 'claude-3-5-haiku-20241022',
color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400'
},
{
label: 'Haiku 4.5',
from: 'claude-haiku-4-5-20251001',
to: 'claude-haiku-4-5-20251001',
color:
'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400'
},
{
label: 'Opus->Sonnet',
from: 'claude-opus-4-5-20251101',
to: 'claude-sonnet-4-5-20250929',
color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400'
}
]
// Preset mappings for quick add - OpenAI
const openaiPresetMappings = [
{
label: 'GPT-5.2',
from: 'gpt-5.2-2025-12-11',
to: 'gpt-5.2-2025-12-11',
color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400'
},
{
label: 'GPT-5.2 Codex',
from: 'gpt-5.2-codex',
to: 'gpt-5.2-codex',
color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400'
},
{
label: 'GPT-5.1 Codex',
from: 'gpt-5.1-codex',
to: 'gpt-5.1-codex',
color:
'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400'
},
{
label: 'Codex Max',
from: 'gpt-5.1-codex-max',
to: 'gpt-5.1-codex-max',
color:
'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
},
{
label: 'Codex Mini',
from: 'gpt-5.1-codex-mini',
to: 'gpt-5.1-codex-mini',
color:
'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400'
},
{
label: 'Max->Codex',
from: 'gpt-5.1-codex-max',
to: 'gpt-5.1-codex',
color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400'
}
]
// Preset mappings for quick add - Gemini
const geminiPresetMappings = [
{
label: 'Flash',
from: 'gemini-2.0-flash',
to: 'gemini-2.0-flash',
color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400'
},
{
label: 'Flash Lite',
from: 'gemini-2.0-flash-lite',
to: 'gemini-2.0-flash-lite',
color:
'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400'
},
{
label: '1.5 Pro',
from: 'gemini-1.5-pro',
to: 'gemini-1.5-pro',
color:
'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
},
{
label: '1.5 Flash',
from: 'gemini-1.5-flash',
to: 'gemini-1.5-flash',
color:
'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400'
}
]
// Computed: current preset mappings based on platform
const presetMappings = computed(() => {
if (form.platform === 'openai') return openaiPresetMappings
if (form.platform === 'gemini') return geminiPresetMappings
return anthropicPresetMappings
})
// Common HTTP error codes for quick selection
const commonErrorCodes = [
{ value: 401, label: 'Unauthorized' },
{ value: 403, label: 'Forbidden' },
{ value: 429, label: 'Rate Limit' },
{ value: 500, label: 'Server Error' },
{ value: 502, label: 'Bad Gateway' },
{ value: 503, label: 'Unavailable' },
{ value: 529, label: 'Overloaded' }
]
const presetMappings = computed(() => getPresetMappingsByPlatform(form.platform))
const form = reactive({
name: '',
......@@ -1899,7 +1691,10 @@ const canExchangeCode = computed(() => {
watch(
() => props.show,
(newVal) => {
if (!newVal) {
if (newVal) {
// Modal opened - fill related models
allowedModels.value = [...getModelsByPlatform(form.platform)]
} else {
resetForm()
}
}
......@@ -1973,6 +1768,16 @@ const handleSelectGeminiOAuthType = (oauthType: 'code_assist' | 'google_one' | '
geminiOAuthType.value = oauthType
}
// Auto-fill related models when switching to whitelist mode or changing platform
watch(
[modelRestrictionMode, () => form.platform],
([newMode]) => {
if (newMode === 'whitelist') {
allowedModels.value = [...getModelsByPlatform(form.platform)]
}
}
)
// Model mapping helpers
const addModelMapping = () => {
modelMappings.value.push({ from: '', to: '' })
......@@ -1983,9 +1788,7 @@ const removeModelMapping = (index: number) => {
}
const addPresetMapping = (from: string, to: string) => {
// Check if mapping already exists
const exists = modelMappings.value.some((m) => m.from === from)
if (exists) {
if (modelMappings.value.some((m) => m.from === from)) {
appStore.showInfo(t('admin.accounts.mappingExists', { model: from }))
return
}
......@@ -2025,28 +1828,6 @@ const removeErrorCode = (code: number) => {
}
}
const buildModelMappingObject = (): Record<string, string> | null => {
const mapping: Record<string, string> = {}
if (modelRestrictionMode.value === 'whitelist') {
// Whitelist mode: map model to itself
for (const model of allowedModels.value) {
mapping[model] = model
}
} else {
// Mapping mode: use custom mappings
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
}
// Methods
const resetForm = () => {
step.value = 1
......@@ -2064,7 +1845,7 @@ const resetForm = () => {
apiKeyValue.value = ''
modelMappings.value = []
modelRestrictionMode.value = 'whitelist'
allowedModels.value = []
allowedModels.value = [...claudeModels] // Default fill related models
customErrorCodesEnabled.value = false
selectedErrorCodes.value = []
customErrorCodeInput.value = null
......@@ -2113,7 +1894,7 @@ const handleSubmit = async () => {
}
// Add model mapping if configured
const modelMapping = buildModelMappingObject()
const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value)
if (modelMapping) {
credentials.model_mapping = modelMapping
}
......
......@@ -111,47 +111,7 @@
<!-- Whitelist Mode -->
<div v-if="modelRestrictionMode === 'whitelist'">
<div class="mb-3 rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20">
<p class="text-xs text-blue-700 dark:text-blue-400">
<svg
class="mr-1 inline h-4 w-4"
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>
{{ t('admin.accounts.selectAllowedModels') }}
</p>
</div>
<!-- Model Checkbox List -->
<div class="mb-3 grid grid-cols-2 gap-2">
<label
v-for="model in commonModels"
:key="model.value"
class="flex cursor-pointer items-center rounded-lg border p-3 transition-all hover:bg-gray-50 dark:border-dark-600 dark:hover:bg-dark-700"
:class="
allowedModels.includes(model.value)
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-200'
"
>
<input
type="checkbox"
:value="model.value"
v-model="allowedModels"
class="mr-2 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{{ model.label }}</span>
</label>
</div>
<ModelWhitelistSelector v-model="allowedModels" :platform="account?.platform || 'anthropic'" />
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{
......@@ -565,6 +525,12 @@ import BaseDialog from '@/components/common/BaseDialog.vue'
import Select from '@/components/common/Select.vue'
import ProxySelector from '@/components/common/ProxySelector.vue'
import GroupSelector from '@/components/common/GroupSelector.vue'
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
import {
getPresetMappingsByPlatform,
commonErrorCodes,
buildModelMappingObject
} from '@/composables/useModelWhitelist'
interface Props {
show: boolean
......@@ -610,167 +576,8 @@ const customErrorCodeInput = ref<number | null>(null)
const interceptWarmupRequests = ref(false)
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
// Common models for whitelist - Anthropic
const anthropicModels = [
{ value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' },
{ value: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4' },
{ value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5' },
{ value: 'claude-3-5-haiku-20241022', label: 'Claude 3.5 Haiku' },
{ value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' },
{ value: 'claude-3-opus-20240229', label: 'Claude 3 Opus' },
{ value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet' },
{ value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku' }
]
// Common models for whitelist - OpenAI
const openaiModels = [
{ value: 'gpt-5.2-2025-12-11', label: 'GPT-5.2' },
{ value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex' },
{ value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' },
{ 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' }
]
// Common models for whitelist - Gemini
const geminiModels = [
{ value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' },
{ value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash Lite' },
{ value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' },
{ value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash' }
]
// Computed: current models based on platform
const commonModels = computed(() => {
if (props.account?.platform === 'openai') return openaiModels
if (props.account?.platform === 'gemini') return geminiModels
return anthropicModels
})
// Preset mappings for quick add - Anthropic
const anthropicPresetMappings = [
{
label: 'Sonnet 4',
from: 'claude-sonnet-4-20250514',
to: 'claude-sonnet-4-20250514',
color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400'
},
{
label: 'Sonnet 4.5',
from: 'claude-sonnet-4-5-20250929',
to: 'claude-sonnet-4-5-20250929',
color:
'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400'
},
{
label: 'Opus 4.5',
from: 'claude-opus-4-5-20251101',
to: 'claude-opus-4-5-20251101',
color:
'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
},
{
label: 'Haiku 3.5',
from: 'claude-3-5-haiku-20241022',
to: 'claude-3-5-haiku-20241022',
color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400'
},
{
label: 'Haiku 4.5',
from: 'claude-haiku-4-5-20251001',
to: 'claude-haiku-4-5-20251001',
color:
'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400'
},
{
label: 'Opus->Sonnet',
from: 'claude-opus-4-5-20251101',
to: 'claude-sonnet-4-5-20250929',
color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400'
}
]
// Preset mappings for quick add - OpenAI
const openaiPresetMappings = [
{
label: 'GPT-5.2',
from: 'gpt-5.2-2025-12-11',
to: 'gpt-5.2-2025-12-11',
color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400'
},
{
label: 'GPT-5.2 Codex',
from: 'gpt-5.2-codex',
to: 'gpt-5.2-codex',
color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400'
},
{
label: 'GPT-5.1 Codex',
from: 'gpt-5.1-codex',
to: 'gpt-5.1-codex',
color:
'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400'
},
{
label: 'Codex Max',
from: 'gpt-5.1-codex-max',
to: 'gpt-5.1-codex-max',
color:
'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
},
{
label: 'Codex Mini',
from: 'gpt-5.1-codex-mini',
to: 'gpt-5.1-codex-mini',
color:
'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400'
},
{
label: 'Max->Codex',
from: 'gpt-5.1-codex-max',
to: 'gpt-5.1-codex',
color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400'
}
]
// Preset mappings for quick add - Gemini
const geminiPresetMappings = [
{
label: 'Flash',
from: 'gemini-2.0-flash',
to: 'gemini-2.0-flash',
color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400'
},
{
label: 'Flash Lite',
from: 'gemini-2.0-flash-lite',
to: 'gemini-2.0-flash-lite',
color:
'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400'
},
{
label: '1.5 Pro',
from: 'gemini-1.5-pro',
to: 'gemini-1.5-pro',
color:
'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
},
{
label: '1.5 Flash',
from: 'gemini-1.5-flash',
to: 'gemini-1.5-flash',
color:
'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400'
}
]
// Computed: current preset mappings based on platform
const presetMappings = computed(() => {
if (props.account?.platform === 'openai') return openaiPresetMappings
if (props.account?.platform === 'gemini') return geminiPresetMappings
return anthropicPresetMappings
})
const presetMappings = computed(() => getPresetMappingsByPlatform(props.account?.platform || 'anthropic'))
// Computed: default base URL based on platform
const defaultBaseUrl = computed(() => {
......@@ -779,17 +586,6 @@ const defaultBaseUrl = computed(() => {
return 'https://api.anthropic.com'
})
// Common HTTP error codes for quick selection
const commonErrorCodes = [
{ value: 401, label: 'Unauthorized' },
{ value: 403, label: 'Forbidden' },
{ value: 429, label: 'Rate Limit' },
{ value: 500, label: 'Server Error' },
{ value: 502, label: 'Bad Gateway' },
{ value: 503, label: 'Unavailable' },
{ value: 529, label: 'Overloaded' }
]
const form = reactive({
name: '',
proxy_id: null as number | null,
......@@ -940,28 +736,6 @@ const removeErrorCode = (code: number) => {
}
}
const buildModelMappingObject = (): Record<string, string> | null => {
const mapping: Record<string, string> = {}
if (modelRestrictionMode.value === 'whitelist') {
// Whitelist mode: model maps to itself
for (const model of allowedModels.value) {
mapping[model] = model
}
} else {
// Mapping mode: use the mapping entries
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
}
// Methods
const handleClose = () => {
emit('close')
......@@ -978,7 +752,7 @@ const handleSubmit = async () => {
if (props.account.type === 'apikey') {
const currentCredentials = (props.account.credentials as Record<string, unknown>) || {}
const newBaseUrl = editBaseUrl.value.trim() || defaultBaseUrl.value
const modelMapping = buildModelMappingObject()
const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value)
// Always update credentials for apikey type to handle model mapping changes
const newCredentials: Record<string, unknown> = {
......
<template>
<div>
<!-- Multi-select Dropdown -->
<div class="relative mb-3">
<div
@click="toggleDropdown"
class="cursor-pointer rounded-lg border border-gray-300 bg-white px-3 py-2 dark:border-dark-500 dark:bg-dark-700"
>
<div class="grid grid-cols-2 gap-1.5">
<span
v-for="model in modelValue"
:key="model"
class="inline-flex items-center justify-between gap-1 rounded bg-gray-100 px-2 py-1 text-xs text-gray-700 dark:bg-dark-600 dark:text-gray-300"
>
<span class="flex items-center gap-1 truncate">
<ModelIcon :model="model" size="14px" />
<span class="truncate">{{ model }}</span>
</span>
<button
type="button"
@click.stop="removeModel(model)"
class="shrink-0 rounded-full hover:bg-gray-200 dark:hover:bg-dark-500"
>
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</span>
</div>
<div class="mt-2 flex items-center justify-between border-t border-gray-200 pt-2 dark:border-dark-600">
<span class="text-xs text-gray-400">{{ t('admin.accounts.modelCount', { count: modelValue.length }) }}</span>
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
<!-- Dropdown List -->
<div
v-if="showDropdown"
class="absolute left-0 right-0 top-full z-50 mt-1 rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dark-600 dark:bg-dark-700"
>
<div class="sticky top-0 border-b border-gray-200 bg-white p-2 dark:border-dark-600 dark:bg-dark-700">
<input
v-model="searchQuery"
type="text"
class="input w-full text-sm"
:placeholder="t('admin.accounts.searchModels')"
@click.stop
/>
</div>
<div class="max-h-52 overflow-auto">
<button
v-for="model in filteredModels"
:key="model.value"
type="button"
@click="toggleModel(model.value)"
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-dark-600"
>
<span
:class="[
'flex h-4 w-4 shrink-0 items-center justify-center rounded border',
modelValue.includes(model.value)
? 'border-primary-500 bg-primary-500 text-white'
: 'border-gray-300 dark:border-dark-500'
]"
>
<svg v-if="modelValue.includes(model.value)" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
</svg>
</span>
<ModelIcon :model="model.value" size="18px" />
<span class="truncate text-gray-900 dark:text-white">{{ model.value }}</span>
</button>
<div v-if="filteredModels.length === 0" class="px-3 py-4 text-center text-sm text-gray-500">
{{ t('admin.accounts.noMatchingModels') }}
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="mb-4 flex flex-wrap gap-2">
<button
type="button"
@click="fillRelated"
class="rounded-lg border border-blue-200 px-3 py-1.5 text-sm text-blue-600 hover:bg-blue-50 dark:border-blue-800 dark:text-blue-400 dark:hover:bg-blue-900/30"
>
{{ t('admin.accounts.fillRelatedModels') }}
</button>
<button
type="button"
@click="clearAll"
class="rounded-lg border border-red-200 px-3 py-1.5 text-sm text-red-600 hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-900/30"
>
{{ t('admin.accounts.clearAllModels') }}
</button>
</div>
<!-- Custom Model Input -->
<div class="mb-3">
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.accounts.customModelName') }}</label>
<div class="flex gap-2">
<input
v-model="customModel"
type="text"
class="input flex-1"
:placeholder="t('admin.accounts.enterCustomModelName')"
@keydown.enter.prevent="handleEnter"
@compositionstart="isComposing = true"
@compositionend="isComposing = false"
/>
<button
type="button"
@click="addCustom"
class="rounded-lg bg-primary-50 px-4 py-2 text-sm font-medium text-primary-600 hover:bg-primary-100 dark:bg-primary-900/30 dark:text-primary-400 dark:hover:bg-primary-900/50"
>
{{ t('admin.accounts.addModel') }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import ModelIcon from '@/components/common/ModelIcon.vue'
import { allModels, getModelsByPlatform } from '@/composables/useModelWhitelist'
const { t } = useI18n()
const props = defineProps<{
modelValue: string[]
platform: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string[]]
}>()
const appStore = useAppStore()
const showDropdown = ref(false)
const searchQuery = ref('')
const customModel = ref('')
const isComposing = ref(false)
const filteredModels = computed(() => {
const query = searchQuery.value.toLowerCase().trim()
if (!query) return allModels
return allModels.filter(
m => m.value.toLowerCase().includes(query) || m.label.toLowerCase().includes(query)
)
})
const toggleDropdown = () => {
showDropdown.value = !showDropdown.value
if (!showDropdown.value) searchQuery.value = ''
}
const removeModel = (model: string) => {
emit('update:modelValue', props.modelValue.filter(m => m !== model))
}
const toggleModel = (model: string) => {
if (props.modelValue.includes(model)) {
removeModel(model)
} else {
emit('update:modelValue', [...props.modelValue, model])
}
}
const addCustom = () => {
const model = customModel.value.trim()
if (!model) return
if (props.modelValue.includes(model)) {
appStore.showInfo(t('admin.accounts.modelExists'))
return
}
emit('update:modelValue', [...props.modelValue, model])
customModel.value = ''
}
const handleEnter = () => {
if (!isComposing.value) addCustom()
}
const fillRelated = () => {
const models = getModelsByPlatform(props.platform)
const newModels = [...props.modelValue]
for (const model of models) {
if (!newModels.includes(model)) newModels.push(model)
}
emit('update:modelValue', newModels)
}
const clearAll = () => {
emit('update:modelValue', [])
}
</script>
This diff is collapsed.
// =====================
// 模型列表(硬编码,与 new-api 一致)
// =====================
// OpenAI
const openaiModels = [
'gpt-3.5-turbo', 'gpt-3.5-turbo-0125', 'gpt-3.5-turbo-1106', 'gpt-3.5-turbo-16k',
'gpt-4', 'gpt-4-turbo', 'gpt-4-turbo-preview',
'gpt-4o', 'gpt-4o-2024-08-06', 'gpt-4o-2024-11-20',
'gpt-4o-mini', 'gpt-4o-mini-2024-07-18',
'gpt-4.5-preview',
'gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano',
'o1', 'o1-preview', 'o1-mini', 'o1-pro',
'o3', 'o3-mini', 'o3-pro',
'o4-mini',
'gpt-5', 'gpt-5-mini', 'gpt-5-nano',
'chatgpt-4o-latest',
'gpt-4o-audio-preview', 'gpt-4o-realtime-preview'
]
// Anthropic Claude
export const claudeModels = [
'claude-3-5-sonnet-20241022', 'claude-3-5-sonnet-20240620',
'claude-3-5-haiku-20241022',
'claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307',
'claude-3-7-sonnet-20250219',
'claude-sonnet-4-20250514', 'claude-opus-4-20250514',
'claude-opus-4-1-20250805',
'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001',
'claude-opus-4-5-20251101',
'claude-2.1', 'claude-2.0', 'claude-instant-1.2'
]
// Google Gemini
const geminiModels = [
'gemini-2.0-flash', 'gemini-2.0-flash-lite-preview', 'gemini-2.0-flash-exp',
'gemini-2.0-pro-exp', 'gemini-2.0-flash-thinking-exp',
'gemini-2.5-pro-exp-03-25', 'gemini-2.5-pro-preview-03-25',
'gemini-3-pro-preview',
'gemini-1.5-pro', 'gemini-1.5-pro-latest',
'gemini-1.5-flash', 'gemini-1.5-flash-latest', 'gemini-1.5-flash-8b',
'gemini-exp-1206'
]
// 智谱 GLM
const zhipuModels = [
'glm-4', 'glm-4v', 'glm-4-plus', 'glm-4-0520',
'glm-4-air', 'glm-4-airx', 'glm-4-long', 'glm-4-flash',
'glm-4v-plus', 'glm-4.5', 'glm-4.6',
'glm-3-turbo', 'glm-4-alltools',
'chatglm_turbo', 'chatglm_pro', 'chatglm_std', 'chatglm_lite',
'cogview-3', 'cogvideo'
]
// 阿里 通义千问
const qwenModels = [
'qwen-turbo', 'qwen-plus', 'qwen-max', 'qwen-max-longcontext', 'qwen-long',
'qwen2-72b-instruct', 'qwen2-57b-a14b-instruct', 'qwen2-7b-instruct',
'qwen2.5-72b-instruct', 'qwen2.5-32b-instruct', 'qwen2.5-14b-instruct',
'qwen2.5-7b-instruct', 'qwen2.5-3b-instruct', 'qwen2.5-1.5b-instruct',
'qwen2.5-coder-32b-instruct', 'qwen2.5-coder-14b-instruct', 'qwen2.5-coder-7b-instruct',
'qwen3-235b-a22b',
'qwq-32b', 'qwq-32b-preview'
]
// DeepSeek
const deepseekModels = [
'deepseek-chat', 'deepseek-coder', 'deepseek-reasoner',
'deepseek-v3', 'deepseek-v3-0324',
'deepseek-r1', 'deepseek-r1-0528',
'deepseek-r1-distill-qwen-32b', 'deepseek-r1-distill-qwen-14b', 'deepseek-r1-distill-qwen-7b',
'deepseek-r1-distill-llama-70b', 'deepseek-r1-distill-llama-8b'
]
// Mistral
const mistralModels = [
'mistral-small-latest', 'mistral-medium-latest', 'mistral-large-latest',
'open-mistral-7b', 'open-mixtral-8x7b', 'open-mixtral-8x22b',
'codestral-latest', 'codestral-mamba',
'pixtral-12b-2409', 'pixtral-large-latest'
]
// Meta Llama
const metaModels = [
'llama-3.3-70b-instruct',
'llama-3.2-90b-vision-instruct', 'llama-3.2-11b-vision-instruct',
'llama-3.2-3b-instruct', 'llama-3.2-1b-instruct',
'llama-3.1-405b-instruct', 'llama-3.1-70b-instruct', 'llama-3.1-8b-instruct',
'llama-3-70b-instruct', 'llama-3-8b-instruct',
'codellama-70b-instruct', 'codellama-34b-instruct', 'codellama-13b-instruct'
]
// xAI Grok
const xaiModels = [
'grok-4', 'grok-4-0709',
'grok-3-beta', 'grok-3-mini-beta', 'grok-3-fast-beta',
'grok-2', 'grok-2-vision', 'grok-2-image',
'grok-beta', 'grok-vision-beta'
]
// Cohere
const cohereModels = [
'command-a-03-2025',
'command-r', 'command-r-plus',
'command-r-08-2024', 'command-r-plus-08-2024',
'c4ai-aya-23-35b', 'c4ai-aya-23-8b',
'command', 'command-light'
]
// Yi (01.AI)
const yiModels = [
'yi-large', 'yi-large-turbo', 'yi-large-rag',
'yi-medium', 'yi-medium-200k',
'yi-spark', 'yi-vision',
'yi-1.5-34b-chat', 'yi-1.5-9b-chat', 'yi-1.5-6b-chat'
]
// Moonshot/Kimi
const moonshotModels = [
'moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k',
'kimi-latest'
]
// 字节跳动 豆包
const doubaoModels = [
'doubao-pro-256k', 'doubao-pro-128k', 'doubao-pro-32k', 'doubao-pro-4k',
'doubao-lite-128k', 'doubao-lite-32k', 'doubao-lite-4k',
'doubao-vision-pro-32k', 'doubao-vision-lite-32k',
'doubao-1.5-pro-256k', 'doubao-1.5-pro-32k', 'doubao-1.5-lite-32k',
'doubao-1.5-pro-vision-32k', 'doubao-1.5-thinking-pro'
]
// MiniMax
const minimaxModels = [
'abab6.5-chat', 'abab6.5s-chat', 'abab6.5s-chat-pro',
'abab6-chat',
'abab5.5-chat', 'abab5.5s-chat'
]
// 百度 文心
const baiduModels = [
'ernie-4.0-8k-latest', 'ernie-4.0-8k', 'ernie-4.0-turbo-8k',
'ernie-3.5-8k', 'ernie-3.5-128k',
'ernie-speed-8k', 'ernie-speed-128k', 'ernie-speed-pro-128k',
'ernie-lite-8k', 'ernie-lite-pro-128k',
'ernie-tiny-8k'
]
// 讯飞 星火
const sparkModels = [
'spark-desk', 'spark-desk-v1.1', 'spark-desk-v2.1',
'spark-desk-v3.1', 'spark-desk-v3.5', 'spark-desk-v4.0',
'spark-lite', 'spark-pro', 'spark-max', 'spark-ultra'
]
// 腾讯 混元
const hunyuanModels = [
'hunyuan-lite', 'hunyuan-standard', 'hunyuan-standard-256k',
'hunyuan-pro', 'hunyuan-turbo', 'hunyuan-large',
'hunyuan-vision', 'hunyuan-code'
]
// Perplexity
const perplexityModels = [
'sonar', 'sonar-pro', 'sonar-reasoning',
'llama-3-sonar-small-32k-online', 'llama-3-sonar-large-32k-online',
'llama-3-sonar-small-32k-chat', 'llama-3-sonar-large-32k-chat'
]
// 所有模型(去重)
const allModelsList: string[] = [
...openaiModels,
...claudeModels,
...geminiModels,
...zhipuModels,
...qwenModels,
...deepseekModels,
...mistralModels,
...metaModels,
...xaiModels,
...cohereModels,
...yiModels,
...moonshotModels,
...doubaoModels,
...minimaxModels,
...baiduModels,
...sparkModels,
...hunyuanModels,
...perplexityModels
]
// 转换为下拉选项格式
export const allModels = allModelsList.map(m => ({ value: m, label: m }))
// =====================
// 预设映射
// =====================
const anthropicPresetMappings = [
{ label: 'Sonnet 4', from: 'claude-sonnet-4-20250514', to: 'claude-sonnet-4-20250514', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' },
{ label: 'Sonnet 4.5', from: 'claude-sonnet-4-5-20250929', to: 'claude-sonnet-4-5-20250929', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
{ label: 'Opus 4.5', from: 'claude-opus-4-5-20251101', to: 'claude-opus-4-5-20251101', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' },
{ label: 'Haiku 3.5', from: 'claude-3-5-haiku-20241022', to: 'claude-3-5-haiku-20241022', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' },
{ label: 'Haiku 4.5', from: 'claude-haiku-4-5-20251001', to: 'claude-haiku-4-5-20251001', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' },
{ label: 'Opus->Sonnet', from: 'claude-opus-4-5-20251101', to: 'claude-sonnet-4-5-20250929', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' }
]
const openaiPresetMappings = [
{ label: 'GPT-4o', from: 'gpt-4o', to: 'gpt-4o', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' },
{ label: 'GPT-4o Mini', from: 'gpt-4o-mini', to: 'gpt-4o-mini', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' },
{ label: 'GPT-4.1', from: 'gpt-4.1', to: 'gpt-4.1', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
{ label: 'o1', from: 'o1', to: 'o1', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' },
{ label: 'o3', from: 'o3', to: 'o3', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' },
{ label: 'GPT-5', from: 'gpt-5', to: 'gpt-5', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' }
]
const geminiPresetMappings = [
{ label: 'Flash 2.0', from: 'gemini-2.0-flash', to: 'gemini-2.0-flash', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' },
{ label: 'Flash Lite', from: 'gemini-2.0-flash-lite-preview', to: 'gemini-2.0-flash-lite-preview', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
{ label: '1.5 Pro', from: 'gemini-1.5-pro', to: 'gemini-1.5-pro', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' },
{ label: '1.5 Flash', from: 'gemini-1.5-flash', to: 'gemini-1.5-flash', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' }
]
// =====================
// 常用错误码
// =====================
export const commonErrorCodes = [
{ value: 401, label: 'Unauthorized' },
{ value: 403, label: 'Forbidden' },
{ value: 429, label: 'Rate Limit' },
{ value: 500, label: 'Server Error' },
{ value: 502, label: 'Bad Gateway' },
{ value: 503, label: 'Unavailable' },
{ value: 529, label: 'Overloaded' }
]
// =====================
// 辅助函数
// =====================
// 按平台获取模型
export function getModelsByPlatform(platform: string): string[] {
switch (platform) {
case 'openai': return openaiModels
case 'anthropic':
case 'claude': return claudeModels
case 'gemini': return geminiModels
case 'zhipu': return zhipuModels
case 'qwen': return qwenModels
case 'deepseek': return deepseekModels
case 'mistral': return mistralModels
case 'meta': return metaModels
case 'xai': return xaiModels
case 'cohere': return cohereModels
case 'yi': return yiModels
case 'moonshot': return moonshotModels
case 'doubao': return doubaoModels
case 'minimax': return minimaxModels
case 'baidu': return baiduModels
case 'spark': return sparkModels
case 'hunyuan': return hunyuanModels
case 'perplexity': return perplexityModels
default: return claudeModels
}
}
// 按平台获取预设映射
export function getPresetMappingsByPlatform(platform: string) {
if (platform === 'openai') return openaiPresetMappings
if (platform === 'gemini') return geminiPresetMappings
return anthropicPresetMappings
}
// =====================
// 构建模型映射对象(用于 API)
// =====================
export function buildModelMappingObject(
mode: 'whitelist' | 'mapping',
allowedModels: string[],
modelMappings: { from: string; to: string }[]
): Record<string, string> | null {
const mapping: Record<string, string> = {}
if (mode === 'whitelist') {
for (const model of allowedModels) {
mapping[model] = model
}
} else {
for (const m of modelMappings) {
const from = m.from.trim()
const to = m.to.trim()
if (from && to) mapping[from] = to
}
}
return Object.keys(mapping).length > 0 ? mapping : null
}
......@@ -945,6 +945,15 @@ export default {
actualModel: 'Actual model',
addMapping: 'Add Mapping',
mappingExists: 'Mapping for {model} already exists',
searchModels: 'Search models...',
noMatchingModels: 'No matching models',
fillRelatedModels: 'Fill related models',
clearAllModels: 'Clear all models',
customModelName: 'Custom model name',
enterCustomModelName: 'Enter custom model name',
addModel: 'Add',
modelExists: 'Model already exists',
modelCount: '{count} models',
customErrorCodes: 'Custom Error Codes',
customErrorCodesHint: 'Only stop scheduling for selected error codes',
customErrorCodesWarning:
......
......@@ -1102,6 +1102,15 @@ export default {
actualModel: '实际模型',
addMapping: '添加映射',
mappingExists: '模型 {model} 的映射已存在',
searchModels: '搜索模型...',
noMatchingModels: '没有匹配的模型',
fillRelatedModels: '填入相关模型',
clearAllModels: '清除所有模型',
customModelName: '自定义模型名称',
enterCustomModelName: '输入自定义模型名称',
addModel: '填入',
modelExists: '该模型已存在',
modelCount: '{count} 个模型',
customErrorCodes: '自定义错误码',
customErrorCodesHint: '仅对选中的错误码停止调度',
customErrorCodesWarning: '仅选中的错误码会停止调度,其他错误将返回 500。',
......
......@@ -17,7 +17,8 @@
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
},
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment