refactor: use decimal-safe strings for model pricing - #2275
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (55)
✅ Files skipped from review due to trivial changes (4)
🚧 Files skipped from review as they are similar to previous changes (44)
WalkthroughConverts 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. ChangesString Price Type Migration and Numeric Coercion
Sequence Diagram(s)(skip) Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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/modelstypes and model catalogs tostring(including tier/region pricing andperSecondPrice). - Updated selection, billing, and UI formatting code to explicitly parse with
Decimal(...)orNumber(...)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.
| const discountPrice = | ||
| ((discountProvider.inputPrice + | ||
| (discountProvider.outputPrice ?? 0)) / | ||
| ((Number(discountProvider.inputPrice) + | ||
| Number(discountProvider.outputPrice ?? "0")) / | ||
| 2) * | ||
| (discountProvider as ProviderModelMapping).discount!; | ||
| discountOf(discountProvider as ProviderModelMapping)!; |
| return mapping?.requestPrice !== undefined | ||
| ? Number(mapping.requestPrice) | ||
| : null; |
| .find((value): value is string => value !== undefined); | ||
| const pricePerSecond = | ||
| pricePerSecondStr !== undefined ? Number(pricePerSecondStr) : undefined; | ||
| if (pricePerSecond === undefined) { |
| const n = typeof price === "string" ? Number(price) : price; | ||
|
|
||
| // Explicitly free | ||
| if (price === 0) { | ||
| if (n === 0) { | ||
| return "Free"; |
| const n = typeof price === "string" ? Number(price) : price; | ||
| if (!n) { |
| const priceNum = Number(price); | ||
| if (priceNum === 0) { | ||
| return 0; | ||
| } | ||
| if (!discount || discount <= 0) { | ||
| return price; | ||
| const discountNum = discount === undefined ? 0 : Number(discount); |
There was a problem hiding this comment.
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 winGuard coerced prices before rendering to avoid
$NaNoutput.Line 118, Line 128, and Line 134 can surface
$NaNwhen a malformed price string slips through;NaN !== 0also 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 liftUse
Decimalfor 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.tsusesDecimalfor 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 winFilter out NaN values explicitly in pricing summary calculation.
The filter on lines 245-248 checks
Number(raw) !== 0, which allowsNaNvalues to pass through (sinceNaN !== 0istrue). If a provider has a malformed price string, the resultingNaNwill 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 winAdd NaN validation after Number() coercion.
After converting
rawRawto a number on line 489, there's no validation that the conversion succeeded. If the pricing field contains a malformed string,NaNwill 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 winAdd explicit NaN validation after Number() coercion.
If
priceis a malformed string,Number(price)returnsNaN, which will flow through the arithmetic operations and produce"$NaN/1M tokens"as output. Add an explicit check forNaNafter 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 winPrevent empty “Image Pricing” sections when
requestPriceis"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 winConsider adding fallback handling for non-numeric string values.
The
Number()coercions assume thatprovider.inputPrice,provider.outputPrice, andprovider.discountcontain 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 returnNaN, which propagates through calculations and displays as"NaN"to users.Consider adding
|| 0fallbacks after eachNumber()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 winConsider 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 whendiscountwas a number,0was 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
📒 Files selected for processing (55)
apps/code/src/components/CodingModelsShowcase.tsxapps/gateway/src/audio.e2e.tsapps/gateway/src/chat-prompt-caching.e2e.tsapps/gateway/src/chat/chat.tsapps/gateway/src/chat/tools/is-model-truly-free.tsapps/gateway/src/chat/tools/resolve-model-info.tsapps/gateway/src/lib/anthropic-pricing.spec.tsapps/gateway/src/lib/coding-models.spec.tsapps/gateway/src/lib/costs.spec.tsapps/gateway/src/lib/costs.tsapps/gateway/src/lib/iam.spec.tsapps/gateway/src/lib/iam.tsapps/gateway/src/mcp/mcp.tsapps/playground/src/lib/model-utils.tsapps/ui/src/app/models/[name]/[provider]/opengraph-image.tsxapps/ui/src/app/models/[name]/[provider]/page.tsxapps/ui/src/app/models/[name]/page.tsxapps/ui/src/components/cost-simulator/cost-simulator-client.tsxapps/ui/src/components/enterprise/cost-calculator.tsxapps/ui/src/components/models/adapt-model.tsapps/ui/src/components/models/category-og-image.tsxapps/ui/src/components/models/model-comparison.tsxapps/ui/src/components/models/model-provider-card.tsxapps/ui/src/components/models/models-list.tsxapps/ui/src/components/models/playground-model-selector.tsxapps/ui/src/components/models/provider-card.tsxapps/ui/src/components/shared/model-card.tsxapps/ui/src/components/token-cost-calculator/token-cost-calculator-client.tsxapps/ui/src/lib/model-utils.tsapps/worker/src/services/video-jobs.tspackages/actions/src/get-cheapest-from-available-providers.tspackages/actions/src/get-cheapest-model-for-provider.tspackages/actions/src/models.spec.tspackages/actions/src/validate-provider-key.tspackages/db/src/discount-helpers.tspackages/models/src/models.tspackages/models/src/models/alibaba.tspackages/models/src/models/anthropic.tspackages/models/src/models/bytedance.tspackages/models/src/models/deepseek.tspackages/models/src/models/google.tspackages/models/src/models/meta.tspackages/models/src/models/minimax.tspackages/models/src/models/mistral.tspackages/models/src/models/moonshot.tspackages/models/src/models/nousresearch.tspackages/models/src/models/openai.tspackages/models/src/models/perplexity.tspackages/models/src/models/xai.tspackages/models/src/models/xiaomi.tspackages/models/src/models/zai.tspackages/models/src/types.tspackages/shared/src/components/model-selector.tsxpackages/shared/src/lib/model-utils.tspackages/shared/src/lib/types.ts
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
apps/ui/src/components/models/model-comparison.tsxapps/ui/src/components/shared/model-card.tsxapps/ui/src/components/token-cost-calculator/token-cost-calculator-client.tsxpackages/actions/src/get-cheapest-model-for-provider.tspackages/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
| !new Decimal(providerInfo.inputPrice ?? "0").isZero() || | ||
| !new Decimal(providerInfo.outputPrice ?? "0").isZero(), |
There was a problem hiding this comment.
🧩 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 -40Repository: 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.tsRepository: 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 2Repository: theopenco/llmgateway
Length of output: 25600
🏁 Script executed:
# Find type definitions for ProviderModelMapping
rg "interface ProviderModelMapping|type ProviderModelMapping" -A 15Repository: 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)" -iRepository: 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 -90Repository: 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 -50Repository: 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 -5Repository: 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 5Repository: 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.
fe57426 to
6baeac0
Compare
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>
6baeac0 to
1d22ecc
Compare
Summary
Switches model pricing from JS
numberto decimal-safestringend-to-end so the Decimal-based cost engine sees exact values from the source.Previously
inputPrice: 0.269 / 1e6already carried IEEE-754 noise at the model definition layer (2.6900000000000004e-7). The Decimal pipeline faithfully propagated that noise, surfacing values like3.3450000000000004e-7in 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
PricingTier,ProviderRegion, andProviderModelMappingprice fields are nowPrice = string;perSecondPriceisRecord<string, string>.0.269 / 1e6→"0.269e-6").getEffectiveDiscountreturnsstringinstead ofnumber, keeping the DB decimal precision intact end-to-end.new Decimal(...), which parses them exactly.Number(...)conversions at display boundaries that render JS numbers (formatters, comparisons, math for display).The previous
quantiseCost/quantisePriceworkaround is no longer required — the underlying values are now exact through the entire pipeline.Test plan
pnpm buildis green across all packages.novita/deepseek-v3.2and the alibaba discounted Qwen pricing in the activity log — should render as3.345e-7/9.12e-7without trailing noise.pnpm test:unit(a few unrelated tests have been flaky locally; verify in CI)./models,/models/[name],/models/[name]/[provider]).🤖 Generated with Claude Code
Summary by CodeRabbit
Refactor
Tests