fix(models): expose current Claude official lineup

This commit is contained in:
程序员阿江(Relakkes)
2026-07-22 17:42:19 +08:00
parent 91829e91a4
commit 29b17c8028
49 changed files with 845 additions and 154 deletions
+2 -2
View File
@@ -271,8 +271,8 @@ describe('Telegram command controller helpers', () => {
await controller.handleProviderCommand(allowed.ctx)
expect(deps.httpClient.activateOfficialProvider).toHaveBeenCalled()
expect(deps.httpClient.setCurrentModel).toHaveBeenCalledWith('claude-opus-4-7')
expect(runtimeModels).toEqual(['claude-opus-4-7'])
expect(deps.httpClient.setCurrentModel).toHaveBeenCalledWith('claude-opus-4-8')
expect(runtimeModels).toEqual(['claude-opus-4-8'])
expect(allowed.replies[0]).toContain('Claude 官方')
const denied = createCommandContext({ userId: 999 })
+1 -1
View File
@@ -16,7 +16,7 @@ import {
export const TELEGRAM_SELECTION_TTL_MS = 15 * 60 * 1000
export const OFFICIAL_PROVIDER_VALUE = 'official'
export const OPENAI_OFFICIAL_PROVIDER_ID = 'openai-official'
export const OFFICIAL_DEFAULT_MODEL_ID = 'claude-opus-4-7'
export const OFFICIAL_DEFAULT_MODEL_ID = 'claude-opus-4-8'
export const OPENAI_OFFICIAL_DEFAULT_MODEL_ID = 'gpt-5.3-codex'
type TelegramSendApi = {
@@ -1906,6 +1906,20 @@ describe('Settings > Providers tab', () => {
expect(providerStoreState.testConfig).not.toHaveBeenCalledWith(expect.objectContaining({
modelId: 'deepseek-v4-pro',
}))
fireEvent.click(within(dialog).getByRole('button', { name: /Save|Add|保存|添加/i }))
await waitFor(() => {
expect(providerStoreState.createProvider).toHaveBeenCalledWith(expect.objectContaining({
models: expect.objectContaining({
fable: 'Qwen3Coder',
main: 'claude-sonnet-4-6',
haiku: 'claude-haiku-4-5',
sonnet: 'claude-sonnet-4-6',
opus: 'claude-opus-4-8',
}),
}))
})
})
it('keeps the provider form locked while save is in flight', async () => {
@@ -43,6 +43,44 @@ beforeEach(() => {
})
describe('ModelSelector', () => {
it('keeps the current Claude Official catalog visible when the API returns legacy settings models', async () => {
const legacyModels: ModelInfo[] = [
{ id: 'claude-opus-4-7', name: 'Opus 4.7', description: 'Legacy Opus', context: '1m' },
{ id: 'claude-sonnet-4-6', name: 'Sonnet 4.6', description: 'Legacy Sonnet', context: '200k' },
{ id: 'claude-haiku-4-5', name: 'Haiku 4.5', description: 'Legacy Haiku', context: '200k' },
]
useHahaOAuthStore.setState({
status: {
loggedIn: true,
expiresAt: null,
scopes: [],
subscriptionType: 'pro',
},
fetchStatus: async () => {},
})
useSettingsStore.setState({
locale: 'en',
availableModels: legacyModels,
currentModel: legacyModels[0],
activeProviderName: 'Claude Official',
})
useProviderStore.setState({
providers: [],
activeId: null,
hasLoadedProviders: true,
isLoading: false,
})
render(<ModelSelector runtimeKey="session-claude-legacy" />)
await clickByRole(/Opus 4\.7/i)
expect(screen.getByRole('button', { name: /Fable 5/ })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /Opus 4\.8/ })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /Sonnet 5/ })).toBeInTheDocument()
expect(screen.getAllByRole('button', { name: /Opus 4\.7/ }).length).toBeGreaterThan(0)
})
it('does not query official OAuth status when mounted', () => {
const fetchClaudeStatus = vi.fn(async () => {})
const fetchOpenAIStatus = vi.fn(async () => {})
@@ -76,6 +76,18 @@ function officialChoices(
}
}
function mergeOfficialModels(availableModels: ModelInfo[]): ModelInfo[] {
const merged = [...OFFICIAL_MODELS]
const knownIds = new Set(merged.map(model => model.id))
for (const model of availableModels) {
if (!knownIds.has(model.id)) {
knownIds.add(model.id)
merged.push(model)
}
}
return merged
}
function buildProviderModels(
provider: SavedProvider,
labels: Record<'main' | 'haiku' | 'sonnet' | 'opus', string>,
@@ -121,7 +133,7 @@ function buildProviderChoices(
grokOfficialLoggedIn: boolean,
): ProviderChoice[] {
const claudeOfficialModels = activeId === null && availableModels.length > 0
? availableModels
? mergeOfficialModels(availableModels)
: OFFICIAL_MODELS
const openAIOfficialModels = activeId === OPENAI_OFFICIAL_PROVIDER_ID && availableModels.length > 0
? availableModels
+19 -9
View File
@@ -1,24 +1,34 @@
import type { ModelInfo } from '../types/settings'
export const OFFICIAL_DEFAULT_MODEL_ID = 'claude-opus-4-7'
export const OFFICIAL_DEFAULT_MODEL_ID = 'claude-opus-4-8'
export const OFFICIAL_MODELS: ModelInfo[] = [
{
id: 'claude-opus-4-7',
name: 'Opus 4.7',
description: 'Most capable for ambitious work',
id: 'claude-fable-5',
name: 'Fable 5',
description: 'Highest capability for long-running tasks',
context: '1m',
},
{
id: 'claude-sonnet-4-6',
name: 'Sonnet 4.6',
description: 'Most efficient for everyday tasks',
context: '200k',
id: 'claude-opus-4-8',
name: 'Opus 4.8',
description: 'Best for complex agentic coding and enterprise work',
context: '1m',
defaultReasoningEffort: 'high',
supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
},
{
id: 'claude-sonnet-5',
name: 'Sonnet 5',
description: 'Best combination of speed and intelligence',
context: '1m',
defaultReasoningEffort: 'high',
supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
},
{
id: 'claude-haiku-4-5',
name: 'Haiku 4.5',
description: 'Fastest for quick answers',
description: 'Fastest with near-frontier intelligence',
context: '200k',
},
]
@@ -67,6 +67,8 @@ describe('provider settings JSON helpers', () => {
ANTHROPIC_AUTH_TOKEN: 'old-auth-token',
ANTHROPIC_BASE_URL: 'https://old.example.com',
ANTHROPIC_MODEL: 'old-model',
ANTHROPIC_DEFAULT_FABLE_MODEL: 'old-fable',
ANTHROPIC_DEFAULT_FABLE_MODEL_NAME: 'Old Fable',
CLAUDE_CODE_MODEL_CONTEXT_WINDOWS: '{"old":100000}',
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: '1',
CC_HAHA_OPENAI_OAUTH_PROVIDER: '1',
+4
View File
@@ -8,6 +8,10 @@ const PROVIDER_SETTINGS_JSON_ENV_KEYS = new Set([
'ANTHROPIC_API_KEY',
'ANTHROPIC_AUTH_TOKEN',
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_FABLE_MODEL_NAME',
'ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
+3 -2
View File
@@ -146,8 +146,9 @@ export const mockPermissionModes = [
]
export const mockModels = [
{ id: 'opus', name: 'Opus 4.7', active: false },
{ id: 'sonnet', name: 'Sonnet 4.6', active: true },
{ id: 'fable', name: 'Fable 5', active: false },
{ id: 'opus', name: 'Opus 4.8', active: true },
{ id: 'sonnet', name: 'Sonnet 5', active: false },
{ id: 'haiku', name: 'Haiku 4.5', active: false },
]
+1 -1
View File
@@ -408,7 +408,7 @@ export default function SessionControls() {
<span className="material-symbols-outlined text-base">
{modelIcons[selectedModel] || 'smart_toy'}
</span>
{activeModel?.name || 'Sonnet 4.6'}
{activeModel?.name || 'Opus 4.8'}
</button>
{/* Attach file button */}
+20 -1
View File
@@ -900,6 +900,7 @@ function stripModel1mMarker(model: string): string {
function stripModel1mMarkers(models: ModelMapping): ModelMapping {
return {
main: stripModel1mMarker(models.main),
...(models.fable ? { fable: stripModel1mMarker(models.fable) } : {}),
haiku: stripModel1mMarker(models.haiku),
sonnet: stripModel1mMarker(models.sonnet),
opus: stripModel1mMarker(models.opus),
@@ -929,6 +930,7 @@ function applyModel1mSupportMapping(
): ModelMapping {
return {
main: applyModel1mSupport(models.main, model1mSupport.main),
...(models.fable ? { fable: stripModel1mMarker(models.fable) } : {}),
haiku: applyModel1mSupport(models.haiku, model1mSupport.haiku),
sonnet: applyModel1mSupport(models.sonnet, model1mSupport.sonnet),
opus: applyModel1mSupport(models.opus, model1mSupport.opus),
@@ -968,6 +970,7 @@ function normalizeModelMapping(models: ModelMapping): ModelMapping {
const main = models.main.trim()
return {
main,
...(models.fable?.trim() ? { fable: models.fable.trim() } : {}),
haiku: models.haiku.trim() || main,
sonnet: models.sonnet.trim() || main,
opus: models.opus.trim() || main,
@@ -982,6 +985,14 @@ function readSettingsEnvString(env: Record<string, unknown>, key: string): strin
}
function readModelMappingFromSettingsEnv(env: Record<string, unknown>): Partial<ModelMapping> {
const hasModelEnv = [
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
].some((key) => Object.prototype.hasOwnProperty.call(env, key))
const fable = readSettingsEnvString(env, 'ANTHROPIC_DEFAULT_FABLE_MODEL')
const haiku = readSettingsEnvString(env, 'ANTHROPIC_DEFAULT_HAIKU_MODEL')
const sonnet = readSettingsEnvString(env, 'ANTHROPIC_DEFAULT_SONNET_MODEL')
const opus = readSettingsEnvString(env, 'ANTHROPIC_DEFAULT_OPUS_MODEL')
@@ -989,6 +1000,7 @@ function readModelMappingFromSettingsEnv(env: Record<string, unknown>): Partial<
return {
...(main ? { main } : {}),
...(hasModelEnv ? { fable } : {}),
...(haiku ? { haiku } : {}),
...(sonnet ? { sonnet } : {}),
...(opus ? { opus } : {}),
@@ -1133,9 +1145,15 @@ function updateSettingsJsonModels(
? parsed.env
: {}
const runtimeModels = applyModel1mSupportMapping(models, model1mSupport)
const env = { ...existingEnv }
delete env.ANTHROPIC_DEFAULT_FABLE_MODEL
delete env.ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION
delete env.ANTHROPIC_DEFAULT_FABLE_MODEL_NAME
delete env.ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES
parsed.env = {
...existingEnv,
...env,
ANTHROPIC_MODEL: runtimeModels.main,
...(runtimeModels.fable ? { ANTHROPIC_DEFAULT_FABLE_MODEL: runtimeModels.fable } : {}),
ANTHROPIC_DEFAULT_HAIKU_MODEL: runtimeModels.haiku,
ANTHROPIC_DEFAULT_SONNET_MODEL: runtimeModels.sonnet,
ANTHROPIC_DEFAULT_OPUS_MODEL: runtimeModels.opus,
@@ -1282,6 +1300,7 @@ function ProviderFormModal({ open, onClose, mode, provider, presets }: ProviderF
ANTHROPIC_BASE_URL: needsProxy ? providerProxyBaseUrl : baseUrl,
...buildSettingsJsonAuthEnv(apiFormat, authStrategy, apiKey, selectedPreset),
ANTHROPIC_MODEL: runtimeModels.main,
...(runtimeModels.fable ? { ANTHROPIC_DEFAULT_FABLE_MODEL: runtimeModels.fable } : {}),
ANTHROPIC_DEFAULT_HAIKU_MODEL: runtimeModels.haiku,
ANTHROPIC_DEFAULT_SONNET_MODEL: runtimeModels.sonnet,
ANTHROPIC_DEFAULT_OPUS_MODEL: runtimeModels.opus,
+1
View File
@@ -13,6 +13,7 @@ export type ProviderRuntimeKind = 'anthropic_compatible' | 'openai_oauth' | 'gro
export type ModelMapping = {
main: string
fable?: string
haiku: string
sonnet: string
opus: string
+1
View File
@@ -2,6 +2,7 @@ import type { ApiFormat, ProviderAuthStrategy } from './provider'
export type ModelMapping = {
main: string
fable?: string
haiku: string
sonnet: string
opus: string
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+13 -6
View File
@@ -115,12 +115,13 @@ export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY =
'__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
// @[MODEL LAUNCH]: Update the latest frontier model.
const FRONTIER_MODEL_NAME = 'Claude Opus 4.7'
const FRONTIER_MODEL_NAME = 'Claude Opus 4.8'
// @[MODEL LAUNCH]: Update the model family IDs below to the latest in each tier.
const CLAUDE_4_5_OR_4_6_MODEL_IDS = {
opus: 'claude-opus-4-7',
sonnet: 'claude-sonnet-4-6',
const LATEST_CLAUDE_MODEL_IDS = {
fable: 'claude-fable-5',
opus: 'claude-opus-4-8',
sonnet: 'claude-sonnet-5',
haiku: 'claude-haiku-4-5',
}
@@ -693,7 +694,7 @@ export async function computeSimpleEnvInfo(
knowledgeCutoffMessage,
process.env.USER_TYPE === 'ant' && isUndercover()
? null
: `The most recent Claude model family is Claude 4.5/4.6. Model IDs — Opus 4.7: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.opus}', Sonnet 4.6: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.sonnet}', Haiku 4.5: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.haiku}'. When building AI applications, default to the latest and most capable Claude models.`,
: `The most recent Claude flagship models are Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5, and Claude Haiku 4.5. Model IDs — Fable 5: '${LATEST_CLAUDE_MODEL_IDS.fable}', Opus 4.8: '${LATEST_CLAUDE_MODEL_IDS.opus}', Sonnet 5: '${LATEST_CLAUDE_MODEL_IDS.sonnet}', Haiku 4.5: '${LATEST_CLAUDE_MODEL_IDS.haiku}'. When building AI applications, default to the latest and most capable Claude models.`,
process.env.USER_TYPE === 'ant' && isUndercover()
? null
: `Claude Code is available as a CLI in the terminal, desktop app (Mac/Windows), web app (claude.ai/code), and IDE extensions (VS Code, JetBrains).`,
@@ -712,7 +713,13 @@ export async function computeSimpleEnvInfo(
// @[MODEL LAUNCH]: Add a knowledge cutoff date for the new model.
function getKnowledgeCutoff(modelId: string): string | null {
const canonical = getCanonicalName(modelId)
if (canonical.includes('claude-sonnet-4-6')) {
if (
canonical.includes('claude-fable-5') ||
canonical.includes('claude-opus-4-8') ||
canonical.includes('claude-sonnet-5')
) {
return 'January 2026'
} else if (canonical.includes('claude-sonnet-4-6')) {
return 'August 2025'
} else if (canonical.includes('claude-opus-4-7')) {
return 'May 2025'
@@ -374,27 +374,28 @@ describe('Business Flow: Models & Effort', () => {
it('should return available fallback models', async () => {
const { data } = await api('GET', '/api/models')
expect(data.models.length).toBe(3)
expect(data.models.length).toBe(4)
const names = data.models.map((m: any) => m.name)
expect(names).toContain('Opus 4.7')
expect(names).toContain('Sonnet 4.6')
expect(names).toContain('Fable 5')
expect(names).toContain('Opus 4.8')
expect(names).toContain('Sonnet 5')
expect(names).toContain('Haiku 4.5')
})
it('should default to Opus model', async () => {
const { data } = await api('GET', '/api/models/current')
expect(data.model.id).toBe('claude-opus-4-7')
expect(data.model.id).toBe('claude-opus-4-8')
})
it('should switch to Opus 4.7', async () => {
it('should switch to Opus 4.8', async () => {
const { status } = await api('PUT', '/api/models/current', {
modelId: 'claude-opus-4-7',
modelId: 'claude-opus-4-8',
})
expect(status).toBe(200)
const { data } = await api('GET', '/api/models/current')
expect(data.model.id).toBe('claude-opus-4-7')
expect(data.model.name).toBe('Opus 4.7')
expect(data.model.id).toBe('claude-opus-4-8')
expect(data.model.name).toBe('Opus 4.8')
})
it('should switch to Haiku 4.5', async () => {
+2 -2
View File
@@ -205,8 +205,8 @@ describe('E2E: Full Flow', () => {
it('should list available models', async () => {
const { data } = await api('GET', '/api/models')
expect(data.models.length).toBe(3)
expect(data.models[0].name).toBe('Opus 4.7')
expect(data.models.length).toBe(4)
expect(data.models[0].name).toBe('Fable 5')
})
it('should switch model', async () => {
@@ -82,6 +82,7 @@ describe('providerRuntimeEnv', () => {
apiFormat: 'anthropic',
models: {
main: 'active-main',
fable: 'active-fable',
haiku: '',
sonnet: 'active-sonnet',
opus: '',
@@ -98,6 +99,9 @@ describe('providerRuntimeEnv', () => {
ANTHROPIC_AUTH_TOKEN: 'sk-active',
ENABLE_TOOL_SEARCH: 'true',
ANTHROPIC_MODEL: 'active-main',
ANTHROPIC_DEFAULT_FABLE_MODEL: 'active-fable',
ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES:
'thinking,effort,adaptive_thinking,xhigh_effort,max_effort',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'active-main',
ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES:
'thinking,effort,adaptive_thinking,xhigh_effort,max_effort',
+146 -5
View File
@@ -24,7 +24,17 @@ import {
primeKeychainCacheFromPrefetch,
} from '../../utils/secureStorage/macOsKeychainHelpers.js'
import type { OpenAIOAuthTokens } from '../../services/openaiAuth/types.js'
import { getModelOptions } from '../../utils/model/modelOptions.js'
import {
getMaxOpus46_1MOption,
getMaxSonnet46_1MOption,
getModelOptions,
getOpus46_1MOption,
getSonnet46_1MOption,
} from '../../utils/model/modelOptions.js'
import {
getDefaultMainLoopModelSetting,
parseUserSpecifiedModel,
} from '../../utils/model/model.js'
import {
getSettingsForSource,
updateSettingsForSource,
@@ -49,6 +59,8 @@ let originalAnthropicDefaultHaikuModel: string | undefined
let originalAnthropicDefaultSonnetModel: string | undefined
let originalAnthropicDefaultOpusModel: string | undefined
let originalAnthropicDefaultFableModel: string | undefined
let originalAnthropicDefaultFableModelName: string | undefined
let originalDisable1mContext: string | undefined
async function setup() {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-test-'))
@@ -68,6 +80,8 @@ async function setup() {
originalAnthropicDefaultSonnetModel = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
originalAnthropicDefaultOpusModel = process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
originalAnthropicDefaultFableModel = process.env.ANTHROPIC_DEFAULT_FABLE_MODEL
originalAnthropicDefaultFableModelName = process.env.ANTHROPIC_DEFAULT_FABLE_MODEL_NAME
originalDisable1mContext = process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT
process.env.CLAUDE_CONFIG_DIR = tmpDir
process.env.HOME = tmpDir
process.env.USERPROFILE = tmpDir
@@ -80,6 +94,8 @@ async function setup() {
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
delete process.env.ANTHROPIC_DEFAULT_FABLE_MODEL
delete process.env.ANTHROPIC_DEFAULT_FABLE_MODEL_NAME
delete process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT
clearKeychainCache()
primeKeychainCacheFromPrefetch(null)
clearOpenAIOAuthTokenCache()
@@ -171,6 +187,18 @@ async function teardown() {
delete process.env.ANTHROPIC_DEFAULT_FABLE_MODEL
}
if (originalAnthropicDefaultFableModelName !== undefined) {
process.env.ANTHROPIC_DEFAULT_FABLE_MODEL_NAME = originalAnthropicDefaultFableModelName
} else {
delete process.env.ANTHROPIC_DEFAULT_FABLE_MODEL_NAME
}
if (originalDisable1mContext !== undefined) {
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = originalDisable1mContext
} else {
delete process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT
}
await fs.rm(tmpDir, { recursive: true, force: true })
}
@@ -645,9 +673,36 @@ describe('Models API', () => {
expect(res.status).toBe(200)
const body = await res.json()
expect(body.models).toBeArray()
expect(body.models.length).toBe(3)
expect(body.models[0].id).toContain('claude')
expect(body.models).toEqual([
{
id: 'claude-fable-5',
name: 'Fable 5',
description: 'Highest capability for long-running tasks',
context: '1m',
},
{
id: 'claude-opus-4-8',
name: 'Opus 4.8',
description: 'Best for complex agentic coding and enterprise work',
context: '1m',
defaultReasoningEffort: 'high',
supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
},
{
id: 'claude-sonnet-5',
name: 'Sonnet 5',
description: 'Best combination of speed and intelligence',
context: '1m',
defaultReasoningEffort: 'high',
supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
},
{
id: 'claude-haiku-4-5',
name: 'Haiku 4.5',
description: 'Fastest with near-frontier intelligence',
context: '200k',
},
])
})
it('GET /api/models should merge env-configured provider models with saved OpenAI OAuth models', async () => {
@@ -731,7 +786,7 @@ describe('Models API', () => {
expect(res.status).toBe(200)
const body = await res.json()
expect(body.model.id).toBe('claude-opus-4-7')
expect(body.model.id).toBe('claude-opus-4-8')
})
it('GET /api/models/current should respect env-configured default model when no provider is active', async () => {
@@ -976,6 +1031,92 @@ describe('Model Options', () => {
beforeEach(setup)
afterEach(teardown)
it('defaults Anthropic API users to Opus 4.8 and exposes the current official options once', () => {
process.env.ANTHROPIC_API_KEY = 'test-api-key'
expect(getDefaultMainLoopModelSetting()).toBe('claude-opus-4-8')
const options = getModelOptions()
const values = options.map(option => option.value)
expect(options[0]?.description).toContain('Opus 4.8')
expect(options[0]?.description).toContain('$5')
expect(values).toContain('fable')
expect(values).toContain('sonnet')
expect(values).not.toContain('opus')
expect(values).not.toContain('opus[1m]')
})
it('keeps Anthropic-compatible third-party URLs on lag-safe defaults', () => {
process.env.ANTHROPIC_API_KEY = 'third-party-key'
process.env.ANTHROPIC_BASE_URL = 'https://api.deepseek.com/anthropic'
expect(getDefaultMainLoopModelSetting()).toBe('claude-sonnet-4-5-20250929')
expect(parseUserSpecifiedModel('best')).toBe('claude-opus-4-7')
expect(parseUserSpecifiedModel('fable')).toBe('claude-opus-4-7')
const options = getModelOptions()
const values = options.map(option => option.value)
expect(options[0]?.description).toContain('Sonnet 4.5')
expect(values).not.toContain('fable')
expect(values).not.toContain('claude-fable-5')
})
it('exposes a custom Fable alias only when a third-party provider configures it', () => {
process.env.ANTHROPIC_API_KEY = 'third-party-key'
process.env.ANTHROPIC_BASE_URL = 'https://api.deepseek.com/anthropic'
process.env.ANTHROPIC_DEFAULT_FABLE_MODEL = 'deepseek-v4-fable'
process.env.ANTHROPIC_DEFAULT_FABLE_MODEL_NAME = 'DeepSeek Fable'
expect(parseUserSpecifiedModel('fable')).toBe('deepseek-v4-fable')
expect(parseUserSpecifiedModel('best')).toBe('deepseek-v4-fable')
expect(getModelOptions()).toContainEqual(expect.objectContaining({
value: 'fable',
label: 'DeepSeek Fable',
}))
})
it('labels extended-context options with current first-party and conservative third-party names', () => {
process.env.ANTHROPIC_API_KEY = 'test-api-key'
expect(getSonnet46_1MOption().description).toContain('Sonnet 5')
expect(getOpus46_1MOption().description).toContain('Opus 4.8')
expect(getMaxSonnet46_1MOption().description).toContain('Sonnet 5')
expect(getMaxOpus46_1MOption().description).toContain('Opus 4.8')
process.env.ANTHROPIC_BASE_URL = 'https://api.deepseek.com/anthropic'
expect(getSonnet46_1MOption().description).toContain('Sonnet 4.6')
expect(getOpus46_1MOption().description).toContain('Opus 4.7')
})
it('does not offer redundant Sonnet 1M choices when extended context is disabled', () => {
process.env.ANTHROPIC_API_KEY = 'test-api-key'
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1'
expect(getModelOptions().map(option => option.value)).not.toContain('sonnet[1m]')
})
it('renders explicit current aliases and Fable model IDs with current marketing names', () => {
process.env.ANTHROPIC_API_KEY = 'test-api-key'
process.env.ANTHROPIC_MODEL = 'opus'
expect(getModelOptions().find(option => option.value === 'opus')?.description)
.toContain('Opus 4.8')
process.env.ANTHROPIC_MODEL = 'opus[1m]'
expect(getModelOptions().find(option => option.value === 'opus[1m]')?.description)
.toContain('Opus 4.8')
process.env.ANTHROPIC_MODEL = 'claude-fable-5'
expect(getModelOptions()).toContainEqual(expect.objectContaining({
value: 'claude-fable-5',
label: 'Fable 5',
description: 'claude-fable-5',
}))
})
it('should keep OpenAI OAuth models visible alongside env-configured provider models', () => {
process.env.ANTHROPIC_API_KEY = 'deepseek-key'
process.env.ANTHROPIC_BASE_URL = 'https://api.deepseek.com/anthropic'
+19 -9
View File
@@ -39,28 +39,38 @@ import { hahaGrokOAuthService } from '../services/hahaGrokOAuthService.js'
const DEFAULT_MODELS = [
{
id: 'claude-opus-4-7',
name: 'Opus 4.7',
description: 'Most capable for ambitious work',
id: 'claude-fable-5',
name: 'Fable 5',
description: 'Highest capability for long-running tasks',
context: '1m',
},
{
id: 'claude-sonnet-4-6',
name: 'Sonnet 4.6',
description: 'Most efficient for everyday tasks',
context: '200k',
id: 'claude-opus-4-8',
name: 'Opus 4.8',
description: 'Best for complex agentic coding and enterprise work',
context: '1m',
defaultReasoningEffort: 'high',
supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
},
{
id: 'claude-sonnet-5',
name: 'Sonnet 5',
description: 'Best combination of speed and intelligence',
context: '1m',
defaultReasoningEffort: 'high',
supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
},
{
id: 'claude-haiku-4-5',
name: 'Haiku 4.5',
description: 'Fastest for quick answers',
description: 'Fastest with near-frontier intelligence',
context: '200k',
},
] as const
const EFFORT_LEVELS = ['low', 'medium', 'high', 'max'] as const
const DEFAULT_MODEL = 'claude-opus-4-7'
const DEFAULT_MODEL = 'claude-opus-4-8'
const DEFAULT_EFFORT = 'max'
const settingsService = new SettingsService()
+1
View File
@@ -8,6 +8,7 @@ import { ApiFormatSchema, ProviderAuthStrategySchema } from '../types/provider.j
const ModelMappingSchema = z.object({
main: z.string(),
fable: z.string().optional(),
haiku: z.string(),
sonnet: z.string(),
opus: z.string(),
@@ -1336,6 +1336,10 @@ export class ConversationService {
'ANTHROPIC_AUTH_TOKEN',
'ENABLE_TOOL_SEARCH',
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_FABLE_MODEL_NAME',
'ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
@@ -1600,6 +1604,10 @@ export class ConversationService {
'ANTHROPIC_AUTH_TOKEN',
'ENABLE_TOOL_SEARCH',
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_FABLE_MODEL_NAME',
'ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
@@ -44,6 +44,7 @@ function isProviderModels(value: unknown): value is JsonObject {
return (
isRecord(value) &&
typeof value.main === 'string' &&
(value.fable === undefined || typeof value.fable === 'string') &&
typeof value.haiku === 'string' &&
typeof value.sonnet === 'string' &&
typeof value.opus === 'string'
@@ -311,6 +312,7 @@ function buildManagedSettingsForMigratedProvider(provider: JsonObject | undefine
ANTHROPIC_BASE_URL: baseUrl,
ANTHROPIC_AUTH_TOKEN: apiKey,
ANTHROPIC_MODEL: provider.models.main,
...(provider.models.fable ? { ANTHROPIC_DEFAULT_FABLE_MODEL: provider.models.fable } : {}),
ANTHROPIC_DEFAULT_HAIKU_MODEL: provider.models.haiku,
ANTHROPIC_DEFAULT_SONNET_MODEL: provider.models.sonnet,
ANTHROPIC_DEFAULT_OPUS_MODEL: provider.models.opus,
+13
View File
@@ -36,6 +36,10 @@ export const MANAGED_PROVIDER_ENV_KEYS = [
'ENABLE_TOOL_SEARCH',
'CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS',
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_FABLE_MODEL_NAME',
'ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
@@ -67,6 +71,7 @@ function isProviderModels(value: unknown): value is SavedProvider['models'] {
return (
isRecord(value) &&
typeof value.main === 'string' &&
(value.fable === undefined || typeof value.fable === 'string') &&
typeof value.haiku === 'string' &&
typeof value.sonnet === 'string' &&
typeof value.opus === 'string'
@@ -128,6 +133,7 @@ export function normalizeModelMapping(models: SavedProvider['models']): SavedPro
const main = models.main.trim()
return {
main,
...(models.fable?.trim() ? { fable: models.fable.trim() } : {}),
haiku: models.haiku.trim() || main,
sonnet: models.sonnet.trim() || main,
opus: models.opus.trim() || main,
@@ -159,6 +165,7 @@ function applyModel1mSupportMapping(
): SavedProvider['models'] {
return {
main: applyModel1mSupport(models.main, model1mSupport?.main),
...(models.fable ? { fable: models.fable.trim() } : {}),
haiku: applyModel1mSupport(models.haiku, model1mSupport?.haiku),
sonnet: applyModel1mSupport(models.sonnet, model1mSupport?.sonnet),
opus: applyModel1mSupport(models.opus, model1mSupport?.opus),
@@ -309,6 +316,9 @@ function getProviderCapabilityEnv(
if (provider.presetId === 'custom') {
const capabilities = getCustomProviderModelCapabilities(provider, models)
return {
...(models.fable
? { ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES: capabilities }
: {}),
ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES: capabilities,
ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES: capabilities,
ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES: capabilities,
@@ -409,6 +419,9 @@ export function buildProviderManagedEnv(
ANTHROPIC_BASE_URL: baseUrl,
...buildProviderAuthEnv(provider, presetDefaultEnv, needsProxy),
ANTHROPIC_MODEL: runtimeModels.main,
...(runtimeModels.fable && {
ANTHROPIC_DEFAULT_FABLE_MODEL: runtimeModels.fable,
}),
ANTHROPIC_DEFAULT_HAIKU_MODEL: runtimeModels.haiku,
ANTHROPIC_DEFAULT_SONNET_MODEL: runtimeModels.sonnet,
ANTHROPIC_DEFAULT_OPUS_MODEL: runtimeModels.opus,
+2
View File
@@ -2208,6 +2208,7 @@ export class SessionService {
// (e.g. "LongCat-2.0-Preview-LongCatAI" instead of "LongCat-2.0-Preview").
const providerEnvModelKeys = [
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
@@ -2280,6 +2281,7 @@ export class SessionService {
const matches: number[] = []
const providerEnvModelKeys = [
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
+1
View File
@@ -45,6 +45,7 @@ export type ProviderRuntimeKind = z.infer<typeof ProviderRuntimeKindSchema>
export const ModelMappingSchema = z.object({
main: z.string(),
fable: z.string().optional(),
haiku: z.string(),
sonnet: z.string(),
opus: z.string(),
+4 -4
View File
@@ -34,10 +34,10 @@ import typescriptClaudeApiToolUse from './claude-api/typescript/claude-api/tool-
// - claude-api/SKILL.md (Current Models pricing table)
// - claude-api/shared/models.md (full model catalog with legacy versions and alias mappings)
export const SKILL_MODEL_VARS = {
OPUS_ID: 'claude-opus-4-7',
OPUS_NAME: 'Claude Opus 4.7',
SONNET_ID: 'claude-sonnet-4-6',
SONNET_NAME: 'Claude Sonnet 4.6',
OPUS_ID: 'claude-opus-4-8',
OPUS_NAME: 'Claude Opus 4.8',
SONNET_ID: 'claude-sonnet-5',
SONNET_NAME: 'Claude Sonnet 5',
HAIKU_ID: 'claude-haiku-4-5',
HAIKU_NAME: 'Claude Haiku 4.5',
// Previous Sonnet ID — used in "do not append date suffixes" example in SKILL.md.
@@ -13,6 +13,9 @@ describe('provider managed env compatibility', () => {
expect(changed).toBe(true)
expect(env.CC_HAHA_SEND_DISABLED_THINKING).toBeUndefined()
expect(env.ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,max_effort',
)
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,max_effort',
)
+65
View File
@@ -1,6 +1,15 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { get3PModelCapabilityOverride } from '../model/modelSupportOverrides.js'
import { resolveSideQueryThinkingConfig } from '../sideQuery.js'
import {
getModelBetas,
modelSupportsAutoMode,
modelSupportsContextManagement,
modelSupportsISP,
modelSupportsStructuredOutputs,
shouldIncludeFirstPartyOnlyBetas,
shouldUseGlobalCacheScope,
} from '../betas.js'
import {
modelSupportsEffort,
modelSupportsMaxEffort,
@@ -15,6 +24,7 @@ import {
} from '../thinking.js'
describe('provider-aware thinking support', () => {
let originalApiKey: string | undefined
let originalBaseUrl: string | undefined
let originalFableModel: string | undefined
let originalFableCapabilities: string | undefined
@@ -26,6 +36,7 @@ describe('provider-aware thinking support', () => {
let originalExplicitDisabledThinking: string | undefined
beforeEach(() => {
originalApiKey = process.env.ANTHROPIC_API_KEY
originalBaseUrl = process.env.ANTHROPIC_BASE_URL
originalFableModel = process.env.ANTHROPIC_DEFAULT_FABLE_MODEL
originalFableCapabilities = process.env.ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES
@@ -36,12 +47,14 @@ describe('provider-aware thinking support', () => {
originalFoundry = process.env.CLAUDE_CODE_USE_FOUNDRY
originalExplicitDisabledThinking = process.env.CC_HAHA_SEND_DISABLED_THINKING
delete process.env.ANTHROPIC_API_KEY
delete process.env.CLAUDE_CODE_USE_BEDROCK
delete process.env.CLAUDE_CODE_USE_VERTEX
delete process.env.CLAUDE_CODE_USE_FOUNDRY
})
afterEach(() => {
restoreEnv('ANTHROPIC_API_KEY', originalApiKey)
restoreEnv('ANTHROPIC_BASE_URL', originalBaseUrl)
restoreEnv('ANTHROPIC_DEFAULT_FABLE_MODEL', originalFableModel)
restoreEnv('ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES', originalFableCapabilities)
@@ -52,9 +65,11 @@ describe('provider-aware thinking support', () => {
restoreEnv('CLAUDE_CODE_USE_FOUNDRY', originalFoundry)
restoreEnv('CC_HAHA_SEND_DISABLED_THINKING', originalExplicitDisabledThinking)
clearCapabilityCache()
clearBetaCache()
})
test('does not assume adaptive thinking for Anthropic-compatible third-party base URLs', () => {
process.env.ANTHROPIC_API_KEY = 'third-party-key'
process.env.ANTHROPIC_BASE_URL = 'https://api.jiekou.ai/anthropic'
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES
clearCapabilityCache()
@@ -63,6 +78,7 @@ describe('provider-aware thinking support', () => {
})
test('honors explicit provider capability overrides with no supported capabilities', () => {
process.env.ANTHROPIC_API_KEY = 'third-party-key'
process.env.ANTHROPIC_BASE_URL = 'https://api.jiekou.ai/anthropic'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'claude-sonnet-4-6'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES = 'none'
@@ -82,6 +98,23 @@ describe('provider-aware thinking support', () => {
expect(modelSupportsAdaptiveThinking('claude-sonnet-4-6')).toBe(true)
})
test('recognizes current first-party flagship model capabilities', () => {
process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com'
clearCapabilityCache()
for (const model of ['claude-fable-5', 'claude-opus-4-8', 'claude-sonnet-5']) {
expect(modelSupportsThinking(model)).toBe(true)
expect(modelSupportsAdaptiveThinking(model)).toBe(true)
expect(modelSupportsEffort(model)).toBe(true)
expect(modelSupportsMaxEffort(model)).toBe(true)
expect(modelSupportsISP(model)).toBe(true)
expect(modelSupportsContextManagement(model)).toBe(true)
expect(modelSupportsStructuredOutputs(model)).toBe(true)
}
expect(shouldIncludeFirstPartyOnlyBetas()).toBe(true)
expect(shouldUseGlobalCacheScope()).toBe(true)
})
test('normalizes Fable to required adaptive thinking', () => {
process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com'
delete process.env.ANTHROPIC_DEFAULT_FABLE_MODEL
@@ -95,6 +128,7 @@ describe('provider-aware thinking support', () => {
})
test('lets an explicit third-party capability declaration disable Fable thinking', () => {
process.env.ANTHROPIC_API_KEY = 'third-party-key'
process.env.ANTHROPIC_BASE_URL = 'https://provider.example.test/anthropic'
process.env.ANTHROPIC_DEFAULT_FABLE_MODEL = 'claude-fable-5'
process.env.ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES = 'effort'
@@ -115,6 +149,7 @@ describe('provider-aware thinking support', () => {
})
test('DeepSeek preset can follow the global thinking setting through capability overrides', () => {
process.env.ANTHROPIC_API_KEY = 'third-party-key'
process.env.ANTHROPIC_BASE_URL = 'https://api.deepseek.com/anthropic'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'deepseek-v4-pro'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES =
@@ -131,6 +166,7 @@ describe('provider-aware thinking support', () => {
})
test('MiniMax preset models declare adaptive thinking without effort passthrough', () => {
process.env.ANTHROPIC_API_KEY = 'third-party-key'
process.env.ANTHROPIC_BASE_URL = 'https://api.minimaxi.com/anthropic'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'MiniMax-M3[1m]'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES =
@@ -145,6 +181,7 @@ describe('provider-aware thinking support', () => {
})
test('Kimi K3 preset requires thinking and supports effort passthrough', () => {
process.env.ANTHROPIC_API_KEY = 'third-party-key'
process.env.ANTHROPIC_BASE_URL = 'https://api.kimi.com/coding/'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'k3'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES =
@@ -163,6 +200,7 @@ describe('provider-aware thinking support', () => {
})
test('third-party base URLs do not default unknown model names to effort support', () => {
process.env.ANTHROPIC_API_KEY = 'third-party-key'
process.env.ANTHROPIC_BASE_URL = 'https://api.kimi.com/coding/'
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES
clearCapabilityCache()
@@ -171,6 +209,27 @@ describe('provider-aware thinking support', () => {
expect(modelSupportsMaxEffort('kimi-k2.6')).toBe(false)
})
test('does not infer first-party effort or betas from current model names on third-party URLs', () => {
process.env.ANTHROPIC_API_KEY = 'third-party-key'
process.env.ANTHROPIC_BASE_URL = 'https://api.deepseek.com/anthropic'
delete process.env.ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES
clearCapabilityCache()
clearBetaCache()
for (const model of ['claude-fable-5', 'claude-opus-4-8', 'claude-sonnet-5']) {
expect(modelSupportsEffort(model)).toBe(false)
expect(modelSupportsMaxEffort(model)).toBe(false)
expect(modelSupportsISP(model)).toBe(false)
expect(modelSupportsContextManagement(model)).toBe(false)
expect(modelSupportsStructuredOutputs(model)).toBe(false)
expect(modelSupportsAutoMode(model)).toBe(false)
expect(getModelBetas(model)).not.toContain('interleaved-thinking-2025-05-14')
}
expect(shouldIncludeFirstPartyOnlyBetas()).toBe(false)
expect(shouldUseGlobalCacheScope()).toBe(false)
})
test('side queries inherit explicit disabled thinking for opted-in providers', () => {
delete process.env.CC_HAHA_SEND_DISABLED_THINKING
expect(resolveSideQueryThinkingConfig(undefined, 1024)).toBeUndefined()
@@ -195,3 +254,9 @@ function clearCapabilityCache() {
cache?: { clear?: () => void }
}).cache?.clear?.()
}
function clearBetaCache() {
;(getModelBetas as typeof getModelBetas & {
cache?: { clear?: () => void }
}).cache?.clear?.()
}
+2 -2
View File
@@ -69,13 +69,13 @@ export function getAttributionTexts(): AttributionTexts {
// @[MODEL LAUNCH]: Update the hardcoded fallback model name below (guards against codename leaks).
// For internal repos, use the real model name. For external repos,
// fall back to "Claude Opus 4.7" for unrecognized models to avoid leaking codenames.
// fall back to "Claude Opus 4.8" for unrecognized models to avoid leaking codenames.
const model = getMainLoopModel()
const isKnownPublicModel = getPublicModelDisplayName(model) !== null
const modelName =
isInternalModelRepoCached() || isKnownPublicModel
? getPublicModelName(model)
: 'Claude Opus 4.7'
: 'Claude Opus 4.8'
const defaultAttribution = `🤖 Generated with [Claude Code](${PRODUCT_URL})`
const defaultCommit = `Co-Authored-By: ${modelName} <noreply@anthropic.com>`
+24 -5
View File
@@ -10,7 +10,10 @@ import {
logEvent,
} from 'src/services/analytics/index.js'
import { getModelStrings } from 'src/utils/model/modelStrings.js'
import { getAPIProvider } from 'src/utils/model/providers.js'
import {
getAPIProvider,
isFirstPartyAnthropicBaseUrl,
} from 'src/utils/model/providers.js'
import {
getIsNonInteractiveSession,
preferThirdPartyAuthentication,
@@ -129,9 +132,14 @@ export function isAnthropicAuthEnabled(): boolean {
process.env.CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR
// Check if API key is from an external source (not managed by /login)
const { source: apiKeySource } = getAnthropicApiKeyWithSource({
skipRetrievingKeyFromApiKeyHelper: true,
})
let apiKeySource: ApiKeySource = 'none'
try {
;({ source: apiKeySource } = getAnthropicApiKeyWithSource({
skipRetrievingKeyFromApiKeyHelper: true,
}))
} catch {
apiKeySource = 'none'
}
const hasExternalApiKey =
apiKeySource === 'ANTHROPIC_API_KEY' || apiKeySource === 'apiKeyHelper'
@@ -1585,7 +1593,14 @@ async function checkAndRefreshOAuthTokenIfNeededImpl(
}
export function isClaudeAISubscriber(): boolean {
if (!isAnthropicAuthEnabled()) {
let anthropicAuthEnabled = false
try {
anthropicAuthEnabled = isAnthropicAuthEnabled()
} catch {
anthropicAuthEnabled = false
}
if (!anthropicAuthEnabled) {
return false
}
@@ -1623,6 +1638,10 @@ export function is1PApiCustomer(): boolean {
return false
}
if (!isFirstPartyAnthropicBaseUrl()) {
return false
}
// Exclude Claude.ai subscribers
if (isClaudeAISubscriber()) {
return false
+32 -2
View File
@@ -89,6 +89,14 @@ export function filterAllowedSdkBetas(
// Generally, foundry supports all 1P features;
// however out of an abundance of caution, we do not enable any which are behind an experiment
function shouldTrustBuiltInClaudeCapabilities(): boolean {
return (
getAPIProvider() !== 'firstParty' ||
isFirstPartyAnthropicBaseUrl() ||
isClaudeAISubscriber()
)
}
export function modelSupportsISP(model: string): boolean {
const supported3P = get3PModelCapabilityOverride(
model,
@@ -104,6 +112,9 @@ export function modelSupportsISP(model: string): boolean {
return true
}
if (provider === 'firstParty') {
if (!shouldTrustBuiltInClaudeCapabilities()) {
return false
}
return !canonical.includes('claude-3-')
}
return (
@@ -129,9 +140,14 @@ export function modelSupportsContextManagement(model: string): boolean {
return true
}
if (provider === 'firstParty') {
if (!shouldTrustBuiltInClaudeCapabilities()) {
return false
}
return !canonical.includes('claude-3-')
}
return (
canonical.includes('claude-fable-5') ||
canonical.includes('claude-sonnet-5') ||
canonical.includes('claude-opus-4') ||
canonical.includes('claude-sonnet-4') ||
canonical.includes('claude-haiku-4')
@@ -146,11 +162,17 @@ export function modelSupportsStructuredOutputs(model: string): boolean {
if (provider !== 'firstParty' && provider !== 'foundry') {
return false
}
if (provider === 'firstParty' && !shouldTrustBuiltInClaudeCapabilities()) {
return false
}
return (
canonical.includes('claude-fable-5') ||
canonical.includes('claude-sonnet-5') ||
canonical.includes('claude-sonnet-4-6') ||
canonical.includes('claude-sonnet-4-5') ||
canonical.includes('claude-opus-4-1') ||
canonical.includes('claude-opus-4-5') ||
canonical.includes('claude-opus-4-8') ||
canonical.includes('claude-opus-4-7') ||
canonical.includes('claude-haiku-4-5')
)
@@ -163,7 +185,10 @@ export function modelSupportsAutoMode(model: string): boolean {
// External: firstParty-only at launch (PI probes not wired for
// Bedrock/Vertex/Foundry yet). Checked before allowModels so the GB
// override can't enable auto mode on unsupported providers.
if (process.env.USER_TYPE !== 'ant' && getAPIProvider() !== 'firstParty') {
if (
process.env.USER_TYPE !== 'ant' &&
(getAPIProvider() !== 'firstParty' || !shouldTrustBuiltInClaudeCapabilities())
) {
return false
}
// GrowthBook override: tengu_auto_mode_config.allowModels force-enables
@@ -215,7 +240,11 @@ export function getToolSearchBetaHeader(): string {
export function shouldIncludeFirstPartyOnlyBetas(): boolean {
return (
(getAPIProvider() === 'firstParty' || getAPIProvider() === 'foundry') &&
(getAPIProvider() !== 'firstParty' || isFirstPartyAnthropicBaseUrl()) &&
(
getAPIProvider() !== 'firstParty' ||
isFirstPartyAnthropicBaseUrl() ||
isClaudeAISubscriber()
) &&
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS)
)
}
@@ -228,6 +257,7 @@ export function shouldIncludeFirstPartyOnlyBetas(): boolean {
export function shouldUseGlobalCacheScope(): boolean {
return (
getAPIProvider() === 'firstParty' &&
(isFirstPartyAnthropicBaseUrl() || isClaudeAISubscriber()) &&
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS)
)
}
+3
View File
@@ -153,10 +153,13 @@ export function sanitizeSurfaceKey(surfaceKey: string): string {
*/
export function sanitizeModelName(shortName: string): string {
// Map internal variants to public equivalents based on model family
if (shortName.includes('fable-5')) return 'claude-fable-5'
if (shortName.includes('opus-4-8')) return 'claude-opus-4-8'
if (shortName.includes('opus-4-6')) return 'claude-opus-4-7'
if (shortName.includes('opus-4-5')) return 'claude-opus-4-5'
if (shortName.includes('opus-4-1')) return 'claude-opus-4-1'
if (shortName.includes('opus-4')) return 'claude-opus-4'
if (shortName.includes('sonnet-5')) return 'claude-sonnet-5'
if (shortName.includes('sonnet-4-6')) return 'claude-sonnet-4-6'
if (shortName.includes('sonnet-4-5')) return 'claude-sonnet-4-5'
if (shortName.includes('sonnet-4')) return 'claude-sonnet-4'
+10 -1
View File
@@ -54,6 +54,9 @@ export function modelSupports1M(model: string): boolean {
const canonical = getCanonicalName(model)
return (
canonical.includes('claude-fable-5') ||
canonical.includes('claude-sonnet-5') ||
canonical.includes('claude-opus-4-8') ||
canonical.includes('claude-opus-4-7') ||
canonical.includes('claude-sonnet-4') ||
canonical.includes('opus-4-6')
)
@@ -232,7 +235,13 @@ export function getModelMaxOutputTokens(model: string): {
const m = getCanonicalName(model)
if (m.includes('opus-4-6')) {
if (
m.includes('fable-5') ||
m.includes('opus-4-8') ||
m.includes('opus-4-7') ||
m.includes('opus-4-6') ||
m.includes('sonnet-5')
) {
defaultTokens = 64_000
upperLimit = 128_000
} else if (m.includes('sonnet-4-6')) {
+26 -8
View File
@@ -1,9 +1,13 @@
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { isUltrathinkEnabled } from './thinking.js'
import { getInitialSettings } from './settings/settings.js'
import { isProSubscriber, isMaxSubscriber, isTeamSubscriber } from './auth.js'
import { isClaudeAISubscriber, isProSubscriber, isMaxSubscriber, isTeamSubscriber } from './auth.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
import { getAPIProvider, isFirstPartyAnthropicBaseUrl } from './model/providers.js'
import {
getAPIProvider,
hasAnthropicCompatibleThirdPartyConfig,
isFirstPartyAnthropicBaseUrl,
} from './model/providers.js'
import { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js'
import { isEnvTruthy } from './envUtils.js'
import type { EffortLevel as RuntimeEffortLevel } from 'src/entrypoints/sdk/runtimeTypes.js'
@@ -24,6 +28,17 @@ export const EFFORT_LEVELS = [
export type EffortValue = EffortLevel | number
function shouldTrustBuiltInClaudeCapabilityList(): boolean {
if (hasAnthropicCompatibleThirdPartyConfig()) {
return false
}
return (
getAPIProvider() !== 'firstParty' ||
isFirstPartyAnthropicBaseUrl() ||
isClaudeAISubscriber()
)
}
// @[MODEL LAUNCH]: Add the new model to the allowlist if it supports the effort parameter.
export function modelSupportsEffort(model: string): boolean {
const m = model.toLowerCase()
@@ -38,7 +53,7 @@ export function modelSupportsEffort(model: string): boolean {
return true
}
// Supported by a subset of Claude 4 models
if (
if (shouldTrustBuiltInClaudeCapabilityList() && (
m.includes('opus-4-6') ||
m.includes('opus-4-7') ||
m.includes('opus-4-8') ||
@@ -48,7 +63,7 @@ export function modelSupportsEffort(model: string): boolean {
m.includes('fable-5') ||
m.includes('mythos-5') ||
m.includes('mythos-preview')
) {
)) {
return true
}
// Exclude any other known legacy models (haiku, older opus/sonnet variants)
@@ -63,7 +78,10 @@ export function modelSupportsEffort(model: string): boolean {
// Default to true for unknown model strings on 1P.
// Do not default to true for 3P as they have different formats for their
// model strings (ex. anthropics/claude-code#30795)
return getAPIProvider() === 'firstParty' && isFirstPartyAnthropicBaseUrl()
return (
getAPIProvider() === 'firstParty' &&
(isFirstPartyAnthropicBaseUrl() || isClaudeAISubscriber())
)
}
// @[MODEL LAUNCH]: Add new models that expose the 'xhigh' effort level.
@@ -77,7 +95,7 @@ export function modelSupportsXHighEffort(model: string): boolean {
return entry?.supportedReasoningEfforts.includes('xhigh') ?? true
}
const m = model.toLowerCase()
return (
return shouldTrustBuiltInClaudeCapabilityList() && (
m.includes('opus-4-7') ||
m.includes('opus-4-8') ||
m.includes('sonnet-5') ||
@@ -97,7 +115,7 @@ export function modelSupportsMaxEffort(model: string): boolean {
return entry?.supportedReasoningEfforts.includes('max') ?? true
}
const m = model.toLowerCase()
if (
if (shouldTrustBuiltInClaudeCapabilityList() && (
m.includes('opus-4-6') ||
m.includes('opus-4-7') ||
m.includes('opus-4-8') ||
@@ -106,7 +124,7 @@ export function modelSupportsMaxEffort(model: string): boolean {
m.includes('fable-5') ||
m.includes('mythos-5') ||
m.includes('mythos-preview')
) {
)) {
return true
}
if (process.env.USER_TYPE === 'ant' && resolveAntModel(model)) {
+1 -1
View File
@@ -140,7 +140,7 @@ export function getFastModeUnavailableReason(): string | null {
}
// @[MODEL LAUNCH]: Update supported Fast Mode models.
export const FAST_MODE_MODEL_DISPLAY = 'Opus 4.7'
export const FAST_MODE_MODEL_DISPLAY = 'Opus 4.8'
export function getFastModeModel(): string {
return 'opus' + (isOpus1mMergeEnabled() ? '[1m]' : '')
+8
View File
@@ -47,6 +47,10 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
'GROK_OAUTH_FILE',
// Model defaults — often set to provider-specific ID formats
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_FABLE_MODEL_NAME',
'ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
@@ -124,6 +128,10 @@ export const SAFE_ENV_VARS = new Set([
'ANTHROPIC_CUSTOM_MODEL_OPTION',
'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION',
'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME',
'ANTHROPIC_DEFAULT_FABLE_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_FABLE_MODEL_NAME',
'ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
+19 -1
View File
@@ -40,12 +40,20 @@ export const CLAUDE_HAIKU_4_5_CONFIG = {
export const CLAUDE_FABLE_5_CONFIG = {
firstParty: 'claude-fable-5',
bedrock: 'us.anthropic.claude-fable-5',
bedrock: 'anthropic.claude-fable-5',
vertex: 'claude-fable-5',
foundry: 'claude-fable-5',
azureOpenAI: 'claude-fable-5',
} as const satisfies ModelConfig
export const CLAUDE_SONNET_5_CONFIG = {
firstParty: 'claude-sonnet-5',
bedrock: 'anthropic.claude-sonnet-5',
vertex: 'claude-sonnet-5',
foundry: 'claude-sonnet-5',
azureOpenAI: 'claude-sonnet-5',
} as const satisfies ModelConfig
export const CLAUDE_SONNET_4_CONFIG = {
firstParty: 'claude-sonnet-4-20250514',
bedrock: 'us.anthropic.claude-sonnet-4-20250514-v1:0',
@@ -94,6 +102,14 @@ export const CLAUDE_OPUS_4_6_CONFIG = {
azureOpenAI: 'claude-opus-4-7',
} as const satisfies ModelConfig
export const CLAUDE_OPUS_4_8_CONFIG = {
firstParty: 'claude-opus-4-8',
bedrock: 'anthropic.claude-opus-4-8',
vertex: 'claude-opus-4-8',
foundry: 'claude-opus-4-8',
azureOpenAI: 'claude-opus-4-8',
} as const satisfies ModelConfig
export const CLAUDE_SONNET_4_6_CONFIG = {
firstParty: 'claude-sonnet-4-6',
bedrock: 'us.anthropic.claude-sonnet-4-6',
@@ -136,10 +152,12 @@ export const ALL_MODEL_CONFIGS = {
sonnet40: CLAUDE_SONNET_4_CONFIG,
sonnet45: CLAUDE_SONNET_4_5_CONFIG,
sonnet46: CLAUDE_SONNET_4_6_CONFIG,
sonnet50: CLAUDE_SONNET_5_CONFIG,
opus40: CLAUDE_OPUS_4_CONFIG,
opus41: CLAUDE_OPUS_4_1_CONFIG,
opus45: CLAUDE_OPUS_4_5_CONFIG,
opus46: CLAUDE_OPUS_4_6_CONFIG,
opus48: CLAUDE_OPUS_4_8_CONFIG,
gpt52codex: GPT_5_2_CODEX_CONFIG,
gpt53codex: GPT_5_3_CODEX_CONFIG,
gpt54codex: GPT_5_4_CODEX_CONFIG,
+79 -2
View File
@@ -1,23 +1,35 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { getContextWindowForModel, modelSupports1M } from '../context.js'
import { computeSimpleEnvInfo } from '../../constants/prompts.js'
import { SKILL_MODEL_VARS } from '../../skills/bundled/claudeApiContent.js'
import { sanitizeModelName } from '../commitAttribution.js'
import { getHardcodedTeammateModelFallback } from '../swarm/teammateModel.js'
import { getAgentModel, getAgentModelOptions } from './agent.js'
import { isModelAlias, isModelFamilyAlias } from './aliases.js'
import { CLAUDE_FABLE_5_CONFIG } from './configs.js'
import {
CLAUDE_FABLE_5_CONFIG,
CLAUDE_OPUS_4_6_CONFIG,
CLAUDE_OPUS_4_8_CONFIG,
CLAUDE_SONNET_5_CONFIG,
} from './configs.js'
import {
firstPartyNameToCanonical,
getDefaultFableModel,
getMarketingNameForModel,
getPublicModelDisplayName,
parseUserSpecifiedModel,
renderDefaultModelSetting,
} from './model.js'
import { getModelStrings } from './modelStrings.js'
import { get3PModelCapabilityOverride } from './modelSupportOverrides.js'
const ENV_KEYS = [
'ANTHROPIC_BASE_URL',
'ANTHROPIC_API_KEY',
'ANTHROPIC_DEFAULT_FABLE_MODEL',
'ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES',
'CLAUDE_CODE_DISABLE_1M_CONTEXT',
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_SUBAGENT_MODEL',
] as const
@@ -40,11 +52,25 @@ describe('Fable model configuration', () => {
test('uses the official provider IDs', () => {
expect(CLAUDE_FABLE_5_CONFIG).toEqual({
firstParty: 'claude-fable-5',
bedrock: 'us.anthropic.claude-fable-5',
bedrock: 'anthropic.claude-fable-5',
vertex: 'claude-fable-5',
foundry: 'claude-fable-5',
azureOpenAI: 'claude-fable-5',
})
expect(CLAUDE_OPUS_4_8_CONFIG).toEqual({
firstParty: 'claude-opus-4-8',
bedrock: 'anthropic.claude-opus-4-8',
vertex: 'claude-opus-4-8',
foundry: 'claude-opus-4-8',
azureOpenAI: 'claude-opus-4-8',
})
expect(CLAUDE_SONNET_5_CONFIG).toEqual({
firstParty: 'claude-sonnet-5',
bedrock: 'anthropic.claude-sonnet-5',
vertex: 'claude-sonnet-5',
foundry: 'claude-sonnet-5',
azureOpenAI: 'claude-sonnet-5',
})
})
test('recognizes fable as an alias and model family', () => {
@@ -74,12 +100,63 @@ describe('Fable model configuration', () => {
expect(getMarketingNameForModel('claude-fable-5[1m]')).toBe(
'Fable 5 (with 1M context)',
)
expect(firstPartyNameToCanonical('anthropic.claude-opus-4-8')).toBe(
'claude-opus-4-8',
)
expect(firstPartyNameToCanonical('claude-sonnet-5')).toBe(
'claude-sonnet-5',
)
expect(getPublicModelDisplayName(getModelStrings().opus48)).toBe('Opus 4.8')
expect(getPublicModelDisplayName(`${getModelStrings().opus48}[1m]`)).toBe(
'Opus 4.8 (1M context)',
)
expect(getPublicModelDisplayName(getModelStrings().sonnet50)).toBe('Sonnet 5')
expect(getPublicModelDisplayName(`${getModelStrings().sonnet50}[1m]`)).toBe(
'Sonnet 5 (1M context)',
)
expect(getMarketingNameForModel('claude-opus-4-8')).toBe('Opus 4.8')
expect(getMarketingNameForModel('claude-sonnet-5')).toBe('Sonnet 5')
expect(renderDefaultModelSetting('opusplan')).toBe(
'Opus 4.8 in plan mode, else Sonnet 5',
)
})
test('publishes current model IDs and knowledge cutoffs in environment context', async () => {
expect(SKILL_MODEL_VARS).toMatchObject({
OPUS_ID: 'claude-opus-4-8',
OPUS_NAME: 'Claude Opus 4.8',
SONNET_ID: 'claude-sonnet-5',
SONNET_NAME: 'Claude Sonnet 5',
})
for (const model of ['claude-fable-5', 'claude-opus-4-8', 'claude-sonnet-5']) {
const info = await computeSimpleEnvInfo(model)
expect(info).toContain('Assistant knowledge cutoff is January 2026.')
expect(info).toContain("Fable 5: 'claude-fable-5'")
expect(info).toContain("Opus 4.8: 'claude-opus-4-8'")
expect(info).toContain("Sonnet 5: 'claude-sonnet-5'")
expect(info).toContain('same Claude Opus 4.8 model')
}
})
test('sanitizes new model trailers and keeps teammate fallbacks provider-safe', () => {
expect(sanitizeModelName('claude-fable-5-experimental')).toBe('claude-fable-5')
expect(sanitizeModelName('claude-opus-4-8-experimental')).toBe('claude-opus-4-8')
expect(sanitizeModelName('claude-sonnet-5-experimental')).toBe('claude-sonnet-5')
expect(getHardcodedTeammateModelFallback()).toBe('claude-opus-4-8')
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
expect(getHardcodedTeammateModelFallback()).toBe(
CLAUDE_OPUS_4_6_CONFIG.bedrock,
)
})
test('exposes the native 1M context window', () => {
expect(modelSupports1M('claude-fable-5')).toBe(true)
expect(getContextWindowForModel('claude-fable-5')).toBe(1_000_000)
expect(getContextWindowForModel('anthropic/claude-fable-5')).toBe(1_000_000)
expect(getContextWindowForModel('claude-opus-4-8')).toBe(1_000_000)
expect(getContextWindowForModel('claude-sonnet-5')).toBe(1_000_000)
})
})
+52 -15
View File
@@ -8,6 +8,7 @@
import { getMainLoopModelOverride } from '../../bootstrap/state.js'
import {
getSubscriptionType,
is1PApiCustomer,
isClaudeAISubscriber,
isMaxSubscriber,
isOpenAIAuthActive,
@@ -24,7 +25,10 @@ import { getModelStrings, resolveOverriddenModel } from './modelStrings.js'
import { formatModelPricing, getOpus46CostTier } from '../modelCost.js'
import { getSettings_DEPRECATED } from '../settings/settings.js'
import type { PermissionMode } from '../permissions/PermissionMode.js'
import { getAPIProvider } from './providers.js'
import {
getAPIProvider,
hasAnthropicCompatibleThirdPartyConfig,
} from './providers.js'
import { LIGHTNING_BOLT } from '../../constants/figures.js'
import { isModelAllowed } from './modelAllowlist.js'
import { type ModelAlias, isModelAlias } from './aliases.js'
@@ -47,7 +51,8 @@ export function isNonCustomOpusModel(model: ModelName): boolean {
model === getModelStrings().opus40 ||
model === getModelStrings().opus41 ||
model === getModelStrings().opus45 ||
model === getModelStrings().opus46
model === getModelStrings().opus46 ||
model === getModelStrings().opus48
)
}
@@ -103,7 +108,11 @@ export function getMainLoopModel(): ModelName {
}
export function getBestModel(): ModelName {
return getDefaultOpusModel()
return getDefaultFableModel()
}
function shouldUseThirdPartyAnthropicModelDefaults(): boolean {
return !isClaudeAISubscriber() && hasAnthropicCompatibleThirdPartyConfig()
}
export function getDefaultFableModel(): ModelName {
@@ -113,6 +122,9 @@ export function getDefaultFableModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_FABLE_MODEL) {
return process.env.ANTHROPIC_DEFAULT_FABLE_MODEL
}
if (shouldUseThirdPartyAnthropicModelDefaults()) {
return getDefaultOpusModel()
}
return getModelStrings().fable5
}
@@ -127,10 +139,10 @@ export function getDefaultOpusModel(): ModelName {
// 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.
if (getAPIProvider() !== 'firstParty') {
if (shouldUseThirdPartyAnthropicModelDefaults()) {
return getModelStrings().opus46
}
return getModelStrings().opus46
return getModelStrings().opus48
}
// @[MODEL LAUNCH]: Update the default Sonnet model (3P providers may lag so keep defaults unchanged).
@@ -142,10 +154,10 @@ export function getDefaultSonnetModel(): ModelName {
return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
}
// Default to Sonnet 4.5 for 3P since they may not have 4.6 yet
if (getAPIProvider() !== 'firstParty') {
if (shouldUseThirdPartyAnthropicModelDefaults()) {
return getModelStrings().sonnet45
}
return getModelStrings().sonnet46
return getModelStrings().sonnet50
}
// @[MODEL LAUNCH]: Update the default Haiku model (3P providers may lag so keep defaults unchanged).
@@ -194,8 +206,9 @@ export function getRuntimeMainLoopModel(params: {
* Get the default main loop model setting.
*
* This handles the built-in default:
* - Opus for Max and Team Premium users
* - Sonnet 4.6 for all other users (including Team Standard, Pro, Enterprise)
* - Opus for Anthropic API/PAYG, Max, and Team Premium users
* - Sonnet for Pro, Team Standard, and Enterprise subscription users
* - Sonnet for third-party providers, where built-in aliases may lag
*
* @returns The default model setting to use
*/
@@ -216,6 +229,10 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
return resolveOpenAICodexModel('opus')
}
if (is1PApiCustomer()) {
return getDefaultOpusModel()
}
// Max users get Opus as default
if (isMaxSubscriber()) {
return getDefaultOpusModel() + (isOpus1mMergeEnabled() ? '[1m]' : '')
@@ -226,8 +243,8 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
return getDefaultOpusModel() + (isOpus1mMergeEnabled() ? '[1m]' : '')
}
// PAYG (1P and 3P), Enterprise, Team Standard, and Pro get Sonnet as default
// Note that PAYG (3P) may default to an older Sonnet model
// Third-party provider defaults and subscription tiers that are not
// Opus-defaulted above use Sonnet. Third-party provider aliases may lag.
return getDefaultSonnetModel()
}
@@ -253,6 +270,9 @@ export function firstPartyNameToCanonical(name: ModelName): ModelShortName {
if (name.includes('claude-fable-5')) {
return 'claude-fable-5'
}
if (name.includes('claude-opus-4-8')) {
return 'claude-opus-4-8'
}
if (name.includes('claude-opus-4-7')) {
return 'claude-opus-4-7'
}
@@ -265,6 +285,9 @@ export function firstPartyNameToCanonical(name: ModelName): ModelShortName {
if (name.includes('claude-opus-4')) {
return 'claude-opus-4'
}
if (name.includes('claude-sonnet-5')) {
return 'claude-sonnet-5'
}
if (name.includes('claude-sonnet-4-6')) {
return 'claude-sonnet-4-6'
}
@@ -329,18 +352,18 @@ export function getClaudeAiUserDefaultModelDescription(
}
if (isMaxSubscriber() || isTeamPremiumSubscriber()) {
if (isOpus1mMergeEnabled()) {
return `Opus 4.7 with 1M context · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`
return `Opus 4.8 with 1M context · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`
}
return `Opus 4.7 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`
return `Opus 4.8 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`
}
return 'Sonnet 4.6 · Best for everyday tasks'
return 'Sonnet 5 · Best for everyday tasks'
}
export function renderDefaultModelSetting(
setting: ModelName | ModelAlias,
): string {
if (setting === 'opusplan') {
return 'Opus 4.7 in plan mode, else Sonnet 4.6'
return `${renderModelName(getDefaultOpusModel())} in plan mode, else ${renderModelName(getDefaultSonnetModel())}`
}
return renderModelName(parseUserSpecifiedModel(setting))
}
@@ -397,6 +420,10 @@ export function getPublicModelDisplayName(model: ModelName): string | null {
return 'Fable 5'
case getModelStrings().fable5 + '[1m]':
return 'Fable 5 (1M context)'
case getModelStrings().opus48:
return 'Opus 4.8'
case getModelStrings().opus48 + '[1m]':
return 'Opus 4.8 (1M context)'
case getModelStrings().opus46:
return 'Opus 4.7'
case getModelStrings().opus46 + '[1m]':
@@ -411,6 +438,10 @@ export function getPublicModelDisplayName(model: ModelName): string | null {
return 'Sonnet 4.6 (1M context)'
case getModelStrings().sonnet46:
return 'Sonnet 4.6'
case getModelStrings().sonnet50 + '[1m]':
return 'Sonnet 5 (1M context)'
case getModelStrings().sonnet50:
return 'Sonnet 5'
case getModelStrings().sonnet45 + '[1m]':
return 'Sonnet 4.5 (1M context)'
case getModelStrings().sonnet45:
@@ -634,6 +665,9 @@ export function getMarketingNameForModel(modelId: string): string | undefined {
if (canonical.includes('claude-fable-5')) {
return has1m ? 'Fable 5 (with 1M context)' : 'Fable 5'
}
if (canonical.includes('claude-opus-4-8')) {
return has1m ? 'Opus 4.8 (with 1M context)' : 'Opus 4.8'
}
if (canonical.includes('claude-opus-4-7')) {
return has1m ? 'Opus 4.7 (with 1M context)' : 'Opus 4.7'
}
@@ -646,6 +680,9 @@ export function getMarketingNameForModel(modelId: string): string | undefined {
if (canonical.includes('claude-opus-4')) {
return 'Opus 4'
}
if (canonical.includes('claude-sonnet-5')) {
return has1m ? 'Sonnet 5 (with 1M context)' : 'Sonnet 5'
}
if (canonical.includes('claude-sonnet-4-6')) {
return has1m ? 'Sonnet 4.6 (with 1M context)' : 'Sonnet 4.6'
}
+4
View File
@@ -4,7 +4,9 @@ export const MODEL_CONTEXT_WINDOW_MAX = 10_000_000
const DIRECT_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
'claude-fable-5': 1_000_000,
'claude-opus-4-8': 1_000_000,
'claude-opus-4-7': 1_000_000,
'claude-sonnet-5': 1_000_000,
'claude-sonnet-4-6': 200_000,
'claude-haiku-4-5': 200_000,
'deepseek-v4-pro': 1_000_000,
@@ -39,6 +41,8 @@ const DIRECT_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
const PATTERN_MODEL_CONTEXT_WINDOWS: Array<[RegExp, number]> = [
[/^anthropic\/claude-fable-5\b/i, 1_000_000],
[/^anthropic\/claude-opus-4\.8\b/i, 1_000_000],
[/^anthropic\/claude-sonnet-5\b/i, 1_000_000],
[/^anthropic\/claude-opus-4\.7\b/i, 1_000_000],
[/^anthropic\/claude-sonnet-4\.6\b/i, 200_000],
[/^anthropic\/claude-haiku-4\.5\b/i, 200_000],
+117 -50
View File
@@ -9,6 +9,7 @@ import {
} from '../auth.js'
import { getModelStrings } from './modelStrings.js'
import {
COST_TIER_10_50,
COST_TIER_3_15,
COST_HAIKU_35,
COST_HAIKU_45,
@@ -16,11 +17,15 @@ import {
} from '../modelCost.js'
import { getSettings_DEPRECATED } from '../settings/settings.js'
import { checkOpus1mAccess, checkSonnet1mAccess } from './check1mAccess.js'
import { getAPIProvider, isFirstPartyAnthropicBaseUrl } from './providers.js'
import {
getAPIProvider,
hasAnthropicCompatibleThirdPartyConfig,
} from './providers.js'
import { isModelAllowed } from './modelAllowlist.js'
import {
getCanonicalName,
getClaudeAiUserDefaultModelDescription,
getDefaultFableModel,
getDefaultSonnetModel,
getDefaultOpusModel,
getDefaultHaikuModel,
@@ -45,10 +50,6 @@ export type ModelOption = {
descriptionForModel?: string
}
function hasAnthropicCompatibleThirdPartyConfig(): boolean {
return getAPIProvider() !== 'firstParty' || !isFirstPartyAnthropicBaseUrl()
}
function pushUniqueOption(
options: ModelOption[],
option: ModelOption | undefined,
@@ -64,6 +65,21 @@ function pushUniqueOption(
options.push(option)
}
function shouldOfferSonnet1mOption(): boolean {
if (!checkSonnet1mAccess()) {
return false
}
return (
shouldUseThirdPartyAnthropicOptions() ||
!getDefaultSonnetModel().toLowerCase().includes('sonnet-5')
)
}
function shouldUseThirdPartyAnthropicOptions(): boolean {
return !isClaudeAISubscriber() && hasAnthropicCompatibleThirdPartyConfig()
}
export function getDefaultOptionForUser(fastMode = false): ModelOption {
if (process.env.USER_TYPE === 'ant') {
const currentModel = renderDefaultModelSetting(
@@ -99,16 +115,23 @@ export function getDefaultOptionForUser(fastMode = false): ModelOption {
}
// PAYG
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const is3P = shouldUseThirdPartyAnthropicOptions()
const currentModel = renderDefaultModelSetting(getDefaultMainLoopModelSetting())
const pricingSuffix =
is3P
? ''
: currentModel.includes('Opus')
? getOpus46PricingSuffix(fastMode)
: ` · ${formatModelPricing(COST_TIER_3_15)}`
return {
value: null,
label: 'Default (recommended)',
description: `Use the default model (currently ${renderDefaultModelSetting(getDefaultMainLoopModelSetting())})${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`,
description: `Use the default model (currently ${currentModel})${pricingSuffix}`,
}
}
function getCustomSonnetOption(): ModelOption | undefined {
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const is3P = shouldUseThirdPartyAnthropicOptions()
const customSonnetModel = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
// When a 3P user has a custom sonnet model string, show it directly
if (is3P && customSonnetModel) {
@@ -127,19 +150,50 @@ function getCustomSonnetOption(): ModelOption | undefined {
// @[MODEL LAUNCH]: Update or add model option functions (getSonnetXXOption, getOpusXXOption, etc.)
// with the new model's label and description. These appear in the /model picker.
function getCustomFableOption(): ModelOption | undefined {
const is3P = shouldUseThirdPartyAnthropicOptions()
const customFableModel = process.env.ANTHROPIC_DEFAULT_FABLE_MODEL
if (is3P && customFableModel) {
const is1m = has1mContext(customFableModel)
return {
value: 'fable',
label:
process.env.ANTHROPIC_DEFAULT_FABLE_MODEL_NAME ?? customFableModel,
description:
process.env.ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION ??
`Custom Fable model${is1m ? ' (1M context)' : ''}`,
descriptionForModel: `${process.env.ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION ?? `Custom Fable model${is1m ? ' with 1M context' : ''}`} (${customFableModel})`,
}
}
}
function getFable5Option(): ModelOption | undefined {
if (shouldUseThirdPartyAnthropicOptions()) {
return undefined
}
return {
value: 'fable',
label: 'Fable',
description: `Fable 5 · Highest capability for long-running tasks · ${formatModelPricing(COST_TIER_10_50)}`,
descriptionForModel:
'Fable 5 - highest capability for hard, long-running tasks',
}
}
function getSonnet46Option(): ModelOption {
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const is3P = shouldUseThirdPartyAnthropicOptions()
const modelName = is3P ? 'Sonnet 4.6' : 'Sonnet 5'
return {
value: is3P ? getModelStrings().sonnet46 : 'sonnet',
label: 'Sonnet',
description: `Sonnet 4.6 · Best for everyday tasks${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`,
description: `${modelName} · Best for everyday tasks${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`,
descriptionForModel:
'Sonnet 4.6 - best for everyday tasks. Generally recommended for most coding tasks',
`${modelName} - best for everyday tasks. Generally recommended for most coding tasks`,
}
}
function getCustomOpusOption(): ModelOption | undefined {
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const is3P = shouldUseThirdPartyAnthropicOptions()
const customOpusModel = process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
// When a 3P user has a custom opus model string, show it directly
if (is3P && customOpusModel) {
@@ -165,39 +219,41 @@ function getOpus41Option(): ModelOption {
}
function getOpus46Option(fastMode = false): ModelOption {
const is3P = getAPIProvider() !== 'firstParty'
const is3P = shouldUseThirdPartyAnthropicOptions()
return {
value: is3P ? getModelStrings().opus46 : 'opus',
label: 'Opus',
description: `Opus 4.7 · Most capable for complex work${getOpus46PricingSuffix(fastMode)}`,
descriptionForModel: 'Opus 4.7 - most capable for complex work',
description: `${is3P ? 'Opus 4.7' : 'Opus 4.8'} · Most capable for complex work${getOpus46PricingSuffix(fastMode)}`,
descriptionForModel: `${is3P ? 'Opus 4.7' : 'Opus 4.8'} - most capable for complex work`,
}
}
export function getSonnet46_1MOption(): ModelOption {
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const is3P = shouldUseThirdPartyAnthropicOptions()
const modelName = is3P ? 'Sonnet 4.6' : 'Sonnet 5'
return {
value: is3P ? getModelStrings().sonnet46 + '[1m]' : 'sonnet[1m]',
label: 'Sonnet (1M context)',
description: `Sonnet 4.6 for long sessions${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`,
description: `${modelName} for long sessions${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`,
descriptionForModel:
'Sonnet 4.6 with 1M context window - for long sessions with large codebases',
`${modelName} with 1M context window - for long sessions with large codebases`,
}
}
export function getOpus46_1MOption(fastMode = false): ModelOption {
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const is3P = shouldUseThirdPartyAnthropicOptions()
const modelName = is3P ? 'Opus 4.7' : 'Opus 4.8'
return {
value: is3P ? getModelStrings().opus46 + '[1m]' : 'opus[1m]',
label: 'Opus (1M context)',
description: `Opus 4.7 for long sessions${getOpus46PricingSuffix(fastMode)}`,
description: `${modelName} for long sessions${getOpus46PricingSuffix(fastMode)}`,
descriptionForModel:
'Opus 4.7 with 1M context window - for long sessions with large codebases',
`${modelName} with 1M context window - for long sessions with large codebases`,
}
}
function getCustomHaikuOption(): ModelOption | undefined {
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const is3P = shouldUseThirdPartyAnthropicOptions()
const customHaikuModel = process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
// When a 3P user has a custom haiku model string, show it directly
if (is3P && customHaikuModel) {
@@ -213,7 +269,7 @@ function getCustomHaikuOption(): ModelOption | undefined {
}
function getHaiku45Option(): ModelOption {
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const is3P = shouldUseThirdPartyAnthropicOptions()
return {
value: 'haiku',
label: 'Haiku',
@@ -224,7 +280,7 @@ function getHaiku45Option(): ModelOption {
}
function getHaiku35Option(): ModelOption {
const is3P = getAPIProvider() !== 'firstParty'
const is3P = shouldUseThirdPartyAnthropicOptions()
return {
value: 'haiku',
label: 'Haiku',
@@ -246,17 +302,17 @@ function getMaxOpusOption(fastMode = false): ModelOption {
return {
value: 'opus',
label: 'Opus',
description: `Opus 4.7 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`,
description: `Opus 4.8 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`,
}
}
export function getMaxSonnet46_1MOption(): ModelOption {
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const is3P = shouldUseThirdPartyAnthropicOptions()
const billingInfo = isClaudeAISubscriber() ? ' · Billed as extra usage' : ''
return {
value: 'sonnet[1m]',
label: 'Sonnet (1M context)',
description: `Sonnet 4.6 with 1M context${billingInfo}${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`,
description: `Sonnet 5 with 1M context${billingInfo}${is3P ? '' : ` · ${formatModelPricing(COST_TIER_3_15)}`}`,
}
}
@@ -265,25 +321,25 @@ export function getMaxOpus46_1MOption(fastMode = false): ModelOption {
return {
value: 'opus[1m]',
label: 'Opus (1M context)',
description: `Opus 4.7 with 1M context${billingInfo}${getOpus46PricingSuffix(fastMode)}`,
description: `Opus 4.8 with 1M context${billingInfo}${getOpus46PricingSuffix(fastMode)}`,
}
}
function getMergedOpus1MOption(fastMode = false): ModelOption {
const is3P = hasAnthropicCompatibleThirdPartyConfig()
const is3P = shouldUseThirdPartyAnthropicOptions()
return {
value: is3P ? getModelStrings().opus46 + '[1m]' : 'opus[1m]',
label: 'Opus (1M context)',
description: `Opus 4.7 with 1M context · Most capable for complex work${!is3P && fastMode ? getOpus46PricingSuffix(fastMode) : ''}`,
description: `${is3P ? 'Opus 4.7' : 'Opus 4.8'} with 1M context · Most capable for complex work${!is3P && fastMode ? getOpus46PricingSuffix(fastMode) : ''}`,
descriptionForModel:
'Opus 4.7 with 1M context - most capable for complex work',
`${is3P ? 'Opus 4.7' : 'Opus 4.8'} with 1M context - most capable for complex work`,
}
}
const MaxSonnet46Option: ModelOption = {
value: 'sonnet',
label: 'Sonnet',
description: 'Sonnet 4.6 · Best for everyday tasks',
description: 'Sonnet 5 · Best for everyday tasks',
}
const MaxHaiku45Option: ModelOption = {
@@ -296,7 +352,7 @@ function getOpusPlanOption(): ModelOption {
return {
value: 'opusplan',
label: 'Opus Plan Mode',
description: 'Use Opus 4.7 in plan mode, Sonnet 4.6 otherwise',
description: 'Use Opus in plan mode, Sonnet otherwise',
}
}
@@ -340,11 +396,12 @@ function getOpenAIModelOptions(): ModelOption[] {
})),
]
if (!hasAnthropicCompatibleThirdPartyConfig()) {
if (!shouldUseThirdPartyAnthropicOptions()) {
return options
}
pushUniqueOption(options, getCustomSonnetOption())
pushUniqueOption(options, getCustomFableOption())
pushUniqueOption(options, getCustomOpusOption())
pushUniqueOption(options, getCustomHaikuOption())
@@ -365,8 +422,10 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
description: m.description ?? `[ANT-ONLY] ${m.label} (${m.model})`,
}))
const fableOption = getFable5Option()
return [
getDefaultOptionForUser(),
...(fableOption ? [fableOption] : []),
...antModelOptions,
getMergedOpus1MOption(fastMode),
getSonnet46Option(),
@@ -383,12 +442,13 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
if (isMaxSubscriber() || isTeamPremiumSubscriber()) {
// Max and Team Premium users: Opus is default, show Sonnet as alternative
const premiumOptions = [getDefaultOptionForUser(fastMode)]
pushUniqueOption(premiumOptions, getFable5Option())
if (!isOpus1mMergeEnabled() && checkOpus1mAccess()) {
premiumOptions.push(getMaxOpus46_1MOption(fastMode))
}
premiumOptions.push(MaxSonnet46Option)
if (checkSonnet1mAccess()) {
if (shouldOfferSonnet1mOption()) {
premiumOptions.push(getMaxSonnet46_1MOption())
}
@@ -398,7 +458,8 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
// Pro/Team Standard/Enterprise users: Sonnet is default, show Opus as alternative
const standardOptions = [getDefaultOptionForUser(fastMode)]
if (checkSonnet1mAccess()) {
pushUniqueOption(standardOptions, getFable5Option())
if (shouldOfferSonnet1mOption()) {
standardOptions.push(getMaxSonnet46_1MOption())
}
@@ -415,34 +476,31 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
return standardOptions
}
// PAYG 1P API: Default (Sonnet) + Sonnet 1M + Opus 4.7 + Opus 1M + Haiku
if (!hasAnthropicCompatibleThirdPartyConfig()) {
// PAYG 1P API: Default (Opus 4.8) + Fable + Sonnet + Haiku.
// Opus 4.8 and Sonnet 5 already have native 1M context windows.
if (!shouldUseThirdPartyAnthropicOptions()) {
const payg1POptions = [getDefaultOptionForUser(fastMode)]
if (checkSonnet1mAccess()) {
pushUniqueOption(payg1POptions, getFable5Option())
payg1POptions.push(getSonnet46Option())
if (shouldOfferSonnet1mOption()) {
payg1POptions.push(getSonnet46_1MOption())
}
if (isOpus1mMergeEnabled()) {
payg1POptions.push(getMergedOpus1MOption(fastMode))
} else {
payg1POptions.push(getOpus46Option(fastMode))
if (checkOpus1mAccess()) {
payg1POptions.push(getOpus46_1MOption(fastMode))
}
}
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.7/Opus1M + Haiku + Opus 4.1
// PAYG 3P: Default (Sonnet 4.5) + optional custom Fable + Sonnet + Opus + Haiku
const payg3pOptions = [getDefaultOptionForUser(fastMode)]
pushUniqueOption(payg3pOptions, getCustomFableOption())
const customSonnet = getCustomSonnetOption()
if (customSonnet !== undefined) {
pushUniqueOption(payg3pOptions, customSonnet)
} else {
// Add Sonnet 4.6 since Sonnet 4.5 is the default
// Add Sonnet 4.6 since Sonnet 4.5 is the third-party default
payg3pOptions.push(getSonnet46Option())
if (checkSonnet1mAccess()) {
if (shouldOfferSonnet1mOption()) {
payg3pOptions.push(getSonnet46_1MOption())
}
}
@@ -479,8 +537,17 @@ function getModelFamilyInfo(
): { alias: string; currentVersionName: string } | null {
const canonical = getCanonicalName(model)
// Fable family
if (canonical.includes('claude-fable-5')) {
const currentName = getMarketingNameForModel(getDefaultFableModel())
if (currentName) {
return { alias: 'Fable', currentVersionName: currentName }
}
}
// Sonnet family
if (
canonical.includes('claude-sonnet-5') ||
canonical.includes('claude-sonnet-4-6') ||
canonical.includes('claude-sonnet-4-5') ||
canonical.includes('claude-sonnet-4-') ||
+4
View File
@@ -45,3 +45,7 @@ export function isFirstPartyAnthropicBaseUrl(): boolean {
return false
}
}
export function hasAnthropicCompatibleThirdPartyConfig(): boolean {
return getAPIProvider() !== 'firstParty' || !isFirstPartyAnthropicBaseUrl()
}
+18
View File
@@ -7,14 +7,17 @@ import {
CLAUDE_3_5_HAIKU_CONFIG,
CLAUDE_3_5_V2_SONNET_CONFIG,
CLAUDE_3_7_SONNET_CONFIG,
CLAUDE_FABLE_5_CONFIG,
CLAUDE_HAIKU_4_5_CONFIG,
CLAUDE_OPUS_4_1_CONFIG,
CLAUDE_OPUS_4_5_CONFIG,
CLAUDE_OPUS_4_6_CONFIG,
CLAUDE_OPUS_4_8_CONFIG,
CLAUDE_OPUS_4_CONFIG,
CLAUDE_SONNET_4_5_CONFIG,
CLAUDE_SONNET_4_6_CONFIG,
CLAUDE_SONNET_4_CONFIG,
CLAUDE_SONNET_5_CONFIG,
} from './model/configs.js'
import {
firstPartyNameToCanonical,
@@ -59,6 +62,15 @@ export const COST_TIER_5_25 = {
webSearchRequests: 0.01,
} as const satisfies ModelCosts
// Pricing tier for Fable 5: $10 input / $50 output per Mtok
export const COST_TIER_10_50 = {
inputTokens: 10,
outputTokens: 50,
promptCacheWriteTokens: 12.5,
promptCacheReadTokens: 1,
webSearchRequests: 0.01,
} as const satisfies ModelCosts
// Fast mode pricing for Opus 4.7: $30 input / $150 output per Mtok
export const COST_TIER_30_150 = {
inputTokens: 30,
@@ -102,6 +114,8 @@ export function getOpus46CostTier(fastMode: boolean): ModelCosts {
// Costs from https://platform.claude.com/docs/en/about-claude/pricing
// Web search cost: $10 per 1000 requests = $0.01 per request
export const MODEL_COSTS: Record<ModelShortName, ModelCosts> = {
[firstPartyNameToCanonical(CLAUDE_FABLE_5_CONFIG.firstParty)]:
COST_TIER_10_50,
[firstPartyNameToCanonical(CLAUDE_3_5_HAIKU_CONFIG.firstParty)]:
COST_HAIKU_35,
[firstPartyNameToCanonical(CLAUDE_HAIKU_4_5_CONFIG.firstParty)]:
@@ -116,6 +130,8 @@ export const MODEL_COSTS: Record<ModelShortName, ModelCosts> = {
COST_TIER_3_15,
[firstPartyNameToCanonical(CLAUDE_SONNET_4_6_CONFIG.firstParty)]:
COST_TIER_3_15,
[firstPartyNameToCanonical(CLAUDE_SONNET_5_CONFIG.firstParty)]:
COST_TIER_3_15,
[firstPartyNameToCanonical(CLAUDE_OPUS_4_CONFIG.firstParty)]: COST_TIER_15_75,
[firstPartyNameToCanonical(CLAUDE_OPUS_4_1_CONFIG.firstParty)]:
COST_TIER_15_75,
@@ -123,6 +139,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_8_CONFIG.firstParty)]:
COST_TIER_5_25,
}
/**
+2
View File
@@ -1,6 +1,7 @@
const DEEPSEEK_THINKING_CAPABILITIES = 'thinking,effort,adaptive_thinking,max_effort'
const DEEPSEEK_CAPABILITY_ENV_KEYS = [
'ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
@@ -10,6 +11,7 @@ function looksLikeDeepSeekManagedEnv(env: Record<string, string>): boolean {
const baseUrl = env.ANTHROPIC_BASE_URL ?? ''
const modelIds = [
env.ANTHROPIC_MODEL,
env.ANTHROPIC_DEFAULT_FABLE_MODEL,
env.ANTHROPIC_DEFAULT_HAIKU_MODEL,
env.ANTHROPIC_DEFAULT_SONNET_MODEL,
env.ANTHROPIC_DEFAULT_OPUS_MODEL,
+7 -4
View File
@@ -1,10 +1,13 @@
import { CLAUDE_OPUS_4_6_CONFIG } from '../model/configs.js'
import { CLAUDE_OPUS_4_6_CONFIG, CLAUDE_OPUS_4_8_CONFIG } from '../model/configs.js'
import { getAPIProvider } from '../model/providers.js'
// @[MODEL LAUNCH]: Update the fallback model below.
// When the user has never set teammateDefaultModel in /config, new teammates
// use Opus 4.7. Must be provider-aware so Bedrock/Vertex/Foundry customers get
// the correct model ID.
// use the current Opus default. Must be provider-aware so Bedrock/Vertex/Foundry
// customers get a conservative provider ID.
export function getHardcodedTeammateModelFallback(): string {
return CLAUDE_OPUS_4_6_CONFIG[getAPIProvider()]
const provider = getAPIProvider()
return provider === 'firstParty'
? CLAUDE_OPUS_4_8_CONFIG.firstParty
: CLAUDE_OPUS_4_6_CONFIG[provider]
}
+8 -2
View File
@@ -161,8 +161,14 @@ export function modelSupportsAdaptiveThinking(model: string): boolean {
return false
}
// Supported by a subset of Claude 4 models
if (canonical.includes('opus-4-6') || canonical.includes('sonnet-4-6')) {
// Supported by current Claude Code flagship models.
if (
canonical.includes('sonnet-5') ||
canonical.includes('opus-4-8') ||
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)