Skip to content

fix(sdk): stop discarding a cost that arrives without a token count - #5721

Open
mmabrouk wants to merge 1 commit into
fix/sdk-streaming-usage-spanfrom
fix/sdk-cost-only-usage
Open

fix(sdk): stop discarding a cost that arrives without a token count#5721
mmabrouk wants to merge 1 commit into
fix/sdk-streaming-usage-spanfrom
fix/sdk-cost-only-usage

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Aug 3, 2026

Copy link
Copy Markdown
Member

Stacked on #5708. Set the base to that branch, so this diff shows only its own change.

The symptom

A run that reports a cost but no trustworthy token split loses the cost entirely on the workflow root span.

That combination is deliberate upstream, not an edge case. The runner returns exactly {total: 0, cost: ...} to say "I have a billed figure but no split I trust", and it has a test pinning that shape (services/runner/tests/unit/sandbox-agent-usage.test.ts:72-76).

record_usage returned as soon as the token total was falsy, and wrote the cost only after that check. So the cost lived on the runner's own spans and never reached the root. Trace-focused analytics read root spans only, and the roll-up works per OTLP request while the workflow root ships in its own request, so nothing downstream could recover it.

The change

The early return now fires only when neither a token count nor a cost is present, and the cost is written independently of the token total.

Missing versus measured zero, drawn deliberately

The two are decided differently, and the reason matters.

Cost is decided by presence. A reported 0.0 is stamped as a real measurement, because a free model or a fully cached turn genuinely costs zero. An absent cost stamps nothing. This matches the API's own convention, which tests for the attribute rather than its value.

A token total of zero is read as absence. It is the runner's own sentinel for "no trustworthy split", and no model call spends zero tokens. Writing zeros would assert a run that consumed nothing and would poison the token roll-up.

Inside a reported split, a zero input or output is written as the measurement it is, because a split can honestly be one-sided, and the runner has a test for exactly that.

One incidental hardening

The body moved inside the existing error handling. The old code read the token total before the guard, so a malformed record raised into the caller instead of being logged. This function is documented as best effort and must never raise.

Verification

  • SDK suite: 2,396 passed, 4 skipped, 10 xfailed. The 97 errors are the acceptance suite refusing to run without a live stack, and nothing else.
  • The service's own agent tests: 100 passed. Those drive this recorder seam, so they are the check that the change stays backward compatible.
  • Six new tests: a cost-only record is stamped with no invented token zeros; a tokens-only record is stamped; records with neither are skipped; a reported zero cost is stamped while an absent one is not; a one-sided split keeps its measured zero; a malformed record never raises.

Note for the reviewer

The runner's ACP path still writes input_tokens = 0 on a cost-only run, which is the opposite of what the root span now does. That is pre-existing and out of scope here, but the two should be made to agree.

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Aug 3, 2026
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview Aug 3, 2026 10:30pm

Request Review

@dosubot dosubot Bot added the python Pull requests that update Python code label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cd296f17-619a-407e-a1e4-6a4a8d824794

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-5721.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-5721-63a5740
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-08-03T22:31:33.101Z

`record_usage` returned as soon as the token total was falsy, and it wrote the cost
only after that check. So a run that reports a cost but no trustworthy token split
lost the cost entirely.

That combination is deliberate upstream, not an edge case. The runner returns exactly
`{total: 0, cost: ...}` to say "I have a billed figure but no split I trust", and it
has a test pinning that shape. The SDK then threw the whole record away, so the cost
lived on the runner's own spans and never reached the workflow root. Trace-focused
analytics read root spans only, and the roll-up works per OTLP request, so nothing
downstream could recover it.

Now the early return fires only when neither a token count nor a cost is present, and
the cost is written independently of the token total.

Missing and measured zero stay distinct, and the two are drawn differently on purpose.
A cost is decided by presence, so a reported 0.0 is stamped as a real measurement, a
free model or a fully cached turn, while an absent cost stamps nothing. That matches
the API's own convention, which tests for the attribute rather than its value. A token
total of zero is read as absence, because it is the runner's own sentinel for "no
trustworthy split" and no model call spends zero tokens; writing zeros there would
assert a run that consumed nothing and would poison the token roll-up. Inside a
reported split, a zero input or output is written as the measurement it is, since a
split can honestly be one sided.

The body also moved inside the existing error handling. The old code read the token
total before the guard, so a malformed record raised into the caller instead of being
logged.

Tests: 2,396 passing in the SDK suite, and 100 passing in the service's own agent
tests, which drive this recorder seam. Six new tests cover a cost-only record, a
tokens-only record, records with neither, a reported zero cost against an absent one,
a one-sided split, and a malformed record.

Claude-Session: https://claude.ai/code/session_01RkWWQUNNzRbaB5jnCAdjYA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Pull requests that update Python code size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant