Multi-Provider Routing for LLM Proxies¶
Overview¶
Multi-provider routing lets one large language model (LLM) proxy expose a single OpenAI-compatible endpoint while routing each request to a selected LLM provider. Applications continue to use the same endpoint and OpenAI-compatible request and response format, even when the upstream provider changes.
For example, an application can send all requests to /openai-multi/chat/completions and select OpenAI or Anthropic with the x-provider request header.
This is useful when you want to:
- Switch providers without changing application code or endpoint URLs
- Compare provider responses using the same OpenAI-compatible request
- Keep vendor credentials in the gateway instead of distributing them to applications
- Apply proxy-level authentication, rate limits, and guardrails consistently across providers
- Introduce provider fallback or selection logic through a routing policy
How It Works¶
A multi-provider LLM proxy has:
- One primary provider in
spec.provider - One or more selectable providers in
spec.additionalProviders - An LLM Header Router policy (
llm-header-router) that selects a provider from a request header - An inline transformer for each additional provider that does not use the OpenAI wire format
The request flow is:
OpenAI-compatible client request
|
| x-provider: anthropic
v
Multi-provider LLM proxy
|
| LLM Header Router selects anthropic-provider
| openai-to-anthropic transforms the request
| provider loopback authentication is added
v
Anthropic LLM provider
|
| vendor authentication is added
v
Anthropic API
|
| response is transformed to OpenAI format
v
OpenAI-compatible client response
The router writes the selected provider name to request metadata. The gateway conditionally applies only the authentication and transformer associated with that provider. When the selection header is missing, empty, or does not match a configured mapping, the router uses defaultProvider when configured; otherwise, the proxy's primary provider is used.
Before You Begin¶
Make sure that:
- The AI Gateway is running and the management API is available at
http://localhost:9090/api/management/v1. - You are using an AI Gateway version that supports multi-provider routing and includes the required router and transformer policies.
- You have credentials for each external LLM provider.
curlandjqare installed if you want to follow the command-line examples.
This guide configures OpenAI as the primary provider and Anthropic as an additional provider. The same configuration model can be extended to Azure OpenAI, Mistral, Gemini, AWS Bedrock, and other providers supported by your AI Gateway version.
Understand the Authentication Layers¶
Multi-provider routing can involve three different kinds of credentials:
| Credential | Used by | Purpose |
|---|---|---|
| Vendor credential | LLM provider to external vendor | Authenticates the gateway to OpenAI, Anthropic, or another external service |
| Provider loopback key | LLM proxy to LLM provider | Authenticates the proxy when it routes internally to a protected provider |
| Proxy consumer key | Application to LLM proxy | Authenticates the application invoking the public proxy endpoint |
Do not use a vendor API key as a loopback or consumer key. Do not commit any of these credentials to source control.
Step 1: Deploy the LLM Providers¶
Each provider must exist before a proxy can reference it.
Deploy the OpenAI provider¶
Replace <openai-api-key> with an OpenAI API key.
curl -X POST http://localhost:9090/api/management/v1/llm-providers \
-u admin:admin \
-H "Content-Type: application/yaml" \
--data-binary @- <<'EOF'
apiVersion: gateway.api-platform.wso2.com/v1
kind: LlmProvider
metadata:
name: openai-provider
spec:
displayName: OpenAI Provider
version: v1.0
template: openai
context: /providers/openai
upstream:
url: https://api.openai.com/v1
auth:
type: api-key
header: Authorization
value: Bearer <openai-api-key>
accessControl:
mode: deny_all
exceptions:
- path: /chat/completions
methods: [POST]
operationPolicies:
- name: api-key-auth
version: v1
paths:
- path: /chat/completions
methods: [POST]
params:
key: X-API-Key
in: header
EOF
Deploy the Anthropic provider¶
Replace <anthropic-api-key> with an Anthropic API key.
curl -X POST http://localhost:9090/api/management/v1/llm-providers \
-u admin:admin \
-H "Content-Type: application/yaml" \
--data-binary @- <<'EOF'
apiVersion: gateway.api-platform.wso2.com/v1
kind: LlmProvider
metadata:
name: anthropic-provider
spec:
displayName: Anthropic Provider
version: v1.0
template: anthropic
context: /providers/anthropic
upstream:
url: https://api.anthropic.com
auth:
type: api-key
header: x-api-key
value: <anthropic-api-key>
accessControl:
mode: deny_all
exceptions:
- path: /v1/messages
methods: [POST]
operationPolicies:
- name: api-key-auth
version: v1
paths:
- path: /v1/messages
methods: [POST]
params:
key: X-API-Key
in: header
EOF
The vendor credentials under spec.upstream.auth are added only when the provider calls its external service.
Step 2: Create Provider Loopback Keys¶
Because both providers in this example use the api-key-auth policy, create an API key for each provider. The proxy uses these keys when routing to the providers through the gateway's internal loopback route.
OPENAI_LOOPBACK_KEY=$(curl -s -X POST \
http://localhost:9090/api/management/v1/llm-providers/openai-provider/api-keys \
-u admin:admin \
-H "Content-Type: application/json" \
-d '{"name":"openai-proxy-loopback"}' \
| jq -r '.apiKey.apiKey')
ANTHROPIC_LOOPBACK_KEY=$(curl -s -X POST \
http://localhost:9090/api/management/v1/llm-providers/anthropic-provider/api-keys \
-u admin:admin \
-H "Content-Type: application/json" \
-d '{"name":"anthropic-proxy-loopback"}' \
| jq -r '.apiKey.apiKey')
Verify that both commands returned a value:
test -n "$OPENAI_LOOPBACK_KEY" && test "$OPENAI_LOOPBACK_KEY" != "null"
test -n "$ANTHROPIC_LOOPBACK_KEY" && test "$ANTHROPIC_LOOPBACK_KEY" != "null"
API key values are returned only when they are created or regenerated. Store them securely.
Step 3: Deploy the Multi-Provider LLM Proxy¶
The following proxy exposes one /chat/completions operation. OpenAI is the primary and default provider. Anthropic is an additional selectable provider with an inline request and response transformer.
curl -X POST http://localhost:9090/api/management/v1/llm-proxies \
-u admin:admin \
-H "Content-Type: application/yaml" \
--data-binary @- <<EOF
apiVersion: gateway.api-platform.wso2.com/v1
kind: LlmProxy
metadata:
name: openai-multi
spec:
displayName: OpenAI Multi-Provider Proxy
version: v1.0
context: /openai-multi
provider:
id: openai-provider
auth:
type: api-key
header: X-API-Key
value: ${OPENAI_LOOPBACK_KEY}
additionalProviders:
- id: anthropic-provider
auth:
type: api-key
header: X-API-Key
value: ${ANTHROPIC_LOOPBACK_KEY}
transformer:
type: openai-to-anthropic
version: v1
params:
model: claude-sonnet-4-5-20250929
operationPolicies:
- name: api-key-auth
version: v1
paths:
- path: /chat/completions
methods: [POST]
params:
key: X-API-Key
in: header
- name: llm-header-router
version: v1
paths:
- path: /chat/completions
methods: [POST]
params:
headerName: x-provider
defaultProvider: openai-provider
mappings:
- headerValue: openai
provider: openai-provider
- headerValue: anthropic
provider: anthropic-provider
EOF
The controller automatically passes the additional provider's effective upstream name to its transformer. Do not add a providerId under transformer.params; it is injected from additionalProviders[].id or additionalProviders[].as.
Step 4: Create a Proxy Consumer Key¶
The proxy uses api-key-auth to protect its public endpoint. Create a key for the application that will invoke it:
PROXY_CONSUMER_KEY=$(curl -s -X POST \
http://localhost:9090/api/management/v1/llm-proxies/openai-multi/api-keys \
-u admin:admin \
-H "Content-Type: application/json" \
-d '{"name":"openai-multi-client"}' \
| jq -r '.apiKey.apiKey')
Verify that a key was returned:
Step 5: Invoke Different Providers¶
All requests use the same URL and OpenAI Chat Completions payload.
Invoke the default provider¶
If x-provider is omitted, the router uses defaultProvider, which is openai-provider in this example.
curl -k -X POST https://localhost:8443/openai-multi/chat/completions \
-H "Content-Type: application/json" \
-H "X-API-Key: ${PROXY_CONSUMER_KEY}" \
-d '{
"model": "gpt-4o-mini",
"messages": [
{
"role": "user",
"content": "Explain multi-provider routing in one sentence."
}
]
}'
Invoke Anthropic¶
Set x-provider to the configured headerValue:
curl -k -X POST https://localhost:8443/openai-multi/chat/completions \
-H "Content-Type: application/json" \
-H "X-API-Key: ${PROXY_CONSUMER_KEY}" \
-H "x-provider: anthropic" \
-d '{
"model": "client-model-name",
"messages": [
{
"role": "user",
"content": "Explain multi-provider routing in one sentence."
}
]
}'
The Anthropic transformer replaces the request's model value with the model configured under transformer.params.model. It also translates the request to the Anthropic Messages format and translates the response back to the OpenAI response shape.
Header names and mapped header values are matched case-insensitively. Leading and trailing whitespace in the header value is ignored. If the header is missing, empty, or does not match a mapping, the router selects defaultProvider.
Add More Providers¶
Add each selectable provider under additionalProviders, then add a corresponding mapping under the LLM Header Router policy (llm-header-router).
Supported provider transformers¶
Use a transformer when an additional provider does not accept and return the OpenAI wire format.
| Target provider | Transformer type | Purpose |
|---|---|---|
| Anthropic | openai-to-anthropic |
Converts OpenAI-compatible requests to the Anthropic Messages format and converts responses back to the OpenAI format. |
| Azure OpenAI | openai-to-azure-openai |
Adapts OpenAI-compatible requests for Azure OpenAI deployments and API versions. |
| Mistral | openai-to-mistral |
Adapts OpenAI-compatible requests and responses for Mistral. |
| Gemini | openai-to-gemini |
Converts OpenAI-compatible requests and responses for Google Gemini. |
| AWS Bedrock | openai-to-bedrock-transformer |
Converts OpenAI-compatible requests and supported AWS Bedrock responses. |
A transformer is not required when the selected provider already exposes an OpenAI-compatible API.
Azure OpenAI¶
- id: azure-openai-provider
auth:
type: api-key
header: X-API-Key
value: <azure-provider-loopback-key>
transformer:
type: openai-to-azure-openai
version: v1
params:
model: gpt-4o
apiVersion: "2024-02-15-preview"
Mistral¶
- id: mistral-provider
auth:
type: api-key
header: X-API-Key
value: <mistral-provider-loopback-key>
transformer:
type: openai-to-mistral
version: v1
params:
model: mistral-large-latest
Gemini¶
- id: gemini-provider
auth:
type: api-key
header: X-API-Key
value: <gemini-provider-loopback-key>
transformer:
type: openai-to-gemini
version: v1
params:
model: gemini-2.5-flash
apiVersion: v1beta
AWS Bedrock¶
- id: aws-bedrock-provider
auth:
type: api-key
header: X-API-Key
value: <aws-bedrock-provider-loopback-key>
transformer:
type: openai-to-bedrock-transformer
version: v1
params:
model: anthropic.claude-3-5-sonnet-20240620-v1:0
For example, the matching router entries are:
mappings:
- headerValue: azure-openai
provider: azure-openai-provider
- headerValue: mistral
provider: mistral-provider
- headerValue: gemini
provider: gemini-provider
- headerValue: aws-bedrock
provider: aws-bedrock-provider
Use Provider Aliases¶
Use as when the logical upstream name used by routing policies should differ from the deployed provider ID:
additionalProviders:
- id: anthropic-provider
as: anthropic-upstream
auth:
type: api-key
header: X-API-Key
value: <anthropic-provider-loopback-key>
transformer:
type: openai-to-anthropic
version: v1
params:
model: claude-sonnet-4-5-20250929
When an alias is present, router mappings must select the alias, not the provider ID:
The alias must:
- Contain only letters, numbers, hyphens, or underscores
- Be between 1 and 100 characters
- Be unique within the proxy
- Not match the primary provider ID or another additional provider's effective name
Configuration Reference¶
additionalProviders¶
| Field | Required | Description |
|---|---|---|
id |
Yes | ID of an already deployed LlmProvider |
as |
No | Logical upstream name used by routing policies; defaults to id |
auth |
No | API key authentication used by the proxy when calling the provider's internal route |
transformer |
No | Request and response transformer applied only when this provider is selected |
transformer¶
| Field | Required | Description |
|---|---|---|
type |
Yes | Installed transformer policy name, such as openai-to-anthropic |
version |
Yes | Major policy version, such as v1 |
params |
No | Transformer-specific parameters, such as model or apiVersion |
LLM Header Router parameters¶
Use llm-header-router as the policy name in the configuration.
| Parameter | Required | Default | Description |
|---|---|---|---|
headerName |
No | x-provider |
Request header used for selection |
defaultProvider |
No | Primary provider | Effective provider name selected when no mapping matches. When omitted, the proxy's primary provider is used. |
mappings |
Yes | None | Header value to effective provider name mappings; the first match wins |
Validation and Troubleshooting¶
The additional provider is not found¶
Deploy every provider before deploying the proxy. Each additionalProviders[].id must match the metadata.name of an existing LlmProvider.
The proxy reports a duplicate upstream name¶
Every effective provider name must be unique. The effective name is as when it is configured; otherwise, it is id. It must not collide with the primary provider ID.
The transformer is rejected during deployment¶
Make sure that:
transformer.typenames a transformer supported by your AI Gateway version.transformer.versionuses a major-only version such asv1.- All parameters required by that transformer are present.
The gateway resolves the major version to an installed full policy version and rejects invalid transformer configuration during deployment.
The request always reaches the default provider¶
Check that:
- The routing policy is attached to the same path and method being invoked.
- The request uses the header configured by
headerName. - The header value matches a
mappings[].headerValue. - The mapping's
providermatches the additional provider'sasvalue when an alias is configured; otherwise, it matchesid.
An unknown header value intentionally falls back to defaultProvider.
The provider returns 401 Unauthorized¶
Confirm which authentication layer rejected the request:
- A rejection at the proxy usually means the proxy consumer key is missing or invalid.
- A rejection on the provider's loopback route usually means
provider.authoradditionalProviders[].authcontains an invalid provider API key. - A rejection from the external vendor usually means
LlmProvider.spec.upstream.authcontains an invalid vendor credential or uses the wrong header format.
The configured transformer is not supported¶
The AI Gateway distribution includes the router and transformer policies supported by that version. Use a supported transformer.type and major version, or upgrade the AI Gateway to a version that includes the required transformer.
Security Recommendations¶
- Store vendor credentials and loopback keys in a secret manager or Kubernetes
Secretinstead of committing plain-text values. - Protect the proxy with an authentication policy so applications cannot invoke it anonymously.
- Expose only required provider operations through
accessControl. - Apply rate limiting and guardrails at the provider or proxy level according to your governance requirements.
- Use explicit router mappings. Do not accept a client-provided value as an unrestricted upstream name.
Complete Example¶
For a larger configuration containing OpenAI, Anthropic, Azure OpenAI, Mistral, Gemini, and AWS Bedrock, see gateway/examples/openai-multi-provider-proxy.yaml.
For automatic traffic distribution across models and providers, see:
AWS Bedrock usage can also be evaluated by the LLM Cost policy.