> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zespan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Guardrails — content checking for LLM calls

> Enable pre- and post-LLM content checks on any wrapped client to block, redact, or flag unsafe inputs and outputs using your configured guardrail policies.

Guardrails let you run content safety checks before sending a prompt to an LLM (pre-check) and before returning the completion to your application (post-check). Each check is evaluated against the guardrail policies you configure in the Zespan dashboard, and the SDK either allows the call to proceed, applies redaction, or throws a `GuardrailBlockedError`.

## Enabling guardrails

Pass `guardrails: true` (`guardrails=True` in Python) to any wrapper to enable both pre- and post-LLM checks with default settings.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import OpenAI from "openai";
  import { zespan } from "@zespan/sdk";

  zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

  const openai = zespan.wrapOpenAI(new OpenAI(), {
    guardrails: true,
  });
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import os
  import zespan

  zespan.init(api_key=os.environ["ZESPAN_API_KEY"])
  zespan.patch_openai(guardrails=True)

  import openai  # import after patching
  client = openai.OpenAI()
  ```
</CodeGroup>

When guardrails are enabled, all calls through this client run a pre-check on the prompt and a post-check on the completion. If any guardrail blocks the content, a `GuardrailBlockedError` is thrown before the LLM call is made (pre) or before the result is returned (post).

<Note>
  In TypeScript, `wrapOpenAI()` returns a wrapped client instance you call directly. In Python, `patch_openai()` monkey-patches the `openai` module in place — import `openai` (or construct `openai.OpenAI()`) after calling `patch_openai()`, then call it exactly as you would unpatched.
</Note>

## Fine-grained configuration

Pass a configuration object instead of `true`/`True` to control exactly which phases run and how errors in the guardrail service are handled.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  const openai = zespan.wrapOpenAI(new OpenAI(), {
    guardrails: {
      pre: true,          // Check prompt before sending to LLM
      post: true,         // Check completion before returning to app
      failClosed: false,  // If guardrail service errors, allow the call through
    },
  });
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  zespan.patch_openai(
      guardrails={
          "pre": True,            # Check prompt before sending to LLM
          "post": True,           # Check completion before returning to app
          "fail_closed": False,   # If guardrail service errors, allow the call through
      },
  )
  ```
</CodeGroup>

<ParamField body="pre" type="boolean" default="true">
  When `true`, runs a content check on the prompt before the LLM call is made. A block at this phase prevents the LLM from ever receiving the prompt.
</ParamField>

<ParamField body="post" type="boolean" default="true">
  When `true`, runs a content check on the completion before it is returned to your application. A block at this phase prevents unsafe completions from reaching users.
</ParamField>

<ParamField body="failClosed" type="boolean" default="false">
  Controls behavior when the guardrail service itself returns an error (e.g. network timeout, service unavailable). Named `fail_closed` in Python.

  * `false` (default): errors from the guardrail service are swallowed and the LLM call proceeds normally.
  * `true`: errors from the guardrail service are re-thrown, blocking the LLM call. Use this in high-risk applications where you prefer to fail safe.
</ParamField>

## Handling `GuardrailBlockedError`

