diff --git a/apps/server/src/usage/usagePricing.test.ts b/apps/server/src/usage/usagePricing.test.ts index 2ce6d6f74eff..b22625b7e490 100644 --- a/apps/server/src/usage/usagePricing.test.ts +++ b/apps/server/src/usage/usagePricing.test.ts @@ -258,4 +258,32 @@ describe("usage pricing", () => { expect(lookupRate(table, "provider-b/example-model")?.inputCostPerToken).toBe(3); expect(lookupRate(table, "example-model")).toBeNull(); }); + + it("prices a bare name at the model maker's rate when a reseller marks it up", () => { + const reseller = { "kyma/gemini-4-argon": rate(5.4e-6) }; + const makers = { + "gemini/gemini-4-argon": rate(2e-6, 1e-7), + "vertex_ai/gemini-4-argon": rate(2e-6, 1e-7), + }; + + for (const document of [ + { ...reseller, ...makers }, + { ...makers, ...reseller }, + ]) { + const table = parseRateTable(document); + expect(lookupRate(table, "gemini-4-argon")).toEqual( + lookupRate(table, "gemini/gemini-4-argon"), + ); + } + }); + + it("leaves a bare name unpriced when model makers' own entries disagree", () => { + const table = parseRateTable({ + "openai/example-model": rate(1), + "xai/example-model": rate(3), + "kyma/example-model": rate(1), + }); + + expect(lookupRate(table, "example-model")).toBeNull(); + }); }); diff --git a/apps/server/src/usage/usagePricing.ts b/apps/server/src/usage/usagePricing.ts index fa512218d7c0..1f515abb736c 100644 --- a/apps/server/src/usage/usagePricing.ts +++ b/apps/server/src/usage/usagePricing.ts @@ -89,6 +89,17 @@ function fastMultiplier(entry: LiteLlmEntry): number | null { return fast !== null && fast > 0 ? fast : null; } +// LiteLLM prefixes for the model makers' own APIs; their price beats a cloud's or reseller's for a bare name. +const FIRST_PARTY_PROVIDERS = new Set([ + "anthropic", + "deepseek", + "gemini", + "meta", + "mistral", + "openai", + "xai", +]); + /** Drops models missing input or output rates to avoid understating cost. */ export function parseRateTable(document: unknown): RateTable { const table = new Map(); @@ -113,18 +124,19 @@ export function parseRateTable(document: unknown): RateTable { }); } - const aliasCandidates = new Map(); + const aliasCandidates = new Map(); for (const [key, rate] of table) { const alias = bareModelName(key); if (alias.length === 0 || alias === key || table.has(alias)) continue; + const firstParty = FIRST_PARTY_PROVIDERS.has(key.slice(0, key.indexOf("/"))); const held = aliasCandidates.get(alias); - if (held === undefined) { - aliasCandidates.set(alias, rate); - } else if (held !== null) { - aliasCandidates.set(alias, mergeAliasRate(held, rate)); + if (held === undefined || (firstParty && !held.firstParty)) { + aliasCandidates.set(alias, { rate, firstParty }); + } else if (held.rate !== null && firstParty === held.firstParty) { + aliasCandidates.set(alias, { rate: mergeAliasRate(held.rate, rate), firstParty }); } } - for (const [alias, rate] of aliasCandidates) { + for (const [alias, { rate }] of aliasCandidates) { if (rate !== null) table.set(alias, rate); }