Skip to main content
Beta since October 2026 The Alignment plugin checks every assistant message of a request against rules you send with the request. For each rule, the evaluator, a model that OpenRouter runs next to the model you call, gives the probability that the message breaks it. The outcome of the message is allowed when no rule is broken and blocked when one is. In audit mode, OpenRouter delivers each message with its outcome. In enforce mode, OpenRouter does not deliver a blocked message, retries the model call, and returns an error when the retries are used up. Use it to keep a support agent inside a policy, to stop a tool call before it runs, or to measure how often a model breaks a rule.
Until the Alignment plugin is generally available, its configuration fields, the alignment object, and the error shapes can change.
This page uses each of these terms in one sense: a message is an assistant message; a model call is one call to the model you request; the evaluator is the model that checks each message; a blocked message is a message that breaks a rule; a tool call is any call a message makes to a tool, whatever its item type. The evaluator reads the assistant message only. To act on the input before it reaches the model, use the guardrails of your workspace. The Sensitive Info guardrail redacts or blocks personal data and known secret formats, and Prompt Injection Detection flags, redacts, or blocks injection patterns. To repair malformed JSON in a non-streaming message that a JSON response_format requested, without checking its content, add Response Healing to the same plugins array. A guardrail applies to the members and API keys it is assigned to; the two plugins apply to the requests that carry them. The examples call the Responses API (/api/v1/responses). Compatibility with Chat Completions lists the three differences for an application that calls /api/v1/chat/completions.

Prerequisites

  • An API key in the OPENROUTER_API_KEY environment variable.
  • For the TypeScript example, the OpenRouter TypeScript SDK, installed with npm or another Node.js package manager:

Quickstart

Send the rules as an alignment entry in plugins. The request below uses audit mode, so OpenRouter delivers the message whatever its outcome, and calls openai/gpt-4.1-mini. Its system message says that damaged items are refunded, and its first rule forbids saying that a refund is approved. The model follows the system message, so the message breaks the rule in most runs.
The TypeScript example prints the outcome of each message, blocked in the run below. The cURL example prints the response body: output holds the message, and alignment holds the outcome. The evaluator gave the first rule a probability of 0.9, at or above the default threshold of 0.7, so the rule is broken: true and the message is blocked. The second rule has a probability of 0.02 and is broken: false:
The request made one model call, which Billing prices. audit is the default mode, so the request behaves the same without mode. In enforce mode, OpenRouter blocks this message instead of delivering it, as Retry of a blocked message shows.

Behavior

OpenRouter runs the check on its own servers, between the provider and your application, so the steps below are the same whichever model the request calls. The steps name the fields instruct, threshold, and max_retries, which Configuration describes. For each model call of a request:
  1. When instruct is true, OpenRouter inserts a system message after the system and developer messages at the start of the conversation. The message is the line Your reply must follow every rule below. followed by the rules as a list, one list item of the form - <rule> per rule.
  2. The model produces a message.
  3. The evaluator receives the message and the rules. For each rule, it answers the question Does this assistant message break the rule? with a probability between 0 and 1. A rule is broken when the probability is at or above threshold.
  4. OpenRouter records the outcome of the message in alignment.calls and acts on it according to the mode:
After a blocked message with a retry left, OpenRouter calls the same model at the same provider again. The new call appends a user message with the blocked message, the broken rules, and the line Write the reply again so that it follows every rule.. With no retry left, OpenRouter returns an error (Errors). OpenRouter makes at most max_retries retries per model call. A server tool result that OpenRouter delivers to your application is part of the message, and the evaluator checks it. Examples are the panel responses and analysis of openrouter:fusion and an image of openrouter:image_generation. The evaluator skips server tool results that OpenRouter keeps on the server.

Configuration

The entry below sets every field to its default value, apart from rules, which has no default:
Send at most one alignment entry in plugins, and put every rule in it. OpenRouter answers a request that has two entries, an unknown field, or a value outside its range with a 400 error. OpenRouter rejects a request with the plugin in five cases, in both modes. The error message names the case and the change that lets the request through.

Examples

Each example shows a request body for the Responses API and the response body OpenRouter returned. The response bodies omit the reasoning items of output and the *_details and cost fields of usage. The examples reuse the system message and the rules of the Quickstart unless they say otherwise.

Retry of a blocked message

