feat: 添加 model/provider 层改进

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
unraid
2026-04-22 22:38:10 +08:00
parent d208855f07
commit 23bb09d240
13 changed files with 689 additions and 472 deletions

View File

@@ -0,0 +1,148 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { resetModelStringsForTestingOnly } from 'src/bootstrap/state.js'
import {
resetSettingsCache,
setSessionSettingsCache,
} from 'src/utils/settings/settingsCache.js'
import { ALL_MODEL_CONFIGS } from '../configs.js'
import { getDefaultOpusModel } from '../model.js'
import { getOpus46Option } from '../modelOptions.js'
import { getModelStrings } from '../modelStrings.js'
/**
* Verifies getDefaultOpusModel() returns Opus 4.7 across all providers
* (firstParty + Bedrock/Vertex/Foundry). This is the Gap #2 assertion:
* as of 2026-04-17 all 3P vendors have published Opus 4.7, so the fork
* must not fall back to Opus 4.6 on 3P.
*
* Authoritative sources for 3P availability:
* - AWS Bedrock: docs.aws.amazon.com/bedrock/.../model-card-anthropic-claude-opus-4-7.html
* - Google Vertex AI: docs.cloud.google.com/vertex-ai/.../claude/opus-4-7
* - Microsoft Foundry: ai.azure.com/catalog/models/claude-opus-4-7
*/
const envKeys = [
'CLAUDE_CODE_USE_GEMINI',
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_VERTEX',
'CLAUDE_CODE_USE_FOUNDRY',
'CLAUDE_CODE_USE_OPENAI',
'CLAUDE_CODE_USE_GROK',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
'OPENAI_DEFAULT_OPUS_MODEL',
'GEMINI_DEFAULT_OPUS_MODEL',
] as const
const savedEnv: Record<string, string | undefined> = {}
function resetProviderState(): void {
resetSettingsCache()
setSessionSettingsCache({ settings: {}, errors: [] })
resetModelStringsForTestingOnly()
}
describe('getDefaultOpusModel', () => {
beforeEach(() => {
for (const key of envKeys) {
savedEnv[key] = process.env[key]
delete process.env[key]
}
resetProviderState()
})
afterEach(() => {
for (const key of envKeys) {
if (savedEnv[key] !== undefined) {
process.env[key] = savedEnv[key]
} else {
delete process.env[key]
}
}
resetProviderState()
})
test('returns Opus 4.7 for firstParty', () => {
expect(getDefaultOpusModel()).toBe(ALL_MODEL_CONFIGS.opus47.firstParty)
})
test('returns Opus 4.7 for bedrock (3P no longer lags)', () => {
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
expect(getDefaultOpusModel()).toBe(ALL_MODEL_CONFIGS.opus47.bedrock)
})
test('returns Opus 4.7 for vertex (3P no longer lags)', () => {
process.env.CLAUDE_CODE_USE_VERTEX = '1'
expect(getDefaultOpusModel()).toBe(ALL_MODEL_CONFIGS.opus47.vertex)
})
test('returns Opus 4.7 for foundry (3P no longer lags)', () => {
process.env.CLAUDE_CODE_USE_FOUNDRY = '1'
expect(getDefaultOpusModel()).toBe(ALL_MODEL_CONFIGS.opus47.foundry)
})
test('honors ANTHROPIC_DEFAULT_OPUS_MODEL env override (any provider)', () => {
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = 'claude-opus-4-1-custom'
expect(getDefaultOpusModel()).toBe('claude-opus-4-1-custom')
})
test('honors OPENAI_DEFAULT_OPUS_MODEL for openai provider', () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_DEFAULT_OPUS_MODEL = 'gpt-5-turbo'
expect(getDefaultOpusModel()).toBe('gpt-5-turbo')
})
})
/**
* Gap #3 addition — "Opus 4.6" must appear as an explicit opt-in option in
* the /model picker across all non-ANT user tiers. The option's value MUST
* be the canonical 4.6 model string, NOT the 'opus' alias (which would
* resolve via getDefaultOpusModel back to 4.7 on firstParty, silently
* defeating the user's explicit choice).
*/
describe('getOpus46Option', () => {
beforeEach(() => {
for (const key of envKeys) {
savedEnv[key] = process.env[key]
delete process.env[key]
}
resetProviderState()
})
afterEach(() => {
for (const key of envKeys) {
if (savedEnv[key] !== undefined) {
process.env[key] = savedEnv[key]
} else {
delete process.env[key]
}
}
resetProviderState()
})
test('firstParty: value is canonical opus46 string, NOT opus alias', () => {
const opt = getOpus46Option(false)
expect(opt.value).toBe(getModelStrings().opus46)
expect(opt.value).not.toBe('opus')
expect(opt.label).toBe('Opus 4.6')
})
test('firstParty: description says "Previous generation", not "Legacy"', () => {
const opt = getOpus46Option(false)
expect(opt.description).toContain('Previous generation')
expect(opt.description).not.toContain('Legacy')
})
test('bedrock: value is canonical opus46 string (unchanged behavior)', () => {
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
const opt = getOpus46Option(false)
expect(opt.value).toBe(getModelStrings().opus46)
expect(opt.value).toBe(ALL_MODEL_CONFIGS.opus46.bedrock)
})
test('option has descriptionForModel that mentions Opus 4.6', () => {
const opt = getOpus46Option(false)
expect(opt.descriptionForModel).toBeDefined()
expect(opt.descriptionForModel).toContain('Opus 4.6')
})
})

