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.
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
| Field | Type | Default | Description |
|---|---|---|---|
name | string | Unique route name for lint output and logs. Required. | |
path | string | Path to match. The gateway removes repeated and trailing slashes before it compares. Required. | |
match | string | prefix | prefix or exact |
methods | array of strings | All methods | HTTP methods that match. Case does not matter. |
headers | map of strings | Each header must have this value. The value comparison ignores case. | |
priority | integer | 0 | When several routes match, the highest priority wins. |
chain | array | Chain for matching requests. Replaces the global chain. |
Consumer group fields
| Field | Type | Default | Description |
|---|---|---|---|
key_header | string | Authorization | Header that carries the API key |
groups[].name | string | Group name for logs. Required. | |
groups[].api_keys | array of strings | SHA-256 hex hashes of the tokens in the group. A group without hashes never matches. | |
groups[].rate_limit.max_requests | integer | Request limit for the group on one gateway instance | |
groups[].rate_limit.window_seconds | integer | Length of the limit window. Required when rate_limit is set. | |
groups[].chain | array | Chain 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.
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.8Header-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:
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: 0Calculate 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())
PYCODEPut 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.
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.
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:
- Collects the routes that match the normalized path, the method, and every header condition.
- Sorts them by
priority, highest first. For equal priorities, the first declared route wins. - Reads the API key from
key_header. ForAuthorization, it removes theBearerprefix. - Hashes the key with SHA-256 and looks for the hash in the consumer groups.
- 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_requestsin memory on each gateway instance. Instances do not share the count. - When a group exceeds
max_requestsinwindow_seconds, the gateway returns HTTP429with error codeconsumer_group_rate_limit_exceededand theX-Verdictan-Consumer-Groupheader. - A group without
rate_limit.max_requestshas no limit. - For limits shared across instances, use the distributed backend in Rate Limit Configuration.
Next steps
- Rate Limit Configuration: global, token, and distributed limits.
- Conditional Chain Configuration: turn one control on or off for some requests instead of replacing the chain.
- Security and Network Configuration: limit client access at the network layer.