mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
fix(api): consult LiteLLM pricing_synced layer in resolveCatalogPricing so deployed models absent from models.dev and defaults get pricing in /v1/models (#9364)
Closes #9364
This commit is contained in:
committed by
GitHub
parent
e64eecf852
commit
ee4cd0d795
1
changelog.d/fixes/9364-models-pricing-gap.md
Normal file
1
changelog.d/fixes/9364-models-pricing-gap.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(api): consult LiteLLM pricing_synced layer in resolveCatalogPricing so deployed models absent from models.dev and defaults get pricing in /v1/models (#9364)
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { AI_PROVIDERS } from "@/shared/constants/providers";
|
||||
import { PROVIDER_ID_TO_ALIAS, PROVIDER_MODELS } from "@/shared/constants/models";
|
||||
import { getSyncStatus, getSyncedCapability, getModelsDevPricing } from "@/lib/modelsDevSync";
|
||||
import { getSyncedPricing } from "@/lib/pricingSync";
|
||||
import { getPricingForModel as getDefaultPricingForModel } from "@/shared/constants/pricing";
|
||||
import {
|
||||
CANONICAL_EFFORT_VALUES,
|
||||
@@ -335,6 +336,45 @@ function resolveCatalogPricing(
|
||||
// pricing lookup must never break catalog assembly
|
||||
}
|
||||
|
||||
// LiteLLM-synced pricing (`pricing_synced` namespace) — Layer 3 in the
|
||||
// documented resolution order (user > models.dev > LiteLLM > defaults).
|
||||
// Consulted only when models.dev returned nothing, matching the order
|
||||
// already implemented in db/settings/pricing.ts::getPricing().
|
||||
try {
|
||||
const litellm = getSyncedPricing() as Record<
|
||||
string,
|
||||
Record<string, Record<string, number>>
|
||||
>;
|
||||
const providerPricing =
|
||||
findInsensitive(litellm, provider) ||
|
||||
findInsensitive(litellm, provider.replace(/-cn$/, ""));
|
||||
if (providerPricing) {
|
||||
const modelPricing =
|
||||
findInsensitive(providerPricing, model) ||
|
||||
findInsensitive(providerPricing, model.replace(/\./g, "-")) ||
|
||||
findInsensitive(
|
||||
providerPricing,
|
||||
model.includes("/") ? model.split("/").pop() || model : model
|
||||
);
|
||||
if (modelPricing && typeof modelPricing === "object") {
|
||||
const input = modelPricing.input;
|
||||
const output = modelPricing.output;
|
||||
if (typeof input === "number" || typeof output === "number") {
|
||||
const pricing: Record<string, number> = {};
|
||||
if (typeof input === "number") pricing.input = input;
|
||||
if (typeof output === "number") pricing.output = output;
|
||||
if (typeof modelPricing.cached === "number") pricing.cached = modelPricing.cached;
|
||||
if (typeof modelPricing.cache_creation === "number") {
|
||||
pricing.cache_creation = modelPricing.cache_creation;
|
||||
}
|
||||
return pricing;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// pricing lookup must never break catalog assembly
|
||||
}
|
||||
|
||||
try {
|
||||
const defaults = getDefaultPricingForModel(provider, model) as Record<string, number> | null;
|
||||
if (defaults && (typeof defaults.input === "number" || typeof defaults.output === "number")) {
|
||||
|
||||
81
tests/unit/model-pricing-litellm-gap-9364.test.ts
Normal file
81
tests/unit/model-pricing-litellm-gap-9364.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it, before, after } from "node:test";
|
||||
import { enrichCatalogModelEntry } from "../../src/lib/modelMetadataRegistry.ts";
|
||||
import {
|
||||
saveModelsDevPricing,
|
||||
clearModelsDevPricing,
|
||||
type PricingByProvider as ModelsDevPricingByProvider,
|
||||
} from "../../src/lib/modelsDevSync.ts";
|
||||
import {
|
||||
saveSyncedPricing,
|
||||
clearSyncedPricing,
|
||||
type PricingByProvider as SyncedPricingByProvider,
|
||||
} from "../../src/lib/pricingSync.ts";
|
||||
|
||||
type CatalogPricing = {
|
||||
input?: number;
|
||||
output?: number;
|
||||
cached?: number;
|
||||
cache_creation?: number;
|
||||
};
|
||||
|
||||
// #9364: resolveCatalogPricing() only consults models_dev_pricing and hardcoded
|
||||
// defaults, skipping the LiteLLM `pricing_synced` namespace entirely. A model
|
||||
// whose pricing exists ONLY in pricing_synced (the documented Layer 3) gets
|
||||
// `pricing: null` in the /v1/models catalog. This test seeds pricing_synced
|
||||
// with pricing for a model absent from both models_dev_pricing and hardcoded
|
||||
// defaults, then asserts enrichCatalogModelEntry() surfaces it.
|
||||
|
||||
describe("catalog pricing LiteLLM gap (#9364)", () => {
|
||||
before(() => {
|
||||
// Seed ONLY the LiteLLM namespace with a model that is absent from both
|
||||
// models.dev and hardcoded defaults (babbage-002 is not in default-pricing
|
||||
// and not registered in the provider registry, so neither layer can match).
|
||||
const synced: SyncedPricingByProvider = {
|
||||
openai: {
|
||||
"babbage-002": { input: 0.4, output: 0.4 },
|
||||
},
|
||||
};
|
||||
saveSyncedPricing(synced);
|
||||
|
||||
// Ensure models_dev_pricing has a different model so we prove the LiteLLM
|
||||
// layer is being consulted, not accidentally overlapping with models.dev.
|
||||
const modelsDev: ModelsDevPricingByProvider = {
|
||||
openai: {
|
||||
"gpt-4o": { input: 2.5, output: 10 },
|
||||
},
|
||||
};
|
||||
saveModelsDevPricing(modelsDev);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
try {
|
||||
clearSyncedPricing();
|
||||
clearModelsDevPricing();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
it("attaches LiteLLM-synced pricing onto catalog entries absent from models.dev and defaults", () => {
|
||||
const entry = enrichCatalogModelEntry({
|
||||
id: "openai/babbage-002",
|
||||
owned_by: "openai",
|
||||
root: "babbage-002",
|
||||
});
|
||||
assert.ok(entry.pricing, "pricing should resolve from pricing_synced (LiteLLM) layer");
|
||||
assert.equal((entry.pricing as CatalogPricing).input, 0.4);
|
||||
assert.equal((entry.pricing as CatalogPricing).output, 0.4);
|
||||
});
|
||||
|
||||
it("still resolves models.dev pricing when present (precedence preserved)", () => {
|
||||
const entry = enrichCatalogModelEntry({
|
||||
id: "openai/gpt-4o",
|
||||
owned_by: "openai",
|
||||
root: "gpt-4o",
|
||||
});
|
||||
assert.ok(entry.pricing);
|
||||
assert.equal((entry.pricing as CatalogPricing).input, 2.5);
|
||||
assert.equal((entry.pricing as CatalogPricing).output, 10);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user