Skip to content

Normalize mixed Anthropic tool history in Chat requests - #40

Merged
maiphucgiang merged 3 commits into
mainfrom
fix/chat-tool-block-normalization
Sep 23, 2026
Merged

maiphucgiang merged 3 commits into
mainfrom
fix/chat-tool-block-normalization

Conversation

@maiphucgiang

@maiphucgiang maiphucgiang commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Changes

  • Normalize Anthropic tool_use / tool_result history embedded in Chat requests after system/developer placement and before upstream routing, fixing HTTP 400 / code 11101 for unsupported content blocks. Keep error parameter paths tied to the caller's original message indices.
  • Preserve call IDs, JSON arguments, text/image order and tool-error markers. Map ordinary thinking to reasoning_content while retaining native Chat fields and leaving the caller's input unchanged.
  • Reject ambiguous fields, invalid or unmatched tool IDs, unsupported mixed blocks and encrypted redacted_thinking locally with a precise parameter path. Keep image limits before conversion and enforce the final wire-size limit afterward.
  • Add focused regressions and synchronize concise English/Chinese client documentation.

Verification

  • All 61 backend regression scripts pass: 1198 unittest cases reported, including 2 existing platform-conditional skips. The new suite has 22 tests covering four backend profiles, both streaming modes, desensitization, images, reasoning, argument integrity, system placement, ordering and local rejection.
  • Restarted the existing local tmux deployment and verified five real hy3 requests through intl-cli: mixed tool history, native Chat history, late system/developer instructions and streaming thinking/tool history all returned HTTP 200 and the expected tool-result marker; the stream completed with [DONE] and no error events.
  • Invalid tool IDs, duplicate results and out-of-order user content (streaming and non-streaming) returned local HTTP 400. SQLite request audits confirm no upstream attempts for rejected requests and one upstream attempt for each successful request. All nine live cases pass; final health is OK with zero in-flight requests.
  • Staged scope matches the tested source; diff checks pass.

Scope and rollback

  • This is input-history compatibility, not a full Anthropic request-body adapter for the Chat endpoint. Anthropic thinking signatures have no Chat equivalent and are not forwarded; redacted_thinking requires the Messages protocol. User messages split into tool results accept only role and content, with all results before ordinary text/images rather than silently reordering them.
  • Revert the PR commits and restart the gateway to roll back; no persistent-state migration is required.

Summary by Sourcery

Normalize mixed Anthropic conversation history before routing Chat requests while preserving supported content and rejecting unsupported input locally.

New Features:

  • Support mixed Anthropic tool-use, tool-result, thinking, and image history in Chat requests while preserving tool metadata and content ordering.

Bug Fixes:

  • Prevent unsupported mixed Anthropic content from reaching upstream Chat handlers and causing request failures.
  • Reject invalid, ambiguous, unmatched, or unsafe mixed history locally with structured parameter errors.

Enhancements:

  • Convert ordinary thinking to Chat reasoning content without exposing signatures or altering native Chat fields.
  • Preserve input immutability and apply image-count and final request-size validation at the appropriate stages.

Documentation:

  • Document mixed Anthropic history compatibility and validation behavior in English and Chinese client guides.

Tests:

  • Add regression coverage for normalization, tool integrity, reasoning, images, streaming modes, routing profiles, size limits, and local rejection.

@sourcery-ai

sourcery-ai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

This PR adds pre-routing normalization for recognizable Anthropic tool, result, image, and thinking blocks embedded in Chat history, with fail-closed validation and preserved native behavior, then verifies the conversion across request policies, streaming, backend profiles, and documented client behavior.

Sequence diagram for Chat history normalization before routing

sequenceDiagram
    participant Client
    participant ChatEndpoint
    participant normalize_chat_messages
    participant Upstream

    Client->>ChatEndpoint: POST /v1/chat/completions
    ChatEndpoint->>normalize_chat_messages: normalize_chat_messages(messages)
    normalize_chat_messages->>normalize_chat_messages: Convert tool_use to tool_calls
    normalize_chat_messages->>normalize_chat_messages: Convert tool_result to role=tool
    normalize_chat_messages->>normalize_chat_messages: Map thinking to reasoning_content
    alt Valid history
        normalize_chat_messages-->>ChatEndpoint: Normalized messages
        ChatEndpoint->>Upstream: Route Chat request
        Upstream-->>Client: Chat response or stream
    else Invalid or unsupported history
        normalize_chat_messages-->>ChatEndpoint: HTTPException 400
        ChatEndpoint-->>Client: Local HTTP 400
    end
Loading

Flow diagram for Anthropic block conversion in Chat messages

flowchart LR
    A[Anthropic block array in Chat message] --> B{Block type}
    B -->|tool_use| C[Chat tool_calls]
    B -->|tool_result| D[role=tool with tool_call_id]
    B -->|thinking| E[reasoning_content]
    B -->|image or text| F[Chat content parts]
    C --> G[Preserve ID and JSON arguments]
    D --> H[Preserve result images and error marker]
    E --> I[Do not expose thinking as visible text]
    F --> J[Keep content order]
    G --> K[Validated Chat history]
    H --> K
    I --> K
    J --> K
    B -->|redacted_thinking or unsupported block| L[Local HTTP 400]
    K --> M[Upstream routing]
Loading

File-Level Changes

Change Details Files
Added a dedicated normalizer for Anthropic content blocks embedded in Chat message history.
  • Converts tool_use blocks into Chat function tool calls while preserving IDs and JSON-serialized inputs.
  • Splits tool_result blocks into ordered Chat tool messages, including result images and error markers.
  • Maps ordinary thinking blocks to reasoning_content and rejects signatures, redacted thinking, invalid roles, conflicts, and unmatched results.
  • Leaves native Chat messages and caller-owned input unchanged.
app/adapters/chat_input.py
Integrated history normalization into Chat request preparation with validation ordering preserved.
  • Normalizes messages before developer-role conversion and upstream routing.
  • Retains image-limit checks before Anthropic image conversion and applies wire-size checks after expanded tool arguments.
  • Prevents invalid histories from credential selection or upstream requests.
converter.py
Added regression coverage for normalization, rejection, routing, and request-policy interactions.
  • Tests IDs, arguments, ordering, images, errors, reasoning, native-field preservation, immutability, and idempotence.
  • Covers malformed, conflicting, unsupported, orphaned, duplicate, and out-of-order content.
  • Verifies streaming modes, desensitization, backend profiles, image limits, wire-size limits, and local rejection.
tests/test_chat_input.py
Documented mixed Anthropic history support and its Chat protocol limitations.
  • Added concise English and Chinese guidance on supported conversions and local rejection behavior.
  • Clarified that Anthropic thinking signatures and redacted thinking are not forwarded through Chat.
docs/clients.md
docs/clients.zh-CN.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-23T13:59:54.535221Z a71d4a0 New commits
🔒 Security Review ✅ Completed 2026-09-23T13:29:38.863237Z 946d985 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai sourcery-ai 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.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="app/adapters/chat_input.py" line_range="106-116" />
<code_context>
+        else:
+            parts.append(_content_part(block, location))
+    if results:
+        # Keep results adjacent to the assistant calls, before any follow-up user text/images.
+        if parts and result_ids != pending.keys():
+            raise _invalid(param, "All pending tool results must precede follow-up user content")
+        return [*results, *([{**message, "content": parts}] if parts else [])]
+    out = {**message, "content": parts if parts else None}
+    if calls:
</code_context>
<issue_to_address>
**issue (bug_risk):** When a user content array contains ordinary text or images before and after a `tool_result`, `_convert_message` emits every tool result first and then emits all ordinary parts in a separate user message. This moves content that preceded the tool result after it, so the normalized history does not preserve the original text/image order.

**Triggers:** When an Anthropic user message mixes `tool_result` blocks with ordinary text or image blocks.

**Suggested fix:** Preserve the original sequence when splitting the message, or reject layouts whose ordinary content cannot be represented without reordering.

```suggestion
        elif kind == "tool_result":
            if role != "user":
                raise _invalid(location, "tool_result requires a user message")
            if message.keys() - {"role", "content"}:
                raise _invalid(param, "Message-level attributes cannot be assigned safely when splitting tool_result content")
            if parts:
                raise _invalid(param, "Ordinary content must follow tool results")
            result = _tool_result(block, location)
            identifier = result["tool_call_id"]
            if pending.get(identifier) != 1 or identifier in result_ids:
                raise _invalid(location + ".tool_use_id", "tool_result must match one preceding, unanswered tool call")
            result_ids.add(identifier)
            results.append(result)
```
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: app/adapters/chat_input.py:116


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread app/adapters/chat_input.py

@sourcery-ai sourcery-ai 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.

Sourcery assessment

Approved.

@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: dfcb3d1b4b

ℹ️ 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 converter.py Outdated
if not isinstance(messages, list) or not messages or any(not isinstance(message, dict) for message in messages):
raise HTTPException(status_code=400, detail={"error": {
"message": "messages must be a non-empty array of objects", "type": "invalid_request_error"}})
messages = normalize_chat_messages(messages)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Hoist late system messages before matching tool results

When a Chat request has no leading system message and places its sole system or developer message between an assistant tool_use and its matching user tool_result, normalization runs before the existing system-message hoisting. The normalizer clears its pending-call state on that intervening message and returns HTTP 400, even though the subsequent logic would have moved the system message to the front and produced valid adjacent assistant/tool messages. Hoist/normalize system messages first, or preserve pending calls across a message that will be hoisted.

Useful? React with 👍 / 👎.

@maiphucgiang
maiphucgiang merged commit d0d26a4 into main Sep 23, 2026
8 of 9 checks passed
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