Skip to content

fix(proxy): apply RequestOverrides on /v1/messages Anthropic passthrough (#335 #337) - #338

Merged
moonming merged 1 commit into
mainfrom
fix/messages-override-apply
May 18, 2026
Merged

fix(proxy): apply RequestOverrides on /v1/messages Anthropic passthrough (#335 #337)#338
moonming merged 1 commit into
mainfrom
fix/messages-override-apply

Conversation

@moonming

@moonming moonming commented May 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

The Anthropic-upstream branch of /v1/messages dispatch bypasses the Hub and the OpenAI bridge — it builds its HTTP request directly via reqwest. That meant the PK's request.* override block (param renames, constraints, default body fields, default headers) silently no-opped on this path: cp-api validated and persisted the override, but the DP never applied it on outbound.

Surfaced by AISIX-Cloud's source-blind E2E matrix (PR #352 D3.3): the param_constraints clamp scenario failed pre-audit; the default_headers injection scenario failed on PR #352's first CI run. Both led to filing #335 and #337 with full repros.

Fix

crates/aisix-proxy/src/messages.rs::dispatch (Anthropic-passthrough branch only):

  • Apply aisix_provider_openai::overrides::{apply_param_renames, apply_param_constraints, apply_default_body_fields} to the outbound JSON body before .json(body).
  • Build an explicit axum::http::HeaderMap so apply_default_headers can inject operator-supplied headers via the shared apply pipeline.

Bridge-owned headers (x-api-key, anthropic-version, content-type, x-aisix-request-id) are inserted FIRST. apply_default_headers skips keys already present + the RESERVED_DEFAULT_HEADERS blacklist (which DOES include x-api-key), so operator headers can never clobber auth.

aisix-provider-openai moves from [dev-dependencies] to [dependencies] in aisix-proxy/Cargo.toml. Same architectural shape as aisix-provider-anthropic.

What's NOT in this PR (filed as follow-ups)

Audit (CLAUDE.md §7) verified that the cross-provider dispatch branch inherits the apply pipeline ONLY for OpenAiBridge and AzureOpenAiBridge. Three more provider bridges have the same wire-in gap:

  • ai-gateway#333 aisix-provider-openai's streaming reasoning_content — actually NOT a gap; closed after verification (false-positive audit on D3.1)
  • ai-gateway#339 wire RequestOverrides on aisix-provider-vertex
  • ai-gateway#340 wire RequestOverrides on aisix-provider-bedrock

This PR is scoped to the Anthropic-passthrough fix. The cross-provider branch through OpenAI-family bridges (OpenAI / DeepSeek / Google) already works via the Bridge's own prepare_outbound_body / build_request_headers.

Tests

Five new wiremock-driven unit tests on the Anthropic-passthrough path, each strict-matching on body or header. All five fail before the fix, pass after. 19 of 19 messages.rs tests green; clippy clean.

Test Pins
anthropic_passthrough_applies_param_renames max_tokensmax_tokens_to_sample on outbound
anthropic_passthrough_clamps_temperature_via_param_constraints caller 0.9 → upstream 0.5
anthropic_passthrough_fills_default_body_fields_when_caller_omits missing top_p filled with 0.9
anthropic_passthrough_injects_default_headers x-tenant-id: acme-prod-42 reaches upstream
anthropic_passthrough_default_headers_cannot_overwrite_x_api_key defense-in-depth — PK secret survives even if default_headers names x-api-key

Audit findings addressed

Severity Finding Resolution
MEDIUM-1 Cross-provider only inherits override pipeline for OpenAI-family bridges Filed as ai-gateway#339 (Vertex) + ai-gateway#340 (Bedrock); explicitly scoped this PR to Anthropic-passthrough
MEDIUM-2 BridgeError::Config error wording could surface in operator logs Acceptable as-is (mirrors bridge.rs:347); follow-up to revisit log level for unreachable-by-construction errors deferred
LOW-1 RESERVED_DEFAULT_HEADERS guard not isolated from contains_key guard in test Acceptable; unit test at overrides.rs:548 covers RESERVED list in isolation
LOW-2 param_renames ordering with model field rewrite Same as OpenAI bridge ordering; comment update documenting precedence is appropriate but optional

References (per CLAUDE.md §7)

Test plan

  • CI green (cargo test + clippy + fmt)
  • Once merged, AISIX-Cloud held-back matrix scenarios flip on (D3.3 param_constraints clamp + default_headers injection)

Closes #335
Closes #337
Refs #339 #340

…ugh (#335 #337)

The Anthropic-upstream branch of `/v1/messages` dispatch bypasses the
Hub and the OpenAI bridge — it builds its HTTP request directly via
reqwest. That meant the PK's `request.*` override block (param renames,
constraints, default body fields, default headers) silently no-opped
on this path: cp-api validated and persisted the override, but the DP
never applied it on outbound. The OpenAI `/v1/chat/completions` path
applied them via `prepare_outbound_body` / `build_request_headers`
inside the Bridge, so the contract was shipped half — half customers
saw their overrides take effect, half didn't, depending on which
endpoint they used.

This commit reaches into the same `aisix_provider_openai::overrides::*`
primitives the OpenAI bridge uses and applies them in
`messages.rs::dispatch` for the Anthropic-passthrough branch:

- `apply_param_renames` (#302 §5)
- `apply_param_constraints`
- `apply_default_body_fields`
- `apply_default_headers` (against an explicit `axum::http::HeaderMap`
  built up before the reqwest `.headers()` call)

Bridge-owned headers (`x-api-key`, `anthropic-version`, `content-type`,
`x-aisix-request-id`) are inserted FIRST. `apply_default_headers`
skips keys already present + the `RESERVED_DEFAULT_HEADERS` blacklist
(`x-api-key` IS in that list), so operator-supplied `default_headers`
can never overwrite the PK's secret or the Anthropic version header
even if cp-api validation slips and accepts a malicious entry.

The cross-provider branch already routes through Hub → Bridge so it
inherits the apply pipeline from `prepare_outbound_body`; no change
needed there.

`aisix-provider-openai` moves from `[dev-dependencies]` to
`[dependencies]` in `aisix-proxy/Cargo.toml`. Same architectural
shape as `aisix-provider-anthropic`, which already lived there for
the wire helpers — both crates are deliberate reach-ins from
messages.rs because the /v1/messages path needs primitives that the
Bridge trait alone doesn't surface.

Tests

Five new `wiremock`-driven unit tests on the Anthropic-passthrough
path, each strict-matching on body or header:

- `anthropic_passthrough_applies_param_renames` — `max_tokens` →
  `max_tokens_to_sample` on outbound
- `anthropic_passthrough_clamps_temperature_via_param_constraints` —
  caller 0.9 → upstream 0.5
- `anthropic_passthrough_fills_default_body_fields_when_caller_omits`
  — missing `top_p` filled with `0.9`
- `anthropic_passthrough_injects_default_headers` — custom
  `x-tenant-id: acme-prod-42` reaches upstream
- `anthropic_passthrough_default_headers_cannot_overwrite_x_api_key`
  — defense-in-depth: PK secret survives even if `default_headers`
  block names `x-api-key`

All five fail before the fix, pass after. 19 of 19 messages.rs tests
green; clippy clean.

References
- LiteLLM applies the same primitives on both endpoints via its
  `litellm_pre_call_hooks` chain (every endpoint dispatch shares the
  same chain): https://github.com/BerriAI/litellm/blob/main/litellm/proxy/proxy_server.py
- Portkey applies header / body transforms in its `transformers`
  layer ahead of the upstream call regardless of provider family:
  https://github.com/Portkey-AI/gateway/blob/main/src/handlers/

Closes #335
Closes #337
Copilot AI review requested due to automatic review settings May 18, 2026 15:02
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: b8cf8b83-ce03-4932-a9b0-b5300e20be28

📥 Commits

Reviewing files that changed from the base of the PR and between e6125b6 and 2e1b01d.

📒 Files selected for processing (2)
  • crates/aisix-proxy/Cargo.toml
  • crates/aisix-proxy/src/messages.rs

📝 Walkthrough

Walkthrough

The PR adds OpenAI provider support to the proxy crate and implements a request override pipeline for Anthropic passthrough. Body parameters are transformed via renames and constraints, default fields are injected, and headers are explicitly constructed with operator-supplied overrides protected from reserved keys.

Changes

Anthropic Passthrough Request Override Pipeline

Layer / File(s) Summary
OpenAI Provider Dependency Setup
crates/aisix-proxy/Cargo.toml
aisix-provider-openai added to proxy dependencies alongside aisix-provider-anthropic with expanded inline documentation explaining why /v1/messages handler requires these provider primitives.
Request Body Override Application
crates/aisix-proxy/src/messages.rs
Anthropic passthrough applies provider-key request.* override config to outbound body: parameter renames, parameter constraints, and default body field injection are guarded by override config presence.
Header Construction with Operator Injection
crates/aisix-proxy/src/messages.rs
Upstream headers explicitly constructed as HeaderMap with gateway-required headers inserted first, then operator-supplied request.default_headers applied through shared override helper with x-api-key protected by reserved-header blacklist.
Request Override Test Suite
crates/aisix-proxy/src/messages.rs
New wiremock-backed tests verify override features: param_renames, param_constraints clamping, default_body_fields population, default_headers injection, and reserved-header protection.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

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

This PR wires ProviderKey request.* overrides into the direct Anthropic /v1/messages passthrough path, which previously bypassed the shared Hub/OpenAI bridge override pipeline.

Changes:

  • Applies param renames, param constraints, default body fields, and default headers before dispatching Anthropic passthrough requests.
  • Builds outbound headers explicitly so default headers can be merged without clobbering bridge-owned auth/version headers.
  • Moves aisix-provider-openai to runtime dependencies and adds wiremock coverage for the new passthrough override behavior.

Reviewed changes

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

File Description
crates/aisix-proxy/src/messages.rs Applies request override primitives to Anthropic passthrough body/headers and adds related tests.
crates/aisix-proxy/Cargo.toml Promotes aisix-provider-openai to a runtime dependency for shared override helpers.
Comments suppressed due to low confidence (1)

crates/aisix-proxy/src/messages.rs:1015

  • This test relies on body_partial_json, so it is not strict on the whole body; it would still pass if the old max_tokens field were left alongside max_tokens_to_sample. Please reword the comment or add an exact/captured-body assertion if that absence is intended to be pinned.
        // primitive now fires on outbound. mock-llm matcher is
        // strict on body — the rename MUST be applied or wiremock
        // returns 404.

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

Comment on lines +969 to +972
// upstream that strict-matches the EXPECTED outbound body shape /
// header after each override is applied — if the override silently
// no-ops the matcher rejects the request and wiremock 404s, which
// surfaces as a non-200 status here.
Comment on lines +230 to +233
// in `crates/aisix-provider-openai/src/bridge.rs:317-323`. The
// OpenAI bridge applies the same primitives via the Hub dispatch,
// but the Anthropic-passthrough path bypasses the Hub and builds
// the request directly here — without this block the override
@moonming
moonming merged commit 3596c0a into main May 18, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the fix/messages-override-apply branch June 25, 2026 06:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants