Skip to main content
Version: 1.7.0

Routes & Policies

Runs unlicensed

Runs unlicensed, in full. The complete Policy-as-Code engine — DSL, versioning, dry-run and shadow rollout — is available in the Development posture. Earlier releases offered only a static model allow/deny list here; 1.7.0 removed that split.

Routes decide which provider answers a request. Policies decide whether the request is allowed at all. Both are versioned, both propagate live to the data plane without restart, and both support dry-run / shadow modes before going active.

Route configuration

Open Routing → Routes in the sidebar.

Route list

Lists every route with ID, model pattern, strategy, providers with weights, version, and pinned model version. Auto-refreshes every 10 seconds. Click Edit to open the route editor or Delete to remove a route.

Routes list page with the New Route buttonRoutes list page with the New Route button
Figure 1. Routes list page with the New Route button

Create / edit route

Click New Route to create a route with:

  • Model Pattern — glob pattern to match model names (e.g. gpt*, claude*, ollama/*)
  • Routing Strategymodel-prefix, round-robin, weighted, latency-aware, cost-aware, canary, geo-aware, intelligent
  • Pinned Model Version — optional, pins requests to a specific model version
  • Providers — dynamic table of provider entries with name, weight, and region. Add / remove rows as needed.

Changes propagate to every gateway instance within a few seconds — no restart and no deployment required. Routes are platform-global (no workspace scope); policies, by contrast, are workspace-scoped or global (blank workspace = global).

Every route mutation (ROUTE_CREATED / _UPDATED / _ROLLED_BACK / _DELETED) and policy mutation (POLICY_CREATED / _UPDATED / _STATUS_CHANGED / _ROLLED_BACK / _PROMOTED / _DELETED) emits a signed audit event — from the Console and from the workspace Portal (policies only).

Create-route form with provider entriesCreate-route form with provider entries
Figure 2. Create-route form with provider entries

Version history

In edit mode, the right panel shows the route's version history (last 10 retained). Each version has two actions:

  • Diff — loads a field-by-field comparison between the selected version and the current configuration. Changed fields are highlighted with an amber background.
  • Rollback — rolls back the route to the selected version (creates a new version with the restored configuration). Requires confirmation.
Route version diff: v1 vs current (v2)Route version diff: v1 vs current (v2)
Figure 3. Route version diff: v1 vs current (v2)

Canary A/B testing

When a route uses the canary strategy, a dedicated canary dashboard is available at Routes > (route) > Canary.

Dashboard features:

  • Canary configuration — baseline provider, candidate provider, current split percentage, and workspace scope
  • Live metrics (auto-refreshes every 5 seconds) — side-by-side comparison of baseline vs. candidate:
    • Request count
    • Error count and error rate
    • Total and average latency
    • Total and average cost
  • Split adjustment — update the traffic split percentage on the live route without restarting
  • Reset — clear all canary metrics and start fresh

Canary and shadow metrics are persisted and survive pod restarts, deployments, and autoscaling events. Counters are aggregated cluster-wide — the dashboard shows a single combined view across every gateway instance, not a per-pod slice. Deleting a route cleans up its live metrics automatically.

The Canary A/B Test Dashboard for route-demo-mock in its empty state, reading “No canary metrics collected yet. Send requests to this route to start collecting data.”The Canary A/B Test Dashboard for route-demo-mock in its empty state, reading “No canary metrics collected yet. Send requests to this route to start collecting data.”
Figure 4. Canary route dashboard

Route shadow dashboard

Shadow sends a copy of live traffic to a second provider without serving its response, so you can compare a candidate against production on real requests. Routes → (route) → Shadow reports the comparison and offers a Reset; the report fragment polls every 5s.

Shadow is a property of a route, not a strategy — the route form carries shadowConfig under any strategy, alongside modelTiers (revealed for intelligent), and offers geo-aware and intelligent as strategies.

Editing a route used to drop its shadow config

Before 1.7.0 an edit rebuilt the route from the form, so any field the form did not render was destroyed on save — shadowConfig among them. The save succeeded, the rendered fields were right, and sampling silently stopped. An edit now mutates the loaded route, so a field the form does not show is preserved rather than dropped.

If you configured shadow before 1.7.0 and later edited that route from the Console, check the shadow config is still there.

Policy management

Open Governance → Policies in the sidebar.

Policy list

Lists every policy with ID, name, workspace, status badge (DRAFT / ACTIVE / SHADOW / ARCHIVED), version, and last updated timestamp. Auto-refreshes every 10 seconds.

Use the Workspace dropdown filter to narrow the list. Status badges are color-coded: blue for DRAFT, green for ACTIVE, purple for SHADOW, grey for ARCHIVED.

Policies list with workspace filter and the New Policy buttonPolicies list with workspace filter and the New Policy button
Figure 5. Policies list with workspace filter and the New Policy button

Create / edit policy

Click New Policy to create a policy with:

  • Name — human-readable policy name
  • Workspace ID — scope to a workspace (blank = global)
  • DSL — YAML policy rules, edited in an in-browser YAML editor with syntax highlighting
  • Status — lifecycle status (DRAFT, ACTIVE, SHADOW, ARCHIVED)
Policy editor with the in-browser YAML editorPolicy editor with the in-browser YAML editor
Figure 6. Policy editor with the in-browser YAML editor

In edit mode, the right panel shows:

  • Version History — same diff and rollback functionality as routes
  • Status Changes — change the policy lifecycle status
  • Dry-Run — simulate the policy against a sample request and see the decision plus timing

DSL validation at submit time

The YAML is compiled and validated before the policy is persisted. Authoring mistakes that would have silently produced a no-op rule before now fail the submit immediately with HTTP 400 INVALID_POLICY_DSL and a compiler error pointing at the rule id and offending key:

  • Unknown keys. Anything that isn't a recognised top-level / rule / condition key — including typos like condition: (singular) and non-existent fields like workspace: as a per-rule condition — is rejected with a hint at the correct shape. The DSL is closed-schema; YAML comments use # as usual but extra unknown keys are not silently absorbed.
  • Unknown rule actions. Rules accept exactly two actions: DENY and WARN_AGENT (case-insensitive). Typos like action: warn (silently fell through to DENY before) or action: shadow (a customer reaching for a per-rule shadow that isn't a real product feature) are rejected with a pointer at the policy-lifecycle SHADOW status — see Shadow policy dashboard below.
  • Conflict detection at promotion time. When you flip a policy from DRAFT to ACTIVE (or promote a SHADOW), the conflict detector checks the candidate against every currently-active policy across all nine condition types — model, tools, MCP server/tool/args, data_residency (flags disjoint allowed-region sets that no request can satisfy), time_of_day (flags redundant rules + timezone mismatches on overlapping ranges), max_tokens, and budget_utilization (flags different thresholds firing at different cap utilization levels). Conflicts are returned as warnings on the status-change response; the policy still activates but operators see the overlap.

How platform and workspace policies interleave

Platform-wide policies (no workspace) and a workspace's own policies are merged into one list and sorted by rule priority alone. Scope is not part of the ordering:

  • Lower priority number evaluates first. The first matching DENY returns immediately; WARN_AGENT collects and continues; no match means allowed.
  • A workspace rule can therefore pre-empt a platform rule. A workspace rule at priority 10 is evaluated before a platform rule at priority 50, and if it denies, the platform rule is never reached. Platform scope confers no inherent precedence — if you need a platform rule to win, give it a lower priority number than any workspace rule can use.
  • WARN_AGENT is never "overridden". It is not a decision, so nothing later cancels it; warnings from either scope accumulate and are attached to the response.
Ties across scopes are emergent, not a contract

Two rules with the same priority number are ordered by a stable sort over a list that happens to be built platform-first — so platform currently wins a tie. That is a property of the current implementation, not a documented guarantee. Do not design a hierarchy that depends on it; give rules distinct priorities where the order matters.

CEL expressions for patterns the closed-form matchers can't compose

For patterns the closed-form conditions: matchers above can't express — anything that needs OR, NOT, or arithmetic across fields — a rule can use a per-rule expression: field instead of conditions:. The value is a CEL (Common Expression Language) string compiled and type-checked at submit time. CEL is the sandboxed expression language Kubernetes admission policies, Envoy, Cilium, and gRPC use for the same problem class — no I/O, no host calls, no loops, deterministic, halting-decidable.

rules:
- id: gpt4o-outside-eu-or-large-requests
expression: |
request.model == "gpt-4o" &&
(context.region != "EU" || request.message_count >= 50)
action: DENY
deny_message: "gpt-4o restricted outside EU or for ≥ 50 messages"

That single rule covers a pattern that would otherwise require three separate conditions: rules.

expression: and conditions: are mutually exclusive on the same rule — specifying both fails the submit; specifying neither also fails (a matcher-less rule is almost certainly an authoring mistake). Mixing rule kinds within the same policy is fine — author whichever shape fits each rule.

Evaluation context (closed surface): request.{model, workspace_id, message_count, total_input_chars, metadata[key]}, context.{region, workspace_region}, budget.utilization_pct, plus the CEL standard library (string methods in receiver form — s.startsWith(t), s.contains(t), s.matches(regex) — list/map indexing, in, comprehensions). Two CEL-API notes worth keeping in mind while authoring: (a) every request.* / context.* / budget.* field is declared as Dyn at the type-checker level so the surface can evolve without breaking compiled policies — cast with string(...) before calling string methods (string(request.model).matches("^gpt-.*$"), not matches(request.model, "...")); (b) the embedded CEL library only registers receiver-style overloads for the string methods, so matches(s, regex) fails to compile and s.matches(regex) is the working shape. Temporal functions (now(), hour(ts, tz)) are not in v1 — for time-of-day deny windows use the existing conditions: { time_of_day: ... } matcher.

Static conflict detection is skipped for CEL rules — equivalence between arbitrary CEL expressions is undecidable in general. The promotion endpoint emits one "skipped — verify manually via dry-run" warning per CEL rule on the candidate policy; non-CEL rules in the same policy still get the full 9-detector check above.

Policy editor with dry-run simulation ALLOWED resultPolicy editor with dry-run simulation ALLOWED result
Figure 7. Policy editor with dry-run simulation ALLOWED result

Shadow policy dashboard

When a policy is in SHADOW status, a dedicated shadow dashboard monitors divergence between the shadow and currently-active policy decisions without affecting real traffic.

Accessing the dashboard:

  • From the policies list: click the eye icon next to any SHADOW-status policy
  • From the policy editor: click View Shadow Dashboard in the status panel

Dashboard features:

  • Period Selector — toggle between 1 h, 24 h, and 7 d time windows
  • Stats Panel (auto-refreshes every 5 seconds) — three cards:
    • Total Evaluations — number of times the shadow policy was evaluated
    • Divergent Count — times shadow would have denied when active allowed
    • Divergence Rate — percentage of evaluations that diverged
  • Rule Breakdown Table — divergence counts grouped by rule ID
  • Recent Divergent Events Table — latest divergent events with timestamp, request ID, model, workspace, active decision, and shadow decision
Shadow policy dashboard with stats and divergent eventsShadow policy dashboard with stats and divergent events
Figure 8. Shadow policy dashboard with stats and divergent events
Rollback appends, it does not rewind

Rolling back to version 1 does not return the policy to version 1. It writes version 1's content as a new version — roll back from v2 and you land on v3 carrying v1's rules, with snapshots 1, 2 and 3 all retained.

That is the useful behaviour: the history is append-only, so a rollback is itself auditable and can be rolled back in turn. Verified on 1.7.0.

A policy created outside the Console cannot be rolled back to its first version. The first snapshot is written by the create path, so a policy inserted another way has no version 1 to restore — the rollback redirects with a flash error and changes nothing, which is easy to misread as rollback being broken.

Promoting a shadow policy

Click Promote to Active to transition the policy from SHADOW to ACTIVE status. This:

  1. Runs conflict detection against existing active policies (warnings shown if conflicts are found)
  2. Changes the policy status to ACTIVE
  3. Clears all shadow event data for the policy
The Promote to Active action in the header of a shadow policy's dashboardThe Promote to Active action in the header of a shadow policy's dashboard
Figure 9. Shadow policy edit view with Promote to Active action
"Promote" moves a status, not a workspace

This action promotes a policy from SHADOW to ACTIVE within the workspace it already belongs to. It does not copy anything anywhere. If what you want is author in a development workspace, then apply the same rule in production, that is the next section — there is no button for it.

Promoting a policy to another workspace

There is no in-product action that copies a policy from one workspace to another. The supported route is the GitOps export → edit → import cycle on the Import / Export page, which is also what makes the promotion reviewable — the thing you apply to production is a file you can diff and commit.

  1. Export from /config/export, scoped to the source workspace or fleet-wide.
  2. Edit the policy entry in the exported JSON: give it a new id and set workspaceId to the target workspace. Keep the dsl byte-identical — that is the whole point of the exercise.
  3. Preview the file at /config/import. The diff names each entity it will create, and nothing is written until you apply.
  4. Apply. Use merge mode, which upserts and never deletes.

Trimming the export down to just the policies you are promoting is fine and is the easier file to review:

{
"version": "1.0",
"policies": [
{
"id": "policy-no-expensive-prod",
"workspaceId": "ws-production",
"name": "No expensive model",
"status": "ACTIVE",
"dsl": "version: \"1\"\nrules:\n - id: no-expensive-model\n conditions:\n model:\n denylist: [gpt-4o-expensive]\n action: DENY\n"
}
]
}

Four things worth knowing, all measured on 1.7.0:

  • It is a copy, not a move. The source policy is untouched and stays active in its own workspace — which is what you want when the source is a development workspace you will keep iterating in.
  • The id must be new. Ids are global, not per-workspace, so reusing the source id updates the source policy instead of creating one in the target. That is merge mode doing exactly what it says, and it is the mistake to avoid.
  • status is carried across, so a policy exported as ACTIVE arrives enforcing. Export it as SHADOW instead if you want to observe divergence in the target workspace before it blocks anything.
  • The imported policy gets a proper version-1 snapshot, so version history and rollback work on it normally. (Policies inserted straight into the database — by a seeding script, say — do not get one, and their rollback silently does nothing.)

Allow up to a poll interval before the new policy is enforced; config reaches the data plane through the version poller, not immediately on commit.

Output schemas

Structured-output JSON schemas that clients reference by ID (instead of sending inline on every request) live under Routing → Schemas in the sidebar (owner and policy-admin only). The page lists every registered schema with its scope (route id, model pattern, or both) and provides a create / edit form with a JSON-schema textarea plus a delete action behind a confirm gate. Schemas are also carried in the Console's GitOps export for pipelines that prefer to keep them in source control next to the apps that consume them.

See Structured Outputs → Inline schema vs. registered schema for when to register a schema, and Administering DVARA for the REST endpoints.