Routing
The router reads the rules in sequence. The rule with the highest priority number has control. An explicit /local or /cloud command always has control. The token limits are the last test, not the first test of the difficulty of a task.
The priority sequence (default profile)
| Priority | Rule | Result |
|---|---|---|
| 100 / 99 | Explicit /local /fast / /cloud /heavy | Sets the route. No other rule can change it. |
| 95 | composer_wrapup_origin | Sends a Cursor Composer summary message to the cloud or to the origin |
| ~85–70 | task_classifier | Sends a tool task or a cloud-only task to the cloud. Sends a small task to the local model. |
| 75 | Complexity test | You must turn this on with complexity.enabled |
| 55 / 50 | Starlark scripts | A message with /swarm or [fanout] goes to the local model and uses fan-out. A refactor task also goes local. This profile sets orchestration.fan_out.enabled, so fan-out operates. |
| 10 / 5 | Context size | More than 8000 tokens goes to the cloud. A smaller context goes to the local model. |
| 0 | Default | Goes to the cloud. In the pure local profile it goes to the local model. |
Glider finds a delegate flag (/claude, /cursor-agent, /agy) before it reads these rules. Refer to Delegation.
How to write a rule
Each entry of routing.rules has this structure:
routing:
rules:
- name: Explicit Local # the name in the log and on the dashboard
priority: 100 # a larger number has control
enabled: true # omit it, and the rule operates
trigger:
type: explicit # one of the six types below
commands: ["/local", "/fast"]
action:
target: local # local | cloud
model: qwen2.5-coder:14b
| Field | Function |
|---|---|
name | The name that the dashboard and the log show. Give a name that you can find. |
priority | An integer. The router reads the rules from the largest number to the smallest. The first rule with a match has control. |
enabled | false keeps the rule in the file, and the router does not use it. With no value, the rule operates. |
The six trigger types
A type that is not in this table gives an error when Glider starts. The error names the rule.
| Type | Fields that it uses | Match condition |
|---|---|---|
explicit | commands | The message contains one of these commands. The command can go at any position. |
regex | pattern | The regular expression agrees with the most recent message. |
context_size | operator, value | The count of tokens agrees with the test. |
always | — | Each message. Use it at priority 0, as the last rule. |
script | file | A Starlark script decides. Refer to below. |
composer_wrapup | — | A summary message of Cursor Composer. The names wrapup_origin and composer_wrapup_origin also operate. |
These are the operators of context_size:
| Operator | Test |
|---|---|
> · >= | More than the value, or the value. |
< · <= | Less than the value, or the value. |
== · = | The same as the value. |
The action
| Field | Values | Function |
|---|---|---|
target | local · cloud | Which side answers. |
backend | a name in backends | Which backend, when the target is the cloud. |
model | a model name | Which model that backend uses. |
adapter | a LoRA name | An adapter for that model. vLLM only. |
A rule in Starlark
A script rule gives the decision to a file. The file must supply a function
evaluate(request). The function returns nothing for no match, or a dictionary.
A script that returns an action wins against the action block of the
rule. Therefore keep the two in agreement.
def evaluate(request):
if "refactor" in request.messages[-1].content:
return {
"matched": True,
"action": {"target": "local", "model": "qwen2.5-coder:14b"},
}
These are the fields of request:
| Field | Type | Content |
|---|---|---|
model | string | The model that the CLI asked for. |
stream | bool | The CLI asked for a stream. |
messages | list | Each item has role and content. |
estimated_tokens | int | The size of the context. |
has_tools | bool | The request carries tools[]. |
complexity_score | int | 0 to 100. |
complexity_source | string | heuristic or cursor. |
temperature | float | Present only when the CLI sent it. |
max_tokens | int | Present only when the CLI sent it. |
The dictionary that you return accepts these keys:
| Key | Function |
|---|---|
matched | True for a match. |
action.target · .backend · .model · .adapter | The same as the action above. |
action.strategy | single, fan_out, pipeline or ensemble. fan_out also needs orchestration.fan_out.enabled. |
POST /api/playground/parse reads a script and gives its errors.
POST /api/router/explain shows which rule a message obeys, and
GET /api/router/lint finds an error in the full set.
The rules that Glider adds
Two sections put rules in the sequence which are not in routing.rules. The
configuration reference gives each setting.
| Section | What it adds |
|---|---|
routing.task_classifier | Three rules: a request with tools goes to the cloud (85), a message that agrees with the cloud patterns goes to the cloud (80), and a message that agrees with the small patterns goes to the local model (70). |
routing.complexity | One rule at priority 75. A score of cloud_above or more goes to the cloud. It is off until you turn it on. |
routing.tool_followup is different. It adds no rule. It decides where the calls
of a tool go after a parent turn has its route, with an allowlist and a denylist of tool
names.
The Cursor MITM turn policy
- Turn-family sticky. Glider keeps the reply summary, the title and the mid-turn subagents at the origin. This starts after a
/cloudcommand, or after a cloudDecideLocaldecision. It continues for a TTL and for a grace time after the parent run. - Deny-local. While the sticky condition is live, Glider refuses the local model for each follow-on message that is not a new user message.
- The next real user message can make a new decision. That message can go to the local model again.
- Tool follow-up. Each child tool call gets a new allow decision or deny decision.
This mechanism applies only to the Cursor MITM protocol. Refer to MITM for the details. The full policy is in planning/routing_and_context.md.