fix(llmobs): pre-parse OpenAI AsyncAPIResponse to fix async streaming span lifecycle [MLOB-6778] - #17432
Conversation
…ming span lifecycle _EndpointHook.handle_request (a sync generator) calls resp.parse() on AsyncAPIResponse, which returns an unawaited coroutine instead of the actual stream. This prevents the stream from being wrapped in TracedAsyncStream, so the span for responses.create(stream=True) is never finished — breaking LLMObs trace grouping for streamed sub-agent calls (e.g. via OpenAI Agents SDK's Runner.run_streamed()). The fix: 1. Pre-parse AsyncAPIResponse in async_wrapper and _trace_and_await before sending into the sync _traced_endpoint generator, so the hook receives the actual AsyncStream and can wrap it. 2. Inject the hook's TracedAsyncStream back into the response's parse cache (_parsed_by_type for SDK v2.x, _parsed for v1.x) so callers iterate the traced version, which finishes the span on completion. Fixes #17060 Made-with: Cursor
…e tests - Extract `_maybe_preparse_async_response()` to deduplicate pre-parse logic between `async_wrapper` and `_trace_and_await` (Kyle comment on duplication) - Wrap pre-parse in try/except so parse failures never suppress the original exception or change application-visible behaviour (Kyle + Codex comments) - Add SDK version documentation to `_inject_into_parse_cache` docstring noting which private internals correspond to which SDK versions - Unify post-hook injection logic between `_trace_and_await` and `async_wrapper` for consistency (Kyle asymmetry comment) - Rewrite tests: replace class-based MagicMock tests with parametrized functions using typed fake objects; add comprehensive tests for the new `_maybe_preparse_async_response` helper including error safety; remove broken FakeEndpointHook test (Codex comment) Made-with: Cursor
Wrap _inject_into_parse_cache in try/except to prevent tracing from raising into application code if OpenAI SDK private internals change. Add two E2E regression tests validating that async streaming spans are properly finished with correct parent-child relationships, covering both Chat Completions and Responses API paths. Addresses review feedback from PR #17061.
Codeowners resolved as |
f6de1e0 to
0271a43
Compare
|
Codex Review: Didn't find any major issues. Another round soon, please! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
c3c4ff4 to
3c587bf
Compare
emmettbutler
left a comment
There was a problem hiding this comment.
release note looks fine
ncybul
left a comment
There was a problem hiding this comment.
Left a couple comments / questions, but the overall structure looks good to me!
…x [MLOB-6778] E2E regression tests in test_openai_v1.py already cover these paths.
3c587bf to
0824d86
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8f7c73e2f1
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8f7c73e2f1
ℹ️ 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".
Description
Cherry-pick of #17061 (authored by @cedomir) with additional safety improvements and E2E regression tests.
Fixes #17060 — when using the OpenAI Agents SDK with
Runner.run_streamed(), streaming spans forresponses.create(stream=True)are never finished, breaking LLM Observability trace grouping for sub-agent calls.Root cause
_EndpointHook.handle_request(a sync generator) callsresp.parse()onAsyncAPIResponse. Sinceparse()isasync def, this returns an unawaited coroutine. The coroutine flows into_handle_streamed_response, which can't recognize it as anAsyncStream, so the stream is never wrapped inTracedAsyncStream. The span is never finished, corrupting parent context for subsequent sub-agent calls.Fix
AsyncAPIResponsevia_maybe_preparse_async_response()before sending into the sync_traced_endpointgenerator, so the hook receives the actualAsyncStream.TracedAsyncStreamback into the response's parse cache via_inject_into_parse_cache()so the SDK caller iterates the traced version — which finishes the span on stream completion.Additional changes (beyond cherry-pick)
_inject_into_parse_cache()now wrapped intry/except Exceptionto prevent tracing from raising into application code if SDK private internals change. Matches the safety pattern already used in_maybe_preparse_async_response().test_openai_v1.pyusing VCR cassettes andtest_spansassertions:test_chat_completion_async_stream_span_finished— Chat Completions async streamingtest_response_async_stream_span_finished— Responses API async streamingtest_achat_completion_raw_response_non_stream— Async non-streamed with_raw_response (parse cache path)import asynciofrom unit test file.Before / After
Runner.run_streamed()with sub-agentsRuntimeWarning: coroutine 'AsyncAPIResponse.parse' was never awaited, spans dropped at shutdownRunner.run()(non-streaming)Before (unpatched ddtrace 4.6.7):

After (patched ddtrace 4.8.0rc2):

Testing
_inject_into_parse_cache(SDK v1.x and v2.x) and_maybe_preparse_async_response(async/sync parse, error safety, None handling)span.duration is not None)with_raw_responseparse cache pathRunner.run_streamed()with sub-agent handoffs) — confirmed broken behavior on ddtrace 4.6.7, confirmed fix on 4.8.0rc2Risks
_parsed_by_typefor v2.x,_parsedfor v1.x) for parse cache injection — could break on SDK updates. Both access points wrapped intry/exceptfor safety; failure degrades to pre-fix behavior (unfinished spans), never crashes the application.parse()is now called eagerly (before the hook) rather than lazily. OpenAI SDK'sparse()is idempotent/cacheable, so no side-effect risk with the current SDK.