// biome-ignore-all assist/source/organizeImports: ANT-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' // @[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', } } // @[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[] { 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 ?? `[ANT-ONLY] ${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)] 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})`, }) } // Append additional model options fetched during bootstrap for (const opt of getGlobalConfig().additionalModelOptionsCache ?? []) { 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 === '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() || {} if (!settings.availableModels) { return options // No restrictions } return options.filter( opt => opt.value === null || (opt.value !== null && isModelAllowed(opt.value)), ) }