Skip to main content
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.

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). 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.
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.

Guardrail modes

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 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:
To see the combined policy that applies to your requests, call GET /api/v1/llm/guardrails or run mka1 guardrails get. To see stored policies per scope, use GET /api/v1/llm/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.
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.
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).

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.
The result reports whether the content passed, and on a failure it names the guardrail and the message a real request would get back:
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:
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.

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.
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.
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:
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