Skip to content

feat(ai-proxy): rewrite Anthropic-to-OpenAI converter with whitelist body construction#13321

Open
nic-6443 wants to merge 14 commits intoapache:masterfrom
nic-6443:feat/ai-proxy-anthropic-converter
Open

feat(ai-proxy): rewrite Anthropic-to-OpenAI converter with whitelist body construction#13321
nic-6443 wants to merge 14 commits intoapache:masterfrom
nic-6443:feat/ai-proxy-anthropic-converter

Conversation

@nic-6443
Copy link
Copy Markdown
Member

@nic-6443 nic-6443 commented Apr 29, 2026

Description

This PR rewrites the Anthropic Messages → OpenAI Chat Completions converter using a whitelist body construction approach instead of the previous core.table.clone() (blacklist) approach.

The core issue: the old code cloned the entire Anthropic request and then tried to delete/rename fields. This meant any unhandled Anthropic-specific fields (metadata, top_k, raw thinking config, output_config, etc.) leaked through to the upstream OpenAI-compatible provider, causing unexpected behavior or errors.

The fix builds the outgoing OpenAI body from scratch (openai_body = {}), explicitly converting only the fields that have OpenAI equivalents. This is the same approach used by other protocol bridges (litellm, higress).

Changes

Bug fixes:

  • Empty tools array no longer produces a tools field in the forwarded request
  • max_tokens correctly maps to max_completion_tokens (never forwards raw max_tokens)
  • output_config/output_format properly converts to response_format
  • Error responses from upstream pass through as Anthropic error format (not generic 500)
  • cache_control fields stripped (OpenAI and compatible providers reject them)
  • stream_options.include_usage=true injected when streaming (required for usage in stream chunks)

New features:

  • Image and document (PDF) content block conversion (base64 → data URL)
  • tool_choice conversion (auto/any/tool/none → OpenAI equivalents)
  • thinking config → reasoning_effort (low/medium/high by budget threshold)
  • tool_result with multimodal content (text + image mixed)
  • reasoning_content/reasoning extraction in non-streaming responses → thinking block
  • Streaming reasoning/thinking blocks with proper content_block events
  • Billing header cch= stripping from system prompts
  • Header conversion (x-api-key → Bearer, remove anthropic-/x-stainless- headers)
  • disable_parallel_tool_useparallel_tool_calls=false
  • Cached tokens handling (deduct from input_tokens, report cache_read/creation)
  • Finish reason normalization (nil/empty/"null" not treated as stop)
  • Stream usage deferred flush with trailing chunk merge
  • Dynamic content_block index counter across thinking/text/tool blocks
  • metadata.user_id → OpenAI user field mapping
  • service_tier passthrough
  • Anthropic built-in tools (computer_, bash_, text_editor_, web_search, code_execution_*) silently skipped
  • Tool name sanitization: truncate >64 chars and replace invalid chars, restore in response
  • ping SSE event pass-through for long-lived streaming connections

Protocol limitations (by design):

  • thinking/redacted_thinking history blocks are dropped from requests: OpenAI Chat Completions has no equivalent semantics for past reasoning content as input

Tests:

  • Expanded from 9 to 48 test cases covering all conversion scenarios
  • Added 4 new fixture files for tool calls, reasoning, errors, and multi-tool responses

…body construction

Replace core.table.clone() (blacklist approach) with whitelist body
construction that builds the OpenAI request from scratch. This eliminates
field leakage of unknown Anthropic fields (metadata, top_k, raw thinking,
output_config, etc.) to upstream providers.

Fixes:
- Bug 1: Empty tools array no longer produces tools field
- Bug 2: max_tokens now correctly maps to max_completion_tokens
- Bug 3: output_config/output_format properly converts to response_format
- Bug 4: Error responses now pass through as Anthropic error format

New features:
- Image and document (PDF) content block conversion
- tool_choice conversion (auto/any/tool/none)
- thinking config → reasoning_effort (low/medium/high thresholds)
- thinking/redacted_thinking history blocks preserved
- tool_result with multimodal content (text + image)
- reasoning_content extraction in non-streaming responses
- Streaming reasoning/thinking blocks with dynamic content indexing
- cache_control preservation on content blocks and tool definitions
- Billing header cch= stripping
- Header conversion (x-api-key → Bearer, remove anthropic-*/x-stainless-*)
- disable_parallel_tool_use → parallel_tool_calls=false
- stop_sequences → stop conversion
- Cached tokens handling (deduct from input_tokens)
- cache_creation_input_tokens passthrough
- Finish reason normalization (nil/empty/"null" not treated as stop)
- Stream usage deferred flush pattern
- Dynamic content_block index counter

Tests expanded from 9 to 40 cases covering all conversion scenarios.
Copilot AI review requested due to automatic review settings April 29, 2026 14:24
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. enhancement New feature or request labels Apr 29, 2026
Address code review finding: validate source.url is a non-empty string
before using it, preventing invalid structures from being sent upstream.
Add test coverage for URL source type including empty/nil edge cases.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Rewrites the Anthropic Messages → OpenAI Chat Completions converter to build outbound OpenAI request bodies via an explicit whitelist, preventing Anthropic-only fields from leaking upstream and expanding conversion coverage (tools, multimodal, reasoning/thinking, SSE behaviors).

