Skip to content

Add reported cost support for OpenRouter usage - #449

Open
bozhnyukAlex wants to merge 3 commits into
pydantic:mainfrom
bozhnyukAlex:openrouter-reported-cost
Open

Add reported cost support for OpenRouter usage#449
bozhnyukAlex wants to merge 3 commits into
pydantic:mainfrom
bozhnyukAlex:openrouter-reported-cost

Conversation

@bozhnyukAlex

@bozhnyukAlex bozhnyukAlex commented Jul 2, 2026

Copy link
Copy Markdown

Summary

Adds support for provider-reported total costs and wires it into OpenRouter usage extraction.

For OpenRouter:

  • chat completions now extract usage.cost
  • responses now have a dedicated usage extractor
  • ExtractedUsage.calc_price() prefers the reported total cost when present

Why

OpenRouter can return the actual billed cost in usage.cost. Recalculating from static model prices can be inaccurate because OpenRouter routing, native token accounting, and billing details may differ from the static price table.

Refs #239. Related to #345/#350: this extends OpenRouter usage extraction support to the Responses API flavor as well.

Notes

OpenRouter reports a total cost, not a full input/output price breakdown. When a reported total cost is present, total_price is authoritative and price_source is set to reported.

The input/output split works as follows:

  1. First, the library calculates the usual static price from tokens and the bundled model price data.
  2. If the static calculation has a non-zero total, the reported OpenRouter total is split using the same input/output proportion as the static calculation.
    For example, if static pricing gives input_price=0.0002, output_price=0.0008, total_price=0.0010, then input is 20% and output is 80%. A reported OpenRouter total of 0.0015 becomes input_price=0.0003, output_price=0.0012, total_price=0.0015.
  3. If the static calculation is zero, the reported total is assigned to output_price when the usage only has output tokens; otherwise it is assigned to input_price.

This makes total_price match OpenRouter's actual billed cost while preserving the existing invariant input_price + output_price == total_price.

The split is intentionally best-effort. OpenRouter's usage.cost is the reliable billing value, but the response does not currently provide an authoritative input/output cost breakdown. Keeping a proportional split avoids breaking existing callers that expect non-null input/output prices and a consistent total, without pretending that the derived breakdown is the source of billing truth.

E2E smoke checks

Also verified the public extraction-to-pricing path locally for both packages:

  • Python: extract_usage(..., provider_id='openrouter', api_flavor='chat').calc_price() uses usage.cost as the reported total_price.
  • Python: extract_usage(..., provider_id='openrouter', api_flavor='responses').calc_price() uses usage.cost as the reported total_price.
  • JS: findProvider -> extractUsage -> calcPrice(..., { reportedTotalPrice }) produces the same reported totals for OpenRouter chat and responses payloads.
  • Responses payloads without usage.cost still fall back to calculated static pricing.

Validation

  • make build
  • make lint
  • make typecheck
  • make test
  • npm run ci
  • npm run typecheck
  • SKIP=no-commit-to-branch uvx pre-commit run --all-files --verbose

@bozhnyukAlex
bozhnyukAlex marked this pull request as ready for review July 2, 2026 17:00

@cubic-dev-ai cubic-dev-ai Bot 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.

1 issue found across 20 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/js/src/types.ts
@adtyavrdhn

adtyavrdhn commented Jul 7, 2026

Copy link
Copy Markdown
Member

Interesting idea @bozhnyukAlex and I agree we should prefer provider cost when we are given that information.

I was doing something similar in Pydantic AI for this but I don't mind this. Let me think about it a bit more on how this should look like. Thanks for working on it

@adtyavrdhn adtyavrdhn self-assigned this Jul 7, 2026
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