Fallback With Translation
Demonstrates provider failover with Responses-to-Chat Completions protocol translation using the iterative_request_router
Category: Setup-dependent integration
Task: Demonstrates provider failover with Responses-to-Chat Completions protocol translation using the iterative_request_router
Prerequisites: The external service, credentials, or certificates referenced by this configuration.
This configuration comes from the selected release. The example has not been run here; external services are not bundled.
Download the source file.
# Inference Fallback with Protocol Translation
# Requires `--features openai-responses` because these filters are opt-in.
#
# Demonstrates provider failover with Responses-to-Chat Completions
# protocol translation using the iterative_request_router. When the
# primary backend returns a retryable status, the request automatically
# retries against a fallback provider. Both backends receive translated
# Chat Completions requests with isolated credentials.
#
# Step boundary: the IRR resets per-step metadata for credential
# isolation but preserves extensions across steps. This means:
#
# - ResponsesState (extension, set by openai_responses_request)
# survives step transitions.
# - openai_responses_request.format (metadata) does not.
#
# Because openai_responses_to_chat_completions reads both metadata and
# extensions, each step must re-run openai_responses_request to
# repopulate its metadata. Without it the translation filter returns
# 500: "request pipeline state is unavailable".
#
# Filter ordering within each step:
#
# 1. openai_responses_request (facts) classify body, set metadata,
# cache the parse
# 2. openai_responses_request (owner) reuse the parse, set metadata
# + ResponsesState extension
# 3. openai_responses_to_chat_completions translate body using both
# 4. path_rewrite /v1/responses -> /v1/chat/completions
# 5. router select cluster
# 6. credential_injection inject per-cluster credentials
# 7. load_balancer select endpoint
#
# The two openai_responses_request entries share one deserialization:
# the first (initialize_state: false) publishes classification facts and
# caches its parse; the second (the managed owner) reuses that parse.
#
# Key ordering constraints:
# - 1 before 2: the owner reuses the facts pass's cached parse
# - 2 before 3: translation reads responses.response_id metadata
# and ResponsesState extension, both set by the owner
# - 5 before 6: credential injection matches on ctx.cluster,
# which the router sets
#
# Pre-IRR filters:
# ai_operation publishes the typed operation identity once as a request
# extension. Extensions persist across IRR steps, so the per-step
# openai_responses_request passes consume the same match without re-adding
# ai_operation. The facts openai_responses_request classifies the request;
# the owner openai_responses_request rejects malformed bodies before the IRR
# allocates sub-request resources.
#
# Adapting for real providers:
# - Add openai_responses_model_rewrite between the facts and owner
# passes in the fallback step to map model names across providers.
# - Use env_var instead of value in credential_injection to read
# API keys from the environment at startup.
# - Add a headers filter (request_set Host) before load_balancer
# for external HTTPS upstreams.
# - Add tls: { sni: "hostname" } to the cluster for HTTPS.
# - Adjust the path_rewrite replacement per provider (e.g.
# /api/v1/chat/completions for OpenRouter).
#
# Usage:
# cargo run -p praxis-ai-proxy --features openai-responses -- -c examples/configs/inference/fallback-with-translation.yaml
listeners:
- name: ai-gateway
address: "127.0.0.1:8080"
filter_chains: [inference-failover]
filter_chains:
- name: inference-failover
filters:
# Publishes the typed operation consumed by every openai_responses_request
# pass below, including the per-IRR-step passes (extensions persist).
- filter: ai_operation
- filter: openai_responses_request
initialize_state: false
on_invalid: reject
headers:
format: x-praxis-ai-format
model: x-praxis-ai-model
stream: x-praxis-ai-stream
- filter: openai_responses_request
on_invalid: reject
headers:
format: ~
model: ~
stream: ~
mode: ~
- filter: iterative_request_router
initial_step: primary
steps:
- name: primary
filters:
- filter: openai_responses_request
initialize_state: false
- filter: openai_responses_request
on_invalid: reject
headers:
format: ~
model: ~
stream: ~
mode: ~
- filter: openai_responses_to_chat_completions
max_rewritten_body_bytes: 67108864
- filter: path_rewrite
replace:
pattern: "^/v1/responses/?$"
replacement: "/v1/chat/completions"
conditions:
- when:
path_prefix: "/v1/responses"
methods: [POST]
- filter: router
routes:
- path_prefix: "/"
cluster: primary-backend
- filter: credential_injection
clusters:
- name: primary-backend
header: Authorization
value: "primary-key"
header_prefix: "Bearer "
strip_client_credential: true
- filter: load_balancer
clusters:
- name: primary-backend
endpoints:
- "127.0.0.1:3001"
on_result:
- status: [429, 502, 503, 504]
next: fallback
- default: true
done: true
- name: fallback
filters:
- filter: openai_responses_request
initialize_state: false
- filter: openai_responses_request
on_invalid: reject
headers:
format: ~
model: ~
stream: ~
mode: ~
- filter: openai_responses_to_chat_completions
max_rewritten_body_bytes: 67108864
- filter: path_rewrite
replace:
pattern: "^/v1/responses/?$"
replacement: "/v1/chat/completions"
conditions:
- when:
path_prefix: "/v1/responses"
methods: [POST]
- filter: router
routes:
- path_prefix: "/"
cluster: fallback-backend
- filter: credential_injection
clusters:
- name: fallback-backend
header: Authorization
value: "fallback-key"
header_prefix: "Bearer "
strip_client_credential: true
- filter: load_balancer
clusters:
- name: fallback-backend
endpoints:
- "127.0.0.1:3002"
on_result:
- default: true
done: true
insecure_options:
allow_private_endpoints: true # example proxies to local backends