diff --git a/src/query.ts b/src/query.ts index 5a028935..51ca1c8f 100644 --- a/src/query.ts +++ b/src/query.ts @@ -79,6 +79,7 @@ import { headlessProfilerCheckpoint } from './utils/headlessProfiler.js' import { getDefaultMainLoopModelSetting, getRuntimeMainLoopModel, + parseUserSpecifiedModel, renderModelName, } from './utils/model/model.js' import { @@ -624,7 +625,7 @@ async function* queryLoop( getDefaultMainLoopModelSetting() let currentModel = getRuntimeMainLoopModel({ permissionMode, - mainLoopModel: appStateMainLoopModel, + mainLoopModel: parseUserSpecifiedModel(appStateMainLoopModel), exceeds200kTokens: permissionMode === 'plan' && doesMostRecentAssistantMessageExceed200k(messagesForQuery), diff --git a/src/utils/model/configs.ts b/src/utils/model/configs.ts index 91a97e58..6cbab5d3 100644 --- a/src/utils/model/configs.ts +++ b/src/utils/model/configs.ts @@ -158,6 +158,19 @@ export const CLAUDE_OPUS_4_6_CONFIG = { minimax: 'MiniMax-M2.5', } as const satisfies ModelConfig +export const CLAUDE_OPUS_4_7_CONFIG = { + firstParty: 'claude-opus-4-7', + bedrock: 'us.anthropic.claude-opus-4-7-v1', + vertex: 'claude-opus-4-7', + foundry: 'claude-opus-4-7', + openai: 'gpt-4o', + gemini: 'gemini-2.5-pro', + github: 'github:copilot', + codex: 'gpt-5.5', + 'nvidia-nim': 'nvidia/llama-3.1-nemotron-70b-instruct', + minimax: 'MiniMax-M2.5', +} as const satisfies ModelConfig + export const CLAUDE_SONNET_4_6_CONFIG = { firstParty: 'claude-sonnet-4-6', bedrock: 'us.anthropic.claude-sonnet-4-6', @@ -184,6 +197,7 @@ export const ALL_MODEL_CONFIGS = { opus41: CLAUDE_OPUS_4_1_CONFIG, opus45: CLAUDE_OPUS_4_5_CONFIG, opus46: CLAUDE_OPUS_4_6_CONFIG, + opus47: CLAUDE_OPUS_4_7_CONFIG, } as const satisfies Record export type ModelKey = keyof typeof ALL_MODEL_CONFIGS diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index 4c966d88..519836f7 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -83,7 +83,8 @@ export function isNonCustomOpusModel(model: ModelName): boolean { model === getModelStrings().opus40 || model === getModelStrings().opus41 || model === getModelStrings().opus45 || - model === getModelStrings().opus46 + model === getModelStrings().opus46 || + model === getModelStrings().opus47 ) } @@ -204,12 +205,12 @@ export function getDefaultOpusModel(): ModelName { return process.env.OPENAI_MODEL || 'grok-4' } // 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. + // since 3P availability lags firstParty and these will diverge again at + // the next model launch. Keep 3P on Opus 4.6 until they roll out 4.7. if (getAPIProvider() !== 'firstParty') { return getModelStrings().opus46 } - return getModelStrings().opus46 + return getModelStrings().opus47 } // @[MODEL LAUNCH]: Update the default Sonnet model (3P providers may lag so keep defaults unchanged). @@ -407,7 +408,10 @@ export function getDefaultMainLoopModel(): ModelName { 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) + // Order matters: check more specific versions first (4-7 before 4-6 before 4-5 before 4) + if (name.includes('claude-opus-4-7')) { + return 'claude-opus-4-7' + } if (name.includes('claude-opus-4-6')) { return 'claude-opus-4-6' } @@ -478,9 +482,9 @@ export function getClaudeAiUserDefaultModelDescription( ): string { if (isMaxSubscriber() || isTeamPremiumSubscriber()) { if (isOpus1mMergeEnabled()) { - return `Opus 4.6 with 1M context · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}` + return `Opus 4.7 with 1M context · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}` } - return `Opus 4.6 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}` + return `Opus 4.7 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}` } return 'Sonnet 4.6 · Best for everyday tasks' } @@ -489,7 +493,7 @@ export function renderDefaultModelSetting( setting: ModelName | ModelAlias, ): string { if (setting === 'opusplan') { - return 'Opus 4.6 in plan mode, else Sonnet 4.6' + return 'Opus 4.7 in plan mode, else Sonnet 4.6' } return renderModelName(parseUserSpecifiedModel(setting)) } @@ -582,10 +586,14 @@ export function getPublicModelDisplayName(model: ModelName): string | null { return 'GPT-5.4' case 'gpt-5.3-codex-spark': return 'GPT-5.3 Codex Spark' - case getModelStrings().opus46: - return 'Opus 4.6' + case getModelStrings().opus47 + '[1m]': + return 'Opus 4.7 (1M context)' + case getModelStrings().opus47: + return 'Opus 4.7' case getModelStrings().opus46 + '[1m]': return 'Opus 4.6 (1M context)' + case getModelStrings().opus46: + return 'Opus 4.6' case getModelStrings().opus45: return 'Opus 4.5' case getModelStrings().opus41: @@ -825,6 +833,9 @@ export function getMarketingNameForModel(modelId: string): string | undefined { const has1m = modelId.toLowerCase().includes('[1m]') const canonical = getCanonicalName(modelId) + if (canonical.includes('claude-opus-4-7')) { + return has1m ? 'Opus 4.7 (with 1M context)' : 'Opus 4.7' + } if (canonical.includes('claude-opus-4-6')) { return has1m ? 'Opus 4.6 (with 1M context)' : 'Opus 4.6' } diff --git a/src/utils/model/modelOptions.ts b/src/utils/model/modelOptions.ts index b2cc785a..7acdcb2e 100644 --- a/src/utils/model/modelOptions.ts +++ b/src/utils/model/modelOptions.ts @@ -159,6 +159,16 @@ function getOpus41Option(): ModelOption { } } +function getOpus47Option(fastMode = false): ModelOption { + const is3P = getAPIProvider() !== 'firstParty' + return { + value: is3P ? getModelStrings().opus47 : 'opus', + label: 'Opus', + description: `Opus 4.7 · Most capable for complex work${getOpus46PricingSuffix(fastMode)}`, + descriptionForModel: 'Opus 4.7 - most capable for complex work', + } +} + function getOpus46Option(fastMode = false): ModelOption { const is3P = getAPIProvider() !== 'firstParty' return { @@ -241,7 +251,7 @@ function getMaxOpusOption(fastMode = false): ModelOption { return { value: 'opus', label: 'Opus', - description: `Opus 4.6 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`, + description: `Opus 4.7 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`, } } @@ -269,9 +279,9 @@ function getMergedOpus1MOption(fastMode = false): ModelOption { 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) : ''}`, + description: `${is3P ? 'Opus 4.6' : 'Opus 4.7'} with 1M context · Most capable for complex work${!is3P && fastMode ? getOpus46PricingSuffix(fastMode) : ''}`, descriptionForModel: - 'Opus 4.6 with 1M context - most capable for complex work', + `${is3P ? 'Opus 4.6' : 'Opus 4.7'} with 1M context - most capable for complex work`, } } @@ -291,7 +301,7 @@ function getOpusPlanOption(): ModelOption { return { value: 'opusplan', label: 'Opus Plan Mode', - description: 'Use Opus 4.6 in plan mode, Sonnet 4.6 otherwise', + description: 'Use Opus 4.7 in plan mode, Sonnet 4.6 otherwise', } } @@ -504,7 +514,7 @@ function getModelOptionsBase(fastMode = false): ModelOption[] { } } - // PAYG 1P API: Default (Sonnet) + Sonnet 1M + Opus 4.6 + Opus 1M + Haiku + // PAYG 1P API: Default (Sonnet) + Sonnet 1M + Opus 4.7 + Opus 4.6 + Opus 1M + Haiku if (getAPIProvider() === 'firstParty') { const payg1POptions = [getDefaultOptionForUser(fastMode)] if (checkSonnet1mAccess()) { @@ -513,6 +523,7 @@ function getModelOptionsBase(fastMode = false): ModelOption[] { if (isOpus1mMergeEnabled()) { payg1POptions.push(getMergedOpus1MOption(fastMode)) } else { + payg1POptions.push(getOpus47Option(fastMode)) payg1POptions.push(getOpus46Option(fastMode)) if (checkOpus1mAccess()) { payg1POptions.push(getOpus46_1MOption(fastMode)) @@ -546,8 +557,9 @@ function getModelOptionsBase(fastMode = false): ModelOption[] { if (customOpus !== undefined) { payg3pOptions.push(customOpus) } else { - // Add Opus 4.1, Opus 4.6 and Opus 4.6 1M + // Add Opus 4.1, Opus 4.7, Opus 4.6 and Opus 4.6 1M payg3pOptions.push(getOpus41Option()) // This is the default opus + payg3pOptions.push(getOpus47Option(fastMode)) payg3pOptions.push(getOpus46Option(fastMode)) if (checkOpus1mAccess()) { payg3pOptions.push(getOpus46_1MOption(fastMode)) diff --git a/src/utils/model/validateModel.ts b/src/utils/model/validateModel.ts index c8a1cd9e..b1c15301 100644 --- a/src/utils/model/validateModel.ts +++ b/src/utils/model/validateModel.ts @@ -202,6 +202,9 @@ function get3PFallbackSuggestion(model: string): string | undefined { return undefined } const lowerModel = model.toLowerCase() + if (lowerModel.includes('opus-4-7') || lowerModel.includes('opus_4_7')) { + return getModelStrings().opus46 + } if (lowerModel.includes('opus-4-6') || lowerModel.includes('opus_4_6')) { return getModelStrings().opus41 } diff --git a/src/utils/modelCost.ts b/src/utils/modelCost.ts index b4867d4e..7089ef65 100644 --- a/src/utils/modelCost.ts +++ b/src/utils/modelCost.ts @@ -11,6 +11,7 @@ import { CLAUDE_OPUS_4_1_CONFIG, CLAUDE_OPUS_4_5_CONFIG, CLAUDE_OPUS_4_6_CONFIG, + CLAUDE_OPUS_4_7_CONFIG, CLAUDE_OPUS_4_CONFIG, CLAUDE_SONNET_4_5_CONFIG, CLAUDE_SONNET_4_6_CONFIG, @@ -123,6 +124,8 @@ export const MODEL_COSTS: Record = { COST_TIER_5_25, [firstPartyNameToCanonical(CLAUDE_OPUS_4_6_CONFIG.firstParty)]: COST_TIER_5_25, + [firstPartyNameToCanonical(CLAUDE_OPUS_4_7_CONFIG.firstParty)]: + COST_TIER_5_25, } /** diff --git a/src/utils/thinking.ts b/src/utils/thinking.ts index 414c7f88..879ddba1 100644 --- a/src/utils/thinking.ts +++ b/src/utils/thinking.ts @@ -131,7 +131,7 @@ export function modelSupportsAdaptiveThinking(model: string): boolean { } const canonical = getCanonicalName(model) // Supported by a subset of Claude 4 models - if (canonical.includes('opus-4-6') || canonical.includes('sonnet-4-6')) { + if (canonical.includes('opus-4-7') || canonical.includes('opus-4-6') || canonical.includes('sonnet-4-6')) { return true } // Exclude any other known legacy models (allowlist above catches 4-6 variants first)