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.
* 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.
*/
@ -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 {
const lower = modelName.toLowerCase();
return lower.includes('claude');
}
function isIncludedModel(modelName: string): boolean {
return isClaudeModel(modelName) || EXPLICIT_MODEL_ALLOWLIST.has(modelName);
}
async function fetchPricingData(): Promise<Record<string, ModelPricing>> {
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>;
console.log(`Fetched pricing for ${Object.keys(data).length} models`);
// Filter to Claude models only and validate entries
const claudeModels: Record<string, ModelPricing> = {};
// Filter to the models currently exposed by this app and validate entries.
const selectedModels: Record<string, ModelPricing> = {};
for (const [modelName, entry] of Object.entries(data)) {
if (isClaudeModel(modelName) && isValidModelPricing(entry)) {
claudeModels[modelName] = entry;
if (isIncludedModel(modelName) && isValidModelPricing(entry)) {
selectedModels[modelName] = entry;
}
}
console.log(`Filtered to ${Object.keys(claudeModels).length} Claude models`);
return claudeModels;
// LiteLLM currently publishes no priced top-level entry for gpt-5.3-codex-spark.
// 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) {
clearTimeout(timeout);
if (error instanceof Error && error.name === 'AbortError') {

View file

@ -22,6 +22,9 @@ export interface DisplayPricing {
const TIER_THRESHOLD = 200_000;
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
const LOWERCASE_KEY_MAP = new Map<string, string>();
@ -55,6 +58,12 @@ export function getPricing(modelName: string): LiteLLMPricing | null {
return tryGetPricing(originalKey);
}
const alias = PRICING_ALIASES[lowerName];
if (alias) {
const aliased = tryGetPricing(alias);
if (aliased) return aliased;
}
return null;
}

View file

@ -20,6 +20,27 @@ describe('Shared Pricing Module', () => {
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', () => {
const pricing = getPricing('totally-fake-model-xyz');
expect(pricing).toBeNull();