Messages To Openai Vllm

Translates native Anthropic Messages API traffic into OpenAI Chat Completions for a vLLM backend that serves /v1/chat/completions, with the same three-boundary credential isolation as the native passthrough config

Category: Setup-dependent integration
Task: Translates native Anthropic Messages API traffic into OpenAI Chat Completions for a vLLM backend that serves /v1/chat/completions, with the same three-boundary credential isolation as the native passthrough config

Prerequisites: The external service, credentials, or certificates referenced by this configuration.

Run it: Use ghcr.io/praxis-proxy/ai:0.8.0 and follow the container quickstart to mount and start the configuration.

This configuration comes from the selected release. The example has not been run here; external services are not bundled.

Download the source file.

Companion resources from the same snapshot:

# Anthropic Messages -> Chat Completions vLLM Passthrough (credential-isolated)
#
# Translates native Anthropic Messages API traffic into OpenAI Chat Completions
# for a vLLM backend that serves `/v1/chat/completions`, with the same
# three-boundary credential isolation as the native passthrough config. This is
# a config an unmodified Claude Code client can drive end to end. It enables
# operator-approved degradation (`allow_lossy_features` on the translation
# filter) so the Anthropic-only features Claude Code sends by default — prompt
# caching (`cache_control` markers) and extended thinking (`thinking`) — are
# stripped and reported instead of rejected. Without that allowlist such
# requests fail closed with a 400 (the strict default of
# `messages-to-openai.yaml`). The client speaks the Anthropic wire format;
# Praxis rewrites both the request and the response, and vLLM only sees Chat
# Completions.
#
# Contrast with `messages-native-vllm.yaml`, which passes the Anthropic wire
# format through UNCHANGED to a vLLM backend that serves `/v1/messages`
# natively. Use this config instead when the backend speaks only OpenAI Chat
# Completions (e.g. llm-d with disaggregation, KServe without Anthropic
# support), and `messages-native-vllm.yaml` when it serves Anthropic natively.
#
# Translation (adapted from `messages-to-openai.yaml`):
#   - `anthropic_messages_to_chat_completions` rewrites the request body
#     (system hoisting, content-block flattening, tool mapping) and the
#     non-streaming response body back into Anthropic Messages.
#   - `anthropic_messages_to_chat_completions_stream` converts streaming SSE,
#     gated to `text/event-stream` responses.
#   - `path_rewrite` maps `POST /v1/messages` to `/v1/chat/completions`.
#
# Credential isolation (three distinct credentials, each at its own boundary),
# identical to `messages-native-vllm.yaml`:
#   - `basic_auth` authenticates the *client* to this gateway. A trusted caller
#     (e.g. Claude Code via `ANTHROPIC_CUSTOM_HEADERS`) presents an
#     `Authorization: Basic ...` gateway credential; `basic_auth` verifies it
#     and, with `strip_authorization: true`, removes it so it never reaches the
#     backend.
#   - The `headers` filter removes the client's `x-api-key` (the native
#     Anthropic auth header) so a caller's Anthropic key never reaches vLLM.
#   - `credential_injection` then injects the backend's own distinct Bearer
#     token from `VLLM_API_KEY` toward the cluster.
#
# The gateway password is resolved from `GATEWAY_AUTH_PASSWORD` at pipeline
# build time; set it (and `VLLM_API_KEY`) in the proxy's environment.
#
# Note: `/v1/messages/count_tokens` has no Chat Completions equivalent, so
# neither body translation nor path rewriting applies to it. A backend that
# also serves the native endpoint can count tokens; a Chat-Completions-only
# backend returns 404. Claude Code treats native token counting as best-effort
# and degrades gracefully.

listeners:
  - name: anthropic-gateway
    address: "127.0.0.1:8080"
    filter_chains: [anthropic-transform-vllm]

filter_chains:
  - name: anthropic-transform-vllm
    filters:
      # Authenticate the client to the gateway BEFORE anything else, and strip
      # the gateway `Authorization` so it never reaches the backend. Real callers
      # present it via a custom `Authorization: Basic ...` header (Claude Code:
      # `ANTHROPIC_CUSTOM_HEADERS`).
      - filter: basic_auth
        realm: "praxis-transform-vllm-gateway"
        strip_authorization: true
        credentials:
          - username: gateway
            env_var: GATEWAY_AUTH_PASSWORD

      # Process the create-message body before translation: validate the
      # envelope and promote facts to internal `x-praxis-ai-*` headers.
      # `on_invalid: reject` refuses a malformed body at the gateway, which is
      # what the separate validator here used to do. No path condition is
      # needed — the operation keys the filter, so bodyless probes such as
      # `GET /v1/models` are released untouched.
      - filter: anthropic_messages_request
        on_invalid: reject

      # Translate the Anthropic request body into OpenAI Chat Completions and the
      # non-streaming response body back into Anthropic Messages.
      - filter: anthropic_messages_to_chat_completions
        max_body_bytes: 1048576
        # Operator-approved degradation. Chat Completions cannot represent these
        # Anthropic features, so the translator strips their wire markers and
        # reports the loss (one WARN log per request, the
        # praxis_anthropic_messages_to_chat_completions_degraded_total counter,
        # and an x-degraded-features response header) instead of
        # rejecting an unmodified Claude Code client with a 400. Remove an entry
        # to make that feature fail closed again; malformed markers and every
        # feature not listed here are still rejected.
        #   - prompt_caching:    drop `cache_control` markers. Prompt and tool
        #     content are preserved; explicit cache breakpoints are not honored,
        #     so cost and latency may differ.
        #   - extended_thinking: drop `thinking` and thinking-only
        #     `context_management` edits. The translated response carries no
        #     thinking blocks.
        allow_lossy_features:
          - prompt_caching
          - extended_thinking
        conditions:
          - when:
              path: "/v1/messages"

      # Convert streaming Chat Completions SSE back into Anthropic SSE. Gated to
      # `text/event-stream` so buffered JSON responses stay on the non-streaming
      # path above.
      - filter: anthropic_messages_to_chat_completions_stream
        max_partial_event_bytes: 10485760
        # Cap on distinct streaming tool-call content blocks retained per
        # response; exceeding it fails the stream closed.
        max_tool_blocks: 10000
        response_conditions:
          - when:
              headers:
                content-type: "text/event-stream"

      # Strip the client's native Anthropic auth header so it never reaches the
      # backend. `credential_injection` below handles `Authorization`.
      - filter: headers
        request_remove:
          - x-api-key

      # Map the Anthropic message endpoint onto the backend's Chat Completions
      # endpoint. Anchored to `^/v1/messages$` so `/v1/messages/count_tokens`
      # and other paths are left unchanged.
      - filter: path_rewrite
        replace:
          pattern: "^/v1/messages$"
          replacement: "/v1/chat/completions"
        conditions:
          - when:
              path_prefix: "/v1/messages"

      - filter: router
        routes:
          - path_prefix: "/"
            cluster: "chat-completions-backend"

      # Inject the backend's own credential after routing selects the cluster.
      # `strip_client_credential` (default true) removes any client-supplied
      # `Authorization` before injection.
      - filter: credential_injection
        clusters:
          - name: chat-completions-backend
            header: Authorization
            env_var: VLLM_API_KEY
            header_prefix: "Bearer "
            strip_client_credential: true

      - filter: load_balancer
        clusters:
          - name: "chat-completions-backend"
            endpoints:
              - "127.0.0.1:8000"

insecure_options:
  allow_private_endpoints: true # example proxies to a local vLLM backend