Docs navigation

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.

Rules are managed from the console (Organization → Rules) with a visual builder, or programmatically through the same console API the UI uses. The document format is identical either way.

The rule document

json
{
  "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 }
  ]
}
FieldMeaning
phaserequest runs before the provider call; response runs on the model's answer.
modeobserve evaluates and records matches but never changes traffic — the safe way to roll a rule out. enforce applies the actions.
priorityLower runs first. The console's up/down arrows rewrite priorities as 10, 20, 30…
scopeEmpty = all traffic. Within a dimension values are OR'd; across dimensions they are AND'd. models accepts globs like openai/*.
expressionThe condition, in the expression language below.
actionsOrdered 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:

bash
# 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.

text
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)
containsSubstring, or list membership for list fields
matchesRegex search. Right side must be /regex/flags (flags: i, m, s) or a @pattern
starts_with ends_withString prefix / suffix
inMembership in a list literal: {"a", "b"}
and or not ( )Boolean logic; not binds tightest
len() lower() upper() trim() count(x, /re/)Functions

Fields

FieldDescription
request.endpointchat | messages | responses | embeddings
request.model / request.providerWhat the client asked for, and the provider it resolves to
request.textAll message text (system + user + assistant + tool results)
request.user_text / request.last_user / request.systemUser messages, the last user message, the system prompt
request.tokens / request.max_tokens / request.temperatureEstimated prompt tokens, requested output cap, temperature
request.tools / request.tool_countTool names offered (list) and count
request.user / request.metadata.*Client-supplied end-user id and metadata fields
request.stream / request.has_images / request.message_countBooleans and counts
request.body.<path>Any raw body field by dotted path, e.g. request.body.top_p
response.text / response.finish_reason / response.modelAssistant text, stop reason (stop | length | tool_calls), reported model
response.input_tokens / output_tokens / total_tokens / cost_usd / latency_msBilling 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.providerThe resolved catalog model
key.id / key.name · team.id / team.name · member.id / member.email / member.nameThe caller
org.id / org.planYour organization
time.hour / time.weekday / time.isoNow, 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.

GroupPatterns
@piiemail, phone, phone_intl, ssn, credit_card, ipv4, iban, us_passport, date_of_birth
@secretopenai_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
@attackprompt_injection, jailbreak, system_prompt_leak, role_override
@contentprofanity, url, markdown_code_block, base64_blob

Actions

ActionConfigEffect
blockmessageReject with HTTP 403 rule_blocked. On the request phase this happens before admission — no provider spend.
redactpatterns[], replacement, roles[]Replace matches in message text (request) or assistant text (response). Shape-preserving: only text leaves change.
transformset, 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.
routemodelServe the request with a different model (catalog id, or auto / auto:cost). Request phase only. Logged with routing_mode=rule.
alertemail, webhook_url, webhook_secret, slack_webhook_url, cooldown_minutes, severityNotify 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.
logtagRecord 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, redact and transform apply fully to non-streaming responses and to /v1/chat/completions streams — when a mutating response rule is in scope the gateway buffers the stream and replays it as a single frame set.
  • On /v1/messages and /v1/responses streams the bytes are relayed verbatim (the passthrough guarantee), so mutating response actions are recorded as observed while alert and log still 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

json
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 observe mode, watch Activity for a day, then flip to enforce.
  • 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 @pattern references over hand-written regex — they are tuned for precision.

Examples

json
// 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

PlanRules
Free3
Starter10
Team50
EnterpriseUnlimited

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.