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

# Guardrails

> Configure banned-words, prompt-injection, and leakage guardrails, test them before you rely on them, and detect guardrail refusals in your code.

Guardrails are content policies that the MKA1 API enforces on your LLM traffic.
The gateway checks user text before the model runs and can optionally check the model's assembled output.
They apply to the Responses API, agent runs, and the Chat Completions compatibility endpoint.
Batch requests do not currently run these checks.

A guardrail block is not an HTTP error.
For the Responses API, the terminal response has `status: "completed"` and carries a refusal instead of generated text.
`output_text` is absent on a blocked response, so code that reads only `output_text` sees an empty answer and cannot tell a guardrail block apart from a model that said nothing.
If your org enforces guardrails, start with [Detect a block in code](#detect-a-block-in-code).

## How guardrails are applied

Two policies can apply to a request: the org-wide policy and the policy of the caller's team.
When both exist, both are enforced.
A team policy adds rules on top of the org-wide policy; it never replaces or weakens it.
If both policies match, the org-wide rule's rejection message takes precedence.
Policies apply to everyone in the scope; changing `X-On-Behalf-Of` does not create a separate policy for an end user.

Each guardrail always checks request input while it is enabled.
Checking model output is a separate opt-in per guardrail (see [Check model output too](#check-model-output-too)).
The input check reads a string `input` or user-message text. It does not inspect image, audio, or file contents.
For the additional check before an auto-routing judge sees request evidence, see [Auto routing](/docs/auto-routing).

<Note>
  API keys need the `read:guardrails` scope to read or test guardrails and the `write:guardrails` scope to change them.
  These scopes govern the policy endpoints; normal response requests are checked against the effective policy even if the key cannot manage guardrails.
</Note>

## Guardrail modes

| Mode               | Blocks                                                                 | Config fields                                         |
| ------------------ | ---------------------------------------------------------------------- | ----------------------------------------------------- |
| `ban_words`        | Case-insensitive substring matches for any word or phrase on your list | `words` (default `[]`), `rejection_message`           |
| `prompt_injection` | Text that matches prompt-injection detection patterns                  | `threshold` (0-1, default `0.7`), `rejection_message` |
| `leakage`          | Text that matches system-prompt extraction patterns                    | `threshold` (0-1, default `0.7`), `rejection_message` |

Every guardrail also takes two fields outside `config`:

* `enabled` (default `true`) turns the guardrail on or off without removing it from the policy.
* `check_output` (off unless set) additionally applies the guardrail to model output.

Each mode has a default `rejection_message`.
Set your own to control exactly what a blocked caller reads back.
For example, banning `secret` also matches `secrets`; matching is not limited to whole words.

Prompt-injection and leakage checks use pattern detection, not a comparison against your actual system prompt.
Checks fail open: a policy lookup or evaluation error lets the request continue. A missing or empty policy also passes.

## Configure guardrails

`PUT /api/v1/llm/guardrails` replaces a policy, so include every guardrail you want to keep.
Omit `team_id` or pass `null` to set the org-wide policy (organization admin or owner only).
Pass a `team_id` to set that team's additional policy. With the required scope, team members can manage their own team's policy; organization admins can manage any team in their organization.
Rules that team members must not be able to remove belong in the org-wide policy.

### Before you start

Use an API key with `read:guardrails`, `write:guardrails`, and `write:responses` for the examples below, and a model available to your organization.
Use a dedicated test team, replace `<team-id>` with its ID, and make the test and response calls with a key belonging to that team.
Save the team's existing row from [`GET /api/v1/llm/guardrails/policies`](/api-reference/guardrails/list-guardrails-policies) before replacing it; the effective set from `GET /guardrails` also includes org-wide rules and is not a team-policy backup.

This example bans a word on both input and output, with a custom rejection message, and enables input leakage detection:

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  import { SDK } from '@meetkai/mka1';

  const mka1 = new SDK({
    bearerAuth: `Bearer ${YOUR_API_KEY}`,
  });

  const policy = await mka1.guardrails.updateGuardrails({
    updateGuardrailsRequest: {
      teamId: '<team-id>',
      guardrails: [
        {
          mode: 'ban_words',
          enabled: true,
          checkOutput: true,
          config: {
            words: ['confidential'],
            rejectionMessage: 'This request mentions restricted material. Please rephrase.',
          },
        },
        {
          mode: 'leakage',
          enabled: true,
        },
      ],
    },
  });
  ```

  ```csharp C# SDK theme={null}
  using MeetKai.MKA1;
  using MeetKai.MKA1.Types.Components;
  using System.Collections.Generic;

  var sdk = new SDK(bearerAuth: "Bearer YOUR_API_KEY");

  var policy = await sdk.Guardrails.UpdateGuardrailsAsync(body: new UpdateGuardrailsRequest()
  {
      TeamId = "<team-id>",
      Guardrails = new List<GuardrailConfig>()
      {
          GuardrailConfig.CreateBanWords(new BanWordsGuardrail()
          {
              CheckOutput = true,
              Config = new BanWordsConfig()
              {
                  Words = new List<string>() { "confidential" },
                  RejectionMessage = "This request mentions restricted material. Please rephrase.",
              },
          }),
          GuardrailConfig.CreateLeakage(new LeakageGuardrail()),
      },
  });
  ```

  ```python Python SDK theme={null}
  from meetkai_mka1 import SDK

  sdk = SDK(bearer_auth="Bearer YOUR_API_KEY")

  policy = sdk.guardrails.update_guardrails(
      team_id="<team-id>",
      guardrails=[
          {
              "mode": "ban_words",
              "enabled": True,
              "check_output": True,
              "config": {
                  "words": ["confidential"],
                  "rejection_message": "This request mentions restricted material. Please rephrase.",
              },
          },
          {
              "mode": "leakage",
              "enabled": True,
          },
      ],
  )
  ```

  ```bash bash theme={null}
  curl https://apigw.mka1.com/api/v1/llm/guardrails \
    --request PUT \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --data '{
      "team_id": "<team-id>",
      "guardrails": [
        {
          "mode": "ban_words",
          "enabled": true,
          "check_output": true,
          "config": {
            "words": ["confidential"],
            "rejection_message": "This request mentions restricted material. Please rephrase."
          }
        },
        {
          "mode": "leakage",
          "enabled": true
        }
      ]
    }'
  ```
</CodeGroup>

To see the combined policy that applies to your requests, call [`GET /api/v1/llm/guardrails`](/api-reference/guardrails/get-effective-guardrails) or run `mka1 guardrails get`.
To see stored policies per scope, use [`GET /api/v1/llm/guardrails/policies`](/api-reference/guardrails/list-guardrails-policies).
Organization admins see all policies in their organization; other callers see the org-wide policy and their own team's policy.
When you finish testing, restore the saved team policy with `PUT`, or delete the team's policy with `DELETE /api/v1/llm/guardrails?team_id=<team-id>` if none existed before. Deleting a team policy leaves org-wide rules enforced.

### Check model output too

Set `check_output: true` on a guardrail to also run it over the model's generated text after the response is assembled.
This catches content the model produces on its own, such as a completion that repeats a banned term the user never typed.
It also covers Responses API output that would otherwise finish as `incomplete`, such as when a token limit is reached. A block replaces that result with a `completed` refusal and preserves the real usage.

<Warning>
  Live streaming text can reach your client before the output check runs. If you must withhold unchecked output, buffer it until the terminal event or use a non-streaming request. See [Streaming](#streaming).
</Warning>

Output checking is opt-in per guardrail, so enabling a guardrail never silently starts blocking completions your users already see.
It also bills differently: the model has already generated the tokens by the time the check runs, so a response blocked at the output stage still bills those tokens (see [Usage and billing](#usage-and-billing)).

## Test your policy

`POST /api/v1/llm/guardrails/test` evaluates a piece of content against your effective guardrails without calling a model.
Use it to validate a policy change before your traffic depends on it.
This endpoint uses the caller's org and team; it has no `team_id` selector. It evaluates all enabled rules, not only those with `check_output` enabled.

<CodeGroup>
  ```bash CLI theme={null}
  mka1 guardrails test --content 'Summarize this confidential roadmap.'
  ```

  ```ts MKA1 SDK theme={null}
  const report = await mka1.guardrails.testGuardrails({
    testGuardrailsRequest: {
      content: 'Summarize this confidential roadmap.',
    },
  });

  console.log(report.passed, report.triggeredGuardrail);
  ```

  ```csharp C# SDK theme={null}
  var report = await sdk.Guardrails.TestGuardrailsAsync(body: new TestGuardrailsRequest()
  {
      Content = "Summarize this confidential roadmap.",
  });
  ```

  ```python Python SDK theme={null}
  report = sdk.guardrails.test_guardrails(content="Summarize this confidential roadmap.")
  ```

  ```bash bash theme={null}
  curl https://apigw.mka1.com/api/v1/llm/guardrails/test \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --data '{
      "content": "Summarize this confidential roadmap."
    }'
  ```
</CodeGroup>

The result reports whether the content passed, and on a failure it names the guardrail and the message a real request would get back:

```json theme={null}
{
  "object": "guardrails_test_result",
  "passed": false,
  "triggered_guardrail": "ban_words",
  "rejection_message": "This request mentions restricted material. Please rephrase.",
  "user_input": "Summarize this confidential roadmap.",
  "details": { "matched_word": "confidential" }
}
```

`details` can explain why the guardrail fired: `matched_word` for `ban_words`, and a `score` or `reason` when provided by `prompt_injection` and `leakage` detection.

## What a blocked response looks like

For a foreground Responses API request, a guardrail block returns HTTP 200 with `status: "completed"` and `error: null`.
For a background request, inspect the terminal response after polling or streaming; the initial response can still be `queued` or `in_progress`.
The block is visible in three places instead:

* `metadata.guardrail_triggered` holds the mode of the guardrail that fired (`ban_words`, `prompt_injection`, or `leakage`).
* `output` contains a single assistant message whose only content part has `type: "refusal"`, carrying the guardrail's rejection message.
* `usage` is all zeros for an input block, because no model ran.

Here is a blocked response to the request from the examples above, trimmed to the relevant fields:

```json theme={null}
{
  "id": "resp_a1b2c3d4e5f6",
  "object": "response",
  "status": "completed",
  "error": null,
  "model": "auto",
  "output": [
    {
      "id": "msg_f6e5d4c3b2a1",
      "type": "message",
      "role": "assistant",
      "status": "completed",
      "content": [
        {
          "type": "refusal",
          "refusal": "This request mentions restricted material. Please rephrase."
        }
      ]
    }
  ],
  "metadata": {
    "guardrail_triggered": "ban_words"
  },
  "usage": {
    "input_tokens": 0,
    "input_tokens_details": { "cached_tokens": 0 },
    "output_tokens": 0,
    "output_tokens_details": { "reasoning_tokens": 0 },
    "total_tokens": 0
  }
}
```

<Warning>
  There is no `output_text` field in a blocked response.
  Text-aggregation helpers such as the OpenAI SDK's `response.output_text` have no text content parts to join; depending on the SDK, text is empty or absent.
  Code that renders only `output_text` shows a blocked user a blank answer with no explanation.
</Warning>

## Detect a block in code

Read the `refusal` content part to show the caller a rejection message, and use `metadata.guardrail_triggered` to identify a platform guardrail block.
The SDK clients below use the same credentials as the configuration example. For the OpenAI SDK, initialize `openai` with your MKA1 API key and `baseURL: 'https://apigw.mka1.com/api/v1/llm/'`, as shown in [Generate a response](/docs/generate-a-response#send-a-simple-prompt).

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const result = await mka1.llm.responses.create({
    responsesCreateRequest: {
      model: 'auto',
      input: 'Summarize this confidential roadmap.',
      stream: false,
    },
  });

  // This SDK method returns a response-or-stream union.
  if (!('output' in result)) throw new Error('Expected a non-streaming response');
  const triggered = result.metadata.guardrail_triggered;
  const refusal = result.output
    .flatMap((item) => item.type === 'message' && 'content' in item && Array.isArray(item.content)
      ? item.content.filter((part) => typeof part !== 'string' && part.type === 'refusal')
      : [])
    .at(0);
  if (refusal) {
    console.log(refusal.refusal);
    if (triggered) console.log(`Guardrail: ${triggered}`);
  } else {
    console.log(result.outputText);
  }
  ```

  ```ts OpenAI SDK theme={null}
  const response = await openai.responses.create({
    model: 'auto',
    input: 'Summarize this confidential roadmap.',
    stream: false,
  });

  const triggered = response.metadata?.guardrail_triggered;
  const refusal = response.output
    .flatMap((item) => item.type === 'message' ? item.content : [])
    .find((part) => part.type === 'refusal');
  if (refusal) {
    console.log(refusal.refusal);
    if (triggered) console.log(`Guardrail: ${triggered}`);
  } else {
    console.log(response.output_text);
  }
  ```

  ```csharp C# SDK theme={null}
  using System.Linq;

  var response = await sdk.Llm.Responses.CreateAsync(new ResponsesCreateRequest()
  {
      Model = "auto",
      Input = ResponsesCreateRequestInput.CreateStr("Summarize this confidential roadmap."),
      Stream = false,
  });
  var res = response.ResponseObject
      ?? throw new InvalidOperationException("Expected a non-streaming response");

  var refusal = res.Output
      .SelectMany(item =>
          item.OutputMessage?.Content.Select(part => part.Refusal?.RefusalValue)
          ?? item.InputMessage?.Content.ArrayOfInputMessageContent?.Select(part => part.Refusal?.RefusalValue)
          ?? Enumerable.Empty<string?>())
      .FirstOrDefault(value => value != null);
  if (refusal != null)
  {
      Console.WriteLine(refusal);
      if (res.Metadata.TryGetValue("guardrail_triggered", out var triggered))
          Console.WriteLine($"Guardrail: {triggered}");
  }
  else
  {
      Console.WriteLine(res.OutputText);
  }
  ```

  ```python Python SDK theme={null}
  res = sdk.llm.responses.create(
      model="auto",
      input="Summarize this confidential roadmap.",
  )

  triggered = res.metadata.get("guardrail_triggered")
  refusal = next(
      (part.refusal for item in res.output
       if item.type == "message" and isinstance(item.content, list)
       for part in item.content if getattr(part, "type", None) == "refusal"),
      None,
  )
  if refusal is not None:
      print(refusal)
      if triggered:
          print(f"Guardrail: {triggered}")
  else:
      print(res.output_text)
  ```

  ```bash bash theme={null}
  curl https://apigw.mka1.com/api/v1/llm/responses \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --data '{
      "model": "auto",
      "input": "Summarize this confidential roadmap."
    }' | jq '([.output[] | select(.type == "message") | .content[]
      | select(.type == "refusal") | .refusal] | first) as $refusal
      | if $refusal != null
      then { blocked_by: .metadata.guardrail_triggered, message: $refusal }
      else { text: .output_text } end'
  ```
</CodeGroup>

The refusal content part also appears when a model declines a request on its own, without a guardrail.
Read refusal parts before falling back to `output_text`; use `metadata.guardrail_triggered` to label a platform guardrail block.
There is no top-level `response.refusal` field in the current MKA1 API.

## Streaming

For a foreground input-stage block, the guardrail path emits `response.created` and `response.completed`, each carrying the full blocked response object. Background streams can include earlier lifecycle events.
No `response.output_text.delta` events arrive, and there are no per-token refusal events.
A consumer that only listens for text deltas renders nothing, so handle the terminal event:

The following examples wait for the terminal response before displaying text, so output-stage blocks cannot expose a partial answer through this display code.
Applications that render deltas immediately must replace that partial answer if the terminal response contains a refusal.

<CodeGroup>
  ```ts OpenAI SDK theme={null}
  const stream = await openai.responses.create({
    model: 'auto',
    input: 'Summarize this confidential roadmap.',
    stream: true,
  });

  for await (const event of stream) {
    if (event.type === 'response.completed' || event.type === 'response.incomplete') {
      const parts = event.response.output.flatMap((item) =>
        item.type === 'message' ? item.content : []);
      const refusal = parts.find((part) => part.type === 'refusal');
      const text = parts.flatMap((part) => part.type === 'output_text' ? [part.text] : []).join('');
      console.log(refusal ? refusal.refusal : text);
    } else if (event.type === 'response.failed') {
      console.error(event.response.error?.message ?? 'Response failed');
    }
  }
  ```

  ```python Python SDK theme={null}
  stream = sdk.llm.responses.create(
      model="auto",
      input="Summarize this confidential roadmap.",
      stream=True,
  )

  for event in stream:
      if event.data.type in ("response.completed", "response.incomplete"):
          parts = [part for item in event.data.response.output
                   if item.type == "message" and isinstance(item.content, list)
                   for part in item.content]
          refusal = next((p.refusal for p in parts if getattr(p, "type", None) == "refusal"), None)
          text = "".join(p.text for p in parts if getattr(p, "type", None) == "output_text")
          print(refusal if refusal is not None else text)
      elif event.data.type == "response.failed":
          print(event.data.response.error)
  ```
</CodeGroup>

Output-stage blocks (`check_output`) behave differently under streaming.
The text deltas have already been sent by the time the output check runs, so they are not recalled.
The `response.completed` event and, when stored, the retrieved response carry the refusal in place of the generated text. Replace any displayed partial answer with that refusal; bytes already sent to the client cannot be recalled.

## Usage and billing

* An input-stage block reports `usage` with every count at zero, and nothing is billed. The request never reached a model.
* An output-stage block (`check_output`) keeps the real token counts in `usage`, and those tokens are billed. The model generated the text before the check rejected it.

## Chat Completions requests

The Chat Completions compatibility endpoint signals blocks in its own dialect.
A blocked request returns HTTP 200 with `finish_reason: "content_filter"` and the rejection message in `message.refusal` instead of `message.content`:

```json theme={null}
{
  "id": "chatcmpl-a1b2c3d4e5f6",
  "object": "chat.completion",
  "created": 1756150000,
  "model": "auto",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "refusal": "This request mentions restricted material. Please rephrase."
      },
      "finish_reason": "content_filter"
    }
  ],
  "usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0
  }
}
```

The usage rules match the Responses API: zeros for an input block, real token counts for an output-stage block.
On a streamed chat completion, an output-stage block appends a final chunk with `finish_reason: "content_filter"` and the rejection message in `delta.refusal` after the text deltas.

Chat Completions cache hits are checked against the current output policy before replay. If cached text is blocked, it is withheld entirely, including for cached streams. Newly generated streams that trigger an output check are not cached.

## Next steps

* Review the [Guardrails endpoints](/api-reference/guardrails/get-effective-guardrails) in the API reference, including [update](/api-reference/guardrails/update-guardrails-policy), [delete](/api-reference/guardrails/delete-guardrails-policy), and [test](/api-reference/guardrails/test-content-against-guardrails)
* See the [authentication deep dive](/docs/authentication-deep-dive) for giving each tenant its own guardrail policy
* See [usage auditing](/docs/usage-auditing) for recording guardrail outcomes alongside the rest of your usage events
* See [generate a response](/docs/generate-a-response) for the Responses API workflow guardrails sit in front of
