mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
fix(models): expose current Claude official lineup
This commit is contained in:
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 },
|
||||
]
|
||||
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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?.()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)) {
|
||||
|
||||
@@ -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]' : '')
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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-') ||
|
||||
|
||||
@@ -45,3 +45,7 @@ export function isFirstPartyAnthropicBaseUrl(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function hasAnthropicCompatibleThirdPartyConfig(): boolean {
|
||||
return getAPIProvider() !== 'firstParty' || !isFirstPartyAnthropicBaseUrl()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user