fix(anthropic): handle with_raw_response results without crashing - #381
fix(anthropic): handle with_raw_response results without crashing#381Shailendra005 wants to merge 5 commits into
Conversation
Messages.create / AsyncMessages.create (and the parse() wrappers) assumed the wrapped call always returns a Message. When called through with_raw_response the SDK returns a LegacyAPIResponse instead, and reading message.model during extraction raised AttributeError into the caller's app after the API call had already succeeded. Detect a raw-response result by shape (a parse() method, mirroring the OpenAI sibling package's ParsableResponse) and extract telemetry from the parsed Message, while returning the original raw response to the caller untouched. Streaming raw responses and any unexpected payload are left uninstrumented rather than crashing. Covers the sync and async create/parse paths. Adds sync and async regression tests. Signed-off-by: Shailendra005 <shivzala06@outlook.com>
Signed-off-by: Shailendra005 <shivzala06@outlook.com>
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Prevents the Anthropic OTel instrumentation from crashing when messages.create(...) is invoked via with_raw_response, by extracting telemetry from the parsed Message while returning the raw-response object untouched.
Changes:
- Add a runtime-checkable
_RawResponseProtocolplus_message_for_extraction()helper to safely parse raw responses for telemetry. - Update sync/async
messages_createwrappers to extract attributes from the parsedMessage(when available) and return the original result. - Add sync/async regression tests (with VCR cassettes) for
with_raw_response.create.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/patch.py | Adds raw-response shape matching + parses for extraction; returns original result to avoid breaking callers. |
| instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/test_sync_messages.py | Adds regression test ensuring with_raw_response.create doesn’t crash and still records a span. |
| instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/test_async_messages.py | Adds async regression test covering the same raw-response behavior. |
| instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/cassettes/test_sync_messages_create_with_raw_response.yaml | VCR cassette for the sync raw-response test. |
| instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/cassettes/test_async_messages_create_with_raw_response.yaml | VCR cassette for the async raw-response test. |
| instrumentation/opentelemetry-instrumentation-genai-anthropic/.changelog/381.fixed | Towncrier fragment documenting the fix. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…assette Signed-off-by: Shailendra005 <shivzala06@outlook.com>
|
Thanks for the review. Addressing both comments: 1. 2. Duplicate interaction in the async cassette: Good catch — fixed in 4d28f80. The async test issues a single |
Pull request dashboard statusWaiting on the author · refreshed 2026-08-13 21:02 UTC Two things need attention:
Status above doesn't look right?
|
Signed-off-by: Shailendra005 <shivzala06@outlook.com>
Signed-off-by: Shailendra005 <shivzala06@outlook.com>
| MessageWrapper(message, capture_content).extract_into( | ||
| invocation | ||
| ) | ||
| invocation.stop() |
There was a problem hiding this comment.
Not a blocker for this fix, but worth a follow-up.
With with_raw_response.create(stream=True) (and with_streaming_response) the SDK
returns a LegacyAPIResponse, not a Stream, so it falls past the
isinstance(result, AnthropicStream) check and we call stop() right away. That gives a
success span with no response attributes, ended before the caller has even called parse().
The openai package solves this in _raw_response.py (#278): the raw response is returned
wrapped in a transparent ObjectProxy so metadata still resolves natively, parse() is
deferred and returns the instrumented stream wrapper so the span ends when the stream is
drained, and a close/aclose fallback finalizes the span if the caller never parses.
Would be good to do the same here.
Fixes #380
Problem
messages.create(...)invoked viawith_raw_responsereturns aLegacyAPIResponse, but the instrumentation assumed aMessageand readmessage.modelduring extraction, raisingAttributeErrorinto the caller after a successful (billed) API call. Instrumentation must not break the app.Fix
Mirror the OpenAI sibling package's
ParsableResponsepattern:_RawResponseruntime-checkable Protocol (shape match onparse(), so we don't import the SDK's private response classes) and a_message_for_extraction()helper.messages_createand asyncasync_messages_create, parse the raw response to obtain theMessagefor telemetry, then return the original result untouched so raw-response callers still get their headers andparse().stream=True) and any unexpected payload are left uninstrumented instead of crashing.Messages.parse/AsyncMessages.parsewrappers reuse the create path, so they are covered too.Tests
Added
test_sync_messages_create_with_raw_responseandtest_async_messages_create_with_raw_response(with cassettes). They fail before the fix (AttributeError: 'LegacyAPIResponse' object has no attribute 'model') and pass after. Full package suite: 108 passed, 3 skipped.ruff check/ruff formatclean.Changelog
Added a towncrier
fixedfragment; I'll rename it to<PR_NUMBER>.fixedto match the convention.