Skip to content

fix(compat): map non-stream messages←responses stop_reason#4

Merged
ax128 merged 1 commit into
mainfrom
fix/messages-compat-nonstream-stop-reason
Jun 13, 2026
Merged

fix(compat): map non-stream messages←responses stop_reason#4
ax128 merged 1 commit into
mainfrom
fix/messages-compat-nonstream-stop-reason

Conversation

@ax128

@ax128 ax128 commented Jun 13, 2026

Copy link
Copy Markdown
Owner

What

Non-streaming Anthropic /v1/messages requests forwarded to a Responses-API upstream returned stop_reason: "end_turn" even when the converted reply contained a tool_use block. Anthropic clients (Claude Code / SDK) key off stop_reason == "tool_use" to execute the tool and continue the turn — reporting end_turn silently breaks the tool loop in this path.

responses_response_to_messages_response (mapper.py) now derives stop_reason from the converted content blocks and the Responses object, mirroring the already-correct streaming path (compat_bridge._messages_stop_reason_from_response):

  • tool_use block present → tool_use
  • status=incomplete + incomplete_details.reason ∈ {max_output_tokens, max_tokens}max_tokens
  • otherwise → end_turn

Why a local helper (not reuse)

mapper cannot import compat_bridgecompat_bridge imports mapper, so the dependency must stay one-directional. A ~12-line local helper keeps that constraint; logic is identical to the streaming mapper.

Scope

  • Only the non-streaming messages←responses stop_reason. No interface/shape change; text-only replies still report end_turn.
  • Streaming path was already correct and is untouched.
  • Out of scope (separately tracked): stop_sequence (upstream doesn't return the matched sequence), streaming input_tokens (unknown at message_start emit time), document/PDF forwarding (needs upstream support check), dead chat_response_to_messages_response.

How to test

pytest aegisgate/tests/test_openai_request_mapping.py

New regression tests (written test-first, watched fail then pass):

  • ..._preserves_function_calls now also asserts stop_reason == "tool_use"
  • ..._text_only_is_end_turn
  • ..._maps_max_tokens

Validation performed

  • pytest test_openai_request_mapping.py → 9 passed
  • Related suites (streaming_router, response_sanitization_routes, sanitize_helpers, forwarding_renderers, forwarding_once_execution, payload_compat) → 159 passed, 3 skipped
  • ruff check clean, mypy mapper.py clean
  • Remote CI (3.10/3.12/3.13): pending — not yet observed

Risks / breaking changes

None expected. Behavior only changes for non-stream messages←responses replies that are tool calls or hit max_output_tokens — those were already incorrect.

🤖 Generated with Claude Code

Non-streaming Anthropic /v1/messages requests forwarded to a Responses
upstream returned stop_reason=end_turn even when the reply contained a
tool_use block, so Anthropic clients treated the turn as finished and
never executed the tool (broken tool loop). Derive stop_reason from the
converted content blocks and the Responses status, mirroring the
streaming path (compat_bridge._messages_stop_reason_from_response):

- tool_use block present        -> "tool_use"
- status=incomplete + max_*tokens -> "max_tokens"
- otherwise                     -> "end_turn"

mapper cannot import compat_bridge (compat_bridge imports mapper), so a
small local helper keeps the dependency one-directional.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ax128 ax128 merged commit f2e4350 into main Jun 13, 2026
3 checks passed
@ax128 ax128 deleted the fix/messages-compat-nonstream-stop-reason branch June 13, 2026 22:47
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