Routing rules

A routing rule decides which model answers each request to a Virtual model. Every Virtual model has one routing rule. Built-in and BYOK models don't need one, because they always answer their own requests.

Routing rules run after rewrite rules, so they see the rewritten conversation. A routing rule only picks a model. It can't change the request.

The Static rule

Static is built into Radius. It sends every request to one model you select. cheap, balanced, and precise use it.

If the selected model can't serve a request, the request fails. Radius doesn't silently switch to a different model.

Custom routing rules

A custom routing rule picks a model for each request. For example, it can send requests with images to a model that supports images, long conversations to a model with a large context window, or short questions to a cheaper model.

Custom rules are small JavaScript modules your organization publishes through the Radius API. The Routing rules page, linked from Models, shows each rule's source, settings, and the models that use it, including for Static. For custom rules, it also shows earlier versions.

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. routingRules.test runs a draft rule against a sample request.
  2. routingRules.publish publishes it.
  3. models.upsert selects it in a Virtual model. You can also select it in the Virtual model's settings on the Models page.

Owners and admins can publish and delete rules.

Write a routing rule

A rule default-exports an object with a route(input) method, which can be async. It returns the ID of one candidate from input.candidates:

export default {
  route(input) {
    const preferred = input.candidates.find(
      (candidate) => candidate.routeId === input.profile.routing.config.model,
    );
    return {
      candidateId: preferred?.id ?? input.defaultCandidateId,
      reason: preferred ? "Preferred model is available" : "Using the default candidate",
    };
  },
};

This rule sends requests to the model set in the Virtual model's model setting (see Rule settings), and falls back to the first candidate when that model isn't available.

Input

Use routingRules.test to see the input for a sample request. A few fields differ in tests; see Testing details.

Result

Return { candidateId, reason? }, where candidateId is a candidate's id, not its routeId. reason is an optional explanation.

If the rule returns an unknown candidate or an invalid result, throws, or times out, the request fails. Radius never picks a different model for you, so build any fallback into the rule.

Limits

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.

Rule settings

A rule can declare settings, so each Virtual model that uses it can configure it differently. The Virtual model's settings page shows them as form fields, and the rule reads the values from input.profile.routing.config.

Declare settings in config_schema when you publish. For example, this routingRules.publish body declares the model setting used above:

{
  "rule_id": "preferred-model",
  "name": "Preferred model",
  "description": "Sends requests to the configured model when it is available.",
  "source": "export default { route(input) { … } };",
  "config_schema": {
    "version": 1,
    "fields": [{ "key": "model", "label": "Preferred model", "kind": "model" }]
  }
}

A Virtual model using this rule then has settings such as { "model": "deepseek-v4.1-flash" }. Rules without settings must declare { "version": 1, "fields": [] }.

Each field has a key, a label, and one of these kinds:

Radius checks a Virtual model's settings against the schema when you save the Virtual model. When you publish a new version, Radius checks it against every Virtual model that uses the rule, including disabled ones. If any of their settings no longer fit, publishing fails and nothing changes. Update those models first, or publish the change under a new rule_id. See Settings validation for the exact rules.

Test a routing rule

routingRules.test runs a rule against a sample request without publishing it, charging credits, or calling a model. Test a draft by sending source, config_schema, and config:

{
  "source": "export default { route(input) { return { candidateId: input.defaultCandidateId }; } };",
  "config_schema": { "version": 1, "fields": [] },
  "config": {}
}

To test a published rule, send its rule_id with either config or profile, the ID of a Virtual model that uses the rule. With profile, the test uses that Virtual model's saved settings.

See Testing details for what a test does and doesn't cover.

When routing rules run

A rule runs once per user message. Follow-up requests in the same reply, such as tool calls and retries, usually reuse that choice for up to an hour. This needs a session ID: without one, the rule runs on every request. Radius can't always reuse a choice, so a rule should return the same answer for the same conversation. See Caching for exactly when a rule runs again.

When you publish a new version or change a Virtual model's settings, new messages use the change within moments, and within an hour at most. A reply that is already in progress usually keeps the model it started with.

Update or delete a rule

Publishing again with the same rule_id creates a new version and switches every Virtual model that uses the rule to it. The Routing rules page shows earlier versions, and so do routingRules.versions.list and routingRules.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 routingRules.revoke. You can't delete a rule that an enabled Virtual model uses.

Reference

Request input

input.request contains these fields. Values that come from the conversation reflect it after rewrite rules have run.

Field Type Meaning
model string The ID of the Virtual model that received the request.
estimatedInputTokens number Estimated size of the conversation in tokens.
messageCount number Number of messages in the conversation.
hasImages boolean Whether the request contains images.
hasTools boolean Whether any tools are defined.
toolNames string[] Names of the defined tools. Empty when there are none.
reasoningRequested boolean Whether the request asks for thinking.
maxOutputTokens number (optional) The request's output token limit, if set.
hasSessionId boolean Whether the request has a session ID. The ID itself isn't included.
lastUserText string (optional) Text of the most recent user message that has text.
context object The conversation, as described below.

lastUserText comes from the most recent user message that contains text. Text blocks are joined with newlines, images are ignored, and surrounding whitespace is trimmed. Empty and image-only messages are skipped, so the text can come from an earlier message. The field is omitted when no user message has text. Assistant messages and tool results don't affect it, so a follow-up request after a tool call has the same lastUserText as the request before it.

context contains:

ContentBlock is one of:

Image data, tool-call arguments, tool parameter schemas, signatures, and request metadata are left out. The system prompt, lastUserText, each user message string, and each text or thinking block are each cut to 64,000 JavaScript UTF-16 code units, ending in …. There is no limit on their combined length, and tool names and descriptions aren't cut.

Caching

Radius caches a rule's choice so that the follow-up requests of a reply use the same model:

Settings validation

Schemas:

Settings values:

Schema checks can't tell whether your rule still behaves correctly after a change, so review code changes as well.

When a request arrives, Radius checks the Virtual model's settings against the exact version it's about to run. If they don't match, for example because a new version was published at the same moment, the request fails instead of running the rule with unchecked settings.

Testing details

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.

Errors

Situation Response
Saving a Virtual model with settings that don't fit the rule HTTP 400, error: "invalid_routing_rule_config", fields: [{ field, message }]
Publishing a version that doesn't fit existing Virtual models HTTP 409, error: "routing_rule_config_incompatible", referencing_profiles: [{ profileId, fields: [{ field, message }] }]
Deleting a rule that an enabled Virtual model uses HTTP 409, error: "routing_rule_in_use", referencing_profiles: [profileId]
Testing with a malformed request, schema, or config that isn't an object HTTP 400
Testing with settings that don't fit the schema HTTP 200, ok: false, phase: "configuration", details.fields
Testing a rule that fails HTTP 200, ok: false, message, and a phase: resolve, setup, load, execute, or decision