Skip to content

fix: use Decimal.js for cost calculations - #1193

Merged
steebchen merged 1 commit into
mainfrom
fix-cost-rounding
Nov 20, 2025
Merged

steebchen merged 1 commit into
mainfrom
fix-cost-rounding

Conversation

@steebchen

@steebchen steebchen commented Nov 20, 2025

Copy link
Copy Markdown
Member

Summary

Prevents floating point rounding errors in cost calculations by using decimal.js throughout the system. This fixes issues like costs being logged as 0.0043880400000000002 instead of the expected precise value.

Changes

  • Added decimal.js dependency to gateway and worker packages
  • Updated gateway cost calculation to use Decimal for all arithmetic operations
  • Updated worker log processing to accumulate costs using Decimal to prevent precision loss

Summary by CodeRabbit

  • Bug Fixes

    • Improved precision and accuracy of cost calculations to prevent floating-point arithmetic errors in billing operations.
  • Chores

    • Added dependency for enhanced numeric precision handling in cost tracking and calculations.

✏️ Tip: You can customize this high-level summary in your review settings.

Prevent floating point rounding errors in cost calculations throughout
the system by using decimal.js in both gateway cost calculation and
worker log processing. Ensures costs like 0.0043880400000000002 are
properly calculated and accumulated without precision loss.
@coderabbitai

coderabbitai Bot commented Nov 20, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Introduces Decimal.js library across gateway and worker services to replace floating-point arithmetic with precise decimal calculations. Converts cost computations in the gateway to use Decimal objects and updates worker cost-tracking maps from numeric to Decimal-based values.

Changes

Cohort / File(s) Summary
Decimal.js dependency
apps/gateway/package.json, apps/worker/package.json
Added "decimal.js" version "10.5.0" to dependencies in both gateway and worker packages.
Cost calculation refactoring
apps/gateway/src/lib/costs.ts
Replaced primitive arithmetic with Decimal-based calculations for all cost fields (inputPrice, outputPrice, cachedInputPrice, etc.). All cost components use Decimal methods (minus, times) internally and return numbers.
Cost tracking refactoring
apps/worker/src/worker.ts
Converted cost-tracking maps from Map<string, number> to Map<string, Decimal>. Updated cost accumulation logic to use Decimal arithmetic with .plus() and threshold comparisons with .greaterThan(). Converts Decimal totals to numbers for SQL updates and logging.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Focus areas:
    • Verify Decimal arithmetic operations are semantically correct (especially minus/times chains and order of operations in costs.ts)
    • Confirm all .toNumber() conversions occur at appropriate boundaries before SQL operations and logging
    • Validate threshold comparisons in worker.ts (.greaterThan(0)) behave as intended
    • Check for any edge cases with zero/null pricing fields defaulting to new Decimal(0)

Possibly related PRs

  • feat(billing): use inputPrice fallback #1114: Modifies apps/gateway/src/lib/costs.ts cachedInputPrice fallback logic (?? inputPrice), which directly interacts with the Decimal conversion applied to these fields in this PR.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: use Decimal.js for cost calculations' directly and accurately summarizes the main change—adopting Decimal.js for cost arithmetic to prevent floating-point precision errors.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-cost-rounding

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@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: 0

🧹 Nitpick comments (2)
apps/worker/src/worker.ts (1)

536-619: Decimal-based aggregation looks correct; consider string params for DB decimals

The switch to Map<string, Decimal> for orgCosts/apiKeyCosts, plus(...) accumulation, and greaterThan(0) checks is logically correct and solves the intra‑batch floating‑point drift that caused weird cost logs. The control flow and gating (row.cost && row.cost > 0 && !row.cached, used_mode === "credits") are preserved.

One refinement to consider, given organization.credits and apiKey.usage are decimal columns:

  • Instead of:

    const costNumber = totalCost.toNumber();
    credits: sql`${organization.credits} - ${costNumber}`,
    usage: sql`${apiKey.usage} + ${costNumber}`,

    use the decimal string to avoid any final IEEE‑754 conversion when sending parameters to Postgres:

    const costString = totalCost.toString();
    credits: sql`${organization.credits} - ${costString}::numeric`,
    usage: sql`${apiKey.usage} + ${costString}::numeric`,

This keeps the entire path (calculation + DB update) in exact decimal form. Please verify with your Drizzle/PG setup that passing strings with an explicit ::numeric cast behaves as expected.

apps/gateway/src/lib/costs.ts (1)

1-1: Decimal.js integration preserves cost semantics and fixes FP drift

The switch to Decimal for inputPrice/outputPrice/cachedInputPrice/requestPrice, the discountMultiplier via new Decimal(1).minus(discount), and the cost formulas (inputCost, outputCost, cachedInputCost, requestCost, totalCost) all preserve the previous logic while removing floating‑point accumulation issues. Returning *.toNumber() keeps the external contract unchanged.

