Skip to content

fix(proxy): translate Anthropic tools/tool_choice to OpenAI shape in cross-provider dispatch (streaming + non-streaming) - #254

Merged
nic-6443 merged 2 commits into
mainfrom
fix/issue-236-anthropic-tools-to-openai
May 12, 2026
Merged

fix(proxy): translate Anthropic tools/tool_choice to OpenAI shape in cross-provider dispatch (streaming + non-streaming)#254
nic-6443 merged 2 commits into
mainfrom
fix/issue-236-anthropic-tools-to-openai

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented May 12, 2026

Copy link
Copy Markdown
Contributor

When a client sends an Anthropic Messages API request with tools and tool_choice to a non-Anthropic upstream, the gateway now translates these fields from Anthropic shape to OpenAI shape before dispatching — in both streaming and non-streaming modes.

Previously, tools ({name, description, input_schema}) and tool_choice ({type: "any"}) were passed through verbatim in ChatFormat.extra, which OpenAI-compatible upstreams silently ignored or rejected.

Request-side (both modes)

  • translate_anthropic_tools_to_openai(): converts Anthropic tool defs to OpenAI {type:"function", function:{name, description, parameters}}
  • translate_anthropic_tool_choice_to_openai(): converts {type:"any"}"required", {type:"tool", name}{type:"function", function:{name}}, etc.
  • Called in cross_provider_dispatch() after parse_inbound_request()

Response-side — non-streaming

  • OpenAiResponseMessage now deserializes tool_calls and propagates to ChatMessage.extra
  • chat_response_into_anthropic_json() translates OpenAI tool_calls → Anthropic tool_use content blocks
  • Malformed entries (missing id/name, non-object arguments) are skipped

Response-side — streaming

  • OpenAiStreamDelta now deserializes tool_calls and propagates to ChatDelta
  • AnthropicSseEncoder updated with per-tool-call state machine that emits:
    • content_block_start with type: tool_use (id, name)
    • content_block_delta with type: input_json_delta (argument fragments)
    • content_block_stop for each open block on finish
  • RenderedDelta updated to forward tool_calls in OpenAI streaming path
  • Proper content-block indexing across mixed text + tool_use streams

Tests

  • 12 unit tests for request/response translators
  • 6 unit tests for streaming encoder (single tool, multi-tool, mixed text+tool, force_finish)
  • 1 unit test for OpenAI stream delta tool_calls deserialization
  • 2 E2E tests (non-streaming + streaming tool round-trip)

Closes #236

Copilot AI review requested due to automatic review settings May 12, 2026 01:38
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds translators to convert Anthropic tools/tool_choice into OpenAI-compatible shapes, re-exports them, wires the translators into cross-provider dispatch for Anthropic-origin requests, propagates upstream OpenAI tool_calls into Anthropic tool_use responses, and adds unit and E2E tests validating the flow.

Changes

Tool translation for cross-provider dispatch

