Repository navigation
fix(core): count a Responses request when the provider omits usage - #4444
ayaangazali wants to merge 4 commits into
Conversation
Usage.requests is documented as the number of requests made to the LLM API, but OpenAIResponsesModel returned a zero-request Usage whenever the provider sent no usage block. OpenAI-compatible gateways behind a custom base_url do that, and the Chat Completions, LiteLLM and AnyLLM adapters already count the request in the same situation. Mark the response inside the HTTP and websocket _fetch_response implementations rather than in get_response, so OpenAIHostedMultiAgentModel, which overrides _fetch_response to multiplex several provider responses into one, keeps its own request accounting.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3c5feac6d3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…em to the span Address the automated review on this PR. Streaming was still reporting zero requests, so identical successful calls were counted differently based only on streaming. The count now happens in the four transport exits this class owns: the HTTP and websocket non-streaming fetches, the HTTP stream wrapper this class builds, and the websocket event iterator. OpenAIHostedMultiAgentModel overrides _fetch_response and uses its own websocket turn iterator, so it keeps its own request accounting. The response span now carries the synthesized request count as well, so request-count dashboards built on spans no longer disagree with the returned ModelResponse.
|
Both points were right. Fixed in the latest commit. Streaming. You are right that this left a streaming versus non-streaming split, which The way out was to stop thinking about
Span usage. Fixed on both paths, non-streaming and streamed, mirroring what Five regression tests now, covering HTTP and websocket in both streaming and non-streaming form plus the span payload. All five fail against One gap I am deliberately leaving, so it is not a surprise: the compatibility branch in |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fc25a9e750
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
seratch
left a comment
There was a problem hiding this comment.
Thanks for the contribution. The underlying issue is valid, and the current head fixes the main HTTP and WebSocket paths. Two supported streaming cases still need correction before merge:
- Route the
responses.create()compatibility fallback through the same successful-terminal accounting as the normal HTTP stream path while preserving its existing async-iterable and cleanup behavior. - Populate the response span usage before yielding
response.completed, so a direct consumer that closes after the terminal event does not lose the synthesized request count.
Please add regression tests for the fallback async-iterable path and for a direct stream_response() consumer that closes immediately after the terminal event. Preserve absent raw usage, zero token counts, and Hosted Multi-Agent's separate accounting. Once these cases are covered and CI passes, this should be ready for another review.
…he terminal yield Address review feedback. The `responses.create()` compatibility fallback now goes through _ResponseStreamWithRequestId, so its terminal event gets the same accounting as the normal HTTP stream. The wrapper resolves its iterator on first use, so that path keeps working when the client hands back an async iterable rather than an iterator, and cleanup still closes whatever object it was given. Response span usage is now recorded when the terminal event is seen rather than after the loop, so a consumer that stops at response.completed and closes the generator keeps the synthesized request count.
|
Thanks, both were right. Done in 95952a2. Fallback path. It now goes through Span usage before the yield. Moved into the Two tests added for exactly those cases:
Both fail against the previous head. Raw usage stays absent and token counts stay zero in every one of these, and the Hosted Multi-Agent suite is untouched and still passing, which is the check that the exclusion still holds. On CI: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 95952a20dd
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Address the automated review on this PR. _close_stream_once only closed the object the wrapper was handed. When a fallback client returns an async iterable whose __aiter__() builds a separate generator, that generator owns the finally blocks, so closing or cancelling stream_response() left them unrun. Close the resolved iterator as well, and the source only when it is a different object. They are the same object on the normal path, so it still closes once.
|
Good catch, that was a leak I introduced when I widened the wrapper to accept an iterable. Fixed in the latest commit.
Worth noting how it is tested, because my first attempt at a regression test was useless: asserting on the existing exhaustion test passes either way, since an exhausted async generator runs its own
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c530b6afd1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| await self._close_object(resolved) | ||
| if self._source is not resolved: | ||
| await self._close_object(self._source) |
There was a problem hiding this comment.
Ensure source cleanup survives iterator close errors
When the fallback receives an async iterable whose resolved iterator has a failing aclose(), the exception from closing resolved skips the source close, and _stream_close_complete prevents any later retry. The fresh evidence beyond the prior cleanup comment is this two-stage failure path: provider resources owned by _source remain open precisely when iterator cleanup fails, so close the source in a finally path while preserving the original cleanup error.
AGENTS.md reference: AGENTS.md:L156-L156
Useful? React with 👍 / 👎.
|
Thanks for trying to resolve the issue. #4453 resolves it and its commit includes your contribution credit. |
Summary
Usage.requestsis documented as the number of requests made to the LLM API, butOpenAIResponsesModelreports zero for a call that completed without ausageblock:That is the same defect #4290 fixed for Chat Completions, LiteLLM and AnyLLM. The Responses adapter was not in that PR's scope, so it still undercounts. Reproduced on
mainagainst a client returning aResponsewithusage=None:Same completed request, two different answers. It matters for the deployments #4290 was written for: OpenAI-compatible gateways behind a custom
base_urlthat omitusage, where request accounting and rate-limit tracking built onUsage.requestsstay at zero for a whole run.openai_responses.py:533already handlesresponse.usage is None, so the adapter expects this shape.Where the mark goes matters. The obvious patch,
else Usage(requests=1)inget_response, is wrong:OpenAIHostedMultiAgentModelextendsOpenAIResponsesModel, inheritsget_responseandstream_response, and deliberately returns usage-less responses because it multiplexes several provider responses into one and reports its counts separately through_agents_sdk_request_count. That version double-counted and broketest_injection_failure_after_completion_starts_continuation_response, which is exactly what the existing_requests_for_response_without_usagedocstring warns about.So the count happens at the four transport exits this class owns, where "one request happened" is actually known:
_fetch_response, non-streaming HTTPOpenAIResponsesWSModel._fetch_response, non-streaming websocket_ResponseStreamWithRequestId.__anext__, the HTTP stream wrapper this class constructs_iter_websocket_response_events, the websocket event iteratorOpenAIHostedMultiAgentModeloverrides_fetch_responseand drives its own_iter_websocket_turn, so it reaches none of the four and is excluded by construction rather than by a check onself. Onlyresponse.completedcounts;failed,incompleteanderrorraise before any usage is consumed. The response span carries the synthesized count too, on both the streamed and non-streamed paths, mirroring whatopenai_chatcompletions.pyalready does at the equivalent point.One gap left on purpose: the compatibility branch in
_fetch_responsefor custom clients that implementresponses.create()but notwith_streaming_responsehands back the provider's iterator unwrapped, so it still does not count. Routing it through_ResponseStreamWithRequestIdwould change that path's stream lifecycle, and the wrapper needs an async iterator where that branch may return an async iterable.Test plan
mainand pass here, so they cover the fix rather than restating it. Each asserts token counts stay at zero, since no usage payload is synthesized.uv run pytest tests/models/ tests/extensions/: 2235 passed, 13 skipped. The Hosted Multi-Agent suite passes unchanged, which is the check that the placement above is right..agents/skills/code-change-verification/scripts/run.sh: format, lint, typecheck and the full test run all pass.Issue number
N/A. Found while comparing the Responses and Chat Completions adapters.
Checks
.agents/skills/code-change-verification/scripts/run.sh/reviewbefore submitting this PRheads up in case I misread something: I traced this myself off #4290 and checked the hosted multi-agent interaction after my first attempt broke its tests, and I talked the placement decision through with Claude Code before settling on it. if the streamed half should have been in here too, or the whole thing belongs elsewhere, tell me and I'll redo it. college freshman, still learning how these boundaries are meant to be drawn :)