Two small follow‑ups to consider:

  1. Import style – As in the worker, please confirm { Decimal } is a valid import form for your decimal.js v10.5.0 ESM/TS setup; otherwise switch to import Decimal from "decimal.js"; here as well.

  2. If you ever need fully exact external values – Right now you re‑enter JS Number space at the edge. If in future you want to expose exact decimal strings (e.g., for high‑precision billing), you could return inputCost.toString() etc. instead of numbers and adjust callers/DB schema accordingly. Not necessary for this PR, but worth noting.

Also applies to: 172-213

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42b261b and 75bce9b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • apps/gateway/package.json (1 hunks)
  • apps/gateway/src/lib/costs.ts (3 hunks)
  • apps/worker/package.json (1 hunks)
  • apps/worker/src/worker.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/worker/src/worker.ts (1)
packages/db/src/schema.ts (2)
  • organization (108-145)
  • apiKey (238-265)
🪛 ESLint
apps/worker/src/worker.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/zujfnxWGQn'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/.pnpm/eslint-import-resolver-typescript@4.4.4_eslint-plugin-import@2.32.0_eslint@9.34.0_jiti@2.6.1_/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:233:12)
at resolveImportType (/home/jailuser/git/node_modules/.pnpm/eslint-plugin-import@2.32.0_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1___986ec7d736a20dae59d4d473ff8a6f0d/node_modules/eslint-plugin-import/lib/core/importType.js:126:2822)
at computeRank (/home/jailuser/git/node_modules/.pnpm/eslint-plugin-import@2.32.0_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1___986ec7d736a20dae59d4d473ff8a6f0d/node_modules/eslint-plugin-import/lib/rules/order.js:529:43)

(import/order)


[error] 1-1: Resolve error: EACCES: permission denied, open '/oWnYEzcjIf'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/.pnpm/eslint-import-resolver-typescript@4.4.4_eslint-plugin-import@2.32.0_eslint@9.34.0_jiti@2.6.1_/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:233:12)
at checkSourceValue (/home/jailuser/git/node_modules/.pnpm/eslint-plugin-import@2.32.0_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1___986ec7d736a20dae59d4d473ff8a6f0d/node_modules/eslint-plugin-import/lib/rules/no-useless-path-segments.js:85:53)
at checkSourceValue (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/no-useless-path-segments)

apps/gateway/src/lib/costs.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/oYgRVpcYic'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/.pnpm/eslint-import-resolver-typescript@4.4.4_eslint-plugin-import@2.32.0_eslint@9.34.0_jiti@2.6.1_/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:233:12)
at resolveImportType (/home/jailuser/git/node_modules/.pnpm/eslint-plugin-import@2.32.0_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1___986ec7d736a20dae59d4d473ff8a6f0d/node_modules/eslint-plugin-import/lib/core/importType.js:126:2822)
at computeRank (/home/jailuser/git/node_modules/.pnpm/eslint-plugin-import@2.32.0_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1___986ec7d736a20dae59d4d473ff8a6f0d/node_modules/eslint-plugin-import/lib/rules/order.js:529:43)

(import/order)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: build-split (docs, linux/amd64)
  • GitHub Check: build-split (worker, linux/amd64)
  • GitHub Check: build-split (gateway, linux/amd64)
  • GitHub Check: build-split (ui, linux/amd64)
  • GitHub Check: build-split (playground, linux/amd64)
  • GitHub Check: build-split (api, linux/amd64)
  • GitHub Check: build-unified (linux/amd64)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: test / run
  • GitHub Check: e2e-shards (4)
  • GitHub Check: build / run
  • GitHub Check: e2e-shards (3)
  • GitHub Check: generate / run
  • GitHub Check: lint / run
  • GitHub Check: autofix
🔇 Additional comments (3)
apps/worker/package.json (1)

25-34: Decimal.js dependency addition looks consistent

Adding "decimal.js": "10.5.0" aligns with the rest of the pinned dependencies and the new Decimal‑based cost logic in the worker.

Please double‑check that all packages in the repo that use Decimal.js are on the same version to avoid subtle behavior differences.

apps/gateway/package.json (1)

25-48: Gateway decimal.js dependency matches worker

Including "decimal.js": "10.5.0" here keeps gateway and worker in sync for Decimal‑based cost calculations.

Please confirm there are no other places in the monorepo pulling in a different Decimal.js version (e.g., via transitive deps) that might cause multiple copies at runtime.

apps/worker/src/worker.ts (1)

1-1: Confirm Decimal.js import style

You’re importing Decimal via a named import ({ Decimal }), whereas many examples use the default export (import Decimal from "decimal.js"). Whether this is correct depends on how decimal.js publishes its ESM/TS typings in your toolchain.

Please confirm with your TS/compiler setup (or decimal.js typings) that named import { Decimal } is valid here; if not, switch to import Decimal from "decimal.js"; in both this file and apps/ggateway/src/lib/costs.ts.

@steebchen
steebchen added this pull request to the merge queue Nov 20, 2025
Merged via the queue into main with commit 24d91ae Nov 20, 2025
25 of 26 checks passed
@steebchen
steebchen deleted the fix-cost-rounding branch November 20, 2025 11:13
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.

1 participant