feat(tracing): emit token usage on spans for SGP billing#458
Open
levilentz wants to merge 9 commits into
Open
Conversation
Contributor
|
Can we rebase and base this pr off of |
7c5249b to
b2fce50
Compare
Captures ResponseCompletedEvent usage in the streaming model (was zeroed) and response.usage in both tracing wrappers, writing span.output.usage for billing. Also implements stream_response on the tracing wrappers, which were abstract and raised TypeError on instantiation.
…spans Streaming calls now default stream_options.include_usage=True, the usage-only final chunk is collected, and both auto_send variants attach completion usage to span output. concat_completion_chunks no longer drops choices when a chunk has none.
TurnSpan.record_usage writes the billable aggregate to span.data (usage + cost_usd), encapsulating the contract so agents cannot re-introduce the double-count bug. Documents the usage/cost span contract in the tracing tutorial.
test_claude_agents_* now remove their placeholder packages after loading, which previously blocked real imports of the temporal plugin tree in later-collected tests. Formats touched files with ruff and narrows span.output types in new tests for pyright.
The wrappers never implemented abstract stream_response, so TemporalTracingModelProvider.get_model() raised TypeError on every call since introduction; no working callers can exist. The streaming provider plus run.py hooks are the live tracing path.
…sage helpers TurnSpan.record_usage now takes the TurnUsage every harness turn adapter reports (cost_usd lifted to data automatically) or a plain dict, replacing the individual-count kwargs and the lib/core/tracing/usage.py helpers that duplicated what next's harness provides.
b2fce50 to
3748d30
Compare
Contributor
|
Checked this against the harness work now on
All harness conformance suites (langgraph/codex/claude_code + |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes every SDK tracing adapter emit real token usage for tracking: the OpenAI Agents SDK temporal models, the LangGraph handler, and litellm streaming/auto_send previously dropped usage and billed zero. Adds
adk.tracing.turn_span()so agents record the per-turn aggregate (data["usage"]+cost_usd) instead of hand-rolling the contract that caused double-counting. Also fixes three bugs found along the way (uninstantiable tracing wrapper models, chunk concat wiping choices on empty-choices chunks,sys.modulesleakage in the claude_agents tests); 30 new tests,tests/libat 260 passing, ruff and pyright clean.Greptile Summary
This PR ensures every SDK tracing adapter emits real token usage so SGP billing can read
span.output["usage"]or the per-turn rollupspan.data["usage"]+span.data["cost_usd"]instead of billing zero. It introducesTurnSpan/turn_span()to capture per-turn aggregate usage, and fixes three concrete bugs found along the way.concat_completion_chunksfix — replaceszip(…, strict=False)withzip_longestso the usage-only final chunk emitted bystream_options.include_usage: Trueno longer wipes the accumulated choices; tested with newtests/lib/utils/test_completions.py._stream_kwargs_with_usageto defaultinclude_usage: Trueand propagates the resultingcompletion.usageontospan.output["usage"]for all three litellm paths; chunks are now always appended before thechoicesguard so the usage-only tail chunk reachesconcat_completion_chunks.TurnSpan/TracingModule.turn_span()— new context manager that writes the turn's rollup usage tospan.data["usage"]/span.data["cost_usd"]; acceptsTurnUsageor a plain recognized-key dict; warns on non-dictspan.dataand unrecognized keys; deletes the uninstantiableTemporalTracingModelProviderwrappers; fixessys.modulesstub leakage in claude_agents tests.Confidence Score: 5/5
Safe to merge — all three previously broken usage emission paths are fixed, the concat bug is correctly addressed, and 30 new tests cover the changed behavior end-to-end.
The changes are tightly scoped to tracing/billing paths with no risk of disrupting the main agent execution path. The zip_longest fix is the most load-bearing change, and it is both logically correct and verified by tests covering the usage-only tail chunk, summed usage, and the empty-choices edge case. The TurnSpan API is well-guarded (no-op when span is None, warning on unexpected types). Deletion of TemporalTracingModelProvider removes dead/broken code without changing any active execution path since TemporalStreamingModel already handles the tracing.
No files require special attention.
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Agent participant turn_span as adk.tracing.turn_span() participant LiteLLM as LiteLLMService participant Gateway as LiteLLMGateway participant Backend as SGP Backend Agent->>turn_span: async with turn_span(trace_id, name, ...) turn_span-->>Agent: TurnSpan handle Agent->>LiteLLM: chat_completion_stream_auto_send(...) LiteLLM->>LiteLLM: _stream_kwargs_with_usage(llm_config) LiteLLM->>Gateway: "acompletion_stream(**completion_kwargs)" Gateway-->>LiteLLM: "delta chunks [choices=[...delta...]]" Gateway-->>LiteLLM: "usage-only chunk [choices=[], usage={...}]" Note over LiteLLM: chunks.append(every chunk), zip_longest fix keeps choices + usage LiteLLM->>LiteLLM: concat_completion_chunks(chunks) LiteLLM->>LiteLLM: "span.output = task_message + usage" Agent->>turn_span: turn.record_usage(result.usage) turn_span->>turn_span: "span.data = {usage: {...}, cost_usd: 0.xx}" turn_span->>Backend: end_span(span) Backend->>Backend: de-dup per-call output[usage] against aggregate data[usage] Backend-->>Agent: billed once per turn%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Agent participant turn_span as adk.tracing.turn_span() participant LiteLLM as LiteLLMService participant Gateway as LiteLLMGateway participant Backend as SGP Backend Agent->>turn_span: async with turn_span(trace_id, name, ...) turn_span-->>Agent: TurnSpan handle Agent->>LiteLLM: chat_completion_stream_auto_send(...) LiteLLM->>LiteLLM: _stream_kwargs_with_usage(llm_config) LiteLLM->>Gateway: "acompletion_stream(**completion_kwargs)" Gateway-->>LiteLLM: "delta chunks [choices=[...delta...]]" Gateway-->>LiteLLM: "usage-only chunk [choices=[], usage={...}]" Note over LiteLLM: chunks.append(every chunk), zip_longest fix keeps choices + usage LiteLLM->>LiteLLM: concat_completion_chunks(chunks) LiteLLM->>LiteLLM: "span.output = task_message + usage" Agent->>turn_span: turn.record_usage(result.usage) turn_span->>turn_span: "span.data = {usage: {...}, cost_usd: 0.xx}" turn_span->>Backend: end_span(span) Backend->>Backend: de-dup per-call output[usage] against aggregate data[usage] Backend-->>Agent: billed once per turnComments Outside Diff (1)
src/agentex/lib/adk/_modules/_langgraph_tracing.py, line 89-91 (link)_extract_usageis called inside the innerfor generation in generation_listloop, sooutput["usage"]gets overwritten on each iteration. Forn=1calls (the common case) this is harmless, but if a caller ever requestsn > 1completions only the last generation'susage_metadatacontributes to the billing blob — earlier generations are silently dropped. Thellm_outputfallback is safe because it reads the response-level aggregate and returns the same dict every iteration, but theusage_metadatapath is per-generation and should accumulate.Prompt To Fix With AI
Reviews (4): Last reviewed commit: "refactor(adk): record_usage accepts harn..." | Re-trigger Greptile