Skip to main content

Providers & Routing

Routes and Consumer Groups

By default, every request runs the global policies.chain. A route gives requests with a matching path, method, or header their own chain. A consumer group gives requests from specific application tokens their own chain and request limit. Neither changes the provider or the upstream path.

The gateway picks one chain per request, in this order: consumer group, then route, then the global chain. The selected chain replaces the others. It does not add to them, so put every mandatory control in every chain.

Start with one route​

This route gives Chat Completions its own chain. Add a working provider target, then apply the file to a test agent with the config-first workflow.

policy-config.yaml
pack:
  name: route-example
  version: 1.0.0
  enabled: true
policies:
  chain: [audit-logger, prompt-injection, pii-detector]
policy:
  audit-logger: {}
  prompt-injection:
    response:
      action: block
  pii-detector:
    action: redact
routes:
  - name: chat-secured
    path: /v1/chat/completions
    match: exact
    methods: [POST]
    chain: [audit-logger, prompt-injection, pii-detector]

verdictan policy lint fails when a route or group chain names a control that is not in the global policies.chain. Put every control that any chain uses in the global chain too, and give each one a policy block. To run a control only on some routes, add it to the global chain and leave it out of a catch-all route, as in Human Oversight.

Fields​

Route fields​

FieldTypeDefaultDescription
namestringUnique route name for lint output and logs. Required.
pathstringPath to match. The gateway removes repeated and trailing slashes before it compares. Required.
matchstringprefixprefix or exact
methodsarray of stringsAll methodsHTTP methods that match. Case does not matter.
headersmap of stringsEach header must have this value. The value comparison ignores case.
priorityinteger0When several routes match, the highest priority wins.
chainarrayChain for matching requests. Replaces the global chain.

Consumer group fields​

FieldTypeDefaultDescription
key_headerstringAuthorizationHeader that carries the API key
groups[].namestringGroup name for logs. Required.
groups[].api_keysarray of stringsSHA-256 hex hashes of the tokens in the group. A group without hashes never matches.
groups[].rate_limit.max_requestsintegerRequest limit for the group on one gateway instance
groups[].rate_limit.window_secondsintegerLength of the limit window. Required when rate_limit is set.
groups[].chainarrayChain for the group. Replaces the route and global chains.

Examples​

Different chains for each endpoint​

Chat Completions also gets an output quality check. Responses keeps the input controls without it.

policy-config.yaml
pack:
  name: multi-endpoint
  version: 1.0.0
  enabled: true
providers:
  targets:
  - id: openai-primary
    provider: openai
    model: your-openai-model
    secret_key_ref:
      env: VERDICTAN_OPENAI_API_KEY
routes:
- name: chat-secured
  path: "/v1/chat/completions"
  match: exact
  chain:
  - audit-logger
  - prompt-injection
  - pii-detector
  - quality-scorer
- name: responses-standard
  path: "/v1/responses"
  match: exact
  chain:
  - audit-logger
  - prompt-injection
  - pii-detector
policies:
  chain:
  - audit-logger
  - prompt-injection
  - pii-detector
  - quality-scorer
policy:
  audit-logger: {}
  prompt-injection: {}
  pii-detector:
    action: redact
  quality-scorer:
    thresholds:
      min_aggregate: 0.8

Header-based routing​

A client can send any header value. A caller-controlled X-Team header does not prove team membership, so use headers for preferences only, unless a trusted proxy removes and replaces the value. Here, the compliance route adds dlp-filter, and every route keeps the mandatory controls:

policy-config.yaml
pack:
  name: config-routes-and-consumers-routes-5
  version: 1.0.0
  enabled: true
providers:
  targets:
  - id: openai-primary
    provider: openai
    model: your-openai-model
    secret_key_ref:
      env: VERDICTAN_OPENAI_API_KEY
policies:
  chain:
  - audit-logger
  - prompt-injection
  - pii-detector
  - dlp-filter
policy:
  audit-logger: {}
  prompt-injection: {}
  pii-detector:
    action: redact
  dlp-filter:
    action: block
routes:
- name: compliance-route
  path: "/v1/chat/completions"
  match: exact
  headers:
    X-Team: compliance
  chain:
  - audit-logger
  - prompt-injection
  - pii-detector
  - dlp-filter
  priority: 10
