fix(pricing): include codex and gemini model costs

This commit is contained in:
iliya 2026-04-02 10:23:57 +03:00
parent 3ac46e2861
commit 759cae2669
4 changed files with 576 additions and 112 deletions

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
/** /**
* Fetch latest model pricing from LiteLLM and save to renderer assets. * Fetch latest model pricing from LiteLLM and save to renderer assets.
* Filters to Claude models only to reduce bundle size. * Filters to the models this app currently exposes in the UI/runtime to reduce bundle size.
* Runs automatically during prebuild. * Runs automatically during prebuild.
*/ */
@ -41,11 +41,28 @@ function isValidModelPricing(entry: unknown): entry is ModelPricing {
); );
} }
const EXPLICIT_MODEL_ALLOWLIST = new Set([
'gpt-5.4',
'gpt-5.4-mini',
'gpt-5.3-codex',
'gpt-5.2-codex',
'gpt-5.2',
'gpt-5.1-codex-max',
'gpt-5.1-codex-mini',
'gemini-2.5-pro',
'gemini-2.5-flash',
'gemini-2.5-flash-lite',
]);
function isClaudeModel(modelName: string): boolean { function isClaudeModel(modelName: string): boolean {
const lower = modelName.toLowerCase(); const lower = modelName.toLowerCase();
return lower.includes('claude'); return lower.includes('claude');
} }
function isIncludedModel(modelName: string): boolean {
return isClaudeModel(modelName) || EXPLICIT_MODEL_ALLOWLIST.has(modelName);
}
async function fetchPricingData(): Promise<Record<string, ModelPricing>> { async function fetchPricingData(): Promise<Record<string, ModelPricing>> {
console.log('Fetching pricing data from LiteLLM...'); console.log('Fetching pricing data from LiteLLM...');
@ -63,16 +80,22 @@ async function fetchPricingData(): Promise<Record<string, ModelPricing>> {
const data = (await response.json()) as Record<string, unknown>; const data = (await response.json()) as Record<string, unknown>;
console.log(`Fetched pricing for ${Object.keys(data).length} models`); console.log(`Fetched pricing for ${Object.keys(data).length} models`);
// Filter to Claude models only and validate entries // Filter to the models currently exposed by this app and validate entries.
const claudeModels: Record<string, ModelPricing> = {}; const selectedModels: Record<string, ModelPricing> = {};
for (const [modelName, entry] of Object.entries(data)) { for (const [modelName, entry] of Object.entries(data)) {
if (isClaudeModel(modelName) && isValidModelPricing(entry)) { if (isIncludedModel(modelName) && isValidModelPricing(entry)) {
claudeModels[modelName] = entry; selectedModels[modelName] = entry;
} }
} }
console.log(`Filtered to ${Object.keys(claudeModels).length} Claude models`); // LiteLLM currently publishes no priced top-level entry for gpt-5.3-codex-spark.
return claudeModels; // Keep cost estimation non-zero by aliasing it to the closest published Codex tier.
if (!selectedModels['gpt-5.3-codex-spark'] && selectedModels['gpt-5.3-codex']) {
selectedModels['gpt-5.3-codex-spark'] = selectedModels['gpt-5.3-codex'];
}
console.log(`Filtered to ${Object.keys(selectedModels).length} supported models`);
return selectedModels;
} catch (error) { } catch (error) {
clearTimeout(timeout); clearTimeout(timeout);
if (error instanceof Error && error.name === 'AbortError') { if (error instanceof Error && error.name === 'AbortError') {

View file

@ -22,6 +22,9 @@ export interface DisplayPricing {
const TIER_THRESHOLD = 200_000; const TIER_THRESHOLD = 200_000;
const PRICING_MAP = pricingData as Record<string, unknown>; const PRICING_MAP = pricingData as Record<string, unknown>;
const PRICING_ALIASES: Record<string, string> = {
'gpt-5.3-codex-spark': 'gpt-5.3-codex',
};
// Pre-compute lowercase key map for O(1) case-insensitive lookups // Pre-compute lowercase key map for O(1) case-insensitive lookups
const LOWERCASE_KEY_MAP = new Map<string, string>(); const LOWERCASE_KEY_MAP = new Map<string, string>();
@ -55,6 +58,12 @@ export function getPricing(modelName: string): LiteLLMPricing | null {
return tryGetPricing(originalKey); return tryGetPricing(originalKey);
} }
const alias = PRICING_ALIASES[lowerName];
if (alias) {
const aliased = tryGetPricing(alias);
if (aliased) return aliased;
}
return null; return null;
} }

View file

@ -20,6 +20,27 @@ describe('Shared Pricing Module', () => {
expect(pricing).not.toBeNull(); expect(pricing).not.toBeNull();
}); });
it('should find pricing for codex runtime models', () => {
const pricing = getPricing('gpt-5.1-codex-mini');
expect(pricing).not.toBeNull();
expect(pricing!.input_cost_per_token).toBeGreaterThan(0);
expect(pricing!.output_cost_per_token).toBeGreaterThan(0);
});
it('should alias codex spark pricing to the closest published codex tier', () => {
const sparkPricing = getPricing('gpt-5.3-codex-spark');
const codexPricing = getPricing('gpt-5.3-codex');
expect(sparkPricing).not.toBeNull();
expect(sparkPricing).toEqual(codexPricing);
});
it('should find pricing for gemini runtime models', () => {
const pricing = getPricing('gemini-2.5-flash-lite');
expect(pricing).not.toBeNull();
expect(pricing!.input_cost_per_token).toBeGreaterThan(0);
expect(pricing!.output_cost_per_token).toBeGreaterThan(0);
});
it('should return null for unknown models', () => { it('should return null for unknown models', () => {
const pricing = getPricing('totally-fake-model-xyz'); const pricing = getPricing('totally-fake-model-xyz');
expect(pricing).toBeNull(); expect(pricing).toBeNull();