Routing#
This guide explains how the Inference Router chooses a provider for each request, and the method to configure and validate it.
The router supports two kinds of routing rules, which plug into the same rule → strategy → policy pipeline and can be mixed freely in one policy:
Rule-based routing: deterministic rules that check request features (message content, context length, tools, model name, metadata) or live provider metadata (cost, performance).
Model-based routing: a classifier model reads the last user message and maps it to an index (for example
0→ local,1→ cloud).
Routing Model#
The router makes a decision in three layers:
Rule: checks one request feature, such as message content, context length, tools, model name, or metadata.
Strategy: combines rules with AND semantics, then filters providers with
provider_selector.Policy: runs strategies in order and selects the final provider using
FirstMatchorAllMatch.
If the policy selects no provider, the request goes to the provider marked
fallback: true in config.yaml (at most one); when no provider is marked, the
request is rejected with a routing error.
Built-in Strategies and Policies#
The default strategies are defined in src/rsd/strategy.yaml:
Strategy |
Trigger |
Provider Selector |
|---|---|---|
|
User message contains |
|
|
User message contains a tabular-data keyword ( |
|
|
Total context length falls in configured zones |
Zone-based |
|
Always matches |
|
|
Classifier maps the last user message to index |
Index-based |
|
Always matches |
|
The default policies are defined in src/rsd/policy.yaml:
Policy |
Criterion |
Strategy Order |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
FirstMatch returns the first strategy that can produce a candidate provider.
AllMatch runs every strategy and selects a provider that appears in every
candidate list. The default YAML file only uses FirstMatch; add an AllMatch
policy if you need to test intersection behavior.
Configure Providers#
Routing depends on provider metadata in config.yaml. A provider can expose
labels, cost, performance, and capabilities:
providers:
- name: local
type: hosted_vllm
model: Qwen/Qwen3.5-9B
enabled: true
metadata:
labels: [planning, local]
cost: 0
performance: 0.85
capability:
complexity: 0.75
tool_calling: true
settings:
endpoint: http://localhost:5000/v1
provider_selector supports these fields:
provider_selector:
label: planning
cost: 0
capability:
complexity: 0.7
tool_calling: true
label, cost, and capability.complexity may also be zone maps, for example
complexity: {0: 0.3, 1: 0.5, 2: 0.7}. Place complexity and tool_calling
under provider_selector.capability; placing them at the top level is invalid.
Select a Policy#
Choose the active policy in your runtime config:
routing:
policy: Balanced
Use the Balanced routing policy to prioritize planning tasks and long-context quality.
Use the CostFirst routing policy when you prefer free or local providers.
Rule-Based Routing#
Rule-based routing decides from request features or provider metadata, with no model inference on the request path.
Built-in Rules#
Rules are configured in src/rsd/strategy.yaml
with the type and param fields.
Rule Type |
Purpose |
Main Parameters |
|---|---|---|
|
Match the request |
|
|
Match text in messages |
|
|
Require or reject tool definitions |
|
|
Match fields in |
|
|
Match a complexity score |
|
|
Bucket the last user message by word count |
|
|
Bucket the total message content length by character count |
|
QueryComplexityScoreRule is currently a placeholder: it returns the midpoint
of score_range, so it does not yet vary with request content.
The model-based rule ModelBasedRule is also a built-in rule; it is described
in Model-Based Routing.
Exercise Rule-Based Routing#
Useful requests to exercise the default rule-based policies:
Goal |
Request Shape |
Expected Route Behavior |
|---|---|---|
Hit |
User message contains |
Selects a provider with |
Hit |
User message mentions tabular data, e.g. “I have a CSV file of sales; find the top region” |
Selects a provider with |
Hit context zone 0 |
Total message content length |
Requires provider complexity |
Hit context zone 1 |
Total message content length |
Requires provider complexity |
Hit context zone 2 |
Total message content length |
Requires provider complexity |
Hit |
Sets |
Selects a provider with |
Test fallback |
Makes every selector fail |
Goes to the provider marked |
Keyword Strategies#
A keyword strategy routes a task type to a provider label with a
MessageContentRule regex over the user messages. DataAnalysis is the
built-in one. The strategies below follow the same pattern; add the ones you
need:
Copy src/rsd/strategy.yaml to
<workspace>/strategy.yaml(a workspace file replaces the built-in one, so keep the strategies you still use) and append the strategies.Add the target label (for example
coding) to themetadata.labelsof the provider that should serve the task.Add the strategy names to a policy in
<workspace>/policy.yaml.
With FirstMatch, the first matching strategy that finds a provider wins, so
order matters when patterns overlap: “write a Python function” matches both
Coding and CreativeWriting, so put Coding first. Put keyword strategies
before catch-all strategies such as ContextLengthQuality. A strategy whose
label no provider carries matches nothing, and the policy moves on.
strategies:
- name: Coding
description: "Routes programming / coding requests."
rules:
- type: MessageContentRule
param:
pattern: '\b(code|coding|debug|debugging|refactor|compile|python|javascript|typescript|java|sql|regex|bash|shell|script|dockerfile|kubernetes|unit test|stack trace|programming)\b'
use_regex: true
roles:
- user
provider_selector:
label: coding
capability:
complexity: 0.7
- name: MathCalculation
description: "Routes math / calculation requests."
rules:
- type: MessageContentRule
param:
pattern: '\b(calculate|compute|solve|equation|arithmetic|math|derivative|integral|percentage)\b|数学|计算'
use_regex: true
roles:
- user
provider_selector:
label: math
capability:
complexity: 0.7
- name: Summarization
description: "Routes summarization requests."
rules:
- type: MessageContentRule
param:
pattern: '\b(summari[sz]e|summari[sz]ing|summari[sz]ation|summary|tldr|recap|condense|gist)\b'
use_regex: true
roles:
- user
provider_selector:
label: summarization
capability:
complexity: 0.5
- name: Translation
description: "Routes translation requests."
rules:
- type: MessageContentRule
param:
pattern: '\btranslat(?:e|ing)\b[^.]{0,60}?\binto\b|\btranslating\b|\btranslation\b|\btranslated\b|\btranslat(?:e|ing)\b[^.]{0,60}?\b(?:english|chinese|french|spanish|german|japanese|korean|mandarin)\b|翻译|译成'
use_regex: true
roles:
- user
provider_selector:
label: translation
capability:
complexity: 0.5
- name: CreativeWriting
description: "Routes writing / creative requests."
rules:
- type: MessageContentRule
param:
pattern: '\b(write|rewrite|draft|compose|essay|story|poem|email|blog|article|paraphrase|rephrase)\b'
use_regex: true
roles:
- user
provider_selector:
label: writing
capability:
complexity: 0.6
A policy using all of them, most specific first:
policies:
- name: Balanced
criterion: FirstMatch
strategies:
- Planning
- Coding
- DataAnalysis
- MathCalculation
- Summarization
- Translation
- CreativeWriting
- ContextLengthQuality
Strategy |
Example request |
Provider label |
|---|---|---|
|
“Write a Python function to merge two sorted lists” |
|
|
“Solve this equation: 3x + 5 = 20” |
|
|
“Summarize this article in three sentences” |
|
|
“Translate this paragraph into French” |
|
|
“Write a one-line birthday message for my mom” |
|
Performance-Based Routing#
PerformanceBased routes on live backend load instead of request content. A
provider probe scrapes each backend’s Prometheus /metrics endpoint
on an interval, publishes the numbers into the provider’s performance.*
metadata, and the strategy filters and ranks on those fresh values.
How the strategy reads the metrics#
The strategy’s provider_selector.performance is a map of
metric -> threshold. Two rules apply to every metric, so pick metrics
where both hold:
Lower is better. A threshold is an upper bound: a provider stays eligible only while
performance.<metric> <= threshold. A provider that is missing the field, or whose value is non-numeric, is dropped (the same conservative stance thecostselector takes). This is why the metrics below are load and latency signals, never capacity or throughput (which are higher-is-better).Read straight from metadata. The value used is whatever the probe last published to
performance.<metric>; the strategy does no arithmetic.sortthen orders the survivors by oneperformance.*attribute (descending: false= smallest first; a provider with no value sorts last).
Example — enable the probe and route on load#
This runs two local backends — one vLLM, one OVMS — and routes each request to
whichever is least loaded. Each provider gets a perf probe that scrapes its
own /metrics and publishes the in-flight request count to the shared
performance.requests field; PerformanceBased then filters and sorts on it.
# config.yaml
providers:
- name: vllm-local
type: hosted_vllm
model: Qwen/Qwen3.6-35B-A3B
enabled: true
metadata:
labels: [local]
cost: 0
settings:
endpoint: http://localhost:8088/v1 # chat base (/v1)
timeout: 300.0
auth: { scheme: none, api_key: null, custom_headers: {} }
probes:
- name: perf
enabled: true
endpoint: http://localhost:8088/metrics # scrape target (NOT /v1)
interval: 5
timeout: 5
extract:
# Capture "requests" as the field name and the running count as the
# value → published as performance.requests.
- regex: 'vllm:num_(requests)_running\{[^}]*\}\s+([0-9.eE+-]+)'
aggregate: { window: 6, method: mean }
publish:
- { to: metadata, prefix: performance } # → routing (live)
- { to: file, path: probes/vllm-local.perf.jsonl } # → history
routing:
policy: PerformanceFirst
PerformanceFirst already ships in src/rsd/policy.yaml
— it runs PerformanceBased first and falls back to ZeroCost when no provider
clears the load threshold:
# policy.yaml (already shipped)
policies:
- name: PerformanceFirst
criterion: FirstMatch
strategies:
- PerformanceBased
- ZeroCost # fallback when no provider clears the perf thresholds
The bundled PerformanceBased strategy (in
src/rsd/strategy.yaml) consumes the field the
probes publish:
# strategy.yaml (already shipped)
strategies:
- name: PerformanceBased
description: "Prefer the least-loaded backend by live in-flight request count."
provider_selector:
performance:
requests: 40 # drop a backend already running 40+ concurrent requests
sort:
- provider_attribute: performance.requests
descending: false # fewest in-flight requests first
With this config, a request is sent to vllm-local or ovms-local depending on
which currently has fewer in-flight requests; a backend above 40 concurrent
requests is skipped entirely, and if both are over the limit the request goes to
the provider marked fallback: true (or is rejected if none is).
Enable it at runtime (no restart)#
Probes can also be added through the providers API — POST the provider with the
probe in settings.probes and it starts immediately (resend the whole
settings block; it is replaced wholesale). See
probe.md.
Verify the probe is feeding the router#
# The live value the strategy reads (durable history is the file sink):
tail -1 <workspace>/probes/vllm-local.perf.jsonl
# {"ts": ..., "provider": "vllm-local", "probe": "perf",
# "aggregates": {"requests": 2.0}}
Send traffic and confirm the route decision’s strategy_name is
PerformanceBased and provider_name flips to the quieter backend as load
shifts. Route decisions also carry policy_name, criterion, and
candidate_count — use them (or the logs / metrics endpoint) to see why a
provider was selected.
Model-Based Routing#
ModelBasedRule runs a small classifier model on the router host against the last user message of each request and returns a routing zone index. The strategy’s index-keyed provider_selector turns that index into a provider.
Quick Start#
Download the default backbone model (see Prepare the Classifier Model):
hf download OpenVINO/Qwen3.5-2B-fp16-ov --local-dir /opt/models/Qwen3.5-2B-FP16 export IR_OV_MODEL=/opt/models/Qwen3.5-2B-FP16
Label your providers
localandcloudinconfig.yaml.Select the built-in policy in
config.yaml:routing: policy: ModelBased
Start or redeploy the router and confirm that the log shows
Preloaded 1 ModelBasedRule classifier(s) for the active policy.
The built-in ModelBased policy runs the ModelBasedRouting strategy:
strategies:
- name: ModelBasedRouting
rules:
- type: ModelBasedRule # no param: default backbone, version v1
provider_selector:
label:
0: local
1: cloud
Prepare the Classifier Model#
Supported Backbones#
Backbone key ( |
Base model |
Packaged version |
IR path variable |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
A classifier version only works on the backbone it was trained for. The router checks this before using a version.
Download#
Both Qwen backbones have prebuilt FP16 OpenVINO IRs on Hugging Face:
pip install -U huggingface_hub
# Qwen3.5-2B (default)
hf download OpenVINO/Qwen3.5-2B-fp16-ov --local-dir /opt/models/Qwen3.5-2B-FP16
export IR_OV_MODEL=/opt/models/Qwen3.5-2B-FP16
# Qwen3-0.6B
hf download OpenVINO/Qwen3-0.6B-fp16-ov --local-dir /opt/models/Qwen3-0.6B-FP16
export IR_OV_MODEL_QWEN3_0P6B=/opt/models/Qwen3-0.6B-FP16
There is no prebuilt FP16 IR for Llama-3.2-1B-Instruct, so convert it once
with optimum-cli (accept the model’s license on Hugging Face first via
hf auth login):
pip install "optimum-intel[openvino]"
# Llama-3.2-1B-Instruct
optimum-cli export openvino --model meta-llama/Llama-3.2-1B-Instruct \
--weight-format fp16 /opt/models/Llama-3.2-1B-Instruct-FP16
export IR_OV_MODEL_LLAMA3_1B=/opt/models/Llama-3.2-1B-Instruct-FP16
Devices#
IR_DEVICE selects the OpenVINO device: GPU (the default), GPU.0,
GPU.1, … or CPU. When the requested GPU is not available, the router
logs a warning and falls back to the CPU. NPU is not supported yet.
Configure the Router#
Model-based routing is configured with environment variables:
Variable |
Default |
Meaning |
|---|---|---|
|
none |
IR directory of the default backbone |
|
none |
IR directory of a specific backbone; takes precedence over |
|
|
Backbone used by rules that set no |
|
|
OpenVINO device; falls back to the CPU if no GPU is available |
|
|
Location of your own classifier versions |
|
|
Per-classifier cache of recent query results; |
|
|
Minimum fingerprint cosine similarity accepted when checking a version against the backbone |
|
|
Policy switch order: |
Docker Compose: export IR_OV_MODEL and, optionally, IR_DEVICE on the
host before running scripts/deploy_docker.sh. The compose file mounts
IR_OV_MODEL into the container at /models/ov-classifier and passes
IR_DEVICE through; for GPU devices, the deploy script also maps
/dev/dri. Put the other IR_* variables in workspace/.env. The workspace
is the container’s working directory, so the default IR_ARTIFACTS_DIR is
workspace/artifacts/router on the host. Compose mounts only one backbone
directory. To serve Qwen3-0.6B, point IR_OV_MODEL at its IR and set
IR_BACKBONE=qwen3-0.6b in workspace/.env.
Enable ModelBasedRule#
Reference the rule from a strategy, and the strategy from the active policy.
You can use the built-in ModelBased policy as is, or define your own
strategies in <workspace>/strategy.yaml and <workspace>/policy.yaml.
REST management goes through the strategy and policy endpoints — see the
API Reference.
Rule parameters for the classifier-version mode:
Parameter |
Default |
Meaning |
|---|---|---|
|
|
Backbone key; see Supported Backbones |
|
|
|
Examples:
rules:
- type: ModelBasedRule # qwen35-2b / v1
rules:
- type: ModelBasedRule
param:
model: qwen3-0.6b
rules:
- type: ModelBasedRule
param:
model: llama3-1b
rules:
- type: ModelBasedRule
param:
model: qwen35-2b
version: v2
Classifier Versions#
A classifier version is a single .rcv file. It holds a prompt, the classification head, the zone map, and a fingerprint of the backbone it was built for. The prompt is either a trained soft prompt (a soft version) or a text system prompt (a hard version, whose head is the backbone’s own output rows for the answer tokens). The router reads the kind from the file; the rule is configured the same way for both. The packaged v1 versions are soft.
Packaged:
v1for each backbone, shipped insrc/rsd/model_based/prompts/<backbone>/v1.rcv.defaultalways means this file.From prompt text: POST a strategy whose
ModelBasedRulesetsparam.text(plus optionallabelsandthreshold). The router packs a hard version into<IR_ARTIFACTS_DIR>/<backbone>/vN.rcv, checks it, and storesversion: vNin the strategy. Posting the same content again reuses that version. See Create or Update Strategy.Your own: Train your own version of classifier will be available next release.
Multi-class Routing#
Set prompt_file to switch ModelBasedRule from a classifier version to multi-class routing: the classifier sorts each request into one of your own categories, and the category’s zone index picks the provider. By default the categories are the provider labels in provider_selector.label: zone i is the category routed to label[i], so you write the names once. No multi-class strategy ships by default. Add one like this to the strategy.yaml in your workspace and reference it from a policy.
strategies:
- name: MultiClassModelBasedRouting
rules:
- type: ModelBasedRule
param:
prompt_file: multiclass_prompt.txt
threshold: 0.7
provider_selector:
label:
0: local
1: cloud
2: planning
Files to Edit#
src/rsd/strategy.yaml Built-in strategy definitions
src/rsd/policy.yaml Built-in policy definitions
<workspace>/strategy.yaml Replaces the built-in strategies when present
<workspace>/policy.yaml Replaces the built-in policies when present
config.yaml Runtime providers and active policy
<IR_ARTIFACTS_DIR>/<backbone>/vN.rcv Your own classifier versions