Layer / File(s) Summary
Tool translation implementation, response rendering, and re-exports
crates/aisix-provider-anthropic/src/wire.rs, crates/aisix-provider-anthropic/src/lib.rs
Adds translate_anthropic_tools_to_openai (maps Anthropic {name,description?,input_schema?} → OpenAI type:"function"/function.parameters) and translate_anthropic_tool_choice_to_openai (maps Anthropic auto/any/none/tool → OpenAI equivalents), updates chat_response_into_anthropic_json to render upstream tool_calls as Anthropic tool_use content blocks, adds unit tests, and re-exports translators.
Cross-provider dispatch integration
crates/aisix-proxy/src/messages.rs
Imports the translators into cross_provider_dispatch; when routing Anthropic-origin requests to non-Anthropic upstreams, extracts tools/tool_choice from chat.extra, converts them to OpenAI shapes, and reinserts translated fields before dispatching.
End-to-end test setup and validation
tests/e2e/src/cases/anthropic-tools-cross-provider-e2e.test.ts
E2E test: starts a mock OpenAI upstream returning tool_calls, spawns the gateway, provisions an OpenAI provider/model and API key, sends Anthropic-shaped /v1/messages with tools and tool_choice: {type:"any"}, asserts Anthropic tool_use in the gateway response, and verifies the upstream received OpenAI-shaped tools and tool_choice: "required".
OpenAI response propagation
crates/aisix-provider-openai/src/wire.rs
Adds optional tool_calls to OpenAiResponseMessage, propagates non-empty tool_calls into ChatMessage.extra["tool_calls"], and adds a unit test validating the mapping.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Gateway as cross_provider_dispatch
    participant Translators as translate_anthropic_*_to_openai
    participant OpenAIUpstream
    Client->>Gateway: POST /v1/messages (Anthropic tools/tool_choice)
    Gateway->>Gateway: Detect upstream is non-Anthropic
    Gateway->>Translators: translate_anthropic_tools_to_openai
    Translators-->>Gateway: OpenAI-shaped tools
    Gateway->>Translators: translate_anthropic_tool_choice_to_openai
    Translators-->>Gateway: OpenAI tool_choice
    Gateway->>OpenAIUpstream: Dispatch with converted shapes
    OpenAIUpstream-->>Gateway: {tool_calls:[...]}
    Gateway-->>Client: Anthropic-shaped tool_use response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • api7/aisix#20: Related implementation of Anthropic↔OpenAI message/tool translation and tool-call handling.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from issue #236: implements Anthropic→OpenAI translators for tools/tool_choice, invokes them in cross-provider dispatch, ensures response translation from OpenAI tool_calls to Anthropic tool_use blocks, and includes comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #236: adding translation helpers, integrating them into the dispatch flow, handling response-side tool_calls propagation, and adding tests. No unrelated changes are present.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding translation logic for Anthropic tools/tool_choice to OpenAI shape in cross-provider dispatch, covering both streaming and non-streaming paths.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aisix-proxy/src/messages.rs`:
- Around line 410-418: The code removes "tools" and "tool_choice" from
chat.extra before attempting translation, which causes silent loss when
translation returns None; update the logic in the block handling
translate_anthropic_tools_to_openai and
translate_anthropic_tool_choice_to_openai so you don't remove the original entry
until translation succeeds — e.g., peek at chat.extra (using get or
remove_then_reinsert pattern) to obtain the value, call
translate_anthropic_tools_to_openai/translate_anthropic_tool_choice_to_openai,
and only replace (remove + insert) when Some(translated) is returned; if
translation returns None, leave the original entry untouched in chat.extra (or
explicitly reinsert the original) to avoid dropping the field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 80517cbf-9f41-4fe1-971b-5eb8d11fa8dd

📥 Commits

Reviewing files that changed from the base of the PR and between 0490c23 and 849e01b.

📒 Files selected for processing (4)
  • crates/aisix-provider-anthropic/src/lib.rs
  • crates/aisix-provider-anthropic/src/wire.rs
  • crates/aisix-proxy/src/messages.rs
  • tests/e2e/src/cases/anthropic-tools-cross-provider-e2e.test.ts

Comment thread crates/aisix-proxy/src/messages.rs

Copilot AI 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.

Pull request overview

Fixes cross-provider tool calling for the Anthropic Messages endpoint (POST /v1/messages) when the selected upstream is OpenAI-compatible by translating tools and tool_choice from Anthropic shape into the OpenAI tools schema before dispatch.

Changes:

  • Add Anthropic → OpenAI translators for tools and tool_choice in aisix-provider-anthropic.
  • Invoke these translators in the /v1/messages cross-provider dispatch path so non-Anthropic bridges receive OpenAI-shaped fields.
  • Add unit coverage for both translators and an E2E test validating the full Anthropic client → OpenAI upstream round-trip (including response tool-call translation back to Anthropic).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
