# 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
