source dump of claude code
at main 540 lines 18 kB view raw
1// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered 2import { getInitialMainLoopModel } from '../../bootstrap/state.js' 3import { 4 isClaudeAISubscriber, 5 isMaxSubscriber, 6 isTeamPremiumSubscriber, 7} from '../auth.js' 8import { getModelStrings } from './modelStrings.js' 9import { 10 COST_TIER_3_15, 11 COST_HAIKU_35, 12 COST_HAIKU_45, 13 formatModelPricing, 14} from '../modelCost.js' 15import { getSettings_DEPRECATED } from '../settings/settings.js' 16import { checkOpus1mAccess, checkSonnet1mAccess } from './check1mAccess.js' 17import { getAPIProvider } from './providers.js' 18import { isModelAllowed } from './modelAllowlist.js' 19import { 20 getCanonicalName, 21 getClaudeAiUserDefaultModelDescription, 22 getDefaultSonnetModel, 23 getDefaultOpusModel, 24 getDefaultHaikuModel, 25 getDefaultMainLoopModelSetting, 26 getMarketingNameForModel, 27 getUserSpecifiedModelSetting, 28 isOpus1mMergeEnabled, 29 getOpus46PricingSuffix, 30 renderDefaultModelSetting, 31 type ModelSetting, 32} from './model.js' 33import { has1mContext } from '../context.js' 34import { getGlobalConfig } from '../config.js' 35 36// @[MODEL LAUNCH]: Update all the available and default model option strings below. 37 38export type ModelOption = { 39 value: ModelSetting 40 label: string 41 description: string 42 descriptionForModel?: string 43} 44 45export function getDefaultOptionForUser(fastMode = false): ModelOption { 46 if (process.env.USER_TYPE === 'ant') { 47 const currentModel = renderDefaultModelSetting( 48 getDefaultMainLoopModelSetting(), 49 ) 50 return { 51 value: null, 52 label: 'Default (recommended)', 53 description: `Use the default model for Ants (currently ${currentModel})`, 54 descriptionForModel: `Default model (currently ${currentModel})`, 55 } 56 } 57 58 // Subscribers 59 if (isClaudeAISubscriber()) { 60 return { 61 value: null, 62 label: 'Default (recommended)', 63 description: getClaudeAiUserDefaultModelDescription(fastMode), 64 } 65 } 66 67 // PAYG 68 const is3P = getAPIProvider() !== 'firstParty' 69 return { 70 value: null, 71 label: 'Default (recommended)', 72 description: `Use the default model (currently ${renderDefaultModelSetting(getDefaultMainLoopModelSetting())})${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`, 73 } 74} 75 76function getCustomSonnetOption(): ModelOption | undefined { 77 const is3P = getAPIProvider() !== 'firstParty' 78 const customSonnetModel = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL 79 // When a 3P user has a custom sonnet model string, show it directly 80 if (is3P && customSonnetModel) { 81 const is1m = has1mContext(customSonnetModel) 82 return { 83 value: 'sonnet', 84 label: 85 process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME ?? customSonnetModel, 86 description: 87 process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION ?? 88 `Custom Sonnet model${is1m ? ' (1M context)' : ''}`, 89 descriptionForModel: `${process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION ?? `Custom Sonnet model${is1m ? ' with 1M context' : ''}`} (${customSonnetModel})`, 90 } 91 } 92} 93 94// @[MODEL LAUNCH]: Update or add model option functions (getSonnetXXOption, getOpusXXOption, etc.) 95// with the new model's label and description. These appear in the /model picker. 96function getSonnet46Option(): ModelOption { 97 const is3P = getAPIProvider() !== 'firstParty' 98 return { 99 value: is3P ? getModelStrings().sonnet46 : 'sonnet', 100 label: 'Sonnet', 101 description: `Sonnet 4.6 · Best for everyday tasks${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`, 102 descriptionForModel: 103 'Sonnet 4.6 - best for everyday tasks. Generally recommended for most coding tasks', 104 } 105} 106 107function getCustomOpusOption(): ModelOption | undefined { 108 const is3P = getAPIProvider() !== 'firstParty' 109 const customOpusModel = process.env.ANTHROPIC_DEFAULT_OPUS_MODEL 110 // When a 3P user has a custom opus model string, show it directly 111 if (is3P && customOpusModel) { 112 const is1m = has1mContext(customOpusModel) 113 return { 114 value: 'opus', 115 label: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME ?? customOpusModel, 116 description: 117 process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION ?? 118 `Custom Opus model${is1m ? ' (1M context)' : ''}`, 119 descriptionForModel: `${process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION ?? `Custom Opus model${is1m ? ' with 1M context' : ''}`} (${customOpusModel})`, 120 } 121 } 122} 123 124function getOpus41Option(): ModelOption { 125 return { 126 value: 'opus', 127 label: 'Opus 4.1', 128 description: `Opus 4.1 · Legacy`, 129 descriptionForModel: 'Opus 4.1 - legacy version', 130 } 131} 132 133function getOpus46Option(fastMode = false): ModelOption { 134 const is3P = getAPIProvider() !== 'firstParty' 135 return { 136 value: is3P ? getModelStrings().opus46 : 'opus', 137 label: 'Opus', 138 description: `Opus 4.6 · Most capable for complex work${getOpus46PricingSuffix(fastMode)}`, 139 descriptionForModel: 'Opus 4.6 - most capable for complex work', 140 } 141} 142 143export function getSonnet46_1MOption(): ModelOption { 144 const is3P = getAPIProvider() !== 'firstParty' 145 return { 146 value: is3P ? getModelStrings().sonnet46 + '[1m]' : 'sonnet[1m]', 147 label: 'Sonnet (1M context)', 148 description: `Sonnet 4.6 for long sessions${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`, 149 descriptionForModel: 150 'Sonnet 4.6 with 1M context window - for long sessions with large codebases', 151 } 152} 153 154export function getOpus46_1MOption(fastMode = false): ModelOption { 155 const is3P = getAPIProvider() !== 'firstParty' 156 return { 157 value: is3P ? getModelStrings().opus46 + '[1m]' : 'opus[1m]', 158 label: 'Opus (1M context)', 159 description: `Opus 4.6 for long sessions${getOpus46PricingSuffix(fastMode)}`, 160 descriptionForModel: 161 'Opus 4.6 with 1M context window - for long sessions with large codebases', 162 } 163} 164 165function getCustomHaikuOption(): ModelOption | undefined { 166 const is3P = getAPIProvider() !== 'firstParty' 167 const customHaikuModel = process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL 168 // When a 3P user has a custom haiku model string, show it directly 169 if (is3P && customHaikuModel) { 170 return { 171 value: 'haiku', 172 label: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME ?? customHaikuModel, 173 description: 174 process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION ?? 175 'Custom Haiku model', 176 descriptionForModel: `${process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION ?? 'Custom Haiku model'} (${customHaikuModel})`, 177 } 178 } 179} 180 181function getHaiku45Option(): ModelOption { 182 const is3P = getAPIProvider() !== 'firstParty' 183 return { 184 value: 'haiku', 185 label: 'Haiku', 186 description: `Haiku 4.5 · Fastest for quick answers${is3P ? '' : ` · ${formatModelPricing(COST_HAIKU_45)}`}`, 187 descriptionForModel: 188 'Haiku 4.5 - fastest for quick answers. Lower cost but less capable than Sonnet 4.6.', 189 } 190} 191 192function getHaiku35Option(): ModelOption { 193 const is3P = getAPIProvider() !== 'firstParty' 194 return { 195 value: 'haiku', 196 label: 'Haiku', 197 description: `Haiku 3.5 for simple tasks${is3P ? '' : ` · ${formatModelPricing(COST_HAIKU_35)}`}`, 198 descriptionForModel: 199 'Haiku 3.5 - faster and lower cost, but less capable than Sonnet. Use for simple tasks.', 200 } 201} 202 203function getHaikuOption(): ModelOption { 204 // Return correct Haiku option based on provider 205 const haikuModel = getDefaultHaikuModel() 206 return haikuModel === getModelStrings().haiku45 207 ? getHaiku45Option() 208 : getHaiku35Option() 209} 210 211function getMaxOpusOption(fastMode = false): ModelOption { 212 return { 213 value: 'opus', 214 label: 'Opus', 215 description: `Opus 4.6 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`, 216 } 217} 218 219export function getMaxSonnet46_1MOption(): ModelOption { 220 const is3P = getAPIProvider() !== 'firstParty' 221 const billingInfo = isClaudeAISubscriber() ? ' · Billed as extra usage' : '' 222 return { 223 value: 'sonnet[1m]', 224 label: 'Sonnet (1M context)', 225 description: `Sonnet 4.6 with 1M context${billingInfo}${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`, 226 } 227} 228 229export function getMaxOpus46_1MOption(fastMode = false): ModelOption { 230 const billingInfo = isClaudeAISubscriber() ? ' · Billed as extra usage' : '' 231 return { 232 value: 'opus[1m]', 233 label: 'Opus (1M context)', 234 description: `Opus 4.6 with 1M context${billingInfo}${getOpus46PricingSuffix(fastMode)}`, 235 } 236} 237 238function getMergedOpus1MOption(fastMode = false): ModelOption { 239 const is3P = getAPIProvider() !== 'firstParty' 240 return { 241 value: is3P ? getModelStrings().opus46 + '[1m]' : 'opus[1m]', 242 label: 'Opus (1M context)', 243 description: `Opus 4.6 with 1M context · Most capable for complex work${!is3P && fastMode ? getOpus46PricingSuffix(fastMode) : ''}`, 244 descriptionForModel: 245 'Opus 4.6 with 1M context - most capable for complex work', 246 } 247} 248 249const MaxSonnet46Option: ModelOption = { 250 value: 'sonnet', 251 label: 'Sonnet', 252 description: 'Sonnet 4.6 · Best for everyday tasks', 253} 254 255const MaxHaiku45Option: ModelOption = { 256 value: 'haiku', 257 label: 'Haiku', 258 description: 'Haiku 4.5 · Fastest for quick answers', 259} 260 261function getOpusPlanOption(): ModelOption { 262 return { 263 value: 'opusplan', 264 label: 'Opus Plan Mode', 265 description: 'Use Opus 4.6 in plan mode, Sonnet 4.6 otherwise', 266 } 267} 268 269// @[MODEL LAUNCH]: Update the model picker lists below to include/reorder options for the new model. 270// Each user tier (ant, Max/Team Premium, Pro/Team Standard/Enterprise, PAYG 1P, PAYG 3P) has its own list. 271function getModelOptionsBase(fastMode = false): ModelOption[] { 272 if (process.env.USER_TYPE === 'ant') { 273 // Build options from antModels config 274 const antModelOptions: ModelOption[] = getAntModels().map(m => ({ 275 value: m.alias, 276 label: m.label, 277 description: m.description ?? `[ANT-ONLY] ${m.label} (${m.model})`, 278 })) 279 280 return [ 281 getDefaultOptionForUser(), 282 ...antModelOptions, 283 getMergedOpus1MOption(fastMode), 284 getSonnet46Option(), 285 getSonnet46_1MOption(), 286 getHaiku45Option(), 287 ] 288 } 289 290 if (isClaudeAISubscriber()) { 291 if (isMaxSubscriber() || isTeamPremiumSubscriber()) { 292 // Max and Team Premium users: Opus is default, show Sonnet as alternative 293 const premiumOptions = [getDefaultOptionForUser(fastMode)] 294 if (!isOpus1mMergeEnabled() && checkOpus1mAccess()) { 295 premiumOptions.push(getMaxOpus46_1MOption(fastMode)) 296 } 297 298 premiumOptions.push(MaxSonnet46Option) 299 if (checkSonnet1mAccess()) { 300 premiumOptions.push(getMaxSonnet46_1MOption()) 301 } 302 303 premiumOptions.push(MaxHaiku45Option) 304 return premiumOptions 305 } 306 307 // Pro/Team Standard/Enterprise users: Sonnet is default, show Opus as alternative 308 const standardOptions = [getDefaultOptionForUser(fastMode)] 309 if (checkSonnet1mAccess()) { 310 standardOptions.push(getMaxSonnet46_1MOption()) 311 } 312 313 if (isOpus1mMergeEnabled()) { 314 standardOptions.push(getMergedOpus1MOption(fastMode)) 315 } else { 316 standardOptions.push(getMaxOpusOption(fastMode)) 317 if (checkOpus1mAccess()) { 318 standardOptions.push(getMaxOpus46_1MOption(fastMode)) 319 } 320 } 321 322 standardOptions.push(MaxHaiku45Option) 323 return standardOptions 324 } 325 326 // PAYG 1P API: Default (Sonnet) + Sonnet 1M + Opus 4.6 + Opus 1M + Haiku 327 if (getAPIProvider() === 'firstParty') { 328 const payg1POptions = [getDefaultOptionForUser(fastMode)] 329 if (checkSonnet1mAccess()) { 330 payg1POptions.push(getSonnet46_1MOption()) 331 } 332 if (isOpus1mMergeEnabled()) { 333 payg1POptions.push(getMergedOpus1MOption(fastMode)) 334 } else { 335 payg1POptions.push(getOpus46Option(fastMode)) 336 if (checkOpus1mAccess()) { 337 payg1POptions.push(getOpus46_1MOption(fastMode)) 338 } 339 } 340 payg1POptions.push(getHaiku45Option()) 341 return payg1POptions 342 } 343 344 // 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 345 const payg3pOptions = [getDefaultOptionForUser(fastMode)] 346 347 const customSonnet = getCustomSonnetOption() 348 if (customSonnet !== undefined) { 349 payg3pOptions.push(customSonnet) 350 } else { 351 // Add Sonnet 4.6 since Sonnet 4.5 is the default 352 payg3pOptions.push(getSonnet46Option()) 353 if (checkSonnet1mAccess()) { 354 payg3pOptions.push(getSonnet46_1MOption()) 355 } 356 } 357 358 const customOpus = getCustomOpusOption() 359 if (customOpus !== undefined) { 360 payg3pOptions.push(customOpus) 361 } else { 362 // Add Opus 4.1, Opus 4.6 and Opus 4.6 1M 363 payg3pOptions.push(getOpus41Option()) // This is the default opus 364 payg3pOptions.push(getOpus46Option(fastMode)) 365 if (checkOpus1mAccess()) { 366 payg3pOptions.push(getOpus46_1MOption(fastMode)) 367 } 368 } 369 const customHaiku = getCustomHaikuOption() 370 if (customHaiku !== undefined) { 371 payg3pOptions.push(customHaiku) 372 } else { 373 payg3pOptions.push(getHaikuOption()) 374 } 375 return payg3pOptions 376} 377 378// @[MODEL LAUNCH]: Add the new model ID to the appropriate family pattern below 379// so the "newer version available" hint works correctly. 380/** 381 * Map a full model name to its family alias and the marketing name of the 382 * version the alias currently resolves to. Used to detect when a user has 383 * a specific older version pinned and a newer one is available. 384 */ 385function getModelFamilyInfo( 386 model: string, 387): { alias: string; currentVersionName: string } | null { 388 const canonical = getCanonicalName(model) 389 390 // Sonnet family 391 if ( 392 canonical.includes('claude-sonnet-4-6') || 393 canonical.includes('claude-sonnet-4-5') || 394 canonical.includes('claude-sonnet-4-') || 395 canonical.includes('claude-3-7-sonnet') || 396 canonical.includes('claude-3-5-sonnet') 397 ) { 398 const currentName = getMarketingNameForModel(getDefaultSonnetModel()) 399 if (currentName) { 400 return { alias: 'Sonnet', currentVersionName: currentName } 401 } 402 } 403 404 // Opus family 405 if (canonical.includes('claude-opus-4')) { 406 const currentName = getMarketingNameForModel(getDefaultOpusModel()) 407 if (currentName) { 408 return { alias: 'Opus', currentVersionName: currentName } 409 } 410 } 411 412 // Haiku family 413 if ( 414 canonical.includes('claude-haiku') || 415 canonical.includes('claude-3-5-haiku') 416 ) { 417 const currentName = getMarketingNameForModel(getDefaultHaikuModel()) 418 if (currentName) { 419 return { alias: 'Haiku', currentVersionName: currentName } 420 } 421 } 422 423 return null 424} 425 426/** 427 * Returns a ModelOption for a known Anthropic model with a human-readable 428 * label, and an upgrade hint if a newer version is available via the alias. 429 * Returns null if the model is not recognized. 430 */ 431function getKnownModelOption(model: string): ModelOption | null { 432 const marketingName = getMarketingNameForModel(model) 433 if (!marketingName) return null 434 435 const familyInfo = getModelFamilyInfo(model) 436 if (!familyInfo) { 437 return { 438 value: model, 439 label: marketingName, 440 description: model, 441 } 442 } 443 444 // Check if the alias currently resolves to a different (newer) version 445 if (marketingName !== familyInfo.currentVersionName) { 446 return { 447 value: model, 448 label: marketingName, 449 description: `Newer version available · select ${familyInfo.alias} for ${familyInfo.currentVersionName}`, 450 } 451 } 452 453 // Same version as the alias — just show the friendly name 454 return { 455 value: model, 456 label: marketingName, 457 description: model, 458 } 459} 460 461export function getModelOptions(fastMode = false): ModelOption[] { 462 const options = getModelOptionsBase(fastMode) 463 464 // Add the custom model from the ANTHROPIC_CUSTOM_MODEL_OPTION env var 465 const envCustomModel = process.env.ANTHROPIC_CUSTOM_MODEL_OPTION 466 if ( 467 envCustomModel && 468 !options.some(existing => existing.value === envCustomModel) 469 ) { 470 options.push({ 471 value: envCustomModel, 472 label: process.env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME ?? envCustomModel, 473 description: 474 process.env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION ?? 475 `Custom model (${envCustomModel})`, 476 }) 477 } 478 479 // Append additional model options fetched during bootstrap 480 for (const opt of getGlobalConfig().additionalModelOptionsCache ?? []) { 481 if (!options.some(existing => existing.value === opt.value)) { 482 options.push(opt) 483 } 484 } 485 486 // Add custom model from either the current model value or the initial one 487 // if it is not already in the options. 488 let customModel: ModelSetting = null 489 const currentMainLoopModel = getUserSpecifiedModelSetting() 490 const initialMainLoopModel = getInitialMainLoopModel() 491 if (currentMainLoopModel !== undefined && currentMainLoopModel !== null) { 492 customModel = currentMainLoopModel 493 } else if (initialMainLoopModel !== null) { 494 customModel = initialMainLoopModel 495 } 496 if (customModel === null || options.some(opt => opt.value === customModel)) { 497 return filterModelOptionsByAllowlist(options) 498 } else if (customModel === 'opusplan') { 499 return filterModelOptionsByAllowlist([...options, getOpusPlanOption()]) 500 } else if (customModel === 'opus' && getAPIProvider() === 'firstParty') { 501 return filterModelOptionsByAllowlist([ 502 ...options, 503 getMaxOpusOption(fastMode), 504 ]) 505 } else if (customModel === 'opus[1m]' && getAPIProvider() === 'firstParty') { 506 return filterModelOptionsByAllowlist([ 507 ...options, 508 getMergedOpus1MOption(fastMode), 509 ]) 510 } else { 511 // Try to show a human-readable label for known Anthropic models, with an 512 // upgrade hint if the alias now resolves to a newer version. 513 const knownOption = getKnownModelOption(customModel) 514 if (knownOption) { 515 options.push(knownOption) 516 } else { 517 options.push({ 518 value: customModel, 519 label: customModel, 520 description: 'Custom model', 521 }) 522 } 523 return filterModelOptionsByAllowlist(options) 524 } 525} 526 527/** 528 * Filter model options by the availableModels allowlist. 529 * Always preserves the "Default" option (value: null). 530 */ 531function filterModelOptionsByAllowlist(options: ModelOption[]): ModelOption[] { 532 const settings = getSettings_DEPRECATED() || {} 533 if (!settings.availableModels) { 534 return options // No restrictions 535 } 536 return options.filter( 537 opt => 538 opt.value === null || (opt.value !== null && isModelAllowed(opt.value)), 539 ) 540}