Passthrough Routes
A passthrough route forwards matching requests to one upstream target without translating the request body. It is useful for provider-native endpoints that AISIX does not model as first-class routes and for forward-proxy traffic delivered with its original Host header.
Each route defines how traffic matches, where it goes, how AISIX authenticates the caller, and whether AISIX injects a provider credential or forwards the caller's credential. AISIX can still apply caller access controls, request limits, guardrails, and telemetry around the relay.
Every passthrough path or host must be claimed by an explicit route. An unclaimed /passthrough/* path follows the ordinary empty-body 404 path. Create and verify the route before moving client traffic.
Passthrough routes do not rewrite model identifiers. If a provider-native request names a model in its body, path, query, or headers, send the identifier expected by that provider.
Prerequisites
Prepare the following:
- One AISIX deployment:
- For AISIX Cloud, an environment with an attached gateway and a write-scoped admin token.
- For the open-source AISIX gateway, a gateway configured to load a declarative resources file.
- An upstream provider credential for an
injectroute. The examples use OpenAI. Aforward_clientroute instead relays the caller's upstream credential. curlandjq.
Understand the Passthrough Flow
AISIX preserves the request body while handling gateway authentication, target construction, intentional header filtering, guardrails, and telemetry:
Each matched request makes one upstream attempt. AISIX does not retry a passthrough request after a transport failure or upstream 5xx response. A route failure also does not place any configured model into cooldown because the route does not resolve a model.
URL rewriting runs before route matching. Both stages below use the rewritten path; optional hosts on a rewrite rule can keep other hosts' paths unchanged. Route matching then runs in two stages:
- A request whose inbound
Hostmatches a route'shostsallowlist is dispatched before the gateway's typed routes. This lets a forward proxy relay an upstream path such as/v1/messageswithout the gateway treating it as its own endpoint. - Path-prefix matching runs after the typed routes, so a path-only route cannot shadow the gateway's
/v1,/mcp, or/a2aendpoints.
When several routes match, host matches beat path-only matches, and a longer matching prefix beats a shorter one.
Configure a Provider-Native Route
The following examples expose OpenAI's native model-list endpoint at /passthrough/openai/v1/models. AISIX injects the configured OpenAI credential and requires a caller key that grants openai-tunnel.
AISIX Cloud
Export the AISIX Cloud connection details and the provider credential:
# AISIX_CP includes /api and has no trailing slash
# The local On-Premises quickstart uses http://localhost:8080/api
export AISIX_CP="YOUR_AISIX_CLOUD_ADMIN_API_URL"
export AISIX_TOKEN="YOUR_ADMIN_TOKEN"
export ENV_ID="YOUR_ENVIRONMENT_ID"
export OPENAI_API_KEY="YOUR_OPENAI_API_KEY"
Create an OpenAI provider key that is available to the environment:
PROVIDER_KEY_ID=$(curl --fail-with-body -sS -X POST \
"$AISIX_CP/provider_keys" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"display_name": "OpenAI passthrough",
"provider": "openai",
"api_key": "'"${OPENAI_API_KEY}"'",
"api_base": "https://api.openai.com/v1",
"allowed_environments": ["'"${ENV_ID}"'"]
}' | jq -er '.provider_key.id')
Create the route with the provider key ID:
ROUTE_RESPONSE=$(curl --fail-with-body -sS -X POST \
"$AISIX_CP/environments/$ENV_ID/passthrough_routes" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "openai-tunnel",
"path_prefix": "/passthrough/openai",
"target_url": "https://api.openai.com/v1",
"credential_mode": "inject",
"provider_key_id": "'"${PROVIDER_KEY_ID}"'"
}')
export ROUTE_ID=$(printf '%s' "$ROUTE_RESPONSE" | jq -er '.passthrough_route.id')
printf '%s' "$ROUTE_RESPONSE" | jq '.warnings // []'
Keep ROUTE_ID for route updates or route-scoped guardrail attachments.
Create a dedicated caller key and grant the route in the same request. The plaintext is returned once:
CALLER_RESPONSE=$(curl --fail-with-body -sS -X POST \
"$AISIX_CP/environments/$ENV_ID/api_keys" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"display_name": "OpenAI passthrough caller",
"allowed_models": [],
"allowed_routes": ["openai-tunnel"]
}')
export AISIX_API_KEY=$(printf '%s' "$CALLER_RESPONSE" | jq -er '.plaintext')
printf '%s' "$CALLER_RESPONSE" | jq '.warnings // []'
The control plane projects the provider key, route, and caller grant to attached gateways. Review any returned compatibility warnings before rollout. Warnings are advisory, so verify traffic through each gateway. In the dashboard, the same workflow is available under Provider keys, an environment's Passthrough Routes, and the caller key's Passthrough route access section.
If you grant an existing caller instead, include every route grant it should keep. The AISIX Cloud Admin API replaces the complete allowed_routes list when that field is patched.
Open-Source AISIX Gateway
Prepare an openai-prod provider key in the complete resources file, then choose a dedicated caller credential:
export PASSTHROUGH_CALLER_KEY="YOUR_CALLER_API_KEY"
Add the route and caller entries to the matching collections. Keep the provider key and other resources unchanged:
passthrough_routes:
- name: openai-tunnel
path_prefix: /passthrough/openai
target_url: https://api.openai.com/v1
provider_key: openai-prod
api_keys:
- display_name: passthrough-caller
key_env: PASSTHROUGH_CALLER_KEY
allowed_models: []
allowed_routes: [openai-tunnel]
The provider_key name is resolved to the provider key's derived ID when AISIX loads the file. An unknown name fails validation. Exact entries in allowed_routes are also checked against the routes defined in the file; wildcard patterns are allowed.
Validate the assembled complete file before loading it:
aisix validate --resources resources.yaml
Because this example introduces PASSTHROUGH_CALLER_KEY, start or recreate the gateway with that variable in its process environment. After it loads, use the same value for the verification request:
export AISIX_API_KEY="$PASSTHROUGH_CALLER_KEY"
Verify the Route
Export the gateway origin without a trailing slash:
# AISIX_PROXY has no trailing slash or endpoint path
# The local quickstarts use http://127.0.0.1:3000
export AISIX_PROXY="YOUR_AISIX_GATEWAY_URL"
Request OpenAI's native model list through AISIX:
curl --fail-with-body -sS \
"$AISIX_PROXY/passthrough/openai/v1/models" \
-H "Authorization: Bearer $AISIX_API_KEY"
The route strips its path_prefix, avoids duplicating the /v1 segment already present in target_url, injects the OpenAI provider credential, and relays the upstream response.
Compare Cloud and Resources-File References
Most route fields use the same names in both management paths. References to provider and caller credentials differ:
| Purpose | AISIX Cloud Admin API | Recommended resources-file field | Explicit ID accepted in a resources file |
|---|---|---|---|
| Injected provider credential | provider_key_id (UUID) | provider_key (display_name) | provider_key_id |
| Anonymous caller principal | anonymous_key_id (UUID) | anonymous_key (display_name) | anonymous_key_id |
Name references in a resources file receive stronger load-time checking and produce candidate names when a reference is unknown. Prefer them over explicit IDs. AISIX Cloud validates that the provider key is visible to the environment and that the anonymous caller key belongs to it.
The route name is fixed after creation in AISIX Cloud. In a resources file, changing the name changes the route identity, so update every caller's allowed_routes entry at the same time.
Configuration Reference
| Field | Behavior |
|---|---|
name | Route identity referenced by callers' allowed_routes patterns and recorded on usage events. |
path_prefix | Gateway path prefix, matched on segment boundaries. A target_url route strips it before joining the remaining path to the target. A preserve_host route keeps the complete path. A path-only route cannot claim /v1, /mcp, /a2a, /admin, /livez, /readyz, or /metrics; a route that also matches hosts may use those upstream-owned paths. |
hosts | Case-insensitive inbound Host allowlist; a port is ignored. A leading *. wildcard matches one additional label and must retain at least two literal labels. |
target_url | Explicit upstream base URL. Configure exactly one target shape: target_url, or preserve_host: true. |
preserve_host | Derive https://<matched-host> as the target. It is accepted only with hosts, which bounds the derived destination. |
auth_mode | gateway_key (default), header_key, or anonymous. |
auth_header_name | Lowercase side-channel header containing the gateway credential in header_key mode. AISIX strips it before forwarding, unless forward_client_headers names it in full; a glob such as x-* does not reach it. authorization, proxy-authorization, cookie, set-cookie, and x-api-key are rejected. |
source_cidrs | Client source allowlist. It is required and nonempty for anonymous; for other modes it is optional hardening. |
credential_mode | inject (default) or forward_client. |
forward_client_headers | Inbound client headers relayed upstream even when this route would otherwise strip them, as exact names or single-* globs, matched case-insensitively. Empty by default. A patch replaces the stored list; send null to clear it. In the dashboard it is Forward client headers under Advanced, one entry per line. See Upstream Request Headers. |
identity_header | Optional lowercase device-injected identity header. AISIX records a bounded value as client_identity and strips the header before forwarding, unless forward_client_headers names it in full; a glob such as x-* does not reach it. Configure it only behind a trusted device that removes and replaces any client-supplied value. authorization, proxy-authorization, cookie, set-cookie, and x-api-key are rejected. |
timeout_ms | Bounds upstream response headers and non-SSE body reads. It does not bound a healthy SSE relay. |
enabled | A disabled route matches nothing. Default: true. |
At least one match dimension, path_prefix or hosts, is required. When both are present, the request must satisfy both.
Gateway Authentication Modes
gateway_keyreads the standard gateway credential fromAuthorization: Bearerorx-api-key.header_keyreads the gateway credential fromauth_header_name, leavingAuthorizationavailable for the caller's upstream credential. This is the standard forward-proxy pairing withforward_client.anonymousaccepts no gateway credential. The request runs as the configured caller-key principal and must originate withinsource_cidrs.
Every resolved principal still needs the route name granted by allowed_routes; * grants every route. A valid key without a matching grant receives 403.
In AISIX Cloud, budgets that already apply to the resolved caller are checked before dispatch. Passthrough usage currently carries no model ID, is recorded with zero cost, and does not add spend to those budgets. The open-source AISIX gateway has no local budget resource.
Upstream Credential Modes
injectstrips inbound credential headers and injects the configured provider key. AISIX usesx-api-keyplusanthropic-versionfor Anthropic andAuthorization: Bearerfor other providers. The provider key's configured header-strip and TLS settings also apply.forward_clientforwards the caller's upstream credential when the gateway credential arrived throughheader_key, or when the route is anonymous. Withgateway_key, AISIX removesAuthorizationandx-api-keybecause either may contain the gateway credential.
A route relays the caller's other headers by default and strips a small set: hop-by-hop and transport headers, host, content-length, the x-aisix-* namespace, proxy-authorization, and the caller's W3C trace headers. AISIX consumes valid W3C context for its own OTLP trace and sends a gateway request ID upstream.
forward_client_headers overrides that strip, and is the one way to put a header back that the route would otherwise remove. Naming authorization under auth_mode: gateway_key therefore relays the caller's own credential — the very header the gateway consumed to authenticate them — in place of the injected one, which is what lets an internal service that authorizes on the end user keep doing so. host, content-length, hop-by-hop headers, and x-aisix-* are stripped whatever the patterns say, and a credential or trace-context header must be named exactly rather than matched by a wildcard. The route's own auth_header_name and identity_header are read the same way: AISIX consumes both, so a glob does not reach either and naming one in full is what forwards it. For every other name the route strips, a glob is enough — including a provider key strip_headers entry, though three of that list's four defaults (authorization, cookie, x-api-key) are credential slots that still need their own entry, leaving set-cookie the only default a glob restores.
There is no credential fallback: an inject route without a resolvable provider key fails closed, and a forward_client route cannot carry a provider key reference.
Envelope Detection and Usage
AISIX detects the request shape for extraction only; detection does not change the relayed body. If several recognized carrier fields appear, detection uses this order:
messagesfor OpenAI-compatible chat or Anthropic Messages traffic.inputas a string or an array for the OpenAI Responses shape.promptfor legacy completions or fill-in-the-middle traffic.- Opaque handling for every other body, including JSON-RPC, REST, non-JSON, and empty bodies.
Detection selects the text presented to guardrails and the token fields recorded on usage events. If a detected shape yields no text, AISIX scans the complete body instead. Request and response bodies are still relayed without schema translation.
Usage Recorded per Request Shape
Two opaque request shapes are still read for usage: a rerank body and an Alibaba Cloud DashScope native body. They stay opaque for guardrails, audit capture, and streamed responses, and AISIX reads only their model field and the token counts of a buffered JSON response:
| Request body | Buffered response | SSE response |
|---|---|---|
| Chat, Responses, or completions, detected as above | Every supported token field of the response usage object. | Usage reported by the stream's own events. |
Rerank: a top-level query and a documents array. Covers Cohere, Jina, and Alibaba Cloud Model Studio's compatible rerank API. | Input tokens from usage.prompt_tokens, usage.input_tokens, or usage.total_tokens, or from Cohere's meta.billed_units.input_tokens. This is the same reading as the rerank endpoint. | Opaque stream rules. |
DashScope native: a top-level model string and an input object. Covers native rerank, text and multimodal embedding, and native generation. | See DashScope Native Token Counts. | Opaque stream rules. |
| Any other body | Nothing. Opaque buffered responses do not receive speculative token extraction. | Opaque stream rules. |
Under the opaque stream rules, a stream reports usage through a top-level usage object on any frame, read with the standard field names, or through a flat token report on a frame the server labels event: usage or event: token_usage.
Every passthrough usage event has the operation passthrough, whatever the body shape. See Tell Request Kinds Apart. For a recognized shape, requested_model is the request body's model value. That value is the upstream model ID the caller sent, not an AISIX model alias, because the route does not resolve a model. Every other body records no model. model_id is always empty, so passthrough usage records zero cost: pricing is keyed on the AISIX model.
DashScope Native Token Counts
For a DashScope native body, AISIX reads the response's top-level usage object as follows:
- Output tokens come from
output_tokens, orcompletion_tokens. - When
total_tokensis present, input tokens aretotal_tokensminus the output tokens. - Without
total_tokens, input tokens areinput_tokens, orprompt_tokens, plus the top-levelimage_tokens. A nestedinput_tokens_details.image_tokensvalue is already part ofinput_tokensand is not added. - Cached-input and reasoning token counts are read from the same object with the standard field names, such as
prompt_tokens_details.cached_tokensandoutput_tokens_details.reasoning_tokens.
This counts image tokens once, whether a service reports them inside input_tokens or next to it:
| Service | Upstream usage | Recorded input | Recorded output |
|---|---|---|---|
Native multimodal rerank, qwen3-vl-rerank | input_tokens: 146, image_tokens: 64, total_tokens: 210 | 210 | 0 |
Multimodal embedding, multimodal-embedding-v1 | input_tokens: 4, image_tokens: 128 | 132 | 0 |
| Native multimodal generation | input_tokens: 79, image_tokens: 66, output_tokens: 14, total_tokens: 93 | 79 | 14 |
Example: Meter Alibaba Cloud Native Rerank
The following AISIX Cloud example relays DashScope's native API for the mainland China region through /passthrough/dashscope. Create an alibaba-cn provider key for the DashScope credential. For the international region, use the alibaba provider and https://dashscope-intl.aliyuncs.com/api/v1. The provider key's api_base does not affect this route, because the route's target_url sets the upstream:
export DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY"
DASHSCOPE_KEY_ID=$(curl --fail-with-body -sS -X POST \
"$AISIX_CP/provider_keys" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"display_name": "DashScope native",
"provider": "alibaba-cn",
"api_key": "'"${DASHSCOPE_API_KEY}"'",
"allowed_environments": ["'"${ENV_ID}"'"]
}' | jq -er '.provider_key.id')
curl --fail-with-body -sS -X POST \
"$AISIX_CP/environments/$ENV_ID/passthrough_routes" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "dashscope-native",
"path_prefix": "/passthrough/dashscope",
"target_url": "https://dashscope.aliyuncs.com/api/v1",
"credential_mode": "inject",
"provider_key_id": "'"${DASHSCOPE_KEY_ID}"'"
}' | jq '.warnings // []'
In a resources file, declare the same route with provider_key set to the display_name of an alibaba-cn provider key. Qwen shows a provider key entry:
passthrough_routes:
- name: dashscope-native
path_prefix: /passthrough/dashscope
target_url: https://dashscope.aliyuncs.com/api/v1
provider_key: dashscope-cn
Grant dashscope-native in the caller key's allowed_routes, as in Configure a Provider-Native Route. Then send a native rerank request with the upstream model ID:
curl --fail-with-body -sS \
"$AISIX_PROXY/passthrough/dashscope/services/rerank/text-rerank/text-rerank" \
-H "Authorization: Bearer $AISIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gte-rerank-v2",
"input": {
"query": "What does a passthrough route record?",
"documents": [
"A passthrough route relays provider-native requests.",
"Usage events carry token counts and the route name."
]
},
"parameters": {"top_n": 2}
}'
AISIX strips the prefix, relays the body to https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank with the injected credential, and returns DashScope's response unchanged. The body's top-level model and input object make it a DashScope native body, so the usage event records gte-rerank-v2 as requested_model and the response's usage.total_tokens as input tokens.
Rate Limits
Caller API-key, team, and member request limits apply before dispatch. On an inject route, a top-level JSON model that resolves to a configured AISIX model of the same provider also reserves that model's request limits. forward_client routes do not perform this model lookup.
Request-count dimensions (rps, rpm, rph, and rpd) are enforced. AISIX also rejects a request when an applicable tpm or tpd counter is already exhausted, but passthrough token usage does not increment those counters. Use recorded usage for telemetry, not passthrough token-quota enforcement.
Concurrency is checked before upstream dispatch. For SSE, the reservation is released when AISIX returns the streaming response, not when the stream ends.
Passthrough routes have no rate-limit field or policy scope. To apply different request limits to different routes, grant them to separate caller keys and configure limits for those caller identities.
Guardrails and Streaming
In AISIX Cloud, a guardrail can be attached to one passthrough route by selecting the Passthrough routes scope. The attachment uses the route UUID. Environment, caller-key, and team guardrails can also apply.
The open-source resources file declares attachments in its guardrail_attachments collection, so a file-defined guardrail reaches passthrough traffic only if an attachment scopes it there — scope_type: env, or scope_type: passthrough_route naming the route.
Input guardrails run before upstream dispatch. A block returns 422 without contacting the upstream. Buffered responses are checked before delivery. Once an SSE response has started, a block ends the stream with an SSE content_filter error frame; it cannot change the HTTP status to 422.
SSE responses relay incrementally unless a hold-back guardrail buffers frames for inspection. AISIX does not rewrite provider-native bodies to apply redaction. For the built-in pii guardrail, a mask-action-only match is forwarded without masking; use a block action when the matched content must not reach the upstream. Guardrail kinds such as Presidio and Lakera instead block a maskable result when passthrough has no write-back channel.
Audit Capture
For successfully relayed traffic, an observability exporter with content_mode: full receives the request body as a string, subject to the exporter's content cap. Buffered responses record extracted text when the response matches a supported extraction shape and otherwise record the body as text. Streamed responses record accumulated extracted text; opaque data payloads are retained as text. Captured content is exporter-only and is never sent through the AISIX Cloud telemetry path.
Usage events carry the route name, caller, recorded token counts, and client_identity. External exporters can expose these values. The current AISIX Cloud Request Logs UI shows caller and token metadata, but it does not display passthrough_route_name or client_identity.
Errors
| Status or signal | Meaning |
|---|---|
401 | Missing or invalid gateway credential for the route's authentication mode. |
403 | The resolved caller key does not grant the route, or the client source is outside source_cidrs. |
404 | An unclaimed /passthrough/* path reaches the ordinary empty-body not-found path. |
422 | A guardrail blocked the request before dispatch or blocked a buffered response before delivery. |
SSE content_filter frame | A guardrail blocked content after a stream had started. |
429 | A gateway request limit or budget check rejected the request, or the upstream returned a relayed 429. |
| Other upstream status | AISIX relays the upstream status and body after filtering response headers. |
The unmatched 404 has an empty body. Other AISIX-generated failures use the gateway error envelope; upstream error statuses and bodies are relayed after response-header filtering.
Next Steps
- Receive TLS-terminated IDE traffic with host-matched routes: Forward Proxy for IDE AI Traffic.
- Configure guardrail behavior: Guardrail Behavior.
- Export request and response content: Observability Exporters.