diff --git a/apps/web/src/lib/ai-gateway/models.test.ts b/apps/web/src/lib/ai-gateway/models.test.ts index 08a8ac98d1..3224c7a229 100644 --- a/apps/web/src/lib/ai-gateway/models.test.ts +++ b/apps/web/src/lib/ai-gateway/models.test.ts @@ -122,7 +122,7 @@ describe('isFreeModel', () => { expect(gpt_5_6_sol_stealth_model.internal_id).toBe('openai/gpt-5.6-sol:optimized'); expect(gpt_5_6_sol_stealth_model.gateway).toBe('martian'); expect(getInferenceProvider(gpt_5_6_sol_stealth_model)?.slug).toBe('stealth'); - expect(gpt_5_6_sol_stealth_model.pricing).toEqual([ + expect(gpt_5_6_sol_stealth_model.pricing?.tiers).toEqual([ { start_context_length: 0, pricing: { @@ -147,9 +147,9 @@ describe('isFreeModel', () => { test('all Kilo exclusive models should have either no pricing or valid ordered pricing tiers', () => { for (const model of kiloExclusiveModels) { if (model.pricing) { - expect(model.pricing[0].start_context_length).toBe(0); + expect(model.pricing.tiers[0].start_context_length).toBe(0); let previousStartContextLength = -1; - for (const tier of model.pricing) { + for (const tier of model.pricing.tiers) { expect(typeof tier.pricing.prompt_per_million).toBe('number'); expect(typeof tier.pricing.completion_per_million).toBe('number'); expect(tier.start_context_length).toBeGreaterThan(previousStartContextLength); diff --git a/apps/web/src/lib/ai-gateway/processUsage.calculateKiloExclusiveCost.test.ts b/apps/web/src/lib/ai-gateway/processUsage.calculateKiloExclusiveCost.test.ts index 12fa53e3ba..eb18df8f32 100644 --- a/apps/web/src/lib/ai-gateway/processUsage.calculateKiloExclusiveCost.test.ts +++ b/apps/web/src/lib/ai-gateway/processUsage.calculateKiloExclusiveCost.test.ts @@ -13,11 +13,19 @@ const makeUsage = (overrides: Partial = {}): JustTheCost ...overrides, }); +const fallbackOnlyModel = { + ...claude_opus_4_7_stealth_model, + pricing: claude_opus_4_7_stealth_model.pricing && { + ...claude_opus_4_7_stealth_model.pricing, + fallbackOnly: true, + }, +}; + describe('calculateKiloExclusiveCost_mUsd with stealth Claude Opus 4.7', () => { test('uses the 20% lower flat price for uncached tokens', () => { const result = calculateKiloExclusiveCost_mUsd( claude_opus_4_7_stealth_model, - makeUsage({ inputTokens: 100_000, outputTokens: 10_000 }) + makeUsage({ inputTokens: 100_000, outputTokens: 10_000, cost_mUsd: 999_000 }) ); expect(result).toBe(600_000); }); @@ -34,4 +42,20 @@ describe('calculateKiloExclusiveCost_mUsd with stealth Claude Opus 4.7', () => { ); expect(result).toBe(735_000); }); + + test('uses fallback-only pricing when market cost is missing', () => { + const result = calculateKiloExclusiveCost_mUsd( + fallbackOnlyModel, + makeUsage({ inputTokens: 100_000, outputTokens: 10_000 }) + ); + expect(result).toBe(600_000); + }); + + test('preserves reported market cost with fallback-only pricing', () => { + const result = calculateKiloExclusiveCost_mUsd( + fallbackOnlyModel, + makeUsage({ inputTokens: 100_000, outputTokens: 10_000, cost_mUsd: 123_000 }) + ); + expect(result).toBeUndefined(); + }); }); diff --git a/apps/web/src/lib/ai-gateway/processUsage.ts b/apps/web/src/lib/ai-gateway/processUsage.ts index 5100c6ac95..a8966f45dc 100644 --- a/apps/web/src/lib/ai-gateway/processUsage.ts +++ b/apps/web/src/lib/ai-gateway/processUsage.ts @@ -1188,11 +1188,14 @@ export function parseMicrodollarUsageFromString( export function calculateKiloExclusiveCost_mUsd( model: KiloExclusiveModel, usage: JustTheCostsUsageStats -): number { +): number | undefined { const pricing = model?.pricing; if (!pricing) { return 0; } + if (pricing.fallbackOnly && usage.cost_mUsd > 0) { + return undefined; + } const uncachedInputTokens = usage.inputTokens - usage.cacheHitTokens - usage.cacheWriteTokens; if (uncachedInputTokens < 0) { captureMessage('SUSPICIOUS: negative uncached input tokens', { @@ -1209,7 +1212,7 @@ export function calculateKiloExclusiveCost_mUsd( cacheHitTokens: usage.cacheHitTokens, cacheWriteTokens: usage.cacheWriteTokens, }, - pricing + pricing.tiers ) ); } @@ -1288,8 +1291,11 @@ export async function processTokenData( const kiloExclusiveModel = findKiloExclusiveModel(usageContext.requested_model); if (kiloExclusiveModel?.pricing) { - usageStats.market_cost = usageStats.cost_mUsd; - usageStats.cost_mUsd = calculateKiloExclusiveCost_mUsd(kiloExclusiveModel, usageStats); + const exclusiveCost_mUsd = calculateKiloExclusiveCost_mUsd(kiloExclusiveModel, usageStats); + if (exclusiveCost_mUsd !== undefined) { + usageStats.market_cost = usageStats.cost_mUsd; + usageStats.cost_mUsd = exclusiveCost_mUsd; + } } const customCost_mUsd = calculateCustomCost_mUsd(usageContext.requested_model, usageStats); diff --git a/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts b/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts index f74bf3f5d5..1de695e479 100644 --- a/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts +++ b/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts @@ -41,7 +41,7 @@ export const claude_opus_4_8_stealth_model: KiloExclusiveModel = { max_completion_tokens: 128_000, gateway: 'martian', flags: ['reasoning', 'vision', 'stealth', 'requires-data-collection'], - pricing: CLAUDE_OPUS_STEALTH_PRICING, + pricing: { tiers: CLAUDE_OPUS_STEALTH_PRICING }, inference_provider_restriction: [], }; @@ -56,7 +56,7 @@ export const claude_opus_4_7_stealth_model: KiloExclusiveModel = { max_completion_tokens: 128_000, gateway: 'martian', flags: ['reasoning', 'vision', 'stealth', 'requires-data-collection'], - pricing: CLAUDE_OPUS_STEALTH_PRICING, + pricing: { tiers: CLAUDE_OPUS_STEALTH_PRICING }, inference_provider_restriction: [], }; @@ -83,7 +83,7 @@ export const claude_sonnet_4_6_stealth_model: KiloExclusiveModel = { max_completion_tokens: 64_000, gateway: 'martian', flags: ['reasoning', 'vision', 'stealth', 'requires-data-collection'], - pricing: CLAUDE_SONNET_STEALTH_PRICING, + pricing: { tiers: CLAUDE_SONNET_STEALTH_PRICING }, inference_provider_restriction: [], }; @@ -98,7 +98,7 @@ export const claude_opus_4_6_stealth_model: KiloExclusiveModel = { max_completion_tokens: 128_000, gateway: 'martian', flags: ['reasoning', 'vision', 'stealth', 'requires-data-collection'], - pricing: CLAUDE_OPUS_STEALTH_PRICING, + pricing: { tiers: CLAUDE_OPUS_STEALTH_PRICING }, inference_provider_restriction: [], }; diff --git a/apps/web/src/lib/ai-gateway/providers/deepseek.ts b/apps/web/src/lib/ai-gateway/providers/deepseek.ts index 444d73884d..1f7a375b78 100644 --- a/apps/web/src/lib/ai-gateway/providers/deepseek.ts +++ b/apps/web/src/lib/ai-gateway/providers/deepseek.ts @@ -15,17 +15,19 @@ export const deepseek_v4_pro_discounted_model: KiloExclusiveModel = { max_completion_tokens: 384000, gateway: 'openrouter', flags: ['reasoning', 'requires-data-collection', 'vercel-routing'], - pricing: [ - { - start_context_length: 0, - pricing: { - prompt_per_million: 0.435, - completion_per_million: 0.87, - input_cache_read_per_million: 0.003625, - input_cache_write_per_million: null, + pricing: { + tiers: [ + { + start_context_length: 0, + pricing: { + prompt_per_million: 0.435, + completion_per_million: 0.87, + input_cache_read_per_million: 0.003625, + input_cache_write_per_million: null, + }, }, - }, - ], + ], + }, inference_provider_restriction: ['deepseek'], }; @@ -40,17 +42,19 @@ const deepseek_v4_flash_discounted_model: KiloExclusiveModel = { max_completion_tokens: 384000, gateway: 'openrouter', flags: ['reasoning', 'requires-data-collection', 'vercel-routing'], - pricing: [ - { - start_context_length: 0, - pricing: { - prompt_per_million: 0.14, - completion_per_million: 0.28, - input_cache_read_per_million: 0.0028, - input_cache_write_per_million: null, + pricing: { + tiers: [ + { + start_context_length: 0, + pricing: { + prompt_per_million: 0.14, + completion_per_million: 0.28, + input_cache_read_per_million: 0.0028, + input_cache_write_per_million: null, + }, }, - }, - ], + ], + }, inference_provider_restriction: ['deepseek'], }; diff --git a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts index be6ddb2b55..acffdf51ac 100644 --- a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts @@ -93,10 +93,24 @@ describe('convertFromKiloExclusiveModel', () => { it('only exposes the cheapest pricing tier in model metadata', () => { const model = makeModel({ internal_id: 'vendor/x', - pricing: [ - { start_context_length: 0, pricing: pricing(1) }, - { start_context_length: 100, pricing: pricing(2) }, - ], + pricing: { + tiers: [ + { start_context_length: 0, pricing: pricing(1) }, + { start_context_length: 100, pricing: pricing(2) }, + ], + }, + }); + + expect(convertFromKiloExclusiveModel(model).pricing.prompt).toBe('0.000001000000'); + }); + + it('exposes fallback-only pricing in model metadata', () => { + const model = makeModel({ + internal_id: 'vendor/x', + pricing: { + fallbackOnly: true, + tiers: [{ start_context_length: 0, pricing: pricing(1) }], + }, }); expect(convertFromKiloExclusiveModel(model).pricing.prompt).toBe('0.000001000000'); diff --git a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts index 6a411b8564..ed498298f1 100644 --- a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts +++ b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts @@ -37,6 +37,12 @@ export type PricingTier = { export type PricingTiers = readonly [PricingTier, ...PricingTier[]]; +export type KiloExclusivePricing = { + tiers: PricingTiers; + /** Only use these tiers when the upstream response does not report a positive market cost. */ + fallbackOnly?: boolean; +}; + export function calculateCost_mUsd(usage: Usage, pricingTiers: PricingTiers): number { if (pricingTiers[0].start_context_length !== 0) { throw new Error('The first pricing tier must start at context length 0'); @@ -75,7 +81,7 @@ export type KiloExclusiveModel = { flags: KiloExclusiveModelFlag[]; gateway: ProviderId; internal_id: string; - pricing: PricingTiers | null; + pricing: KiloExclusivePricing | null; /** * Upstream inference providers this model may be routed to; empty means no * restriction. Only honored by the OpenRouter and Vercel AI Gateway upstreams. @@ -207,7 +213,7 @@ function formatPricePerMillionAsPerToken(price: number | null | undefined): stri } export function convertFromKiloExclusiveModel(model: KiloExclusiveModel) { - const cheapestPricing = model.pricing?.[0].pricing; + const cheapestPricing = model.pricing?.tiers[0].pricing; const isFree = !model.pricing; return { id: model.public_id, diff --git a/apps/web/src/lib/ai-gateway/providers/openai-exclusive.ts b/apps/web/src/lib/ai-gateway/providers/openai-exclusive.ts index d9ca20032c..81b28d6271 100644 --- a/apps/web/src/lib/ai-gateway/providers/openai-exclusive.ts +++ b/apps/web/src/lib/ai-gateway/providers/openai-exclusive.ts @@ -11,25 +11,27 @@ export const gpt_5_6_sol_stealth_model: KiloExclusiveModel = { max_completion_tokens: 128_000, gateway: 'martian', flags: ['reasoning', 'vision', 'stealth', 'requires-data-collection'], - pricing: [ - { - start_context_length: 0, - pricing: { - prompt_per_million: 4, - completion_per_million: 24, - input_cache_read_per_million: 0.4, - input_cache_write_per_million: 5, + pricing: { + tiers: [ + { + start_context_length: 0, + pricing: { + prompt_per_million: 4, + completion_per_million: 24, + input_cache_read_per_million: 0.4, + input_cache_write_per_million: 5, + }, }, - }, - { - start_context_length: 272_000, - pricing: { - prompt_per_million: 8, - completion_per_million: 36, - input_cache_read_per_million: 0.8, - input_cache_write_per_million: 10, + { + start_context_length: 272_000, + pricing: { + prompt_per_million: 8, + completion_per_million: 36, + input_cache_read_per_million: 0.8, + input_cache_write_per_million: 10, + }, }, - }, - ], + ], + }, inference_provider_restriction: [], }; diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/free-endpoint-data-policy.test.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/free-endpoint-data-policy.test.ts index 9fdb02fe55..a6982da457 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/free-endpoint-data-policy.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/free-endpoint-data-policy.test.ts @@ -50,17 +50,19 @@ function dataCollectionExclusiveModel( inference_provider_restriction: KiloExclusiveModel['inference_provider_restriction'] ): KiloExclusiveModel { const model = freeExclusiveModel(public_id, inference_provider_restriction); - model.pricing = [ - { - start_context_length: 0, - pricing: { - prompt_per_million: 1, - completion_per_million: 1, - input_cache_read_per_million: null, - input_cache_write_per_million: null, + model.pricing = { + tiers: [ + { + start_context_length: 0, + pricing: { + prompt_per_million: 1, + completion_per_million: 1, + input_cache_read_per_million: null, + input_cache_write_per_million: null, + }, }, - }, - ]; + ], + }; model.flags = ['requires-data-collection']; return model; } @@ -191,17 +193,19 @@ describe('applyFreeEndpointDataPolicy', () => { const disabled = freeExclusiveModel('example/model:free', []); disabled.status = 'disabled'; const paid = freeExclusiveModel('example/model:discounted', []); - paid.pricing = [ - { - start_context_length: 0, - pricing: { - prompt_per_million: 1, - completion_per_million: 1, - input_cache_read_per_million: null, - input_cache_write_per_million: null, + paid.pricing = { + tiers: [ + { + start_context_length: 0, + pricing: { + prompt_per_million: 1, + completion_per_million: 1, + input_cache_read_per_million: null, + input_cache_write_per_million: null, + }, }, - }, - ]; + ], + }; applyFreeEndpointDataPolicy({ providerModelData: [{ provider: { slug: 'novita' }, models: [model] }], diff --git a/apps/web/src/lib/ai-gateway/providers/qwen.ts b/apps/web/src/lib/ai-gateway/providers/qwen.ts index 7578ce1ed1..eeb6210d18 100644 --- a/apps/web/src/lib/ai-gateway/providers/qwen.ts +++ b/apps/web/src/lib/ai-gateway/providers/qwen.ts @@ -59,29 +59,31 @@ export const qwen36_plus_stealth_model: KiloExclusiveModel = { flags: ['reasoning', 'vision', 'stealth', 'requires-data-collection'], gateway: 'martian', internal_id: 'qwen/qwen3.6-plus', - pricing: makeTieredPricing( - [ - { - start_context_length: 0, - pricing: { - prompt_per_million: 0.5, - completion_per_million: 3, - input_cache_read_per_million: 0.05, - input_cache_write_per_million: 0.625, + pricing: { + tiers: makeTieredPricing( + [ + { + start_context_length: 0, + pricing: { + prompt_per_million: 0.5, + completion_per_million: 3, + input_cache_read_per_million: 0.05, + input_cache_write_per_million: 0.625, + }, }, - }, - { - start_context_length: TOKENS_256K, - pricing: { - prompt_per_million: 2, - completion_per_million: 6, - input_cache_read_per_million: 0.2, - input_cache_write_per_million: 2.5, + { + start_context_length: TOKENS_256K, + pricing: { + prompt_per_million: 2, + completion_per_million: 6, + input_cache_read_per_million: 0.2, + input_cache_write_per_million: 2.5, + }, }, - }, - ], - KILO_STEALTH_DISCOUNT_FACTOR - ), + ], + KILO_STEALTH_DISCOUNT_FACTOR + ), + }, inference_provider_restriction: [], };