Repository navigation
fix: count a model request when the provider omits usage - #4281
abhay-codes07 wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fee6f48150
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
seratch
left a comment
There was a problem hiding this comment.
Before we can merge, please make missing-usage request counting consistent for streaming. Today ChatCmplStreamHandler leaves terminal usage as None and the run loop converts that to Usage(), so OpenAI Chat Completions, LiteLLM, and AnyLLM still report requests == 0 when streamed; AnyLLM Responses has the same non-streaming/streaming split.
Please fix this at the owning adapter boundaries, preserving raw_usage=None when no provider payload was received. Do not change the generic run-loop fallback to Usage(requests=1), because Hosted Multi-Agent emits interim logical completions with usage=None and later reports accumulated request usage, so that would double-count. Add regression coverage for the shared Chat Completions stream path and AnyLLM Chat/Responses paths, including streaming. Once those fail on main and pass on the branch, this should be ready for another review.
|
Thanks, all three points are addressed in 0194e7a. Streaming was still reporting zero. Confirmed before changing anything: a streamed run over a provider that sends no usage chunk reported Fixed at the owning adapter boundaries. An adapter that reaches a terminal response without a provider usage payload now marks that response: mark_request_completed_without_usage(final_response)Nothing is synthesized, so
The generic run-loop fallback is unchanged in effect. It reads a helper that defaults to zero: def requests_for_response_without_usage(response: Any) -> int:
"""Defaults to zero so adapters that multiplex several provider responses into one,
and report their counts separately, are not double-counted."""
return 1 if getattr(response, _REQUEST_WITHOUT_USAGE_ATTR, False) else 0So Hosted Multi-Agent's interim logical completions with Regression coverage, each failing on
Plus paired guards that pass either way, so the usage-less path cannot start double-counting later: The end-to-end streaming test asserts what a user sees rather than the marker, so it is a genuine assert result.context_wrapper.usage.requests == 1 # 0 on main
assert result.context_wrapper.usage.total_tokens == 0
assert completed.response.usage is None # nothing synthesized
|
|
Can you resolve the conflicts? |
`Usage.requests` is documented as "Total requests made to the LLM API",
but a completed call was counted as zero requests whenever the provider
returned no usage block:
usage = (Usage(requests=1, ...) if response.usage else Usage())
Some OpenAI-compatible providers and gateways omit `usage` entirely, and
LiteLLM already logs "No usage information returned from Litellm" for the
same situation. In those deployments `usage.requests` stays at zero for a
whole run even though every turn made a real HTTP call, so anything built
on it, such as request accounting or rate-limit tracking, silently
undercounts.
The retry path already takes the opposite position. `apply_retry_attempt_usage`
forces the successful attempt to count with `max(usage.requests, 1)`, so
the same single call is reported differently depending on whether a retry
happened to occur:
failed_attempts=0 -> 1 HTTP call, reported requests=0
failed_attempts=1 -> 2 HTTP calls, reported requests=2
Count the request itself in the Chat Completions, LiteLLM and any_llm
models. Token counts stay at zero, since the provider genuinely did not
report them.
The Responses model is deliberately left alone. `OpenAIHostedMultiAgentModel`
subclasses it and merges several provider responses into a single
`ModelResponse`, tracking its own count through `_agents_sdk_request_count`.
There, a response without usage means the usage was already reported on an
earlier response rather than that a request went uncounted, so applying the
same change double-counts.
test_get_response_with_refusal asserted `requests == 0` for a completed
call with no usage. That expectation is the behaviour being corrected, so
it now asserts 1 while keeping the zeroed token assertions.
Addresses review feedback. The first revision only fixed the non-streaming adapters, so OpenAI Chat Completions, LiteLLM and AnyLLM still reported `requests == 0` when streamed: `ChatCmplStreamHandler` leaves the terminal usage as `None` and the run loop turned that into `Usage()`. AnyLLM's Responses path had the same non-streaming/streaming split. Fixed at the owning adapter boundaries rather than in the run loop. An adapter that reaches a terminal response without a provider usage payload now marks that response with `mark_request_completed_without_usage`, and no usage object is synthesized, so `raw_usage` stays absent and token counts are not reported as real zeros. `ChatCmplStreamHandler` is shared by the OpenAI Chat Completions, LiteLLM and AnyLLM chat streams, so marking it there covers all three. AnyLLM's Responses stream is marked where it captures the completed response. The generic run-loop fallback is unchanged in effect. It now reads `requests_for_response_without_usage`, which defaults to zero, so Hosted Multi-Agent keeps working: it emits interim logical completions with `usage=None` and reports accumulated request usage later, and would be double-counted by an unconditional `Usage(requests=1)`.
0194e7a to
7b52a69
Compare
|
Rebased onto The conflict was with the new
That work also makes the intent here more explicit than when I first wrote it. Re-verified on the rebased branch against |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7b52a69f9a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
seratch
left a comment
There was a problem hiding this comment.
Thanks for the update. The streaming accounting, AnyLLM Responses path, raw-usage behavior, and Hosted Multi-Agent double-count protection now look correct.
One small API-surface change remains: please rename mark_request_completed_without_usage and requests_for_response_without_usage to underscore-prefixed internal helpers and update their imports and tests. These functions are adapter/run-loop plumbing rather than supported agents.usage API, so they should not create an accidental public contract. Once that is done and the current CI finishes green, this should be ready to approve.
Two follow-ups from review. Rename `mark_request_completed_without_usage` and `requests_for_response_without_usage` to underscore-prefixed helpers. They are adapter and run-loop plumbing rather than supported `agents.usage` API, so they should not create an accidental public contract. Also record the request in the generation span, not only in the run result. Non-streaming already writes a span usage object with `requests: 1` when the provider reports no usage, but the streaming paths populated span usage only when the terminal response carried a usage payload. A streamed run therefore reported one request while its model span omitted the request entirely. The OpenAI Chat Completions, LiteLLM and AnyLLM chat spans now fall back to a shared `_span_usage_without_provider_totals()`, so streamed and non-streaming tracing agree. Token totals stay at zero, since the provider reported none.
|
Thanks. Both done in 93ebe1e. Renamed to internal helpers. The automated review also caught a real gap in the same area, so it is fixed here. Span usage was populated only when the terminal response carried a usage payload, so a streamed run reported one request while its model span omitted the request entirely:
The OpenAI Chat Completions, LiteLLM and AnyLLM chat spans now fall back to a shared
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93ebe1ef0b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@abhay-codes07 I won't review again unless all the codex review feedback are resolved. |
|
ok I am trying my best to resolve it @seratch |
Two more findings from automated review. The generation span was populated after the stream loop finished. A caller that consumes `stream_response()` directly and stops once it sees `response.completed` closes the generator at that yield, so the loop never resumes and the span was left without usage or output even though the run counted the request. Both the OpenAI Chat Completions and LiteLLM streams now populate the span at the point the terminal response is captured, before it is handed out, matching what the AnyLLM chat path already did. The AnyLLM Responses path never set `span_response.span_data.usage` at all, so with tracing enabled the run result and the turn and task spans counted a request that the model response span did not show. It now records usage the way `OpenAIResponsesModel` does, on both the streaming and non-streaming paths, which also makes the usage-less request count visible there.
|
All Codex threads on this PR are now addressed and resolved. Span usage is recorded before the terminal event is yielded. It was being set after the stream loop, so a caller that consumes Covered by The AnyLLM Responses path now records span usage at all. It never set The remaining thread was the original "count usage-less streamed requests too" note, written against
|
|
typecheck is failing, so #4290 will take over the completion. Your contribution credit is included in the PR's commit. |
Summary
Usage.requestsis documented as "Total requests made to the LLM API", but a completed call is counted as zero requests whenever the provider returns no usage block:Some OpenAI-compatible providers and gateways omit
usageentirely. The SDK already knows this happens:litellm_model.pylogs"No usage information returned from Litellm", and a comment inrun_loop.pycalls it "common for some Chat Completions / LiteLLM streams". In those deploymentsusage.requestsstays at zero for an entire run even though every turn made a real HTTP call, so request accounting and rate-limit tracking built on it silently undercount.Reproduced against
OpenAIChatCompletionsModelwith a completion whoseusageisNone:The retry path already takes the opposite position
apply_retry_attempt_usageforces the successful attempt to count viamax(usage.requests, 1). So the same single call is reported differently depending on whether a retry happened to occur:requests(before)The successful request is counted only when something else failed first. That is the inconsistency this fixes.
Fix
Count the request itself in the Chat Completions, LiteLLM and any_llm models. Token counts stay at zero, since the provider genuinely did not report them.
Why the Responses model is deliberately excluded
OpenAIHostedMultiAgentModelsubclassesOpenAIResponsesModeland merges several provider responses into a singleModelResponse, tracking its own count through_agents_sdk_request_count. There, a response without usage means the usage was already reported on an earlier response, not that a request went uncounted.I confirmed this rather than assuming it: applying the same change to
openai_responses.pyand to the streaming branch inrun_loop.pyturnedtest_injection_failure_after_completion_starts_continuation_responsefromrequests == 2intorequests == 3, in both the streamed and non-streamed parametrizations. So that path is left as is. If the intent there is different, I am happy to extend the change, but I did not want to guess at the semantics of an experimental multiplexing path.This does mean the Responses path still undercounts when a provider omits usage. I would rather fix that separately, with the subclass opting out explicitly, than change it on an inference.
Test plan
New coverage, all failing before the change and passing after:
test_request_is_counted_when_provider_omits_usage, Chat Completions withusage=Nonetest_request_is_counted_when_litellm_reports_no_usage, in a newtests/models/test_litellm_usage_requests.pyOne existing assertion changed.
test_get_response_with_refusalassertedresp.usage.requests == 0for a completed call with no usage, which is the behaviour being corrected. It now asserts1, and its zeroed token assertions are unchanged.make format/make lintmake mypymake testsRun on Windows, where a set of sandbox and tracing tests fail regardless of this change. I diffed the failing set against
mainat the same commit: no new failures. Two tests that varied between runs,tests/test_tracing.py::test_simple_tracingandtests/sandbox/test_docker.py::test_docker_pty_exec_write_and_poll, both pass in isolation on this branch and fail intermittently under-n 6on unmodifiedmain, so they are part of the pre-existing set.Checks
.agents/skills/code-change-verification/scripts/run.sh/reviewbefore submitting this PRThe verification script shells out to
make; I ran the underlying steps individually, with the results above.