Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
21 kB · 618 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered/** * Ensure that any model codenames introduced here are also added to * scripts/excluded-strings.txt to avoid leaking them. Wrap any codename string * literals with process.env.USER_TYPE === 'ant' for Bun to remove the codenames * during dead code elimination */import { getMainLoopModelOverride } from '../../bootstrap/state.js'import { getSubscriptionType, isClaudeAISubscriber, isMaxSubscriber, isProSubscriber, isTeamPremiumSubscriber,} from '../auth.js'import { has1mContext, is1mContextDisabled, modelSupports1M,} from '../context.js'import { isEnvTruthy } from '../envUtils.js'import { getModelStrings, resolveOverriddenModel } from './modelStrings.js'import { formatModelPricing, getOpus46CostTier } from '../modelCost.js'import { getSettings_DEPRECATED } from '../settings/settings.js'import type { PermissionMode } from '../permissions/PermissionMode.js'import { getAPIProvider } from './providers.js'import { LIGHTNING_BOLT } from '../../constants/figures.js'import { isModelAllowed } from './modelAllowlist.js'import { type ModelAlias, isModelAlias } from './aliases.js'import { capitalize } from '../stringUtils.js'
export type ModelShortName = stringexport type ModelName = stringexport type ModelSetting = ModelName | ModelAlias | null
export function getSmallFastModel(): ModelName { return process.env.ANTHROPIC_SMALL_FAST_MODEL || getDefaultHaikuModel()}
export function isNonCustomOpusModel(model: ModelName): boolean { return ( model === getModelStrings().opus40 || model === getModelStrings().opus41 || model === getModelStrings().opus45 || model === getModelStrings().opus46 )}
/** * Helper to get the model from /model (including via /config), the --model flag, environment variable, * or the saved settings. The returned value can be a model alias if that's what the user specified. * Undefined if the user didn't configure anything, in which case we fall back to * the default (null). * * Priority order within this function: * 1. Model override during session (from /model command) - highest priority * 2. Model override at startup (from --model flag) * 3. ANTHROPIC_MODEL environment variable * 4. Settings (from user's saved settings) */export function getUserSpecifiedModelSetting(): ModelSetting | undefined { let specifiedModel: ModelSetting | undefined
const modelOverride = getMainLoopModelOverride() if (modelOverride !== undefined) { specifiedModel = modelOverride } else { const settings = getSettings_DEPRECATED() || {} specifiedModel = process.env.ANTHROPIC_MODEL || settings.model || undefined }
// Ignore the user-specified model if it's not in the availableModels allowlist. if (specifiedModel && !isModelAllowed(specifiedModel)) { return undefined }
return specifiedModel}
/** * Get the main loop model to use for the current session. * * Model Selection Priority Order: * 1. Model override during session (from /model command) - highest priority * 2. Model override at startup (from --model flag) * 3. ANTHROPIC_MODEL environment variable * 4. Settings (from user's saved settings) * 5. Built-in default * * @returns The resolved model name to use */export function getMainLoopModel(): ModelName { const model = getUserSpecifiedModelSetting() if (model !== undefined && model !== null) { return parseUserSpecifiedModel(model) } return getDefaultMainLoopModel()}
export function getBestModel(): ModelName { return getDefaultOpusModel()}
// @[MODEL LAUNCH]: Update the default Opus model (3P providers may lag so keep defaults unchanged).export function getDefaultOpusModel(): ModelName { if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) { return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL } // 3P providers (Bedrock, Vertex, Foundry) — kept as a separate branch // even when values match, since 3P availability lags firstParty and // these will diverge again at the next model launch. if (getAPIProvider() !== 'firstParty') { return getModelStrings().opus46 } return getModelStrings().opus46}
// @[MODEL LAUNCH]: Update the default Sonnet model (3P providers may lag so keep defaults unchanged).export function getDefaultSonnetModel(): ModelName { if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) { return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL } // Default to Sonnet 4.5 for 3P since they may not have 4.6 yet if (getAPIProvider() !== 'firstParty') { return getModelStrings().sonnet45 } return getModelStrings().sonnet46}
// @[MODEL LAUNCH]: Update the default Haiku model (3P providers may lag so keep defaults unchanged).export function getDefaultHaikuModel(): ModelName { if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) { return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL }
// Haiku 4.5 is available on all platforms (first-party, Foundry, Bedrock, Vertex) return getModelStrings().haiku45}
/** * Get the model to use for runtime, depending on the runtime context. * @param params Subset of the runtime context to determine the model to use. * @returns The model to use */export function getRuntimeMainLoopModel(params: { permissionMode: PermissionMode mainLoopModel: string exceeds200kTokens?: boolean}): ModelName { const { permissionMode, mainLoopModel, exceeds200kTokens = false } = params
// opusplan uses Opus in plan mode without [1m] suffix. if ( getUserSpecifiedModelSetting() === 'opusplan' && permissionMode === 'plan' && !exceeds200kTokens ) { return getDefaultOpusModel() }
// sonnetplan by default if (getUserSpecifiedModelSetting() === 'haiku' && permissionMode === 'plan') { return getDefaultSonnetModel() }
return mainLoopModel}
/** * Get the default main loop model setting. * * This handles the built-in default: * - Opus for Max and Team Premium users * - Sonnet 4.6 for all other users (including Team Standard, Pro, Enterprise) * * @returns The default model setting to use */export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias { // Ants default to defaultModel from flag config, or Opus 1M if not configured if (process.env.USER_TYPE === 'ant') { return ( getAntModelOverrideConfig()?.defaultModel ?? getDefaultOpusModel() + '[1m]' ) }
// Max users get Opus as default if (isMaxSubscriber()) { return getDefaultOpusModel() + (isOpus1mMergeEnabled() ? '[1m]' : '') }
// Team Premium gets Opus (same as Max) if (isTeamPremiumSubscriber()) { return getDefaultOpusModel() + (isOpus1mMergeEnabled() ? '[1m]' : '') }
// PAYG (1P and 3P), Enterprise, Team Standard, and Pro get Sonnet as default // Note that PAYG (3P) may default to an older Sonnet model return getDefaultSonnetModel()}
/** * Synchronous operation to get the default main loop model to use * (bypassing any user-specified values). */export function getDefaultMainLoopModel(): ModelName { return parseUserSpecifiedModel(getDefaultMainLoopModelSetting())}
// @[MODEL LAUNCH]: Add a canonical name mapping for the new model below./** * Pure string-match that strips date/provider suffixes from a first-party model * name. Input must already be a 1P-format ID (e.g. 'claude-3-7-sonnet-20250219', * 'us.anthropic.claude-opus-4-6-v1:0'). Does not touch settings, so safe at * module top-level (see MODEL_COSTS in modelCost.ts). */export function firstPartyNameToCanonical(name: ModelName): ModelShortName { name = name.toLowerCase() // Special cases for Claude 4+ models to differentiate versions // Order matters: check more specific versions first (4-5 before 4) if (name.includes('claude-opus-4-6')) { return 'claude-opus-4-6' } if (name.includes('claude-opus-4-5')) { return 'claude-opus-4-5' } if (name.includes('claude-opus-4-1')) { return 'claude-opus-4-1' } if (name.includes('claude-opus-4')) { return 'claude-opus-4' } if (name.includes('claude-sonnet-4-6')) { return 'claude-sonnet-4-6' } if (name.includes('claude-sonnet-4-5')) { return 'claude-sonnet-4-5' } if (name.includes('claude-sonnet-4')) { return 'claude-sonnet-4' } if (name.includes('claude-haiku-4-5')) { return 'claude-haiku-4-5' } // Claude 3.x models use a different naming scheme (claude-3-{family}) if (name.includes('claude-3-7-sonnet')) { return 'claude-3-7-sonnet' } if (name.includes('claude-3-5-sonnet')) { return 'claude-3-5-sonnet' } if (name.includes('claude-3-5-haiku')) { return 'claude-3-5-haiku' } if (name.includes('claude-3-opus')) { return 'claude-3-opus' } if (name.includes('claude-3-sonnet')) { return 'claude-3-sonnet' } if (name.includes('claude-3-haiku')) { return 'claude-3-haiku' } const match = name.match(/(claude-(\d+-\d+-)?\w+)/) if (match && match[1]) { return match[1] } // Fall back to the original name if no pattern matches return name}
/** * Maps a full model string to a shorter canonical version that's unified across 1P and 3P providers. * For example, 'claude-3-5-haiku-20241022' and 'us.anthropic.claude-3-5-haiku-20241022-v1:0' * would both be mapped to 'claude-3-5-haiku'. * @param fullModelName The full model name (e.g., 'claude-3-5-haiku-20241022') * @returns The short name (e.g., 'claude-3-5-haiku') if found, or the original name if no mapping exists */export function getCanonicalName(fullModelName: ModelName): ModelShortName { // Resolve overridden model IDs (e.g. Bedrock ARNs) back to canonical names. // resolved is always a 1P-format ID, so firstPartyNameToCanonical can handle it. return firstPartyNameToCanonical(resolveOverriddenModel(fullModelName))}
// @[MODEL LAUNCH]: Update the default model description strings shown to users.export function getClaudeAiUserDefaultModelDescription( fastMode = false,): string { if (isMaxSubscriber() || isTeamPremiumSubscriber()) { if (isOpus1mMergeEnabled()) { return `Opus 4.6 with 1M context · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}` } return `Opus 4.6 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}` } return 'Sonnet 4.6 · Best for everyday tasks'}
export function renderDefaultModelSetting( setting: ModelName | ModelAlias,): string { if (setting === 'opusplan') { return 'Opus 4.6 in plan mode, else Sonnet 4.6' } return renderModelName(parseUserSpecifiedModel(setting))}
export function getOpus46PricingSuffix(fastMode: boolean): string { if (getAPIProvider() !== 'firstParty') return '' const pricing = formatModelPricing(getOpus46CostTier(fastMode)) const fastModeIndicator = fastMode ? ` (${LIGHTNING_BOLT})` : '' return ` ·${fastModeIndicator} ${pricing}`}
export function isOpus1mMergeEnabled(): boolean { if ( is1mContextDisabled() || isProSubscriber() || getAPIProvider() !== 'firstParty' ) { return false } // Fail closed when a subscriber's subscription type is unknown. The VS Code // config-loading subprocess can have OAuth tokens with valid scopes but no // subscriptionType field (stale or partial refresh). Without this guard, // isProSubscriber() returns false for such users and the merge leaks // opus[1m] into the model dropdown — the API then rejects it with a // misleading "rate limit reached" error. if (isClaudeAISubscriber() && getSubscriptionType() === null) { return false } return true}
export function renderModelSetting(setting: ModelName | ModelAlias): string { if (setting === 'opusplan') { return 'Opus Plan' } if (isModelAlias(setting)) { return capitalize(setting) } return renderModelName(setting)}
// @[MODEL LAUNCH]: Add display name cases for the new model (base + [1m] variant if applicable)./** * Returns a human-readable display name for known public models, or null * if the model is not recognized as a public model. */export function getPublicModelDisplayName(model: ModelName): string | null { switch (model) { case getModelStrings().opus46: return 'Opus 4.6' case getModelStrings().opus46 + '[1m]': return 'Opus 4.6 (1M context)' case getModelStrings().opus45: return 'Opus 4.5' case getModelStrings().opus41: return 'Opus 4.1' case getModelStrings().opus40: return 'Opus 4' case getModelStrings().sonnet46 + '[1m]': return 'Sonnet 4.6 (1M context)' case getModelStrings().sonnet46: return 'Sonnet 4.6' case getModelStrings().sonnet45 + '[1m]': return 'Sonnet 4.5 (1M context)' case getModelStrings().sonnet45: return 'Sonnet 4.5' case getModelStrings().sonnet40: return 'Sonnet 4' case getModelStrings().sonnet40 + '[1m]': return 'Sonnet 4 (1M context)' case getModelStrings().sonnet37: return 'Sonnet 3.7' case getModelStrings().sonnet35: return 'Sonnet 3.5' case getModelStrings().haiku45: return 'Haiku 4.5' case getModelStrings().haiku35: return 'Haiku 3.5' default: return null }}
function maskModelCodename(baseName: string): string { // Mask only the first dash-separated segment (the codename), preserve the rest // e.g. capybara-v2-fast → cap*****-v2-fast const [codename = '', ...rest] = baseName.split('-') const masked = codename.slice(0, 3) + '*'.repeat(Math.max(0, codename.length - 3)) return [masked, ...rest].join('-')}
export function renderModelName(model: ModelName): string { const publicName = getPublicModelDisplayName(model) if (publicName) { return publicName } if (process.env.USER_TYPE === 'ant') { const resolved = parseUserSpecifiedModel(model) const antModel = resolveAntModel(model) if (antModel) { const baseName = antModel.model.replace(/\[1m\]$/i, '') const masked = maskModelCodename(baseName) const suffix = has1mContext(resolved) ? '[1m]' : '' return masked + suffix } if (resolved !== model) { return `${model} (${resolved})` } return resolved } return model}
/** * Returns a safe author name for public display (e.g., in git commit trailers). * Returns "Claude {ModelName}" for publicly known models, or "Claude ({model})" * for unknown/internal models so the exact model name is preserved. * * @param model The full model name * @returns "Claude {ModelName}" for public models, or "Claude ({model})" for non-public models */export function getPublicModelName(model: ModelName): string { const publicName = getPublicModelDisplayName(model) if (publicName) { return `Claude ${publicName}` } return `Claude (${model})`}
/** * Returns a full model name for use in this session, possibly after resolving * a model alias. * * This function intentionally does not support version numbers to align with * the model switcher. * * Supports [1m] suffix on any model alias (e.g., haiku[1m], sonnet[1m]) to enable * 1M context window without requiring each variant to be in MODEL_ALIASES. * * @param modelInput The model alias or name provided by the user. */export function parseUserSpecifiedModel( modelInput: ModelName | ModelAlias,): ModelName { const modelInputTrimmed = modelInput.trim() const normalizedModel = modelInputTrimmed.toLowerCase()
const has1mTag = has1mContext(normalizedModel) const modelString = has1mTag ? normalizedModel.replace(/\[1m]$/i, '').trim() : normalizedModel
if (isModelAlias(modelString)) { switch (modelString) { case 'opusplan': return getDefaultSonnetModel() + (has1mTag ? '[1m]' : '') // Sonnet is default, Opus in plan mode case 'sonnet': return getDefaultSonnetModel() + (has1mTag ? '[1m]' : '') case 'haiku': return getDefaultHaikuModel() + (has1mTag ? '[1m]' : '') case 'opus': return getDefaultOpusModel() + (has1mTag ? '[1m]' : '') case 'best': return getBestModel() default: } }
// Opus 4/4.1 are no longer available on the first-party API (same as // Claude.ai) — silently remap to the current Opus default. The 'opus' // alias already resolves to 4.6, so the only users on these explicit // strings pinned them in settings/env/--model/SDK before 4.5 launched. // 3P providers may not yet have 4.6 capacity, so pass through unchanged. if ( getAPIProvider() === 'firstParty' && isLegacyOpusFirstParty(modelString) && isLegacyModelRemapEnabled() ) { return getDefaultOpusModel() + (has1mTag ? '[1m]' : '') }
if (process.env.USER_TYPE === 'ant') { const has1mAntTag = has1mContext(normalizedModel) const baseAntModel = normalizedModel.replace(/\[1m]$/i, '').trim()
const antModel = resolveAntModel(baseAntModel) if (antModel) { const suffix = has1mAntTag ? '[1m]' : '' return antModel.model + suffix }
// Fall through to the alias string if we cannot load the config. The API calls // will fail with this string, but we should hear about it through feedback and // can tell the user to restart/wait for flag cache refresh to get the latest values. }
// Preserve original case for custom model names (e.g., Azure Foundry deployment IDs) // Only strip [1m] suffix if present, maintaining case of the base model if (has1mTag) { return modelInputTrimmed.replace(/\[1m\]$/i, '').trim() + '[1m]' } return modelInputTrimmed}
/** * Resolves a skill's `model:` frontmatter against the current model, carrying * the `[1m]` suffix over when the target family supports it. * * A skill author writing `model: opus` means "use opus-class reasoning" — not * "downgrade to 200K". If the user is on opus[1m] at 230K tokens and invokes a * skill with `model: opus`, passing the bare alias through drops the effective * context window from 1M to 200K, which trips autocompact at 23% apparent usage * and surfaces "Context limit reached" even though nothing overflowed. * * We only carry [1m] when the target actually supports it (sonnet/opus). A skill * with `model: haiku` on a 1M session still downgrades — haiku has no 1M variant, * so the autocompact that follows is correct. Skills that already specify [1m] * are left untouched. */export function resolveSkillModelOverride( skillModel: string, currentModel: string,): string { if (has1mContext(skillModel) || !has1mContext(currentModel)) { return skillModel } // modelSupports1M matches on canonical IDs ('claude-opus-4-6', 'claude-sonnet-4'); // a bare 'opus' alias falls through getCanonicalName unmatched. Resolve first. if (modelSupports1M(parseUserSpecifiedModel(skillModel))) { return skillModel + '[1m]' } return skillModel}
const LEGACY_OPUS_FIRSTPARTY = [ 'claude-opus-4-20250514', 'claude-opus-4-1-20250805', 'claude-opus-4-0', 'claude-opus-4-1',]
function isLegacyOpusFirstParty(model: string): boolean { return LEGACY_OPUS_FIRSTPARTY.includes(model)}
/** * Opt-out for the legacy Opus 4.0/4.1 → current Opus remap. */export function isLegacyModelRemapEnabled(): boolean { return !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_LEGACY_MODEL_REMAP)}
export function modelDisplayString(model: ModelSetting): string { if (model === null) { if (process.env.USER_TYPE === 'ant') { return `Default for Ants (${renderDefaultModelSetting(getDefaultMainLoopModelSetting())})` } else if (isClaudeAISubscriber()) { return `Default (${getClaudeAiUserDefaultModelDescription()})` } return `Default (${getDefaultMainLoopModel()})` } const resolvedModel = parseUserSpecifiedModel(model) return model === resolvedModel ? resolvedModel : `${model} (${resolvedModel})`}
// @[MODEL LAUNCH]: Add a marketing name mapping for the new model below.export function getMarketingNameForModel(modelId: string): string | undefined { if (getAPIProvider() === 'foundry') { // deployment ID is user-defined in Foundry, so it may have no relation to the actual model return undefined }
const has1m = modelId.toLowerCase().includes('[1m]') const canonical = getCanonicalName(modelId)
if (canonical.includes('claude-opus-4-6')) { return has1m ? 'Opus 4.6 (with 1M context)' : 'Opus 4.6' } if (canonical.includes('claude-opus-4-5')) { return 'Opus 4.5' } if (canonical.includes('claude-opus-4-1')) { return 'Opus 4.1' } if (canonical.includes('claude-opus-4')) { return 'Opus 4' } if (canonical.includes('claude-sonnet-4-6')) { return has1m ? 'Sonnet 4.6 (with 1M context)' : 'Sonnet 4.6' } if (canonical.includes('claude-sonnet-4-5')) { return has1m ? 'Sonnet 4.5 (with 1M context)' : 'Sonnet 4.5' } if (canonical.includes('claude-sonnet-4')) { return has1m ? 'Sonnet 4 (with 1M context)' : 'Sonnet 4' } if (canonical.includes('claude-3-7-sonnet')) { return 'Claude 3.7 Sonnet' } if (canonical.includes('claude-3-5-sonnet')) { return 'Claude 3.5 Sonnet' } if (canonical.includes('claude-haiku-4-5')) { return 'Haiku 4.5' } if (canonical.includes('claude-3-5-haiku')) { return 'Claude 3.5 Haiku' }
return undefined}
export function normalizeModelStringForAPI(model: string): string { return model.replace(/\[(1|2)m\]/gi, '')}