fix(provider): scope OpenAI Responses text reconciliation to one output item - #421
Open
kevin9327 wants to merge 1 commit into
Open
fix(provider): scope OpenAI Responses text reconciliation to one output item#421kevin9327 wants to merge 1 commit into
kevin9327 wants to merge 1 commit into
Conversation
…ut item `reconcile_response_text` exists to repair a stream whose `output_text` deltas are incomplete: when `response.output_text.done` or `response.output_item.done` reports a final text longer than what was streamed, the missing suffix is emitted as one more delta. The comparison baseline was wrong. `text` accumulated every delta of the whole response, but `final_text` only ever describes a single output item. A Responses stream routinely carries more than one text-bearing item -- the model writes a sentence, calls a tool, then writes more, and each block is its own `message` item -- so from the second item onward `final_text.starts_with(streamed)` compares against the concatenation of all previous items and is false. The repair silently stops happening. The visible failure is an assistant turn that ends in blank text. When an upstream relay reports text only through terminal events and sends no `output_text.delta` (a common shape for OpenAI-compatible gateways), the first item is emitted correctly and every later one is dropped entirely, because the accumulator it is checked against is no longer empty. The same baseline also breaks the partial-delta repair for any later item. Track the `output_index` the accumulator belongs to and clear it when the provider moves to a different item, so each item reconciles against its own deltas. Events that omit `output_index` keep the current scope, so lenient relays behave exactly as before, and repeated reports for one item (`output_text.done` followed by `output_item.done`) still reconcile against the same baseline and cannot replay text twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner
|
Are you a robot 😂? |
Contributor
Author
|
Human, I promise 😄 A Korean developer in my 30s who basically codes around the clock, so the PRs land at odd hours. Happy to answer anything on any of them. |
Xiashangning
added a commit
to Xiashangning/cursor-byok
that referenced
this pull request
Sep 5, 2026
与 Rust 侧 leookun#421(97feb2a)同源:output_text.done/output_item.done 报告的 终态文本只描述单个输出 item,整条流累积的补全基线从第 2 个文本 item 起 前缀恒不匹配,补齐(及只发终态的中继的整段文本)被静默丢弃。按事件 output_index 切换作用域并清空基线;缺 output_index 沿用当前作用域;同 item 重复终态不重放。仅改插件 SDK 协议层,codex/grok 等共用该实现的 Responses 插件同受其益。
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.
The bug
reconcile_response_textrepairs a stream whoseoutput_textdeltas are incomplete: whenresponse.output_text.doneorresponse.output_item.donereports a final text longer than what was streamed, the missing suffix is emitted as one moreTextDelta.Its baseline is wrong.
textaccumulated every delta of the whole response, butfinal_textonly ever describes one output item:A Responses stream routinely carries more than one text-bearing item — the model writes a sentence, calls a tool, then writes more, and each block is its own
messageitem. From the second item onward,streamedholds the concatenation of all previous items,starts_withis false, and the repair silently stops happening.Failure mode
An assistant turn that ends in blank text.
When an upstream relay reports text only through terminal events and never sends
response.output_text.delta— a common shape for OpenAI-compatible gateways — the reconciler is the only thing emitting text. Item 0 is emitted correctly, and every later item is dropped entirely, because the accumulator it is checked against is no longer empty:streamedbeforefinal_textoutput_item.done(message,output_index0)"""checking the file"TextStart,TextDelta("checking the file")output_item.done(function_call, 1)output_item.done(message,output_index2)"checking the file""the file looks fine"The same wrong baseline also disables the partial-delta repair for any later item, so a truncated delta stream loses its tail instead of being completed.
The fix
Track the
output_indexthe accumulator belongs to, and clear it when the provider moves to a different item, so each item reconciles against its own deltas. This mirrors how tool state is already keyed byoutput_indexin this file.Two properties are preserved deliberately:
output_indexkeep the current scope, so lenient relays behave exactly as before — nothing new is maderequired_u64.response.output_text.donefollowed byresponse.output_item.done) resolve to the same index, so they still share a baseline and cannot replay the same text twice.Verification
cargo +1.95 test --package cursor-server --lib provider::openai_responsesBefore (fix neutered in place, tests kept):
After:
Gates (
make check, Rust half):cargo fmt --all -- --check— cleancargo clippy --workspace --all-targets -- -D warnings— clean on CI's stable; see note belowcargo test --workspace --all-targets— passes, apart from a pre-existing local flake incursor_trace_queue::trace_producers_do_not_wait_for_sqlite_and_artifacts_stay_ordered(a 10 ms-poll timing assertion, 1 failure in 5 runs on this Windows box, unrelated to this change; green in CI onmain).Toolchain note: this machine's
stableis broken, so the gates were run with+1.95. Clippy 1.95 reports onecollapsible_matcherror inserver/src/cursor/compile/model.rs:109on unmodifiedmain— that is a 1.95-only false positive (its own suggestion,"fast" if parse_bool(parameter)? =>, does not compile, since?is not allowed in a match guard) and CI's 1.98.1 does not emit it, somainis green. Clippy here was therefore run with-A clippy::collapsible_match; nothing in this diff is affected either way.