Rewrite rules

A rewrite rule changes the conversation before a model sees it. Use rewrite rules to:

Rewrite rules only change what the model receives. They don't change its response.

Every model, whether Built-in, Virtual, or BYOK, has its own selection of rewrite rules. For a Virtual model, its rewrite rules run before its routing rule, and the rules of the model it picks don't run.

Built-in rewrite rules

Radius includes three rules that compact the output of bash tool calls:

Each rule only changes output it recognizes, and only when the result gets shorter. Unusual formats and failed commands are left unchanged. The Git diff and grep rules also leave short output unchanged. See Built-in rule details for the exact limits.

To get the original output for one command, prefix it with RADIUS_NO_PROCESS=1, for example RADIUS_NO_PROCESS=1 git diff.

The Rewrite rules page, linked from Models, shows the source of every rule.

Choose rules for a model

Open a model on the Models page and select its rewrite rules. New models use none, and publishing a rule doesn't add it to any model. A model can use up to 8 rules.

Selected rules always run in the order shown on the Rewrite rules page, from lowest to highest priority. The order is the same for every model, and a model can't change it. Owners and admins can change priorities on that page. See Priorities.

Custom rewrite rules

A custom rewrite rule is a small JavaScript module your organization publishes through the Radius API. To create one, ask Pi. Set up Pi as described in Manage models with an agent, then describe the rule you want. Pi uses these operations:

  1. rewriteRules.test runs a draft rule against a sample conversation.
  2. rewriteRules.publish publishes it.
  3. models.upsert selects it for a model. You can also select it in the model's settings on the Models page.

Owners and admins can publish and delete rules.

Write a rewrite rule

A rule default-exports an object with a rewrite(input) method, which can be async. This rule adds an instruction to the system prompt:

export default {
  rewrite(input) {
    const systemPrompt = input.request.context.systemPrompt ?? "";
    return { systemPrompt: `${systemPrompt}\nKeep answers concise.` };
  },
};

input.request.context contains the conversation: systemPrompt, messages, and tools. Unlike routing rules, rewrite rules receive the complete conversation, including image data, tool-call arguments, and tool schemas. See Rule input.

A rule returns the new messages, the new systemPrompt, or both. It can also change input.request.context directly and return nothing. Rules can't change tools or any other part of the request. See Return values.

Keep the conversation valid. Every tool result must still follow its tool call, and the conversation can't be empty. Change existing messages rather than creating new assistant or tool-result messages.

Rules run in a sandbox without network access, with up to 50 ms of CPU time and a 1 second timeout. A rule's top-level variables can carry over from one request to the next, including requests from other people in your organization. This isn't guaranteed, so don't rely on it, and don't keep conversation data in top-level variables.

If a rule fails, times out, or returns an invalid conversation, the request fails. Radius never silently skips a failing rule, so a rule that removes sensitive content can't be bypassed by an error. This also applies to built-in rules.

Use built-in rules and helpers

Custom rules can import helpers from #radius/rewriter-helpers: analyzeBashInvocation, rewriteBashToolResults, rewriteBashResultText, stripAnsi, and truncateText.

They can also apply the built-in rules themselves, for example only under certain conditions:

import { applyBuiltinRewriteRules } from "#radius/rewrite-rules/v1";

export default {
  rewrite(input) {
    return applyBuiltinRewriteRules(input, {
      enabled: ["compact_git_diff", "compact_git_status"],
    });
  },
};

The built-in rule IDs are compact_git_diff, compact_git_status, and compact_grep. Applying a built-in rule twice can change the output again, so don't also select a rule for the model if your custom rule already applies it.

Test a rewrite rule

rewriteRules.test runs one rule against a sample conversation without publishing it, charging credits, or calling a model. Test a draft by sending source:

{
  "source": "export default { rewrite(input) { return { systemPrompt: (input.request.context.systemPrompt ?? '') + '\\nKeep answers concise.' }; } };",
  "request": {
    "model": "balanced",
    "context": {
      "systemPrompt": "You are a helpful assistant.",
      "messages": [{ "role": "user", "content": "Explain what this project does.", "timestamp": 0 }]
    }
  }
}

To test a published custom rule, send its rule_id instead of source. The response contains the rewritten conversation in rewritten.context and the estimated effect in impact.

Test conversations where the rule should change something and where it shouldn't, including complete tool calls with their results. See Testing details.

When rewrite rules run

Rewrite rules run on every request, before routing. That includes follow-up requests and retries in the same reply, even when a Virtual model reuses its earlier routing choice.

When you publish a new version, change priorities, or change a model's rules, new requests use the change within moments, and within an hour at most.

Radius records the estimated change in input tokens for each request, and Pi stores it with the assistant message in the session.

Update or delete a rule

Publishing again with the same rule_id creates a new version and switches every model that uses the rule to it. The rule keeps its priority unless you send a new one.

A custom rule's page shows its earlier versions, and so do rewriteRules.versions.list and rewriteRules.versions.read. Viewing an earlier version doesn't make it active again: to go back, publish its source as a new version.

To delete a rule, use its page or rewriteRules.revoke. You can't delete a rule that an enabled model uses.

Custom rules see the complete conversation, including anything sensitive in it. Keep their source short and review it like any other code that handles your data.

Reference

Rule input

input contains:

request.context contains systemPrompt, messages, and tools in Pi's message format. Before your rule runs, it already contains the changes of rules with a lower priority.

Return values

A rule can return:

If a returned object sets the same field at more than one level, request.context wins over context, which wins over the top level. When a rule returns an object, Radius ignores any direct changes it made to input.

Radius then checks the result:

Built-in rule details

All three rules only change bash tool results that contain exactly one text block, aren't errors, and follow a matching tool call. A rule only replaces a result when its version is shorter. Setting RADIUS_NO_PROCESS to any value other than empty or 0 in the command turns all three off for that result.

Priorities

Rule IDs

Custom rule IDs use lowercase letters, digits, and hyphens, start and end with a letter or digit, and are up to 64 characters long. Built-in and custom rules are told apart by kind. Built-in rule IDs contain underscores, so they never match a custom rule ID.

Model selections in the API

In models.upsert, rewrite_rules lists the selected rules as { "kind": "builtin" | "custom", "rule_id": "…" }. The order of the list doesn't matter, because rules always run in priority order. An empty list [] turns rewriting off for the model. Leaving out rewrite_rules keeps the model's current selection.

Testing details

Errors

Situation Response
A rule fails during a request HTTP 503, error: { code: "profile_context_rewriting_component_execution_failed", message }
Publishing a new rule_id without priority HTTP 400, error: "priority_required"
Deleting a rule that an enabled model uses HTTP 409, error: "rewrite_rule_in_use", referencing_profiles: [profileId]
Testing a rule that fails HTTP 200, ok: false, message, and a phase: resolve, setup, load, execute, or invalid_output