Files
picoclaw/docs/guides/routing-guide.md
T
lxowalle 77b0c43392 refactor: support explicit provider field in model list entries (#2609)
* refactor: support explicit model list providers

* fix(web): preserve explicit model providers

* fix(web): preserve legacy provider prefixes on model updates

fix(models): normalize explicit provider-prefixed ids

fix(api): preserve legacy model updates across providers

fix(agent): preserve config identity for explicit provider refs

* fix ci
2026-04-22 11:28:47 +08:00

334 lines
7.0 KiB
Markdown

# Routing Guide
> Back to [README](../README.md)
In PicoClaw, routing has two user-facing parts:
- **agent routing**: choose which agent should handle a message
- **model routing**: choose whether a turn should use the primary model or the configured light model
This guide explains how to configure both for real deployments.
## Quick Start
### Route one Telegram group to a support agent
```json
{
"agents": {
"list": [
{ "id": "main", "default": true },
{ "id": "support" }
],
"dispatch": {
"rules": [
{
"name": "telegram support group",
"agent": "support",
"when": {
"channel": "telegram",
"chat": "group:-1001234567890"
}
}
]
}
}
}
```
### Route only Slack mentions in one workspace
```json
{
"agents": {
"list": [
{ "id": "main", "default": true },
{ "id": "support" }
],
"dispatch": {
"rules": [
{
"name": "slack mentions",
"agent": "support",
"when": {
"channel": "slack",
"space": "workspace:t001",
"mentioned": true
}
}
]
}
}
}
```
### Use a light model for simple turns
```json
{
"model_list": [
{
"model_name": "gpt-main",
"provider": "openai",
"model": "gpt-5.4",
"api_keys": ["sk-main"]
},
{
"model_name": "flash-light",
"provider": "gemini",
"model": "gemini-2.0-flash-exp",
"api_keys": ["sk-light"]
}
],
"agents": {
"defaults": {
"model_name": "gpt-main",
"routing": {
"enabled": true,
"light_model": "flash-light",
"threshold": 0.35
}
}
}
}
```
## Agent Routing
Agent routing is configured with:
```text
agents.dispatch.rules
```
Rules are evaluated from top to bottom.
The **first matching rule wins**.
If no rule matches, PicoClaw falls back to the default agent.
## Supported Match Fields
| Field | Meaning | Example |
| --- | --- | --- |
| `channel` | Channel name | `telegram`, `slack`, `discord` |
| `account` | Normalized account ID | `default`, `bot2` |
| `space` | Workspace, guild, or similar container | `workspace:t001`, `guild:123456` |
| `chat` | Direct chat, group, or channel | `direct:user123`, `group:-100123`, `channel:c123` |
| `topic` | Thread or topic | `topic:42` |
| `sender` | Normalized sender identity | `12345`, `john` |
| `mentioned` | Whether the bot was explicitly mentioned | `true` |
Values must match the normalized runtime shape, not the raw incoming payload.
## Rule Ordering
Put more specific rules before broader rules.
Good:
1. VIP sender inside one group
2. all traffic for that group
3. channel-wide fallback
Bad:
1. all traffic for that group
2. VIP sender inside the same group
In the bad ordering, the broad rule wins first and the VIP rule never runs.
## Session Interaction
Routing and sessions are related but different.
- routing decides which agent handles the message
- session settings decide which messages share memory
You can override the global `session.dimensions` value for one matched rule with `session_dimensions`.
Example:
```json
{
"agents": {
"list": [
{ "id": "main", "default": true },
{ "id": "support" },
{ "id": "sales" }
],
"dispatch": {
"rules": [
{
"name": "vip in support group",
"agent": "sales",
"when": {
"channel": "telegram",
"chat": "group:-1001234567890",
"sender": "12345"
},
"session_dimensions": ["chat", "sender"]
},
{
"name": "support group",
"agent": "support",
"when": {
"channel": "telegram",
"chat": "group:-1001234567890"
},
"session_dimensions": ["chat"]
}
]
}
},
"session": {
"dimensions": ["chat"]
}
}
```
In this configuration:
- the VIP gets routed to `sales`
- everyone else in the group goes to `support`
- the VIP route also gets per-user session isolation
## Identity Links
`session.identity_links` also affects routing when you match on `sender`.
Use it when the same real user may appear under multiple raw sender IDs.
Example:
```json
{
"session": {
"identity_links": {
"john": ["slack:u123", "legacy-user-42"]
}
},
"agents": {
"dispatch": {
"rules": [
{
"name": "john goes to sales",
"agent": "sales",
"when": {
"sender": "john"
}
}
]
}
}
}
```
## Model Routing
Model routing is configured under:
```text
agents.defaults.routing
```
Current fields:
| Field | Meaning |
| --- | --- |
| `enabled` | Turn model routing on or off |
| `light_model` | `model_name` from `model_list` used for simple turns |
| `threshold` | Complexity cutoff in `[0, 1]` |
Important behavior:
- the light model must exist in `model_list`
- PicoClaw resolves the light model at startup; if it is invalid, routing is disabled
- one turn stays on one model tier, even if it later calls tools
## What Affects The Complexity Score
The current model router looks at structural signals such as:
- message length
- fenced code blocks
- recent tool calls in the same session
- conversation depth
- media or attachments
This means a "simple" turn may still go to the primary model if it includes:
- code
- images or audio
- a very long prompt
- a tool-heavy ongoing workflow
## Choosing A Threshold
Recommended starting point:
```json
{
"agents": {
"defaults": {
"routing": {
"enabled": true,
"light_model": "flash-light",
"threshold": 0.35
}
}
}
}
```
General rule:
- lower threshold: use the primary model more often
- higher threshold: use the light model more aggressively
Practical suggestions:
- `0.25` if you want safer routing with fewer light-model turns
- `0.35` as the default starting point
- `0.50+` only if your light model is already strong enough for most chat traffic
## Troubleshooting
### A rule is not matching
Check:
- rule order
- normalized value shape such as `group:-100123` instead of just `-100123`
- whether the channel actually provides `space`, `topic`, or `mentioned`
### The wrong agent handles a message
The most common cause is ordering.
Remember: first match wins.
### The light model is never used
Check:
- `agents.defaults.routing.enabled` is `true`
- `light_model` exists in `model_list`
- the light model can actually initialize
- your threshold is not too low
### The primary model is still chosen for short messages
That can still happen when the turn includes:
- a code block
- media or attachments
- recent tool-heavy history
### Routing works, but the conversation memory is still too shared
Adjust `session.dimensions` globally or `session_dimensions` on the specific route.
Routing chooses the agent, but sessions decide context sharing.
## Related Guides
- [Session Guide](session-guide.md)
- [Configuration Guide](configuration.md)
- [Providers & Model Configuration](providers.md)