Repository navigation
fix(openai): correlate rotated Responses API item ids - #18548
Merged
Merged
Conversation
addyCooks
pushed a commit
to addyCooks/nanocoder
that referenced
this pull request
Aug 10, 2026
Copilot rotates the opaque reasoning item id mid-stream while output_index stays stable, as confirmed by the upstream trace in vercel/ai#18548. Treating each rotated id as a new reasoning item split one item into several and left the original never concluded. Correlate on output_index instead: a rotated id is rewritten to the id already announced at that index, so summary parts accumulate on a single reasoning item that concludes properly. A reasoning item that was never announced at all still gets a synthetic response.output_item.added. Closes Nano-Collective#719
addyCooks
added a commit
to addyCooks/nanocoder
that referenced
this pull request
Aug 10, 2026
Copilot rotates the opaque reasoning item id mid-stream while output_index stays stable, as confirmed by the upstream trace in vercel/ai#18548. Treating each rotated id as a new reasoning item split one item into several and left the original never concluded. Correlate on output_index instead: a rotated id is rewritten to the id already announced at that index, so summary parts accumulate on a single reasoning item that concludes properly. A reasoning item that was never announced at all still gets a synthetic response.output_item.added. Closes Nano-Collective#719
addyCooks
added a commit
to addyCooks/nanocoder
that referenced
this pull request
Aug 10, 2026
Copilot rotates the opaque reasoning item id mid-stream while output_index stays stable, as confirmed by the upstream trace in vercel/ai#18548. Treating each rotated id as a new reasoning item split one item into several and left the original never concluded. Correlate on output_index instead: a rotated id is rewritten to the id already announced at that index, so summary parts accumulate on a single reasoning item that concludes properly. A reasoning item that was never announced at all still gets a synthetic response.output_item.added. Closes Nano-Collective#719
addyCooks
added a commit
to addyCooks/nanocoder
that referenced
this pull request
Aug 12, 2026
Copilot rotates the opaque reasoning item id mid-stream while output_index stays stable, as confirmed by the upstream trace in vercel/ai#18548. Treating each rotated id as a new reasoning item split one item into several and left the original never concluded. Correlate on output_index instead: a rotated id is rewritten to the id already announced at that index, so summary parts accumulate on a single reasoning item that concludes properly. A reasoning item that was never announced at all still gets a synthetic response.output_item.added. Closes Nano-Collective#719
will-lamerton
pushed a commit
to Nano-Collective/nanocoder
that referenced
this pull request
Aug 13, 2026
#835) * fix: announce rotated Copilot reasoning item ids before the SDK parses them GitHub Copilot's Responses API proxy emits reasoning summary events under an item_id it never announced via response.output_item.added — either a rotated id or no announcement at all. The OpenAI Responses stream parser assumes that state exists and dereferences it, so the stream dies with "Cannot read properties of undefined (reading 'summaryParts')". The repository already carries a defensive patch for this in patches/@[email protected], but pnpm patchedDependencies is a workspace-local install setting: it is not published, and dist is not bundled, so every npm/brew user resolves @ai-sdk/openai straight from the registry. ^3.0.71 currently floats to 3.0.91, which still has the unguarded dereferences, which is why the crash survived the patch. Normalizes the Copilot SSE stream instead, in code that actually ships: when a reasoning event references an unannounced item_id, a synthetic response.output_item.added is emitted ahead of it so the parser always has state. Healthy streams pass through byte for byte. Closes #719 * fix: map rotated Copilot reasoning ids to the item at that output_index Copilot rotates the opaque reasoning item id mid-stream while output_index stays stable, as confirmed by the upstream trace in vercel/ai#18548. Treating each rotated id as a new reasoning item split one item into several and left the original never concluded. Correlate on output_index instead: a rotated id is rewritten to the id already announced at that index, so summary parts accumulate on a single reasoning item that concludes properly. A reasoning item that was never announced at all still gets a synthetic response.output_item.added. Closes #719 * refactor: collapse the duplicated reasoning-id branches into one path The reasoning-item and reasoning-summary branches each carried their own copy of the announce, rewrite and synthesize logic. Resolve the item id once up front, then run a single path for all three outcomes. Behaviour is unchanged: the same 25 unit tests pass, and the streams verified against an unpatched @ai-sdk/openai produce identical results. * fix: normalize Copilot reasoning ids regardless of SSE framing The normalizer parsed SSE more strictly than the parser directly behind it. `EventSourceParserStream` accepts LF, CRLF and lone CR, and ignores any `event:` line in front of the payload; anything it accepts that we rejected was an event we silently failed to fix. - rewrite() now operates on an SSE block rather than assuming the block is a single `data:` line, so an `event:`-framed stream is normalized instead of passed straight through. Replacements are written in place, leaving `event:`/`id:` lines intact. - Blocks split on /\r\n\r\n|\n\n|\r\r/ and are re-emitted with their own separator. Splitting on '\n\n' alone never matched a CRLF stream, so the whole body buffered until close and streaming output stalled. - flush() runs the trailing buffer through rewrite(), so a final event with no trailing separator is normalized too. - A synthetic announcement is only emitted when `output_index` is a number. It is required by the SDK schema, and a chunk that fails validation sets finishReason to 'error' rather than being skipped, so announcing without one traded the crash for a broken response. The event is passed through untouched instead, and nothing is recorded as announced, so a later well-formed event can still be repaired. Adds coverage for `event:` framing, CRLF framing, a CRLF separator split across chunk boundaries, a trailing event with no separator, and a missing `output_index`. All seven fail against the previous implementation.
AryanNandanwar
pushed a commit
to AryanNandanwar/nanocoder
that referenced
this pull request
Aug 13, 2026
Nano-Collective#835) * fix: announce rotated Copilot reasoning item ids before the SDK parses them GitHub Copilot's Responses API proxy emits reasoning summary events under an item_id it never announced via response.output_item.added — either a rotated id or no announcement at all. The OpenAI Responses stream parser assumes that state exists and dereferences it, so the stream dies with "Cannot read properties of undefined (reading 'summaryParts')". The repository already carries a defensive patch for this in patches/@[email protected], but pnpm patchedDependencies is a workspace-local install setting: it is not published, and dist is not bundled, so every npm/brew user resolves @ai-sdk/openai straight from the registry. ^3.0.71 currently floats to 3.0.91, which still has the unguarded dereferences, which is why the crash survived the patch. Normalizes the Copilot SSE stream instead, in code that actually ships: when a reasoning event references an unannounced item_id, a synthetic response.output_item.added is emitted ahead of it so the parser always has state. Healthy streams pass through byte for byte. Closes Nano-Collective#719 * fix: map rotated Copilot reasoning ids to the item at that output_index Copilot rotates the opaque reasoning item id mid-stream while output_index stays stable, as confirmed by the upstream trace in vercel/ai#18548. Treating each rotated id as a new reasoning item split one item into several and left the original never concluded. Correlate on output_index instead: a rotated id is rewritten to the id already announced at that index, so summary parts accumulate on a single reasoning item that concludes properly. A reasoning item that was never announced at all still gets a synthetic response.output_item.added. Closes Nano-Collective#719 * refactor: collapse the duplicated reasoning-id branches into one path The reasoning-item and reasoning-summary branches each carried their own copy of the announce, rewrite and synthesize logic. Resolve the item id once up front, then run a single path for all three outcomes. Behaviour is unchanged: the same 25 unit tests pass, and the streams verified against an unpatched @ai-sdk/openai produce identical results. * fix: normalize Copilot reasoning ids regardless of SSE framing The normalizer parsed SSE more strictly than the parser directly behind it. `EventSourceParserStream` accepts LF, CRLF and lone CR, and ignores any `event:` line in front of the payload; anything it accepts that we rejected was an event we silently failed to fix. - rewrite() now operates on an SSE block rather than assuming the block is a single `data:` line, so an `event:`-framed stream is normalized instead of passed straight through. Replacements are written in place, leaving `event:`/`id:` lines intact. - Blocks split on /\r\n\r\n|\n\n|\r\r/ and are re-emitted with their own separator. Splitting on '\n\n' alone never matched a CRLF stream, so the whole body buffered until close and streaming output stalled. - flush() runs the trailing buffer through rewrite(), so a final event with no trailing separator is normalized too. - A synthetic announcement is only emitted when `output_index` is a number. It is required by the SDK schema, and a chunk that fails validation sets finishReason to 'error' rather than being skipped, so announcing without one traded the crash for a broken response. The event is passed through untouched instead, and nothing is recorded as announced, so a later well-formed event can still be repaired. Adds coverage for `event:` framing, CRLF framing, a CRLF separator split across chunk boundaries, a trailing event with no separator, and a missing `output_index`. All seven fail against the previous implementation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
@ai-sdk/openaikeyed active Responses API reasoning state and emitted stream-part IDs by each event's opaqueitem_id. A real request to GitHub Copilot's/responsesendpoint withgpt-5.3-codexshowed that Copilot sendsresponse.output_item.added, but rotates the opaque ID on every subsequent reasoning and message event.output_indexremains stable.That caused the reported
activeReasoningPart.summaryPartscrash. If the crash was bypassed, the same rotation produced the downstreamtext part ... not founderror because text start, delta, and end used different IDs.The committed fixture is the complete 69-event SSE sequence captured from the real provider on 2026-08-06. Opaque identifiers, encrypted reasoning payloads, obfuscation padding, and the safety identifier were sanitized; event order, event types, output indexes, deltas, usage, and response structure were preserved.
Summary
output_indexwhen parsing text-delta and reasoning-summary events.response.output_item.addedbyoutput_index.@ai-sdk/openai.Contributor Credit
Thanks to @A-S-Manoj for the upstream report and diagnosis; @drmgc and @EntropyParadigm for the Nanocoder reports; @daitmaamar4-png, @OverHash, and @jianminLee for independent downstream reports; and @mussaratshamsher for the related work in #18337.
End-to-End Verification
I first sent an authenticated request to the real GitHub Copilot Responses API using the credential created by
nanocoder copilot login. Replaying that exact raw response through publiccreateOpenAI(...).responses(...)andstreamText(...)failed before this change with:After rebuilding
@ai-sdk/openai, replaying the same unsanitized raw response completed successfully.I also ran the checked-in localhost HTTP/SSE reproduction:
cd examples/ai-functions pnpm tsx src/stream-text/openai/responses-reasoning-id-rotation.tsIt now prints:
This is the complete end-to-end example used for that verification:
Checklist
pnpm changesetin the project root)Future Work
#18337 proposes synthesizing
reasoning-startwhen a provider truly omitsresponse.output_item.added. The real Copilot trace includes that event with a different ID, so this PR keeps that separate behavior out of scope. It can be evaluated independently if a raw provider trace confirms the missing-event variant.Related Issues
Fixes #18270