This pass rewrites a small set of ant-only diagnostic and UI labels to neutral internal wording while leaving command definitions, flags, and runtime logic untouched. It focuses on internal debug output, dead UI branches, and noninteractive headings rather than broader product text. Constraint: Label cleanup only; do not change command semantics or ant-only logic gates Rejected: Renaming ant-only command descriptions in main.tsx | broader UX surface better handled in a separate reviewed pass Confidence: high Scope-risk: narrow Reversibility: clean Directive: Remaining ANT-ONLY hits are mostly command descriptions and intentionally deferred user-facing strings Tested: bun run build Tested: bun run smoke Tested: bun run verify:privacy Tested: bun run test:provider Tested: bun run test:provider-recommendation Not-tested: Full repo typecheck (upstream baseline remains noisy) Co-authored-by: anandh8x <test@example.com>
652 lines
21 KiB
TypeScript
652 lines
21 KiB
TypeScript
// biome-ignore-all assist/source/organizeImports: internal-only import markers must not be reordered
|
|
import { getInitialMainLoopModel } from '../../bootstrap/state.js'
|
|
import {
|
|
isClaudeAISubscriber,
|
|
isMaxSubscriber,
|
|
isTeamPremiumSubscriber,
|
|
} from '../auth.js'
|
|
import { getModelStrings } from './modelStrings.js'
|
|
import {
|
|
COST_TIER_3_15,
|
|
COST_HAIKU_35,
|
|
COST_HAIKU_45,
|
|
formatModelPricing,
|
|
} from '../modelCost.js'
|
|
import { getSettings_DEPRECATED } from '../settings/settings.js'
|
|
import { checkOpus1mAccess, checkSonnet1mAccess } from './check1mAccess.js'
|
|
import { getAPIProvider } from './providers.js'
|
|
import { isModelAllowed } from './modelAllowlist.js'
|
|
import {
|
|
getCanonicalName,
|
|
getClaudeAiUserDefaultModelDescription,
|
|
getDefaultSonnetModel,
|
|
getDefaultOpusModel,
|
|
getDefaultHaikuModel,
|
|
getDefaultMainLoopModelSetting,
|
|
getMarketingNameForModel,
|
|
getUserSpecifiedModelSetting,
|
|
isOpus1mMergeEnabled,
|
|
getOpus46PricingSuffix,
|
|
renderDefaultModelSetting,
|
|
type ModelSetting,
|
|
} from './model.js'
|
|
import { has1mContext } from '../context.js'
|
|
import { getGlobalConfig } from '../config.js'
|
|
import { getActiveOpenAIModelOptionsCache } from '../providerProfiles.js'
|
|
import { getCachedOllamaModelOptions, isOllamaProvider } from './ollamaModels.js'
|
|
|
|
// @[MODEL LAUNCH]: Update all the available and default model option strings below.
|
|
|
|
export type ModelOption = {
|
|
value: ModelSetting
|
|
label: string
|
|
description: string
|
|
descriptionForModel?: string
|
|
}
|
|
|
|
export function getDefaultOptionForUser(fastMode = false): ModelOption {
|
|
if (process.env.USER_TYPE === 'ant') {
|
|
const currentModel = renderDefaultModelSetting(
|
|
getDefaultMainLoopModelSetting(),
|
|
)
|
|
return {
|
|
value: null,
|
|
label: 'Default (recommended)',
|
|
description: `Use the default model for Ants (currently ${currentModel})`,
|
|
descriptionForModel: `Default model (currently ${currentModel})`,
|
|
}
|
|
}
|
|
|
|
// Subscribers
|
|
if (isClaudeAISubscriber()) {
|
|
return {
|
|
value: null,
|
|
label: 'Default (recommended)',
|
|
description: getClaudeAiUserDefaultModelDescription(fastMode),
|
|
}
|
|
}
|
|
|
|
// PAYG
|
|
const is3P = getAPIProvider() !== 'firstParty'
|
|
return {
|
|
value: null,
|
|
label: 'Default (recommended)',
|
|
description: `Use the default model (currently ${renderDefaultModelSetting(getDefaultMainLoopModelSetting())})${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`,
|
|
}
|
|
}
|
|
|
|
function getCustomSonnetOption(): ModelOption | undefined {
|
|
const is3P = getAPIProvider() !== 'firstParty'
|
|
const customSonnetModel = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
|
|
// When a 3P user has a custom sonnet model string, show it directly
|
|
if (is3P && customSonnetModel) {
|
|
const is1m = has1mContext(customSonnetModel)
|
|
return {
|
|
value: 'sonnet',
|
|
label:
|
|
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME ?? customSonnetModel,
|
|
description:
|
|
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION ??
|
|
`Custom Sonnet model${is1m ? ' (1M context)' : ''}`,
|
|
descriptionForModel: `${process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION ?? `Custom Sonnet model${is1m ? ' with 1M context' : ''}`} (${customSonnetModel})`,
|
|
}
|
|
}
|
|
}
|
|
|
|
// @[MODEL LAUNCH]: Update or add model option functions (getSonnetXXOption, getOpusXXOption, etc.)
|
|
// with the new model's label and description. These appear in the /model picker.
|
|
function getSonnet46Option(): ModelOption {
|
|
const is3P = getAPIProvider() !== 'firstParty'
|
|
return {
|
|
value: is3P ? getModelStrings().sonnet46 : 'sonnet',
|
|
label: 'Sonnet',
|
|
description: `Sonnet 4.6 · Best for everyday tasks${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`,
|
|
descriptionForModel:
|
|
'Sonnet 4.6 - best for everyday tasks. Generally recommended for most coding tasks',
|
|
}
|
|
}
|
|
|
|
function getCustomOpusOption(): ModelOption | undefined {
|
|
const is3P = getAPIProvider() !== 'firstParty'
|
|
const customOpusModel = process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
|
// When a 3P user has a custom opus model string, show it directly
|
|
if (is3P && customOpusModel) {
|
|
const is1m = has1mContext(customOpusModel)
|
|
return {
|
|
value: 'opus',
|
|
label: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME ?? customOpusModel,
|
|
description:
|
|
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION ??
|
|
`Custom Opus model${is1m ? ' (1M context)' : ''}`,
|
|
descriptionForModel: `${process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION ?? `Custom Opus model${is1m ? ' with 1M context' : ''}`} (${customOpusModel})`,
|
|
}
|
|
}
|
|
}
|
|
|
|
function getOpus41Option(): ModelOption {
|
|
return {
|
|
value: 'opus',
|
|
label: 'Opus 4.1',
|
|
description: `Opus 4.1 · Legacy`,
|
|
descriptionForModel: 'Opus 4.1 - legacy version',
|
|
}
|
|
}
|
|
|
|
function getOpus46Option(fastMode = false): ModelOption {
|
|
const is3P = getAPIProvider() !== 'firstParty'
|
|
return {
|
|
value: is3P ? getModelStrings().opus46 : 'opus',
|
|
label: 'Opus',
|
|
description: `Opus 4.6 · Most capable for complex work${getOpus46PricingSuffix(fastMode)}`,
|
|
descriptionForModel: 'Opus 4.6 - most capable for complex work',
|
|
}
|
|
}
|
|
|
|
export function getSonnet46_1MOption(): ModelOption {
|
|
const is3P = getAPIProvider() !== 'firstParty'
|
|
return {
|
|
value: is3P ? getModelStrings().sonnet46 + '[1m]' : 'sonnet[1m]',
|
|
label: 'Sonnet (1M context)',
|
|
description: `Sonnet 4.6 for long sessions${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`,
|
|
descriptionForModel:
|
|
'Sonnet 4.6 with 1M context window - for long sessions with large codebases',
|
|
}
|
|
}
|
|
|
|
export function getOpus46_1MOption(fastMode = false): ModelOption {
|
|
const is3P = getAPIProvider() !== 'firstParty'
|
|
return {
|
|
value: is3P ? getModelStrings().opus46 + '[1m]' : 'opus[1m]',
|
|
label: 'Opus (1M context)',
|
|
description: `Opus 4.6 for long sessions${getOpus46PricingSuffix(fastMode)}`,
|
|
descriptionForModel:
|
|
'Opus 4.6 with 1M context window - for long sessions with large codebases',
|
|
}
|
|
}
|
|
|
|
function getCustomHaikuOption(): ModelOption | undefined {
|
|
const is3P = getAPIProvider() !== 'firstParty'
|
|
const customHaikuModel = process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
|
// When a 3P user has a custom haiku model string, show it directly
|
|
if (is3P && customHaikuModel) {
|
|
return {
|
|
value: 'haiku',
|
|
label: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME ?? customHaikuModel,
|
|
description:
|
|
process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION ??
|
|
'Custom Haiku model',
|
|
descriptionForModel: `${process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION ?? 'Custom Haiku model'} (${customHaikuModel})`,
|
|
}
|
|
}
|
|
}
|
|
|
|
function getHaiku45Option(): ModelOption {
|
|
const is3P = getAPIProvider() !== 'firstParty'
|
|
return {
|
|
value: 'haiku',
|
|
label: 'Haiku',
|
|
description: `Haiku 4.5 · Fastest for quick answers${is3P ? '' : ` · ${formatModelPricing(COST_HAIKU_45)}`}`,
|
|
descriptionForModel:
|
|
'Haiku 4.5 - fastest for quick answers. Lower cost but less capable than Sonnet 4.6.',
|
|
}
|
|
}
|
|
|
|
function getHaiku35Option(): ModelOption {
|
|
const is3P = getAPIProvider() !== 'firstParty'
|
|
return {
|
|
value: 'haiku',
|
|
label: 'Haiku',
|
|
description: `Haiku 3.5 for simple tasks${is3P ? '' : ` · ${formatModelPricing(COST_HAIKU_35)}`}`,
|
|
descriptionForModel:
|
|
'Haiku 3.5 - faster and lower cost, but less capable than Sonnet. Use for simple tasks.',
|
|
}
|
|
}
|
|
|
|
function getHaikuOption(): ModelOption {
|
|
// Return correct Haiku option based on provider
|
|
const haikuModel = getDefaultHaikuModel()
|
|
return haikuModel === getModelStrings().haiku45
|
|
? getHaiku45Option()
|
|
: getHaiku35Option()
|
|
}
|
|
|
|
function getMaxOpusOption(fastMode = false): ModelOption {
|
|
return {
|
|
value: 'opus',
|
|
label: 'Opus',
|
|
description: `Opus 4.6 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`,
|
|
}
|
|
}
|
|
|
|
export function getMaxSonnet46_1MOption(): ModelOption {
|
|
const is3P = getAPIProvider() !== 'firstParty'
|
|
const billingInfo = isClaudeAISubscriber() ? ' · Billed as extra usage' : ''
|
|
return {
|
|
value: 'sonnet[1m]',
|
|
label: 'Sonnet (1M context)',
|
|
description: `Sonnet 4.6 with 1M context${billingInfo}${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`,
|
|
}
|
|
}
|
|
|
|
export function getMaxOpus46_1MOption(fastMode = false): ModelOption {
|
|
const billingInfo = isClaudeAISubscriber() ? ' · Billed as extra usage' : ''
|
|
return {
|
|
value: 'opus[1m]',
|
|
label: 'Opus (1M context)',
|
|
description: `Opus 4.6 with 1M context${billingInfo}${getOpus46PricingSuffix(fastMode)}`,
|
|
}
|
|
}
|
|
|
|
function getMergedOpus1MOption(fastMode = false): ModelOption {
|
|
const is3P = getAPIProvider() !== 'firstParty'
|
|
return {
|
|
value: is3P ? getModelStrings().opus46 + '[1m]' : 'opus[1m]',
|
|
label: 'Opus (1M context)',
|
|
description: `Opus 4.6 with 1M context · Most capable for complex work${!is3P && fastMode ? getOpus46PricingSuffix(fastMode) : ''}`,
|
|
descriptionForModel:
|
|
'Opus 4.6 with 1M context - most capable for complex work',
|
|
}
|
|
}
|
|
|
|
const MaxSonnet46Option: ModelOption = {
|
|
value: 'sonnet',
|
|
label: 'Sonnet',
|
|
description: 'Sonnet 4.6 · Best for everyday tasks',
|
|
}
|
|
|
|
const MaxHaiku45Option: ModelOption = {
|
|
value: 'haiku',
|
|
label: 'Haiku',
|
|
description: 'Haiku 4.5 · Fastest for quick answers',
|
|
}
|
|
|
|
function getOpusPlanOption(): ModelOption {
|
|
return {
|
|
value: 'opusplan',
|
|
label: 'Opus Plan Mode',
|
|
description: 'Use Opus 4.6 in plan mode, Sonnet 4.6 otherwise',
|
|
}
|
|
}
|
|
|
|
function getCodexPlanOption(): ModelOption {
|
|
return {
|
|
value: 'gpt-5.4',
|
|
label: 'gpt-5.4',
|
|
description: 'GPT-5.4 on the Codex backend with high reasoning',
|
|
}
|
|
}
|
|
|
|
function getCodexSparkOption(): ModelOption {
|
|
return {
|
|
value: 'gpt-5.3-codex-spark',
|
|
label: 'gpt-5.3-codex-spark',
|
|
description: 'GPT-5.3 Codex Spark on the Codex backend for fast tool loops',
|
|
}
|
|
}
|
|
|
|
function getCodexModelOptions(): ModelOption[] {
|
|
return [
|
|
{
|
|
value: 'gpt-5.4',
|
|
label: 'gpt-5.4',
|
|
description: 'GPT-5.4 with high reasoning',
|
|
},
|
|
{
|
|
value: 'gpt-5.3-codex',
|
|
label: 'gpt-5.3-codex',
|
|
description: 'GPT-5.3 Codex with high reasoning',
|
|
},
|
|
{
|
|
value: 'gpt-5.3-codex-spark',
|
|
label: 'gpt-5.3-codex-spark',
|
|
description: 'GPT-5.3 Codex Spark for fast tool loops',
|
|
},
|
|
{
|
|
value: 'codexspark',
|
|
label: 'codexspark',
|
|
description: 'GPT-5.3 Codex Spark alias for fast tool loops',
|
|
},
|
|
{
|
|
value: 'gpt-5.2-codex',
|
|
label: 'gpt-5.2-codex',
|
|
description: 'GPT-5.2 Codex with high reasoning',
|
|
},
|
|
{
|
|
value: 'gpt-5.1-codex-max',
|
|
label: 'gpt-5.1-codex-max',
|
|
description: 'GPT-5.1 Codex Max for deep reasoning',
|
|
},
|
|
{
|
|
value: 'gpt-5.1-codex-mini',
|
|
label: 'gpt-5.1-codex-mini',
|
|
description: 'GPT-5.1 Codex Mini - faster, cheaper',
|
|
},
|
|
{
|
|
value: 'gpt-5.4-mini',
|
|
label: 'gpt-5.4-mini',
|
|
description: 'GPT-5.4 Mini - faster, cheaper',
|
|
},
|
|
]
|
|
}
|
|
|
|
// @[MODEL LAUNCH]: Update the model picker lists below to include/reorder options for the new model.
|
|
// Each user tier (ant, Max/Team Premium, Pro/Team Standard/Enterprise, PAYG 1P, PAYG 3P) has its own list.
|
|
function getModelOptionsBase(fastMode = false): ModelOption[] {
|
|
// When using Ollama, show models from the Ollama server instead of Claude models
|
|
if (getAPIProvider() === 'openai' && isOllamaProvider()) {
|
|
const defaultOption = getDefaultOptionForUser(fastMode)
|
|
const ollamaModels = getCachedOllamaModelOptions()
|
|
if (ollamaModels.length > 0) {
|
|
return [defaultOption, ...ollamaModels]
|
|
}
|
|
// Fallback: if models not yet fetched, show current model instead of Claude models
|
|
const currentModel = getUserSpecifiedModelSetting() ?? getInitialMainLoopModel()
|
|
if (currentModel != null) {
|
|
return [
|
|
defaultOption,
|
|
{
|
|
value: currentModel,
|
|
label: currentModel,
|
|
description: 'Currently configured Ollama model',
|
|
},
|
|
]
|
|
}
|
|
return [defaultOption]
|
|
}
|
|
|
|
if (process.env.USER_TYPE === 'ant') {
|
|
// Build options from antModels config
|
|
const antModelOptions: ModelOption[] = getAntModels().map(m => ({
|
|
value: m.alias,
|
|
label: m.label,
|
|
description: m.description ?? `[internal] ${m.label} (${m.model})`,
|
|
}))
|
|
|
|
return [
|
|
getDefaultOptionForUser(),
|
|
...antModelOptions,
|
|
getMergedOpus1MOption(fastMode),
|
|
getSonnet46Option(),
|
|
getSonnet46_1MOption(),
|
|
getHaiku45Option(),
|
|
]
|
|
}
|
|
|
|
if (isClaudeAISubscriber()) {
|
|
if (isMaxSubscriber() || isTeamPremiumSubscriber()) {
|
|
// Max and Team Premium users: Opus is default, show Sonnet as alternative
|
|
const premiumOptions = [getDefaultOptionForUser(fastMode)]
|
|
if (!isOpus1mMergeEnabled() && checkOpus1mAccess()) {
|
|
premiumOptions.push(getMaxOpus46_1MOption(fastMode))
|
|
}
|
|
|
|
premiumOptions.push(MaxSonnet46Option)
|
|
if (checkSonnet1mAccess()) {
|
|
premiumOptions.push(getMaxSonnet46_1MOption())
|
|
}
|
|
|
|
premiumOptions.push(MaxHaiku45Option)
|
|
return premiumOptions
|
|
}
|
|
|
|
// Pro/Team Standard/Enterprise users: Sonnet is default, show Opus as alternative
|
|
const standardOptions = [getDefaultOptionForUser(fastMode)]
|
|
if (checkSonnet1mAccess()) {
|
|
standardOptions.push(getMaxSonnet46_1MOption())
|
|
}
|
|
|
|
if (isOpus1mMergeEnabled()) {
|
|
standardOptions.push(getMergedOpus1MOption(fastMode))
|
|
} else {
|
|
standardOptions.push(getMaxOpusOption(fastMode))
|
|
if (checkOpus1mAccess()) {
|
|
standardOptions.push(getMaxOpus46_1MOption(fastMode))
|
|
}
|
|
}
|
|
|
|
standardOptions.push(MaxHaiku45Option)
|
|
return standardOptions
|
|
}
|
|
|
|
// PAYG 1P API: Default (Sonnet) + Sonnet 1M + Opus 4.6 + Opus 1M + Haiku
|
|
if (getAPIProvider() === 'firstParty') {
|
|
const payg1POptions = [getDefaultOptionForUser(fastMode)]
|
|
if (checkSonnet1mAccess()) {
|
|
payg1POptions.push(getSonnet46_1MOption())
|
|
}
|
|
if (isOpus1mMergeEnabled()) {
|
|
payg1POptions.push(getMergedOpus1MOption(fastMode))
|
|
} else {
|
|
payg1POptions.push(getOpus46Option(fastMode))
|
|
if (checkOpus1mAccess()) {
|
|
payg1POptions.push(getOpus46_1MOption(fastMode))
|
|
}
|
|
}
|
|
payg1POptions.push(getHaiku45Option())
|
|
return payg1POptions
|
|
}
|
|
|
|
// PAYG 3P: Default (Sonnet 4.5) + Sonnet (3P custom) or Sonnet 4.6/1M + Opus (3P custom) or Opus 4.1/Opus 4.6/Opus1M + Haiku + Opus 4.1
|
|
const payg3pOptions = [getDefaultOptionForUser(fastMode)]
|
|
|
|
// Add Codex models for openai and codex providers
|
|
if (getAPIProvider() === 'openai' || getAPIProvider() === 'codex') {
|
|
payg3pOptions.push(...getCodexModelOptions())
|
|
}
|
|
|
|
const customSonnet = getCustomSonnetOption()
|
|
if (customSonnet !== undefined) {
|
|
payg3pOptions.push(customSonnet)
|
|
} else {
|
|
// Add Sonnet 4.6 since Sonnet 4.5 is the default
|
|
payg3pOptions.push(getSonnet46Option())
|
|
if (checkSonnet1mAccess()) {
|
|
payg3pOptions.push(getSonnet46_1MOption())
|
|
}
|
|
}
|
|
|
|
const customOpus = getCustomOpusOption()
|
|
if (customOpus !== undefined) {
|
|
payg3pOptions.push(customOpus)
|
|
} else {
|
|
// Add Opus 4.1, Opus 4.6 and Opus 4.6 1M
|
|
payg3pOptions.push(getOpus41Option()) // This is the default opus
|
|
payg3pOptions.push(getOpus46Option(fastMode))
|
|
if (checkOpus1mAccess()) {
|
|
payg3pOptions.push(getOpus46_1MOption(fastMode))
|
|
}
|
|
}
|
|
const customHaiku = getCustomHaikuOption()
|
|
if (customHaiku !== undefined) {
|
|
payg3pOptions.push(customHaiku)
|
|
} else {
|
|
payg3pOptions.push(getHaikuOption())
|
|
}
|
|
return payg3pOptions
|
|
}
|
|
|
|
// @[MODEL LAUNCH]: Add the new model ID to the appropriate family pattern below
|
|
// so the "newer version available" hint works correctly.
|
|
/**
|
|
* Map a full model name to its family alias and the marketing name of the
|
|
* version the alias currently resolves to. Used to detect when a user has
|
|
* a specific older version pinned and a newer one is available.
|
|
*/
|
|
function getModelFamilyInfo(
|
|
model: string,
|
|
): { alias: string; currentVersionName: string } | null {
|
|
const canonical = getCanonicalName(model)
|
|
|
|
// Sonnet family
|
|
if (
|
|
canonical.includes('claude-sonnet-4-6') ||
|
|
canonical.includes('claude-sonnet-4-5') ||
|
|
canonical.includes('claude-sonnet-4-') ||
|
|
canonical.includes('claude-3-7-sonnet') ||
|
|
canonical.includes('claude-3-5-sonnet')
|
|
) {
|
|
const currentName = getMarketingNameForModel(getDefaultSonnetModel())
|
|
if (currentName) {
|
|
return { alias: 'Sonnet', currentVersionName: currentName }
|
|
}
|
|
}
|
|
|
|
// Opus family
|
|
if (canonical.includes('claude-opus-4')) {
|
|
const currentName = getMarketingNameForModel(getDefaultOpusModel())
|
|
if (currentName) {
|
|
return { alias: 'Opus', currentVersionName: currentName }
|
|
}
|
|
}
|
|
|
|
// Haiku family
|
|
if (
|
|
canonical.includes('claude-haiku') ||
|
|
canonical.includes('claude-3-5-haiku')
|
|
) {
|
|
const currentName = getMarketingNameForModel(getDefaultHaikuModel())
|
|
if (currentName) {
|
|
return { alias: 'Haiku', currentVersionName: currentName }
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Returns a ModelOption for a known Anthropic model with a human-readable
|
|
* label, and an upgrade hint if a newer version is available via the alias.
|
|
* Returns null if the model is not recognized.
|
|
*/
|
|
function getKnownModelOption(model: string): ModelOption | null {
|
|
const marketingName = getMarketingNameForModel(model)
|
|
if (!marketingName) return null
|
|
|
|
const familyInfo = getModelFamilyInfo(model)
|
|
if (!familyInfo) {
|
|
return {
|
|
value: model,
|
|
label: marketingName,
|
|
description: model,
|
|
}
|
|
}
|
|
|
|
// Check if the alias currently resolves to a different (newer) version
|
|
if (marketingName !== familyInfo.currentVersionName) {
|
|
return {
|
|
value: model,
|
|
label: marketingName,
|
|
description: `Newer version available · select ${familyInfo.alias} for ${familyInfo.currentVersionName}`,
|
|
}
|
|
}
|
|
|
|
// Same version as the alias — just show the friendly name
|
|
return {
|
|
value: model,
|
|
label: marketingName,
|
|
description: model,
|
|
}
|
|
}
|
|
|
|
export function getModelOptions(fastMode = false): ModelOption[] {
|
|
const options = getModelOptionsBase(fastMode)
|
|
|
|
// Add the custom model from the ANTHROPIC_CUSTOM_MODEL_OPTION env var
|
|
const envCustomModel = process.env.ANTHROPIC_CUSTOM_MODEL_OPTION
|
|
if (
|
|
envCustomModel &&
|
|
!options.some(existing => existing.value === envCustomModel)
|
|
) {
|
|
options.push({
|
|
value: envCustomModel,
|
|
label: process.env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME ?? envCustomModel,
|
|
description:
|
|
process.env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION ??
|
|
`Custom model (${envCustomModel})`,
|
|
})
|
|
}
|
|
|
|
const additionalOptions =
|
|
getAPIProvider() === 'openai'
|
|
? getActiveOpenAIModelOptionsCache()
|
|
: getGlobalConfig().additionalModelOptionsCache ?? []
|
|
|
|
// Append additional model options fetched during bootstrap/endpoints.
|
|
for (const opt of additionalOptions) {
|
|
if (!options.some(existing => existing.value === opt.value)) {
|
|
options.push(opt)
|
|
}
|
|
}
|
|
|
|
// Add custom model from either the current model value or the initial one
|
|
// if it is not already in the options.
|
|
let customModel: ModelSetting = null
|
|
const currentMainLoopModel = getUserSpecifiedModelSetting()
|
|
const initialMainLoopModel = getInitialMainLoopModel()
|
|
if (currentMainLoopModel !== undefined && currentMainLoopModel !== null) {
|
|
customModel = currentMainLoopModel
|
|
} else if (initialMainLoopModel !== null) {
|
|
customModel = initialMainLoopModel
|
|
}
|
|
if (customModel === null || options.some(opt => opt.value === customModel)) {
|
|
return filterModelOptionsByAllowlist(options)
|
|
} else if (customModel === 'opusplan') {
|
|
return filterModelOptionsByAllowlist([...options, getOpusPlanOption()])
|
|
} else if (customModel === 'gpt-5.4') {
|
|
return filterModelOptionsByAllowlist([...options, getCodexPlanOption()])
|
|
} else if (customModel === 'gpt-5.3-codex-spark') {
|
|
return filterModelOptionsByAllowlist([...options, getCodexSparkOption()])
|
|
} else if (customModel === 'opus' && getAPIProvider() === 'firstParty') {
|
|
return filterModelOptionsByAllowlist([
|
|
...options,
|
|
getMaxOpusOption(fastMode),
|
|
])
|
|
} else if (customModel === 'opus[1m]' && getAPIProvider() === 'firstParty') {
|
|
return filterModelOptionsByAllowlist([
|
|
...options,
|
|
getMergedOpus1MOption(fastMode),
|
|
])
|
|
} else {
|
|
// Try to show a human-readable label for known Anthropic models, with an
|
|
// upgrade hint if the alias now resolves to a newer version.
|
|
const knownOption = getKnownModelOption(customModel)
|
|
if (knownOption) {
|
|
options.push(knownOption)
|
|
} else {
|
|
options.push({
|
|
value: customModel,
|
|
label: customModel,
|
|
description: 'Custom model',
|
|
})
|
|
}
|
|
return filterModelOptionsByAllowlist(options)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Filter model options by the availableModels allowlist.
|
|
* Always preserves the "Default" option (value: null).
|
|
*/
|
|
function filterModelOptionsByAllowlist(options: ModelOption[]): ModelOption[] {
|
|
const settings = getSettings_DEPRECATED() || {}
|
|
const filtered = !settings.availableModels
|
|
? options // No restrictions
|
|
: options.filter(
|
|
opt =>
|
|
opt.value === null || (opt.value !== null && isModelAllowed(opt.value)),
|
|
)
|
|
|
|
// Select state uses option values as identity keys. If two entries share the
|
|
// same value (e.g. provider-specific aliases collapsing to one model ID),
|
|
// navigation/focus can become inconsistent and appear as duplicate rendering.
|
|
const seen = new Set<string>()
|
|
return filtered.filter(opt => {
|
|
const key = String(opt.value)
|
|
if (seen.has(key)) {
|
|
return false
|
|
}
|
|
seen.add(key)
|
|
return true
|
|
})
|
|
}
|