Platform
Rules
Match requests and responses with an expression, then block, redact, transform, route, or alert.
Rules are policies the gateway evaluates on every request (and, optionally, every response) in scope. A rule is when <expression> → then [actions], scoped to any combination of teams, members, API keys, providers, models, and endpoints. They are deterministic and run in-process from a compiled cache — no extra round trips, no judge model, microseconds per request. For probabilistic review of content, see Monitors.
The rule document
{
"name": "Block leaked secrets",
"phase": "request",
"mode": "enforce",
"priority": 10,
"enabled": true,
"scope": { "teams": ["<team-id>"], "endpoints": ["chat", "messages"] },
"expression": "request.text matches @secret.openai_key or request.text matches @secret.private_key",
"actions": [
{ "action": "block", "message": "Remove the secret and try again." },
{ "action": "alert", "email": true, "severity": "high", "cooldown_minutes": 60 }
]
}| Field | Meaning |
|---|---|
phase | request runs before the provider call; response runs on the model's answer. |
mode | observe evaluates and records matches but never changes traffic — the safe way to roll a rule out. enforce applies the actions. |
priority | Lower runs first. The console's up/down arrows rewrite priorities as 10, 20, 30… |
scope | Empty = all traffic. Within a dimension values are OR'd; across dimensions they are AND'd. models accepts globs like openai/*. |
expression | The condition, in the expression language below. |
actions | Ordered list. block stops the chain; the first route wins; redact and transform compose. |
Managing rules via the API
The console API is session-authenticated (the browser talks to it through the same-origin BFF). From scripts, sign in to obtain a bearer token and call the endpoints directly:
# Create
curl -X POST https://api.tokenrouter.io/api/rules \
-H "Authorization: Bearer $CONSOLE_TOKEN" -H "Content-Type: application/json" \
-d @rule.json
# List / update / delete / reorder
curl https://api.tokenrouter.io/api/rules -H "Authorization: Bearer $CONSOLE_TOKEN"
curl -X PATCH https://api.tokenrouter.io/api/rules/<id> -d '{"mode":"enforce"}' ...
curl -X DELETE https://api.tokenrouter.io/api/rules/<id> ...
curl -X POST https://api.tokenrouter.io/api/rules/reorder -d '{"ids":["<id-1>","<id-2>"]}' ...
# Validate an expression without saving
curl -X POST https://api.tokenrouter.io/api/rules/validate \
-d '{"expression":"request.tokens > 20000","phase":"request"}' ...
# Dry-run rules against a sample exchange (nothing is sent to a provider)
curl -X POST https://api.tokenrouter.io/api/rules/test \
-d '{"endpoint":"chat","request":{"model":"openai/gpt-5-mini","messages":[{"role":"user","content":"…"}]}}' ...
# Activity feed (no bodies — identifiers, outcome, counts)
curl "https://api.tokenrouter.io/api/rules/events?outcome=blocked" ...Expression language
Modeled on the filter languages you already know (Cloudflare firewall rules, Wireshark): fields, comparison operators, and / or / not with parentheses, regex literals, a pattern library, and a few functions. Every comparison is null-safe — a missing field never matches, except against null.
request.text matches @secret.openai_key
request.model == "gpt-4o" and request.tokens > 8000
lower(request.last_user) contains "competitor" or request.user in {"bot-1", "bot-2"}
response.text matches /\b(internal|confidential)\b/i and not key.name starts_with "staff-"
(time.hour < 7 or time.weekday in {"sat", "sun"}) and model.provider == "anthropic"
request.max_tokens == null
count(request.text, @pii.email) >= 3| Operators | |
|---|---|
== != | Equality (strings, numbers, booleans, null) |
< <= > >= | Numeric comparison (numeric strings are coerced) |
contains | Substring, or list membership for list fields |
matches | Regex search. Right side must be /regex/flags (flags: i, m, s) or a @pattern |
starts_with ends_with | String prefix / suffix |
in | Membership in a list literal: {"a", "b"} |
and or not ( ) | Boolean logic; not binds tightest |
len() lower() upper() trim() count(x, /re/) | Functions |
Fields
| Field | Description |
|---|---|
| request.endpoint | chat | messages | responses | embeddings |
| request.model / request.provider | What the client asked for, and the provider it resolves to |
| request.text | All message text (system + user + assistant + tool results) |
| request.user_text / request.last_user / request.system | User messages, the last user message, the system prompt |
| request.tokens / request.max_tokens / request.temperature | Estimated prompt tokens, requested output cap, temperature |
| request.tools / request.tool_count | Tool names offered (list) and count |
| request.user / request.metadata.* | Client-supplied end-user id and metadata fields |
| request.stream / request.has_images / request.message_count | Booleans and counts |
| request.body.<path> | Any raw body field by dotted path, e.g. request.body.top_p |
| response.text / response.finish_reason / response.model | Assistant text, stop reason (stop | length | tool_calls), reported model |
| response.input_tokens / output_tokens / total_tokens / cost_usd / latency_ms | Billing and timing |
| response.tool_calls / response.tool_count / response.streamed / response.body.<path> | Tool calls made, streaming flag, raw body |
| model.id / model.name / model.provider | The resolved catalog model |
| key.id / key.name · team.id / team.name · member.id / member.email / member.name | The caller |
| org.id / org.plan | Your organization |
| time.hour / time.weekday / time.iso | Now, in your org timezone (hour 0–23; mon…sun) |
Pattern library
Reference built-in patterns as @group.name anywhere a regex is accepted — in matches, count(), and in redact / replace actions.
| Group | Patterns |
|---|---|
| @pii | email, phone, phone_intl, ssn, credit_card, ipv4, iban, us_passport, date_of_birth |
| @secret | openai_key, anthropic_key, tokenrouter_key, aws_access_key, aws_secret_key, github_token, slack_token, stripe_key, google_api_key, jwt, private_key, generic_api_key, password_assignment, database_url |
| @attack | prompt_injection, jailbreak, system_prompt_leak, role_override |
| @content | profanity, url, markdown_code_block, base64_blob |
Actions
| Action | Config | Effect |
|---|---|---|
block | message | Reject with HTTP 403 rule_blocked. On the request phase this happens before admission — no provider spend. |
redact | patterns[], replacement, roles[] | Replace matches in message text (request) or assistant text (response). Shape-preserving: only text leaves change. |
transform | set, unset, system_prepend, system_append, replace[] (request); replace[], append_text (response) | Rewrite body fields, inject instructions, find-and-replace. model/messages/input/stream/tools cannot be set — use route and redact. |
route | model | Serve the request with a different model (catalog id, or auto / auto:cost). Request phase only. Logged with routing_mode=rule. |
alert | email, webhook_url, webhook_secret, slack_webhook_url, cooldown_minutes, severity | Notify org admins / POST a JSON payload (HMAC-SHA256 in X-TokenRouter-Signature when a secret is set) / post to Slack. One delivery per cooldown window; repeats are counted. |
log | tag | Record the match in Activity with a tag. |
Phases and streaming
- Request rules always apply, on every surface, streaming or not. They run after model resolution (so
model.*is known) and before admission, so a block never costs a token. - Response rules evaluate the assembled response.
block,redactandtransformapply fully to non-streaming responses and to/v1/chat/completionsstreams — when a mutating response rule is in scope the gateway buffers the stream and replays it as a single frame set. - On
/v1/messagesand/v1/responsesstreams the bytes are relayed verbatim (the passthrough guarantee), so mutating response actions are recorded asobservedwhilealertandlogstill fire.
Ordering
Rules run in priority order (lowest first). Within a rule, actions run in the order written. A block stops everything after it; the first route wins and later routes are ignored; redact and transform compose across rules. Rules in observe mode never change traffic — they record what they would have done.
What clients see on a block
HTTP/1.1 403
{ "error": { "message": "Remove the secret and try again.", "type": "invalid_request_error", "code": "rule_blocked", "param": null } }Blocked requests are logged with status=blocked and block_reason=rule_blocked; a rule event records which rule fired.
Rollout advice
- Create every mutating rule in
observemode, watch Activity for a day, then flip toenforce. - Use the Test panel to dry-run a draft against a sample request before saving.
- Scope narrowly first (one key or team), widen once the match rate looks right.
- Prefer
@patternreferences over hand-written regex — they are tuned for precision.
Examples
// 1. Redact PII in user messages before any provider sees it
{ "phase": "request", "expression": "request.user_text matches @pii.ssn or request.user_text matches @pii.credit_card",
"actions": [{ "action": "redact", "patterns": ["@pii.ssn", "@pii.credit_card"], "roles": ["user"] }] }
// 2. Long prompts to a premium model get a cheaper one
{ "phase": "request", "expression": "request.tokens > 20000 and model.provider == \"openai\" and not request.tools",
"actions": [{ "action": "route", "model": "openai/gpt-5-mini" }] }
// 3. Pin temperature and inject policy for the support team
{ "phase": "request", "scope": { "teams": ["<support-team-id>"] }, "expression": "request.endpoint != \"embeddings\"",
"actions": [{ "action": "transform", "set": { "temperature": 0.2 }, "system_prepend": "Never promise refunds." }] }
// 4. Alert when a response leaks confidential terms
{ "phase": "response", "expression": "response.text matches /\\b(project falcon|internal only)\\b/i",
"actions": [{ "action": "alert", "email": true, "slack_webhook_url": "https://hooks.slack.com/…", "severity": "high" }] }
// 5. Catch runaway agents
{ "phase": "response", "expression": "response.finish_reason == \"length\" and response.output_tokens > 4000",
"actions": [{ "action": "alert", "email": true, "cooldown_minutes": 120 }, { "action": "log", "tag": "runaway" }] }Limits
| Plan | Rules |
|---|---|
| Free | 3 |
| Starter | 10 |
| Team | 50 |
| Enterprise | Unlimited |
Expressions are capped at 4,000 characters and regexes at 1,000; text fields are clipped to 200k characters before matching. Rule events are retained alongside request logs and never contain prompt or response content.