fix(sdk): keep the completion a Responses stream yielded - #4772
alanhuangyoo wants to merge 3 commits into
Conversation
Both responses() and aresponses() capture the ResponseCompletedEvent as
they drain the stream, then read it back off the wrapper:
completed_response = getattr(ret, "completed_response", completed_response)
getattr's default only applies when the attribute is absent. A wrapper
that exposes `completed_response` and leaves it None overwrites the
event the stream just yielded, and the call fails with
LLMNoResponseError: Responses stream finished without a completed response
even though the stream completed normally.
Take the wrapper's value only when it has one, so it can still supply the
completion for wrappers that set it late, without a stale None clobbering
what iteration already found. Both the sync and async paths had the same
line.
Closes OpenHands#4769
|
👋 This PR needs a couple of things fixed before OpenHands can review it:
Push an update once this is addressed and this check re-runs automatically. This is an automated check - no AI was used to generate this comment. |
|
🚦 CI is currently failing on this PR's latest commit. Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request This is an automated check - no AI was used to generate this comment. |
The HUMAN note is filled in; that check only re-runs on a push.
…keeps-yielded-completion
|
The only red here is #4769 was filed by @yifanxiong272 on 2026-08-30, after the So this needs a maintainer to add Content-wise nothing has changed since the last push: the regression test fails on main with the stale wrapper state overwriting the yielded completion event, and passes here. |
|
@neubig #4976 names this PR as the fix for the yielded-versus-wrapper completion bug, so here is where it stands. I re-checked it against
So the bug is still on The only red check is |
HUMAN:
Ran the streaming path and confirmed the completion the Responses stream yielded is the one kept, on both the sync and async call sites.
AGENT:
Why
responses()andaresponses()capture theResponseCompletedEventwhile draining the stream, then read it back off the wrapper:getattr's default only applies when the attribute is absent. A wrapper that exposescompleted_responseand leaves itNonetherefore overwrites the event iteration just found, and the call fails witheven though the stream completed normally. Both the sync and async paths carried the same line.
Summary
Noneno longer clobbers what iteration already found.completed_response = None.Issue Number
Closes #4769
How to Test
test_responses_streaming_keeps_the_yielded_completionandtest_aresponses_streaming_keeps_the_yielded_completionboth fail onmainwith exactly the reported error:and pass here, with
response.raw_response is completed_response.The test double is the smallest thing that reproduces it:
Video/Screenshots
Not applicable — no GUI surface; the test output above is the evidence.
Design Doc
Not added; the change is one conditional, applied to the two paths that shared the line.