The model system is split across two directories. Every path below is in src/.
| File | Responsibility |
|---|---|
| utils/model/configs.ts | Provider-specific model ID strings; the single registry for all known models |
| utils/model/providers.ts | Detect active API provider (firstParty / bedrock / vertex / foundry) |
| utils/model/model.ts | Selection priority chain, alias resolution, default per subscription tier |
| utils/model/aliases.ts | Canonical alias list: sonnet, opus, haiku, best, opusplan, [1m] variants |
| utils/model/modelStrings.ts | Runtime model-string resolution; Bedrock inference-profile discovery |
| utils/model/modelAllowlist.ts | availableModels enforcement — three-tier matching logic |
| utils/model/modelOptions.ts | Build /model picker options per subscription tier |
| utils/model/agent.ts | Subagent model resolution, Bedrock region-prefix inheritance |
| utils/model/check1mAccess.ts | Gate 1M context behind extra-usage billing for subscribers |
| utils/model/deprecation.ts | Retirement-date warnings per provider |
| utils/model/validateModel.ts | Live model validation via a minimal API probe call |
| utils/model/modelSupportOverrides.ts | 3P env-var overrides for effort/thinking capability flags |
| utils/model/modelCapabilities.ts | Ant-only: fetch/cache model capability metadata from API |
| utils/model/antModels.ts | Ant-only: codename models from Statsig feature flag |
| utils/model/bedrock.ts | AWS Bedrock inference-profile listing and region-prefix utilities |
| utils/effort.ts | Effort levels, API mapping, persistence rules |
| utils/fastMode.ts | Fast mode toggle, cooldown, availability checks |
| commands/model/ | /model slash command + ModelPicker UI |
| commands/effort/ | /effort slash command |
| commands/fast/ | /fast slash command |
| migrations/migrate*.ts | Automated settings upgrades on startup |
Every model the system can reference is declared in a single object in configs.ts. Each entry is a ModelConfig — a record keyed by provider — so the same logical model has exactly one canonical entry regardless of how Bedrock or Vertex want to name it.
// utils/model/configs.ts
export type ModelConfig = Record<APIProvider, ModelName>
export const CLAUDE_OPUS_4_6_CONFIG = {
firstParty: 'claude-opus-4-6',
bedrock: 'us.anthropic.claude-opus-4-6-v1',
vertex: 'claude-opus-4-6',
foundry: 'claude-opus-4-6',
} as const satisfies ModelConfig
// @[MODEL LAUNCH]: Register the new config here.
export const ALL_MODEL_CONFIGS = {
haiku35: CLAUDE_3_5_HAIKU_CONFIG,
haiku45: CLAUDE_HAIKU_4_5_CONFIG,
sonnet35: CLAUDE_3_5_V2_SONNET_CONFIG,
sonnet37: CLAUDE_3_7_SONNET_CONFIG,
sonnet40: CLAUDE_SONNET_4_CONFIG,
sonnet45: CLAUDE_SONNET_4_5_CONFIG,
sonnet46: CLAUDE_SONNET_4_6_CONFIG,
opus40: CLAUDE_OPUS_4_CONFIG,
opus41: CLAUDE_OPUS_4_1_CONFIG,
opus45: CLAUDE_OPUS_4_5_CONFIG,
opus46: CLAUDE_OPUS_4_6_CONFIG,
} as const satisfies Record<string, ModelConfig>
@[MODEL LAUNCH] comment is a search target. All seven files that need editing when a new model ships are marked with this same tag. Grepping for it gives a launch checklist.
The Four Providers
firstParty
Default. Clean model IDs like claude-opus-4-6.
bedrock
AWS cross-region inference profiles; auto-discovered at boot.
vertex
GCP. Format: claude-opus-4-6 (no date suffix).
foundry
Azure. Deployment IDs are user-defined; marketing name lookup disabled.
// utils/model/providers.ts
export function getAPIProvider(): APIProvider {
return isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)
? 'bedrock'
: isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)
? 'vertex'
: isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
? 'foundry'
: 'firstParty'
}
How Bedrock model strings are resolved at runtime
Bedrock users may have custom cross-region inference profiles (e.g. eu.anthropic.claude-opus-4-6-v1) that differ from the hardcoded strings in configs.ts. On startup Claude Code calls getBedrockInferenceProfiles() — which lists SYSTEM_DEFINED profiles from AWS — then matches each config's firstParty ID as a substring to find the right profile for the user's region.
This happens asynchronously in the background so the REPL is not blocked. While the profile fetch is in flight, requests fall back to the hardcoded Bedrock IDs. The fetch is memoized — one call per process lifetime.
async function getBedrockModelStrings(): Promise<ModelStrings> {
const profiles = await getBedrockInferenceProfiles()
const out = {} as ModelStrings
for (const key of MODEL_KEYS) {
const needle = ALL_MODEL_CONFIGS[key].firstParty
out[key] = findFirstMatch(profiles, needle) || fallback[key]
}
return out
}
Additionally, users can override individual model strings via settings.json → modelOverrides. This is keyed by canonical first-party ID (e.g. "claude-opus-4-6": "arn:aws:bedrock:...") and layered on top of the discovered strings every time getModelStrings() is called.
getMainLoopModel() in model.ts is the single function that determines which model runs a conversation. It walks five layers in strict priority order:
-
/model command during session Stored in bootstrap state as
mainLoopModelOverride. Highest priority — overrides everything for the rest of the session. -
--model CLI flag at startup Also sets
mainLoopModelOverridein bootstrap state, but only before the session begins. -
ANTHROPIC_MODEL env var Checked synchronously. Accepts both full model IDs and aliases.
-
settings.json → model field Set by
/modeland persisted across sessions. The only layer migrations touch. -
Built-in subscription default Max/Team Premium → Opus 4.6. Everyone else → Sonnet 4.6 (3P may default to 4.5).
getUserSpecifiedModelSetting() checks the model against settings.availableModels. If a user-specified model is not on the allowlist it is silently ignored and the default is used instead.
// utils/model/model.ts — simplified
export function getUserSpecifiedModelSetting(): ModelSetting | undefined {
const modelOverride = getMainLoopModelOverride() // layers 1+2
if (modelOverride !== undefined) return modelOverride
const settings = getSettings_DEPRECATED() || {}
const specifiedModel =
process.env.ANTHROPIC_MODEL || settings.model || undefined // layers 3+4
if (specifiedModel && !isModelAllowed(specifiedModel)) return undefined
return specifiedModel
}
How opusplan and runtime mode affect model selection
Some model values carry behavior beyond a simple model ID. opusplan is an alias that switches between Sonnet and Opus depending on the current permission mode:
export function getRuntimeMainLoopModel(params: {
permissionMode: PermissionMode
mainLoopModel: string
exceeds200kTokens?: boolean
}): ModelName {
if (getUserSpecifiedModelSetting() === 'opusplan'
&& permissionMode === 'plan'
&& !exceeds200kTokens) {
return getDefaultOpusModel() // upgrade to Opus in plan mode
}
if (getUserSpecifiedModelSetting() === 'haiku' && permissionMode === 'plan') {
return getDefaultSonnetModel() // upgrade haiku to sonnet for plan
}
return mainLoopModel
}
If a haiku alias is active and the user enters plan mode, it is silently upgraded to Sonnet. This prevents plan mode running on an underpowered model.
Users never need to type a full versioned model ID. The alias system resolves short names to current provider-appropriate model strings:
// utils/model/aliases.ts
export const MODEL_ALIASES = [
'sonnet', 'opus', 'haiku', 'best',
'sonnet[1m]', 'opus[1m]',
'opusplan',
] as const
The [1m] suffix is special: it is accepted on any alias or model ID and signals the 1 million token context window variant. parseUserSpecifiedModel() strips it before resolving the base model, then re-appends it so the API receives the correct model string:
export function parseUserSpecifiedModel(modelInput: ModelName | ModelAlias): ModelName {
const has1mTag = has1mContext(normalizedModel) // checks for [1m] suffix
const baseModel = has1mTag
? normalizedModel.replace(/\[1m]$/i, '').trim()
: normalizedModel
if (isModelAlias(baseModel)) {
switch (baseModel) {
case 'opus': return getDefaultOpusModel() + (has1mTag ? '[1m]' : '')
case 'sonnet': return getDefaultSonnetModel() + (has1mTag ? '[1m]' : '')
case 'haiku': return getDefaultHaikuModel() + (has1mTag ? '[1m]' : '')
// ...
}
}
return modelInputTrimmed // preserve original case for custom IDs
}
model: opus, resolveSkillModelOverride() automatically carries the [1m] suffix from the parent session to the skill invocation — as long as the target family supports 1M context. A skill that sets model: haiku will intentionally drop 1M because haiku has no 1M variant.
Extended context is not a separate model; it is a runtime flag on an existing model. The [1m] suffix travels through the entire pipeline and is stripped only at the API call boundary by normalizeModelStringForAPI().
Access Gates
Two functions gate access to 1M, one each for Opus and Sonnet:
// utils/model/check1mAccess.ts
export function checkOpus1mAccess(): boolean {
if (is1mContextDisabled()) return false // CLAUDE_CODE_DISABLE_1M_CONTEXT env
if (isClaudeAISubscriber()) return isExtraUsageEnabled() // subscriber: extra-usage billing
return true // API/PAYG: always available
}
The Opus 1M Merge
For Max and Team Premium subscribers, there is only one Opus option: opus[1m] — they always get extended context. This "merge" is controlled by isOpus1mMergeEnabled():
export function isOpus1mMergeEnabled(): boolean {
if (is1mContextDisabled() || isProSubscriber() || getAPIProvider() !== 'firstParty')
return false
if (isClaudeAISubscriber() && getSubscriptionType() === null)
return false // fail closed: unknown sub type → no 1M (avoids API rate-limit error)
return true
}
getSubscriptionType() === null guard exists because OAuth tokens can be refreshed without a subscriptionType field. Without this guard, a partially-refreshed token would incorrectly expose opus[1m] to Pro users, whose API tier rejects it with a misleading "rate limit reached" error.
Model picker options per subscription tier
The /model picker shows a different option list depending on who is logged in:
| Tier | Default | Options shown |
|---|---|---|
| Max / Team Premium | Opus 4.6 (1M) | Opus, Sonnet, Sonnet 1M, Haiku |
| Pro / Team Standard | Sonnet 4.6 | Sonnet, Sonnet 1M, Opus, Opus 1M, Haiku |
| PAYG 1P (API key) | Sonnet 4.6 | Sonnet, Sonnet 1M, Opus 1M, Haiku |
| PAYG 3P (Bedrock/Vertex) | Sonnet 4.5 | Custom or Sonnet 4.6/4.5, Opus 4.1/4.6, Haiku |
The "Ant" tier (Anthropic employees) gets a completely different list built from a Statsig feature flag — see antModels.ts.
Effort is a thinking-budget hint sent to the API as a parameter. It only applies to models that support it — currently claude-opus-4-6 and claude-sonnet-4-6.
Resolution order
// utils/effort.ts — resolveAppliedEffort()
// Priority: env > appState > model default
const envOverride = getEffortEnvOverride() // CLAUDE_CODE_EFFORT_LEVEL env var
if (envOverride === null) return undefined // null = explicit 'unset'
const resolved = envOverride ?? appStateEffortValue ?? getDefaultEffortForModel(model)
// API rejects 'max' on non-Opus-4.6 — auto-downgrade
if (resolved === 'max' && !modelSupportsMaxEffort(model)) return 'high'
return resolved
Persistence rules
Not all effort values can be saved to settings.json. The toPersistableEffort() function enforces these constraints:
low,medium,high— always persistedmax— only persisted for Anthropic employees (USER_TYPE === 'ant'); session-scoped for everyone else- Numeric values (e.g.
42) — never persisted, model-default only
CLAUDE_CODE_EFFORT_LEVEL is set and a user runs /effort high, the CLI saves the setting to disk but warns them: "CLAUDE_CODE_EFFORT_LEVEL=max overrides this session — clear it and high takes over." The env var always wins at runtime; the setting takes effect next session after clearing the env.
Fast mode is a research-preview feature that enables a lower-latency execution path for Opus 4.6 only on the first-party API. It is toggled via /fast or settings, and the command is hidden unless fast mode is available for the session.
// commands/fast/index.ts
const fast = {
type: 'local-jsx',
name: 'fast',
get description() { return `Toggle fast mode (${FAST_MODE_MODEL_DISPLAY} only)` },
availability: ['claude-ai', 'console'],
isEnabled: () => isFastModeEnabled(),
get isHidden() { return !isFastModeEnabled() },
}
Availability checks
Fast mode goes through a layered availability check before being offered to the user:
CLAUDE_CODE_DISABLE_FAST_MODE=1hard-disables it globally- Only available on
firstPartyprovider — not on Bedrock/Vertex/Foundry - Requires a paid subscription; free accounts see "Fast mode requires a paid subscription"
- Subscribers need extra-usage billing enabled; without it the reason is "Fast mode requires extra usage billing"
- Not available in the Agent SDK (non-interactive sessions) unless
fastMode: trueis explicitly set in flag settings - Statsig feature flag
tengu_penguins_offcan kill it remotely with a custom reason string
Model switching on toggle
Enabling fast mode only switches the model if the current model does not support fast mode. If you are already on Opus 4.6, the model is left unchanged:
// commands/fast/fast.tsx — applyFastMode()
if (enable) {
setAppState(prev => {
const needsSwitch = !isFastModeSupportedByModel(prev.mainLoopModel)
return {
...prev,
...(needsSwitch ? { mainLoopModel: getFastModeModel() } : {}),
fastMode: true,
}
})
}
clearFastModeCooldown().
Enterprise and team admins can restrict which models users can select by setting availableModels in a policy settings file. The matching logic has three tiers, applied in order:
- Family wildcard —
"opus"allows any Opus model, unless narrower entries for that family also exist - Version prefix —
"opus-4-5"or"claude-opus-4-5"matches any build of that version at a segment boundary - Exact ID —
"claude-opus-4-5-20251101"matches only that exact string
// Example: restrict to Sonnet 4.6 and Haiku 4.5 only
// ~/.claude/settings.json (admin policy)
{
"availableModels": ["sonnet", "haiku"]
}
// Example: allow only Opus 4.5 (not 4.6)
{
"availableModels": ["opus-4-5"]
}
// Example: empty allowlist blocks ALL user-specified models
// (user gets the subscription default only)
{
"availableModels": []
}
"opus" and "opus-4-5" appear in the allowlist, the family wildcard "opus" is ignored. The system detects that a more specific entry exists for the opus family and only applies the version-prefix match. This prevents an admin from accidentally allowing all Opus while intending to restrict to one version.
The full allowlist matching implementation
export function isModelAllowed(model: string): boolean {
const { availableModels } = settings
if (!availableModels) return true // no restriction
if (availableModels.length === 0) return false // empty blocks all
// 1. Direct match — but skip family aliases that are narrowed
if (allowlist.includes(model)) {
if (!isModelFamilyAlias(model) || !familyHasSpecificEntries(model, allowlist))
return true
}
// 2. Family wildcard (only if no specific entries for that family)
for (const entry of allowlist) {
if (isModelFamilyAlias(entry)
&& !familyHasSpecificEntries(entry, allowlist)
&& modelBelongsToFamily(model, entry)) return true
}
// 3. Version-prefix match at segment boundary
for (const entry of allowlist) {
if (!isModelFamilyAlias(entry) && !isModelAlias(entry)) {
if (modelMatchesVersionPrefix(model, entry)) return true
}
}
return false
}
When Claude Code spawns a subagent (via Task tool or SDK), the subagent needs to know which model to use. The resolution happens in utils/model/agent.ts and follows this precedence:
-
CLAUDE_CODE_SUBAGENT_MODEL env var Global override for all subagents. Useful in CI pipelines.
-
Tool-specified model (model param on the Task call) The orchestrating model can request a specific model for a subagent.
-
Agent config model field Set in the agent definition. Accepts any alias or model ID.
-
"inherit" (default) Subagent uses the same model as the parent thread, including opusplan resolution.
Alias-matches-parent-tier optimization
When a bare family alias (opus, sonnet, haiku) matches the parent model's family, the subagent inherits the parent's exact model string instead of resolving the alias to the provider default. This prevents a Vertex user on a pinned Opus version from having subagents silently downgrade:
// utils/model/agent.ts
function aliasMatchesParentTier(alias: string, parentModel: string): boolean {
const canonical = getCanonicalName(parentModel)
switch (alias.toLowerCase()) {
case 'opus': return canonical.includes('opus')
case 'sonnet': return canonical.includes('sonnet')
case 'haiku': return canonical.includes('haiku')
default: return false
}
}
// Note: opus[1m], best, opusplan fall through — they have extra semantics
Bedrock region prefix inheritance
On Bedrock, cross-region inference profiles carry a region prefix (eu., us.). If the parent model uses eu.anthropic.claude-opus-4-6-v1, subagents using alias models must also use the eu. prefix. This is handled by applyParentRegionPrefix() — which is skipped if the subagent's model spec already carries its own explicit prefix.
When Claude Code upgrades its default models, users who had previously pinned a specific model need their settings migrated automatically. The migration system runs a set of functions on every startup; each migration is idempotent (safe to re-run).
migrateFennecToOpus
Ant-only. Remaps codename aliases (fennec-latest, fennec-fast-latest, opus-4-5-fast) to their public equivalents (opus, opus[1m], and fast mode flag). Only touches userSettings — project/policy pins are left alone.
migrateLegacyOpusToCurrent
First-party only. Rewrites explicit Opus 4.0/4.1 strings (claude-opus-4-20250514, claude-opus-4-1-20250805) to the opus alias. Sets legacyOpusMigrationTimestamp in global config so the REPL can show a one-time notification. CLAUDE_CODE_DISABLE_LEGACY_MODEL_REMAP=1 skips this.
migrateSonnet1mToSonnet45
Pins sonnet[1m] settings to the explicit sonnet-4-5-20250929[1m] string. Needed because Sonnet 4.6 1M was offered to a different access group than Sonnet 4.5 1M — the sonnet alias now resolves to 4.6. Tracked by sonnet1m45MigrationComplete flag.
migrateSonnet45ToSonnet46
Pro/Max/Team Premium first-party users only. Upgrades explicit Sonnet 4.5 strings back to the sonnet alias (which now resolves to 4.6). Sets sonnet45To46MigrationTimestamp for new-version notification — but skips the notification for brand-new users (startup count ≤ 1).
migrateOpusToOpus1m
For Max/Team Premium where the 1M merge is enabled: upgrades a pinned opus setting to opus[1m]. Skipped if the upgrade would result in the same resolved model as the subscription default (to avoid storing a redundant explicit setting).
resetProToOpusDefault
Pro subscribers on first-party who had no custom model are given a migration timestamp — this enables the REPL to display a one-time "You've been upgraded to Opus 4.5 as your default" callout. Users who already had a custom model setting are silently marked complete.
userSettings, never merged/project/policy settings; (2) use completion flags or idempotent write logic so reruns are safe; (3) log an analytics event so the team can measure migration success rates.
Live model validation
When a user types a custom model ID into /model, the system cannot know if it is valid just by string-matching. validateModel() sends a real API call with max_tokens: 1 to probe the model:
// utils/model/validateModel.ts
await sideQuery({
model: normalizedModel,
max_tokens: 1,
maxRetries: 0,
querySource: 'model_validation',
messages: [{ role: 'user', content: [{ type: 'text', text: 'Hi' }] }],
})
A NotFoundError (404) means the model does not exist. For 3P users, a fallback suggestion is returned (e.g. if you ask for opus-4-6 on Vertex but it is not available yet, it suggests opus-4-1).
Deprecation warnings
The deprecation table in deprecation.ts maps model substrings to retirement dates per provider:
const DEPRECATED_MODELS: Record<string, DeprecationEntry> = {
'claude-3-7-sonnet': {
modelName: 'Claude 3.7 Sonnet',
retirementDates: {
firstParty: 'February 19, 2026',
bedrock: 'April 28, 2026',
vertex: 'May 11, 2026',
foundry: 'February 19, 2026',
},
},
// ...
}
If the active model matches any entry and has a retirement date for the current provider, the UI shows a yellow warning banner. Bedrock and Vertex often get longer retirement windows than first-party.
Key Takeaways
- One registry, four providers.
ALL_MODEL_CONFIGSinconfigs.tsis the single source of truth for every model ID. Adding a new model requires updating that file plus six other tagged locations — all marked with@[MODEL LAUNCH]. - Selection has exactly five layers. /model command → --model flag → ANTHROPIC_MODEL env → settings.json → subscription default. The allowlist gate runs before any of them.
- The [1m] suffix is not a separate model. It is a runtime flag stripped at the API boundary by
normalizeModelStringForAPI(). Access is gated by subscription tier and extra-usage billing. - Effort and fast mode are orthogonal knobs. Effort controls thinking budget (low/medium/high/max). Fast mode is a latency optimization, Opus 4.6 first-party only. Both have env-var overrides that win over UI settings.
- Migrations only touch userSettings. Project and policy settings are intentionally left alone to avoid silently promoting a project-scoped pin to the global default.
- Subagents default to "inherit". The parent's exact model string is passed through, including opusplan → Opus resolution. Bedrock region prefixes are also inherited to avoid IAM permission failures.
- The allowlist narrowing rule is non-obvious.
["opus", "opus-4-5"]does NOT allow all Opus — the specific entry narrows the family wildcard to only opus-4-5. - Fast mode is first-party only and requires extra-usage billing for subscribers. The check is multi-layered: env flag, Statsig kill switch, provider check, subscription check, billing check.