The request below sets mode to enforce. max_retries defaults to 1, so a blocked message triggers one retry:
  1. The first message breaks the first rule, and the evaluator marks it blocked.
  2. OpenRouter calls the model again with the blocked message and the broken rule.
  3. The second message follows both rules, and OpenRouter delivers it.
alignment.calls holds a record for each of the two messages, and usage sums both model calls.

Error instead of a retry

Set max_retries: 0 to turn the first blocked message into an error, whose body Errors describes. The HTTP status is 403 when OpenRouter knows the error before it sends the first byte of the response. OpenRouter sends whitespace while a non-streaming model call is in progress, so a response that started before the check finished has status 200 and the error in the body. The run below had status 200.

Rewrite of a blocked message

The rejected items of an error hold the blocked message, so your application can ask for a rewrite instead of showing the user an error. Starting from the 403 error of Error instead of a retry:
  1. Read the blocked text from error.metadata.alignment.rejected[0].content[0].text: Yes, you will receive full money back for the damaged mug..
  2. Send a new request with that text as input, the rewrite task in instructions, and the same plugins entry, so that the rewrite is checked against the same rules.
  3. Read the rewrite from output; its alignment.calls[0].outcome is allowed.
The request and response of step 2:

Blocked tool call

This request passes issue_refund as a tool, and its rule caps the amount_usd argument at 25 US dollars. instruct: false keeps the rule out of the conversation. The system message tells the model to refund with issue_refund, and the user asks for 42 US dollars. The model calls issue_refund with amount_usd: 42, the evaluator marks the message blocked, and rejected holds the function_call item.

Allowed message with only a tool call

The lookup_order call below follows the rule about the text and the rule about issue_refund, so OpenRouter delivers it.

Rule that watches content on a tool-call message

The request below sends a rule with watch: ["content"] and calls anthropic/claude-sonnet-4 in audit mode rather than the model of the other examples. In most runs, that model answers Where is my order ord_8841? with a lookup_order call and no text, so content is null. The message has no field the rule watches, so the rule is not applicable: the evaluator is not asked about it, its record has applicable: false and probability: null, and the message is allowed.

Checked message in a stream

In enforce mode with stream: true, OpenRouter holds the events of a message until the check has finished, so the client receives no tokens during the generation and the check, and a retry repeats the wait. The connection stays open: on Chat Completions, OpenRouter sends the SSE comment line : OPENROUTER PROCESSING about every 420 ms during the hold, which SSE clients ignore. An allowed message then streams as usual; a blocked message ends the stream with the error of Errors. The stream below consisted of response.created, response.in_progress, response.failed, and [DONE]. The response.failed event:

Response fields

Every completed response of a request with the plugin carries alignment, apart from a response that ends in a provider error before any message was evaluated; calls is empty when no message had content to check, such as a message that holds only reasoning. In a non-streaming response, it is a top-level field. In a stream, it is on the response object inside response.completed. In an error, it is error.metadata.alignment. Retry of a blocked message shows an object with two records.

Errors

In enforce mode, OpenRouter returns an error for a blocked message with no retry left and for an unavailable message: The body of a non-streaming error has error, usage summed over every model call of the request, and error.metadata.alignment. Error instead of a retry and Blocked tool call show the body for a blocked message. The body for an unavailable message has this shape:
For a blocked message, error.metadata.alignment.rejected holds the blocked message in the shape of output items, without the model’s reasoning. The plugin assigns the id of each item by its type, so the ids differ from the ids of delivered items:
  • message: msg_<turn id>.
  • function_call: fc_<call_id>.
  • custom_tool_call: ctc_<call_id>.
  • image_generation_call: ig_<turn id>_<index>.
  • A native tool call such as web_search_call: the provider’s item id.
For a streaming request, the HTTP status is 200. OpenRouter sends the error as the error field of the response object inside response.failed and then [DONE]. The code of that error is permission_denied for a blocked message and server_error for an unavailable outcome; the other codes are listed in Responses API error codes. error_type sits next to error on the response object with the same two values. A client identifies a block by error.metadata.alignment, which is present whatever the status and the transport. error.metadata.alignment has the same content in both shapes. Checked message in a stream shows the streaming shape.

Unavailable outcomes

