Skip to content

fix(proxy): stop folding search findings into the text on /v1/messages - #154

Merged
orangeboyChen merged 2 commits into
mainfrom
worktree-server-tool-mixed-turn-findings
Sep 17, 2026
Merged

orangeboyChen merged 2 commits into
mainfrom
worktree-server-tool-mixed-turn-findings

Conversation

@orangeboyChen

Copy link
Copy Markdown
Owner

What happened

A turn that mixed a locally executed web search with a client-owned tool call (e.g. Bash) could not continue in the server-tool loop — the client owns the outstanding calls — so buildMixedTurnPayload folded the search findings into the assistant text alongside those calls.

On /v1/messages those findings were already on the wire: the route emits server_tool_use and web_search_tool_result blocks for the same search. The client got every result twice — once as a structured block, once as prose reading as if the model had written it, including this line, which is an instruction to the model rather than something the user asked to see:

Cite the URL of any result you rely on. If the results do not answer the question, say so instead of guessing.

Reproduced against one upstream call, streaming and non-streaming. Client-facing blocks were:

server_tool_use → web_search_tool_result → text(the findings verbatim) → tool_use(Bash)

Replaying that transcript put both copies back on the wire to the upstream model, so the duplication also accumulated across turns.

Why the fold existed

A mixed turn genuinely cannot be continued locally: re-issuing the transcript with only server-tool results would leave the client's calls unanswered, which upstream rejects as an invalid tool-call transcript. Handing the outstanding calls back with the findings attached is what keeps the transcript valid — so the fold is removed only where the results have another channel, not deleted outright.

The change

Routes that render findings structurally now pass findingsAsStructuredBlocks, and buildMixedTurnPayload leaves the text alone.

  • /v1/messages sets it on both the streaming and non-streaming paths.
  • /v1/chat/completions and /v1/responses keep folding — neither has a structured channel for the results, so the fold is their only way to surface them. (/v1/responses emits only action.query via web_search_call, never the results.)

Tests

Two tests, both verified to fail/pass as expected:

  • does not fold findings into the text when a structured block carries them — asserts the result block is present, the prose does not repeat the URL, and the model-only instruction never reaches the user. Fails without the fix.
  • keeps folding findings for routes without a structured channel — guards /v1/chat/completions so the opt-out is not applied more broadly. Passes with and without the fix.

Verification

lint, format:check, typecheck, test:coverage (95.24% lines, above the 90% floor), build, and test:patch-branches (100%) all pass.

Note

Related: #152 fixes adjacent block-ordering for non-streaming turns. Its commit message notes the merged-string problem is unfixable at the renderer because withIntermediateTurns collapses every hop into one content — the same constraint that made this fold the wrong place to carry the results.

A turn that mixed a locally executed web search with a client-owned tool
call could not continue in the loop, so `buildMixedTurnPayload` folded the
search findings into the assistant text alongside the outstanding calls.
On /v1/messages those findings were already on the wire: the route emits
`server_tool_use` and `web_search_tool_result` blocks for the same search.

The client therefore received every result twice — once as a result block
and once as prose reading as if the model had written it, including the
"Cite the URL of any result you rely on" line, which is an instruction to
the model rather than something the user asked to see. Replaying that
transcript put both copies back on the wire to the upstream model.

The fold exists because a mixed turn cannot be continued locally: the
client owns the outstanding calls, and re-issuing the transcript with only
server-tool results would leave them unanswered. It is still needed by
routes with no other way to carry the findings, so it is now opted out of
rather than removed: routes that render findings structurally pass
`findingsAsStructuredBlocks`, and the text is left alone.

/v1/messages sets the flag on both its streaming and non-streaming paths.
/v1/chat/completions and /v1/responses keep folding — neither has a
structured channel for the results, so the fold is their only way to
surface them.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 205db66b2b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/server/proxy/anthropic.ts
@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.41%. Comparing base (7feb4ed) to head (283e3ed).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #154   +/-   ##
=======================================
  Coverage   95.41%   95.41%           
=======================================
  Files          36       36           
  Lines        6349     6351    +2     
  Branches     1813     1816    +3     
=======================================
+ Hits         6058     6060    +2     
  Misses        291      291           
Flag Coverage Δ
unittests 95.41% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The opt-out added in the previous commit was only consulted by
`buildMixedTurnPayload`. A streaming /v1/messages request whose *first*
upstream turn mixed a local search with a client-owned call never reaches
that helper: `createInlineServerToolStream` emits its own text delta from
the search results before handing the outstanding calls back, so the
findings were still sent as prose alongside the `web_search_tool_result`
block on the most common streaming path.

Apply the same flag there. The result event carries the findings once, so
a text copy is the second.
@orangeboyChen
orangeboyChen enabled auto-merge (squash) September 17, 2026 08:56
@orangeboyChen
orangeboyChen merged commit b31ee53 into main Sep 17, 2026
7 checks passed
@orangeboyChen
orangeboyChen deleted the worktree-server-tool-mixed-turn-findings branch September 17, 2026 08:58
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.

1 participant