tests/e2e/src/cases/anthropic-tools-cross-provider-e2e.test.ts New E2E test ensuring Anthropic-shaped tools/tool_choice are translated to OpenAI shape on the upstream request and tool calls translate back on the response.
crates/aisix-proxy/src/messages.rs Applies the new translations in cross_provider_dispatch() for non-Anthropic upstreams.
crates/aisix-provider-anthropic/src/wire.rs Implements Anthropic → OpenAI translation helpers and adds unit tests for them.
crates/aisix-provider-anthropic/src/lib.rs Re-exports the new translation helpers for use by the proxy crate.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@jarvis9443
jarvis9443 force-pushed the fix/issue-236-anthropic-tools-to-openai branch from 849e01b to d6fb334 Compare May 12, 2026 01:42
Copilot AI review requested due to automatic review settings May 12, 2026 01:50
@jarvis9443
jarvis9443 force-pushed the fix/issue-236-anthropic-tools-to-openai branch from d6fb334 to 7c6cb3a Compare May 12, 2026 01:50

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aisix-provider-anthropic/src/wire.rs`:
- Around line 804-821: The current code uses unwrap_or("") for id/name and
accepts any parsed arguments, which can emit invalid "tool_use" entries; change
the logic that processes each tc so it only pushes a "tool_use" JSON when
tc.get("id") yields a non-empty string, tc.get("function").get("name") yields a
non-empty string, and the parsed arguments from
tc.get("function").get("arguments") successfully deserialize to a
serde_json::Value that is an object (e.g., Value::is_object()); otherwise skip
that tc entry entirely. Locate the variables id, name, input and the
content.push call in this block and replace the unconditional unwrap_or behavior
with guarded checks (if let / match) that continue/skip malformed entries before
constructing the tool_use object.
- Around line 314-319: The code currently copies optional fields directly into
the function map (using t.get("description")/t.get("input_schema") and
function.insert), which can forward non-string/non-object values; update the
logic to validate types first: only insert "description" when the retrieved desc
is a string and only insert "parameters" when the retrieved schema is a JSON
object/map/struct (i.e., the expected input_schema shape); if the type check
fails, silently skip the field to preserve lenient behavior and avoid producing
invalid OpenAI tools payloads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 18f3ee19-9835-40ab-bcd4-e7b85f3e1e67

📥 Commits

Reviewing files that changed from the base of the PR and between d6fb334 and 7c6cb3a.

📒 Files selected for processing (4)
  • crates/aisix-provider-anthropic/src/lib.rs
  • crates/aisix-provider-anthropic/src/wire.rs
  • crates/aisix-proxy/src/messages.rs
  • tests/e2e/src/cases/anthropic-tools-cross-provider-e2e.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/aisix-provider-anthropic/src/lib.rs
  • tests/e2e/src/cases/anthropic-tools-cross-provider-e2e.test.ts

Comment thread crates/aisix-provider-anthropic/src/wire.rs
Comment thread crates/aisix-provider-anthropic/src/wire.rs Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread crates/aisix-provider-anthropic/src/wire.rs
Comment thread crates/aisix-provider-anthropic/src/wire.rs
@jarvis9443
jarvis9443 force-pushed the fix/issue-236-anthropic-tools-to-openai branch from 7c6cb3a to 965c1a9 Compare May 12, 2026 01:58

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aisix-provider-openai/src/wire.rs`:
- Around line 189-210: The streaming conversion path currently only copies
role/content and drops message.tool_calls, so streamed tool-use events are lost;
update the stream-handling code that builds ChatMessage (the same logic that
calls role_from_str and finish_reason and constructs ChatMessage with
content_blocks/name/tool_call_id/extra) to mirror this non-stream branch: create
or reuse a serde_json::Map extra, check c.message.tool_calls (or the stream
chunk equivalent) and, if present and non-empty, insert "tool_calls" ->
Value::Array(tool_calls) into extra, then assign that extra to ChatMessage.extra
so streamed responses propagate tool_calls the same way as the non-stream path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8f93a045-7e1a-470b-9ab0-da76edaab897