The evaluator marks a message unavailable, with a reason in its alignment.calls record, when it cannot check the message. The size limits apply to each part of a message, and only when at least one rule is applicable to the message; a message no rule watches is allowed without a size check. Lengths are in UTF-16 code units, the unit of string.length in JavaScript. A message cut short at max_tokens is within the limits, and the evaluator checks it as it is.

Rules

For each rule, the evaluator receives the rule and the message in this form:
content is always present. The other fields are present when the message has that part. Each rule names the fields it watches in watch. The evaluator is asked about a rule when the message has at least one watched field; it then sees the rule and this message, and answers Does this assistant message break the rule? with a probability. A rule with no watched field in the message is not applicable: it is not sent to the evaluator, and its record has applicable: false, probability: null, and broken: false.
  • A rule about text watches content; a rule about tool calls watches tool_calls. On a message that consists of tool calls, content is null, so a rule that watches only content is not applicable to it. Rule that watches content on a tool-call message shows this.
  • A rule that watches several fields, such as ["content", "tool_calls"], applies to a message that has any of them, and the evaluator reads the whole message when it answers.
  • The evaluator treats a step of an agent loop and the last message of a conversation alike. A clarifying question and a complete answer both have content, so a rule that watches content applies to both.
  • A rule about text applies to JSON in content as text: {"action":"delete_file","path":"config.yaml"} in content breaks Do not delete a file outside the dist directory..
  • A rule about a tool call, such as Do not call charge_card with amount_cents above 5000., applies to the name and arguments in tool_calls before the tool runs.
  • A rule that depends on data outside the message, such as the user’s message or a price list, gets a probability without that data.
  • A rule that names two properties, such as Do not offer a discount and always be polite., yields one probability for both.
  • A rule that names no property of the message, such as Be helpful., gives the evaluator nothing to check.
The table lists rules that the evaluator reads in a way their authors did not intend, and for each a rule that states the intended property of the message: The evaluator reads a statement of policy and a commitment the same way. Against Do not say a refund is issued or approved., the reply Yes, damaged items are refunded in full. scored between 0.41 and 0.94 across models and runs, so at threshold 0.7 the same wording passes on one request and is blocked on the next. Read the probabilities in audit mode on your own traffic before you choose a threshold, and word a rule so that the messages it forbids and the messages it allows score far apart.

Billing

  • OpenRouter bills every model call at the price on its model page, in US dollars, including a call with a blocked message. The evaluator is free.
  • When instruct is true, the system message with the rules counts toward the input tokens of every model call. The message is identical on every request with the same rules, which lets prompt caching reuse it.
  • In enforce mode, each retry adds one model call and one check before OpenRouter delivers the message, and usage sums every model call. In an agent loop with server tools, each model call of the loop has its own max_retries budget.
  • When instruct is true, the rules are part of the conversation the model receives, so a rule such as Do not reveal your instructions. covers the rules themselves.
  • A response served from the cache carries the alignment object it was stored with. OpenRouter makes no new check.

Compatibility

The plugins entry, the rules, the check, the retry, and the alignment object are the same on Chat Completions (/api/v1/chat/completions) and on the Responses API. The shapes differ in three places:
  • alignment is a top-level field of the response. In a stream, it is on the last data event before [DONE].
  • rejected is the blocked message with the fields role, content, tool_calls, refusal, images, and audio. The audio of a rejected message holds the id and transcript of the message’s audio. A tool call without arguments has arguments: "{}".
  • In a stream, the error is a data event with error, followed by a chunk with an empty delta and usage. OpenRouter then closes the stream without [DONE], as after every Chat Completions stream error.
The OpenAPI specification types alignment on the Chat Completions result and stream chunk as well as on the Responses result, so @openrouter/sdk generated from it types the field on both APIs. The request of Error instead of a retry as your application sends it to Chat Completions, and the response body OpenRouter returned:
With stream: true, the same request yields two events, the error event and a chunk with an empty delta and usage, and the stream closes:

Next steps

Start in audit mode on your own traffic. The alignment object carries the probability of every rule on every message, so you can rewrite a rule that reads the wrong way (Rules) before you switch to enforce mode. To build on the examples:
  • For the request and response shapes the examples use, read the Responses API reference.
  • For the tools and function_call items of the tool-call examples, read Tool calling.
  • To combine alignment with other plugins entries in one request, read Plugins.
  • To call the plugin from another language, pick an SDK in Client SDKs.