diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index c6e08684..64524d51 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -164,27 +164,10 @@

- -
- -
+

{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }} @@ -832,8 +815,12 @@ import ConfirmDialog from '@/components/common/ConfirmDialog.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 Icon from '@/components/icons/Icon.vue' -import { buildModelMappingObject as buildModelMappingPayload } from '@/composables/useModelWhitelist' +import { + buildModelMappingObject as buildModelMappingPayload, + getPresetMappingsByPlatform +} from '@/composables/useModelWhitelist' interface Props { show: boolean @@ -865,26 +852,20 @@ const allAnthropicOAuthOrSetupToken = computed(() => { ) }) -const platformModelPrefix: Record = { - anthropic: ['claude-'], - antigravity: ['claude-', 'gemini-', 'gpt-oss-', 'tab_'], - openai: ['gpt-'], - gemini: ['gemini-'], - sora: [] -} - -const filteredModels = computed(() => { - if (props.selectedPlatforms.length === 0) return allModels - const prefixes = [...new Set(props.selectedPlatforms.flatMap(p => platformModelPrefix[p] || []))] - if (prefixes.length === 0) return allModels - return allModels.filter(m => prefixes.some(prefix => m.value.startsWith(prefix))) -}) - const filteredPresets = computed(() => { - if (props.selectedPlatforms.length === 0) return presetMappings - const prefixes = [...new Set(props.selectedPlatforms.flatMap(p => platformModelPrefix[p] || []))] - if (prefixes.length === 0) return presetMappings - return presetMappings.filter(m => prefixes.some(prefix => m.from.startsWith(prefix))) + if (props.selectedPlatforms.length === 0) return [] + + const dedupedPresets = new Map[number]>() + for (const platform of props.selectedPlatforms) { + for (const preset of getPresetMappingsByPlatform(platform)) { + const key = `${preset.from}=>${preset.to}` + if (!dedupedPresets.has(key)) { + dedupedPresets.set(key, preset) + } + } + } + + return Array.from(dedupedPresets.values()) }) // Model mapping type @@ -937,204 +918,6 @@ const umqModeOptions = computed(() => [ { value: 'serialize', label: t('admin.accounts.quotaControl.rpmLimit.umqModeSerialize') }, ]) -// All models list (combined Anthropic + OpenAI + Gemini) -const allModels = [ - { value: 'claude-opus-4-6', label: 'Claude Opus 4.6' }, - { value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' }, - { 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' }, - { value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex' }, - { value: 'gpt-5.3-codex-spark', label: 'GPT-5.3 Codex Spark' }, - { value: 'gpt-5.4', label: 'GPT-5.4' }, - { 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' }, - { value: 'gemini-3.1-flash-image', label: 'Gemini 3.1 Flash Image' }, - { value: 'gemini-2.5-flash-image', label: 'Gemini 2.5 Flash Image' }, - { 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-pro-image', label: 'Gemini 3 Pro Image (Legacy)' }, - { 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 + Gemini) -const presetMappings = [ - { - 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: 'Opus 4.6', - from: 'claude-opus-4-6', - to: 'claude-opus-4-6-thinking', - color: - 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' - }, - { - label: 'Opus 4.6-thinking', - from: 'claude-opus-4-6-thinking', - to: 'claude-opus-4-6-thinking', - color: - 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' - }, - { - label: 'Sonnet 4.6', - from: 'claude-sonnet-4-6', - to: 'claude-sonnet-4-6', - color: - 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' - }, - { - label: 'Sonnet4→4.6', - from: 'claude-sonnet-4-20250514', - to: 'claude-sonnet-4-6', - color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' - }, - { - label: 'Sonnet4.5→4.6', - from: 'claude-sonnet-4-5-20250929', - to: 'claude-sonnet-4-6', - color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' - }, - { - label: 'Sonnet3.5→4.6', - from: 'claude-3-5-sonnet-20241022', - to: 'claude-sonnet-4-6', - color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' - }, - { - label: 'Opus4.5→4.6', - from: 'claude-opus-4-5-20251101', - to: 'claude-opus-4-6-thinking', - color: - 'bg-violet-100 text-violet-700 hover:bg-violet-200 dark:bg-violet-900/30 dark:text-violet-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' - }, - { - label: 'Gemini 2.5 Image', - from: 'gemini-2.5-flash-image', - to: 'gemini-2.5-flash-image', - color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' - }, - { - label: 'Gemini 3.1 Image', - from: 'gemini-3.1-flash-image', - to: 'gemini-3.1-flash-image', - color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' - }, - { - label: 'G3 Image→3.1', - from: 'gemini-3-pro-image', - to: 'gemini-3.1-flash-image', - color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' - }, - { - label: 'GPT-5.3 Codex', - from: 'gpt-5.3-codex', - to: 'gpt-5.3-codex', - color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' - }, - { - label: 'GPT-5.3 Spark', - from: 'gpt-5.3-codex-spark', - to: 'gpt-5.3-codex-spark', - color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' - }, - { - label: 'GPT-5.4', - from: 'gpt-5.4', - to: 'gpt-5.4', - color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400' - }, - { - label: '5.2→5.3', - from: 'gpt-5.2-codex', - to: 'gpt-5.3-codex', - color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400' - }, - { - 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: 'Max->Codex', - 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: '3-Pro-Preview→3.1-Pro-High', - from: 'gemini-3-pro-preview', - to: 'gemini-3.1-pro-high', - color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' - }, - { - label: '3-Pro-High→3.1-Pro-High', - from: 'gemini-3-pro-high', - to: 'gemini-3.1-pro-high', - color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' - }, - { - label: '3-Pro-Low→3.1-Pro-Low', - from: 'gemini-3-pro-low', - to: 'gemini-3.1-pro-low', - color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400' - }, - { - label: '3-Flash透传', - from: 'gemini-3-flash', - to: 'gemini-3-flash', - color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400' - }, - { - label: '2.5-Flash-Lite透传', - from: 'gemini-2.5-flash-lite', - to: 'gemini-2.5-flash-lite', - color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' - } -] - // Common HTTP error codes const commonErrorCodes = [ { value: 401, label: 'Unauthorized' }, diff --git a/frontend/src/components/account/ModelWhitelistSelector.vue b/frontend/src/components/account/ModelWhitelistSelector.vue index 16ffa225..ebce3740 100644 --- a/frontend/src/components/account/ModelWhitelistSelector.vue +++ b/frontend/src/components/account/ModelWhitelistSelector.vue @@ -131,7 +131,8 @@ const { t } = useI18n() const props = defineProps<{ modelValue: string[] - platform: string + platform?: string + platforms?: string[] }>() const emit = defineEmits<{ @@ -144,11 +145,36 @@ const showDropdown = ref(false) const searchQuery = ref('') const customModel = ref('') const isComposing = ref(false) +const normalizedPlatforms = computed(() => { + const rawPlatforms = + props.platforms && props.platforms.length > 0 + ? props.platforms + : props.platform + ? [props.platform] + : [] + + return Array.from( + new Set( + rawPlatforms + .map(platform => platform?.trim()) + .filter((platform): platform is string => Boolean(platform)) + ) + ) +}) + const availableOptions = computed(() => { - if (props.platform === 'sora') { - return getModelsByPlatform('sora').map(m => ({ value: m, label: m })) + if (normalizedPlatforms.value.length === 0) { + return allModels } - return allModels + + const allowedModels = new Set() + for (const platform of normalizedPlatforms.value) { + for (const model of getModelsByPlatform(platform)) { + allowedModels.add(model) + } + } + + return allModels.filter(model => allowedModels.has(model.value)) }) const filteredModels = computed(() => { @@ -192,10 +218,13 @@ const handleEnter = () => { } const fillRelated = () => { - const models = getModelsByPlatform(props.platform) const newModels = [...props.modelValue] - for (const model of models) { - if (!newModels.includes(model)) newModels.push(model) + for (const platform of normalizedPlatforms.value) { + for (const model of getModelsByPlatform(platform)) { + if (!newModels.includes(model)) { + newModels.push(model) + } + } } emit('update:modelValue', newModels) }