- name: default-route
  path: "/v1/chat/completions"
  match: exact
  chain:
  - audit-logger
  - prompt-injection
  - pii-detector
  priority: 0

Calculate key hashes​

A consumer group lists hashes of existing application tokens. The hash selects a group. It does not create or authorize a token. Hash the token without the Bearer prefix and without a trailing newline:

python3 - <<'PYCODE'
import hashlib
import os
print(hashlib.sha256(os.environ["VERDICTAN_CLIENT_TOKEN"].encode()).hexdigest())
PYCODE

Put the 64-character result in api_keys. The hashes in the examples below are placeholders.

Tiered access​

Each tier gets a different request limit. Every tier keeps the mandatory controls. professional has no chain, so it uses the global chain.

policy-config.yaml
pack:
  name: "tiered-access"
  version: "1.0.0"
  enabled: true

providers:
  targets:
    - id: "openai-primary"
      provider: "openai"
      model: "your-openai-model"
      secret_key_ref:
        env: "VERDICTAN_OPENAI_API_KEY"

consumer_groups:
  key_header: "Authorization"
  groups:
    - name: "enterprise"
      api_keys:
        - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      rate_limit:
        max_requests: 5000
        window_seconds: 3600
      chain:
        - "audit-logger"
        - "prompt-injection"
        - "pii-detector"
    - name: "professional"
      api_keys:
        - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
        - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
      rate_limit:
        max_requests: 500
        window_seconds: 3600
    - name: "free-tier"
      api_keys:
        - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
      rate_limit:
        max_requests: 20
        window_seconds: 3600
      chain:
        - "audit-logger"
        - "prompt-injection"
        - "pii-detector"
        - "quality-scorer"

policies:
  chain:
    - "audit-logger"
    - "prompt-injection"
    - "pii-detector"
    - "quality-scorer"

Routes with consumer groups​

A request from the internal group uses the group chain. A request from anyone else to /v1/responses uses the route chain. Every other request uses the global chain.

policy-config.yaml
pack:
  name: "combined-routing"
  version: "1.0.0"
  enabled: true

providers:
  targets:
    - id: "openai-primary"
      provider: "openai"
      model: "your-openai-model"
      secret_key_ref:
        env: "VERDICTAN_OPENAI_API_KEY"

routes:
  - name: "responses-standard"
    path: "/v1/responses"
    match: exact
    chain:
      - "audit-logger"
      - "prompt-injection"
      - "pii-detector"

consumer_groups:
  groups:
    - name: "internal"
      api_keys: ["dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"]
      rate_limit:
        max_requests: 10000
        window_seconds: 3600
      chain:
        - "audit-logger"
        - "prompt-injection"
        - "pii-detector"

policies:
  chain:
    - "audit-logger"
    - "prompt-injection"
    - "pii-detector"
    - "quality-scorer"

Limits and failure behavior​

Request families that resolve routes and groups​

Routes and consumer groups apply to POST /v1/chat/completions, POST /v1/responses, and POST /v1/messages, and to the GET upgrade of the WebSocket gateway. Other families, such as embeddings, always use the global chain.

The HTTP handlers match routes with POST. A route whose methods leaves out POST never matches them. Include POST, or leave methods out.

Chain resolution​

For each request, the gateway:

  1. Collects the routes that match the normalized path, the method, and every header condition.
  2. Sorts them by priority, highest first. For equal priorities, the first declared route wins.
  3. Reads the API key from key_header. For Authorization, it removes the Bearer prefix.
  4. Hashes the key with SHA-256 and looks for the hash in the consumer groups.
  5. Uses the group chain if a group matched, else the route chain if a route matched, else the global chain.

Consumer group request limits​

  • The gateway counts max_requests in memory on each gateway instance. Instances do not share the count.
  • When a group exceeds max_requests in window_seconds, the gateway returns HTTP 429 with error code consumer_group_rate_limit_exceeded and the X-Verdictan-Consumer-Group header.
  • A group without rate_limit.max_requests has no limit.
  • For limits shared across instances, use the distributed backend in Rate Limit Configuration.

Next steps​