When a guardrail policy blocks content, the SDK throws (Python: raises) a `GuardrailBlockedError`. Catch it to handle the block gracefully — for example, by returning a fallback response to the user.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import { zespan, GuardrailBlockedError } from "@zespan/sdk";
  import OpenAI from "openai";

  zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

  const openai = zespan.wrapOpenAI(new OpenAI(), {
    guardrails: { pre: true, post: true, failClosed: false },
  });

  async function generateResponse(userMessage: string): Promise<string> {
    try {
      const response = await openai.chat.completions.create({
        model: "gpt-4o",
        messages: [{ role: "user", content: userMessage }],
      });
      return response.choices[0].message.content ?? "";
    } catch (err) {
      if (err instanceof GuardrailBlockedError) {
        console.warn(`Guardrail blocked ${err.phase} content:`, err.results);
        // Return a safe fallback rather than exposing the block reason
        return "I'm sorry, I can't help with that request.";
      }
      throw err;
    }
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import os
  import zespan
  from zespan import GuardrailBlockedError
  import openai

  zespan.init(api_key=os.environ["ZESPAN_API_KEY"])
  zespan.patch_openai(guardrails={"pre": True, "post": True, "fail_closed": False})

  client = openai.OpenAI()

  def generate_response(user_message: str) -> str:
      try:
          response = client.chat.completions.create(
              model="gpt-4o",
              messages=[{"role": "user", "content": user_message}],
          )
          return response.choices[0].message.content or ""
      except GuardrailBlockedError as err:
          print(f"Guardrail blocked {err.phase} content:", err.results)
          # Return a safe fallback rather than exposing the block reason
          return "I'm sorry, I can't help with that request."
  ```
</CodeGroup>

<Warning>
  **Known issue (Python SDK):** the wrapper-guardrails flow above (`patch_openai(guardrails=...)` and the equivalent option on every other `patch_*()` function) does not currently raise `GuardrailBlockedError` when content is blocked. Internally, `run_guardrails_for_phase()` builds each `GuardrailResult` from the dict `ZespanClient.check_guardrails()` returns — but that dict uses the same camelCase keys as the wire API (`guardrailSlug`, `modifiedText`, `latencyMs`), while `GuardrailResult` only accepts snake\_case keyword arguments. Constructing it raises `TypeError: __init__() got an unexpected keyword argument 'guardrailSlug'` before `GuardrailBlockedError` is ever built.

  In practice this means:

  * With the default `fail_closed=False`, that `TypeError` is swallowed by the same broad error handling used for guardrail-service outages, so the check is silently skipped and the LLM call proceeds — blocked content is **not** actually blocked.
  * With `fail_closed=True`, the raw `TypeError` propagates instead of a catchable `GuardrailBlockedError`, so `except GuardrailBlockedError` as shown above will not catch it.

  Until this is fixed, the wrapper-level `guardrails` option does not reliably block content in Python. If you need working pre/post checks today, call [`zespan.check_guardrails()`](#calling-guardrails-directly) directly and branch on `result["allowed"]` yourself — that path returns the raw dict without going through `GuardrailResult` construction, so it isn't affected by this issue.
</Warning>

## `GuardrailBlockedError` properties

Both SDKs expose the same two attributes on the error instance.

<ParamField body="phase" type="string">
  Which phase was blocked: `"pre"` (input check) or `"post"` (output check).
</ParamField>

<ParamField body="results" type="GuardrailResult[]">
  Array (Python: list) of results from individual guardrail policies that ran. Each result describes what the guardrail found and what action it took.
</ParamField>

## `GuardrailResult` fields

Each entry in `error.results` represents one guardrail policy evaluation:

<ParamField body="guardrailSlug" type="string">
  Unique identifier of the guardrail policy that produced this result. Named `guardrail_slug` in Python.
</ParamField>

<ParamField body="passed" type="boolean">
  `true` if this guardrail allowed the content, `false` if it triggered.
</ParamField>

<ParamField body="action" type="string">
  What the guardrail did. One of `"allowed"`, `"blocked"`, `"redacted"`, or `"warned"`.

  * `"allowed"` — content passed without modification
  * `"blocked"` — content was rejected; `GuardrailBlockedError` is thrown
  * `"redacted"` — sensitive content was removed and the modified text is used instead
  * `"warned"` — content was flagged but allowed through
</ParamField>

<ParamField body="reason" type="string">
  Human-readable explanation of why this guardrail triggered, or `null` (Python: `None`) if it did not trigger.
</ParamField>

<ParamField body="modifiedText" type="string">
  The redacted version of the text, or `null` (Python: `None`) if no modification was made. When a guardrail redacts content, the modified text is used in place of the original for the LLM call or returned response. Named `modified_text` in Python.
</ParamField>

## Guardrails on other wrappers

Guardrails work on all provider wrappers. Pass the same `guardrails` option to `wrapAnthropic`, `wrapGoogle`, `wrapGoogleGenAI`, `wrapOpenRouter`, `wrapBedrock`, `wrapMistral`, `wrapLiteLLM`, or the LangChain callback handler.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  // Anthropic with post-only check and fail-closed
  const anthropic = zespan.wrapAnthropic(new Anthropic(), {
    guardrails: { pre: false, post: true, failClosed: true },
  });

  // LangChain with full guardrails
  import { ZespanCallbackHandler } from "@zespan/sdk";
  const handler = new ZespanCallbackHandler({
    guardrails: { pre: true, post: true, failClosed: false },
  });
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  # Anthropic with post-only check and fail-closed
  zespan.patch_anthropic(guardrails={"pre": False, "post": True, "fail_closed": True})

  # LangChain with full guardrails
  from zespan import ZespanCallbackHandler
  handler = ZespanCallbackHandler(
      guardrails={"pre": True, "post": True, "fail_closed": False},
  )
  ```
</CodeGroup>

The Python SDK accepts the same `guardrails` keyword argument on `patch_anthropic`, `patch_google`, `patch_google_genai`, `patch_bedrock`, `patch_mistral`, `patch_litellm`, and `patch_groq`.

<Warning>
  `patch_openrouter()` in the Python SDK does not currently accept a `guardrails` argument — it delegates to `patch_openai()` with no options. If you need guardrails on OpenRouter traffic in Python today, call `patch_openai(guardrails=...)` against the OpenAI-compatible client you use for OpenRouter, or check content with the [direct API](#calling-guardrails-directly) instead.
</Warning>

<Note>
  If you trace multi-agent workflows with [`withAgent`](/sdk/agent-tracing), the agent name and its recent tool calls are automatically included in every guardrail check made inside that block — no extra wiring needed. This lets agent-specific guardrail policies (agent rate limiting, tool misuse, loop detection, scope enforcement, delegation control) evaluate agent behavior alongside standard content checks.

  This automatic agent-context injection is currently TypeScript-only. The Python SDK's `with_agent` context manager traces agent structure but does not thread `agent_name`, `tool_name`, or tool-call history into guardrail checks. If you need agent-aware guardrail evaluation from Python today, pass that context explicitly through the [direct API](#calling-guardrails-directly) — note that the fields covering it (`agent_name`, `tool_name`, `recent_tool_calls`) are TypeScript-only, so cost- and token-based policies are the ones you can drive from Python.
</Note>

<Tip>
  Configure your guardrail policies in the Zespan dashboard under **Settings → Guardrails**. Policies are evaluated server-side, so you can update them without redeploying your application.
</Tip>

## Calling guardrails directly

The `guardrails` wrapper option only checks content that flows through an LLM wrapper call. Sometimes you need to gate something that isn't an LLM call at all — a RAG retrieval step before it's fed into a prompt, a tool result, or any other non-LLM action. For that, call the guardrail check directly instead of going through a wrapper.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import { zespan } from "@zespan/sdk";

  zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

  const result = await zespan.getClient().checkGuardrails({
    text: retrievedChunks.join("\n"),
    phase: "pre",
    traceId: span.traceId,
    spanId: span.spanId,
    operation: "rag-retrieval",
    agentName: "ResearchAgent",
  });

  if (!result.allowed) {
    throw new Error(
      `Retrieval blocked: ${result.results.map((r) => r.reason).join(", ")}`
    );
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import os
  import zespan

  zespan.init(api_key=os.environ["ZESPAN_API_KEY"])

  result = zespan.check_guardrails(
      text="\n".join(retrieved_chunks),
      phase="pre",
      trace_id=span.trace_id,
      span_id=span.span_id,
      operation="rag-retrieval",
  )

  if not result["allowed"]:
      reasons = [r.get("reason") for r in result["results"] if r.get("reason")]
      raise RuntimeError(f"Retrieval blocked: {', '.join(reasons)}")
  ```
</CodeGroup>

<Note>
  Unlike the wrapper-level flow, this direct call never throws `GuardrailBlockedError` itself — it always resolves to a response object. Check `allowed` (`result.allowed` in TypeScript, `result["allowed"]` in Python) yourself and decide what to do: skip the step, raise your own error, log and continue, and so on.
</Note>

### Input fields

<ParamField body="text" type="string" required>
  The content to check.
</ParamField>

<ParamField body="phase" type="&#x22;pre&#x22; | &#x22;post&#x22;" required>
  Which phase this check represents. Purely descriptive at the call site — it's passed straight through to whichever guardrail policies key off phase, and shows up in guardrail event logs.
</ParamField>

<ParamField body="traceId" type="string">
  The trace this check is associated with, for correlating the guardrail event with the rest of the trace in the dashboard. Named `trace_id` in Python.
</ParamField>

<ParamField body="spanId" type="string">
  The span this check is associated with. Named `span_id` in Python.
</ParamField>

<ParamField body="model" type="string">
  The model this check relates to, if any. Some guardrail policies scope their rules to specific models.
</ParamField>

<ParamField body="operation" type="string">
  A label for what kind of call this is (e.g. `"rag-retrieval"`, `"chat"`). Lets you scope guardrail policies to specific operations.
</ParamField>

<ParamField body="estimatedCost" type="number">
  Estimated cost in USD of the operation being checked, in case a guardrail policy enforces a per-call cost ceiling. Named `estimated_cost` in Python.
</ParamField>

<ParamField body="inputTokens" type="number">
  Token count of the input being checked, in case a guardrail policy enforces a token limit. Named `input_tokens` in Python.
</ParamField>

<ParamField body="agentName" type="string">
  TypeScript only. Name of the agent making this call, for agent-scoped policies (agent rate limiting, scope enforcement, delegation control).
</ParamField>

<ParamField body="toolName" type="string">
  TypeScript only. Name of the tool being invoked, for tool-misuse and delegation-control policies that allowlist/blocklist specific tools.
</ParamField>

<ParamField body="recentToolCalls" type="Array<{ toolName: string; args?: string }>">
  TypeScript only. Recent tool calls in this trace (not including the current one), used by loop-detection and tool-misuse policies to spot repeated or excessive calls.
</ParamField>

<ParamField body="fail_open" type="boolean" default="true">
  Python only. When the guardrail service request itself fails (network error, timeout, non-2xx response), `fail_open=True` (default) swallows the error and returns `{"allowed": True, "results": [], "modifiedText": None}`. Set `fail_open=False` to re-raise the underlying exception instead.
</ParamField>

<ParamField body="timeout" type="float" default="5.0">
  Python only. Request timeout in seconds for the guardrail check call.
</ParamField>

### Response shape

Both languages return the same three top-level fields: `allowed` (boolean), `results` (an array/list of per-policy results), and `modifiedText` — the redacted text, or `null`/`None` if nothing was modified.

In TypeScript, `results` is a typed array of [`GuardrailResult`](#guardrailresult-fields) objects using the field names documented above (`guardrailSlug`, `passed`, `action`, `reason`, `modifiedText`).

<Warning>
  In Python, `zespan.check_guardrails()` returns a plain `dict` — `result["allowed"]`, `result["results"]`, `result["modifiedText"]` — and each entry in `result["results"]` is also a raw dict using the **same camelCase keys as the wire format** (`guardrailSlug`, `passed`, `action`, `reason`, `modifiedText`, `latencyMs`), not the snake\_case names used elsewhere in the Python SDK. Use dict-key access (`r["guardrailSlug"]` or `r.get("reason")`) when working with the direct API's response in Python.

  By design, this differs from `GuardrailBlockedError.results` (used in the wrapper flow above), where each entry is meant to be a `GuardrailResult` object with snake\_case attributes (`guardrail_slug`, `modified_text`, etc.) — but see the known-issue warning in [Handling `GuardrailBlockedError`](#handling-guardrailblockederror) above: that conversion currently fails at runtime, so this direct API is the reliable option in Python today.
</Warning>

<Tip>
  Reach for this direct API whenever the content you want checked doesn't pass through a wrapped LLM call — a retrieved document before it's inserted into a prompt, a tool's return value, or a user-supplied value you're about to act on.
</Tip>
