Guardrails
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
A guardrail puts a classifier model in front of the answer. Before a guarded request reaches the model that would normally answer it, every message the model is about to see is judged against one or more guard types. If anything is judged a hit, the answer is produced by a different, protected model instead — the hit model.
The feature has two halves, in two places:
| Where | What it does |
|---|---|
Guardrails (/admin/guardrails) |
The catalog: which guard types exist, what each one detects, and where its hit threshold sits |
| Groups → edit → Guardrails | Activation: whether a group is guarded at all, which guard types apply, and which models classify and answer |
Nothing is guarded until a group turns it on. A guard type sitting active in the catalog does nothing on its own.
Viewing and managing the catalog is gated by the Guardrails admin area — an Owner, or any admin holding a bundle that grants it. Activating guardrails for a specific group is a separate grant, on that group’s own Groups area.
The guard-type catalog
Section titled “The guard-type catalog”Each row is one classifier policy.
| Column | Reads |
|---|---|
| Name | Admin-facing label |
| Key | The stable id a group’s config references — immutable after creation |
| Labels | The severity ladder this guard may return, lowest first |
| Hit From | The first label that counts as a hit |
| Active | Whether groups may select it |
| Updated | Last write |
Two guard types are preloaded:
| Key | Detects | Labels | Hit from |
|---|---|---|---|
ci-data |
Business-sensitive content — customers, internal infrastructure, credentials, unannounced financials, roadmap, personnel data | NONE → LOW → MEDIUM → HIGH |
MEDIUM |
prompt-override |
Attempts to override, exfiltrate, or circumvent the system prompt | SAFE → ATTEMPT |
ATTEMPT |
Preloaded rows can be edited and disabled but not deleted; the Delete button is disabled on them.
Writing a guard type
Section titled “Writing a guard type”-
Press Add Guard Type.
-
Give it a Name and a Key. The key is what group configs store, so it cannot be changed later — pick something short and stable (
ci-data, notci-data-v2-final). -
Write the Policy Text. This is the classifier’s entire system prompt. The preloaded policies follow a four-section shape — Instructions, Definitions, Criteria, Examples — and end with a strict output contract on the last line. Copy that shape: the classifier must return one label and nothing else.
-
List the Labels, comma-separated, lowest severity first. The order is the ladder, so
SAFE, ATTEMPTandNONE, LOW, MEDIUM, HIGHboth work but the sequence matters. -
Pick Hit From Label — the first label that counts as a hit. Everything at or above it routes to the hit model.
-
Leave Active on and save.
Raising the threshold later takes effect immediately, including on verdicts already cached: the label is cached, the hit/miss decision is re-derived from the current ladder on every use.
Switching them on for a group
Section titled “Switching them on for a group”Open a group under Groups and expand Guardrails.
| Field | Meaning |
|---|---|
| Enable guardrails for this group | The master switch |
| Classifier provider | The model that judges content. Fast and cheap is the right trade — it emits one label |
| Hit answer provider | The protected model that answers when anything hits |
| Miss answer provider | The model that answers a clean request. Leave it unset to use the group’s normal model for the mode |
| Active guard types | Checkboxes over the active catalog rows |
Only guard types that are active in the catalog appear here. If the list is empty, add one on Guardrails first.
Images reach the guard one of two ways. Tick Vision on the classifier row and the model reads them itself. Otherwise, if the row names a vision delegate, each image is read by that delegate before the guard runs and the guard judges the description as text — so what the guard scores is what the delegate saw. That is weaker than judging the image directly, and it is still the stronger of the two options available: without it the turn is not classified at all.
If neither route works — no Vision, and no delegate, or a delegate that does not resolve, cannot see either, or fails — the turn fails closed to the hit model and the decision row records unjudgeable with the number of images that caused it. Descriptions are billed to the user as vision:describe_image on the token usage board, exactly as they are on the answering path.
The classifier provider’s Reasoning effort applies to the classification call exactly as you set it on the row — nothing overrides it here. A thinking classifier spends its thinking tokens out of Guardrail verdict max_tokens, so it can run out of budget before it reaches the JSON verdict, and a verdict that does not arrive fails the guard closed onto the hit model. Set the row to None, or point the slot at a model that does not reason; the card says so beneath the picker when the chosen row reasons.
How a request is judged
Section titled “How a request is judged”The classifier judges the union of every message the answer model will receive, not just the newest user turn. Judging only the latest message would let a leak arrive in turn one and be answered in turn five.
- Assistant turns are not classified — the user turn that prompted them already was.
- Identical text appearing in several turns is judged once, keyed by content hash.
- A verdict is cached per (guard type, policy, text) with a sliding TTL of
AiProxy:GuardrailVerdictCacheTtlHours, default 24 hours. Steady state is roughly one classifier call per turn. - Once any message already in the history is a known hit, the route is pinned and the new turn is not classified at all. Set
AiProxy:GuardrailShortCircuitOnHittofalseto keep classifying anyway, at one extra call per turn.
Outcomes
Section titled “Outcomes”| Outcome | When | Answered by |
|---|---|---|
MISS |
Every judged unit came back below the hit threshold | Miss model |
HIT |
At least one unit hit at least one guard | Hit model |
ERROR |
A verdict was malformed, or the request could not be judged at all | Hit model |
| BYPASS | The chat came from a private-type persona, the group maps that type to its own provider, and the bypass switch is on | The mapped provider, without classifying |
ERROR outranks HIT, and both route to the hit model — the whole design fails closed. Two cases short-circuit to ERROR before any classifier call:
- A forwarded message contains a non-text part the classifier cannot handle — an attachment that isn’t an image, always; an image, only when the classifier provider does not have Vision turned on (see below).
- The gate itself throws while resolving the config.
A separate failure mode refuses the request outright with 503 Guardrails are enabled for your group but misconfigured (classifier or hit model missing): the configured classifier or hit provider is not resolvable in the provider catalog, or a selected guard’s label set is invalid. Refusing beats answering unguarded.
Judging images
Section titled “Judging images”A guarded turn that carries an image is judged, not failed closed, when the classifier provider has Vision turned on (the same capability switch a normal answer model uses). The image is forwarded to the classifier exactly as the client sent it; anything else non-text — a PDF/file attachment, or any shape the client didn’t send as an image_url part — still fails closed to ERROR regardless of the classifier’s capabilities.
The private-persona fast path
Section titled “The private-persona fast path”Two settings together take private-persona chats off the classifier: a Persona type routing entry for private on the group page, and the Let private personas answer unclassified switch in this group’s Guardrails section. The switch is off by default and does nothing on its own; so does the mapping. With both, those Assistant chats answer on the mapped provider and are marked protected, exactly as a hit would be — you have named the model private work belongs on and said it may answer without being judged first.
This holds even when the classifier is misconfigured: a request that never needed one is not refused for it. It does not hold when the mapped provider stops resolving — a deleted or disabled row means the chat falls back through the normal routing cascade and is classified like any other.
Nothing else changes. Only the literal type private fast-paths; general, fast, code and any word you invent keep being classified, switch or no switch.
What the user sees
Section titled “What the user sees”A protected answer carries a neutral marker — {"guardrail":{"protected":true}}, on the streamed chunk or the buffered envelope — and nothing else. Not the outcome, not which guard fired, not which model answered. The desktop client turns that into a badge on the message. A clean miss is not marked.
Reading the decision log
Section titled “Reading the decision log”/admin/guardrail-decisions — the Guardrails tab of the Audit log. One row per guarded request, newest first, capped at the 200 most recent. Filter by outcome, or search across user, mode, and provider ids.
| Column | Reads |
|---|---|
| Outcome | HIT / MISS / ERROR, plus a Sensitive KB pill where it applies |
| Guards hit | Hits over decisive verdicts for that request |
| Answered by | The provider id that produced the answer |
| Classifier | The provider id that judged it |
| Latency | Classifier round-trip in milliseconds |
The row’s info button opens the per-guard verdicts: each guard key with its label, whether it hit, whether the reply was malformed, the finish reason, and the rationale. A short-circuited request records the prior hit’s content hash as its rationale.
What the decision log stores
Section titled “What the decision log stores”Judged content is not persisted, and there is no setting that changes that. Each row keeps a SHA-256 hash of the decisive content — enough to correlate two requests that carried the same text, not enough to read it.
Earlier versions offered a per-group Log a plaintext content preview option, which stored the first 200 characters of the decisive content and the classifier’s raw reply on malformed verdicts. It is gone: the content a data-leakage guard has just flagged is precisely what should not sit in a queryable table, and an opt-in that has to be switched off again is a control that outlives the investigation it was enabled for. The raw reply is no longer written to the debug log either — a log sink is still somewhere the content lands.
A stale LogContentPreview left in a group’s settings JSON from an older release is ignored, and the ContentPreview column is dropped by the DropGuardrailContentPreview migration, which also discards whatever it held.
Diagnosing a malformed verdict without the reply
Section titled “Diagnosing a malformed verdict without the reply”The row still tells you what you need:
| Signal | What it says |
|---|---|
malformed |
The reply did not parse into a verdict |
| finish reason | length means the verdict was truncated — raise AiProxy:GuardrailVerdictMaxTokens |
| reply length | How many characters came back. A long reply with finishReason=stop is reasoning prose, the signature of a reasoning classifier model; zero means nothing came back at all |
| reply shape | Which form the reply took — see the table below. A category, never a quotation |
| image count | How many images the judged turn carried. A vision-capable classifier is handed them, and a model that will not judge an image declines instead of classifying |
| classifier error | Present only when the call itself failed. The exception type and the upstream HTTP status (HttpRequestException(500)), never a message — a bare status cannot echo the content the guard was inspecting, and it separates an upstream rejection from a timeout or format drift |
The reply shape is what turns a malformed row into an action, without anyone having to store what the model said:
| Shape | What happened | What to change |
|---|---|---|
refusal-no-content |
The model declined: the reply carried a refusal instead of a verdict, in a field that is not the verdict | Check whether those turns carry images or material this model will not process; consider a classifier model that will |
content-null, content-empty |
Nothing came back | With finishReason=length, raise Guardrail verdict max_tokens; with stop, the upstream returned an empty message |
prose-no-json |
The model answered instead of classifying | A non-reasoning classifier model, or a stricter output contract in the guard policy |
json-without-label-key |
JSON, keyed on something other than label |
Align the guard policy’s output contract with the shape the parser reads |
label-not-double-quoted |
A label named in single quotes, or bare | As above |
json-unparseable |
A quoted label inside JSON that would not parse | Usually an unescaped quote inside the rationale |
If malformed verdicts recur, the shape says which fix applies — a non-reasoning classifier model for the prose cases, a different model or fewer images for the refusals — rather than a look at the reply.
A row carrying a classifier error with no finish reason never received a reply at all — the call itself failed, so the truncation remedy above does not apply to it. HttpRequestException(500) against a reasoning-capable classifier model is the signature of an upstream that refuses to answer inside AiProxy:GuardrailVerdictMaxTokens because the model spent that budget thinking. Every turn then fails closed to the hit model, which looks normal to the user, so the decision log is where this surfaces. Raise it from Guardrail verdict max_tokens in the AI Payload Caps section of Limits & Policies — it takes effect without a restart, and the value is clamped to 64–8192 whatever you type. That buys headroom rather than a fix, though: the durable answers are a non-reasoning classifier model, or a provider whose reasoning off-switch that upstream actually honours — see AI providers.
What the rows still say about a person
Section titled “What the rows still say about a person”No content does not mean no personal data. Each row keeps UserId, CreatedAt, the mode and the outcome — so the table is a record of when someone triggered a guard. Over a year that is a behavioural history, without a word of the text.
That is what bounds it: Guardrails:RetentionDays (default 365) deletes rows older than the window, swept every six hours (see Cost and growth). Setting it to 0 keeps rows forever — under the GDPR that is a decision you have to be able to justify, not a default to leave alone.
Cost and growth
Section titled “Cost and growth”Classifier spend is charged to the user, and shows up on the token usage board as a template id of guardrail:<guard-key> — one row per guard per request, so a guard type’s cost can be read off directly. Only calls that actually classified are charged; a cached verdict costs nothing.
Decision rows are pruned by Guardrails:RetentionDays (default 365), on a six-hourly background sweep. 0 disables it and keeps every row forever. Audit:RetentionDays does not reach this table, and the wipe actions on Settings → Data still exclude it — the retention key is the only thing that reclaims the space without a manual DELETE.