Architecture
How a request becomes a decision: signals, policy, strategies, utility, plans.
OpenSmartRoute is a decision layer, not a gateway. Given a request and a catalogue of targets it returns which target should handle this, how confident we are, and why — and it learns from what happened next. Executing the target is optional and pluggable.
flowchart LR
subgraph In
REQ[RouteRequest]
end
subgraph Middleware
G[Guard] --> T[Tenant] --> C[Cache]
end
subgraph Core
S[Signals] --> P[Policy] --> ST[Strategies] --> E[Ensemble] --> D[Decision + Trace]
end
subgraph Learn
O[Outcome] --> AL[AutoLearner] --> ST
O --> H[Health] --> P
end
REQ --> G
C --> S
D --> X[Executor / handler]
X --> O
CAT[(TargetRegistry)] --> PLayers
| Layer | Package | Responsibility | Depends on |
|---|---|---|---|
| Core model | core | RouteTarget, RouteRequest, RouteDecision, RouteTrace, Outcome, TargetRegistry | stdlib |
| Signals | signals | Deterministic features of a request: complexity, domain/action, language, modality, PII, jailbreak, token estimate; opt-in verbalised difficulty, draft-response and response-uncertainty features (signals.uncertainty) | core |
| Policy | policy | Hard constraints → admissible set + rejection reasons. Never traded off. | core, signals |
| Strategies | strategies | Soft scorers ∈ [0,1] with rationale: rules, capability fit, similarity, Thompson bandit, LLM judge, cascade executor; opt-in hidden-state probe, token budgets, edge/cloud tiers, auctions; post-answer controls (self-escalation, mixture-of-agents, protocol selection) | core, signals |
| Learning | learning, math | Strategies that update from outcomes (IRT, Bradley–Terry, LinUCB, Markov/MDP, multi-turn history embeddings, per-user shrinkage) and the pure-math they rest on (bandits, calibration, Dirichlet probe, energy model, mixture-cure hand-off); AutoLearner fan-out, drift, atomic persistence; the routing SLM (RouterSLM, SLMStrategy, distill_router) and its champion/challenger SelfImprover | strategies |
| Discovery | discovery, retrieval | Retrieve-then-rank narrowing, schema-aware tool matching, cache-preserving tool order, skill graphs and submodular skill sets for large catalogues | core, strategies |
| Router | router | Orchestrates signals → policy → strategies → confidence-weighted ensemble → decision; optional judge escalation; optional plan (persona → skill → model) | all above |
| Real-time | realtime | Circuit breaker, token bucket, rolling budget, HealthRegistry; HealthPolicy (hard) and HealthStrategy (soft) | policy, strategies, math |
| Enterprise | enterprise | RouterBuilder, EnterpriseRouter, middleware chain, telemetry/state/audit ports and in-process adapters | router, realtime, learning |
| Security | security | InputGuard (size, normalisation, confounder gadgets), Redactor, GuardMiddleware, OutputGuard (response-side PII / secrets / injection: flag, redact or block), moderate() behind /v1/moderations, OriginPolicy (tool-parameter provenance), ResourceLimits (steps, tool calls, tokens, cost per task), prompt sanitiser, secret loading, the safety red-team suite | enterprise |
| Adapters | adapters | OpenAI-compatible HTTP client + glue (judge, embedder, handler); agent harnesses; MCP clients (stdio and Streamable HTTP: tools, resources, prompts as targets) and server recommendation, A2A cards (task polling), LangGraph and Microsoft Agent Framework glue, SKILL.md and persona loaders, semantic-router import; marketplace client and MarketplaceSync; live model catalogue (OpenRouter prices, Hugging Face cards) and web-search knowledge (https-only, byte-capped, risk-scored); optional sentence-transformers and OpenTelemetry | strategies, enterprise |
| Config | config | JSON/YAML loaders for targets and rules with validation errors | core, strategies |
| Protocols | responses, anthropic, media, execution, compress, estimate | One routed chat call behind several client dialects: OpenAI Chat Completions and Responses, Anthropic Messages, speech / transcription / image generation; the plan executor with tool rounds and fallbacks; prompt compaction; the pre-call cost estimate | router, adapters |
| Surface | aio, server, mcp_server, cli, stack, ocm, eval | Async façade; FastAPI app (/route, /feedback, /v1/*, /mcp, tokens, guard / health / audit / state-store flags); the MCP server (stdio and Streamable HTTP); the osr CLI; declarative stacks; Open Capability Manifests; RouterBench-style evaluation, off-policy evaluation and dataset collection from the Hugging Face Hub (eval.collect) | router, enterprise |
Dependency direction is strictly downward in this table. math depends on nothing but stdlib and
is usable on its own.
The decision pipeline, precisely
- Signals —
extract_signals(request); ~0.1 ms; no I/O. - Candidate pool —
registry.all(primary_only=True), optionally filtered bykinds. - Policy —
Policy.filter(pool)→(admissible, rejections).HealthPolicyadds breaker/rate/budget checks (non-consuming). Empty admissible set →NoRouteErrorwith the reasons. - Pinned rules — a matching
Rule(pin=True)narrows the admissible set (Arch-Router semantics). - Scoring — every strategy returns
{target_id: StrategyScore(score, rationale, confidence)}. - Ensemble — quality estimate where is the strategy's self-confidence (learners report , so they are silent until they have evidence). Utility with log-min-max normalised cost/latency and a hard quality floor.
- Confidence — softmax margin over utilities (). If below
escalate_llm_judge_belowand a judge is configured, re-score once with the judge included. - Decision — top target, ranked alternatives, full trace. Optionally a
RoutePlanwhere each slot (persona, skill, llm, tool) is its own sub-routing over that kind; a slot is left empty when its best candidate's quality estimate is belowslot_quality_floor(tool_quality_floorfor tools), so irrelevant skills and tools are never attached. A model or agent primary gets up totool_slotstool slots - the functions it may call. - Execute (optional,
execution.py) —Router.run()walks the plan: theskillslot's handler runs as a pre-processor (may rewrite the request or attachskill_output), persona/skill/primaryinstructionsare composed intocontext["system"], tool slots become function-calling specs incontext["openai"]["tools"], the prompt is compacted (compress.py: whitespace, repeated lines, history budget; code fences untouched), then the primary handler is called (chat_handler, a skill function, or an agent harness viaCallableHarness/HTTPHarness/SubprocessHarness). Tool calls the model makes to offered tools are run and fed back for up totool_roundsrounds. OneOutcomeper participant (role=None|"persona"|"skill"|"tool", sharedtask_id) is recorded — success, latency, cost from tokens × unit cost. - Learn —
Outcome→ every strategy'supdate(), drift detectors, health registry, feedback store.
Extension points
| To add… | Implement | Register via |
|---|---|---|
| a target | RouteTarget(...) or a YAML entry | registry.add() / load_targets() |
| a signal | SignalExtractor.extract() | Router(extractors=[...]) |
| a hard rule | Policy.check() → reason or None | RouterBuilder.with_policy() |
| a scorer | Strategy.score() (+ update() if it learns) | with_strategy(s, weight) |
| pre/post processing | Middleware.__call__(request, next_) | with_middleware() |
| metrics/tracing | Telemetry.on_decision/on_outcome/on_error | with_telemetry() |
| span/event capture | EventSink.emit(event) (+ span_start/span_end for live bridges) | with_tracing(sink) / configure_tracing(sink) |
| persistence | StateStore.get/put/delete | pass to learners / caches |
| a provider | RouteTarget.handler = chat_handler(client, model) | adapters.OpenAICompatClient |
Concurrency model
Router.route()is pure with respect to router state (no writes) and safe to call from many threads.- Learners, health registry, caches and stores take their own locks on write.
AsyncRouterruns routing in the default executor and awaits coroutine handlers.- Multi-replica: learner updates are commutative sums (Beta counts, IRT gradients are small and
order-insensitive in practice, BT strengths, LinUCB
A/b, Markov counts), so replicas can be folded together withmerge()/learning.merge_learners(), or readers can adopt a writer's snapshot through aStateStorewithAutoLearner.refresh(). Corrupt snapshots are quarantined, never fatal (AutoLearner.quarantined).
Performance envelope
Measured with the reproducible routing benchmark, 300 requests per configuration (CPython 3.11, x86-64 Windows laptop, pure
Python, no C extensions; 2026-10-09). Numbers are wall-clock per route() including signal extraction, policy,
every strategy and the ensemble; re-run the benchmark on your own hardware before sizing. The continuous build
runs the same benchmark and fails when the full configuration's p95 passes 50 ms, so the overhead cannot drift
up unnoticed.
27-target example catalogue with its rules (examples/targets.yaml, examples/rules.yaml, examples/eval_dataset.jsonl):
| Configuration | p50 | p95 | p99 |
|---|---|---|---|
| rules + capability + similarity + bandit | 4.6 ms | 6.6 ms | 8.3 ms |
| + IRT + preference + LinUCB + Markov + health | 7.4 ms | 9.1 ms | 14.1 ms |
Scaling with catalogue size (synthetic LLM catalogue, defaults + auto-learning). Cost is linear in
admissible targets x strategies until retrieval narrowing (BM25 + hashed dense, reciprocal-rank fusion)
caps the candidate pool; the default Settings.routing only narrows above 500 admissible targets (to 50),
RouterBuilder.with_retrieval(narrow_above=32, narrow_to=24) turns it on earlier:
| Targets | Narrowing | p50 | p95 | p99 |
|---|---|---|---|---|
| 16 | default (none) | 9.2 ms | 9.5 ms | 10.0 ms |
| 16 | top-24 above 32 (none triggered) | 9.2 ms | 9.6 ms | 10.5 ms |
| 64 | default (none) | 35.7 ms | 36.6 ms | 37.1 ms |
| 64 | top-24 above 32 | 14.5 ms | 15.1 ms | 15.6 ms |
| 256 | default (none) | 142 ms | 145 ms | 158 ms |
| 256 | top-24 above 32 | 16.7 ms | 17.6 ms | 18.5 ms |
| 1024 | default (auto top-50) | 38.7 ms | 43.5 ms | 49.1 ms |
| 1024 | top-24 above 32 | 24.1 ms | 28.2 ms | 32.9 ms |
Rule of thumb: keep the scored pool at or below ~50 targets and routing stays under 25 ms p99 in pure
Python regardless of catalogue size; the retrieval stage itself is ~10 ms at 1k targets and grows
roughly linearly in the catalogue. The LLM judge, when triggered, adds one provider round-trip; keep
escalate_llm_judge_below low (<= 0.5) so it fires only on genuinely ambiguous requests.
Deployment shapes
- Library — import in-process. Lowest latency. State on local disk or in-memory.
- Sidecar —
osr serveper application (container image fromdeploy/Dockerfile); sharedStateStorefor global learning. - Control plane — one HA deployment (the published Helm chart); a gateway
(LiteLLM, Envoy, aisix) calls
/route, executes the chosen target, and posts/feedback. - Hosted platform — the same router behind accounts, API keys, plans and quotas, BYOK providers,
tenants, SSO, billing and the dashboard (Platform guide),
reached over REST, the
/v1dialects, MCP, the Python CLI (osr login) or the TypeScript client (@opensmartroute/sdk, which also targets anosr serve).
Multi-replica learning: run one writer replica that learns and saves through a StateStore, and have
readers call AutoLearner.refresh() on a timer; or let every replica learn independently and fold them
together periodically with learning.merge_learners(). See Deploy with Docker and Helm.
Non-goals
Model serving, prompt versioning, chat UI, and provider-to-provider protocol translation as a product:
the responses, anthropic and media modules exist so that clients speaking the OpenAI or Anthropic
dialects can reach the router with one base-URL change - each becomes one routed chat call, not a
general translation layer.
Something wrong or out of date on this page? Tell us - the page is named for you, so a one-line note is enough.