Guardrails
Enforce guardrails around your model calls from the SDK.
Guardrails
Guardrails are safety and policy rules that act on your model calls — redacting sensitive text, returning a safe response, throwing an error, or re-asking the model. You author, validate, and promote them in the platform; the SDK enforces the live rules in your application.
For what guardrails are, how to author them, and how they behave, see the Guardrails guide in the platform documentation.
Before you start
Enforcement only applies guardrails that already exist and are live — so create one first, either way you prefer:
- In the platform — author it from the evaluator library, validate it against sample content, and promote it to live. See Authoring a guardrail and Lifecycle: draft → validate → promote.
- From code — create and update guardrails with the SDK. See Manage Guardrails as Code. Note that the SDK only ever produces drafts; promoting a draft to live stays a platform action.
Setup
Guardrails need SDK credentials to resolve rules. How you initialise depends on which surface you use:
- Automatic enforcement on every auto-traced model call — initialise the SDK and bind the live guardrail names under
guardrails. This is the only surface that requires init. - Manual
check/enforce, and guardrails-as-code CRUD — these work offAISDK_PUBLIC_KEY/AISDK_SECRET_KEYin the environment, so a plain init (or no init at all) is enough.
import { Observe } from '@browserstack/ai-sdk';
await Observe.init({
publicKey: process.env.AISDK_PUBLIC_KEY,
secretKey: process.env.AISDK_SECRET_KEY,
guardrails: {
// Names of LIVE guardrails enforced on every auto-traced model call.
rules: ['pii-redact', 'jailbreak-judge'],
},
});
// Import your LLM provider AFTER init so its calls are patched and guarded
import OpenAI from 'openai';import os
from browserstack_ai_sdk import Observe
Observe.init(
public_key=os.environ["AISDK_PUBLIC_KEY"],
secret_key=os.environ["AISDK_SECRET_KEY"],
guardrails={
# Names of LIVE guardrails enforced on every auto-traced model call.
"rules": ["pii-redact", "jailbreak-judge"],
},
)
# Import your LLM provider AFTER Observe.init() so its calls are patched
import openaiimport com.browserstack.aisdk.AISDK;
AISDK sdk = AISDK.builder()
.publicKey(System.getenv("AISDK_PUBLIC_KEY"))
.secretKey(System.getenv("AISDK_SECRET_KEY"))
.guardrails("pii-redact", "jailbreak-judge")
.build();
sdk.observe().init();import (
"context"
"os"
aisdk "github.com/browserstack/ai-sdk-go"
)
sdk, err := aisdk.New(
aisdk.WithCredentials(os.Getenv("AISDK_PUBLIC_KEY"), os.Getenv("AISDK_SECRET_KEY")),
aisdk.WithGuardrails("pii-redact", "jailbreak-judge"),
)
if err != nil {
return err
}
if err := sdk.InitObserve(context.Background()); err != nil {
return err
}Bound names must be guardrails that are live on the platform — a draft never enforces. Rules are fetched by name (60s cache), enforced in-process, and every failure fails open.
Binding per agent
Instead of one global list, rules also takes a map of agent name → guardrail names, with "*" for the global set. At a seam, the active agent's set ∪ the global set applies.
await Observe.init({
guardrails: {
rules: {
'*': ['pii-redact'],
'support-agent': ['jailbreak-judge'],
},
},
});In Go, use aisdk.WithGuardrailMap(map[string][]string{...}).
Configuration
Every knob is optional. The enforcement-tuning knobs each have an AISDK_GUARDRAILS_* environment fallback.
| Option | Env var | Default | What it does |
|---|---|---|---|
rules | — | none | Guardrail binding (list, or agent-wise map). Empty keeps enforcement off with zero overhead. |
enabled | AISDK_GUARDRAILS_ENABLED | true | Global on/off for all guardrails, auto and manual. Flip at runtime with Observe.setGuardrailsStatus(status). |
maxPayloadChars | AISDK_GUARDRAILS_MAX_PAYLOAD_CHARS | 256000 | Max characters scanned by deterministic rules; larger inputs fail open (payload_too_large). LLM-judge rules are unaffected. |
timeoutMs | AISDK_GUARDRAILS_TIMEOUT_MS | server value, else 2000 | Per-rule fail-open timeout. Overrides the server-supplied per-rule timeout. |
seamBudgetMs | AISDK_GUARDRAILS_SEAM_BUDGET_MS | 10000 | Wall-clock budget per hook point per call across all rules. Rules still pending fail open (budget_breach). |
Python uses snake_case keys (max_payload_chars, timeout_ms, seam_budget_ms); Go uses the matching aisdk.WithGuardrails* options.
SDK surfaces
This section covers the SDK surface:
Manual Enforcement
Run guardrails explicitly over your own content with check / enforce.
Manage Guardrails as Code
Author, fetch, list, update, and archive guardrails from code.
When a re-ask fails
A reask rule discards the model's flagged reply and re-asks the model once, with the same request. Usually the second reply is clean and your call returns it, and there is nothing to handle.
Behaviour change — a live reask rule now needs a catch. When the re-ask has actually run and still cannot produce a usable reply, the SDK raises instead of quietly returning the rule's substitute text as though the model had written it. Before this release a reask always returned a response object, so this path has never thrown in your code. Only re-ask-after-re-ask changes; see What has not changed.
Once the retry has run, two things can stop it producing an answer, and they surface differently:
| What went wrong on the retry | What your call site gets |
|---|---|
| The retry's own model call failed — the provider errored on dispatch, or its response could not be drained | The provider's error, verbatim. The SDK does not wrap, replace, or swallow it, so your existing provider-error handling applies unchanged. |
| The retry's reply tripped the same rule again — or, on a Java stream, its reply could never be read at all | A guardrail blocked error, with action set to reask. The rule's substitute text rides along on the error, so you can still show the author's wording — you just have to choose to. |
The substitute is no longer delivered on its own here because doing so hands your caller a guardrail's sentence dressed as the model's answer, with nothing in the response to say the model never wrote it. Putting it on the error keeps the wording available and makes the failure visible.
Catching it
The error type is the same one enforce raises — see Manual Enforcement. Check action to tell a failed re-ask apart from an error_4xx block.
import { GuardrailBlockedError } from '@browserstack/ai-sdk';
try {
const res = await openai.chat.completions.create({ model, messages });
return res.choices[0].message.content;
} catch (err) {
if (err instanceof GuardrailBlockedError && err.action === 'reask') {
// Asked twice, still blocked. `defaultResponse` is the rule's authored text.
return err.defaultResponse;
}
// Provider errors from the retry arrive here untouched — handle them as before.
throw err;
}from browserstack_ai_sdk import GuardrailBlockedError
try:
res = client.chat.completions.create(model=model, messages=messages)
return res.choices[0].message.content
except GuardrailBlockedError as err:
if err.action == "reask":
# Asked twice, still blocked. `default_response` is the rule's authored text.
return err.default_response
raise
# Provider errors from the retry propagate untouched — handle them as before.import com.browserstack.aisdk.guardrails.GuardrailBlockedException;
try {
return openai.chat().completions().create(params)
.choices().get(0).message().content();
} catch (GuardrailBlockedException blocked) {
if ("reask".equals(blocked.getAction())) {
// Asked twice, still blocked. getDefaultResponse() is the rule's authored text.
return blocked.getDefaultResponse();
}
throw blocked;
}
// Provider errors from the retry propagate untouched — handle them as before.// Nothing to change. The Go SDK does not re-dispatch a `reask` on any surface,
// so no retry ever runs and this behaviour change cannot reach it.What has not changed
The change reaches only a call the SDK actually asked twice. Everything below behaves exactly as it did before:
- A surface with no second attempt available. Where the SDK has no re-dispatch path for a call, a
reasknever runs a retry, so it still collapses straight to the rule's substitute text. Re-dispatch is wired for the auto-traced OpenAI, Anthropic, and Google Gemini surfaces in the Node, Python, and Java SDKs; the Go SDK does not re-dispatch anywhere. - The first violation. Attempt 1 still records its
reaskdecision and never throws — triggering the retry is all it does. - A
reaskon abefore_modelhook point. There is no earlier reply to discard, so the call simply proceeds with the original input. - Every other action.
redactstill delivers masked text,default_responsestill delivers its authored body, anderror_4xxstill throws — including when they fire on the retry's reply.