| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
…ft#7461) Setting AZURE_EXPERIMENTAL_ENABLE_GENAI_TRACING=true made every streaming request fail with: AttributeError: 'AsyncStreamWrapper' object has no attribute 'parse' The Azure GenAI instrumentor replaces the SDK's raw-response wrapper with an object that *is* the event stream and exposes neither .parse() nor .headers. Both streaming paths already read .headers defensively via getattr, with a comment explaining that instrumentors wrap the response -- but then called .parse() unconditionally on that same object. Read .parse defensively too: _open_event_stream() uses .parse() when present and otherwise iterates the object directly, letting the instrumentor own the stream's lifetime. Behavior is unchanged for the normal SDK wrapper. The non-streaming .parse() call sites are left alone: the instrumentor's wrapper is stream-specific and those paths were not reported as failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Note
Copilot was unable to run its full agentic suite in this review.
Adds a defensive streaming path so chat streaming continues to work when telemetry/tracing replaces the SDK raw-response wrapper (removing .parse() / .headers), and introduces a regression test for that scenario.
Changes:
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| python/packages/openai/agent_framework_openai/_chat_client.py | Adds _open_event_stream() and switches streaming to use it to avoid .parse() AttributeError under instrumentation. |
| python/packages/openai/tests/openai/test_openai_chat_client.py | Adds regression coverage for streaming when the raw response is replaced by a bare async stream. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Sorry, something went wrong.
Review feedback: yielding the telemetry wrapper as-is only moved the AttributeError. Verified against azure-ai-projects==2.3.0 with openai==2.53.0: with_raw_response.create() routes through the instrumented AsyncResponses.create, so AsyncStreamWrapper.stream_async_iter is the still-unparsed LegacyAPIResponse, which is not an async iterator. Iterating the wrapper fails on the first __anext__ and traced streaming stays broken. Parse that inner raw response and hand it back to the wrapper instead, so the wrapper stays in the iteration path and keeps recording telemetry while real events flow through it. The previous test patched with_raw_response.create, i.e. above the layer that does the wrapping, so it could not catch this. The test now models the observed object graph -- a wrapper with no parse/headers whose stream_async_iter is an unparsed raw response -- and fails against the previous fix. A second test covers a bare event stream with nothing to parse. Also guard with callable() rather than an is-None check, per review. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks both — Evan Mattson (@moonbox3) you were right, and the PR is updated. Verified the failure you describedI reproduced it against the real instrumentor (azure-ai-projects==2.3.0, openai==2.53.0, AZURE_EXPERIMENTAL_ENABLE_GENAI_TRACING=true), with a mocked transport so no Azure resources are needed. The instrumentor needs both an OTel TracerProvider and azure-core-tracing-opentelemetry for _create_responses_span_from_parameters to return a span — without them it returns the result unwrapped and the bug doesn't appear at all, which is what made this easy to miss. With tracing genuinely active: [wrap] _wrap_async_streaming_response receives stream=openai._legacy_response.LegacyAPIResponse
with_raw_response.create() -> azure.ai.projects.telemetry._responses_instrumentor.AsyncStreamWrapper
has .parse() : False
has .headers : False
.stream_async_iter = openai._legacy_response.LegacyAPIResponse
has __anext__: False has parse: True
--- iterating the wrapper directly (what the first commit did) ---
FAILS: AttributeError: 'LegacyAPIResponse' object has no attribute '__anext__'
Exactly as you said: with_raw_response.create() routes through the instrumented AsyncResponses.create, so stream_async_iter is the still-unparsed LegacyAPIResponse. My first commit just traded one AttributeError for another and traced streaming stayed broken. You were also right about why the test missed it: it patched with_raw_response.create, which is above the layer that installs the wrapper, so the wrapper's inner raw response never existed in that test. What changed_open_event_stream() now parses the raw response the wrapper holds and hands it back, so the wrapper stays in the iteration path and keeps recording telemetry: inner = getattr(raw_response, "stream_async_iter", None)
inner_parse = getattr(inner, "parse", None)
if callable(inner_parse):
async with cast("Any", inner_parse()) as stream:
raw_response.stream_async_iter = stream
yield raw_response
returnEnd-to-end against the real instrumentor now: raw from with_raw_response : AsyncStreamWrapper .stream_async_iter : LegacyAPIResponse _open_event_stream yielded: AsyncStreamWrapper RESULT: streamed 'Hello world' telemetry wrapper still in iteration path: True The test now models that observed object graph — a wrapper with no parse/headers whose stream_async_iter is an unparsed raw response — and fails against my previous commit with AttributeError: '_UnparsedRawResponse' object has no attribute '__aiter__', mirroring the real failure. I added a second test for a bare event stream with nothing to parse, covering the remaining branch. I kept the fake rather than adding azure-ai-projects as a test dependency, since that would be a heavy dep for this package — happy to add a marked integration test instead if you'd prefer the real thing in CI. On the two Copilot comments
Checks
|
Sorry, something went wrong.
|
madanmishra1223 please resolve all open comments if they've been addressed |
Sorry, something went wrong.
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
Sorry, something went wrong.
Test Typing Checks caught two problems in the new test helpers: - ty: the wrapper's stream_async_iter was annotated `object`, so delegating to __aiter__/__anext__ was an attribute error. Annotate it `Any`, which also makes the two `type: ignore` comments unnecessary. - zuban: `_BareEventStream` was declared inside the test function, and its own forward-referenced return annotation does not resolve there. Move it to module scope alongside the other stream fakes. Verified with the task CI runs, `poe test-typing -P openai`: mypy, pyrefly, ty, zuban and pyright all pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Hi madanmishra1223, Thanks for the contributions! Could you also create an issue in the Azure SDKs repo and link it here? |
Sorry, something went wrong.
|
Thanks Tao Chen (@TaoChenOSU) — filed upstream: Azure/azure-sdk-for-python#48646 It has a standalone repro (openai + azure-ai-projects only, mocked transport, no Azure resources) and frames it as the raw-response contract being broken by the instrumentor:
I called out there that reproducing needs both an OTel TracerProvider and azure-core-tracing-opentelemetry — without them _create_responses_span_from_parameters returns None, the instrumentor returns the result unwrapped, and the bug silently doesn't appear. That tripped me up initially, so it seemed worth stating explicitly for whoever picks it up. I also linked this PR from that issue and noted the workaround here can be dropped once it's fixed upstream. One process note: this branch is still showing action_required on the workflows, so the Test Typing Checks fix in c85069cd hasn't been re-verified by CI yet. It passes locally via the task CI uses (poe test-typing -P openai: mypy, pyrefly, ty, zuban, pyright all green) — but it would need a workflow approval to confirm in CI. |
Sorry, something went wrong.
|
Hi madanmishra1223, Thanks! Please also resolve all open comments when you can. |
Sorry, something went wrong.
|
Tao Chen (@TaoChenOSU) all three review threads are resolved. Since resolving collapses them, here is what each one says, so it is readable without expanding: 1. Copilot — guard parse with callable() (r3799495111) parse = getattr(raw_response, "parse", None)
if callable(parse): ...
inner_parse = getattr(inner, "parse", None)
if callable(inner_parse): ...2. Copilot — use AsyncIterator instead of AsyncGenerator (r3799495126) Annotating the return type as `-> AsyncIterator[Foo]` with `@asynccontextmanager` is deprecated. Use `-> AsyncGenerator[Foo]` instead. (reportDeprecated) Baseline for the package is 0 errors; AsyncIterator took it to 1. Switching back would break Test Typing Checks. Happy to revisit if you would rather match the convention and suppress the rule. 3. Evan Mattson (@moonbox3) — exercise it through the real azure-ai-projects instrumentor (r3800092642) AttributeError: 'LegacyAPIResponse' object has no attribute '__anext__' Your point about the test was right too — it patched with_raw_response.create, above the layer that installs the wrapper, so the inner raw response never existed in it. _open_event_stream() now parses the wrapped raw response and hands it back, keeping the wrapper in the iteration path so telemetry still records. End-to-end against the real package: raw from with_raw_response : AsyncStreamWrapper .stream_async_iter : LegacyAPIResponse _open_event_stream yielded: AsyncStreamWrapper RESULT: streamed 'Hello world' telemetry wrapper still in iteration path: True The test now models that object graph and fails against my previous commit. Status: upstream issue filed and linked (Azure/azure-sdk-for-python#48646), branch up to date with main, and CI is green on the current head — 35 checks, 23 success / 12 skipped / 0 failed, including the Test Typing Checks job that was previously failing. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Fixes #7461
Problem
With AZURE_EXPERIMENTAL_ENABLE_GENAI_TRACING=true, every streaming request fails:
The Azure GenAI instrumentor replaces the SDK's raw-response wrapper with an object that is the event stream and exposes neither .parse() nor .headers.
Both streaming paths in _chat_client.py already anticipate this for headers, and say so in a comment:
.headers is read through getattr, then .parse() is called unconditionally on that same object. So the defense is half-applied: the attribute that only degrades a nice-to-have is guarded, and the attribute that breaks the entire call is not.
Fix
Read .parse defensively too. _open_event_stream() uses .parse() when present — preserving the async with so the socket still closes deterministically — and otherwise iterates the object directly, letting the instrumentor own the stream's lifetime.
Behavior is unchanged for the normal SDK wrapper; only the instrumented case changes, from raising to streaming.
Both affected streaming call sites are updated (the retrieve continuation path and the create path).
The two non-streaming .parse() sites are deliberately left alone: the instrumentor's wrapper is stream-specific (AsyncStreamWrapper), and non-streaming was not reported as failing. Happy to extend if maintainers prefer symmetry.
Verification
Repro before the fix, using a stand-in wrapper that is async-iterable with no .parse()/.headers:
instrumented object: AsyncStreamWrapper has .parse() : False has .headers : False async-iterable: True BUG: ChatClientException: (... service failed to complete the prompt: 'AsyncStreamWrapper' object has no attribute 'parse'", AttributeError(...))After:
Added test_streaming_survives_instrumented_response_without_parse, which fails on main with the exact reported AttributeError and passes with this change.
Checks run locally:
Note
The issue is assigned to Tao Chen (@TaoChenOSU). There was no linked PR after ~2.5 weeks, so I picked it up — happy to close this if it is already in progress.