Skip to content

Commit 3ef5f9c

Browse files
gulivanryoppippi
andauthored
fix(amp): fallback missing model pricing to zero-cost in codex/amp (#858)
* fix: fallback missing model pricing to zero-cost in codex/amp * refactor(codex,amp): use `as const satisfies` for zero-cost pricing constants Replace explicit type annotations with `as const satisfies ModelPricing` for FREE_MODEL_PRICING (codex) and ZERO_MODEL_PRICING (amp) to get narrower literal types while maintaining type safety. --------- Co-authored-by: ryoppippi <1560508+ryoppippi@users.noreply.github.com>
1 parent 69af5a2 commit 3ef5f9c

File tree

2 files changed

+61
-2
lines changed

2 files changed

+61
-2
lines changed

apps/amp/src/pricing.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import { prefetchAmpPricing } from './_macro.ts' with { type: 'macro' };
77
import { logger } from './logger.ts';
88

99
const AMP_PROVIDER_PREFIXES = ['anthropic/'];
10+
const ZERO_MODEL_PRICING = {
11+
inputCostPerMToken: 0,
12+
cachedInputCostPerMToken: 0,
13+
cacheCreationCostPerMToken: 0,
14+
outputCostPerMToken: 0,
15+
} as const satisfies ModelPricing;
1016

1117
function toPerMillion(value: number | undefined, fallback?: number): number {
1218
const perToken = value ?? fallback ?? 0;
@@ -44,7 +50,8 @@ export class AmpPricingSource implements PricingSource, Disposable {
4450

4551
const pricing = directLookup.value;
4652
if (pricing == null) {
47-
throw new Error(`Pricing not found for model ${model}`);
53+
logger.warn(`Pricing not found for model ${model}; defaulting to zero-cost pricing.`);
54+
return ZERO_MODEL_PRICING;
4855
}
4956

5057
return {
@@ -134,5 +141,15 @@ if (import.meta.vitest != null) {
134141
const expected = 1000 * 1e-6 + 500 * 5e-6 + 200 * 1e-7 + 100 * 1.25e-6;
135142
expect(cost).toBeCloseTo(expected);
136143
});
144+
145+
it('falls back to zero pricing for unknown models', async () => {
146+
using source = new AmpPricingSource({
147+
offline: true,
148+
offlineLoader: async () => ({}),
149+
});
150+
151+
const pricing = await source.getPricing('anthropic/unknown');
152+
expect(pricing).toEqual(ZERO_MODEL_PRICING);
153+
});
137154
});
138155
}

apps/codex/src/pricing.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@ const CODEX_MODEL_ALIASES_MAP = new Map<string, string>([
1111
['gpt-5-codex', 'gpt-5'],
1212
['gpt-5.3-codex', 'gpt-5.2-codex'],
1313
]);
14+
const FREE_MODEL_PRICING = {
15+
inputCostPerMToken: 0,
16+
cachedInputCostPerMToken: 0,
17+
outputCostPerMToken: 0,
18+
} as const satisfies ModelPricing;
19+
20+
function isOpenRouterFreeModel(model: string): boolean {
21+
const normalized = model.trim().toLowerCase();
22+
if (normalized === 'openrouter/free') {
23+
return true;
24+
}
25+
26+
return normalized.startsWith('openrouter/') && normalized.endsWith(':free');
27+
}
1428

1529
function hasNonZeroTokenPricing(pricing: LiteLLMModelPricing): boolean {
1630
return (
@@ -49,6 +63,10 @@ export class CodexPricingSource implements PricingSource, Disposable {
4963
}
5064

5165
async getPricing(model: string): Promise<ModelPricing> {
66+
if (isOpenRouterFreeModel(model)) {
67+
return FREE_MODEL_PRICING;
68+
}
69+
5270
const directLookup = await this.fetcher.getModelPricing(model);
5371
if (Result.isFailure(directLookup)) {
5472
throw directLookup.error;
@@ -67,7 +85,8 @@ export class CodexPricingSource implements PricingSource, Disposable {
6785
}
6886

6987
if (pricing == null) {
70-
throw new Error(`Pricing not found for model ${model}`);
88+
logger.warn(`Pricing not found for model ${model}; defaulting to zero-cost pricing.`);
89+
return FREE_MODEL_PRICING;
7190
}
7291

7392
return {
@@ -101,6 +120,29 @@ if (import.meta.vitest != null) {
101120
expect(pricing.cachedInputCostPerMToken).toBeCloseTo(0.125);
102121
});
103122

123+
it('returns zero pricing for OpenRouter free routes', async () => {
124+
using source = new CodexPricingSource({
125+
offline: true,
126+
offlineLoader: async () => ({}),
127+
});
128+
129+
const directFree = await source.getPricing('openrouter/free');
130+
expect(directFree).toEqual(FREE_MODEL_PRICING);
131+
132+
const modelFree = await source.getPricing('openrouter/openai/gpt-5:free');
133+
expect(modelFree).toEqual(FREE_MODEL_PRICING);
134+
});
135+
136+
it('falls back to zero pricing for unknown non-free models', async () => {
137+
using source = new CodexPricingSource({
138+
offline: true,
139+
offlineLoader: async () => ({}),
140+
});
141+
142+
const pricing = await source.getPricing('openrouter/unknown');
143+
expect(pricing).toEqual(FREE_MODEL_PRICING);
144+
});
145+
104146
it('falls back to alias pricing when direct model pricing is all zeros', async () => {
105147
using source = new CodexPricingSource({
106148
offline: true,

0 commit comments

Comments
 (0)