Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/code/src/components/CodingModelsShowcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export function CodingModelsShowcase({
{provider.inputPrice !== undefined && (
<>
<span className="font-mono font-medium text-foreground">
${formatPrice(provider.inputPrice)}
${formatPrice(Number(provider.inputPrice))}
</span>
<span className="text-muted-foreground"> in</span>
</>
Expand All @@ -144,7 +144,7 @@ export function CodingModelsShowcase({
<>
<span className="text-muted-foreground mx-1">/</span>
<span className="font-mono font-medium text-foreground">
${formatPrice(provider.outputPrice)}
${formatPrice(Number(provider.outputPrice))}
</span>
<span className="text-muted-foreground"> out</span>
</>
Expand Down
3 changes: 2 additions & 1 deletion apps/gateway/src/audio.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ describe("e2e audio input", getTestOptions(), () => {
expect(typeof audioInputCost).toBe("number");

const expected =
audioTokens * (provider.inputAudioPrice ?? provider.inputPrice ?? 0);
audioTokens *
Number(provider.inputAudioPrice ?? provider.inputPrice ?? "0");
expect(audioInputCost).toBeCloseTo(expected, 8);
},
);
Expand Down
4 changes: 2 additions & 2 deletions apps/gateway/src/chat-prompt-caching.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,8 @@ describe("e2e prompt caching", getConcurrentTestOptions(), () => {

if (providerMapping?.cachedInputPrice && providerMapping?.inputPrice) {
// Verify the pricing ratio is correct (cached should be cheaper)
expect(providerMapping.cachedInputPrice).toBeLessThan(
providerMapping.inputPrice,
expect(Number(providerMapping.cachedInputPrice)).toBeLessThan(
Number(providerMapping.inputPrice),
);
}
},
Expand Down
20 changes: 11 additions & 9 deletions apps/gateway/src/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1838,7 +1838,9 @@ chat.openapi(completions, async (c) => {
// Find the cheapest among the suitable providers for this model
for (const provider of suitableProviders) {
const totalPrice =
((provider.inputPrice ?? 0) + (provider.outputPrice ?? 0)) / 2;
(Number(provider.inputPrice ?? "0") +
Number(provider.outputPrice ?? "0")) /
2;

if (totalPrice < lowestPrice) {
lowestPrice = totalPrice;
Expand Down Expand Up @@ -2276,8 +2278,8 @@ chat.openapi(completions, async (c) => {
(p) => p.providerId === requestedProvider,
);
const originalProviderPrice = originalProviderInfo
? (originalProviderInfo.inputPrice ?? 0) +
(originalProviderInfo.outputPrice ?? 0)
? Number(originalProviderInfo.inputPrice ?? "0") +
Number(originalProviderInfo.outputPrice ?? "0")
: 0;

const originalProviderScore = {
Expand Down Expand Up @@ -2467,8 +2469,8 @@ chat.openapi(completions, async (c) => {
(p) => p.providerId === requestedProvider,
);
const originalProviderPrice = originalProviderInfo
? (originalProviderInfo.inputPrice ?? 0) +
(originalProviderInfo.outputPrice ?? 0)
? Number(originalProviderInfo.inputPrice ?? "0") +
Number(originalProviderInfo.outputPrice ?? "0")
: 0;

// Create score entry for the original requested provider
Expand Down Expand Up @@ -2691,8 +2693,8 @@ chat.openapi(completions, async (c) => {
providerId: rlProviderId,
score: -1,
price: providerInfo
? (providerInfo.inputPrice ?? 0) +
(providerInfo.outputPrice ?? 0)
? Number(providerInfo.inputPrice ?? "0") +
Number(providerInfo.outputPrice ?? "0")
: 0,
rate_limited: true,
});
Expand Down Expand Up @@ -2889,8 +2891,8 @@ chat.openapi(completions, async (c) => {
{
providerId: "custom" as const,
modelName: usedModel,
inputPrice: 0,
outputPrice: 0,
inputPrice: "0",
outputPrice: "0",
contextSize: 8192,
maxOutput: 4096,
streaming: true,
Expand Down
4 changes: 2 additions & 2 deletions apps/gateway/src/chat/tools/is-model-truly-free.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ export function isModelTrulyFree(modelInfo: ModelDefinition): boolean {
}
return !modelInfo.providers.some((provider) => {
const hasRequestPrice =
provider.requestPrice !== undefined && provider.requestPrice > 0;
provider.requestPrice !== undefined && Number(provider.requestPrice) > 0;
const hasPerSecondPrice = Object.values(provider.perSecondPrice ?? {}).some(
(price) => price > 0,
(price) => Number(price) > 0,
);
return hasRequestPrice || hasPerSecondPrice;
});
Expand Down
4 changes: 2 additions & 2 deletions apps/gateway/src/chat/tools/resolve-model-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export function resolveModelInfo(
{
providerId: "custom" as const,
modelName: requestedModel,
inputPrice: 0,
outputPrice: 0,
inputPrice: "0",
outputPrice: "0",
contextSize: 8192,
maxOutput: 4096,
streaming: true,
Expand Down
9 changes: 5 additions & 4 deletions apps/gateway/src/lib/anthropic-pricing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ const LEGACY_RATIO_EXCEPTIONS = new Set(["claude-3-haiku-20240307"]);
function assertRatio(
modelName: string,
label: string,
actual: number,
actualStr: string,
expected: number,
) {
const actual = Number(actualStr);
expect(
actual,
`${modelName} ${label}: expected ${expected} (got ${actual}). If Anthropic's published price diverges from the standard multiplier, add the modelName to LEGACY_RATIO_EXCEPTIONS.`,
Expand Down Expand Up @@ -78,7 +79,7 @@ describe("Anthropic model pricing", () => {
if (provider.inputPrice === undefined) {
return;
}
const base = provider.inputPrice;
const base = Number(provider.inputPrice);
if (provider.cacheWriteInputPrice !== undefined) {
assertRatio(
provider.modelName,
Expand Down Expand Up @@ -107,7 +108,7 @@ describe("Anthropic model pricing", () => {
if (tier.inputPrice === undefined) {
continue;
}
const tierBase = tier.inputPrice;
const tierBase = Number(tier.inputPrice);
const label = `tier "${tier.name}"`;
if (tier.cacheWriteInputPrice !== undefined) {
assertRatio(
Expand Down Expand Up @@ -197,7 +198,7 @@ describe("AWS Bedrock Anthropic model pricing", () => {
if (provider.inputPrice === undefined) {
return;
}
const base = provider.inputPrice;
const base = Number(provider.inputPrice);
if (provider.cacheWriteInputPrice !== undefined) {
assertRatio(
provider.modelName,
Expand Down
6 changes: 3 additions & 3 deletions apps/gateway/src/lib/coding-models.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const baseProvider: ProviderModelMapping = {
streaming: true,
tools: true,
jsonOutputSchema: true,
cachedInputPrice: 0.1 / 1e6,
cachedInputPrice: "0.1e-6",
};

const baseModel: ModelDefinition = {
Expand All @@ -22,13 +22,13 @@ const baseModel: ModelDefinition = {

describe("providerSupportsCachedInput", () => {
it("returns true when cachedInputPrice is set to a positive number", () => {
expect(providerSupportsCachedInput({ cachedInputPrice: 0.1 / 1e6 })).toBe(
expect(providerSupportsCachedInput({ cachedInputPrice: "0.1e-6" })).toBe(
true,
);
});

it("returns true when cachedInputPrice is zero", () => {
expect(providerSupportsCachedInput({ cachedInputPrice: 0 })).toBe(true);
expect(providerSupportsCachedInput({ cachedInputPrice: "0" })).toBe(true);
});

it("returns false when cachedInputPrice is undefined", () => {
Expand Down
6 changes: 3 additions & 3 deletions apps/gateway/src/lib/costs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ describe("calculateCosts", () => {

it("should apply discount when model has discount field", async () => {
vi.mocked(mockGetEffectiveDiscount).mockResolvedValueOnce({
discount: 0.1,
discount: "0.1",
source: "global_provider",
discountId: "disc-global-openai",
});
Expand All @@ -327,7 +327,7 @@ describe("calculateCosts", () => {
null,
"openai",
"gpt-4",
0,
"0",
"gpt-4",
);
});
Expand Down Expand Up @@ -622,7 +622,7 @@ describe("calculateCosts", () => {
const azureProvider = models
.find((m) => m.id === "gpt-image-2")
?.providers.find((p) => p.providerId === "azure");
const discountMultiplier = 1 - (azureProvider?.discount ?? 0);
const discountMultiplier = 1 - Number(azureProvider?.discount ?? "0");
const expectedTextInputCost =
(promptTokens - reportedImageInputTokens) *
(5 / 1e6) *
Expand Down
30 changes: 15 additions & 15 deletions apps/gateway/src/lib/costs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ export function shouldBillCancelledRequests(): boolean {
*/
function getPricingForTokenCount(
pricingTiers: PricingTier[] | undefined,
baseInputPrice: number,
baseOutputPrice: number,
baseCachedInputPrice: number | undefined,
baseCacheWriteInputPrice: number | undefined,
baseCacheWriteInputPrice1h: number | undefined,
baseInputPrice: string,
baseOutputPrice: string,
baseCachedInputPrice: string | undefined,
baseCacheWriteInputPrice: string | undefined,
baseCacheWriteInputPrice1h: string | undefined,
promptTokens: number,
): {
inputPrice: number;
outputPrice: number;
cachedInputPrice: number | undefined;
cacheWriteInputPrice: number | undefined;
cacheWriteInputPrice1h: number | undefined;
inputPrice: string;
outputPrice: string;
cachedInputPrice: string | undefined;
cacheWriteInputPrice: string | undefined;
cacheWriteInputPrice1h: string | undefined;
tierName: string | undefined;
} {
if (!pricingTiers || pricingTiers.length === 0) {
Expand Down Expand Up @@ -292,8 +292,8 @@ export async function calculateCosts(
// Get pricing based on token count (supports tiered pricing)
const pricing = getPricingForTokenCount(
providerInfo.pricingTiers,
providerInfo.inputPrice ?? 0,
providerInfo.outputPrice ?? 0,
providerInfo.inputPrice ?? "0",
providerInfo.outputPrice ?? "0",
providerInfo.cachedInputPrice,
providerInfo.cacheWriteInputPrice,
providerInfo.cacheWriteInputPrice1h,
Expand All @@ -316,11 +316,11 @@ export async function calculateCosts(
pricing.cacheWriteInputPrice1h !== undefined
? new Decimal(pricing.cacheWriteInputPrice1h)
: cacheWriteInputPrice;
const requestPrice = new Decimal(providerInfo.requestPrice ?? 0);
const requestPrice = new Decimal(providerInfo.requestPrice ?? "0");

// Get effective discount (checks org-specific, global, then hardcoded)
// Pass both the root model ID and the provider-specific model name for matching
const hardcodedDiscount = providerInfo.discount ?? 0;
const hardcodedDiscount = providerInfo.discount ?? "0";
const effectiveDiscountResult = await getEffectiveDiscount(
organizationId,
provider,
Expand Down Expand Up @@ -600,7 +600,7 @@ export async function calculateCosts(
cachedTokens,
cacheWriteTokens,
estimatedCost: isEstimated,
discount: discount !== 0 ? discount : undefined,
discount: Number(discount) !== 0 ? Number(discount) : undefined,
pricingTier: pricing.tierName,
};
}
24 changes: 12 additions & 12 deletions apps/gateway/src/lib/iam.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,22 @@ const threeProviderModel: ModelDefinition = {
providerId: "google-vertex",
modelName: "test-model-vertex",
streaming: true,
inputPrice: 0.5,
outputPrice: 1.0,
inputPrice: "0.5",
outputPrice: "1.0",
},
{
providerId: "google-ai-studio",
modelName: "test-model-studio",
streaming: true,
inputPrice: 0.5,
outputPrice: 1.0,
inputPrice: "0.5",
outputPrice: "1.0",
},
{
providerId: "openai",
modelName: "test-model-openai",
streaming: true,
inputPrice: 0.3,
outputPrice: 0.6,
inputPrice: "0.3",
outputPrice: "0.6",
},
],
};
Expand All @@ -75,8 +75,8 @@ const singleActiveProviderModel: ModelDefinition = {
providerId: "google-vertex",
modelName: "test-model-vertex",
streaming: true,
inputPrice: 0.5,
outputPrice: 1.0,
inputPrice: "0.5",
outputPrice: "1.0",
},
],
};
Expand All @@ -102,15 +102,15 @@ const paidModel: ModelDefinition = {
providerId: "openai",
modelName: "paid-model-openai",
streaming: true,
inputPrice: 5.0,
outputPrice: 15.0,
inputPrice: "5.0",
outputPrice: "15.0",
},
{
providerId: "anthropic",
modelName: "paid-model-anthropic",
streaming: true,
inputPrice: 3.0,
outputPrice: 10.0,
inputPrice: "3.0",
outputPrice: "10.0",
},
],
};
Expand Down
4 changes: 2 additions & 2 deletions apps/gateway/src/lib/iam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ async function evaluateRule(
if (
ruleValue.maxInputPrice !== undefined &&
provider.inputPrice &&
provider.inputPrice > ruleValue.maxInputPrice
Number(provider.inputPrice) > ruleValue.maxInputPrice
) {
return {
allowed: false,
Expand All @@ -234,7 +234,7 @@ async function evaluateRule(
if (
ruleValue.maxOutputPrice !== undefined &&
provider.outputPrice &&
provider.outputPrice > ruleValue.maxOutputPrice
Number(provider.outputPrice) > ruleValue.maxOutputPrice
) {
return {
allowed: false,
Expand Down
2 changes: 1 addition & 1 deletion apps/gateway/src/mcp/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,7 @@ function createMcpServer(apiKey: string): McpServer {
responseText += `- **Family:** ${modelData.family}\n`;
if (
modelData.requestPrice !== undefined &&
modelData.requestPrice > 0
Number(modelData.requestPrice) > 0
) {
responseText += `- **Price:** $${modelData.requestPrice} per request\n`;
}
Expand Down
11 changes: 8 additions & 3 deletions apps/playground/src/lib/model-utils.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import type { ApiModel, ApiProvider } from "@/lib/fetch-models";

export function formatPrice(price: number | undefined): string {
export function formatPrice(price: number | string | undefined): string {
// Unknown / missing pricing
if (price === undefined) {
return "Unknown";
}

const n = typeof price === "string" ? Number(price) : price;
if (!Number.isFinite(n)) {
return "Unknown";
}

// Explicitly free
if (price === 0) {
if (n === 0) {
return "Free";
}

// All model prices in the catalog are stored as "per token" with values like 2 / 1e6.
// For the playground we always want to show an explicit per‑million price (to match the /models UI),
// otherwise small numbers like 2e‑6 end up rounded to $0.00/1K.
const perMillion = price * 1_000_000;
const perMillion = n * 1_000_000;
// Show full precision (up to 4 decimals) without trailing zeros
const formatted = parseFloat(perMillion.toFixed(4)).toString();
return `$${formatted}/1M tokens`;
Expand Down
Loading
Loading