Skip to content

Add price mode: emit computed dollar cost instead of token counts#8

Open
anassg-lago wants to merge 1 commit into
mainfrom
feature/pricing-mode
Open

Add price mode: emit computed dollar cost instead of token counts#8
anassg-lago wants to merge 1 commit into
mainfrom
feature/pricing-mode

Conversation

@anassg-lago

Copy link
Copy Markdown
Collaborator

New optional pricing_mode ("tokens" default | "price"). In price mode the SDK emits one llm_cost event per call whose value is Σ(unit_price × tokens) × markup, with a full per-field breakdown (tokens, unit_price, cost, base_cost, markup, price_source) in properties.

Pricing sources (public, no API keys):

  • OpenRouter /api/v1/models for native anthropic/openai/mistral/gemini (USD/token)
  • AWS Bedrock Price List Bulk API (per-region offer files) for Bedrock

Design:

  • pricing.py: PricingProvider with a TTL cache + injectable fetcher; lookup() is pure in-memory and never blocks the call; maybe_refresh() does the HTTP on the queue's background thread. Fork-safe via a PID self-heal (no register_at_fork — that tripped macOS's objc fork-safety abort).
  • Conservative, vendor-gated model matching; Bedrock parser keys on inferenceType and rejects priority/flex/batch tiers, scales per-1K units.
  • Money in Decimal floored to 12 dp; identical output to the JS BigInt impl (locked by a cross-repo golden fixture).
  • Fallback: unavailable price -> emit token events + on_error (never under-bill).
  • mode + markup overridable per-call via extra_lago; global via LagoConfig.

Default mode is "tokens" -> zero behavior change. New config: pricing_mode, markup, cost_metric_code, pricing_ttl_seconds, bedrock_default_region, pricing_provider. 28 new pricing unit tests + env-gated live test.

New optional pricing_mode ("tokens" default | "price"). In price mode the SDK
emits one llm_cost event per call whose value is Σ(unit_price × tokens) × markup,
with a full per-field breakdown (tokens, unit_price, cost, base_cost, markup,
price_source) in properties.

Pricing sources (public, no API keys):
  - OpenRouter /api/v1/models for native anthropic/openai/mistral/gemini (USD/token)
  - AWS Bedrock Price List Bulk API (per-region offer files) for Bedrock

Design:
  - pricing.py: PricingProvider with a TTL cache + injectable fetcher; lookup()
    is pure in-memory and never blocks the call; maybe_refresh() does the HTTP on
    the queue's background thread. Fork-safe via a PID self-heal (no
    register_at_fork — that tripped macOS's objc fork-safety abort).
  - Conservative, vendor-gated model matching; Bedrock parser keys on
    inferenceType and rejects priority/flex/batch tiers, scales per-1K units.
  - Money in Decimal floored to 12 dp; identical output to the JS BigInt impl
    (locked by a cross-repo golden fixture).
  - Fallback: unavailable price -> emit token events + on_error (never under-bill).
  - mode + markup overridable per-call via extra_lago; global via LagoConfig.

Default mode is "tokens" -> zero behavior change. New config: pricing_mode,
markup, cost_metric_code, pricing_ttl_seconds, bedrock_default_region,
pricing_provider. 28 new pricing unit tests + env-gated live test.

Gate: ruff + format + mypy clean; 346 unit tests; coverage 88.27%.
@anassg-lago anassg-lago requested a review from vladmarascu June 11, 2026 18:47
@vladmarascu

Copy link
Copy Markdown
Contributor

@anassg-lago do we only compute this in dollars? Is there any plan to do other currencies? Seems like if there is room for that we should do it in this PR.

Comment thread src/lago_agent_sdk/sdk.py
"timestamp": int(time.time()),
# Top-level amount (in cents) for Lago's dynamic charge model —
# the charge sums these into a single fee.
"precise_total_amount_cents": breakdown.total_cents,

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.

is this sent as string? Worth checking if we accept this as str in the Lago model

Comment thread src/lago_agent_sdk/sdk.py
self._queue.push(event)
price = self._pricing.lookup(usage.provider, usage.model, usage.api)
if price is None:
# Don't silently under-bill: fall back to token events + report.

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.

Here it could be worth adding a more clear fall-back?

Add a price_fallback config with three modes, and — this is the important part — pick a default that matches the setup the README tells people to create (a single llm_cost dynamic charge).

# config.py
# What to emit in price mode when a unit price can't be resolved.
PriceFallback = Literal["zero_cost_event", "token_events", "drop"]
price_fallback: PriceFallback = "zero_cost_event"
# sdk.py — the price-None branch
price = self._pricing.lookup(usage.provider, usage.model, usage.api)
if price is None:
    self._report_error(PricingUnavailableError(usage.provider, usage.model, usage.api), "pricing")
    self._handle_price_unavailable(usage, sub, dimensions)  # switches on config.price_fallback
    return

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