Skip to content

fix(openai): guard against AsyncAPIResponse without .id in async responses wrapper#4078

Merged
dvirski merged 3 commits into
mainfrom
fix(openai)-guard-against-AsyncAPIResponse-without-.id-in-async-responses-wrapper-
May 14, 2026
Merged

fix(openai): guard against AsyncAPIResponse without .id in async responses wrapper#4078
dvirski merged 3 commits into
mainfrom
fix(openai)-guard-against-AsyncAPIResponse-without-.id-in-async-responses-wrapper-

Conversation

@dvirski
Copy link
Copy Markdown
Contributor

@dvirski dvirski commented May 6, 2026

Fixes #4058 — OpenAI Agents crash when AsyncAPIResponse without .id is returned by parse_response

Summary by CodeRabbit

  • Bug Fixes

    • Response parsing now correctly unwraps legacy, modern, and async response wrappers, returning the underlying response objects to avoid wrapped results and improve compatibility.
  • Tests

    • Added unit tests verifying synchronous and asynchronous unwrapping for legacy and modern wrappers, and that plain response-like objects remain unchanged.

Review Change Stack

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 6, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 389002a8-7d49-4945-b28f-0815eb3da0fc

📥 Commits

Reviewing files that changed from the base of the PR and between 1e27c6c and 80231dd.

⛔ Files ignored due to path filters (2)
  • packages/opentelemetry-instrumentation-openai/uv.lock is excluded by !**/*.lock
  • packages/sample-app/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py
  • packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py

📝 Walkthrough

Walkthrough

parse_response now recognizes OpenAI APIResponse (calling .parse()); new async_parse_response awaits .parse() on AsyncAPIResponse; async instrumentation paths updated. Tests added for legacy, APIResponse, AsyncAPIResponse unwraps and passthrough for plain responses.

Changes

Responses parsing and tests

Layer / File(s) Summary
Parsing logic and async callers
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py
Imports APIResponse and AsyncAPIResponse; updates parse_response to call .parse() for LegacyAPIResponse and APIResponse; adds async_parse_response that awaits .parse() for AsyncAPIResponse; async instrumentation now uses await async_parse_response(response).
Unit tests for parsing helpers
packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py
Adds tests: test_parse_response_unwraps_legacy_api_response, test_parse_response_unwraps_api_response, test_async_parse_response_unwraps_async_api_response (uses AsyncMock), and test_parse_response_passes_through_plain_response.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through wrappers, old and new,
Calling .parse() to fetch the view.
Async waits, sync unwraps with care,
Plain responses pass by, left bare.
Tests nibble crumbs to show it's true.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: guarding against AsyncAPIResponse without .id in async responses wrapper, which directly addresses the core issue being fixed.
Linked Issues check ✅ Passed The PR fulfills the requirements from issue #4058 by modifying parse_response to unwrap APIResponse/AsyncAPIResponse via .parse() calls, and implements async_parse_response to handle AsyncAPIResponse in async contexts, preventing AttributeError crashes when accessing .id on parsed responses.
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing the linked issue: expanding parse_response to handle APIResponse, adding async_parse_response for AsyncAPIResponse handling, and updating async wrapper paths to use the new async function.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix(openai)-guard-against-AsyncAPIResponse-without-.id-in-async-responses-wrapper-

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dvirski dvirski requested a review from netanel-tl May 6, 2026 16:32
@doronkopit5
Copy link
Copy Markdown
Member

doronkopit5 commented May 10, 2026

2 suggestions toward a simpler/more robust shape before this lands:

1. The same parse_response(response).id pattern is unguarded in 4 other places

Only the one site you patched has the guard. The others will crash the same way under with_raw_response (sync) or in the cancel paths:

Line Function Guarded?
570 responses_get_or_create_wrapper (sync)
737 async_responses_get_or_create_wrapper ✅ (this PR)
827 responses_cancel_wrapper
859 async_responses_cancel_wrapper
~1050 ResponseStream finalize path

The reporter only hit the async path because the cs-agents-demo is async, but the sync caller using client.responses.with_raw_response.create(...) trips the identical AttributeError on line 572.

2. The cleanest fix is in parse_response itself

parse_response (line 236) currently only unwraps LegacyAPIResponse:

def parse_response(response):
    if isinstance(response, LegacyAPIResponse):
        return response.parse()
    return response

But APIResponse and AsyncAPIResponse (the non-legacy variants returned by with_raw_response()) also expose a .parse() method that yields the typed Response — which has the id everything downstream needs. So the one-line fix is to unwrap them too:

def parse_response(response):
    if hasattr(response, "parse") and callable(getattr(response, "parse", None)):
        return response.parse()
    return response

or, more explicit:

from openai._response import APIResponse, AsyncAPIResponse  # alongside LegacyAPIResponse

def parse_response(response):
    if isinstance(response, (LegacyAPIResponse, APIResponse, AsyncAPIResponse)):
        return response.parse()
    return response

Benefits over the per-site hasattr guard:

  • Fixes all 5 call sites at once.
  • Preserves telemetry — the current early-return drops the span entirely, so users who instrument with with_raw_response silently get no observability for those calls. Unwrapping at the source means they get a proper span with the real id.
  • Matches the function's declared return type (-> Response).

If unwrapping in parse_response feels too broad for this PR, then at minimum please mirror the hasattr guard to the other 4 sites — a guard in only one of five identical places is harder to reason about than no guard at all.

@dvirski
Copy link
Copy Markdown
Contributor Author

dvirski commented May 12, 2026

@doronkopit5 You are absolutely right. made that change plus fixed the below suggested code rabbit change

@dvirski dvirski force-pushed the fix(openai)-guard-against-AsyncAPIResponse-without-.id-in-async-responses-wrapper- branch from f4224e5 to c41e14f Compare May 12, 2026 10:06
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py`:
- Around line 237-239: The current parse_response function calls
AsyncAPIResponse.parse() synchronously which returns a coroutine; split into two
helpers: keep parse_response for sync responses (LegacyAPIResponse, APIResponse
-> call .parse() and return result) and add parse_response_async that is async
and awaits AsyncAPIResponse.parse() (handle Response passthrough similarly),
then update async wrappers async_responses_cancel_wrapper and
async_responses_get_or_create_wrapper to call and await
parse_response_async(response) instead of calling parse_response so the
coroutine is awaited before accessing attributes like .id.

In
`@packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py`:
- Around line 839-855: The test incorrectly mocks AsyncAPIResponse.parse as a
synchronous method; update test_parse_response_unwraps_async_api_response to
model async behavior by making wrapper.parse an async coroutine that returns
inner (or patch it with an async function) and call/await parse_response
accordingly (use asyncio.run or mark the test async) so the
AsyncAPIResponse.parse contract is respected; reference AsyncAPIResponse,
parse_response, and wrapper.parse when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 87b2f966-53c6-4c37-ab1c-60f3a1f95c90

📥 Commits

Reviewing files that changed from the base of the PR and between abbcc11 and f4224e5.

⛔ Files ignored due to path filters (2)
  • packages/opentelemetry-instrumentation-openai/uv.lock is excluded by !**/*.lock
  • packages/sample-app/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/responses_wrappers.py
  • packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py

Comment thread packages/opentelemetry-instrumentation-openai/tests/traces/test_responses.py Outdated
@dvirski dvirski force-pushed the fix(openai)-guard-against-AsyncAPIResponse-without-.id-in-async-responses-wrapper- branch from 1e27c6c to 80231dd Compare May 13, 2026 15:19
@dvirski dvirski merged commit 935dc64 into main May 14, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 Bug Report: OpenAI Agents instrumentation

3 participants