fix(proxy): apply RequestOverrides on /v1/messages Anthropic passthrough (#335 #337) - #338
Conversation
…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
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe 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. ChangesAnthropic Passthrough Request Override Pipeline
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Note 🎁 Summarized by CodeRabbit FreeYour 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 |
There was a problem hiding this comment.
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-openaito 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 oldmax_tokensfield were left alongsidemax_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.
| // 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. |
| // 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 |
Summary
The Anthropic-upstream branch of
/v1/messagesdispatch bypasses the Hub and the OpenAI bridge — it builds its HTTP request directly via reqwest. That meant the PK'srequest.*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_constraintsclamp scenario failed pre-audit; thedefault_headersinjection 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):aisix_provider_openai::overrides::{apply_param_renames, apply_param_constraints, apply_default_body_fields}to the outbound JSON body before.json(body).axum::http::HeaderMapsoapply_default_headerscan 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_headersskips keys already present + theRESERVED_DEFAULT_HEADERSblacklist (which DOES includex-api-key), so operator headers can never clobber auth.aisix-provider-openaimoves from[dev-dependencies]to[dependencies]inaisix-proxy/Cargo.toml. Same architectural shape asaisix-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
OpenAiBridgeandAzureOpenAiBridge. Three more provider bridges have the same wire-in gap:aisix-provider-openai's streamingreasoning_content— actually NOT a gap; closed after verification (false-positive audit on D3.1)aisix-provider-vertexaisix-provider-bedrockThis 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.anthropic_passthrough_applies_param_renamesmax_tokens→max_tokens_to_sampleon outboundanthropic_passthrough_clamps_temperature_via_param_constraintsanthropic_passthrough_fills_default_body_fields_when_caller_omitstop_pfilled with0.9anthropic_passthrough_injects_default_headersx-tenant-id: acme-prod-42reaches upstreamanthropic_passthrough_default_headers_cannot_overwrite_x_api_keydefault_headersnamesx-api-keyAudit findings addressed
BridgeError::Configerror wording could surface in operator logsbridge.rs:347); follow-up to revisit log level for unreachable-by-construction errors deferredRESERVED_DEFAULT_HEADERSguard not isolated fromcontains_keyguard in testoverrides.rs:548covers RESERVED list in isolationparam_renamesordering withmodelfield rewriteReferences (per CLAUDE.md §7)
litellm_pre_call_hookschain.transformerslayer ahead of the upstream call regardless of provider family.request.*override block server-side — this is the gateway's customer-facing primitive (refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302 §5 RuntimeConfig).Test plan
param_constraintsclamp +default_headersinjection)Closes #335
Closes #337
Refs #339 #340