Skip to content

feat(messages): cross-provider content blocks beyond text — image / tool_use / tool_result / document - #726

Merged
jarvis9443 merged 1 commit into
mainfrom
feat/722-messages-content-blocks
Jul 6, 2026
Merged

feat(messages): cross-provider content blocks beyond text — image / tool_use / tool_result / document#726
jarvis9443 merged 1 commit into
mainfrom
feat/722-messages-content-blocks

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

Summary

Closes the "Anthropic Messages 跨厂商 = 仅文本块" row of AISIX-Cloud#873 §⑤: /v1/messages requests routed to a non-Anthropic Model now translate the full content-block set instead of silently dropping everything but text. The user-visible failure this fixes: a Claude-Code-style multi-turn tool loop lost its tool history on cross-provider dispatch (the upstream saw empty user turns), and vision/document inputs vanished — wire.rs even documented it as "land in a follow-up PR". This is that follow-up.

Fixes #722

Block map (LiteLLM LiteLLMAnthropicMessagesAdapter parity)

Anthropic block OpenAI wire result
text {type:"text"} part / plain string (unchanged)
image base64 image_url part with data:<media>;base64,… URL
image url image_url part, URL passthrough
document image_url data-URL part (LiteLLM's _translate_anthropic_image_to_openai mapping)
assistant tool_use tool_calls[] entry — JSON-encoded arguments, 64-char name cap (LiteLLM truncate_tool_name); a pure tool-call turn gets content: null
user tool_result a role:"tool" message keyed by tool_use_id, emitted before the user turn (OpenAI requires tool answers directly after the assistant tool_calls turn); string / single-text-collapse / multi-block (text+image parts) content forms
thinking / redacted_thinking dropped — another vendor's signed reasoning blocks are not replayable on the OpenAI wire; LiteLLM's OpenAI provider transform discards them identically (the top-level thinking key still maps to reasoning_effort)

Mechanics: multimodal parts ride the existing ChatMessage.content_blocks (already forwarded verbatim by the openai bridge — the same array vision uses on /v1/chat/completions), so no bridge changes were needed; content keeps the concatenated text so guardrail scans still see the message. Anthropic-upstream passthrough is untouched (byte-for-byte as before). Response direction (text + tool_use blocks, non-stream + stream) already worked — regression-covered.

Tests

  • 9 new unit tests on the translation (each block type, data-URL shape, tool-message ordering, name truncation, null-content pure-tool turn, thinking-drop leak check) + the pre-existing text-concat test updated to pin the new preserved-image behavior.
  • New DP e2e anthropic-content-blocks-cross-provider-e2e.test.ts (real binary + etcd + mock OpenAI upstream) pinning the translated upstream wire: full tool-loop history (user → assistant(tool_calls) → tool → user) and vision data-URL round-trip.
  • Existing /v1/messages proxy tests (39), anthropic crate tests (90) and the cross-provider matrix / tools e2e all green.

…on cross-provider dispatch

Pre-#722 the /v1/messages inbound parse kept only text blocks when the
target Model was non-Anthropic: image, document, tool_use, tool_result
and thinking blocks were silently dropped, so a Claude-Code-style
multi-turn tool loop lost its tool history (empty user turns upstream)
and vision/document inputs vanished.

The translation now follows LiteLLM's anthropic adapter map
(litellm/llms/anthropic/experimental_pass_through/adapters/
transformation.py):

- image (base64 -> data: URL, url -> passthrough) and document ->
  OpenAI image_url content parts, riding ChatMessage.content_blocks
  (forwarded verbatim by the openai bridge); content keeps the
  concatenated text for guardrail scans
- assistant tool_use -> OpenAI tool_calls[] (JSON-encoded arguments,
  64-char name cap); a pure tool-call turn gets content: null
- user tool_result -> a role:"tool" message keyed by tool_use_id,
  emitted BEFORE the user turn (OpenAI ordering); string /
  single-text / multi-block (text+image) content forms
- thinking / redacted_thinking history blocks drop -- not replayable
  on the OpenAI wire; LiteLLM's OpenAI provider transform discards
  them the same way (top-level thinking still maps to
  reasoning_effort)

Response direction (text + tool_use, non-stream + stream) already
worked and is regression-covered.

Fixes #722
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8dd71d74-1427-4423-982a-4883eb4bc68b

📥 Commits

Reviewing files that changed from the base of the PR and between a09043e and 1cf3e9e.

📒 Files selected for processing (4)
  • crates/aisix-gateway/src/chat.rs
  • crates/aisix-provider-anthropic/src/wire.rs
  • crates/aisix-proxy/src/messages.rs
  • tests/e2e/src/cases/anthropic-content-blocks-cross-provider-e2e.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/722-messages-content-blocks

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

@jarvis9443
jarvis9443 merged commit 3510ae3 into main Jul 6, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the feat/722-messages-content-blocks branch July 6, 2026 09:23
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.

feat(messages): cross-provider content blocks beyond text — image / tool_use / tool_result / document

1 participant