diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx index 3facd801..bbead8e4 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx @@ -37,8 +37,8 @@ const ModelSelectBox = ({ options, featureName, className }: { options: ModelOpt options={options} selectedOption={selectedOption} onChangeOption={onChangeOption} - getOptionDisplayName={(option) => option.selection.modelName} - getOptionDropdownName={(option) => option.selection.modelName} + getOptionDisplayName={(option) => option.name} + getOptionDropdownName={(option) => option.name} getOptionDropdownDetail={(option) => option.selection.providerName} getOptionsEqual={(a, b) => optionsEqual([a], [b])} className={className} diff --git a/src/vs/workbench/contrib/void/common/localSingleModelProviders.ts b/src/vs/workbench/contrib/void/common/localSingleModelProviders.ts index 588ae055..58b060ef 100644 --- a/src/vs/workbench/contrib/void/common/localSingleModelProviders.ts +++ b/src/vs/workbench/contrib/void/common/localSingleModelProviders.ts @@ -3,6 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { defaultModelsOfProvider } from './modelCapabilities.js'; import { ProviderName, VoidStatefulModelInfo } from './voidSettingsTypes.js'; /** One loaded model per endpoint (mlx_lm.server / afm). */ @@ -13,16 +14,100 @@ export type SingleAutodetectedLocalProvider = typeof singleAutodetectedLocalProv const canonicalAppleFoundationModelName = (modelName: string) => modelName === 'foundation-models' || modelName === 'foundation-model' ? 'foundation' : modelName +/** Map legacy / alias API ids to one canonical name per provider (avoids duplicate list entries). */ +export const canonicalModelNameForProvider = (providerName: ProviderName, modelName: string): string => { + const lower = modelName.toLowerCase() + + if (providerName === 'appleFoundationModels') { + return canonicalAppleFoundationModelName(modelName) + } + + if (providerName === 'anthropic') { + if (lower === 'claude-sonnet-4-5-20250929') return 'claude-sonnet-4-5' + if (lower === 'claude-3-7-sonnet-20250219') return 'claude-3-7-sonnet-latest' + if (lower === 'claude-sonnet-4-20250514') return 'claude-sonnet-4-6' + if (lower === 'claude-opus-4-20250514') return 'claude-opus-4-6' + } + + if (providerName === 'openAI') { + if (lower === 'gpt-4o-mini') return 'gpt-4.1-mini' + if (lower === 'gpt-4o') return 'gpt-4.1' + if (lower === 'o1-mini') return 'o4-mini' + if (lower === 'o1') return 'o3' + if (lower === 'o3-mini') return 'o4-mini' + } + + if (providerName === 'xAI') { + if (lower.startsWith('grok-2') || lower.startsWith('grok-3')) return 'grok-4.3' + } + + if (providerName === 'mistral') { + if (lower === 'magistral-small-latest') return 'mistral-small-latest' + if (lower === 'devstral-small-latest') return 'devstral-latest' + } + + if (providerName === 'gemini') { + if (lower.includes('preview') || lower.includes('-exp-') || lower.includes('2.0') || lower.includes('1.5')) { + if (lower.includes('pro')) return 'gemini-2.5-pro' + if (lower.includes('flash-lite') || lower.includes('flash_lite')) return 'gemini-2.5-flash-lite' + return 'gemini-2.5-flash' + } + } + + return modelName +} + +const modelTypePriority: Record = { + autodetected: 3, + default: 2, + custom: 1, +} + +/** Collapse duplicate model names (aliases, default+custom overlap, etc.). */ +export const dedupeProviderModels = (providerName: ProviderName, models: VoidStatefulModelInfo[]): VoidStatefulModelInfo[] => { + const defaultNames = new Set(defaultModelsOfProvider[providerName] ?? []) + const groups = new Map() + + for (const model of models) { + const canonical = canonicalModelNameForProvider(providerName, model.modelName) + const normalized = canonical === model.modelName ? model : { ...model, modelName: canonical } + const key = canonical.toLowerCase() + const group = groups.get(key) ?? [] + group.push(normalized) + groups.set(key, group) + } + + const deduped: VoidStatefulModelInfo[] = [] + for (const group of groups.values()) { + const best = group.reduce((keep, candidate) => { + const keepPriority = modelTypePriority[keep.type] + const candidatePriority = modelTypePriority[candidate.type] + if (candidatePriority > keepPriority) return candidate + if (keepPriority > candidatePriority) return keep + const keepInDefaults = defaultNames.has(keep.modelName) ? 1 : 0 + const candidateInDefaults = defaultNames.has(candidate.modelName) ? 1 : 0 + return candidateInDefaults > keepInDefaults ? candidate : keep + }) + deduped.push({ + ...best, + modelName: best.modelName, + isHidden: group.every(m => m.isHidden), + }) + } + + return deduped +} + export const normalizeAutodetectedModelNamesForProvider = (providerName: ProviderName, modelNames: string[]): string[] => { if (providerName === 'appleFoundationModels') { const normalized = modelNames.map(canonicalAppleFoundationModelName) return [new Set(normalized).values().next().value ?? 'foundation'] } if (providerName === 'mlx') { - const unique = [...new Set(modelNames)] + const unique = [...new Set(modelNames.map(n => canonicalModelNameForProvider(providerName, n)))] return unique.length > 0 ? [unique[0]] : [] } - return modelNames + return modelNames.map(n => canonicalModelNameForProvider(providerName, n)) } export const consolidateSingleAutodetectedProviderModels = ( @@ -45,8 +130,8 @@ export const consolidateSingleAutodetectedProviderModels = ( modelName: primaryName, type: 'autodetected', } - return [ + return dedupeProviderModels(providerName, [ primary, - ...customModels.filter(m => m.modelName !== primaryName), - ] + ...customModels.filter(m => canonicalModelNameForProvider(providerName, m.modelName).toLowerCase() !== primaryName.toLowerCase()), + ]) } diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 88518c0f..54a951e9 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -420,8 +420,10 @@ const extensiveModelOptionsFallback: VoidStaticProviderInfo['modelOptionsFallbac if (lower.includes('flash')) return toFallback(geminiModelOptions, 'gemini-2.5-flash') return toFallback(geminiModelOptions, 'gemini-2.5-pro') } + if (lower.includes('gemini')) return toFallback(geminiModelOptions, 'gemini-2.5-flash') - if (lower.includes('claude-3-5') || lower.includes('claude-3.5')) return toFallback(anthropicModelOptions, 'claude-3-5-sonnet-20241022') + if (lower.includes('claude-3-7') || lower.includes('claude-3.7')) return toFallback(anthropicModelOptions, 'claude-3-7-sonnet-20250219') + if (lower.includes('claude-3-5') || lower.includes('claude-3.5')) return toFallback(anthropicModelOptions, 'claude-sonnet-4-6') if (lower.includes('opus-4-7') || lower.includes('opus-4.7')) return toFallback(anthropicModelOptions, 'claude-opus-4-7') if (lower.includes('sonnet-4-6') || lower.includes('sonnet-4.6')) return toFallback(anthropicModelOptions, 'claude-sonnet-4-6') if (lower.includes('haiku-4-5') || lower.includes('haiku-4.5')) return toFallback(anthropicModelOptions, 'claude-haiku-4-5') @@ -434,7 +436,7 @@ const extensiveModelOptionsFallback: VoidStaticProviderInfo['modelOptionsFallbac if (lower.includes('reasoning')) return toFallback(xAIModelOptions, 'grok-4.20-0309-reasoning') return toFallback(xAIModelOptions, 'grok-4.3') } - if (lower.includes('grok2') || lower.includes('grok-2')) return toFallback(xAIModelOptions, 'grok-2') + if (lower.includes('grok2') || lower.includes('grok-2') || lower.includes('grok-3') || lower.includes('grok3')) return toFallback(xAIModelOptions, 'grok-4.3') if (lower.includes('grok')) return toFallback(xAIModelOptions, 'grok-4.3') if (lower.includes('deepseek') && (lower.includes('v4-pro') || lower.includes('v4_pro'))) return toFallback(deepseekModelOptions, 'deepseek-v4-pro') @@ -473,12 +475,12 @@ const extensiveModelOptionsFallback: VoidStaticProviderInfo['modelOptionsFallbac if (lower.includes('gpt') && lower.includes('nano') && (lower.includes('4.1') || lower.includes('4-1'))) return toFallback(openAIModelOptions, 'gpt-4.1-nano') if (lower.includes('gpt') && (lower.includes('4.1') || lower.includes('4-1'))) return toFallback(openAIModelOptions, 'gpt-4.1') - if (lower.includes('4o') && lower.includes('mini')) return toFallback(openAIModelOptions, 'gpt-4o-mini') - if (lower.includes('4o')) return toFallback(openAIModelOptions, 'gpt-4o') + if (lower.includes('4o') && lower.includes('mini')) return toFallback(openAIModelOptions, 'gpt-4.1-mini') + if (lower.includes('4o')) return toFallback(openAIModelOptions, 'gpt-4.1') - if (lower.includes('o1') && lower.includes('mini')) return toFallback(openAIModelOptions, 'o1-mini') - if (lower.includes('o1')) return toFallback(openAIModelOptions, 'o1') - if (lower.includes('o3') && lower.includes('mini')) return toFallback(openAIModelOptions, 'o3-mini') + if (lower.includes('o1') && lower.includes('mini')) return toFallback(openAIModelOptions, 'o4-mini') + if (lower.includes('o1')) return toFallback(openAIModelOptions, 'o3') + if (lower.includes('o3') && lower.includes('mini')) return toFallback(openAIModelOptions, 'o4-mini') if (lower.includes('o3')) return toFallback(openAIModelOptions, 'o3') if (lower.includes('o4') && lower.includes('mini')) return toFallback(openAIModelOptions, 'o4-mini') @@ -565,67 +567,6 @@ const anthropicModelOptions = { reasoningCapabilities: anthropicThinkingCapabilities, }, - 'claude-opus-4-20250514': { - contextWindow: 200_000, - reservedOutputTokenSpace: 8_192, - cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 30.00 }, - downloadable: false, - supportsFIM: false, - specialToolFormat: 'anthropic-style', - supportsSystemMessage: 'separated', - reasoningCapabilities: anthropicThinkingCapabilities, - - }, - 'claude-sonnet-4-20250514': { - contextWindow: 200_000, - reservedOutputTokenSpace: 8_192, - cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 6.00 }, - downloadable: false, - supportsFIM: false, - specialToolFormat: 'anthropic-style', - supportsSystemMessage: 'separated', - reasoningCapabilities: anthropicThinkingCapabilities, - - }, - 'claude-3-5-sonnet-20241022': { - contextWindow: 200_000, - reservedOutputTokenSpace: 8_192, - cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, - downloadable: false, - supportsFIM: false, - specialToolFormat: 'anthropic-style', - supportsSystemMessage: 'separated', - reasoningCapabilities: false, - }, - 'claude-3-5-haiku-20241022': { - contextWindow: 200_000, - reservedOutputTokenSpace: 8_192, - cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, - downloadable: false, - supportsFIM: false, - specialToolFormat: 'anthropic-style', - supportsSystemMessage: 'separated', - reasoningCapabilities: false, - }, - 'claude-3-opus-20240229': { - contextWindow: 200_000, - reservedOutputTokenSpace: 4_096, - cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, - downloadable: false, - supportsFIM: false, - specialToolFormat: 'anthropic-style', - supportsSystemMessage: 'separated', - reasoningCapabilities: false, - }, - 'claude-3-sonnet-20240229': { // no point of using this, but including this for people who put it in - contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, - downloadable: false, - reservedOutputTokenSpace: 4_096, - supportsFIM: false, - specialToolFormat: 'anthropic-style', - supportsSystemMessage: 'separated', - reasoningCapabilities: false, - } } as const satisfies { [s: string]: VoidStaticModelInfo } const anthropicSettings: VoidStaticProviderInfo = { @@ -650,13 +591,10 @@ const anthropicSettings: VoidStaticProviderInfo = { else if (lower.includes('haiku-4-5') || lower.includes('haiku-4.5')) fallbackName = 'claude-haiku-4-5' else if (lower.includes('opus-4-6') || lower.includes('opus-4.6')) fallbackName = 'claude-opus-4-6' else if (lower.includes('sonnet-4-5') || lower.includes('sonnet-4.5')) fallbackName = 'claude-sonnet-4-5-20250929' - else if (lower.includes('claude-4-opus') || lower.includes('claude-opus-4')) fallbackName = 'claude-opus-4-20250514' - else if (lower.includes('claude-4-sonnet') || lower.includes('claude-sonnet-4')) fallbackName = 'claude-sonnet-4-20250514' else if (lower.includes('claude-3-7-sonnet')) fallbackName = 'claude-3-7-sonnet-20250219' - else if (lower.includes('claude-3-5-sonnet')) fallbackName = 'claude-3-5-sonnet-20241022' - else if (lower.includes('claude-3-5-haiku')) fallbackName = 'claude-3-5-haiku-20241022' - else if (lower.includes('claude-3-opus')) fallbackName = 'claude-3-opus-20240229' - else if (lower.includes('claude-3-sonnet')) fallbackName = 'claude-3-sonnet-20240229' + else if (lower.includes('claude-3-5-sonnet') || lower.includes('claude-3-5-haiku') || lower.includes('claude-3-opus') || lower.includes('claude-3-sonnet')) fallbackName = 'claude-sonnet-4-6' + else if (lower.includes('claude-4-opus') || lower.includes('claude-opus-4')) fallbackName = 'claude-opus-4-6' + else if (lower.includes('claude-4-sonnet') || lower.includes('claude-sonnet-4')) fallbackName = 'claude-sonnet-4-6' if (fallbackName) return { modelName: fallbackName, recognizedModelName: fallbackName, ...anthropicModelOptions[fallbackName] } return null }, @@ -762,53 +700,6 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing supportsSystemMessage: 'developer-role', reasoningCapabilities: false, }, - 'o1': { - contextWindow: 128_000, - reservedOutputTokenSpace: 100_000, - cost: { input: 15.00, cache_read: 7.50, output: 60.00, }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'developer-role', - reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: false, reasoningSlider: { type: 'effort_slider', values: ['low', 'medium', 'high'], default: 'low' } }, - }, - 'o3-mini': { - contextWindow: 200_000, - reservedOutputTokenSpace: 100_000, - cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'developer-role', - reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: false, reasoningSlider: { type: 'effort_slider', values: ['low', 'medium', 'high'], default: 'low' } }, - }, - 'gpt-4o': { - contextWindow: 128_000, - reservedOutputTokenSpace: 16_384, - cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, - downloadable: false, - supportsFIM: false, - specialToolFormat: 'openai-style', - supportsSystemMessage: 'system-role', - reasoningCapabilities: false, - }, - 'o1-mini': { - contextWindow: 128_000, - reservedOutputTokenSpace: 65_536, - cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: false, // does not support any system - reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: false, reasoningSlider: { type: 'effort_slider', values: ['low', 'medium', 'high'], default: 'low' } }, - }, - 'gpt-4o-mini': { - contextWindow: 128_000, - reservedOutputTokenSpace: 16_384, - cost: { input: 0.15, cache_read: 0.075, output: 0.60, }, - downloadable: false, - supportsFIM: false, - specialToolFormat: 'openai-style', - supportsSystemMessage: 'system-role', // ?? - reasoningCapabilities: false, - }, } as const satisfies { [s: string]: VoidStaticModelInfo } @@ -837,17 +728,17 @@ const openAISettings: VoidStaticProviderInfo = { else fallbackName = 'gpt-5.4' } else if (lower.includes('o4') && lower.includes('mini')) fallbackName = 'o4-mini' - else if (lower.includes('o3') && lower.includes('mini')) fallbackName = 'o3-mini' + else if (lower.includes('o3') && lower.includes('mini')) fallbackName = 'o4-mini' else if (lower.includes('o3')) fallbackName = 'o3' - else if (lower.includes('o1') && lower.includes('mini')) fallbackName = 'o1-mini' - else if (lower.includes('o1')) fallbackName = 'o1' + else if (lower.includes('o1') && lower.includes('mini')) fallbackName = 'o4-mini' + else if (lower.includes('o1')) fallbackName = 'o3' else if (lower.includes('gpt-4.1') || lower.includes('gpt4.1')) { if (lower.includes('nano')) fallbackName = 'gpt-4.1-nano' else if (lower.includes('mini')) fallbackName = 'gpt-4.1-mini' else fallbackName = 'gpt-4.1' } - else if (lower.includes('4o') && lower.includes('mini')) fallbackName = 'gpt-4o-mini' - else if (lower.includes('4o') || lower.includes('gpt-4o')) fallbackName = 'gpt-4o' + else if (lower.includes('4o') && lower.includes('mini')) fallbackName = 'gpt-4.1-mini' + else if (lower.includes('4o') || lower.includes('gpt-4o')) fallbackName = 'gpt-4.1' if (fallbackName) return { modelName: fallbackName, recognizedModelName: fallbackName, ...openAIModelOptions[fallbackName] } return null }, @@ -897,57 +788,6 @@ const xAIModelOptions = { specialToolFormat: 'openai-style', reasoningCapabilities: false, }, - 'grok-2': { - contextWindow: 131_072, - reservedOutputTokenSpace: null, - cost: { input: 2.00, output: 10.00 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - specialToolFormat: 'openai-style', - reasoningCapabilities: false, - }, - 'grok-3': { - contextWindow: 131_072, - reservedOutputTokenSpace: null, - cost: { input: 3.00, output: 15.00 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - specialToolFormat: 'openai-style', - reasoningCapabilities: false, - }, - 'grok-3-fast': { - contextWindow: 131_072, - reservedOutputTokenSpace: null, - cost: { input: 5.00, output: 25.00 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - specialToolFormat: 'openai-style', - reasoningCapabilities: false, - }, - // only mini supports thinking - 'grok-3-mini': { - contextWindow: 131_072, - reservedOutputTokenSpace: null, - cost: { input: 0.30, output: 0.50 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - specialToolFormat: 'openai-style', - reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: false, reasoningSlider: { type: 'effort_slider', values: ['low', 'high'], default: 'low' } }, - }, - 'grok-3-mini-fast': { - contextWindow: 131_072, - reservedOutputTokenSpace: null, - cost: { input: 0.60, output: 4.00 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - specialToolFormat: 'openai-style', - reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: false, reasoningSlider: { type: 'effort_slider', values: ['low', 'high'], default: 'low' } }, - }, } as const satisfies { [s: string]: VoidStaticModelInfo } const xAISettings: VoidStaticProviderInfo = { @@ -960,8 +800,7 @@ const xAISettings: VoidStaticProviderInfo = { else fallbackName = 'grok-4.20-0309-reasoning' } else if (lower.includes('grok-4') || lower.includes('grok4.3') || lower.includes('grok-4.3')) fallbackName = 'grok-4.3' - else if (lower.includes('grok-2')) fallbackName = 'grok-2' - else if (lower.includes('grok-3')) fallbackName = 'grok-4.3' // grok-3 aliases redirect to grok-4.3 + else if (lower.includes('grok-2') || lower.includes('grok-3')) fallbackName = 'grok-4.3' else if (lower.includes('grok')) fallbackName = 'grok-4.3' if (fallbackName) return { modelName: fallbackName, recognizedModelName: fallbackName, ...xAIModelOptions[fallbackName] } return null @@ -1039,115 +878,6 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing reasoningReservedOutputTokenSpace: 8192, }, }, - // https://ai.google.dev/gemini-api/docs/thinking#set-budget - 'gemini-2.5-pro-preview-05-06': { - contextWindow: 1_048_576, - reservedOutputTokenSpace: 8_192, - cost: { input: 0, output: 0 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'separated', - specialToolFormat: 'gemini-style', - reasoningCapabilities: { - supportsReasoning: true, - canTurnOffReasoning: true, - canIOReasoning: false, - reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // max is really 24576 - reasoningReservedOutputTokenSpace: 8192, - }, - }, - 'gemini-2.0-flash-lite': { - contextWindow: 1_048_576, - reservedOutputTokenSpace: 8_192, - cost: { input: 0, output: 0 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'separated', - specialToolFormat: 'gemini-style', - reasoningCapabilities: false, // no reasoning - }, - 'gemini-2.5-flash-preview-04-17': { - contextWindow: 1_048_576, - reservedOutputTokenSpace: 8_192, - cost: { input: 0.15, output: .60 }, // TODO $3.50 output with thinking not included - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'separated', - specialToolFormat: 'gemini-style', - reasoningCapabilities: { - supportsReasoning: true, - canTurnOffReasoning: true, - canIOReasoning: false, - reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // max is really 24576 - reasoningReservedOutputTokenSpace: 8192, - }, - }, - 'gemini-2.5-pro-exp-03-25': { - contextWindow: 1_048_576, - reservedOutputTokenSpace: 8_192, - cost: { input: 0, output: 0 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'separated', - specialToolFormat: 'gemini-style', - reasoningCapabilities: { - supportsReasoning: true, - canTurnOffReasoning: true, - canIOReasoning: false, - reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // max is really 24576 - reasoningReservedOutputTokenSpace: 8192, - }, - }, - 'gemini-2.0-flash': { - contextWindow: 1_048_576, - reservedOutputTokenSpace: 8_192, // 8_192, - cost: { input: 0.10, output: 0.40 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'separated', - specialToolFormat: 'gemini-style', - reasoningCapabilities: false, - }, - 'gemini-2.0-flash-lite-preview-02-05': { - contextWindow: 1_048_576, - reservedOutputTokenSpace: 8_192, // 8_192, - cost: { input: 0.075, output: 0.30 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'separated', - specialToolFormat: 'gemini-style', - reasoningCapabilities: false, - }, - 'gemini-1.5-flash': { - contextWindow: 1_048_576, - reservedOutputTokenSpace: 8_192, // 8_192, - cost: { input: 0.075, output: 0.30 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'separated', - specialToolFormat: 'gemini-style', - reasoningCapabilities: false, - }, - 'gemini-1.5-pro': { - contextWindow: 2_097_152, - reservedOutputTokenSpace: 8_192, - cost: { input: 1.25, output: 5.00 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'separated', - specialToolFormat: 'gemini-style', - reasoningCapabilities: false, - }, - 'gemini-1.5-flash-8b': { - contextWindow: 1_048_576, - reservedOutputTokenSpace: 8_192, - cost: { input: 0.0375, output: 0.15 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'separated', - specialToolFormat: 'gemini-style', - reasoningCapabilities: false, - }, } as const satisfies { [s: string]: VoidStaticModelInfo } const geminiSettings: VoidStaticProviderInfo = { @@ -1161,9 +891,7 @@ const geminiSettings: VoidStaticProviderInfo = { else if (lower.includes('flash')) fallbackName = 'gemini-2.5-flash' else fallbackName = 'gemini-2.5-pro' } - else if (lower.includes('2.0') && lower.includes('flash')) fallbackName = 'gemini-2.0-flash' - else if (lower.includes('1.5') && lower.includes('pro')) fallbackName = 'gemini-1.5-pro' - else if (lower.includes('1.5') && lower.includes('flash')) fallbackName = 'gemini-1.5-flash' + else if (lower.includes('2.0') || lower.includes('1.5') || lower.includes('preview') || lower.includes('-exp-')) fallbackName = 'gemini-2.5-flash' if (fallbackName) return { modelName: fallbackName, recognizedModelName: fallbackName, ...geminiModelOptions[fallbackName] } return null }, @@ -1289,26 +1017,6 @@ const mistralModelOptions = { // https://docs.mistral.ai/getting-started/models/ supportsSystemMessage: 'system-role', reasoningCapabilities: { supportsReasoning: true, canIOReasoning: true, canTurnOffReasoning: false, openSourceThinkTags: ['', ''] }, }, - 'magistral-small-latest': { // Magistral Small 1.2 (deprecated → Mistral Small 4) — https://docs.mistral.ai/models/model-cards/magistral-small-1-2-25-09 - contextWindow: 128_000, - reservedOutputTokenSpace: 8_192, - cost: { input: 0.30, output: 0.90 }, - supportsFIM: false, - specialToolFormat: 'openai-style', - downloadable: { sizeGb: 'not-known' }, - supportsSystemMessage: 'system-role', - reasoningCapabilities: { supportsReasoning: true, canIOReasoning: true, canTurnOffReasoning: false, openSourceThinkTags: ['', ''] }, - }, - 'devstral-small-latest': { // Devstral Small 2 (labs, deprecated → Devstral 2) — https://docs.mistral.ai/models/model-cards/devstral-small-2-25-12 - contextWindow: 256_000, - reservedOutputTokenSpace: 8_192, - cost: { input: 0.20, output: 0.80 }, - supportsFIM: false, - specialToolFormat: 'openai-style', - downloadable: { sizeGb: 14 }, - supportsSystemMessage: 'system-role', - reasoningCapabilities: false, - }, 'ministral-14b-latest': { // Ministral 3 14B — https://docs.mistral.ai/models/model-cards/ministral-3-14b-25-12 contextWindow: 256_000, reservedOutputTokenSpace: 4_096, @@ -1347,8 +1055,8 @@ const mistralSettings: VoidStaticProviderInfo = { const lower = modelName.toLowerCase() let fallbackName: keyof typeof mistralModelOptions | null = null if (lower.includes('codestral')) fallbackName = 'codestral-latest' - else if (lower.includes('magistral')) fallbackName = lower.includes('small') ? 'magistral-small-latest' : 'magistral-medium-latest' - else if (lower.includes('devstral')) fallbackName = lower.includes('small') ? 'devstral-small-latest' : 'devstral-latest' + else if (lower.includes('magistral')) fallbackName = lower.includes('small') ? 'mistral-small-latest' : 'magistral-medium-latest' + else if (lower.includes('devstral')) fallbackName = 'devstral-latest' else if (lower.includes('ministral')) { if (lower.includes('14')) fallbackName = 'ministral-14b-latest' else if (lower.includes('8')) fallbackName = 'ministral-8b-latest' @@ -1719,51 +1427,6 @@ const openRouterModelOptions_assumingOpenAICompat = { supportsSystemMessage: 'system-role', reasoningCapabilities: { supportsReasoning: true, canIOReasoning: true, canTurnOffReasoning: false }, }, - 'microsoft/phi-4-reasoning-plus:free': { // a 14B model... - contextWindow: 32_768, - reservedOutputTokenSpace: null, - cost: { input: 0, output: 0 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - reasoningCapabilities: { supportsReasoning: true, canIOReasoning: true, canTurnOffReasoning: false }, - }, - 'mistralai/mistral-small-3.1-24b-instruct:free': { - contextWindow: 128_000, - reservedOutputTokenSpace: null, - cost: { input: 0, output: 0 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - reasoningCapabilities: false, - }, - 'google/gemini-2.0-flash-lite-preview-02-05:free': { - contextWindow: 1_048_576, - reservedOutputTokenSpace: null, - cost: { input: 0, output: 0 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - reasoningCapabilities: false, - }, - 'google/gemini-2.0-pro-exp-02-05:free': { - contextWindow: 1_048_576, - reservedOutputTokenSpace: null, - cost: { input: 0, output: 0 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - reasoningCapabilities: false, - }, - 'google/gemini-2.0-flash-exp:free': { - contextWindow: 1_048_576, - reservedOutputTokenSpace: null, - cost: { input: 0, output: 0 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - reasoningCapabilities: false, - }, 'deepseek/deepseek-r1': { ...openSourceModelOptions_assumingOAICompat.deepseekR1, contextWindow: 128_000, @@ -1771,87 +1434,6 @@ const openRouterModelOptions_assumingOpenAICompat = { cost: { input: 0.8, output: 2.4 }, downloadable: false, }, - 'anthropic/claude-opus-4': { - contextWindow: 200_000, - reservedOutputTokenSpace: null, - cost: { input: 15.00, output: 75.00 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - reasoningCapabilities: false, - }, - 'anthropic/claude-sonnet-4': { - contextWindow: 200_000, - reservedOutputTokenSpace: null, - cost: { input: 3.00, output: 15.00 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - reasoningCapabilities: false, - }, - 'anthropic/claude-3.7-sonnet:thinking': { - contextWindow: 200_000, - reservedOutputTokenSpace: null, - cost: { input: 3.00, output: 15.00 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - reasoningCapabilities: { // same as anthropic, see above - supportsReasoning: true, - canTurnOffReasoning: false, - canIOReasoning: true, - reasoningReservedOutputTokenSpace: 8192, - reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000. - }, - }, - 'anthropic/claude-3.7-sonnet': { - contextWindow: 200_000, - reservedOutputTokenSpace: null, - cost: { input: 3.00, output: 15.00 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - reasoningCapabilities: false, // stupidly, openrouter separates thinking from non-thinking - }, - 'anthropic/claude-3.5-sonnet': { - contextWindow: 200_000, - reservedOutputTokenSpace: null, - cost: { input: 3.00, output: 15.00 }, - downloadable: false, - supportsFIM: false, - supportsSystemMessage: 'system-role', - reasoningCapabilities: false, - }, - 'mistralai/codestral-2501': { - ...openSourceModelOptions_assumingOAICompat.codestral, - contextWindow: 256_000, - reservedOutputTokenSpace: null, - cost: { input: 0.3, output: 0.9 }, - downloadable: false, - reasoningCapabilities: false, - }, - 'mistralai/devstral-small:free': { - ...openSourceModelOptions_assumingOAICompat.devstral, - contextWindow: 130_000, - reservedOutputTokenSpace: null, - cost: { input: 0, output: 0 }, - downloadable: false, - reasoningCapabilities: false, - }, - 'qwen/qwen-2.5-coder-32b-instruct': { - ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], - contextWindow: 33_000, - reservedOutputTokenSpace: null, - cost: { input: 0.07, output: 0.16 }, - downloadable: false, - }, - 'qwen/qwq-32b': { - ...openSourceModelOptions_assumingOAICompat['qwq'], - contextWindow: 33_000, - reservedOutputTokenSpace: null, - cost: { input: 0.07, output: 0.16 }, - downloadable: false, - } } as const satisfies { [s: string]: VoidStaticModelInfo } const openRouterSettings: VoidStaticProviderInfo = { diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 6dd4055b..681c84ca 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -14,7 +14,7 @@ import { IMetricsService } from './metricsService.js'; import { defaultProviderSettings, getModelCapabilities, ModelOverrides } from './modelCapabilities.js'; import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js'; import { isMacintosh } from '../../../../base/common/platform.js'; -import { consolidateSingleAutodetectedProviderModels, normalizeAutodetectedModelNamesForProvider } from './localSingleModelProviders.js'; +import { consolidateSingleAutodetectedProviderModels, dedupeProviderModels, normalizeAutodetectedModelNamesForProvider } from './localSingleModelProviders.js'; import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, MCPUserStateOfName as MCPUserStateOfName, MCPUserState } from './voidSettingsTypes.js'; @@ -86,8 +86,8 @@ export interface IVoidSettingsService { -const _modelsWithSwappedInNewModels = (options: { existingModels: VoidStatefulModelInfo[], models: string[], type: 'autodetected' | 'default' }) => { - const { existingModels, models, type } = options +const _modelsWithSwappedInNewModels = (options: { providerName: ProviderName, existingModels: VoidStatefulModelInfo[], models: string[], type: 'autodetected' | 'default' }) => { + const { providerName, existingModels, models, type } = options const existingModelsMap: Record = {} for (const existingModel of existingModels) { @@ -96,13 +96,13 @@ const _modelsWithSwappedInNewModels = (options: { existingModels: VoidStatefulMo const newDefaultModels = models.map((modelName, i) => ({ modelName, type, isHidden: !!existingModelsMap[modelName]?.isHidden, })) - return [ + return dedupeProviderModels(providerName, [ ...newDefaultModels, // swap out all the models of this type for the new models of this type ...existingModels.filter(m => { const keep = m.type !== type return keep }) - ] + ]) } export const modelFilterOfFeatureName: { @@ -129,7 +129,7 @@ const _stateWithMergedDefaultModels = (state: VoidSettingsState): VoidSettingsSt const defaultModels = defaultSettingsOfProvider[providerName]?.models ?? [] const currentModels = newSettingsOfProvider[providerName]?.models ?? [] const defaultModelNames = defaultModels.map(m => m.modelName) - const newModels = _modelsWithSwappedInNewModels({ existingModels: currentModels, models: defaultModelNames, type: 'default' }) + const newModels = _modelsWithSwappedInNewModels({ providerName, existingModels: currentModels, models: defaultModelNames, type: 'default' }) newSettingsOfProvider = { ...newSettingsOfProvider, [providerName]: { @@ -165,14 +165,40 @@ const _validatedModelState = (state: Omit): } } + // dedupe models (aliases, default+custom overlap, persisted legacy ids) + for (const providerName of providerNames) { + const models = newSettingsOfProvider[providerName].models + const deduped = dedupeProviderModels(providerName, models) + if (deduped.length !== models.length || deduped.some((m, i) => m.modelName !== models[i]?.modelName || m.type !== models[i]?.type || m.isHidden !== models[i]?.isHidden)) { + newSettingsOfProvider = { + ...newSettingsOfProvider, + [providerName]: { + ...newSettingsOfProvider[providerName], + models: deduped, + }, + } + } + } + // update model options + const visibleModelNameCounts = new Map() + for (const providerName of providerNames) { + if (!newSettingsOfProvider[providerName]._didFillInProviderSettings) continue + for (const { modelName, isHidden } of newSettingsOfProvider[providerName].models) { + if (isHidden) continue + visibleModelNameCounts.set(modelName, (visibleModelNameCounts.get(modelName) ?? 0) + 1) + } + } + let newModelOptions: ModelOption[] = [] for (const providerName of providerNames) { const providerTitle = providerName // displayInfoOfProviderName(providerName).title.toLowerCase() // looks better lowercase, best practice to not use raw providerName if (!newSettingsOfProvider[providerName]._didFillInProviderSettings) continue // if disabled, don't display model options for (const { modelName, isHidden } of newSettingsOfProvider[providerName].models) { if (isHidden) continue - newModelOptions.push({ name: `${modelName} (${providerTitle})`, selection: { providerName, modelName } }) + const showProviderInName = (visibleModelNameCounts.get(modelName) ?? 0) > 1 + const name = showProviderInName ? `${modelName} (${providerTitle})` : modelName + newModelOptions.push({ name, selection: { providerName, modelName } }) } } @@ -345,6 +371,10 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { readS.settingsOfProvider[providerName].models, ) } + readS.settingsOfProvider[providerName].models = dedupeProviderModels( + providerName, + readS.settingsOfProvider[providerName].models, + ) } } @@ -521,7 +551,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const oldModelNames = models.map(m => m.modelName) const normalizedNames = normalizeAutodetectedModelNamesForProvider(providerName, autodetectedModelNames) - let newModels = _modelsWithSwappedInNewModels({ existingModels: models, models: normalizedNames, type: 'autodetected' }) + let newModels = _modelsWithSwappedInNewModels({ providerName, existingModels: models, models: normalizedNames, type: 'autodetected' }) if (providerName === 'mlx' || providerName === 'appleFoundationModels') { newModels = consolidateSingleAutodetectedProviderModels(providerName, newModels) }