Changes:

  • Replaced clone-and-delete request conversion with whitelist body construction and expanded field mappings (tools/tool_choice, response_format, reasoning_effort, stop, etc.).
  • Enhanced response + SSE conversion (reasoning/thinking blocks, cached token accounting, deferred usage flush, finish_reason normalization).
  • Expanded integration/unit-style tests and added new OpenAI fixture responses to cover more scenarios.

Reviewed changes

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

Show a summary per file
File Description
apisix/plugins/ai-protocols/converters/anthropic-messages-to-openai-chat.lua Major converter rewrite (request whitelist, header conversion, response/SSE conversion updates).
apisix/plugins/ai-providers/base.lua Invokes protocol converter header transformation during upstream request build.
t/plugin/ai-proxy-anthropic.t Expands test coverage from basic routing to extensive conversion and streaming behavior verification.
t/fixtures/openai/chat-with-tool-calls.json New fixture for tool_calls response conversion coverage.
t/fixtures/openai/chat-with-reasoning.json New fixture for reasoning_content → thinking conversion and cached token usage handling.
t/fixtures/openai/chat-with-multiple-tool-calls.json New fixture for multi-tool_calls + text conversion ordering.
t/fixtures/openai/chat-error.json New fixture for upstream error passthrough behavior.

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

Comment thread apisix/plugins/ai-protocols/converters/anthropic-messages-to-openai-chat.lua Outdated
Comment thread apisix/plugins/ai-protocols/converters/anthropic-messages-to-openai-chat.lua Outdated
Comment thread apisix/plugins/ai-protocols/converters/anthropic-messages-to-openai-chat.lua Outdated
Comment thread t/plugin/ai-proxy-anthropic.t
- Add local pairs = pairs (luacheck compliance)
- Strip all anthropic-* prefixed headers (not just version/beta)
- Don't flatten single text block with cache_control to string
- Drop thinking/redacted_thinking blocks from content (unsupported by OpenAI)
- Remove variable shadowing in base.lua header conversion hook
- Fix misleading comment about system block handling
- Add test for arbitrary anthropic-* header removal
Remove the explicit elseif branch for thinking/redacted_thinking blocks
and use a comment above the closing 'end' instead.
Whitespace-padded 'null' (e.g. ' null ') would bypass the check
and be treated as a valid finish_reason, prematurely stopping the
stream.
- TEST 2: use lookaheads instead of sequential matching (JSON key
  order is non-deterministic)
- TEST 7,9,10: remove enclosing quotes from text assertions (values
  end with period before closing quote)
- input_tokens = max(0, prompt_tokens - cached) prevents negative
  values when cached_tokens exceeds prompt_tokens
- convert_media_block returns nil when source.data is nil/empty
  instead of generating an empty data URL
JSON object keys like 'text' and 'type' can appear in any order
within the same object. Use independent lookaheads instead of
sequential matching within a single lookahead.
membphis
membphis previously approved these changes Apr 30, 2026
AlinsRan
AlinsRan previously approved these changes Apr 30, 2026
…builtin tools, tool name sanitization, ping event, metadata.user_id, service_tier

- Add stream_options.include_usage=true when stream=true (P0)
- Strip cache_control from system, messages, and tools (P0)
- Update thinking blocks comment to explain protocol limitation (P0)
- Map metadata.user_id to OpenAI user field (P1)
- Skip Anthropic built-in tools with debug log (P1)
- Handle ping SSE event pass-through (P1)
- Tool name truncation/sanitization with mapping restoration (P1)
- Passthrough service_tier field (P1)
@nic-6443 nic-6443 dismissed stale reviews from AlinsRan and membphis via f0d6509 April 30, 2026 07:55
…unused test routes

- Apply same sanitization to tool_choice.function.name as tool definitions
  so upstream receives consistent names
- Disambiguate collisions when multiple tools sanitize to same name by
  appending numeric suffix (_2, _3, ...)
- Remove unused route setup tests (TEST 15, TEST 31) that created admin
  routes never hit by HTTP requests
- Add test coverage for collision and tool_choice consistency
@nic-6443
Copy link
Copy Markdown
Member Author

CI failures are in unrelated tests not touched by this PR:

  • t/admin/plugins-reload.t (TEST 1, 2) — grep_error_log_out mismatch in plugin reload
  • t/discovery/eureka.t (TEST 4) — eureka host fallback pattern mismatch
  • t/xrpc/prometheus.t (TEST 3) — metrics response_body mismatch
  • t/xrpc/redis.t (TEST 5) — arithmetic on userdata in redis pipeline

Our test shard (t/plugin/[a-k]*.t) passed.

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

Labels

enhancement New feature or request size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants