fix(api): make an agent run's cost and tokens reach the trace - #5709
fix(api): make an agent run's cost and tokens reach the trace#5709mmabrouk wants to merge 1 commit into
Conversation
An agent trace shows no cost. `ag.metrics.costs.cumulative.*` was present on 0 of 48,005 spans measured on a real stack, root spans and child spans alike, while the playground showed a correct cost for the same run. The playground reads the number the harness streams to the browser. The trace reads a number the platform recomputes, and the recompute produces nothing for an agent run. Three causes, fixed here. The platform never accepted a reported cost, it only recomputed one. The runner already stamps the harness's own figure as `gen_ai.usage.cost`, but no ingest adapter mapped it into the `ag.*` namespace, so it sat at a raw path no product surface reads. Map it to `ag.metrics.costs.cumulative.total`. Cumulative, not incremental: the value is the run's aggregate total, so calling it incremental would double count against the runner's own model-call spans. The roll-up then overwrote it. Measured on the real pipeline: a span reporting $0.42 with one priceable child came out at $0.00021. The two numbers describe the same spend, not additive spend, and the recomputed one is the lossier estimate because it prices cached tokens at the full uncached rate. So the roll-up now fills only spans that report nothing. The roll-up did not run at all on the runner's telemetry. One run arrives in two OTLP requests: the SDK sends the workflow root span, and the runner sends its own subtree headed by a span whose parent lives in the other request. The tree builder seeded only from spans with no parent, so the runner's request produced an empty tree and nothing was cumulated. Measured: 0 of 8,719 child agent spans carried `ag.metrics.tokens.cumulative`. A span whose parent is absent from the batch now seeds a tree, because a dangling parent id means "not in this batch" rather than "no parent". Roll-up stays batch local and still does not cross requests. Tests: 1,608 passing in OSS and 1,867 in EE, including six new tests that drive the real ingest sequence rather than mocking it, and four covering the widened seeding rule against disconnected subtrees and a parent cycle. Claude-Session: https://claude.ai/code/session_01RkWWQUNNzRbaB5jnCAdjYA
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe adapter maps reported usage cost to cumulative totals. Span-tree construction now handles batch-local roots and cycles. Cost roll-up preserves producer-reported totals and recomputes costs only when no reported total exists. ChangesReported Cost Ingest
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant OTLPSpans
participant LogfireAdapter
participant TraceMetricPropagation
participant SpanMetrics
OTLPSpans->>LogfireAdapter: parse gen_ai.usage.cost
LogfireAdapter->>SpanMetrics: store costs.cumulative.total
SpanMetrics->>TraceMetricPropagation: propagate trace metrics
TraceMetricPropagation->>SpanMetrics: preserve reported totals or recompute child costs
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
This is one of three independent fixes for the same reported problem: an agent run shows a cost in the playground and nothing in the trace. They can be reviewed and merged separately, in any order.
Live end-to-end verification of all three together on a running stack is in progress, and I will post the result here. @coderabbitai review |
|
✅ Action performedReview finished.
|
Railway Preview Environment
|
Live verification found a defect this PR creates. Do not merge yet.I deployed all three fixes to a running EE stack and drove real agent runs against it. The headline result is good, and one real problem came out of it. What worksCost reaches the trace end to end. Before: The three fixes agree on cost. The root span and The streaming fix was verified in its exact failure scenario. A run with a 75-second silent tool gap emitted four SSE keepalives, survived them, and still recorded 4,653 tokens and $0.1773 on the workflow span. The The runner fix showed a clean cutover at restart: 36 of 40 The defect
The runner stamps the run's total usage on Cost escapes only by accident, because it maps to the cumulative bucket where the new guard short-circuits. This is a latent bug in the producer that the seeding fix exposes, not a flaw in the seeding logic. A fix is in progress and will land as a follow-up on this stack. The rule it aims for: exactly one span owns each incremental observation, and a parent's total is either rolled up from its children or carried as an explicit cumulative summary, never repeated as an incremental one. Two pre-existing problems, not caused by this PROn the Pi harness the workflow root still shows no cost. It reproduces in batch mode, which the streaming fix never touched, and historically almost no Pi root ever had a cost. So on Pi the trace now has the cost and the playground does not, which is the reported bug inverted. A run paused for approval records no usage anywhere. Verification detailNothing needed rebuilding; all three fix paths are bind-mounted from the checkout. Python hot-reloads, and the runner runs under No regressions: streaming streams, keepalives fire, tools execute, batch works, no run errored, no new log warnings. Not verified: nobody clicked run in the playground, so the playground comparison was made against the exact |
The symptom
An agent run shows a correct cost in the playground and nothing in the trace.
Measured on a real stack:
ag.metrics.costs.cumulative.*present on 0 of 48,005 spans, root spans and child spans alike. Separately,ag.metrics.tokens.cumulativepresent on 0 of 8,719 child agent spans.The two surfaces never read the same field. The playground reads the number the harness streams to the browser. The trace reads a number the platform recomputes from stored span attributes, and that recompute produces nothing for an agent run.
Three causes, fixed here
1. The platform never accepted a reported cost, it only recomputed one
The runner already stamps the harness's own figure as
gen_ai.usage.cost, and the SDK stamps it on the workflow root span. No ingest adapter mapped it into theag.*namespace, so it sat at a raw JSON path that no product surface reads.It is now mapped to
ag.metrics.costs.cumulative.total.Cumulative, not incremental.
record_usage's own docstring says the value is the run's aggregate total, so calling it incremental would misname it and would double count against the runner's own model-call spans if anything ever aggregates over all spans.The adapter that carries this mapping is named after a vendor but is not vendor gated.
AdapterRegistry.extract_featuresruns every registered adapter over every span unconditionally, and the file already maps our own runner's cache-token attributes. It is in effect thegen_ai.*semantic-convention mapper. The name is misleading and worth changing separately.2. The roll-up then overwrote it
Measured on the real pipeline: a span reporting $0.42 with one priceable child came out at $0.00021.
The two numbers describe the same spend, not additive spend. The recomputed one is also the lossier estimate, because
cost_per_tokennever receives the cache buckets and so prices cached reads at the full uncached rate. One measured span with 1 uncached prompt token, 25,182 cache-read tokens and 20 completion tokens was priced at $0.000303 against a harness-reported $0.0082.So the roll-up now fills only spans that report nothing. A cumulative total already present on entry came from the producer, because
_set_cumulativeis called exactly once per span per pass.This is not a hypothetical guard. It goes live the moment anything makes
ag.meta.response.modelpresent on our spans, which is what makes the recompute succeed at all.3. The roll-up did not run on the runner's telemetry at all
One agent run's telemetry arrives in two separate OTLP requests. The SDK sends the workflow root span in one. The runner sends its own subtree, headed by an
invoke_agentspan whose parent id points into the other request.The tree builder seeded only from spans with
parent_id is None. The runner's request contains no such span, so it produced an empty tree and nothing was cumulated. Even the leaf spans had no cumulative value, which is the decisive tell: had the roll-up run at all, a leaf would carry its own incremental value as its cumulative.A span whose parent is absent from the batch now seeds a tree, because a dangling parent id means "not in this batch" rather than "no parent". Roll-up stays batch local and still does not cross requests; bridging the two requests is a larger design question and is deliberately out of scope.
Verification
test_reported_cost_wins_over_recomputed_child_costs.Notes for the reviewer
parsing.pystill keys its top-leveltracesdict offparent_id is None, so the query response shape is unchanged.Related
Part of a set of three independent fixes for the same reported problem. The other two are the Python SDK streaming fix and the runner usage fix.