Skip to content

test(providers): verify interleaved Messages reasoning and tool calls - #1345

Draft
weselben wants to merge 10 commits into
ENTERPILOT:mainfrom
weselben:test/interleaved-messages-responses
Draft

weselben wants to merge 10 commits into
ENTERPILOT:mainfrom
weselben:test/interleaved-messages-responses

Conversation

@weselben

@weselben weselben commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Rebase notice

This branch is based on the current head of PR #1343. Rebase it onto updated main after PR #1343 merges, before marking this draft ready for review.

Scope

Follow-up for #1344, which is a sub-issue of #723. Verify whether interleaved Anthropic thinking blocks and tool calls preserve ordering across the full Messages → chat → Responses → chat → Messages path, and fix the protocol defects the verification exposes.

What the verification found

Two defects in the Anthropic Messages stream converter, both on the path where a Responses-only upstream interleaves reasoning with the argument deltas of a tool call.

A tool_use block closed before its arguments were done. Anthropic streams one content block at a time. When reasoning arrived between the argument deltas of a call, the reasoning opened a thinking block, which closed the tool_use block; the remaining argument deltas then landed on a stopped block index. No Anthropic client accepts that.

The content that arrives while a tool_use block streams its arguments is now held back and rendered once the tool call is complete: when the next tool call starts, or when the turn ends. The reasoning is not lost and the tool_use block never closes before its arguments are done.

The thinking block the held reasoning opened was never closed. The flush opens a block of its own. That block was left open: the next tool block opened on top of it and the message ended with it still open, which leaves the client with a dangling index. It is now closed before the next block opens and before the message ends.

Ordering: what is preserved and what is not

The streaming path preserves the upstream order, because it emits the blocks as they arrive: thinking A → tool call A → thinking B → tool call B reaches the client in that order.

The buffered path cannot. The canonical chat shape carries the reasoning in one member beside the answer text and the tool calls, so the buffered response renders thinking first, then text, then tool_use. That is what the Anthropic contract asks for, and the upstream interleaving is not representable in that shape. This is documented in ADR-0011 rather than papered over.

The two directions are separate contracts, and ADR-0011 now states both: a signed thinking block in the request history is the client's artifact and is not replayed to a Responses-only upstream, while the upstream's reasoning summary is rendered as a thinking block in the response with an empty signature.

Tests

internal/server/messages_interleaved_test.go drives the real Messages handler over the real ChatGPT provider against a fake Responses upstream, reusing echotest and providertest:

  • the streaming block order and the block indexes, with the assertion that no delta targets a closed block
  • the buffered response: thinking first, then both tool calls with their IDs and arguments intact
  • the request replay: user, assistant, tool call and tool result reach the upstream, and the reasoning does not
  • the assembled stream compared with the buffered result
  • a tool result stays bound to the call ID it answers
  • every content block that is opened is also closed

internal/anthropicapi/stream_test.go pins the two defects at the converter level, and internal/providers/chatgpt/interleaved_reasoning_test.go covers the composed path.

Each regression test fails on the unfixed code.

Verification

go build ./..., gofmt, go vet, golangci-lint (0 issues), the full ./internal/... suite, -race on the touched packages, and the repository's test conventions all pass.

ChatViaResponses dropped reasoning on the floor: chatResponseFromResponses
ignored reasoning output items and responsesChatStream had no case for
reasoning delta events. An Anthropic client on /v1/messages with thinking
enabled against a Responses-only upstream (chatgpt/Codex) never saw a
thinking block — silent loss of a feature the client asked for.

Non-streaming: reasoning item summaries join onto the message's
reasoning_content extra field. Streaming: reasoning_summary_text and
reasoning_text deltas emit as reasoning_content chunk deltas. Both ride the
existing chat dialect handling (anthropicapi renders them as unsigned
thinking blocks; the streaming codec relays the member).

Refs ENTERPILOT#1085
…nses

CodeRabbit review on weselben-orion#11:

- chatToResponsesRequest asked only for reasoning.effort; the Responses
  API emits summaries only on request, so the converter had nothing to
  render in the real flow. Reasoning gains an optional Summary member,
  set to "auto" whenever the request asks for reasoning (off efforts
  excluded).
- Non-streaming joined reasoning items with no separator; items now
  join with the same \n\n reasoningSummaryText uses between parts,
  empty items contributing nothing.
- Streaming ignored summary_index; an advancing index per item now
  emits the separator delta, an omitted index forwards untouched.

Refs ENTERPILOT#1085
ADR-0011's ladder is a living table (edited in place by ENTERPILOT#973 and ENTERPILOT#963;
the doc itself says 'add a row when an adapter gains a rule'): record
the Responses reasoning-summary -> reasoning_content rendering rule
and the gateway-set reasoning.summary=auto request member.
…ream

Second CodeRabbit pass:

- Streaming emitted no separator between reasoning items, so two items
  merged into one run-on string while the non-streaming converter joins
  them with \n\n. The last text-contributing item is now tracked and an
  item boundary emits the item separator.
- An empty delta at a new summary_index emitted a leading separator and
  advanced the tracked index; empty deltas carry no text and now skip
  all separator and index handling, matching the non-streaming filter
  of empty parts.
- ADR-0011: qualify the reasoning.summary ladder row — the gateway sets
  it only for a non-off effort (cubic)
- streaming: skip empty response.reasoning_text.delta events like the
  summary handler already does (cubic)
- request translation: drop echoed reasoning_content instead of
  rejecting the turn, so multi-turn chat requests keep working after
  the first response carried reasoning (greptile)
- reasoning effort override: preserve a client-sent reasoning.summary
  when replacing the effort (coderabbit)
@mintlify

mintlify Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Preview approval required

This pull request comes from a fork, so Mintlify won't build a preview until someone with write access approves commit df8c6c6.

Approve preview

@coderabbitai

coderabbitai Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size:L 100-499 effective changed lines (test files excluded in mixed PRs). label Oct 30, 2026
A provider that streams reasoning between the argument deltas of a function
call made the Messages stream converter close the tool_use block when the
reasoning opened a thinking block. The remaining argument deltas then landed
on a stopped block index, which no Anthropic client accepts.

The content that arrives while a tool_use block streams its arguments is now
held back and rendered once the tool call is complete: when the next tool
call starts, or when the turn ends. The reasoning is not lost, and the
tool_use block never closes before its arguments are done.

The buffered path is unchanged: the canonical chat shape carries the
reasoning in one member, so the thinking block leads the answer text and the
tool calls, as the Anthropic contract requires.
The reasoning held back while a tool call streamed its arguments opens a
thinking block of its own when it is flushed. That block was never closed: the
next tool block opened on top of it, and the message ended with it still
open. A block that is opened and never closed leaves the client with a
dangling index, because the Anthropic stream allows exactly one open block
at a time.

The block is now closed before the next block opens and before the message
ends. The order of the flush and the close matters in both places: the
flush opens the block of the held content, so the close has to follow it.
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 effective changed lines (test files excluded in mixed PRs).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants