Repository navigation
Conversation
…me output_index
Live incident (2026-08-08): an OpenClaw agent sent a short preamble line
("Kör nu, på riktigt — apply_patch på vibe-scriptet:") followed by an
apply_patch tool call in the same turn. The client only spoke the preamble
and never executed the patch, even though OmniRoute's own recorded
responseBody had a complete, valid tool_calls entry.
Root cause: emitToolCall/closeToolCall computed a tool call's output_index
as `reasoningIndex + 1 + tcIdx`, assuming reasoningIndex + 1 was free for
the first tool call (tcIdx=0). But a text message emitted in the same turn
ALSO claims reasoningIndex + 1 (or index 0 with no reasoning) — so a
turn with reasoning + text content + a tool call collided the tool call's
added/delta/done events onto the same output_index as the just-closed
message. A client that tracks response items by output_index (as expected
for the Responses API) sees the tool call events land on an index it
already marked complete and can silently drop them.
Fix: track whether a message item was actually emitted at that index
(state.msgItemAdded) and, if so, tool calls start one slot after it.
Extracted a shared toolCallOutputIndexBase() helper so emitToolCall and
closeToolCall can no longer compute this independently and drift apart.
Confirmed via the live call log artifact (id 1786223153235-770a1c):
response.output_item.done for the text message and response.output_item.added
for the tool call both carried output_index=1 in the raw SSE stream, 1.84s
apart, exactly matching the reported symptom.
Collaborator
|
Thanks for the PR. Please address the mandatory items (tests and/or merge blockers) in this branch, then rerun checks before /merge-prs. |
2 similar comments
Collaborator
|
Thanks for the PR. Please address the mandatory items (tests and/or merge blockers) in this branch, then rerun checks before /merge-prs. |
Collaborator
|
Thanks for the PR. Please address the mandatory items (tests and/or merge blockers) in this branch, then rerun checks before /merge-prs. |
Collaborator
|
Obrigado pelo PR. Mantive a revisão de
|
Contributor
Author
|
Code landed in #9843 |
hartmark
added a commit
to hartmark/OmniRoute
that referenced
this pull request
Aug 12, 2026
…tput_index emitToolCallAdded/closeToolCall used the provider's raw Chat Completions tool_calls[].index directly as the Responses API output_index. That index is scoped only to the tool_calls array and legitimately restarts at 0 for the first tool call, but a reasoning item (and/or a text message) emitted earlier in the same turn may already have claimed output_index 0 (and 1). A client that tracks response items by output_index (as the Responses API spec expects) then sees the tool call's added/delta/done events land on an index it already marked complete, and silently drops the tool call -- producing an "incomplete turn" that never dispatches it. Reported live: OpenClaw on combo default -> opencode-zen/big-pickle sent a reasoning block immediately followed by a function call in the same turn (no text message in between); the function call's output_index collided with the reasoning item's. A similar collision (tool call after a *text message*) was already fixed in open-sse/translator/response/openai-responses.ts (cheaper-inference#9822/cheaper-inference#9843), but that file is only used by the zed-hosted executor -- the general /v1/responses path (wired via responsesHandler.ts) goes through this file, which never received the equivalent fix. Fix: compute the tool call's output_index once (offset past any reasoning/ message item already emitted this turn) and cache it in state.funcOutputIndex, so every added/delta/done event for that call -- including ones emitted later from the finish_reason handler or flush() -- shares exactly the same output_index. TDD: new regression tests in tests/unit/responses-transformer-tool-call-reasoning-collision.test.ts reproduce the exact live scenario (reasoning immediately followed by a tool call, and multiple tool calls after reasoning) -- confirmed failing against the pre-fix code, passing after. Full transformer test suite (responses-transformer*.test.ts, responses-replay-fixes.test.ts, responses-api-truncation.test.ts, responses-request-translation.test.ts) -- 24/24 passing, no regressions.⚠️ base-red inherited: cheaper-inference#9985
diegosouzapw
pushed a commit
that referenced
this pull request
Aug 13, 2026
…tput_index (#10025) emitToolCallAdded/closeToolCall used the provider's raw Chat Completions tool_calls[].index directly as the Responses API output_index. That index is scoped only to the tool_calls array and legitimately restarts at 0 for the first tool call, but a reasoning item (and/or a text message) emitted earlier in the same turn may already have claimed output_index 0 (and 1). A client that tracks response items by output_index (as the Responses API spec expects) then sees the tool call's added/delta/done events land on an index it already marked complete, and silently drops the tool call -- producing an "incomplete turn" that never dispatches it. Reported live: OpenClaw on combo default -> opencode-zen/big-pickle sent a reasoning block immediately followed by a function call in the same turn (no text message in between); the function call's output_index collided with the reasoning item's. A similar collision (tool call after a *text message*) was already fixed in open-sse/translator/response/openai-responses.ts (#9822/#9843), but that file is only used by the zed-hosted executor -- the general /v1/responses path (wired via responsesHandler.ts) goes through this file, which never received the equivalent fix. Fix: compute the tool call's output_index once (offset past any reasoning/ message item already emitted this turn) and cache it in state.funcOutputIndex, so every added/delta/done event for that call -- including ones emitted later from the finish_reason handler or flush() -- shares exactly the same output_index. TDD: new regression tests in tests/unit/responses-transformer-tool-call-reasoning-collision.test.ts reproduce the exact live scenario (reasoning immediately followed by a tool call, and multiple tool calls after reasoning) -- confirmed failing against the pre-fix code, passing after. Full transformer test suite (responses-transformer*.test.ts, responses-replay-fixes.test.ts, responses-api-truncation.test.ts, responses-request-translation.test.ts) -- 24/24 passing, no regressions.⚠️ base-red inherited: #9985
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…tput_index (cheaper-inference#10025) emitToolCallAdded/closeToolCall used the provider's raw Chat Completions tool_calls[].index directly as the Responses API output_index. That index is scoped only to the tool_calls array and legitimately restarts at 0 for the first tool call, but a reasoning item (and/or a text message) emitted earlier in the same turn may already have claimed output_index 0 (and 1). A client that tracks response items by output_index (as the Responses API spec expects) then sees the tool call's added/delta/done events land on an index it already marked complete, and silently drops the tool call -- producing an "incomplete turn" that never dispatches it. Reported live: OpenClaw on combo default -> opencode-zen/big-pickle sent a reasoning block immediately followed by a function call in the same turn (no text message in between); the function call's output_index collided with the reasoning item's. A similar collision (tool call after a *text message*) was already fixed in open-sse/translator/response/openai-responses.ts (cheaper-inference#9822/cheaper-inference#9843), but that file is only used by the zed-hosted executor -- the general /v1/responses path (wired via responsesHandler.ts) goes through this file, which never received the equivalent fix. Fix: compute the tool call's output_index once (offset past any reasoning/ message item already emitted this turn) and cache it in state.funcOutputIndex, so every added/delta/done event for that call -- including ones emitted later from the finish_reason handler or flush() -- shares exactly the same output_index. TDD: new regression tests in tests/unit/responses-transformer-tool-call-reasoning-collision.test.ts reproduce the exact live scenario (reasoning immediately followed by a tool call, and multiple tool calls after reasoning) -- confirmed failing against the pre-fix code, passing after. Full transformer test suite (responses-transformer*.test.ts, responses-replay-fixes.test.ts, responses-api-truncation.test.ts, responses-request-translation.test.ts) -- 24/24 passing, no regressions.⚠️ base-red inherited: cheaper-inference#9985
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.
Summary
A text message and a following tool call in the same turn could collide on the
same
output_indexin the Responses API translation (open-sse/translator/response/openai-responses.ts).emitToolCall/closeToolCallcomputed a tool call'soutput_indexasreasoningIndex + 1 + tcIdx, assumingreasoningIndex + 1was free for thefirst tool call (
tcIdx=0). But a text message emitted in the same turn ALSOclaims
reasoningIndex + 1(or index0with no reasoning) — so a turn withreasoning + text content + a tool call collided the tool call's
added/delta/doneevents onto the sameoutput_indexas the just-closedmessage.
Live incident (2026-08-08): an agentic coding client sent a short preamble
line ("Kör nu, på riktigt — apply_patch på vibe-scriptet:") followed by an
apply_patchtool call in the same turn. The client only spoke the preambleand never executed the patch — even though OmniRoute's own recorded
responseBodyhad a complete, validtool_callsentry withfinish_reason: "tool_calls". A client that tracks response items byoutput_index(asexpected for the Responses API) saw the tool call's events land on an index
it had already marked complete (the just-closed text message) and silently
dropped them.
Confirmed via the live call log artifact's raw SSE stream:
response.output_item.donefor the text message and
response.output_item.addedfor the tool call bothcarried
output_index=1, 1.84s apart.Related but distinct from #9183/#9741: #9183 covered a different
output-index-collision class (already closed, superseded by #9741's narrower
reasoning-cache
messageIndexsync). This is a separate collision between amessage item and a tool-call item, first found live on 2026-08-08 after
#9183/#9741 had already landed — not something either of those covers.
Fix
Extracted a shared
toolCallOutputIndexBase()helper that tracks whether amessage item was actually emitted at the reasoning/base index
(
state.msgItemAdded) and, if so, starts tool calls one slot after it.emitToolCallandcloseToolCallpreviously computed this independently —that duplication is exactly how they could drift out of sync in the first
place, so the fix removes the duplication rather than just patching both
call sites in place.
Test plan
tests/unit/translator-resp-openai-responses.test.tsreproduces the exact live scenario (reasoning + text preamble +
apply_patchtool call) — confirmed failing (
message and tool call must not collide on the same output_index) against the pre-fix code on a cleanrelease/v3.8.50checkout, passing afternpm run typecheck:core— cleannpm run lint— cleannpm run check:file-size— clean (own growth:openai-responses.ts1204->1224 rebaselined); the one other violation the gate reports
(
src/sse/handlers/chat.ts) is confirmed pre-existing/untouched by thisbranch (
git diff upstream/release/v3.8.50...HEAD -- src/sse/handlers/chat.tsis empty)
translator-resp-openai-responses.test.ts) —19/19 passing, no regressions