diff --git a/src/utils/model/model.openai-shim-providers.test.ts b/src/utils/model/model.openai-shim-providers.test.ts index c0b18c059b..67dc3bdefd 100644 --- a/src/utils/model/model.openai-shim-providers.test.ts +++ b/src/utils/model/model.openai-shim-providers.test.ts @@ -306,6 +306,29 @@ test('getDefaultMainLoopModelSetting defaults MiniMax to M3', async () => { expect(getDefaultMainLoopModel()).toBe('MiniMax-M3') }) +test('getDefaultMainLoopModelSetting uses the NVIDIA NIM route model', async () => { + process.env.NVIDIA_NIM = '1' + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_MODEL = 'meta/llama-3.3-70b-instruct' + + const { + getDefaultMainLoopModel, + getDefaultMainLoopModelSetting, + } = await importFreshModelModule() + expect(getDefaultMainLoopModelSetting()).toBe('meta/llama-3.3-70b-instruct') + expect(getDefaultMainLoopModel()).toBe('meta/llama-3.3-70b-instruct') +}) + +test('getDefaultMainLoopModelSetting falls back to the NVIDIA NIM descriptor default', async () => { + process.env.NVIDIA_NIM = '1' + process.env.CLAUDE_CODE_USE_OPENAI = '1' + + const { getDefaultMainLoopModelSetting } = await importFreshModelModule() + expect(getDefaultMainLoopModelSetting()).toBe( + 'nvidia/llama-3.1-nemotron-70b-instruct', + ) +}) + test('getDefaultMainLoopModelSetting defaults Xiaomi MiMo to mimo-v2.5-pro', async () => { process.env.MIMO_API_KEY = 'mimo-test' process.env.CLAUDE_CODE_USE_OPENAI = '1' diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index 606f3cd6b2..0a63f9e1ca 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -30,6 +30,7 @@ import { type ModelAlias, isModelAlias } from './aliases.js' import { capitalize } from '../stringUtils.js' import { DEFAULT_GEMINI_MODEL } from '../providerProfile.js' import { getAntModelOverrideConfig, resolveAntModel } from './antModels.js' +import { getRouteDefaultModel } from '../../integrations/routeMetadata.js' export type ModelShortName = string export type ModelName = string @@ -388,6 +389,15 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias { if (getAPIProvider() === 'codex') { return process.env.OPENAI_MODEL || 'gpt-5.5' } + // NVIDIA NIM uses OpenAI-compatible model ids. Keep this fallback aligned + // with the route descriptor so headless sessions never send a Claude model. + if (getAPIProvider() === 'nvidia-nim') { + return ( + process.env.OPENAI_MODEL || + getRouteDefaultModel('nvidia-nim') || + 'nvidia/llama-3.1-nemotron-70b-instruct' + ) + } // xAI provider: always use the configured Grok model (default grok-4.3) if (getAPIProvider() === 'xai') { return process.env.OPENAI_MODEL || 'grok-4.3' diff --git a/src/utils/modelCost.modelGate.test.ts b/src/utils/modelCost.modelGate.test.ts index a532e0d271..9b78436f03 100644 --- a/src/utils/modelCost.modelGate.test.ts +++ b/src/utils/modelCost.modelGate.test.ts @@ -22,6 +22,29 @@ afterEach(() => { } }) +test('unknown models do not inherit the configured default model price', async () => { + mock.module('./model/model.js', () => ({ + firstPartyNameToCanonical: (model: string) => { + if (model.includes('claude-haiku-4-5')) return 'claude-haiku-4-5' + return model + }, + getCanonicalName: (model: string) => { + if (model.includes('claude-haiku-4-5')) return 'claude-haiku-4-5' + return model + }, + getDefaultMainLoopModelSetting: () => 'claude-haiku-4-5', + })) + const { getModelCosts, COST_HAIKU_45, COST_TIER_5_25 } = + await importFreshModelCost() + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const usage = {} as any + const costs = getModelCosts('meta/llama-3.3-70b-instruct', usage) + + expect(costs).toEqual(COST_TIER_5_25) + expect(costs).not.toEqual(COST_HAIKU_45) +}) + // Regression for #1769: fast mode is now enabled for Opus 4.8, but getModelCosts // only applied the elevated fast-mode tier to opus-4-6, so fast-mode 4.8 was // billed at the normal rate while the picker advertised the fast-mode price. diff --git a/src/utils/modelCost.ts b/src/utils/modelCost.ts index ea4bdedcd7..c528067d9c 100644 --- a/src/utils/modelCost.ts +++ b/src/utils/modelCost.ts @@ -21,7 +21,6 @@ import { import { firstPartyNameToCanonical, getCanonicalName, - getDefaultMainLoopModelSetting, type ModelShortName, } from './model/model.js' @@ -166,10 +165,7 @@ export function getModelCosts(model: string, usage: Usage): ModelCosts { const costs = MODEL_COSTS[shortName] if (!costs) { trackUnknownModelCost(model, shortName) - return ( - MODEL_COSTS[getCanonicalName(getDefaultMainLoopModelSetting())] ?? - DEFAULT_UNKNOWN_MODEL_COST - ) + return DEFAULT_UNKNOWN_MODEL_COST } return costs } @@ -184,7 +180,8 @@ function trackUnknownModelCost(model: string, shortName: ModelShortName): void { } // Calculate the cost of a query in US dollars. -// If the model's costs are not found, use the default model's costs. +// Unknown models use the explicit unknown-model estimate and are marked in +// session state; they must never inherit an unrelated configured default. export function calculateUSDCost(resolvedModel: string, usage: Usage): number { const modelCosts = getModelCosts(resolvedModel, usage) return tokensToUSDCost(modelCosts, usage)