the claude code sourcemaps leaked march 31
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 618 lines 21 kB view raw
1// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered 2/** 3 * Ensure that any model codenames introduced here are also added to 4 * scripts/excluded-strings.txt to avoid leaking them. Wrap any codename string 5 * literals with process.env.USER_TYPE === 'ant' for Bun to remove the codenames 6 * during dead code elimination 7 */ 8import { getMainLoopModelOverride } from '../../bootstrap/state.js' 9import { 10 getSubscriptionType, 11 isClaudeAISubscriber, 12 isMaxSubscriber, 13 isProSubscriber, 14 isTeamPremiumSubscriber, 15} from '../auth.js' 16import { 17 has1mContext, 18 is1mContextDisabled, 19 modelSupports1M, 20} from '../context.js' 21import { isEnvTruthy } from '../envUtils.js' 22import { getModelStrings, resolveOverriddenModel } from './modelStrings.js' 23import { formatModelPricing, getOpus46CostTier } from '../modelCost.js' 24import { getSettings_DEPRECATED } from '../settings/settings.js' 25import type { PermissionMode } from '../permissions/PermissionMode.js' 26import { getAPIProvider } from './providers.js' 27import { LIGHTNING_BOLT } from '../../constants/figures.js' 28import { isModelAllowed } from './modelAllowlist.js' 29import { type ModelAlias, isModelAlias } from './aliases.js' 30import { capitalize } from '../stringUtils.js' 31 32export type ModelShortName = string 33export type ModelName = string 34export type ModelSetting = ModelName | ModelAlias | null 35 36export function getSmallFastModel(): ModelName { 37 return process.env.ANTHROPIC_SMALL_FAST_MODEL || getDefaultHaikuModel() 38} 39 40export function isNonCustomOpusModel(model: ModelName): boolean { 41 return ( 42 model === getModelStrings().opus40 || 43 model === getModelStrings().opus41 || 44 model === getModelStrings().opus45 || 45 model === getModelStrings().opus46 46 ) 47} 48 49/** 50 * Helper to get the model from /model (including via /config), the --model flag, environment variable, 51 * or the saved settings. The returned value can be a model alias if that's what the user specified. 52 * Undefined if the user didn't configure anything, in which case we fall back to 53 * the default (null). 54 * 55 * Priority order within this function: 56 * 1. Model override during session (from /model command) - highest priority 57 * 2. Model override at startup (from --model flag) 58 * 3. ANTHROPIC_MODEL environment variable 59 * 4. Settings (from user's saved settings) 60 */ 61export function getUserSpecifiedModelSetting(): ModelSetting | undefined { 62 let specifiedModel: ModelSetting | undefined 63 64 const modelOverride = getMainLoopModelOverride() 65 if (modelOverride !== undefined) { 66 specifiedModel = modelOverride 67 } else { 68 const settings = getSettings_DEPRECATED() || {} 69 specifiedModel = process.env.ANTHROPIC_MODEL || settings.model || undefined 70 } 71 72 // Ignore the user-specified model if it's not in the availableModels allowlist. 73 if (specifiedModel && !isModelAllowed(specifiedModel)) { 74 return undefined 75 } 76 77 return specifiedModel 78} 79 80/** 81 * Get the main loop model to use for the current session. 82 * 83 * Model Selection Priority Order: 84 * 1. Model override during session (from /model command) - highest priority 85 * 2. Model override at startup (from --model flag) 86 * 3. ANTHROPIC_MODEL environment variable 87 * 4. Settings (from user's saved settings) 88 * 5. Built-in default 89 * 90 * @returns The resolved model name to use 91 */ 92export function getMainLoopModel(): ModelName { 93 const model = getUserSpecifiedModelSetting() 94 if (model !== undefined && model !== null) { 95 return parseUserSpecifiedModel(model) 96 } 97 return getDefaultMainLoopModel() 98} 99 100export function getBestModel(): ModelName { 101 return getDefaultOpusModel() 102} 103 104// @[MODEL LAUNCH]: Update the default Opus model (3P providers may lag so keep defaults unchanged). 105export function getDefaultOpusModel(): ModelName { 106 if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) { 107 return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL 108 } 109 // 3P providers (Bedrock, Vertex, Foundry) — kept as a separate branch 110 // even when values match, since 3P availability lags firstParty and 111 // these will diverge again at the next model launch. 112 if (getAPIProvider() !== 'firstParty') { 113 return getModelStrings().opus46 114 } 115 return getModelStrings().opus46 116} 117 118// @[MODEL LAUNCH]: Update the default Sonnet model (3P providers may lag so keep defaults unchanged). 119export function getDefaultSonnetModel(): ModelName { 120 if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) { 121 return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL 122 } 123 // Default to Sonnet 4.5 for 3P since they may not have 4.6 yet 124 if (getAPIProvider() !== 'firstParty') { 125 return getModelStrings().sonnet45 126 } 127 return getModelStrings().sonnet46 128} 129 130// @[MODEL LAUNCH]: Update the default Haiku model (3P providers may lag so keep defaults unchanged). 131export function getDefaultHaikuModel(): ModelName { 132 if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) { 133 return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL 134 } 135 136 // Haiku 4.5 is available on all platforms (first-party, Foundry, Bedrock, Vertex) 137 return getModelStrings().haiku45 138} 139 140/** 141 * Get the model to use for runtime, depending on the runtime context. 142 * @param params Subset of the runtime context to determine the model to use. 143 * @returns The model to use 144 */ 145export function getRuntimeMainLoopModel(params: { 146 permissionMode: PermissionMode 147 mainLoopModel: string 148 exceeds200kTokens?: boolean 149}): ModelName { 150 const { permissionMode, mainLoopModel, exceeds200kTokens = false } = params 151 152 // opusplan uses Opus in plan mode without [1m] suffix. 153 if ( 154 getUserSpecifiedModelSetting() === 'opusplan' && 155 permissionMode === 'plan' && 156 !exceeds200kTokens 157 ) { 158 return getDefaultOpusModel() 159 } 160 161 // sonnetplan by default 162 if (getUserSpecifiedModelSetting() === 'haiku' && permissionMode === 'plan') { 163 return getDefaultSonnetModel() 164 } 165 166 return mainLoopModel 167} 168 169/** 170 * Get the default main loop model setting. 171 * 172 * This handles the built-in default: 173 * - Opus for Max and Team Premium users 174 * - Sonnet 4.6 for all other users (including Team Standard, Pro, Enterprise) 175 * 176 * @returns The default model setting to use 177 */ 178export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias { 179 // Ants default to defaultModel from flag config, or Opus 1M if not configured 180 if (process.env.USER_TYPE === 'ant') { 181 return ( 182 getAntModelOverrideConfig()?.defaultModel ?? 183 getDefaultOpusModel() + '[1m]' 184 ) 185 } 186 187 // Max users get Opus as default 188 if (isMaxSubscriber()) { 189 return getDefaultOpusModel() + (isOpus1mMergeEnabled() ? '[1m]' : '') 190 } 191 192 // Team Premium gets Opus (same as Max) 193 if (isTeamPremiumSubscriber()) { 194 return getDefaultOpusModel() + (isOpus1mMergeEnabled() ? '[1m]' : '') 195 } 196 197 // PAYG (1P and 3P), Enterprise, Team Standard, and Pro get Sonnet as default 198 // Note that PAYG (3P) may default to an older Sonnet model 199 return getDefaultSonnetModel() 200} 201 202/** 203 * Synchronous operation to get the default main loop model to use 204 * (bypassing any user-specified values). 205 */ 206export function getDefaultMainLoopModel(): ModelName { 207 return parseUserSpecifiedModel(getDefaultMainLoopModelSetting()) 208} 209 210// @[MODEL LAUNCH]: Add a canonical name mapping for the new model below. 211/** 212 * Pure string-match that strips date/provider suffixes from a first-party model 213 * name. Input must already be a 1P-format ID (e.g. 'claude-3-7-sonnet-20250219', 214 * 'us.anthropic.claude-opus-4-6-v1:0'). Does not touch settings, so safe at 215 * module top-level (see MODEL_COSTS in modelCost.ts). 216 */ 217export function firstPartyNameToCanonical(name: ModelName): ModelShortName { 218 name = name.toLowerCase() 219 // Special cases for Claude 4+ models to differentiate versions 220 // Order matters: check more specific versions first (4-5 before 4) 221 if (name.includes('claude-opus-4-6')) { 222 return 'claude-opus-4-6' 223 } 224 if (name.includes('claude-opus-4-5')) { 225 return 'claude-opus-4-5' 226 } 227 if (name.includes('claude-opus-4-1')) { 228 return 'claude-opus-4-1' 229 } 230 if (name.includes('claude-opus-4')) { 231 return 'claude-opus-4' 232 } 233 if (name.includes('claude-sonnet-4-6')) { 234 return 'claude-sonnet-4-6' 235 } 236 if (name.includes('claude-sonnet-4-5')) { 237 return 'claude-sonnet-4-5' 238 } 239 if (name.includes('claude-sonnet-4')) { 240 return 'claude-sonnet-4' 241 } 242 if (name.includes('claude-haiku-4-5')) { 243 return 'claude-haiku-4-5' 244 } 245 // Claude 3.x models use a different naming scheme (claude-3-{family}) 246 if (name.includes('claude-3-7-sonnet')) { 247 return 'claude-3-7-sonnet' 248 } 249 if (name.includes('claude-3-5-sonnet')) { 250 return 'claude-3-5-sonnet' 251 } 252 if (name.includes('claude-3-5-haiku')) { 253 return 'claude-3-5-haiku' 254 } 255 if (name.includes('claude-3-opus')) { 256 return 'claude-3-opus' 257 } 258 if (name.includes('claude-3-sonnet')) { 259 return 'claude-3-sonnet' 260 } 261 if (name.includes('claude-3-haiku')) { 262 return 'claude-3-haiku' 263 } 264 const match = name.match(/(claude-(\d+-\d+-)?\w+)/) 265 if (match && match[1]) { 266 return match[1] 267 } 268 // Fall back to the original name if no pattern matches 269 return name 270} 271 272/** 273 * Maps a full model string to a shorter canonical version that's unified across 1P and 3P providers. 274 * For example, 'claude-3-5-haiku-20241022' and 'us.anthropic.claude-3-5-haiku-20241022-v1:0' 275 * would both be mapped to 'claude-3-5-haiku'. 276 * @param fullModelName The full model name (e.g., 'claude-3-5-haiku-20241022') 277 * @returns The short name (e.g., 'claude-3-5-haiku') if found, or the original name if no mapping exists 278 */ 279export function getCanonicalName(fullModelName: ModelName): ModelShortName { 280 // Resolve overridden model IDs (e.g. Bedrock ARNs) back to canonical names. 281 // resolved is always a 1P-format ID, so firstPartyNameToCanonical can handle it. 282 return firstPartyNameToCanonical(resolveOverriddenModel(fullModelName)) 283} 284 285// @[MODEL LAUNCH]: Update the default model description strings shown to users. 286export function getClaudeAiUserDefaultModelDescription( 287 fastMode = false, 288): string { 289 if (isMaxSubscriber() || isTeamPremiumSubscriber()) { 290 if (isOpus1mMergeEnabled()) { 291 return `Opus 4.6 with 1M context · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}` 292 } 293 return `Opus 4.6 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}` 294 } 295 return 'Sonnet 4.6 · Best for everyday tasks' 296} 297 298export function renderDefaultModelSetting( 299 setting: ModelName | ModelAlias, 300): string { 301 if (setting === 'opusplan') { 302 return 'Opus 4.6 in plan mode, else Sonnet 4.6' 303 } 304 return renderModelName(parseUserSpecifiedModel(setting)) 305} 306 307export function getOpus46PricingSuffix(fastMode: boolean): string { 308 if (getAPIProvider() !== 'firstParty') return '' 309 const pricing = formatModelPricing(getOpus46CostTier(fastMode)) 310 const fastModeIndicator = fastMode ? ` (${LIGHTNING_BOLT})` : '' 311 return ` ·${fastModeIndicator} ${pricing}` 312} 313 314export function isOpus1mMergeEnabled(): boolean { 315 if ( 316 is1mContextDisabled() || 317 isProSubscriber() || 318 getAPIProvider() !== 'firstParty' 319 ) { 320 return false 321 } 322 // Fail closed when a subscriber's subscription type is unknown. The VS Code 323 // config-loading subprocess can have OAuth tokens with valid scopes but no 324 // subscriptionType field (stale or partial refresh). Without this guard, 325 // isProSubscriber() returns false for such users and the merge leaks 326 // opus[1m] into the model dropdown — the API then rejects it with a 327 // misleading "rate limit reached" error. 328 if (isClaudeAISubscriber() && getSubscriptionType() === null) { 329 return false 330 } 331 return true 332} 333 334export function renderModelSetting(setting: ModelName | ModelAlias): string { 335 if (setting === 'opusplan') { 336 return 'Opus Plan' 337 } 338 if (isModelAlias(setting)) { 339 return capitalize(setting) 340 } 341 return renderModelName(setting) 342} 343 344// @[MODEL LAUNCH]: Add display name cases for the new model (base + [1m] variant if applicable). 345/** 346 * Returns a human-readable display name for known public models, or null 347 * if the model is not recognized as a public model. 348 */ 349export function getPublicModelDisplayName(model: ModelName): string | null { 350 switch (model) { 351 case getModelStrings().opus46: 352 return 'Opus 4.6' 353 case getModelStrings().opus46 + '[1m]': 354 return 'Opus 4.6 (1M context)' 355 case getModelStrings().opus45: 356 return 'Opus 4.5' 357 case getModelStrings().opus41: 358 return 'Opus 4.1' 359 case getModelStrings().opus40: 360 return 'Opus 4' 361 case getModelStrings().sonnet46 + '[1m]': 362 return 'Sonnet 4.6 (1M context)' 363 case getModelStrings().sonnet46: 364 return 'Sonnet 4.6' 365 case getModelStrings().sonnet45 + '[1m]': 366 return 'Sonnet 4.5 (1M context)' 367 case getModelStrings().sonnet45: 368 return 'Sonnet 4.5' 369 case getModelStrings().sonnet40: 370 return 'Sonnet 4' 371 case getModelStrings().sonnet40 + '[1m]': 372 return 'Sonnet 4 (1M context)' 373 case getModelStrings().sonnet37: 374 return 'Sonnet 3.7' 375 case getModelStrings().sonnet35: 376 return 'Sonnet 3.5' 377 case getModelStrings().haiku45: 378 return 'Haiku 4.5' 379 case getModelStrings().haiku35: 380 return 'Haiku 3.5' 381 default: 382 return null 383 } 384} 385 386function maskModelCodename(baseName: string): string { 387 // Mask only the first dash-separated segment (the codename), preserve the rest 388 // e.g. capybara-v2-fast → cap*****-v2-fast 389 const [codename = '', ...rest] = baseName.split('-') 390 const masked = 391 codename.slice(0, 3) + '*'.repeat(Math.max(0, codename.length - 3)) 392 return [masked, ...rest].join('-') 393} 394 395export function renderModelName(model: ModelName): string { 396 const publicName = getPublicModelDisplayName(model) 397 if (publicName) { 398 return publicName 399 } 400 if (process.env.USER_TYPE === 'ant') { 401 const resolved = parseUserSpecifiedModel(model) 402 const antModel = resolveAntModel(model) 403 if (antModel) { 404 const baseName = antModel.model.replace(/\[1m\]$/i, '') 405 const masked = maskModelCodename(baseName) 406 const suffix = has1mContext(resolved) ? '[1m]' : '' 407 return masked + suffix 408 } 409 if (resolved !== model) { 410 return `${model} (${resolved})` 411 } 412 return resolved 413 } 414 return model 415} 416 417/** 418 * Returns a safe author name for public display (e.g., in git commit trailers). 419 * Returns "Claude {ModelName}" for publicly known models, or "Claude ({model})" 420 * for unknown/internal models so the exact model name is preserved. 421 * 422 * @param model The full model name 423 * @returns "Claude {ModelName}" for public models, or "Claude ({model})" for non-public models 424 */ 425export function getPublicModelName(model: ModelName): string { 426 const publicName = getPublicModelDisplayName(model) 427 if (publicName) { 428 return `Claude ${publicName}` 429 } 430 return `Claude (${model})` 431} 432 433/** 434 * Returns a full model name for use in this session, possibly after resolving 435 * a model alias. 436 * 437 * This function intentionally does not support version numbers to align with 438 * the model switcher. 439 * 440 * Supports [1m] suffix on any model alias (e.g., haiku[1m], sonnet[1m]) to enable 441 * 1M context window without requiring each variant to be in MODEL_ALIASES. 442 * 443 * @param modelInput The model alias or name provided by the user. 444 */ 445export function parseUserSpecifiedModel( 446 modelInput: ModelName | ModelAlias, 447): ModelName { 448 const modelInputTrimmed = modelInput.trim() 449 const normalizedModel = modelInputTrimmed.toLowerCase() 450 451 const has1mTag = has1mContext(normalizedModel) 452 const modelString = has1mTag 453 ? normalizedModel.replace(/\[1m]$/i, '').trim() 454 : normalizedModel 455 456 if (isModelAlias(modelString)) { 457 switch (modelString) { 458 case 'opusplan': 459 return getDefaultSonnetModel() + (has1mTag ? '[1m]' : '') // Sonnet is default, Opus in plan mode 460 case 'sonnet': 461 return getDefaultSonnetModel() + (has1mTag ? '[1m]' : '') 462 case 'haiku': 463 return getDefaultHaikuModel() + (has1mTag ? '[1m]' : '') 464 case 'opus': 465 return getDefaultOpusModel() + (has1mTag ? '[1m]' : '') 466 case 'best': 467 return getBestModel() 468 default: 469 } 470 } 471 472 // Opus 4/4.1 are no longer available on the first-party API (same as 473 // Claude.ai) — silently remap to the current Opus default. The 'opus' 474 // alias already resolves to 4.6, so the only users on these explicit 475 // strings pinned them in settings/env/--model/SDK before 4.5 launched. 476 // 3P providers may not yet have 4.6 capacity, so pass through unchanged. 477 if ( 478 getAPIProvider() === 'firstParty' && 479 isLegacyOpusFirstParty(modelString) && 480 isLegacyModelRemapEnabled() 481 ) { 482 return getDefaultOpusModel() + (has1mTag ? '[1m]' : '') 483 } 484 485 if (process.env.USER_TYPE === 'ant') { 486 const has1mAntTag = has1mContext(normalizedModel) 487 const baseAntModel = normalizedModel.replace(/\[1m]$/i, '').trim() 488 489 const antModel = resolveAntModel(baseAntModel) 490 if (antModel) { 491 const suffix = has1mAntTag ? '[1m]' : '' 492 return antModel.model + suffix 493 } 494 495 // Fall through to the alias string if we cannot load the config. The API calls 496 // will fail with this string, but we should hear about it through feedback and 497 // can tell the user to restart/wait for flag cache refresh to get the latest values. 498 } 499 500 // Preserve original case for custom model names (e.g., Azure Foundry deployment IDs) 501 // Only strip [1m] suffix if present, maintaining case of the base model 502 if (has1mTag) { 503 return modelInputTrimmed.replace(/\[1m\]$/i, '').trim() + '[1m]' 504 } 505 return modelInputTrimmed 506} 507 508/** 509 * Resolves a skill's `model:` frontmatter against the current model, carrying 510 * the `[1m]` suffix over when the target family supports it. 511 * 512 * A skill author writing `model: opus` means "use opus-class reasoning" — not 513 * "downgrade to 200K". If the user is on opus[1m] at 230K tokens and invokes a 514 * skill with `model: opus`, passing the bare alias through drops the effective 515 * context window from 1M to 200K, which trips autocompact at 23% apparent usage 516 * and surfaces "Context limit reached" even though nothing overflowed. 517 * 518 * We only carry [1m] when the target actually supports it (sonnet/opus). A skill 519 * with `model: haiku` on a 1M session still downgrades — haiku has no 1M variant, 520 * so the autocompact that follows is correct. Skills that already specify [1m] 521 * are left untouched. 522 */ 523export function resolveSkillModelOverride( 524 skillModel: string, 525 currentModel: string, 526): string { 527 if (has1mContext(skillModel) || !has1mContext(currentModel)) { 528 return skillModel 529 } 530 // modelSupports1M matches on canonical IDs ('claude-opus-4-6', 'claude-sonnet-4'); 531 // a bare 'opus' alias falls through getCanonicalName unmatched. Resolve first. 532 if (modelSupports1M(parseUserSpecifiedModel(skillModel))) { 533 return skillModel + '[1m]' 534 } 535 return skillModel 536} 537 538const LEGACY_OPUS_FIRSTPARTY = [ 539 'claude-opus-4-20250514', 540 'claude-opus-4-1-20250805', 541 'claude-opus-4-0', 542 'claude-opus-4-1', 543] 544 545function isLegacyOpusFirstParty(model: string): boolean { 546 return LEGACY_OPUS_FIRSTPARTY.includes(model) 547} 548 549/** 550 * Opt-out for the legacy Opus 4.0/4.1 → current Opus remap. 551 */ 552export function isLegacyModelRemapEnabled(): boolean { 553 return !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_LEGACY_MODEL_REMAP) 554} 555 556export function modelDisplayString(model: ModelSetting): string { 557 if (model === null) { 558 if (process.env.USER_TYPE === 'ant') { 559 return `Default for Ants (${renderDefaultModelSetting(getDefaultMainLoopModelSetting())})` 560 } else if (isClaudeAISubscriber()) { 561 return `Default (${getClaudeAiUserDefaultModelDescription()})` 562 } 563 return `Default (${getDefaultMainLoopModel()})` 564 } 565 const resolvedModel = parseUserSpecifiedModel(model) 566 return model === resolvedModel ? resolvedModel : `${model} (${resolvedModel})` 567} 568 569// @[MODEL LAUNCH]: Add a marketing name mapping for the new model below. 570export function getMarketingNameForModel(modelId: string): string | undefined { 571 if (getAPIProvider() === 'foundry') { 572 // deployment ID is user-defined in Foundry, so it may have no relation to the actual model 573 return undefined 574 } 575 576 const has1m = modelId.toLowerCase().includes('[1m]') 577 const canonical = getCanonicalName(modelId) 578 579 if (canonical.includes('claude-opus-4-6')) { 580 return has1m ? 'Opus 4.6 (with 1M context)' : 'Opus 4.6' 581 } 582 if (canonical.includes('claude-opus-4-5')) { 583 return 'Opus 4.5' 584 } 585 if (canonical.includes('claude-opus-4-1')) { 586 return 'Opus 4.1' 587 } 588 if (canonical.includes('claude-opus-4')) { 589 return 'Opus 4' 590 } 591 if (canonical.includes('claude-sonnet-4-6')) { 592 return has1m ? 'Sonnet 4.6 (with 1M context)' : 'Sonnet 4.6' 593 } 594 if (canonical.includes('claude-sonnet-4-5')) { 595 return has1m ? 'Sonnet 4.5 (with 1M context)' : 'Sonnet 4.5' 596 } 597 if (canonical.includes('claude-sonnet-4')) { 598 return has1m ? 'Sonnet 4 (with 1M context)' : 'Sonnet 4' 599 } 600 if (canonical.includes('claude-3-7-sonnet')) { 601 return 'Claude 3.7 Sonnet' 602 } 603 if (canonical.includes('claude-3-5-sonnet')) { 604 return 'Claude 3.5 Sonnet' 605 } 606 if (canonical.includes('claude-haiku-4-5')) { 607 return 'Haiku 4.5' 608 } 609 if (canonical.includes('claude-3-5-haiku')) { 610 return 'Claude 3.5 Haiku' 611 } 612 613 return undefined 614} 615 616export function normalizeModelStringForAPI(model: string): string { 617 return model.replace(/\[(1|2)m\]/gi, '') 618}