Add cost accounting extension for Usage events#3071
Conversation
c4d7aa8 to
686b857
Compare
|
Update: I added an integration-style test that runs the extension through Focused validation: Live provider smoke: The live smoke also caught and fixed dated OpenAI model revision lookup ( |
686b857 to
5244ca4
Compare
Codecov Report❌ Patch coverage is
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
|
Hi @elCaptnCode, thanks for the PR, but this implementation drops the subagent cost to $0. |
genisis0x
left a comment
There was a problem hiding this comment.
The mechanics here look solid: Decimal end to end with Decimal(str(value)) to avoid float artifacts, clamping tokens to >= 0, and separating regular input / cache-read / cache-creation / output / reasoning into their own line items is the right granularity for something people will reconcile against a provider invoice.
The one thing I'd want to nail down is the assumption baked into this block, because it's provider-dependent:
cached_input_tokens = min(prompt_tokens, cache_read_tokens + cache_creation_tokens)
regular_input_tokens = prompt_tokens - cached_input_tokensThis only produces the right regular_input_tokens if usage.prompt_tokens is defined as including the cached tokens (so cached is a subset you subtract back out). That's the OpenAI convention — prompt_tokens is the total and cached_tokens is a subset of it. Anthropic reports it the other way: input_tokens is the non-cached input, with cache_read_input_tokens / cache_creation_input_tokens reported separately and not folded in. If AG2's Usage.prompt_tokens carries the Anthropic-style non-cached value, then for a cached Anthropic call prompt_tokens (say 100) is smaller than cache_read_tokens (say 1000), so cached_input_tokens = min(100, 1000) = 100, regular_input_tokens = 0, and the 100 genuinely-uncached input tokens get billed at zero — an undercharge that the min(...) guard hides rather than surfaces. So the key question: is Usage.prompt_tokens normalized to always include cache tokens across providers before it reaches the estimator? If yes, this is correct; if it's passed through per-provider, the cache subtraction needs to branch on the provider's convention (or the estimator needs the raw non-cached input count).
Two smaller notes:
-
cache_read_input_token_cost or pricing.input_cost_per_token(and the cache-creation equivalent) fall back to the full input rate when a cache rate isn't configured. Cache reads are normally much cheaper than fresh input (OpenAI ~0.25-0.5x, Anthropic ~0.1x), so this silently overcharges cache reads rather than flagging that the catalog is missing a cache rate. A conservative default is defensible, but a warning (like thenot_configuredpath already does for a missing model) would keep the estimate honest instead of quietly high. -
The reasoning-token handling (
regular_output = completion - thinkingonly when a reasoning rate is set) assumescompletion_tokensincludesthinking_tokens, which matches the OpenAI o-series convention — same normalization question as above, just on the output side. Worth one line in the docstring stating thatUsageis expected to report completion-inclusive-of-reasoning so a future provider mapping doesn't quietly double count.
The structure is good — it's really just the token-accounting conventions that decide whether the numbers match a real bill, so making the Usage field semantics explicit is what would give me confidence in the totals.
Why are these changes needed?
AG2 currently normalizes provider token usage into
Usage/UsageEvent, but there is no optional first-class way to derive estimated cost from that usage. Model prices change frequently and provider billing structures vary, so this PR keeps AG2 core usage accounting factual and adds cost accounting as an extension.This adds
ag2.extensions.cost_accounting, which estimates USD cost fromUsageEventdata and a maintained pricing catalog. The catalog loader supports LiteLLM-style fields such as:input_cost_per_tokenoutput_cost_per_tokencache_read_input_token_costcache_creation_input_token_costoutput_cost_per_reasoning_tokenThe extension watches
UsageEvent, AG2's source of truth for usage reporting, so it avoids double counting model responses and sub-agent rollups.Included APIs:
CostCatalog/ModelPricingUsageCostEstimatorCostEstimateCostAccountingObserverUnknown models return
pricing_source="not_configured"with a warning instead of guessing.Related issue number
No existing issue. This PR implements an optional cost accounting extension over AG2 usage telemetry.
Checks
ag2/extensions/cost_accounting/README.mdbecause this repository clone does not include a published docs page for this new extension.uv/ latestrufftoolchain in this local environment, so I also ran focused pytest, compile checks, and a live OpenAI smoke locally.Validation run locally:
Live provider smoke with
OpenAIConfigandgpt-4.1-nano:That smoke used a real AG2
Agent.ask()call withOpenAIConfig,MemoryStream, andCostAccountingObserverattached.AI assistance