📥 Commits

Reviewing files that changed from the base of the PR and between 7c6cb3a and 965c1a9.

📒 Files selected for processing (5)
  • crates/aisix-provider-anthropic/src/lib.rs
  • crates/aisix-provider-anthropic/src/wire.rs
  • crates/aisix-provider-openai/src/wire.rs
  • crates/aisix-proxy/src/messages.rs
  • tests/e2e/src/cases/anthropic-tools-cross-provider-e2e.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-provider-anthropic/src/lib.rs
  • tests/e2e/src/cases/anthropic-tools-cross-provider-e2e.test.ts
  • crates/aisix-provider-anthropic/src/wire.rs

Comment thread crates/aisix-provider-openai/src/wire.rs
@jarvis9443
jarvis9443 force-pushed the fix/issue-236-anthropic-tools-to-openai branch from 965c1a9 to d820d24 Compare May 12, 2026 02:06
Copilot AI review requested due to automatic review settings May 12, 2026 02:06

Copilot AI 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.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Comment thread crates/aisix-provider-anthropic/src/wire.rs
Comment thread crates/aisix-provider-anthropic/src/wire.rs Outdated
Comment thread crates/aisix-proxy/src/messages.rs
Comment thread crates/aisix-provider-anthropic/src/wire.rs
…cross-provider dispatch (#236)

When a client sends an Anthropic Messages API request with tools and
tool_choice to a non-Anthropic upstream, the gateway now translates
these fields from Anthropic shape to OpenAI shape before dispatching.

Previously, tools ({name, description, input_schema}) and tool_choice
({type: "any"}) were passed through verbatim in ChatFormat.extra,
which OpenAI-compatible upstreams silently ignored or rejected.

Added:
- translate_anthropic_tools_to_openai(): converts Anthropic tool defs
  to OpenAI {type:"function", function:{name, description, parameters}}
- translate_anthropic_tool_choice_to_openai(): converts Anthropic
  tool_choice ({type:"any"} → "required", {type:"tool",name} →
  {type:"function",function:{name}}, etc.)
- Called in cross_provider_dispatch() after parse_inbound_request()
- Unit tests for both translators
- E2E test for Anthropic client → OpenAI upstream tool round-trip

Closes #236
@jarvis9443
jarvis9443 force-pushed the fix/issue-236-anthropic-tools-to-openai branch from d820d24 to 84c4320 Compare May 12, 2026 02:13
…vider dispatch

Extends #236 fix to handle streaming mode:

- Add tool_calls field to ChatDelta (gateway core) and OpenAiStreamDelta
- Propagate streaming tool_calls through stream_chunk_into_chat_chunk
- Update AnthropicSseEncoder with per-tool-call state machine that
  emits content_block_start (tool_use), content_block_delta
  (input_json_delta), and content_block_stop events
- Update RenderedDelta to forward tool_calls in OpenAI streaming path
- Add unit tests for streaming encoder (single tool, multi-tool,
  mixed text+tool, force_finish with open tool blocks)
- Add E2E test for streaming tool_calls round-trip
Copilot AI review requested due to automatic review settings May 12, 2026 03:01
@jarvis9443 jarvis9443 changed the title fix(proxy): translate Anthropic tools/tool_choice to OpenAI shape in cross-provider dispatch fix(proxy): translate Anthropic tools/tool_choice to OpenAI shape in cross-provider dispatch (streaming + non-streaming) May 12, 2026

Copilot AI 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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@jarvis9443

Copy link
Copy Markdown
Contributor Author

CI status: lint, unit tests, build, coverage all pass. E2E failures are pre-existing flaky guardrail tests (guardrail-keyword-e2e, guardrail-output-e2e) that timeout on waitConfigPropagation — unrelated to this PR. My two E2E tests (anthropic-tools-cross-provider-e2e: non-streaming + streaming) pass.

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.

bug: /v1/messages silently drops tools and tool_choice when upstream is OpenAI-compatible

3 participants