Skip to content

refactor: use decimal-safe strings for model pricing - #2275

Merged
steebchen merged 3 commits into
mainfrom
decimal-model-pricing-refactor
May 13, 2026
Merged

steebchen merged 3 commits into
mainfrom
decimal-model-pricing-refactor

Conversation

@steebchen

@steebchen steebchen commented May 12, 2026 •

Copy link
Copy Markdown
Member

Summary

Switches model pricing from JS number to decimal-safe string end-to-end so the Decimal-based cost engine sees exact values from the source.

Previously inputPrice: 0.269 / 1e6 already carried IEEE-754 noise at the model definition layer (2.6900000000000004e-7). The Decimal pipeline faithfully propagated that noise, surfacing values like 3.3450000000000004e-7 in activity logs and routing scores. The quantise helper (#2255) was patching the symptom at the end of the pipeline; this PR fixes the root cause.

Changes

  • packages/models: PricingTier, ProviderRegion, and ProviderModelMapping price fields are now Price = string; perSecondPrice is Record<string, string>.
  • packages/models/src/models/*: All numeric price expressions converted to decimal-safe string literals (0.269 / 1e6 → "0.269e-6").
  • packages/db: getEffectiveDiscount returns string instead of number, keeping the DB decimal precision intact end-to-end.
  • costs.ts / get-cheapest-from-available-providers.ts: Pass strings directly into new Decimal(...), which parses them exactly.
  • UI / admin / scripts: Explicit Number(...) conversions at display boundaries that render JS numbers (formatters, comparisons, math for display).

The previous quantiseCost / quantisePrice workaround is no longer required — the underlying values are now exact through the entire pipeline.

Test plan

  • pnpm build is green across all packages.
  • Spot-check the routing-score price for novita/deepseek-v3.2 and the alibaba discounted Qwen pricing in the activity log — should render as 3.345e-7 / 9.12e-7 without trailing noise.
  • Run pnpm test:unit (a few unrelated tests have been flaky locally; verify in CI).
  • Verify model pages render unchanged (/models, /models/[name], /models/[name]/[provider]).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor

    • Standardized pricing to be string-safe across the product, coercing values for display and calculations.
    • Results: consistent price formatting ("Free"/"Unknown"), accurate "% off" badges, reliable cost simulation, and corrected model/provider sorting, filtering, and pricing displays.
  • Tests

    • Updated unit and e2e tests and fixtures to use string-backed pricing and validate numeric coercion and ratio/assertion behavior.

Review Change Stack

Copilot AI review requested due to automatic review settings May 12, 2026 13:20
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented May 12, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6c546e8e-c9a6-42fe-b04e-d87f0a3320a8

📥 Commits

Reviewing files that changed from the base of the PR and between 6baeac0 and 1d22ecc.

📒 Files selected for processing (55)
  • apps/code/src/components/CodingModelsShowcase.tsx
  • apps/gateway/src/audio.e2e.ts
  • apps/gateway/src/chat-prompt-caching.e2e.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/is-model-truly-free.ts
  • apps/gateway/src/chat/tools/resolve-model-info.ts
  • apps/gateway/src/lib/anthropic-pricing.spec.ts
  • apps/gateway/src/lib/coding-models.spec.ts
  • apps/gateway/src/lib/costs.spec.ts
  • apps/gateway/src/lib/costs.ts
  • apps/gateway/src/lib/iam.spec.ts
  • apps/gateway/src/lib/iam.ts
  • apps/gateway/src/mcp/mcp.ts
  • apps/playground/src/lib/model-utils.ts
  • apps/ui/src/app/models/[name]/[provider]/opengraph-image.tsx
  • apps/ui/src/app/models/[name]/[provider]/page.tsx
  • apps/ui/src/app/models/[name]/page.tsx
  • apps/ui/src/components/cost-simulator/cost-simulator-client.tsx
  • apps/ui/src/components/enterprise/cost-calculator.tsx
  • apps/ui/src/components/models/adapt-model.ts
  • apps/ui/src/components/models/category-og-image.tsx
  • apps/ui/src/components/models/model-comparison.tsx
  • apps/ui/src/components/models/model-provider-card.tsx
  • apps/ui/src/components/models/models-list.tsx
  • apps/ui/src/components/models/playground-model-selector.tsx
  • apps/ui/src/components/models/provider-card.tsx
  • apps/ui/src/components/shared/model-card.tsx
  • apps/ui/src/components/token-cost-calculator/token-cost-calculator-client.tsx
  • apps/ui/src/lib/model-utils.ts
  • apps/worker/src/services/video-jobs.ts
  • packages/actions/src/get-cheapest-from-available-providers.ts
  • packages/actions/src/get-cheapest-model-for-provider.ts
  • packages/actions/src/models.spec.ts
  • packages/actions/src/validate-provider-key.ts
  • packages/db/src/discount-helpers.ts
  • packages/models/src/models.ts
  • packages/models/src/models/alibaba.ts
  • packages/models/src/models/anthropic.ts
  • packages/models/src/models/bytedance.ts
  • packages/models/src/models/deepseek.ts
  • packages/models/src/models/google.ts
  • packages/models/src/models/meta.ts
  • packages/models/src/models/minimax.ts
  • packages/models/src/models/mistral.ts
  • packages/models/src/models/moonshot.ts
  • packages/models/src/models/nousresearch.ts
  • packages/models/src/models/openai.ts
  • packages/models/src/models/perplexity.ts
  • packages/models/src/models/xai.ts
  • packages/models/src/models/xiaomi.ts
  • packages/models/src/models/zai.ts
  • packages/models/src/types.ts
  • packages/shared/src/components/model-selector.tsx
  • packages/shared/src/lib/model-utils.ts
  • packages/shared/src/lib/types.ts
✅ Files skipped from review due to trivial changes (4)
  • apps/gateway/src/chat/tools/resolve-model-info.ts
  • apps/gateway/src/lib/iam.ts
  • apps/code/src/components/CodingModelsShowcase.tsx
  • apps/gateway/src/lib/iam.spec.ts
🚧 Files skipped from review as they are similar to previous changes (44)
  • packages/shared/src/lib/types.ts
  • apps/gateway/src/chat-prompt-caching.e2e.ts
  • apps/gateway/src/mcp/mcp.ts
  • apps/ui/src/components/models/provider-card.tsx
  • packages/models/src/models/perplexity.ts
  • apps/gateway/src/audio.e2e.ts
  • apps/ui/src/components/models/models-list.tsx
  • apps/ui/src/components/enterprise/cost-calculator.tsx
  • apps/ui/src/app/models/[name]/[provider]/page.tsx
  • apps/gateway/src/chat/tools/is-model-truly-free.ts
  • packages/actions/src/validate-provider-key.ts
  • apps/worker/src/services/video-jobs.ts
  • packages/actions/src/get-cheapest-from-available-providers.ts
  • apps/ui/src/app/models/[name]/[provider]/opengraph-image.tsx
  • packages/models/src/models/mistral.ts
  • apps/ui/src/lib/model-utils.ts
  • apps/gateway/src/lib/anthropic-pricing.spec.ts
  • apps/ui/src/components/models/playground-model-selector.tsx
  • apps/ui/src/components/cost-simulator/cost-simulator-client.tsx
  • apps/ui/src/components/token-cost-calculator/token-cost-calculator-client.tsx
  • apps/ui/src/components/models/category-og-image.tsx
  • packages/db/src/discount-helpers.ts
  • packages/models/src/models.ts
  • apps/gateway/src/lib/costs.ts
  • apps/ui/src/components/models/model-comparison.tsx
  • packages/models/src/models/xiaomi.ts
  • packages/shared/src/lib/model-utils.ts
  • apps/ui/src/app/models/[name]/page.tsx
  • apps/ui/src/components/models/adapt-model.ts
  • apps/gateway/src/chat/chat.ts
  • packages/models/src/models/deepseek.ts
  • packages/models/src/models/minimax.ts
  • apps/gateway/src/lib/costs.spec.ts
  • apps/playground/src/lib/model-utils.ts
  • packages/models/src/models/bytedance.ts
  • packages/models/src/models/moonshot.ts
  • packages/models/src/models/xai.ts
  • packages/actions/src/models.spec.ts
  • apps/ui/src/components/models/model-provider-card.tsx
  • packages/shared/src/components/model-selector.tsx
  • packages/models/src/models/meta.ts
  • packages/models/src/models/zai.ts
  • packages/models/src/models/google.ts
  • packages/models/src/models/alibaba.ts

Walkthrough

Converts pricing/discount fields to a string-based Price across model catalogs and updates gateway/actions/worker/UI code and tests to coerce string prices with Number() or Decimal where numeric operations or filtering are required.

Changes

String Price Type Migration and Numeric Coercion

Layer / File(s) Summary
Model price type and datasets
packages/models/*, packages/models/src/models.ts, packages/models/src/types.ts
Adds Price = string and converts provider/model pricing fields from numeric literals to string-encoded Price values across model catalogs.
Core logic and cost calculations
apps/gateway/*, packages/actions/*, packages/db/*, apps/worker/*
Refactors routing, provider selection, cost calculation, discount handling, and media pricing to accept string prices and coerce with Number() or Decimal where required.
UI components, formatters, and selectors
apps/ui/*, packages/shared/*, apps/playground/*, apps/code/*
Updates format helpers and UI components to accept string prices, coerce them for display/filters, compute discounted values, and adjust render gating for request/per-second pricing.
Tests and fixtures
apps/gateway/src/*, packages/actions/src/models.spec.ts, various *.spec.ts and *.e2e.ts
Updates tests/fixtures to use string-backed pricing values and adjust assertions to coerce via Number() or helper functions; validates Decimal-based paths in actions/tests.

Sequence Diagram(s)

(skip)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • smakosh
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch decimal-model-pricing-refactor

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors model/provider pricing to use decimal-safe strings (instead of JS number) end-to-end so pricing values remain exact through the Decimal-based cost pipeline and don’t pick up IEEE-754 noise early in the flow.

Changes:

  • Converted pricing fields across @llmgateway/models types and model catalogs to string (including tier/region pricing and perSecondPrice).
  • Updated selection, billing, and UI formatting code to explicitly parse with Decimal(...) or Number(...) at the appropriate boundaries.
  • Updated DB discount helpers and tests to preserve decimal precision as strings.

Reviewed changes

Copilot reviewed 55 out of 55 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/shared/src/lib/types.ts Updates shared UI model type pricing fields to string.
packages/shared/src/lib/model-utils.ts Accepts string pricing and converts for formatting.
packages/shared/src/components/model-selector.tsx Parses string prices/discounts for display, filtering, and aggregation.
packages/models/src/types.ts Updates exported pricing types to string / Record<string,string>.
packages/models/src/models/zai.ts Converts model price literals to decimal-safe strings.
packages/models/src/models/xiaomi.ts Converts model price literals to decimal-safe strings.
packages/models/src/models/xai.ts Converts model price literals (incl. tiers/discounts) to strings.
packages/models/src/models/perplexity.ts Converts model price literals to strings.
packages/models/src/models/nousresearch.ts Converts model price literals to strings.
packages/models/src/models/moonshot.ts Converts model price literals to strings.
packages/models/src/models/mistral.ts Converts model price literals to strings.
packages/models/src/models/minimax.ts Converts model price literals to strings.
packages/models/src/models/meta.ts Converts model price literals to strings.
packages/models/src/models/deepseek.ts Converts model price literals (incl. region overrides/discounts) to strings.
packages/models/src/models/bytedance.ts Converts model price literals to strings (incl. scientific notation).
packages/models/src/models/anthropic.ts Converts model price literals to strings (incl. cache/web search/discounts).
packages/models/src/models.ts Introduces Price = string and updates core pricing interfaces.
packages/db/src/discount-helpers.ts Returns discount percent as decimal-safe string and updates API surface.
packages/actions/src/validate-provider-key.ts Parses string prices/discounts for validation model selection.
packages/actions/src/models.spec.ts Updates tests for string prices/discounts.
packages/actions/src/get-cheapest-model-for-provider.ts Parses string prices/discounts when computing cheapest model.
packages/actions/src/get-cheapest-from-available-providers.ts Uses string inputs with Decimal(...) for selection pricing.
apps/worker/src/services/video-jobs.ts Adjusts video pricing types and parses string request/per-second pricing.
apps/ui/src/lib/model-utils.ts Updates UI pricing formatter to accept string.
apps/ui/src/components/token-cost-calculator/token-cost-calculator-client.tsx Parses string pricing/discounts for calculator math and display.
apps/ui/src/components/shared/model-card.tsx Parses string prices for display.
apps/ui/src/components/models/provider-card.tsx Parses string prices/discounts for display.
apps/ui/src/components/models/playground-model-selector.tsx Parses string prices for sorting/filtering.
apps/ui/src/components/models/models-list.tsx Parses string prices for display.
apps/ui/src/components/models/model-provider-card.tsx Parses string prices/discounts across detailed provider pricing UI.
apps/ui/src/components/models/model-comparison.tsx Parses string prices/discounts when summarizing/comparing pricing.
apps/ui/src/components/models/category-og-image.tsx Parses string discounts for category counts.
apps/ui/src/components/models/adapt-model.ts Preserves pricing/discount strings in API adaptation.
apps/ui/src/components/enterprise/cost-calculator.tsx Parses string prices/discounts for calculator math.
apps/ui/src/components/cost-simulator/cost-simulator-client.tsx Parses string prices/discounts for simulation math.
apps/ui/src/app/models/[name]/page.tsx Treats discounts as strings and parses for calculations.
apps/ui/src/app/models/[name]/[provider]/page.tsx Treats discounts as strings.
apps/ui/src/app/models/[name]/[provider]/opengraph-image.tsx Parses string pricing/discounts for OG image rendering.
apps/playground/src/lib/model-utils.ts Updates playground formatter to accept string.
apps/gateway/src/mcp/mcp.ts Parses requestPrice for conditional output.
apps/gateway/src/lib/iam.ts Parses string prices for rule evaluation comparisons.
apps/gateway/src/lib/iam.spec.ts Updates IAM tests to use string prices.
apps/gateway/src/lib/costs.ts Switches to string base prices, passes strings into Decimal(...), and converts at output boundaries.
apps/gateway/src/lib/costs.spec.ts Updates costs tests for string discount plumbing.
apps/gateway/src/lib/coding-models.spec.ts Updates cached-input support tests for string pricing.
apps/gateway/src/lib/anthropic-pricing.spec.ts Parses string prices when checking pricing ratios.
apps/gateway/src/chat/tools/resolve-model-info.ts Updates “custom” provider placeholders to string prices.
apps/gateway/src/chat/tools/is-model-truly-free.ts Parses string request/per-second pricing for “truly free” check.
apps/gateway/src/chat/chat.ts Parses string prices when computing provider scores/prices in routing.
apps/gateway/src/chat-prompt-caching.e2e.ts Parses string cached/input prices for ratio assertion.
apps/gateway/src/audio.e2e.ts Parses string input audio pricing for expected-cost computation.
apps/code/src/components/CodingModelsShowcase.tsx Parses string prices for display formatting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/actions/src/models.spec.ts Outdated
Comment on lines +457 to +461
const discountPrice =
((discountProvider.inputPrice +
(discountProvider.outputPrice ?? 0)) /
((Number(discountProvider.inputPrice) +
Number(discountProvider.outputPrice ?? "0")) /
2) *
(discountProvider as ProviderModelMapping).discount!;
discountOf(discountProvider as ProviderModelMapping)!;
Comment thread apps/worker/src/services/video-jobs.ts Outdated
Comment on lines +1554 to +1556
return mapping?.requestPrice !== undefined
? Number(mapping.requestPrice)
: null;
Comment thread apps/worker/src/services/video-jobs.ts Outdated
.find((value): value is string => value !== undefined);
const pricePerSecond =
pricePerSecondStr !== undefined ? Number(pricePerSecondStr) : undefined;
if (pricePerSecond === undefined) {
Comment on lines +9 to 13
const n = typeof price === "string" ? Number(price) : price;

// Explicitly free
if (price === 0) {
if (n === 0) {
return "Free";
Comment thread apps/ui/src/lib/model-utils.ts Outdated
Comment on lines +4 to +5
const n = typeof price === "string" ? Number(price) : price;
if (!n) {
Comment on lines +250 to +254
const priceNum = Number(price);
if (priceNum === 0) {
return 0;
}
if (!discount || discount <= 0) {
return price;
const discountNum = discount === undefined ? 0 : Number(discount);

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
apps/ui/src/components/shared/model-card.tsx (1)

118-135: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard coerced prices before rendering to avoid $NaN output.

Line 118, Line 128, and Line 134 can surface $NaN when a malformed price string slips through; NaN !== 0 also passes the request-price gate.

Proposed fix
-									<span className="font-mono text-foreground font-bold">
-										${(Number(provider.inputPrice) * 1e6).toFixed(2)}
-									</span>{" "}
+									{Number.isFinite(Number(provider.inputPrice)) && (
+										<span className="font-mono text-foreground font-bold">
+											${(Number(provider.inputPrice) * 1e6).toFixed(2)}
+										</span>
+									)}{" "}
 									<span className="text-muted-foreground">in</span>
@@
-									<span className="font-mono text-foreground font-bold">
-										${(Number(provider.outputPrice) * 1e6).toFixed(2)}
-									</span>{" "}
+									{Number.isFinite(Number(provider.outputPrice)) && (
+										<span className="font-mono text-foreground font-bold">
+											${(Number(provider.outputPrice) * 1e6).toFixed(2)}
+										</span>
+									)}{" "}
 									<span className="text-muted-foreground">out</span>
@@
-							{provider.requestPrice !== undefined &&
-								Number(provider.requestPrice) !== 0 &&
+							{provider.requestPrice !== undefined &&
+								Number.isFinite(Number(provider.requestPrice)) &&
+								Number(provider.requestPrice) > 0 &&
 								` / $${(Number(provider.requestPrice) * 1000).toFixed(2)} per 1K req`}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ui/src/components/shared/model-card.tsx` around lines 118 - 135, The
rendered price expressions can produce "$NaN" because provider.inputPrice,
provider.outputPrice, and provider.requestPrice are coerced without validating;
update the JSX around the spans that reference provider.inputPrice,
provider.outputPrice, and provider.requestPrice to first parse/coerce each value
(e.g., parseFloat or Number), check Number.isFinite(value) and handle non-finite
values by skipping rendering or showing a safe fallback (like '-' or 'N/A');
also replace the request-price gate (currently using
Number(provider.requestPrice) !== 0) with a finite-and-nonzero check
(Number.isFinite(req) && req !== 0) before formatting and multiplying. Ensure
you update only the rendering logic inside the ModelCard component where those
three price references occur so malformed strings no longer render as $NaN.
packages/actions/src/get-cheapest-model-for-provider.ts (1)

58-76: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Use Decimal for price arithmetic to match the PR goal and sister routing file.

This code uses Number() for price calculations (filtering paid models, averaging prices, applying discounts), which reintroduces IEEE‑754 precision issues. The PR objectives require: "Cost and routing code: pass strings directly into new Decimal(...) for exact parsing."

The sister file get-cheapest-from-available-providers.ts uses Decimal for identical price operations in its routing logic. Refactor to match:

Suggested changes
import { Decimal } from "decimal.js";

// Lines 58-59: Free model check
new Decimal(providerInfo.inputPrice ?? "0").isZero() && 
new Decimal(providerInfo.outputPrice ?? "0").isZero()

// Lines 70-76: Cheapest model calculation  
const discount = new Decimal((providerInfo as ProviderModelMapping).discount ?? "0");
const discountMultiplier = new Decimal(1).minus(discount);
const totalPrice = new Decimal(providerInfo.inputPrice!)
	.add(new Decimal(providerInfo.outputPrice!))
	.div(2)
	.times(discountMultiplier);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/actions/src/get-cheapest-model-for-provider.ts` around lines 58 -
76, The price arithmetic uses Number() and must be converted to Decimal for
exact math: import Decimal from "decimal.js", replace the free-model check that
uses Number(providerInfo.inputPrice/outputPrice) with new
Decimal(providerInfo.inputPrice ?? "0").isZero() && new
Decimal(providerInfo.outputPrice ?? "0").isZero(), and in the cheapest-model
loop (modelsToConsider) replace Number-based discount, averaging and totalPrice
with Decimal operations—create discount via new Decimal((providerInfo as
ProviderModelMapping).discount ?? "0"), compute discountMultiplier as new
Decimal(1).minus(discount), and compute totalPrice by summing inputPrice and
outputPrice as Decimals, dividing by 2, then multiplying by discountMultiplier;
ensure lowestPrice comparison uses Decimal comparisons or converts the Decimal
totalPrice to a numeric ordering safely.
apps/ui/src/components/models/model-comparison.tsx (2)

240-292: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Filter out NaN values explicitly in pricing summary calculation.

The filter on lines 245-248 checks Number(raw) !== 0, which allows NaN values to pass through (since NaN !== 0 is true). If a provider has a malformed price string, the resulting NaN will flow through the arithmetic operations and comparisons, potentially producing incorrect results.

🛡️ Proposed fix to filter NaN values
 	const entries = providers
 		.filter((provider) => {
 			const raw = provider[field];
-			return raw !== undefined && raw !== null && Number(raw) !== 0;
+			const num = Number(raw);
+			return raw !== undefined && raw !== null && !Number.isNaN(num) && num !== 0;
 		})
 		.map((provider) => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ui/src/components/models/model-comparison.tsx` around lines 240 - 292,
The filter in getPricingSummary lets NaN values pass because Number(raw) !== 0
is true for NaN; update the filter used when building entries (the
providers.filter(...) block) to explicitly exclude non-finite numbers (e.g., use
Number.isFinite(Number(raw)) or !Number.isNaN(Number(raw))) in addition to the
existing null/undefined check and zero check so that rawValue (computed later)
is always a valid number before multiplier/discount math runs.

478-507: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add NaN validation after Number() coercion.

After converting rawRaw to a number on line 489, there's no validation that the conversion succeeded. If the pricing field contains a malformed string, NaN will flow through the arithmetic and produce incorrect formatted prices.

🛡️ Proposed fix with NaN check
 	const rawRaw = provider[field];
 	if (rawRaw === undefined || rawRaw === null) {
 		return undefined;
 	}
 	const raw = Number(rawRaw);
+	if (Number.isNaN(raw)) {
+		return undefined;
+	}
 	const multiplier =
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ui/src/components/models/model-comparison.tsx` around lines 478 - 507,
In getProviderPricingSummary, validate the numeric conversion of rawRaw after
calling Number(rawRaw) (the variable raw) to guard against NaN from malformed
strings; if Number(rawRaw) is NaN, return undefined (or skip pricing) so
downstream math/formatPriceValue doesn't receive NaN. Update the logic around
rawRaw/raw and use isNaN(raw) (or Number.isFinite) to detect invalid numbers
before computing multiplier, discountNum, discounted, original, and returning
the PricingSummary.
apps/playground/src/lib/model-utils.ts (1)

3-23: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add explicit NaN validation after Number() coercion.

If price is a malformed string, Number(price) returns NaN, which will flow through the arithmetic operations and produce "$NaN/1M tokens" as output. Add an explicit check for NaN after the conversion on line 9.

🛡️ Proposed fix with explicit NaN check
 	const n = typeof price === "string" ? Number(price) : price;
+
+	// Invalid numeric string
+	if (Number.isNaN(n)) {
+		return "Unknown";
+	}

 	// Explicitly free
 	if (n === 0) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/lib/model-utils.ts` around lines 3 - 23, In formatPrice,
after the Number() coercion (the const n = ... line), add an explicit NaN check
(e.g. using Number.isNaN(n)) and handle it the same as undefined—return
"Unknown"—so malformed string inputs don't produce "$NaN/1M tokens"; update the
code around the n variable and early returns in the formatPrice function
accordingly.
packages/shared/src/components/model-selector.tsx (1)

1531-1542: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prevent empty “Image Pricing” sections when requestPrice is "0".

The section-gating condition still treats string "0" as truthy, so the block can render with no rows.

Suggested fix
- {(previewEntry.mapping?.requestPrice ??
-   previewEntry.mapping?.imageInputPrice ??
-   previewEntry.mapping?.imageOutputPrice) && (
+ {(Number(previewEntry.mapping?.requestPrice ?? 0) > 0 ||
+   previewEntry.mapping?.imageInputPrice !== undefined ||
+   previewEntry.mapping?.imageOutputPrice !== undefined) && (

- {(selectedDetails.mapping?.requestPrice ??
-   selectedDetails.mapping?.imageInputPrice ??
-   selectedDetails.mapping?.imageOutputPrice) && (
+ {(Number(selectedDetails.mapping?.requestPrice ?? 0) > 0 ||
+   selectedDetails.mapping?.imageInputPrice !== undefined ||
+   selectedDetails.mapping?.imageOutputPrice !== undefined) && (

Also applies to: 2065-2076

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/components/model-selector.tsx` around lines 1531 - 1542,
The "Image Pricing" block is being shown when any of
previewEntry.mapping?.requestPrice, imageInputPrice, or imageOutputPrice are
non-empty strings like "0" (which are truthy) but render no rows; change the
gating condition to test numeric value > 0 instead of truthiness by coercing
each price (e.g., Number(previewEntry.mapping?.requestPrice) > 0 ||
Number(previewEntry.mapping?.imageInputPrice) > 0 ||
Number(previewEntry.mapping?.imageOutputPrice) > 0) so the section only renders
when at least one price is genuinely > 0; update the identical check used around
the per-row render logic (the existing comparisons that use Number(...) > 0 are
fine—make the outer condition consistent with them).
🧹 Nitpick comments (2)
apps/ui/src/components/models/provider-card.tsx (1)

165-223: ⚡ Quick win

Consider adding fallback handling for non-numeric string values.

The Number() coercions assume that provider.inputPrice, provider.outputPrice, and provider.discount contain valid numeric strings. If any of these contain malformed values (e.g., empty strings after the truthy check passes for "0", or corrupted data), Number() will return NaN, which propagates through calculations and displays as "NaN" to users.

Consider adding || 0 fallbacks after each Number() call to ensure graceful degradation.

🛡️ Example defensive pattern
-					${(Number(provider.inputPrice) * 1e6).toFixed(2)}
+					${((Number(provider.inputPrice) || 0) * 1e6).toFixed(2)}

Apply similar pattern to all Number() coercions in this component.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ui/src/components/models/provider-card.tsx` around lines 165 - 223, The
UI shows "NaN" if provider.inputPrice, provider.outputPrice, or
provider.discount are non-numeric; update all uses of
Number(provider.inputPrice), Number(provider.outputPrice) and
Number(provider.discount) in this component (including the calculations that
multiply by 1e6, apply (1 - discount), and call toFixed) to coerce safely e.g.
const input = Number(provider.inputPrice) || 0, const output =
Number(provider.outputPrice) || 0, const discount = Number(provider.discount) ||
0 and use those local vars in the rendering logic (also use the parsed discount
when deciding whether to show the Badge) so toFixed is always called on a valid
number and malformed strings degrade to 0 gracefully.
apps/ui/src/components/token-cost-calculator/token-cost-calculator-client.tsx (1)

569-571: ⚡ Quick win

Consider adding numeric zero-check for discount display.

The condition checks the string value directly, so "0" is truthy and would display "0% off" instead of nothing. Previously when discount was a number, 0 was falsy and wouldn't display.

This is unlikely to occur in practice (no zero discounts in the current data), but for consistency with prior behavior consider:

-{cheapestMapping.discount
+{cheapestMapping.discount && Number(cheapestMapping.discount) !== 0
     ? ` · ${Number(cheapestMapping.discount) * 100}% off`
     : ""}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/ui/src/components/token-cost-calculator/token-cost-calculator-client.tsx`
around lines 569 - 571, The display condition for the discount uses a truthy
string check on cheapestMapping.discount which causes "0" to render as "0% off";
update the check in token-cost-calculator-client.tsx (around where
cheapestMapping.discount is used) to coerce/parse the value to a number and only
render when the numeric discount is greater than 0 (e.g.,
parseFloat/Number(cheapestMapping.discount) > 0) so "0" or non-positive values
do not show the discount text.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ui/src/app/models/`[name]/page.tsx:
- Around line 355-356: The UI currently displays "0% off" because
minPriceItem?.discount is truthy for "0"; update the rendering logic for
minPrice (and the output-token and image-output sections where similar code
appears) to only show the "(X% off)" suffix when Number(minPriceItem?.discount)
> 0 (or parseFloat(...) > 0) and otherwise render just
`$${minPrice.toFixed(2)}/M`; locate occurrences referencing
minPriceItem.discount (and the analogous variables in the output-token and
image-output blocks) and replace the boolean check with a numeric > 0 check so
zero discounts are omitted.

In `@apps/ui/src/lib/model-utils.ts`:
- Around line 3-15: In formatPrice, after coercing price to a number, explicitly
check Number.isNaN(n) and handle that case instead of letting NaN fall through
the falsy check; update the function (formatPrice) to return a sensible fallback
(e.g. "Invalid price") when Number.isNaN(n) is true, keep the existing "Free"
behavior only for undefined/null/0 as intended, and then proceed with the
remaining numeric thresholds and toFixed calls.

In `@packages/shared/src/lib/model-utils.ts`:
- Around line 3-21: The formatPrice function can return "$NaN/1M tokens" when a
malformed string is passed; after converting the price (the variable n in
formatPrice) add an explicit NaN check (Number.isNaN(n)) and treat it like an
unknown/missing price (e.g., return "Unknown") before the zero/free check and
perMillion calculation so bad inputs don't propagate to the formatted string.

---

Outside diff comments:
In `@apps/playground/src/lib/model-utils.ts`:
- Around line 3-23: In formatPrice, after the Number() coercion (the const n =
... line), add an explicit NaN check (e.g. using Number.isNaN(n)) and handle it
the same as undefined—return "Unknown"—so malformed string inputs don't produce
"$NaN/1M tokens"; update the code around the n variable and early returns in the
formatPrice function accordingly.

In `@apps/ui/src/components/models/model-comparison.tsx`:
- Around line 240-292: The filter in getPricingSummary lets NaN values pass
because Number(raw) !== 0 is true for NaN; update the filter used when building
entries (the providers.filter(...) block) to explicitly exclude non-finite
numbers (e.g., use Number.isFinite(Number(raw)) or !Number.isNaN(Number(raw)))
in addition to the existing null/undefined check and zero check so that rawValue
(computed later) is always a valid number before multiplier/discount math runs.
- Around line 478-507: In getProviderPricingSummary, validate the numeric
conversion of rawRaw after calling Number(rawRaw) (the variable raw) to guard
against NaN from malformed strings; if Number(rawRaw) is NaN, return undefined
(or skip pricing) so downstream math/formatPriceValue doesn't receive NaN.
Update the logic around rawRaw/raw and use isNaN(raw) (or Number.isFinite) to
detect invalid numbers before computing multiplier, discountNum, discounted,
original, and returning the PricingSummary.

In `@apps/ui/src/components/shared/model-card.tsx`:
- Around line 118-135: The rendered price expressions can produce "$NaN" because
provider.inputPrice, provider.outputPrice, and provider.requestPrice are coerced
without validating; update the JSX around the spans that reference
provider.inputPrice, provider.outputPrice, and provider.requestPrice to first
parse/coerce each value (e.g., parseFloat or Number), check
Number.isFinite(value) and handle non-finite values by skipping rendering or
showing a safe fallback (like '-' or 'N/A'); also replace the request-price gate
(currently using Number(provider.requestPrice) !== 0) with a finite-and-nonzero
check (Number.isFinite(req) && req !== 0) before formatting and multiplying.
Ensure you update only the rendering logic inside the ModelCard component where
those three price references occur so malformed strings no longer render as
$NaN.

In `@packages/actions/src/get-cheapest-model-for-provider.ts`:
- Around line 58-76: The price arithmetic uses Number() and must be converted to
Decimal for exact math: import Decimal from "decimal.js", replace the free-model
check that uses Number(providerInfo.inputPrice/outputPrice) with new
Decimal(providerInfo.inputPrice ?? "0").isZero() && new
Decimal(providerInfo.outputPrice ?? "0").isZero(), and in the cheapest-model
loop (modelsToConsider) replace Number-based discount, averaging and totalPrice
with Decimal operations—create discount via new Decimal((providerInfo as
ProviderModelMapping).discount ?? "0"), compute discountMultiplier as new
Decimal(1).minus(discount), and compute totalPrice by summing inputPrice and
outputPrice as Decimals, dividing by 2, then multiplying by discountMultiplier;
ensure lowestPrice comparison uses Decimal comparisons or converts the Decimal
totalPrice to a numeric ordering safely.

In `@packages/shared/src/components/model-selector.tsx`:
- Around line 1531-1542: The "Image Pricing" block is being shown when any of
previewEntry.mapping?.requestPrice, imageInputPrice, or imageOutputPrice are
non-empty strings like "0" (which are truthy) but render no rows; change the
gating condition to test numeric value > 0 instead of truthiness by coercing
each price (e.g., Number(previewEntry.mapping?.requestPrice) > 0 ||
Number(previewEntry.mapping?.imageInputPrice) > 0 ||
Number(previewEntry.mapping?.imageOutputPrice) > 0) so the section only renders
when at least one price is genuinely > 0; update the identical check used around
the per-row render logic (the existing comparisons that use Number(...) > 0 are
fine—make the outer condition consistent with them).

---

Nitpick comments:
In `@apps/ui/src/components/models/provider-card.tsx`:
- Around line 165-223: The UI shows "NaN" if provider.inputPrice,
provider.outputPrice, or provider.discount are non-numeric; update all uses of
Number(provider.inputPrice), Number(provider.outputPrice) and
Number(provider.discount) in this component (including the calculations that
multiply by 1e6, apply (1 - discount), and call toFixed) to coerce safely e.g.
const input = Number(provider.inputPrice) || 0, const output =
Number(provider.outputPrice) || 0, const discount = Number(provider.discount) ||
0 and use those local vars in the rendering logic (also use the parsed discount
when deciding whether to show the Badge) so toFixed is always called on a valid
number and malformed strings degrade to 0 gracefully.

In
`@apps/ui/src/components/token-cost-calculator/token-cost-calculator-client.tsx`:
- Around line 569-571: The display condition for the discount uses a truthy
string check on cheapestMapping.discount which causes "0" to render as "0% off";
update the check in token-cost-calculator-client.tsx (around where
cheapestMapping.discount is used) to coerce/parse the value to a number and only
render when the numeric discount is greater than 0 (e.g.,
parseFloat/Number(cheapestMapping.discount) > 0) so "0" or non-positive values
do not show the discount text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a1fee39d-bb37-4da0-8196-8b048402ea6b

📥 Commits

Reviewing files that changed from the base of the PR and between 77bafbd and cdb4e0c.

📒 Files selected for processing (55)
  • apps/code/src/components/CodingModelsShowcase.tsx
  • apps/gateway/src/audio.e2e.ts
  • apps/gateway/src/chat-prompt-caching.e2e.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/is-model-truly-free.ts
  • apps/gateway/src/chat/tools/resolve-model-info.ts
  • apps/gateway/src/lib/anthropic-pricing.spec.ts
  • apps/gateway/src/lib/coding-models.spec.ts
  • apps/gateway/src/lib/costs.spec.ts
  • apps/gateway/src/lib/costs.ts
  • apps/gateway/src/lib/iam.spec.ts
  • apps/gateway/src/lib/iam.ts
  • apps/gateway/src/mcp/mcp.ts
  • apps/playground/src/lib/model-utils.ts
  • apps/ui/src/app/models/[name]/[provider]/opengraph-image.tsx
  • apps/ui/src/app/models/[name]/[provider]/page.tsx
  • apps/ui/src/app/models/[name]/page.tsx
  • apps/ui/src/components/cost-simulator/cost-simulator-client.tsx
  • apps/ui/src/components/enterprise/cost-calculator.tsx
  • apps/ui/src/components/models/adapt-model.ts
  • apps/ui/src/components/models/category-og-image.tsx
  • apps/ui/src/components/models/model-comparison.tsx
  • apps/ui/src/components/models/model-provider-card.tsx
  • apps/ui/src/components/models/models-list.tsx
  • apps/ui/src/components/models/playground-model-selector.tsx
  • apps/ui/src/components/models/provider-card.tsx
  • apps/ui/src/components/shared/model-card.tsx
  • apps/ui/src/components/token-cost-calculator/token-cost-calculator-client.tsx
  • apps/ui/src/lib/model-utils.ts
  • apps/worker/src/services/video-jobs.ts
  • packages/actions/src/get-cheapest-from-available-providers.ts
  • packages/actions/src/get-cheapest-model-for-provider.ts
  • packages/actions/src/models.spec.ts
  • packages/actions/src/validate-provider-key.ts
  • packages/db/src/discount-helpers.ts
  • packages/models/src/models.ts
  • packages/models/src/models/alibaba.ts
  • packages/models/src/models/anthropic.ts
  • packages/models/src/models/bytedance.ts
  • packages/models/src/models/deepseek.ts
  • packages/models/src/models/google.ts
  • packages/models/src/models/meta.ts
  • packages/models/src/models/minimax.ts
  • packages/models/src/models/mistral.ts
  • packages/models/src/models/moonshot.ts
  • packages/models/src/models/nousresearch.ts
  • packages/models/src/models/openai.ts
  • packages/models/src/models/perplexity.ts
  • packages/models/src/models/xai.ts
  • packages/models/src/models/xiaomi.ts
  • packages/models/src/models/zai.ts
  • packages/models/src/types.ts
  • packages/shared/src/components/model-selector.tsx
  • packages/shared/src/lib/model-utils.ts
  • packages/shared/src/lib/types.ts

Comment thread apps/ui/src/app/models/[name]/page.tsx Outdated
Comment thread apps/ui/src/lib/model-utils.ts
Comment thread packages/shared/src/lib/model-utils.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/actions/src/get-cheapest-model-for-provider.ts`:
- Around line 60-61: Add a safe Decimal parse wrapper (e.g., tryDecimal) and use
it wherever the code constructs new Decimal for providerInfo.inputPrice,
providerInfo.outputPrice, and providerInfo.discount so malformed strings don't
throw: implement tryDecimal(value) to return a Decimal or null on parse failure,
replace direct new Decimal(...) uses in the logic that filters/compares prices
(the expressions using providerInfo.inputPrice/outputPrice and the later
discount arithmetic) with guarded calls to tryDecimal, and skip or treat null
results as "no price" (e.g., continue, treat as zero or exclude the provider) so
invalid strings are handled without crashing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3c522e5d-f98e-44aa-af40-6be44e77d9bb

📥 Commits

Reviewing files that changed from the base of the PR and between 850f870 and fe57426.

📒 Files selected for processing (5)
  • apps/ui/src/components/models/model-comparison.tsx
  • apps/ui/src/components/shared/model-card.tsx
  • apps/ui/src/components/token-cost-calculator/token-cost-calculator-client.tsx
  • packages/actions/src/get-cheapest-model-for-provider.ts
  • packages/shared/src/components/model-selector.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/ui/src/components/shared/model-card.tsx
  • apps/ui/src/components/models/model-comparison.tsx
  • packages/shared/src/components/model-selector.tsx
  • apps/ui/src/components/token-cost-calculator/token-cost-calculator-client.tsx

Comment on lines +60 to +61
!new Decimal(providerInfo.inputPrice ?? "0").isZero() ||
!new Decimal(providerInfo.outputPrice ?? "0").isZero(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does decimal.js (v10.x) throw an exception when constructed with an invalid numeric string (e.g., "abc")? Please cite the official docs/API behavior.

💡 Result:

Yes. In decimal.js v10.x, the Decimal constructor throws an exception when constructed with an invalid numeric string (e.g. "abc"). The official API docs state: "Throws on an invalid value" for Decimal(value) [1]. Additionally, the v10.x API page documents that the constructor accepts values of type number|string|Decimal and that a "legitimate value" is a valid integer/float/±Infinity/NaN; invalid inputs are not treated as NaN silently, but instead trigger the documented throw behavior [1].

Citations:


🏁 Script executed:

# First, let's check the file exists and read the relevant lines
cat -n packages/actions/src/get-cheapest-model-for-provider.ts | head -85 | tail -40

Repository: theopenco/llmgateway

Length of output: 1673


🏁 Script executed:

# Check the full file to understand the context better
wc -l packages/actions/src/get-cheapest-model-for-provider.ts

Repository: theopenco/llmgateway

Length of output: 123


🏁 Script executed:

# Look for type definitions for the provider info to confirm field types
rg "inputPrice|outputPrice|discount" packages/actions/src/ -A 2 -B 2

Repository: theopenco/llmgateway

Length of output: 25600


🏁 Script executed:

# Find type definitions for ProviderModelMapping
rg "interface ProviderModelMapping|type ProviderModelMapping" -A 15

Repository: theopenco/llmgateway

Length of output: 29980


🏁 Script executed:

# Check if there's any validation or schema parsing of provider data
rg "ProviderModelMapping|providerMapping" packages/actions/src/ --type ts -B 2 | grep -E "(zod|validator|validate|parse|schema)" -i

Repository: theopenco/llmgateway

Length of output: 3825


🏁 Script executed:

# Look at the full context of the function to see if there's any pre-validation
cat -n packages/actions/src/get-cheapest-model-for-provider.ts | head -90

Repository: theopenco/llmgateway

Length of output: 3494


🏁 Script executed:

# Check if there's any try-catch or error handling wrapping these function calls
rg "getCheapestModelForProvider|getCheapestFromAvailableProviders" packages/actions/src/ -B 3 -A 3 | head -50

Repository: theopenco/llmgateway

Length of output: 3486


🏁 Script executed:

# Look for any Zod schemas or validation on the models object
fd "models" packages/models/src/ -type f | head -5

Repository: theopenco/llmgateway

Length of output: 235


🏁 Script executed:

# Check if Price type has any validation constraints
rg "type Price|export const Price" packages/models/src/ -B 2 -A 5

Repository: theopenco/llmgateway

Length of output: 471


Add error handling for Decimal parsing to prevent crashes on malformed price strings.

new Decimal(...) throws on invalid numeric strings. If any of inputPrice, outputPrice, or discount become malformed, the function fails. Add a safe parse wrapper before arithmetic operations.

Suggested approach:

const tryDecimal = (value: string | undefined): Decimal | null => {
	try {
		return new Decimal(value ?? "0");
	} catch {
		return null;
	}
};

Then use it at lines 60–61 and 72–80 to skip or safely handle invalid values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/actions/src/get-cheapest-model-for-provider.ts` around lines 60 -
61, Add a safe Decimal parse wrapper (e.g., tryDecimal) and use it wherever the
code constructs new Decimal for providerInfo.inputPrice,
providerInfo.outputPrice, and providerInfo.discount so malformed strings don't
throw: implement tryDecimal(value) to return a Decimal or null on parse failure,
replace direct new Decimal(...) uses in the logic that filters/compares prices
(the expressions using providerInfo.inputPrice/outputPrice and the later
discount arithmetic) with guarded calls to tryDecimal, and skip or treat null
results as "no price" (e.g., continue, treat as zero or exclude the provider) so
invalid strings are handled without crashing.

@steebchen
steebchen force-pushed the decimal-model-pricing-refactor branch from fe57426 to 6baeac0 Compare May 12, 2026 17:05
steebchen and others added 3 commits May 13, 2026 14:13
Model definitions now carry prices as exact decimal-safe strings (e.g.
"0.269e-6") instead of JS numbers. Previously `0.269 / 1e6` already
carried IEEE-754 noise at the source, which the Decimal-based cost
engine faithfully propagated, surfacing values like 3.3450000000000004e-7
in activity logs and routing scores.

Changes:
- packages/models: PricingTier, ProviderRegion, ProviderModelMapping
  price fields are now Price = string; perSecondPrice becomes
  Record<string, string>.
- All model files converted from `X / 1eN` expressions to "XeN" string
  literals (script-driven, no functional change in intent).
- getEffectiveDiscount returns string instead of number, keeping the
  full DB decimal precision end-to-end into the Decimal cost engine.
- Cost calculation and provider routing pass strings directly into
  `new Decimal(...)`, which parses them exactly.
- UI/admin consumers explicitly convert via `Number(...)` at the
  display boundary where rendering accepts JS numbers.

The previous `quantiseCost` workaround is no longer needed: the
underlying values are now exact through the entire pipeline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses PR review feedback:

- formatPrice (ui, playground, shared): treat malformed strings as
  "Unknown" via Number.isFinite() instead of silently rendering
  "$NaN/1M tokens" or hiding bad data behind "Free".
- video-jobs: validate Number(...) results with Number.isFinite() for
  requestPrice and pricePerSecond so NaN doesn't propagate into
  billing.
- model-selector applyDiscount: return undefined when price/discount
  parse to NaN so min/max comparisons stay correct.
- /models/[name] page and provider-card components: switch JSX
  `provider.discount ?` truthy checks to `Number(... ?? "0") > 0`
  so a stored "0" discount no longer renders a "0% off" badge.
- models.spec.ts: fix the expected discount math (`* (1 - discount)`
  instead of `* discount`); the previous form still passed the
  inequality check but diverged from the real pricing semantics.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Follow-up to PR review feedback:

- get-cheapest-model-for-provider: switch to Decimal arithmetic to
  match get-cheapest-from-available-providers and preserve precision
  end-to-end through the routing layer.
- shared/model-card: guard inputPrice/outputPrice/requestPrice with
  Number.isFinite so a malformed price string renders nothing instead
  of "$NaN".
- model-comparison: filter providers whose price coerces to NaN and
  early-return undefined from getProviderPricingSummary in the same
  case, so bad data doesn't poison min/max comparisons.
- model-selector: image-pricing sections previously rendered when
  requestPrice was the string "0" (truthy). Gate on Number(...) > 0
  or explicit imageInput/imageOutput price presence.
- token-cost-calculator: don't show "0% off" when discount is "0".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@steebchen
steebchen force-pushed the decimal-model-pricing-refactor branch from 6baeac0 to 1d22ecc Compare May 13, 2026 07:16
@steebchen
steebchen added this pull request to the merge queue May 13, 2026
Merged via the queue into main with commit d8eb9d2 May 13, 2026
17 of 18 checks passed
@steebchen
steebchen deleted the decimal-model-pricing-refactor branch May 13, 2026 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants