A routing policy is a caller-facing model name that resolves to one or more real models. Use a policy for failover, conditional selection, traffic splitting, learned selection, or guardrails the caller cannot remove. Use an alias when one name always maps to one target.
Policies are a standalone feature. Hybrid gateways receive their attempt plan from the connected control plane.
routing:
policies:
fast:
select:
- default: openai:gpt-5-mini
on_failure:
- anthropic:claude-haiku-4-5Callers send model: fast. If the first provider fails before any response is
sent, Otari tries the fallback. The response keeps the policy name; Activity and
usage record the models that were attempted.
select chooses where a request starts. on_failure lists what to try after a
retryable failure.
routing:
policies:
thrifty:
select:
- when:
budget_used_pct: {gte: 80}
target: openai:gpt-5-nano
- default: openai:gpt-5-mini
on_failure:
- anthropic:claude-haiku-4-5Selection entries are evaluated in order. The default must be last. Supported conditions are:
budget_used_pctbudget_remaining_usduser_idkey_id
All conditions in one when block must match. Numeric comparisons use exactly
one of gt, gte, lt, or lte. A budget condition does not match a
caller with no finite budget.
The weighted router chooses independently for each request and normalizes the configured weights:
routing:
policies:
balanced:
select:
- router: weighted
candidates:
- openai:gpt-5
- anthropic:claude-sonnet-4-6
weights:
openai:gpt-5: 7
anthropic:claude-sonnet-4-6: 3
- default: openai:gpt-5
on_failure:
- gemini:gemini-2.5-flash7:3 and 70:30 describe the same split. A candidate omitted from
weights receives no initial traffic but remains available in the failure
ordering. At least one weight must be positive.
Each request is a fresh draw. Weighted routing has no conversation stickiness or
health-adjusted weights. If the selected provider fails before responding, Otari
continues through the remaining weighted pool, then on_failure.
Caller allow-lists filter candidates before weights are normalized. Use
otari routing explain to see the effective split for a restricted caller.
The knn router uses scored examples to rank candidates for each user's
traffic:
routing:
policies:
smart:
select:
- router: knn
candidates:
- openai:gpt-5-nano
- openai:gpt-5
- default: openai:gpt-5
on_failure:
- anthropic:claude-haiku-4-5It embeds the request, finds similar scored prompts, and balances predicted quality against configured model cost. Every candidate therefore needs pricing. Until a user's pool is warm, or when the router cannot decide confidently, the default target serves.
Teach the router through POST /v1/routing/preferences/rank. Each example
contains a prompt and a score from 0 to 1 for each candidate. Read pool status
through GET /v1/routing/status. scripts/seed_routing_demo.py provides a
runnable example.
Learned memory is scoped by user and workspace. Optional task IDs create separate pools. It is not learned automatically from live traffic.
| Header | Effect |
|---|---|
Otari-Router: off |
Skip learned or weighted selection and use the policy default. |
Otari-Conversation-Id |
Reuse a learned decision for a conversation when granularity is trace_sticky. |
Otari-Router-Task |
Use examples from one task partition. |
The default learned-routing settings are k=5, a 20-example warm-up, and
trace_sticky granularity. They are deployment settings named
router_k, router_seed_count, router_alpha,
router_confidence_floor, router_embedding_model, router_granularity, and
router_max_records_per_user.
Trace stickiness is process-local. A restart or request routed to another replica may choose again. The result is still valid, but prompt-cache locality is not guaranteed across replicas.
A policy can run guardrails even when the caller did not request them:
routing:
policies:
safe:
select:
- default: openai:gpt-5-mini
guardrails:
- profile: prompt-injection
mode: block
on_unavailable: blockmode is required. on_unavailable: block fails closed when the guardrail
service cannot run; monitor lets the request continue and records the skipped
check. Only input checks can be mandated by a policy.
Organization-mandated and request-provided guardrails compose with policy guardrails. See Guardrails.
Inspect a policy without calling a provider:
otari routing explain fast
otari routing explain thrifty --budget-used-pct 85
otari routing explain balanced --allowed-model "anthropic:*"The command shows ordered candidates, filtered candidates and their reasons,
effective weighted shares, and mandatory guardrails. The API equivalent is
POST /v1/routing/policies/explain; it can also validate an unsaved draft.
Config-file policies apply to every workspace. Standalone operators can also
manage stored policies through the Routing page or /v1/routing/policies.
Stored policies belong to one workspace and can optionally be scoped to one
user.
An organization's owners and admins manage their own workspaces' policies and
aliases through /v1/organizations/me/routing-policies and
/v1/organizations/me/aliases, which the Routing page uses for a caller who
does not operate the deployment. Those routes require the workspace named, must
name a workspace of the caller's own organization, accept no user scope, and
refuse a target the organization holds no provider access for. Both reads take a
server-capped limit, which bounds the response rather than paginating it. A
member of the organization reads the same two lists and writes neither.
Those routes are workspace-wide entries only, on the reads as much as the
writes. A stored policy or alias can also be scoped to one user, and user_id
names an API-key user rather than a dashboard identity, so it is neither a
tenant's to set nor a tenant's to interpret. Managing a user-scoped entry stays
on the deployment-wide routes.
The authenticating API key determines which workspace resolves a policy. User-scoped entries take precedence over wider entries. A config-file policy cannot be changed through the API.
Changes apply immediately on the worker that accepts the write. Other workers refresh stored routing configuration within 30 seconds.
Use the generated OpenAPI document for create, rename, delete, list, and explain request schemas.
- A plan may contain at most five candidates, including router pools and
on_failure. - Targets must be concrete provider or instance selectors. Policies and aliases cannot chain.
- Policy names cannot contain
:or/or collide with an alias or provider instance. routing.enabled: falsedisables policy resolution but still validates the configured policies.- Caller model allow-lists apply to every candidate.
- Dynamic policies apply to Chat Completions, Messages, and Responses. A static one-target policy can resolve on other model-taking endpoints.
Pricing, budgets, and usage use the resolved model. Completion responses use the policy name.
Each failed attempt before a successful fallback gets an absorbed usage row.
All attempts share a request_group_id. Absorbed rows have no settled model
cost and do not increase request or error totals; the final row represents the
caller-visible request.
Built-in tool charges settle on the final row. Candidate price and remaining budget are checked before each attempt, so a fallback cannot silently bypass pricing or spend limits.
Failover occurs only before response bytes reach the client. A streaming failure after the stream begins is returned to the client because switching models mid-answer would corrupt the response.
A tool loop that has already produced assistant state cannot be replayed on another provider. Shared service failures, such as an unavailable mandatory guardrail or sandbox, also do not improve by changing candidates.
If every candidate fails, Otari returns a gateway error based on the final failure. If caller restrictions remove every candidate, Otari returns 403 without revealing the hidden targets.