feat: add Opus 4.7 as default model and fix alias/thinking bugs (#928)

- Add CLAUDE_OPUS_4_7_CONFIG and register it in ALL_MODEL_CONFIGS
- Set Opus 4.7 as default for firstParty in getDefaultOpusModel() (3P stays on 4.6 until rollout)
- Fix sonnet[1m] → 404 bug: query.ts was passing raw alias to API without resolving via parseUserSpecifiedModel
- Add opus-4-7 to modelSupportsAdaptiveThinking so it uses { type: 'adaptive' } not { type: 'enabled' }
- Fix duplicate opus47 case and wrong opus46[1m] fallthrough in getPublicModelDisplayName switch
- Update user-facing display strings (picker labels, plan mode description) to reference Opus 4.7
- Add 3P fallback suggestion chain for opus-4-7 → opus-4-6 in validateModel

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
This commit is contained in:
Kevin Codex
2026-04-28 17:31:06 +08:00
committed by GitHub
parent 6ea3eb6483
commit 4c93a9f9f1
7 changed files with 62 additions and 18 deletions

View File

@@ -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),

View File

@@ -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<string, ModelConfig>
export type ModelKey = keyof typeof ALL_MODEL_CONFIGS

View File

@@ -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'
}

View File

@@ -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))

View File

@@ -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
}

View File

@@ -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<ModelShortName, ModelCosts> = {
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,
}
/**

View File

@@ -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)