View File

@@ -106,6 +106,16 @@ export const CLAUDE_OPUS_4_6_CONFIG = {
grok: 'claude-opus-4-6',
} 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: 'claude-opus-4-7',
gemini: 'claude-opus-4-7',
grok: 'claude-opus-4-7',
} as const satisfies ModelConfig
export const CLAUDE_SONNET_4_6_CONFIG = {
firstParty: 'claude-sonnet-4-6',
bedrock: 'us.anthropic.claude-sonnet-4-6',
@@ -129,6 +139,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

@@ -28,18 +28,6 @@ import { getAPIProvider } from './providers.js'
import { LIGHTNING_BOLT } from '../../constants/figures.js'
import { isModelAllowed } from './modelAllowlist.js'
import { type ModelAlias, isModelAlias } from './aliases.js'
/**
* Returns true if the value is a model alias or a model alias with a suffix
* like [1m] (e.g. "opus", "opus[1m]", "sonnet", "haiku[1m]").
* Used to guard against infinite recursion when getDefault*Model() falls back
* to the user-specified setting — an alias like "opus[1m]" would cause
* parseUserSpecifiedModel → getDefaultOpusModel → parseUserSpecifiedModel loop.
*/
function isAliasOrAliasWithSuffix(value: string): boolean {
const base = value.replace(/\[1m\]$/i, '').trim()
return isModelAlias(base)
}
import { capitalize } from '../stringUtils.js'
export type ModelShortName = string
@@ -64,7 +52,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
)
}
@@ -138,21 +127,14 @@ export function getDefaultOpusModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) {
return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
}
// Fall back to user's configured model — custom providers may not
// recognize hardcoded Anthropic model IDs.
// Skip if the user setting is a model alias (e.g. "opus", "opus[1m]") to
// avoid infinite recursion: parseUserSpecifiedModel(alias) → getDefaultOpusModel().
const userSpecifiedOpus = getUserSpecifiedModelSetting()
if (userSpecifiedOpus && !isAliasOrAliasWithSuffix(userSpecifiedOpus)) {
return parseUserSpecifiedModel(userSpecifiedOpus)
}
// 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.
// 3P providers (Bedrock, Vertex, Foundry) all publish Opus 4.7 in sync
// with firstParty as of 2026-04-17 (AWS Bedrock, Google Vertex AI, and
// Microsoft Foundry announcements and model catalogs all confirm). The
// branch is kept as a structural hook in case a future launch lags on 3P.
if (provider !== 'firstParty') {
return getModelStrings().opus46
return getModelStrings().opus47
}
return getModelStrings().opus46
return getModelStrings().opus47
}
// @[MODEL LAUNCH]: Update the default Sonnet model (3P providers may lag so keep defaults unchanged).
@@ -173,14 +155,6 @@ export function getDefaultSonnetModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) {
return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
}
// Fall back to user's configured model (ANTHROPIC_MODEL / settings) —
// custom providers (proxies, national clouds) may not recognize the
// hardcoded Anthropic model IDs.
// Skip if the user setting is a model alias to avoid infinite recursion.
const userSpecified = getUserSpecifiedModelSetting()
if (userSpecified && !isAliasOrAliasWithSuffix(userSpecified)) {
return parseUserSpecifiedModel(userSpecified)
}
// Default to Sonnet 4.5 for 3P since they may not have 4.6 yet
if (provider !== 'firstParty') {
return getModelStrings().sonnet45
@@ -203,13 +177,6 @@ export function getDefaultHaikuModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) {
return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
}
// Fall back to user's configured model — custom providers may not
// recognize hardcoded Anthropic model IDs.
// Skip if the user setting is a model alias to avoid infinite recursion.
const userSpecifiedHaiku = getUserSpecifiedModelSetting()
if (userSpecifiedHaiku && !isAliasOrAliasWithSuffix(userSpecifiedHaiku)) {
return parseUserSpecifiedModel(userSpecifiedHaiku)
}
// Haiku 4.5 is available on all platforms (first-party, Foundry, Bedrock, Vertex)
return getModelStrings().haiku45
@@ -296,6 +263,9 @@ 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)
if (name.includes('claude-opus-4-7')) {
return 'claude-opus-4-7'
}
if (name.includes('claude-opus-4-6')) {
return 'claude-opus-4-6'
}
@@ -366,9 +336,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 ? getOpusPricingSuffix(true) : ''}`
}
return `Opus 4.6 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`
return `Opus 4.7 · Most capable for complex work${fastMode ? getOpusPricingSuffix(true) : ''}`
}
return 'Sonnet 4.6 · Best for everyday tasks'
}
@@ -377,12 +347,12 @@ 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))
}
export function getOpus46PricingSuffix(fastMode: boolean): string {
export function getOpusPricingSuffix(fastMode: boolean): string {
if (getAPIProvider() !== 'firstParty') return ''
const pricing = formatModelPricing(getOpus46CostTier(fastMode))
const fastModeIndicator = fastMode ? ` (${LIGHTNING_BOLT})` : ''
@@ -426,6 +396,10 @@ export function renderModelSetting(setting: ModelName | ModelAlias): string {
*/
export function getPublicModelDisplayName(model: ModelName): string | null {
switch (model) {
case getModelStrings().opus47:
return 'Opus 4.7'
case getModelStrings().opus47 + '[1m]':
return 'Opus 4.7 (1M context)'
case getModelStrings().opus46:
return 'Opus 4.6'
case getModelStrings().opus46 + '[1m]':
@@ -549,9 +523,10 @@ export function parseUserSpecifiedModel(
// Opus 4/4.1 are no longer available on the first-party API (same as
// Claude.ai) — silently remap to the current Opus default. The 'opus'
// alias already resolves to 4.6, so the only users on these explicit
// strings pinned them in settings/env/--model/SDK before 4.5 launched.
// 3P providers may not yet have 4.6 capacity, so pass through unchanged.
// alias resolves to the current default Opus (4.7), so the only users
// on these explicit strings pinned them in settings/env/--model/SDK
// before 4.5 launched. 3P providers may not yet have 4.6/4.7 capacity,
// so pass through unchanged.
if (
getAPIProvider() === 'firstParty' &&
isLegacyOpusFirstParty(modelString) &&
@@ -654,6 +629,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

@@ -44,7 +44,10 @@ function getCachePath(): string {
}
function isModelCapabilitiesEligible(): boolean {
if (process.env.USER_TYPE !== 'ant') return false
// Upstream gates this to ant-only, but the /v1/models API is available
// to all firstParty users (API key and OAuth). Enabling for everyone
// lets model capabilities (max_input_tokens, max_tokens) be fetched
// dynamically instead of relying on hardcoded values in context.ts.
if (getAPIProvider() !== 'firstParty') return false
if (!isFirstPartyAnthropicBaseUrl()) return false
return true

View File

@@ -27,7 +27,7 @@ import {
getMarketingNameForModel,
getUserSpecifiedModelSetting,
isOpus1mMergeEnabled,
getOpus46PricingSuffix,
getOpusPricingSuffix,
renderDefaultModelSetting,
type ModelSetting,
} from './model.js'
@@ -82,8 +82,8 @@ function getCustomSonnetOption(): ModelOption | undefined {
provider === 'openai'
? process.env.OPENAI_DEFAULT_SONNET_MODEL
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_SONNET_MODEL
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
? process.env.GEMINI_DEFAULT_SONNET_MODEL
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
// When a 3P user has a custom sonnet model string, show it directly
if (is3P && customSonnetModel) {
const is1m = has1mContext(customSonnetModel)
@@ -92,14 +92,14 @@ function getCustomSonnetOption(): ModelOption | undefined {
provider === 'openai'
? process.env.OPENAI_DEFAULT_SONNET_MODEL_NAME
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_SONNET_MODEL_NAME
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME
? process.env.GEMINI_DEFAULT_SONNET_MODEL_NAME
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME
const descEnv =
provider === 'openai'
? process.env.OPENAI_DEFAULT_SONNET_MODEL_DESCRIPTION
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_SONNET_MODEL_DESCRIPTION
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION
? process.env.GEMINI_DEFAULT_SONNET_MODEL_DESCRIPTION
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION
return {
value: 'sonnet',
label: nameEnv ?? customSonnetModel,
@@ -131,8 +131,8 @@ function getCustomOpusOption(): ModelOption | undefined {
provider === 'openai'
? process.env.OPENAI_DEFAULT_OPUS_MODEL
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_OPUS_MODEL
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
? process.env.GEMINI_DEFAULT_OPUS_MODEL
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
// When a 3P user has a custom opus model string, show it directly
if (is3P && customOpusModel) {
const is1m = has1mContext(customOpusModel)
@@ -141,14 +141,14 @@ function getCustomOpusOption(): ModelOption | undefined {
provider === 'openai'
? process.env.OPENAI_DEFAULT_OPUS_MODEL_NAME
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_OPUS_MODEL_NAME
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME
? process.env.GEMINI_DEFAULT_OPUS_MODEL_NAME
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME
const descEnv =
provider === 'openai'
? process.env.OPENAI_DEFAULT_OPUS_MODEL_DESCRIPTION
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_OPUS_MODEL_DESCRIPTION
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION
? process.env.GEMINI_DEFAULT_OPUS_MODEL_DESCRIPTION
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION
return {
value: 'opus',
label: nameEnv ?? customOpusModel,
@@ -167,13 +167,27 @@ function getOpus41Option(): ModelOption {
}
}
function getOpus46Option(fastMode = false): ModelOption {
function getOpus47Option(fastMode = false): ModelOption {
const is3P = getAPIProvider() !== 'firstParty'
return {
value: is3P ? getModelStrings().opus46 : 'opus',
label: 'Opus',
description: `Opus 4.6 · Most capable for complex work${getOpus46PricingSuffix(fastMode)}`,
descriptionForModel: 'Opus 4.6 - most capable for complex work',
value: is3P ? getModelStrings().opus47 : 'opus',
label: 'Opus 4.7',
description: `Opus 4.7 · Most capable for complex work${getOpusPricingSuffix(fastMode)}`,
descriptionForModel: 'Opus 4.7 - most capable for complex work',
}
}
export function getOpus46Option(fastMode = false): ModelOption {
// Always use the canonical 4.6 model string (not the 'opus' alias, which
// resolves via getDefaultOpusModel() to opus47 on firstParty). Users
// selecting "Opus 4.6" must get 4.6 actually dispatched, not alias-routed
// to 4.7. The same string is correct for 3P (getModelStrings maps per
// provider).
return {
value: getModelStrings().opus46,
label: 'Opus 4.6',
description: `Opus 4.6 · Previous generation Opus${getOpusPricingSuffix(fastMode)}`,
descriptionForModel: 'Opus 4.6 - previous generation Opus model',
}
}
@@ -188,12 +202,22 @@ export function getSonnet46_1MOption(): ModelOption {
}
}
export function getOpus46_1MOption(fastMode = false): ModelOption {
export function getOpus47_1MOption(fastMode = false): ModelOption {
const is3P = getAPIProvider() !== 'firstParty'
return {
value: is3P ? getModelStrings().opus46 + '[1m]' : 'opus[1m]',
label: 'Opus (1M context)',
description: `Opus 4.6 for long sessions${getOpus46PricingSuffix(fastMode)}`,
value: is3P ? getModelStrings().opus47 + '[1m]' : 'opus[1m]',
label: 'Opus 4.7 (1M context)',
description: `Opus 4.7 with 1M context${getOpusPricingSuffix(fastMode)}`,
descriptionForModel:
'Opus 4.7 with 1M context window - for long sessions with large codebases',
}
}
export function getOpus46_1MOption(fastMode = false): ModelOption {
return {
value: getModelStrings().opus46 + '[1m]',
label: 'Opus 4.6 (1M context)',
description: `Opus 4.6 with 1M context${getOpusPricingSuffix(fastMode)}`,
descriptionForModel:
'Opus 4.6 with 1M context window - for long sessions with large codebases',
}
@@ -207,8 +231,8 @@ function getCustomHaikuOption(): ModelOption | undefined {
provider === 'openai'
? process.env.OPENAI_DEFAULT_HAIKU_MODEL
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_HAIKU_MODEL
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
? process.env.GEMINI_DEFAULT_HAIKU_MODEL
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
// When a 3P user has a custom haiku model string, show it directly
if (is3P && customHaikuModel) {
// Use appropriate NAME/DESCRIPTION env vars based on provider
@@ -216,14 +240,14 @@ function getCustomHaikuOption(): ModelOption | undefined {
provider === 'openai'
? process.env.OPENAI_DEFAULT_HAIKU_MODEL_NAME
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_HAIKU_MODEL_NAME
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME
? process.env.GEMINI_DEFAULT_HAIKU_MODEL_NAME
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME
const descEnv =
provider === 'openai'
? process.env.OPENAI_DEFAULT_HAIKU_MODEL_DESCRIPTION
: provider === 'gemini'
? process.env.GEMINI_DEFAULT_HAIKU_MODEL_DESCRIPTION
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION
? process.env.GEMINI_DEFAULT_HAIKU_MODEL_DESCRIPTION
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION
return {
value: 'haiku',
label: nameEnv ?? customHaikuModel,
@@ -266,8 +290,8 @@ function getHaikuOption(): ModelOption {
function getMaxOpusOption(fastMode = false): ModelOption {
return {
value: 'opus',
label: 'Opus',
description: `Opus 4.6 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`,
label: 'Opus 4.7',
description: `Opus 4.7 · Most capable for complex work${fastMode ? getOpusPricingSuffix(true) : ''}`,
}
}
@@ -281,23 +305,23 @@ export function getMaxSonnet46_1MOption(): ModelOption {
}
}
export function getMaxOpus46_1MOption(fastMode = false): ModelOption {
export function getMaxOpus47_1MOption(fastMode = false): ModelOption {
const billingInfo = isClaudeAISubscriber() ? ' · Billed as extra usage' : ''
return {
value: 'opus[1m]',
label: 'Opus (1M context)',
description: `Opus 4.6 with 1M context${billingInfo}${getOpus46PricingSuffix(fastMode)}`,
label: 'Opus 4.7 (1M context)',
description: `Opus 4.7 with 1M context${billingInfo}${getOpusPricingSuffix(fastMode)}`,
}
}
function getMergedOpus1MOption(fastMode = false): ModelOption {
const is3P = getAPIProvider() !== 'firstParty'
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) : ''}`,
value: is3P ? getModelStrings().opus47 + '[1m]' : 'opus[1m]',
label: 'Opus 4.7 (1M context)',
description: `Opus 4.7 with 1M context · Most capable for complex work${!is3P && fastMode ? getOpusPricingSuffix(fastMode) : ''}`,
descriptionForModel:
'Opus 4.6 with 1M context - most capable for complex work',
'Opus 4.7 with 1M context - most capable for complex work',
}
}
@@ -317,7 +341,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',
}
}
@@ -344,11 +368,9 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
if (isClaudeAISubscriber()) {
if (isMaxSubscriber() || isTeamPremiumSubscriber()) {
// Max and Team Premium users: Opus is default, show Sonnet as alternative
// Max and Team Premium users: Default = Opus 4.7 1M (merged), plus Opus 4.6 1M
const premiumOptions = [getDefaultOptionForUser(fastMode)]
if (!isOpus1mMergeEnabled() && checkOpus1mAccess()) {
premiumOptions.push(getMaxOpus46_1MOption(fastMode))
}
premiumOptions.push(getOpus46_1MOption(fastMode))
premiumOptions.push(MaxSonnet46Option)
if (checkSonnet1mAccess()) {
@@ -359,44 +381,47 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
return premiumOptions
}
// Pro/Team Standard/Enterprise users: Sonnet is default, show Opus as alternative
// Pro/Team Standard/Enterprise users: Sonnet is default, show Opus 4.7 1M + Opus 4.6 1M
const standardOptions = [getDefaultOptionForUser(fastMode)]
if (checkSonnet1mAccess()) {
standardOptions.push(getMaxSonnet46_1MOption())
}
if (isOpus1mMergeEnabled()) {
standardOptions.push(getMergedOpus1MOption(fastMode))
} else {
standardOptions.push(getMaxOpusOption(fastMode))
if (checkOpus1mAccess()) {
standardOptions.push(getMaxOpus46_1MOption(fastMode))
standardOptions.push(getMaxOpus47_1MOption(fastMode))
}
}
standardOptions.push(getOpus46_1MOption(fastMode))
if (checkSonnet1mAccess()) {
standardOptions.push(getMaxSonnet46_1MOption())
}
standardOptions.push(MaxHaiku45Option)
return standardOptions
}
// PAYG 1P API: Default (Sonnet) + Sonnet 1M + Opus 4.6 + Opus 1M + Haiku
// PAYG 1P API: Default (Sonnet) + Opus 4.7 1M + Opus 4.6 1M + Sonnet 1M + Haiku
if (getAPIProvider() === 'firstParty') {
const payg1POptions = [getDefaultOptionForUser(fastMode)]
if (checkSonnet1mAccess()) {
payg1POptions.push(getSonnet46_1MOption())
}
if (isOpus1mMergeEnabled()) {
payg1POptions.push(getMergedOpus1MOption(fastMode))
} else {
payg1POptions.push(getOpus46Option(fastMode))
payg1POptions.push(getOpus47Option(fastMode))
if (checkOpus1mAccess()) {
payg1POptions.push(getOpus46_1MOption(fastMode))
payg1POptions.push(getOpus47_1MOption(fastMode))
}
}
payg1POptions.push(getOpus46_1MOption(fastMode))
if (checkSonnet1mAccess()) {
payg1POptions.push(getSonnet46_1MOption())
}
payg1POptions.push(getHaiku45Option())
return payg1POptions
}
// 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
// PAYG 3P: Default (Sonnet 4.5) + Sonnet (3P custom) or Sonnet 4.6/1M + Opus (3P custom) or Opus 4.7/Opus 4.6 Legacy/Opus 4.7 1M + Haiku
const payg3pOptions = [getDefaultOptionForUser(fastMode)]
const customSonnet = getCustomSonnetOption()
@@ -414,12 +439,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
payg3pOptions.push(getOpus41Option()) // This is the default opus
payg3pOptions.push(getOpus46Option(fastMode))
if (checkOpus1mAccess()) {
payg3pOptions.push(getOpus46_1MOption(fastMode))
}
// Add Opus 4.7 1M + Opus 4.6 1M (no redundant non-1M entries)
payg3pOptions.push(getOpus47_1MOption(fastMode))
payg3pOptions.push(getOpus46_1MOption(fastMode))
}
const customHaiku = getCustomHaikuOption()
if (customHaiku !== undefined) {

View File

@@ -4,6 +4,7 @@ import { getAPIProvider } from './providers.js'
export type ModelCapabilityOverride =
| 'effort'
| 'max_effort'
| 'xhigh_effort'
| 'thinking'
| 'adaptive_thinking'
| 'interleaved_thinking'

View File

@@ -146,6 +146,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
}