`, other repo paths to GitHub blob/tree, images to raw GitHub) ->
Shiki (`github-dark-default`, languages listed in `CODE_LANGS`) -> HTML. ```` ```mermaid ```` fences
become `` and render client-side in `components/docs/docs-article.tsx`.
4. **REST API reference** - `src/lib/docs/openapi.ts` reads the snapshot and groups operations by
tag; `components/docs/api-reference.tsx` renders the overview (`/docs/api`: auth schemes, base URL,
conventions, groups) and one page per tag (parameters, request body, responses, curl and JSON
examples). Example bodies come from `exampleBodyFor()`; add a hint there for a new request model.
5. **Versions** - `src/lib/docs/versions.ts` parses `## [x.y.z] - date` headings from `CHANGELOG.md`;
`components/docs/docs-topbar.tsx` shows the current release and a menu that links each release (and
the unreleased section) to `/docs/changelog#`. A release needs no code change here.
6. **Pages** - `src/app/(docs)/docs/page.tsx` (index: intro, install, entry points per audience, one
list per section), `[...slug]/page.tsx`
(`generateStaticParams` from the catalogue, `dynamicParams = false`, breadcrumb, reading time, edit
link, prev/next; API pages branch on `kind === "api"`), `search.json/route.ts` (static index; API
pages index their operations) consumed by `components/layout/search-command.tsx`. `sitemap.ts`
lists every catalogue entry.
To publish a new user-facing document: add the Markdown under `docs/`, add a `STATIC_SECTIONS` entry
in `catalogue.ts` (slug, file, title, description, icon), link it from `README.md`
(`tests/test_docs.py` requires every `docs/*.md` to be linked), run `npm run build` and open
`/docs/`. Internal documents are linked from `README.md` only. A new
`.claude/skills//SKILL.md` needs no code change. When a heading is renamed, search for the old
anchor across `docs/`, `README.md` and `platform/web/src`.
Style: the docs pages are plain reference pages - headings, paragraphs, tables and code blocks. No
icon cards, badges, pills, uppercase eyebrow labels, hover animations or drop shadows; no reading
time or page counts; copy states what an endpoint or option does, without marketing adjectives.
## Deploy
```
azd provision # infra/main.bicep: Container Apps osr-api + osr-web, Azure OpenAI, ACR, Files share, DNS zone
azd deploy api # platform/api/Dockerfile, context = repo root
azd deploy web # platform/web/Dockerfile, context = platform/web (prepackage hook syncs content/)
azd deploy platform # same Dockerfile -> osr-platform (serves /platform; osr-web forwards to it)
.\scripts\domains.ps1 # custom domain: dns (zone + name servers) -> bind (certs) -> verify; -Phase clean retires osr-platform
.\scripts\sso.ps1 # sign-in providers: microsoft (az app registration + secret) | google | github | gitlab | show | push | apply
```
JSON-valued deployment inputs (`OSR_PLATFORM_SSO_PROVIDERS_B64`, `OSR_PLATFORM_STRIPE_PRICES_B64`) travel
base64-encoded: azd substitutes `${VAR}` textually into `infra/main.parameters.json`, so raw JSON breaks every
provision ("invalid character after object key:value pair"); `infra/main.bicep` decodes with `base64ToString`.
`sso.ps1` is the only writer of the SSO map (local azd env -> `gh secret set` -> `azd provision`); every provider
uses the single redirect `/auth/callback` (kept without the `/platform` prefix on purpose - the web app
308s it to `/platform/auth/callback`, so the URI registered at every provider never changes). Google and GitHub have no API for OAuth clients, so those
commands print the console steps and prompt for the id and hidden secret.
`.github/workflows/platform.yml` runs API tests and the OpenAPI snapshot check, `npm run check && npm
run build`, a two-container smoke (`/`, `/docs`, `/docs/GUIDE`, `/docs/PLATFORM`, `/docs/api`,
`/docs/api/routing`, `/docs/skills/osr-contributing`, `/docs/search.json`, `/pricing`, `/api/v1/info`;
`/docs/sales/*` must 404) and then `azd up` on `main`. After deploying, verify `/docs/api` and
`/api/v1/info` (`edition`, `sdk_version`). `NEXT_PUBLIC_SITE_URL` is baked at build time via
`buildArgs` in `azure.yaml`; SQLite on Azure Files needs `nobrl` mount options and a single API replica.
The public domain is an Azure DNS zone (`infra/dns.bicep`, azd `OSR_DNS_ZONE=opensmartroute.ai`): apex A to the
environment static IP, `www`/`api` CNAMEs, `asuid.*` TXT; the registrar delegates to the zone's name servers.
`www` redirects come from `platform/web/src/proxy.ts` (`OSR_WEB_REDIRECT_HOSTS`), not from an edge proxy.
Apex managed certificates use `HTTP` domain-control validation, subdomains `CNAME` (`TXT` never completes for an
apex). The `azd up` job signs in with an OIDC federated credential whose subject is
`repo:isathish/OpenSmartRoute:environment:production` (repository variables `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`,
`AZURE_SUBSCRIPTION_ID`, custom-domain `OSR_*` vars; secret `OSR_PLATFORM_ADMIN_TOKEN`) and seeds
`SERVICE_*_IMAGE_NAME` from the running apps before `azd provision`; setup in `platform/README.md`
("Continuous deployment").
---
---
name: osr-routing-catalogue
description: Define and tune an OpenSmartRoute routing catalogue. Add a new target to targets.yaml (LLM, agent, skill, persona, tool, human) with capabilities, domains, a complexity band, cost, latency and a quality prior. Write a rules.yaml rule that prefers, avoids or pins targets, for example pin PII requests to an on-prem model. Set hard constraints (region, data boundary, tenant, max cost) and objective weights per request. Build plans and close the learn loop with Outcomes. Debug why a request routed to a given target with the trace. Use when editing targets or rules YAML or explaining a routing decision.
license: Apache-2.0
compatibility: OpenSmartRoute >= 0.4, Python >= 3.10
metadata:
author: opensmartroute
osr-domains: "coding general"
osr-tags: "opensmartroute routing catalogue"
osr-quality-prior: "0.85"
osr-primary: "false"
---
# OpenSmartRoute routing catalogue
Routing is `signals -> policy -> strategies -> ensemble -> decision (+plan)`. The catalogue is
data: add a target and it is routable immediately, no retraining.
## Target schema (`targets.yaml` / `.json`, or `TargetRegistry.from_dicts`)
Top level is `targets: [...]` (a bare list also works). Keys are exactly the `RouteTarget` fields -
unknown keys raise `ConfigurationError`.
```yaml
targets:
- id: llm-small # required, unique; used in rules, outcomes, plans
kind: llm # llm | agent | skill | persona | tool | workflow | human | destination
name: Small fast model
description: Cheap, fast generalist for simple chat, classification and extraction.
capabilities:
domains: [general, customer_support] # match signals.domains
actions: [qa, classify, extract, summarize]
languages: [en, es] # default [en]; "*" = any
modalities: [text] # default [text]
min_complexity: 0.0 # request complexity band this target fits
max_complexity: 0.45
context_window: 128000 # optional; policy rejects oversize inputs
supports_tools: true
tags: []
constraints:
regions: [us, eu] # empty = anywhere
data_boundary: public # public | private | on_prem
pii_allowed: true
max_tokens_in: null
tenants: [] # empty = every tenant
cost: { usd_per_1k_tokens: 0.0002 } # also wh_per_1k_tokens, gco2_per_1k_tokens
latency_ms: 300
quality_prior: 0.55 # 0..1 belief before any outcomes
examples: ["Hi, how are you today?"] # anchor the similarity strategy
instructions: "" # system prompt fragment, disclosed only when selected
primary: true # false = slot-only (persona/skill layered on a model)
family: "" # siblings share breaker/budget state
effort: "" # reasoning-effort variant label
enabled: true
metadata: {} # free-form; keys read by optional strategies below
```
`metadata` keys that optional strategies read (all opt-in, none required):
- `tier: edge | cloud` - `strategies.EdgeCloudStrategy` (edge decode time vs SLO, cloud upload
penalty, cloud excluded for private / on_prem requests).
- `input_schema: {properties: {...}, required: [...]}` on `kind: tool` - `discovery.SchemaAwareStrategy`
scores how many required parameters the request text can fill (MCP imports set it automatically).
- `requires`, `conflicts`, `composes` (lists of skill ids) on `kind: skill` - `discovery.SkillGraph`;
SKILL.md packages declare them as `osr-requires` / `osr-conflicts` / `osr-composes` frontmatter.
- Elastic models: do not hand-write `@` siblings; call
`strategies.expand_elastic(parent, [BudgetVariant(tokens, quality=, cost_scale=)])` and add the result;
it sets `metadata.token_budget`, which `strategies.TokenBudgetStrategy` scores against the tokens
the answer needs.
Guidance:
- Give every target 2-5 `examples` phrased like real requests; similarity is the cheapest strong signal.
- Set `min_complexity`/`max_complexity` bands so the capability strategy can penalise overkill and
too-hard routes; overlap bands slightly.
- `quality_prior` is a prior, not a score - bandits/IRT move it with `Outcome`s.
- Personas and skills are usually `primary: false`; they fill plan slots.
- Python handlers: `registry.get("id").handler = fn` where `fn(request, **kw) -> response`.
## Rules (`rules.yaml`, Arch-Router style)
```yaml
rules:
- name: pii-stays-onprem
when: { contains_pii: true } # domains, actions, language, min/max_complexity,
prefer: [llm-onprem, human-escalation] # contains_pii, needs_tools, context: {key: value}
avoid: [llm-frontier]
weight: 1.0
pin: false # true = restrict candidates to `prefer`
```
Rules push scores; they never override policy. There is no `boost` key - use `weight`.
## Hard constraints and objectives (per request)
```python
from opensmartroute import RouteRequest, RequestConstraints, Objective
req = RouteRequest("Summarise this contract",
constraints=RequestConstraints(region="eu", data_boundary="private", max_cost_per_1k=0.01,
max_latency_ms=3000, allowed_kinds=["llm"], deny_targets=[],
tenant="acme", require_tools=False),
objective=Objective(quality=1.0, cost=0.3, latency=0.05, quality_floor=0.6, energy=0.0, carbon=0.0))
```
Constraints are filtered by `Policy` (rejections appear in `decision.trace.policy_rejections`);
objective weights shape the utility `quality - cost*norm(cost) - latency*norm(latency)`.
Request `context` keys that strategies read: `user_id` (`learning.UserAdaptiveStrategy`),
`last_target` and `last_failed` (`learning.HistoryTargetStrategy` incumbent bonus / penalty),
`difficulty` (`signals.VerbalisedDifficultySignal`), `draft` (`signals.DraftResponseSignal`),
`session_id` (`MarkovStrategy` transitions). Pass prior
turns in `RouteRequest.history=[{"role": ..., "content": ...}]` so multi-turn strategies see them.
## Route, plan, learn
```python
from opensmartroute import Router, Outcome, load_targets, load_rules
router = Router(load_targets("targets.yaml"), strategies=[load_rules("rules.yaml"), *default_strategies()])
d = router.route(req, plan=True) # d.target, d.confidence, d.alternatives, d.plan.slots, d.trace
print(d.trace.explain()) # per-strategy scores for every candidate
router.learn(Outcome(d.request_id, d.target.id, success=True, quality=0.9, cost_usd=0.002,
latency_ms=1200, domains=d.trace.signals.domains, complexity=d.trace.signals.complexity))
res = router.run(req) # route + execute plan; res.text, res.outcomes, res.system_prompt
```
CLI equivalents: `osr -t targets.yaml -r rules.yaml route "text" [--plan] [--json]`,
`osr -t targets.yaml targets`, `osr -t targets.yaml stats`.
## Debugging a surprising route
1. `d.trace.explain()` - check `policy: N -> M candidates` and the rejection reasons first.
2. Compare `capability` (domain/action/complexity fit) vs `similarity` (examples) columns; fix the
catalogue (examples, complexity band) before touching weights.
3. Weights and thresholds live in `Settings` (`osr settings`); override with `OSR_WEIGHTS_`
or `Router(weights={...})` only after the catalogue is right.
---
---
name: osr-security-hardening
description: Harden OpenSmartRoute deployments against prompt injection, gadget/steering attacks, PII leakage, runaway agents and untrusted tool arguments - InputGuard and GuardMiddleware, learned gadget detector, PII redaction, ResourceLimits for tools and agents, OriginPolicy for tool-parameter provenance, signed MCP manifests, secret loading and the safety-routing red-team suite. Use when reviewing security of a routing setup, handling PII or private data boundaries, or wiring MCP tools safely.
license: Apache-2.0
compatibility: OpenSmartRoute >= 0.4, Python >= 3.10
metadata:
author: opensmartroute
osr-domains: "coding legal general"
osr-tags: "opensmartroute security injection pii mcp"
osr-quality-prior: "0.85"
osr-primary: "false"
---
# OpenSmartRoute security hardening
Threat model: the router sits between untrusted text (users, retrieved documents, tool output) and
expensive or state-changing targets. Defend the input, the catalogue, and the execution budget.
## 1. Input guard (every deployment)
```python
from opensmartroute.security import InputGuard, GuardMiddleware
guard = InputGuard(max_chars=32_000, max_tokens_est=8_000, gadget_threshold=0.6,
reject_on_gadget=False, learned=False, strip_steering=True)
app = RouterBuilder(reg).with_defaults().with_middleware(GuardMiddleware(guard, redact=True)).build()
```
- `guard.inspect(text) -> GuardReport(ok, reasons, gadget_suspected, clean_text, score, steering_removed)`.
- `GuardMiddleware` raises `SecurityError` when `not ok`; with `redact=True` PII is replaced before
routing (original in `request.context["_original_text"]`, map in `context["_pii_map"]`).
- `learned=True` uses `GadgetDetector.default()`; train your own with `osr train --gadget gadget.json`
and `GadgetDetector.load`.
- Pure functions for ad-hoc checks: `injection_risk(text)`, `inspect_injection(text)`,
`strip_steering(text)`, `sanitize_for_prompt(text)`, `shannon_entropy(text)`.
## 2. Data boundaries and PII in the catalogue
- Targets declare `constraints.data_boundary: public|private|on_prem` and `pii_allowed`. Requests set
`RequestConstraints(data_boundary=..., contains_pii=...)`; the `PIISignal` also detects PII.
- Policy rejects mismatches (`DataBoundaryRule`, `pii` rule) - verify in `trace.policy_rejections`.
- Pair with a rule: `when: {contains_pii: true} prefer: [llm-onprem] pin: true`.
- Tenant isolation: `constraints.tenants` on targets + `TenantMiddleware(require=True)`.
## 3. Budgets for tools, agents and workflows
```python
from opensmartroute.security import ResourceLimits, ResourceLimiter, ResourceLimitMiddleware, apply_limits
limiter = ResourceLimiter(ResourceLimits(max_steps=50, max_tool_calls=100, max_depth=4,
max_total_tokens=500_000, max_cost_usd=5.0, max_wall_s=900.0))
apply_limits(registry, limiter, kinds=("tool", "agent", "workflow")) # wraps handlers per task_id
builder.with_middleware(ResourceLimitMiddleware(limiter))
```
Exceeding a limit raises `ResourceLimitExceeded` (a `SecurityError`). Budgets key on
`request.context["task_id"]`.
## 4. Tool-parameter provenance (indirect injection)
```python
from opensmartroute.security import OriginPolicy, OriginRule, mark_untrusted, apply_origin_policy
mark_untrusted(request, retrieved_text, source="retrieved") # tag text you did not author
policy = OriginPolicy(rules=[OriginRule(state_changing=["delete_*", "send_*"], allowed_origins=["user"])])
apply_origin_policy(registry, policy) # wraps tool handlers
```
A state-changing tool whose arguments were copied from untrusted text raises `OriginViolation`.
## 5. MCP catalogues
- `tools_from_mcp(payload, max_description_risk=0.6, guard=InputGuard())` drops tools whose
descriptions look like steering ("ignore previous instructions ...").
- Sign catalogues offline and verify at load: `osr mcp-manifest tools.json --key-env OSR_MCP_KEY
--server name --out manifest.json`; then `tools_from_manifest(manifest, key, require_signature=True,
max_age_s=86400)`. `--algorithm ed25519` needs the `crypto` extra.
## 6. Secrets
`load_secret("OPENAI_API_KEY")` reads the env var or the file named by `OPENAI_API_KEY_FILE`
(Kubernetes secret mounts). Never put secrets in `targets.yaml`; handlers read them at call time.
Telemetry never logs raw text.
## 7. Red-team suite
`osr -t targets.yaml safety [--learned-guard] [--json]` or `run_safety_suite(app.route)` runs
`SafetyCase`s (jailbreaks, PII exfiltration, boundary hops, cost bombs) and reports `pass_rate` and
`by_category`. Add it next to `osr eval --min-accuracy` in CI.
## Review checklist
- [ ] `GuardMiddleware` first in the chain, `redact=True` when any target is `pii_allowed: false`.
- [ ] Every private/on-prem target has `data_boundary` and `regions` set; requests carry tenant.
- [ ] `apply_limits` on all `tool`/`agent`/`workflow` targets; `max_cost_usd` matches the tenant budget.
- [ ] MCP tools loaded from a signed manifest; `max_description_risk` left at or below 0.6.
- [ ] Secrets via `load_secret`; audit sink enabled; `osr safety` passes in CI.
---
# Changelog
All notable changes are documented here. Format follows [Keep a Changelog](https://keepachangelog.com);
versions follow [SemVer](https://semver.org). Releases are cut by the `Prepare release` workflow
(`scripts/release.py`), which turns the Unreleased section into a dated section.
## [Unreleased]
### Added
Workspace self-service: outcome reporting in the dashboard, key rotation, data export
- Outcomes without curl: `components/dashboard/outcome-form.tsx` - good / poor, a quality slider and "this
target would have been better" (recorded as a pairwise preference) - on every expanded activity row and in
the request trace; the onboarding step "report an outcome" now links to the activity page (or billing on
plans without the feature).
- API keys: `PATCH /api/v1/keys/{id}` renames, `POST /api/v1/keys/{id}/rotate` issues a new secret for the
same key in one transaction (shown once; the old secret dies immediately; id, name and creation date stay,
so audit rows and dashboards remain attached). The keys page renames inline and rotates with a warning.
- Data portability: `GET /api/v1/me/export?days=` returns one JSON document with the person (profile,
identities, sessions, memberships, history) and the workspace (plan, members, key prefixes, tenants,
policy, SSO, usage, request log, reported outcomes); *Download my data* on the account page. Secrets and
password hashes never appear; tests assert the export never contains the API key or password.
Machine-readable site: real llms.txt, Markdown pages, complete OpenAPI
- Every documentation page is served as Markdown at `/docs/.md` (relative links resolved to the site or
the repository, SKILL.md frontmatter stripped) and advertises it with a `text/markdown` alternate link;
`/llms.txt` now links to those Markdown sources as llmstxt.org intends and `/llms-full.txt` is the whole
documentation in one file. `/openapi.json` on the website serves the complete committed OpenAPI document
(146 paths, `servers` set to the site) instead of proxying the gateway process, whose own document only
lists the routes it serves itself once domains run as separate services (4 paths locally, 19 in
production). Footer: `llms-full.txt` added, *Discussions* (not enabled on the repository, a 404) replaced
by *Issues*; the support page sends questions to a labelled issue. Tests: `platform/web/tests/llms.test.ts`.
Operator console: analytics, health, request log, marketplace moderation, leads, plans, configuration, search
- `platform/api/osr_platform/admin_console.py`: `GET /api/v1/admin/analytics?days=` (deployment-wide daily
requests / failures / cost / tokens / active workspaces / mean latency, per endpoint, per target, busiest
workspaces, signups per day), `GET /api/v1/admin/activity` (every metered request across workspaces with
cursor paging and endpoint / target / failed / workspace / request-id filters), `GET /api/v1/admin/lookup?q=`
(users, workspaces, API keys by prefix, operators, tenants, request ids - each with its console page),
`GET /api/v1/admin/settings` (effective configuration grouped with the `OSR_PLATFORM_*` variable of every
value; secrets and nested `client_secret` / `api_key` values are masked), `GET /api/v1/admin/retention` and
`POST /api/v1/admin/retention/purge` (run the sweep now), and `GET /api/v1/admin/health?window=` served by
the routing process itself (readiness, HTTP series, last five minutes, circuit breakers, observability,
deployment alerts, the alert evaluator, replica host).
- Console pages `/platform/admin/analytics`, `/health`, `/activity`, `/leads`, `/plans`, `/marketplace`
(review queue and every status, approve / reject / unpublish with a note, featured and verified flags,
imported catalogue per source, scheduled refresh with *run now*), `/settings` (configuration + retention)
and `/search`; a search box in the console header opens the search page. Navigation regrouped into
Deployment, Directory, Learning, Platform and Access.
Operators see both portals
- Superadmins open any workspace's dashboard from the console: `POST /api/v1/admin/accounts/{id}/session`
mints a one-hour browser session on the workspace as its owner (provider `operator`, visible in the owner's
session list, audited as `workspace.open`); *Open dashboard* on `/platform/admin/workspaces` and on each
workspace page switches to the workspace portal, which shows an *Operator view* banner (who, which
workspace, when the session ends) with *Back to console* and *End operator view* (revokes the session).
The view sees every plan feature (statistics, audit, tenants ...) whatever the workspace's plan - quotas
stay the workspace's - so no page is hidden behind an upgrade prompt for the operator.
The console's account menu leads to the workspace dashboard and the dashboard's account menu back to the
console while an operator session exists. `GET /api/v1/me` now includes `session` (null for API keys).
True streaming on the OpenAI-compatible endpoint
- `POST /v1/chat/completions` with `stream: true` now relays each provider token as it arrives instead of
completing the request first and replaying the finished text as chunks (time to first token was the whole
completion time - measured 2.55 s to the first of 39 chunks that then all landed within 130 ms). The final
chunk still carries `usage` and the `opensmartroute` metadata, the call is metered and learned from exactly
like the plain response, quota / routing / provider errors before the first token are ordinary HTTP errors,
`X-OSR-Target` / `X-OSR-Confidence` name the routed target, and the automatic fallback to the next target
applies only before the first token (`committed` in `_execute_with_fallback`).
- SDK: `OpenAICompatClient.chat(..., on_delta=)` / `stream_chat()` consume the upstream `stream: true`
server-sent events (with `stream_options.include_usage`; token counts estimated when the provider sends
none) and fall back to a single delta for providers that ignore `stream`; `chat_handler` forwards the
callback, so `execute(decision, request, on_delta=...)` streams through the whole SDK pipeline.
`platform/api/tests/test_streaming.py` drives a paced SSE upstream through the SDK client, the TestClient
and a real uvicorn server (tokens spread over the provider's pacing, not one burst).
Operator console: directory, audit and mail administration
- `GET /api/v1/admin/users` filters by `disabled`, `verified` (email confirmed) and `method` (`password` /
`sso`), sorts by `created_at`, `last_login_at`, `email` or `name` (`order=asc|desc`), returns the filtered
`total` and downloads the matching rows with `format=csv`; `POST /api/v1/admin/users/bulk` applies
`disable`, `enable`, `verify`, `revoke_sessions`, `reset_link` or `delete` to up to 500 users and reports each
as done or failed. `GET /api/v1/admin/accounts` gains `plan`, `disabled`, `sort`, `order` and `format=csv`.
- `GET /api/v1/admin/audit-log` filters by `actor_type`, `actor_id`, `after` and free text `q` (actor, IP,
details), returns the distinct `actions` with counts for the filter menu and exports with `format=csv`.
Every operator action (`operator.*`, `user.*`, `account.*`, `mail.*`, `users.bulk`, `*.export`) is now
recorded in `audit_log` with the operator as actor, not only mirrored on the event bus.
- Outbox: `GET /api/v1/admin/mail` filters by `kind` and `failed`, pages with `offset`, returns `total` and a
`summary` (composed, unsent, failed, kinds); `POST /api/v1/admin/mail/{id}/resend` delivers a message again
(409 without SMTP), `DELETE /api/v1/admin/mail/{id}` removes it, `POST /api/v1/admin/mail/test` sends a
delivery check to an address. `Mailer.resend()` and `Mailer.test_message()`.
- `GET|DELETE /api/v1/admin/operators/{id}/sessions`: superadmins inspect any operator's console sessions and
revoke one (`session_id`) or all of them; operators see their own.
- Console pages: `/platform/admin/users` (status / email / sign-in filters, sortable columns, last sign-in and
verified columns, row selection with a bulk-action bar, *Export CSV*), `/platform/admin/workspaces` (kind,
plan and status filters, sortable columns, *Export CSV*), `/platform/admin/audit` (action menu with counts,
actor, time window, user / workspace and text filters, *Load older*, *Export CSV*, click an action badge to
filter by it), `/platform/admin/mail` (counters, kind and delivery filters, paging, *Resend*, *Delete*,
*Send a test message*), `/platform/admin/operators` (*Sessions* of another operator with per-session and
sign-out-everywhere revocation).
SLO-aware routing (the platform plan's open "rolling p50/p90/p99 per endpoint" row)
- `realtime.LatencyWindow`: each target in `HealthRegistry` keeps its last 256 observed latencies with
nearest-rank p50 / p90 / p99 (`health_snapshot()` gains `latency_p50_ms`, `latency_p90_ms`, `latency_p99_ms`;
the platform's `/api/v1/stats` and the Health page show them next to the breaker state).
- `RequestConstraints.preferred_max_latency_ms`: a *soft* latency target. `HealthStrategy` now scores the
observed p90 (once five calls are in the window; EWMA, then the declared latency before that) against the hard
`max_latency_ms`, else this soft target, else `with_health(latency_slo_ms=...)`, so a target with a good mean
and a bad tail is penalised without being excluded. Accepted by `osr route --constraint
preferred_max_latency_ms=...`, the MCP `route` tool, `POST /api/v1/route` constraints and workspace / tenant
policy (`preferred_max_latency_ms` in `POLICY_KEYS`; the stricter of request and policy wins).
Dashboard chart kit
- `components/charts`: the platform's own chart primitives on top of the time-series ones - `RankedBars`
(horizontal ranking with values), `Donut` (share with a centre label), `Bars`, `ScatterPlot`, `Sparkline`,
`Meter` (bounded value against a threshold), `ShareBar` (stacked composition, HTML only) and `Heatmap`
(day x hour density, HTML only); `ChartTooltip`, `ChartLegend`, `ChartEmpty`, a `SERIES` palette with
`seriesColor()` and the theme tokens `--chart-axis`, `--chart-grid`, `--chart-cursor`, `--chart-other`
(light and dark). `components/dashboard/analytics.tsx` renders the API's breakdowns as charts instead of
tables: `TargetBreakdown`, `EndpointShare`, `TrafficHeatmap`, `SavingsChart` (routed against the baseline
per day), `SavingsByTarget`, `PlanMix`, `DecisionsBreakdown`, `LatencyHistogram`, `RouteTraffic`,
`BreakerGrid`, `StrategyWeights`, `OutcomesByTarget`. `StatCard` takes a `trend` (delta pill with
direction) and an `info` tooltip. Tests: `platform/web/tests/charts.test.tsx`.
Notifications and alert delivery
- Alerts are now delivered, not only shown. `platform/api/osr_platform/notifications.py`: the API
re-evaluates the workspace and deployment alert rules every `OSR_PLATFORM_ALERTS_INTERVAL_S` (60 s; one
replica through a Redis lease) and turns each condition into a notification episode (opened, escalated,
resolved) kept in an inbox with unread counts, published to the `alerts` Kafka topic in cluster mode and
delivered to channels: e-mail (platform mailer), `https` webhooks (JSON with `X-OSR-Event`,
`X-OSR-Delivery` and an HMAC `X-OSR-Signature`), Slack and Teams incoming webhooks. Channels filter by
minimum severity and rule names, keep last delivery / last error, have *Send test*; failed webhooks are
retried on the next rounds (three attempts). Endpoints `GET /api/v1/notifications[/unread]`,
`POST /api/v1/notifications/read`, `GET|POST /api/v1/notifications/channels`,
`PATCH|DELETE /api/v1/notifications/channels/{id}`, `POST .../channels/{id}/test`,
`GET /api/v1/notifications/deliveries`; operator equivalents under `/api/v1/admin/notifications` plus
`POST /api/v1/admin/notifications/evaluate`. Dashboard page `/dashboard/notifications` with a bell and
unread badge in the header; console page `/admin/notifications`.
The platform's own observability
- The platform observes itself; no external metrics, tracing or alerting system is involved (the OTLP bootstrap
and the Prometheus / Grafana / Jaeger compose profile added earlier were removed). `osr_platform.telemetry`:
a `DbEventSink` on the tracer persists every span and event to the platform database from a bounded queue
and a daemon writer (never on the request path; `OSR_PLATFORM_TELEMETRY_STORE`, on by default;
`OSR_PLATFORM_TELEMETRY_RETENTION_DAYS`, 14, purged by the retention sweep), so `GET /api/v1/events`
(`since` / `until`, `source`, `persisted`, `retention_start`) and `GET /api/v1/trace/{id}` (`source`) survive
restarts and `GET /api/v1/activity` marks persisted requests `traced`. `HttpMetrics.series` keeps per-minute
HTTP counters for 24 hours. `GET /api/v1/telemetry/series?window=1h|6h|24h|7d|30d` buckets the workspace's
metered traffic (requests, failures, p50 / p95, cost, tokens; per target and endpoint) and, with `stats`, the
deployment's HTTP requests, 4xx / 5xx and p50 / p95 / p99. `osr_platform.alerts` + `GET /api/v1/alerts`: rules
evaluated in-process - budgets at 80 % / exhausted (workspace and tenants), failing requests, and on `stats`
plans readiness, 5xx rate, latency SLO, open breakers, drift alarms, autopilot errors, a stale SLM, tracing
off - each naming the dashboard page that fixes it. Web: `AlertsPanel` and 24-hour traffic / latency series
on the overview, deployment HTTP series with a window selector and the alerts on Health, a 1 h - 7 d window on
Events served from the store, trace view labels the source. Tests: `platform/api/tests/test_telemetry.py`.
Platform services and model providers
- The platform API runs as one process or as several: `docs`, `rankings`, `marketplace`, `providers`,
`onboarding`, `accounts` (sign-in, sessions, workspaces, keys, tenants, policy, governance, usage), `billing`,
`admin`, `mcp` (the `/mcp` Model Context Protocol endpoint), `openai` (the `/v1` proxy; streamed completions
are relayed through the gateway chunk by chunk) and `routing` (`/api/v1/route`, feedback, targets, stats,
audit, the trace / event / learning reads of its decisions and the autopilot) each have their own entry point
(`osr-platform-`, `python -m osr_platform.services `,
ports 8091-8101) on the API image (`platform/api/osr_platform/services.py`). The API is the gateway: with
`OSR_PLATFORM__URL` set it forwards that domain's paths (headers, body, `X-Forwarded-For`) and marks
the answer with `X-OSR-Service`; unset, it serves the domain in-process. `/api/v1/info`, `/status`,
`/estimate` and the health / metrics endpoints always stay with the API; every service also serves
`/healthz`, `/readyz` and `/metrics`. Each process owns its background
work (`PlatformContext`: the API bootstraps operators and evaluates alerts, the owner of `routing` runs the
autopilot and the telemetry store, `rankings` refreshes the
reference catalogue, `marketplace` seeds the registry). `GET /api/v1/info` -> `services`,
`GET /api/v1/admin/services` (topology + live health) and the `/admin/services` page; the dev
`compose.yaml` and `platform/docker-compose.yml` start every service next to the gateway.
- Model providers managed at run time: `ProviderStore` (tables `providers`, `provider_models`) with
`GET|POST /api/v1/admin/providers`, `GET|PATCH|DELETE /api/v1/admin/providers/{id}`,
`POST /api/v1/admin/providers/{id}/check` (probes `/models`, records latency and the upstream model ids)
and `PUT|DELETE /api/v1/admin/providers/models/{target_id}`; presets for OpenAI, Azure, OpenRouter,
Anthropic, Mistral, Groq, Together, Fireworks, DeepSeek, Ollama, vLLM, LiteLLM. The mounted
`OSR_PLATFORM_PROVIDERS` file is imported once and stays merged underneath; every write re-binds the
handlers at once and other API replicas re-read the store every `OSR_PLATFORM_PROVIDERS_RELOAD_S` (30 s).
Public `GET /api/v1/providers`: the OpenRouter-style catalogue (provider, upstream model, reference prices,
context, health, 7-day traffic per executable target; never credentials) and its page on the site, `/providers`.
Console page `/admin/providers`.
- `GET /api/v1/onboarding`: the getting-started checklist of a workspace (e-mail confirmed, key, first route,
first outcome, provider connected, teammate invited) with links and the completing API call; the dashboard
overview shows it until every step is done. Signup,
verification, password reset and invitation links moved into the `onboarding` router (same paths).
### Changed
Website and platform as two services, one hostname
- The signed-in product now lives under `/platform`: `/platform/login`, `/platform/signup`, password recovery,
invitations and `/platform/cli/authorize`; the dashboard at `/platform/dashboard/...`; the operator console at
`/platform/admin/...`; the playground at `/platform/playground` and the publish flow at
`/platform/marketplace/publish`. The website (landing, `/docs`, `/models`, `/vendors`, `/rankings`,
`/pricing`, `/marketplace`, `/compare`, `/roi`, `/estimate`) keeps the root. The old paths redirect (308,
query string preserved), the OAuth redirect URI registered at providers stays `/auth/callback`, and
links the API generates (emails, invitations, device sign-in, Stripe return, alert `href`s) use the new paths.
- Deployment: a third container app, `osr-platform` (azd service `platform`, the web image, internal ingress),
serves `/platform`; `osr-web` forwards those requests to it (`OSR_WEB_PLATFORM_URL`, `platform/web/src/proxy.ts`).
A single web process without that variable serves both halves (`next dev`, Compose, the CI smoke container).
### Fixed
- Azure cluster mode connected to PostgreSQL, Redis and Kafka through `.internal.` names, which
resolve to the HTTP ingress and never answer on a TCP port; the API hung inside `psycopg.connect` until the
probes killed it and the platform silently kept serving from the previous SQLite revision. `infra/` now uses
the bare container-app names (`osr-postgres:5432`, `osr-redis:6379`, `osr-kafka:9092`), the database URL
carries `connect_timeout=10` (also the default in `Database` for PostgreSQL URLs without one) and the API and
domain services have a Startup probe so a slow first boot is not restarted mid-migration.
## [1.0.0] - 2026-09-07
1.0 freezes the public API. Every `opensmartroute.*` name in `tests/public_api.json` is stable from here;
removals go through `errors.deprecated()` for at least one minor first. The release rows of the v1.0
readiness table are met from the repository (`python scripts/release.py readiness`); the adoption rows -
independent security report, accepted leaderboard listings, two production users - stay open and are
tracked in `docs/ROADMAP.md`, `docs/SECURITY_REVIEW.md`, `examples/leaderboard/results/README.md` and
`ADOPTERS.md`.
### Added
Routing SLM and datasets
- `RouterSLM.info()`: metadata, shape (encoder, dims, embedder), calibration, size and the fit history of a
model without its weights; `osr slm info` prints it and the platform's `GET /api/v1/learning` uses it.
- `osr collect --file NAME=PATH`: load a benchmark you downloaded by hand (`.jsonl` / `.json` / `.csv` in the
`--preset` shape; `--tier` folds model columns onto your targets) into the dataset cache - the path for
gated RouterBench / RouterEval copies.
Release readiness
- `scripts/release.py readiness` distinguishes **release** rows (repository evidence; block a `1.x`
version in `check`) from **adoption** rows (third-party evidence; tracked and printed, never blocking),
and counts the freeze step `0.Y.0 -> 1.0.0` as a non-breaking release step when the changelog has no
removals and the tagged `tests/public_api.json` only grew.
Platform administration and the operator console
- Operators sign in to `/admin` with a **username and password** (`POST /api/v1/admin/auth/login` mints an
`osr_op_` session valid for twelve hours; scrypt password hashes; sign-in attempts rate limited per address).
`OSR_PLATFORM_ADMIN_USERNAME` / `OSR_PLATFORM_ADMIN_PASSWORD` create - or reset - the first `superadmin` at
API start-up; the static `OSR_PLATFORM_ADMIN_TOKEN` keeps working and counts as a superadmin. Superadmins
manage operators (`/api/v1/admin/operators`); every operator changes their own password and sessions.
- Full administration API and web console: deployment overview (`/api/v1/admin/overview`), users (search,
create with personal workspace / plan / password / first key, detail with workspaces, identities and sessions,
rename, disable, reset password, sign out everywhere, delete), workspaces (search, create organizations for an
owner email, plan / name / slug / disable / delete, members with roles, API keys, tenants with validated
configuration, policy, SSO and usage), every tenant across workspaces, plans. Pages `/admin`,
`/admin/users[/{id}]`, `/admin/workspaces[/{id}]`, `/admin/tenants`, `/admin/operators`, `/admin/login`.
Every mutation is written to the access log as `operator:` and published as an `admin` event.
- Email + password for workspace users: `POST /api/v1/signup` accepts a `password` (and then also returns a
browser session), `POST /api/v1/auth/password/login` signs in by email, `POST|DELETE /api/v1/auth/password`
set, change or remove it (`has_password` on the user); `/login`, `/signup` and `/dashboard/account` have the
forms. `OSR_PLATFORM_PASSWORD_LOGIN=false` turns it off.
Self-hosted cluster: PostgreSQL, Redis, Kafka as containers
- `OSR_PLATFORM_DATABASE_URL=postgresql://...` runs the platform store on PostgreSQL through `psycopg` (the same
SQL is written once; the backend rewrites placeholders, `INSERT OR IGNORE`, `REAL`/identity columns); the
whole test suite passes against PostgreSQL (`OSR_TEST_DATABASE_URL`). SQLite stays the single-replica default.
- `OSR_PLATFORM_REDIS_URL` shares rate-limit windows between replicas (atomic sliding window in Redis, falls
back to per-replica limiting when Redis is unreachable) and persists the enterprise learners' state in Redis
instead of the state directory.
- `OSR_PLATFORM_KAFKA_BOOTSTRAP` publishes `usage`, `feedback` and `admin` events as JSON to Kafka topics
`osr.` (any Kafka-protocol broker; never prompt text; broker outages drop events instead of failing
requests). `GET /readyz` reports `database_dialect`, `redis` and `events`; `GET /api/v1/info` and `/admin`
report `storage` (`{database, clustered, redis, events}`).
- `platform/docker-compose.yml`: the complete platform on any Docker host with PostgreSQL, Redis, a single-node
KRaft Kafka broker, the API and the web app - no cloud service involved. `infra/`: `OSR_PLATFORM_CLUSTER_MODE`
(default true) adds `osr-postgres`, `osr-redis` and `osr-kafka` container apps with internal TCP ingress and
their own Azure Files shares, and scales `osr-api` to `OSR_PLATFORM_API_MAX_REPLICAS` replicas; the API image
ships the `psycopg`, `redis` and `kafka-python` clients (`osr-platform-api[cluster]`).
- The routing SLM as its own service: `osr-platform-slm` (`osr_platform.slm_service`, the API image with another
entry point; `slm` in the Compose stack, `osr-slm` on Container Apps) consumes the `training` topic (request id +
prompt, published by the API when `OSR_PLATFORM_TRAINING_EVENTS=true`) and `feedback`, runs the `SelfImprover`
champion/challenger cycle on a schedule or on demand and writes promotions to `/data/autopilot/slm.json`; API
replicas reload that file every `OSR_PLATFORM_SLM_RELOAD_S` seconds (and serve it even without a mounted bundle).
Operator endpoints `GET /api/v1/admin/slm`, `/slm/reports`, `POST /slm/cycle`, `/slm/predict` proxy to the
service (`OSR_PLATFORM_SLM_URL`); the console page `/admin/slm` shows model, evidence, cycles and a prompt probe.
- Microservice deployment of the platform: `platform/docker-compose.yml` runs `docs`, `rankings`, `marketplace`,
`providers`, `onboarding` and `slm` as their own containers (`python -m osr_platform.services `) behind the
API gateway (`OSR_PLATFORM__URL`, answers carry `X-OSR-Service`); Container Apps cluster mode adds the
internal apps `osr-docs`, `osr-rankings`, `osr-marketplace`, `osr-providers`, `osr-onboarding` (HTTP ingress, 1-3
replicas, shared configuration and `/data` share) and points `osr-api` at them. The operator console page
`/admin/services` shows the topology with a live health probe per remote service; CI's cluster smoke asserts each
domain is served by its own container.
- `compose.yaml`: the whole hosted platform on a workstation with `docker compose up --watch` - development
images `platform/api/Dockerfile.dev` (editable installs, uvicorn `--reload`) and `platform/web/Dockerfile.dev`
(`next dev`), Compose Watch syncing `src/`, `platform/`, `docs/`, `examples/` and the skills into the running
containers so edits show immediately, a persistent data volume, and an optional `llm` profile that starts
Ollama with `deploy/compose/providers.ollama.yaml` so chat completions execute end to end without cloud keys.
- `python scripts/release.py readiness`: the v1.0 readiness table of `docs/ROADMAP.md` computed from the
repository - frozen API snapshot and test, performance envelope, reference deployments, review pack and CI
safety suite, published leaderboard run, two consecutive non-breaking minor steps (changelog sections
without removals, verified against the tagged `tests/public_api.json` when both tags exist), independent
report rows in `docs/SECURITY_REVIEW.md`, `accepted` rows under *Listings* in
`examples/leaderboard/results/README.md` and *Production users* rows in the new `ADOPTERS.md`.
`--require` exits 1 while a row is open and `release.py check` applies the same rule to any `1.x`
version, so the *Release* workflow cannot tag 1.0.0 before the evidence exists; the workflow prints the
table in its job summary.
- Tests for the OpenTelemetry bridge (`OpenTelemetrySink` driven through a fake `opentelemetry` API:
spans, nesting through the context, `traceparent` parent, events, counters, duration histogram, error
status, missing-extra error).
- Documentation trust gates: `tests/test_docs_claims.py` fails when any Markdown source names an `osr`
command or flag the CLI parser does not have, or an `OSR_*` variable nothing in the code or deployment
configuration reads; `platform/api/tests/test_platform_docs_claims.py` fails when a documented HTTP endpoint is not
a route of the platform app (method included), a documented `/dashboard/` has no page, a dashboard
page or nav entry is undocumented, a user-facing `docs/*.md` is missing from the site catalogue, or a
CHANGELOG section lacks a date, body or compare link. Historical tags `v0.2.0`, `v0.3.0`, `v0.4.0` mark
the commits that carried those versions so the compare links and the readiness snapshot check resolve.
### Changed
- Marketing and docs honesty: the editions matrix credits the community edition with the guard, PII
redaction, metrics and tracing it actually runs; the SLM showcase caveats the 0.94 figure as a
single-source number; the pricing page labels the `sso` feature; the hero shows the version only when the
API reports it; the Learning page points operators at `POST /api/v1/admin/autopilot/cycle` instead of a
button that could only fail; `platform/README.md` documents `MODELS_REFRESH`, `REGISTRY_AUTO_PUBLISH` and
`REGISTRY_SEED`; the Helm chart and deploy guide state that autopilot promotion is per pod; RESEARCH.md
reflects the gated RouterBench / RouterEval sources.
## [0.5.0] - 2026-09-06
### Added
Roadmap: everything 1.0 needs from the repository
- `examples/leaderboard/results/`: the published 0.5.0 benchmark run made with the leaderboard recipe (SLM trained on
Arena-55k + RouteLLM battles, seed 0; MT-Bench human, PPE human and WebDev Arena held out at the suite level,
`--limit 1000`): one JSON per suite for the learned router (`--baselines --calibration --robustness`) and for the
declarative router, plus a README with the data table, the SLM SHA-256 and the result table (learned 0.52-0.57 vs
declarative 0.19-0.42 and static task table 0.54-0.67; paraphrase 0.88-1.00; held-out isotonic ECE 0.03-0.05).
Published on the documentation site as `/docs/leaderboard-results`.
- `docs/SECURITY_REVIEW.md`: the external security review pack - engagement terms, system and trust boundaries,
the threat-model reference, an evidence table with a reproduction command per row, nine ordered reviewer
questions, known gaps that need not be re-reported and the review log where a third-party report is linked.
`SECURITY.md` supported versions updated to 0.5.x / 0.4.x.
- `docs/ROADMAP.md`: v0.9 is Complete (its exit criterion now names the published review pack; the independent
report moved to the v1.0 readiness table as the adoption item it is) and v1.0 has an explicit readiness table:
five repository items met, one of two stable minors done, three adoption rows open (independent report, accepted
leaderboard listings, two production users).
### Added
Hosted platform: billing and SSO as deployment inputs
- `infra/main.bicep` accepts `stripeSecret`, `stripeWebhookSecret`, `stripePrices`, `ssoProviders` and
`metricsPublic` (azd `OSR_PLATFORM_STRIPE_SECRET`, `_STRIPE_WEBHOOK_SECRET`, `_STRIPE_PRICES`,
`_SSO_PROVIDERS`, `_METRICS_PUBLIC`; GitHub secrets / variables of the same names in the deploy job). Container
secrets are added only when set, so a provision without them changes nothing; with the Stripe values the hosted
pricing page goes to Checkout instead of the operator instructions. `GET /metrics` on the Azure deployment now
requires `X-Admin-Token` by default.
- `platform/api/tests/test_docs_links.py` checks that every `/docs/#anchor` link in the web app and every
`file.md#anchor` link between the Markdown sources points at an existing heading (the site's `rehype-slug` ids).
The container smoke in `.github/workflows/platform.yml` now also requests `/models`, `/rankings`, `/marketplace`,
`/playground`, `/estimate`, `/roi`, `/mcp`, `/login`, `/signup`, `/cli/authorize`, `/docs/REFERENCE`,
`/sitemap.xml`, `/openapi.json` and `/api/v1/status`.
- The web app has a unit test suite (`platform/web/tests/*.test.ts`, vitest, `npm test`; part of `npm run check`
and therefore of the CI web job): the documentation catalogue against the content snapshot, the Markdown
renderer on the real documents (heading ids, link rewriting, Shiki, skill frontmatter), changelog version
parsing, the `/api` / `/v1` proxy (forwarded headers, 304/204, 502), `pageMetadata` and JSON-LD builders and
the formatting helpers.
- Browser end-to-end journey for the hosted web app (`platform/web/e2e/customer-journey.spec.ts`, Playwright,
`npm run e2e`; the CI web job runs it against the standalone build and a throw-away platform API): sign up in
the UI and read the one-time key, dashboard overview, every sidebar page, a playground decision and the free-plan
upgrade hint, key create and revoke, the activity row, sign-out and the login redirect, a rejected key, and the
public pages with live data. The dashboard sidebar is a `navigation` landmark ("Dashboard").
### Fixed
- `osr login --url --token osr_local_...` printed "Signed in to URL as URL": a self-hosted `osr serve`
token has no account behind it, so the message now names the URL once ("Signed in to URL (self-hosted edition)").
- Web: the dashboard sidebar never listed Governance, Events, Learning and Health (the pages existed and were in
`dashboardNav`, but not in the sidebar's section map); they are now an "Operate" section, and any future nav
entry without a section renders under "More" instead of disappearing. The section label reads "Organization"
like the rest of the site.
- Web: on 1024-1279 px viewports the header's "Compare" link rendered over the search box; the wide search box now
appears only where it fits (md-lg without the desktop nav, xl and up with it) and the icon button elsewhere.
- Web: documentation sidebar entries were hard-clipped mid-word ("Cost estimates and the MCP se") because Radix
ScrollArea lays its content out as a table; the viewport content is now block-level and long titles wrap.
- Web: the support and privacy pages linked `/docs/PLATFORM#13-errors` and `#12-privacy-and-data-handling`, two
sections behind the current numbering (`#15-errors`, `#14-privacy-and-data-handling`). `/dashboard/billing`
points at support when self-serve billing is disabled instead of only showing the operator command.
- `platform/README.md`: `OSR_PLATFORM_SSO_PROVIDERS` is a JSON map keyed by provider id, not a list.
### Added
Marketplace: the public skills ecosystem
- `osr_platform.harvest` imports the Agent Skills ecosystem and the official MCP registry into the marketplace:
every `SKILL.md` in each repository on the skills.sh leaderboard, GitHub code search for the long tail (token
required), and the latest active version of every MCP server as a `tool` listing with its remote endpoints.
Rows become OCM manifests with `metadata.source` (provider, repository, path, ref, url), the upstream owner as
publisher, the author's licence and an install command in the readme; the listing page shows a *Source* card
that links back. `platform/api/scripts/harvest_skills.py collect|stats|publish` runs it (resumable JSONL cache,
batches through the new admin endpoints `GET|POST /api/v1/admin/registry/import`, or straight into SQLite).
Marketplace facets are cached for 60 s and the index paginates with a window (the hosted catalogue now has
140 000+ listings). Every listing payload carries a compact `source` (provider, url, repository) and cards
show a provenance badge. The catalogue keeps itself current from inside the platform process:
`OSR_PLATFORM_MARKETPLACE_REFRESH_S` (weekly on Azure) harvests what is new upstream and publishes the
slugs the store lacks in small batches (`harvest.MarketplaceRefresher`; status under
`GET /api/v1/admin/registry/import`, `POST /api/v1/admin/registry/import/refresh` runs it now). From a
machine, `publish --only-new` asks `POST /api/v1/admin/registry/import/check` which slugs exist and sends
only the rest. Browsing 100k+ listings stays fast: covering indexes for facets, search counts and the
popular sort, cached stats and a start-up prewarm. `docs/MARKETPLACE.md` "Imported catalogue".
Roadmap exit criteria: v0.5 and v0.6 closed
- `eval.criteria.effort_token_savings()` and `synthetic_effort_rows()` measure the effort-routing half of the
v0.5 exit criterion: a reasoning model exposed through `expand_elastic()` as `effort="low"` / `effort="high"`
siblings, routed by the real `Router` with `EffortStrategy` + `TokenBudgetStrategy`, spends 61.6 % of the
always-think tokens at equal quality (2 000 rows). `EvalRow.tokens` (per-target tokens spent) is the new
dataset field and `osr eval DATASET --effort` runs the same comparison on rows collected from live
thinking / non-thinking pairs.
- `eval.agentic` (`AgentTask`, `load_agentic_tasks()`, `synthetic_agentic_tasks()`, `task_routing_frontier()`)
measures the tau-bench half of the v0.6 exit criterion: multi-step tasks with a per-agent per-step success /
latency table are replayed through `ProgressRouter` + `TaskCredit` with `CallableHarness` agents and compared
with every always-use-agent-X policy on common random numbers; routed accuracy 0.987 vs 0.977 for the best
single agent at 45.4 % of its latency (1 000 tasks). `osr eval TASKS.jsonl --agentic [--retries N]` runs it on
a real table.
- `scripts/exit_criteria.py` and `criteria.run_all()` include both; 8/8 criteria met. `docs/ROADMAP.md` marks
v0.5-v0.8 **Complete**; v0.9 closes with the review pack and v1.0 gets its readiness table (see above).
- v0.4 public-suite measurement published in `docs/ROADMAP.md`: on the held-out MT-Bench / PPE / WebDev Arena
human-preference suites relabelled to three tiers, the learned router beats the declarative router by 10-14 pp
and every naive baseline, but not the always-best-single-tier task table (weak pairwise labels, noise floor not
measurable on single-sample rows); ECE <= 0.10 holds only after fitting a calibrator.
- `calibration_report()` returns `held_out`: a `TemperatureScaler` and an `IsotonicCalibrator` fitted on the
even rows and scored on the odd rows (raw vs temperature vs isotonic ECE / Brier), so `osr eval --calibration`
shows what a fitted calibrator would achieve out of sample instead of only the raw ECE.
- `examples/leaderboard/`: the three-tier catalogue (`targets.yaml`) and the collect / train / evaluate recipe
behind the v0.4 table, i.e. the reproducible config the v1.0 leaderboard submissions link to; published on the
documentation site as "Benchmarks and leaderboard recipe" (`/docs/leaderboard`, Python SDK section).
### Changed
Web site redesign
- The hosted web app has a light, editorial design: Inter for text, Instrument Serif for display titles (marketing,
docs and auth pages), Geist Mono for code and numerals, Quicksand kept for the wordmark; a warm neutral palette
(white pages, hairline rules, ink buttons and links) with the brand hues reserved for the mark, charts and target
kinds. The landing page has a new section structure and copy (hero with the live ledger of most-routed targets,
vendor strip, six routing principles, how it works, featured models, model landscape, code, live catalogue,
editions, research, get started) while keeping every live data feed. Marketing pages, the light footer, the auth
pages, the dashboard shell and the documentation shell (sidebar, topbar, titles) follow the same system; the
documentation prose itself is unchanged. `docs/BRAND.md` records the website palette and typography.
- The site header dropdown menus render in the shared centered viewport (they previously overflowed the right
viewport edge and flickered on hover). The landing page gains a live usage band (tokens routed all time and in
the last 24 hours, requests, success rate, models, accounts), a value section with concrete savings / audit /
cost numbers, per-industry routing examples and a real healthcare `rules.yaml`, and restrained motion (scroll
reveals, animated counters, a vendor-logo marquee, all disabled under `prefers-reduced-motion`). A later
typography pass removed the italic mid-headline pivot from every display heading, the decorative hero glow
and the card tilt.
- Vendor icon coverage matches the live catalogue (Kwaipilot, Meituan LongCat, IBM Granite added; the Moonshot AI
mark switched to the visible monochrome variant) and curated vendor display names win over harvested ones.
The hero prompt examples are chip pills, a floating back-to-top button appears on every page, the rankings
page shows the daily per-target traffic and vendor/domain share charts, and alert notices flow inline text
correctly.
- A dark landing-page band presents the routing SLM: the collect / train / eval / serve pipeline, the measured
holdout numbers, and the autopilot, champion-challenger and distillation loops.
### Added
Account lifecycle end to end
- Anyone can sign up with any email address and a password, and recover the account: `POST /api/v1/auth/password/forgot`
emails a single-use, one-hour reset link (always `202`, never reveals whether an address exists) and
`POST /api/v1/auth/password/reset` sets the new password, revokes every session, confirms the address and signs the
person in. Web pages `/forgot-password`, `/reset-password` and a *forgot password?* link on `/login`.
- Email confirmation: signup sends a link to `/verify-email`; SSO profiles with a verified address, accepted invitations
and password resets confirm it too. `email_verified` and `last_login_at` on the user object; the account page shows
a banner with *Send again* (`POST /api/v1/auth/verify`, `POST /api/v1/auth/verify/send`).
- Self-service deletion (`POST /api/v1/me/delete` with the email typed and the password): sessions, identities and
memberships go, solo workspaces are disabled with their keys, an organization with other members needs another
owner first; a notice is emailed. *Delete my account* on `/dashboard/account`.
- Invitations are emailed to the invitee (the link is still returned once to the inviter); a removed member's
sessions on that workspace are revoked.
- Account audit log (`audit_log` table): signups, sign-ins and failed attempts, password and email changes,
invitations, joins, role changes, removals, deletions and operator actions, mirrored on the `admin` event topic.
`GET /api/v1/me/audit` (own history, shown on the account page), `GET /api/v1/workspace/audit` (admins),
`GET /api/v1/admin/audit-log` and the `/admin/audit` console page.
- Outbound email (`osr_platform/mail.py`): standard-library SMTP from `OSR_PLATFORM_SMTP_URL` (`smtp://` STARTTLS or
`smtps://`) with `OSR_PLATFORM_MAIL_FROM`; every message is also kept in an outbox, so without a mail server operators
hand the links over from `/admin/mail` (`GET /api/v1/admin/mail[/{id}]`). `mail` in `GET /api/v1/info`; Bicep/azd
parameters and the deploy workflow carry the two variables.
- Operator console per user: *Send reset link* (`POST /api/v1/admin/users/{id}/reset-link`, link returned for
support), *Resend confirmation* / *Mark verified* (`POST .../verify-link`, `PATCH` with `email_verified`), lifecycle
history (`GET .../audit`), last sign-in; users created without a password get a reset link to choose one.
Single sign-on configuration from the CLI
- `scripts/sso.ps1` configures the hosted platform's sign-in providers end to end: `microsoft` creates the Microsoft
Entra app registration with `az` (work/school and personal accounts, `/auth/callback` redirect, Graph
`openid email profile User.Read`, service principal, two-year client secret), `google` / `github` / `gitlab` walk
through the provider consoles and prompt for the client pair (no API exists for those OAuth clients), `show`,
`remove`, `push` (GitHub Actions secret) and `apply` (`azd provision` keeping the running images, then verifies
`GET /api/v1/auth/providers`). Secrets are never printed.
- JSON-valued deployment inputs are now base64-encoded parameters (`OSR_PLATFORM_SSO_PROVIDERS_B64`,
`OSR_PLATFORM_STRIPE_PRICES_B64`, decoded in `infra/main.bicep`): `azd` substitutes them textually into
`main.parameters.json`, so a raw JSON value made every provision fail.
Web site analytics and search optimisation
- Google Analytics 4 on the public pages of the hosted web app with Consent Mode v2: a consent banner (Accept /
Decline, stored in `localStorage` as `osr-consent`, changeable on the privacy page), denied defaults until the
visitor accepts, truncated IPs, one `page_view` per client-side navigation and `sign_up` / `login` events as the
conversions for Google Ads. The tag never loads under `/dashboard` and is absent when the build has no
`NEXT_PUBLIC_GA_MEASUREMENT_ID` (azd: `OSR_GA_MEASUREMENT_ID`; optional `OSR_GOOGLE_ADS_ID`,
`OSR_GOOGLE_SITE_VERIFICATION`, `OSR_BING_SITE_VERIFICATION`).
- Every public page now ships a canonical URL, keywords, Open Graph and Twitter card (`pageMetadata` in
`platform/web/src/lib/seo.ts`); the landing page has a descriptive title, and the model, marketplace, comparison
and documentation pages have per-entity titles and descriptions. schema.org JSON-LD: `Organization` and `WebSite`
site-wide, `SoftwareApplication` for the product and for each marketplace listing (with ratings), `Product` offers
and `FAQPage` on pricing, `BreadcrumbList` and `TechArticle` on documentation, model and comparison pages.
`robots.txt` blocks the dashboard, proxies, one-time links and query-string variants; the sitemap lists canonical
paths only. The privacy page and the platform guide describe the analytics tag.
- Search and social distribution end to end: generated Open Graph cards for every page (`/og?title=...`, brand
typeface, used automatically by `pageMetadata`), programmatic vendor hubs (`/vendors` and `/vendors/`
with per-vendor prices, context windows, benchmarks and FAQ), a `/compare` index for the alternatives pages,
`/llms.txt` for AI assistants, an Atom release feed at `/feed.xml` (advertised on every page), a `SearchAction`
on the `WebSite` schema and `ItemList` schema on hub pages. IndexNow: the web app serves `INDEXNOW_KEY` at
`/indexnow.txt` (azd: `OSR_INDEXNOW_KEY`) and the platform workflow submits the sitemap to Bing/Yandex after
each deploy (`platform/web/scripts/indexnow.mjs`). Analytics: Core Web Vitals reported to GA4, outbound-link
and `data-track` call-to-action events, `copy_code` on install snippets and Google Ads conversion labels per
event (`OSR_GOOGLE_ADS_CONVERSIONS`, e.g. `sign_up=AW-123/label`). Vendors and Compare join the header,
footer and command palette.
### Added
CLI installers and sign-in
- Native installers for the `osr` command: `curl -LsSf https://opensmartroute.ai/install.sh | sh` (Linux, macOS)
and `irm https://opensmartroute.ai/install.ps1 | iex` (Windows). Both pick `uv tool install`, `pipx` or a private
venv (never the system Python), bootstrap uv when nothing else is available, honour `OSR_VERSION`, `OSR_EXTRAS`,
`OSR_INSTALLER` and `OSR_NO_MODIFY_PATH`, put `osr` on PATH and verify with `osr --version`. The scripts live at
the repository root, ship in the sdist and with every GitHub release, and the web app serves them at
`/install.sh` and `/install.ps1`; `osr --version` prints the SDK version.
- `osr login` signs in to the hosted platform (community or enterprise edition) with the OAuth 2.0 device
authorization grant (RFC 8628): the CLI prints a short code, opens `/cli/authorize`, the user approves in the
browser and the platform mints a workspace API key for that machine. `osr login --token` / `--with-token` store a
pasted key or a self-hosted server token instead; `--url` and `--profile` keep several deployments side by side.
`osr whoami`, `osr logout [--all]`, `osr token generate|create|list|revoke` and `osr mcp --remote` (bridge to the
signed-in workspace without copying the key) complete the set. Credentials are stored per profile in
`~/.config/opensmartroute/credentials.json` (`%APPDATA%\opensmartroute` on Windows, `OSR_CONFIG_DIR`); flags,
then `OSR_API_URL` / `OSR_API_KEY`, then the saved profile decide which one applies.
- `opensmartroute.credentials`: `config_dir`, `CredentialStore`, `Credential`, `PlatformClient`, `device_login`,
`whoami`, `generate_token`, `token_kind`, `redact` - stdlib only, injectable transport. `branding` gains
`WEBSITE`, `REPOSITORY`, `INSTALL_SCRIPT_SH|PS1`, `API_URL_ENV`, `API_KEY_ENV`, `CONFIG_DIR_ENV`, `TOKEN_PREFIX`,
`LOCAL_TOKEN_PREFIX` and `platform_url()`; `errors.AuthenticationError` (`OSR_AUTH`).
- Self-hosted `osr serve` can now require access tokens: `serve --token T` (repeatable), `--generate-token`
(prints an `osr_local_...` token once with the matching `osr login` command), `--require-auth`, or
`ServerSettings` (`OSR_SERVER_AUTH_TOKENS`, `OSR_SERVER_AUTH_TOKENS_FILE`, `OSR_SERVER_REQUIRE_AUTH`);
`create_app(router, auth_tokens=[...])`. Every path except `/healthz`, `/readyz`, `/metrics`, `/whoami` and the
OpenAPI documents then needs `Authorization: Bearer` or `X-API-Key` (constant-time compare, 401 with
`WWW-Authenticate`). New `GET /whoami` describes the server and whether the caller is authenticated. Helm chart:
`auth.tokens`, `auth.existingSecret`, `auth.required`.
- Platform API: `POST /api/v1/auth/device/code` (anonymous, rate limited), `GET /api/v1/auth/device/{user_code}`,
`POST /api/v1/auth/device/approve|deny` (admin role, plan key quota) and `POST /api/v1/auth/device/token`
(`authorization_pending`, `slow_down`, `access_denied`, `expired_token`); device codes are stored hashed and
expire after 15 minutes. Web: `/cli/authorize` approval page (sign-in redirect, code entry, key name, approve/deny),
install one-liners on the docs landing page, footer and integrations page.
Tracing and observability
- `opensmartroute.observability`: a `Tracer` that every stage of a request reports to - spans `request`,
`http.request`, `route`, `plan`, `execute`, `autopilot.cycle` and events for signals, policy, pinning,
narrowing, shortlist, ranking (per-strategy timings), escalation, exploration, abstention, fallback, plan
slots, execution steps, outcomes, task credit, calibration, breaker transitions, cache hits and misses,
tenant rejections, slow decisions, guard verdicts, shadow comparisons and A/B verdicts, fair-share
throttling and autopilot drift (`SPAN_NAMES`, `EVENT_NAMES`). Sinks: `MemorySink` (ring buffer with
`events()` / `trace(request_id)`), `MetricsSink` (counters, p50/p95/p99, `prometheus()`), `LoggingSink`,
`FileSink` (JSONL) and `adapters.optional.OpenTelemetrySink` (`otel` extra: OTel spans nested through
the OTel context and joined to an inbound `traceparent`, span events, counters and a duration histogram).
`configure_tracing(*sinks)` / `get_tracer()` for the process-wide tracer, `Router(tracer=)`,
`RouterBuilder.with_tracing(...)`, `current_tracer()` / `use_tracer()` for components; trace ids come
from `traceparent`, a UUID request id or a hash of the request id. Request text never enters a span
(`text_digest()`); attributes are bounded and JSON-clean; a failing sink is counted, never raised.
`ObservabilitySettings` (`OSR_OBSERVABILITY_METRICS|MEMORY_EVENTS|LOG_EVENTS|EVENTS_FILE|OTEL|SAMPLE_RATE|ATTRIBUTE_MAX_LEN`).
- `osr serve`: `http.request` span per request (probes and observability endpoints excluded),
`X-OSR-Trace-Id` response header, `GET /events` (filter by request id, trace id, name glob, kind, level),
`GET /trace/{request_id}`, tracer counters appended to `GET /metrics`, `observability` block in `/stats`.
`osr route --events` prints the trace of the decision; every CLI router traces into the sinks named by
`OSR_OBSERVABILITY_*`. The hosted platform wires the same tracer into both editions (`/api/v1/stats`).
New `docs/OBSERVABILITY.md`.
Metrics, caching and AI governance end to end
- `enterprise.MetricsTelemetry` is a real metrics store: `snapshot()` returns decision counts per target,
policy rejections, errors by type, a routing-latency histogram with p50/p95/p99, mean confidence,
outcomes per target (success rate, cost, quality, latency) and uptime; `prometheus(namespace=, extra=)`
renders the Prometheus exposition format (`osr_route_decisions_total`, `osr_route_latency_ms_bucket`,
`osr_outcome_cost_usd_total`, `osr_decision_cache_hits_total`, ...). `CacheMiddleware.stats()`,
`invalidate()` and `len()`; `RouterBuilder.with_cache(ttl_s, max_size)` and `with_audit(sink,
outcomes=True)` so the hash chain also records how each decision turned out.
- `osr serve`: `/healthz` and `/readyz` probes, `GET /metrics` in Prometheus format (router metrics plus
tracer counters and a `targets` gauge), request-latency and error observation on every routed call. The
Helm chart and the Azure Container Apps template use `/readyz` for readiness; the chart's default
`podAnnotations` carry `prometheus.io/scrape|path|port`.
- Platform API observability (`osr_platform.observability`): every response carries `X-Request-Id`
(echoed when the client sends one) and `Server-Timing`; JSON access log on `osr.platform.access` with
route template, status, latency and account; per-route HTTP counters, latency histogram and in-flight
gauge. `GET /healthz` (targets, uptime), `GET /readyz` (503 with the failing check: database,
catalogue, router, audit file) and `GET /metrics` (`osr_platform_http_*` plus the router's metrics;
public by default, `OSR_PLATFORM_METRICS_PUBLIC=false` restricts it to `X-Admin-Token`).
`GET /api/v1/stats` adds `http`, `controls`, `cache` and `observability` blocks; breaker snapshots
serialise cleanly (`budget_remaining: null` instead of infinity).
- Platform caching: public catalogue reads (`/api/v1/info`, `/models*`, `/rankings`, `/llms*`,
`/catalogue`, `/stats/public`) return `ETag` and `Cache-Control: public, max-age, stale-while-revalidate`
and answer `304` to `If-None-Match` (`OSR_PLATFORM_HTTP_CACHE_MAX_AGE_S`, `0` disables). An optional
decision cache in front of either edition (`OSR_PLATFORM_ROUTE_CACHE_TTL_S`) reuses recent decisions for
identical requests; hit rates appear in `/api/v1/stats` and `/metrics`.
- Platform governance: a **workspace policy** (`GET|PUT|DELETE /api/v1/policy`, admin role) applies the
tenant constraint keys plus `daily_budget_usd` / `monthly_budget_usd` to every request of the
workspace; tenants accept the same budget keys. Policy and tenant constraints merge deterministically
(boundaries fill in, caps take the stricter value, deny lists union, allow lists intersect); unknown
keys, unknown targets and allow/deny overlaps are rejected with `400`. Executed spend is attributed per
tenant (`X-OSR-Tenant`); once a budget is exhausted routed calls return `429` with `Retry-After` and
`X-Budget-Limit|Used|Period`. `GET /api/v1/governance` returns the whole posture in one payload:
controls in force (input guard, PII redaction, steering strip, metrics, tracing, audit chain, circuit
breakers, decision cache, learning persistence), audit-chain verification, retention, policy, budgets
and spend, quota, tenants with monthly spend, and the catalogue's data boundaries and PII-capable
targets. Usage rows older than `OSR_PLATFORM_RETENTION_DAYS` (default 365, `0` keeps everything) are
purged by a background sweep.
- Web: `/dashboard/governance` (policy editor with budgets, controls, budget bars, audit and retention,
catalogue boundaries, tenant spend) and `/dashboard/health` (enterprise: HTTP traffic, error rate,
routing decisions and latency histogram, circuit breakers, decision cache hit rate, per-route table,
auto-refresh). The tenant editor gains daily and monthly budgets. The proxy forwards `If-None-Match`,
`ETag`, `Server-Timing` and the `X-Budget-*` headers; `ApiError` exposes `requestId` and `budget`.
Observability portal: traces, events and readiness in the dashboard
- Platform API: `GET /api/v1/trace/{request_id}` returns the activity row of one of the workspace's
requests plus every span and event the tracer buffered for it (`trace_id`, `spans`, root `duration_ms`;
`404` for another workspace's id, `events: []` once evicted). `GET /api/v1/events` lists recent tracer
events scoped to the workspace through its request ids (`name` with `*` wildcard, `kind`, `level`,
`request_id`, `limit` up to 2000; deployment-wide events without a request on plans with `stats`;
`404` when `OSR_OBSERVABILITY_MEMORY_EVENTS=0`). `GET /api/v1/status` is a public readiness endpoint
for status pages (checks, edition, versions, uptime, controls, tracing buffer; `503` while a check
fails, `no-store`). `POST /api/v1/route` responses carry `trace_id` and `X-OSR-Trace-Id`;
`GET /api/v1/activity` marks rows still in the buffer with `traced` and returns the buffer state.
`usage(request_id)` is indexed.
- Closed loop: `POST /api/v1/feedback` is now stored per request (`feedback` table, purged with usage
retention); `GET /api/v1/trace/{request_id}` returns the request's `outcomes` and its hash-chained
`audit` records (decision and outcome, plans with `audit`), and activity rows carry an `outcome`
summary (`reports`, `success`, mean `quality`).
- Web: `/dashboard/events` (live event stream with auto-refresh, stage/kind/level filters, most-frequent
names, click-through to the trace); a **trace drawer** on every traced activity row and a *Trace* tab
in the playground rendering a span waterfall (offsets and durations relative to the root span, nested
events, level badges, expandable attributes, problems filter) with a *How it was routed* story
(signals, policy rejections and guard verdicts, ranking bars, decision with fallbacks and execution
steps), the reported outcome (or the feedback call to make) and the audit records; an *Outcome* column
in the activity log; a *Systems* card on `/dashboard/health`
(also shown in the community edition) and a status pill on the overview polling `/api/v1/status`.
New `components/observability/trace-view.tsx` and `system-status.tsx`; the proxy forwards
`X-OSR-Trace-Id`.
Routing SLM and autopilot on the hosted platform
- SDK: `EnterpriseRouter` now runs `Router.observers` on the auto-learning path too, so an autopilot's
drift hook sees every outcome in the enterprise edition (it was silently dead there). `SelfImprover`
reports each cycle as a `learn.improve` event (rows, champion vs challenger accuracy, verdict, reason)
and each promotion as `learn.promote`; `Autopilot(tracer=)` names the tracer cycles report to when the
host's tracer is not the process-wide one.
- Platform: `OSR_PLATFORM_SLM` serves a routing SLM as the `slm` strategy; `OSR_PLATFORM_AUTOPILOT`
(with `_OFFLINE`, `_SOURCES`, `_INTERVAL_S`, `_MIN_ROWS`) runs the self-improvement loop in-process on
the platform's own feedback, promoting a challenger only when it beats the champion and saving it to
`DATA_DIR/autopilot/slm.json` (preferred over the mounted file after a restart). `GET /api/v1/learning`
(any plan) returns the strategy ensemble and weights, learner state (persistence, drift resets,
quarantine), per-target outcome statistics (JSON-safe), the SLM in service (rows, sources, encoder,
calibration, fit history) and the autopilot status with recent improvement reports;
`POST /api/v1/admin/autopilot/cycle` schedules a cycle; `controls` / `info` gain `slm` and `autopilot`.
- Web: `/dashboard/learning` (strategy weights, outcomes per target, SLM card with accuracy sparkline and
fit history, autopilot card with drift, cycles, promotions, champion-vs-challenger and a *Run cycle*
action, improvement-cycle table); Governance lists the SLM and autopilot controls.
Azure DNS zone and custom domain
- `infra/dns.bicep` (parameter `dnsZoneName`, azd `OSR_DNS_ZONE`): public Azure DNS zone for the platform with
apex A to the Container Apps environment static IP, `www`/`api` CNAMEs to the app FQDNs, `asuid.*` TXT records
for hostname validation and SPF/DMARC reject records; outputs `OSR_DNS_NAME_SERVERS`. `scripts/domains.ps1` is
now Azure-only (phases `dns`, `bind`, `verify`, `clean`; no Cloudflare token): it creates the zone, waits for the
registrar delegation, binds the hostnames with managed certificates, rebuilds web with the domain baked in and
checks site, API, MCP and redirects end to end; `clean` retires the superseded `osr-platform` container app.
- Web: `platform/web/src/proxy.ts` 301s alias hostnames listed in `OSR_WEB_REDIRECT_HOSTS` (set by Bicep to the
web alias domains, e.g. `www.`) to the canonical `NEXT_PUBLIC_SITE_URL`.
- Apex managed certificates validate over HTTP (`TXT` validation never completed for the apex); subdomains keep
CNAME validation.
- Continuous deployment: the `azd up` job in `.github/workflows/platform.yml` signs in with an OIDC federated
credential bound to the `production` GitHub environment, reads the custom-domain parameters from repository
variables and seeds `SERVICE_*_IMAGE_NAME` from the running apps so a provision never swaps them to the placeholder
image. Setup commands in `platform/README.md` ("Continuous deployment").
Quotes, recommended models and the MCP server
- `opensmartroute.estimate`: quote a request before sending it. `estimate(router, request,
output_tokens=, kinds=, prices=, quality_tolerance=, decision=)` routes as usual and prices every
ranked candidate (`TargetEstimate`: input / output / per-call cost, latency, quality estimate,
context fit, energy and CO2, rationale) into a `RequestEstimate` with the `recommended`,
`cheapest` (within a quality tolerance of the best), `best_quality` and `fastest` picks and
`savings_usd`; `estimate_tokens` / `estimate_messages_tokens` heuristics, `target_prices` (split
`usd_per_1k_input` / `usd_per_1k_output` when declared), `PriceHook` for live price feeds. CLI
`osr estimate TEXT [--output-tokens N] [--cost-weight W] [--monthly R] [--json]`.
- `opensmartroute.mcp_server`: a standard-library Model Context Protocol server (`2025-06-18`, JSON-RPC
2.0) over any router. Tools `route`, `estimate`, `recommend` (priority balanced / cost / quality /
speed), `explain`, `list_targets`, `feedback`, plus `ask` (route and execute) and
`marketplace_search` / `marketplace_get` when the `execute` / `marketplace` hooks are given;
resources `osr://targets` and `osr://stats`; `serve_stdio` transport, `RemoteMCP` / `bridge_stdio`
HTTP forwarder. CLI `osr mcp [--list-tools]` serves a catalogue over stdio, `osr mcp --url /mcp
--api-key` bridges to a hosted platform. `osr serve` mounts `POST /mcp` and `GET /mcp`.
- Platform: `POST /api/v1/estimate` (anonymous, rate limited per client, or keyed and metered as
`estimate`; vendor / model names, live catalogue prices, optional `monthly_requests` projection),
`GET /api/v1/models/recommended` (recommended, cheapest, best-quality and fastest pick per use case -
chat, code, reasoning, summarise, extraction, pii - from live quotes, plus a leaderboard, cached one
minute) and the MCP endpoint `POST /mcp` / `GET /mcp` (tag `mcp`; scoped to the workspace, metered
as `mcp`, `ask` on the plan tier, marketplace tools backed by the registry). Web: `/estimate` page
(quote form, four picks, signals, candidate table, monthly projection), "Recommended per use case"
on `/models`, *Dashboard -> Integrations* with copy-paste MCP configuration for VS Code, Cursor,
Windsurf, Claude Code and Claude Desktop, and `/mcp` proxied on the web origin. `docs/MCP.md`.
Web app chrome and platform pages
- Header: the main navigation collapses into the menu below the `lg` breakpoint (it overflowed on
tablets), the menu closes on navigation and lists the signed-in shortcuts, active items carry
`aria-current`, and a "Skip to content" link targets ``.
- Footer: plain headings, four groups (Product, Developers, Resources, Company) with the REST and
Python references labelled apart, licence / SDK version / edition as a definition list, and a legal
bar (Terms, Privacy, Support, Security).
- New pages `/support` (documentation entry points, issues and discussions, account and billing,
security reports, enterprise, service status), `/privacy` (what the platform stores per table, request
content handling, browser storage, processors, retention, access and deletion) and `/terms` (accounts
and keys, plans and payment, content, acceptable use, marketplace, availability, suspension, open
source, liability). Linked from the footer, the signup form, the dashboard footer, the 404 and error
pages, the command palette and the sitemap; the CI container smoke test requests them.
- Command palette lists every public page (marketplace, estimate, pricing, REST API, support).
Marketplace and declarative stacks
- `opensmartroute.stack`: declarative *stacks* - one YAML/JSON document (`osr: "1"`, `kind: stack`)
holding targets (plain or OCM), rules, settings, objective and `imports` of other stacks, catalogues
or marketplace templates (`registry://slug@version`). `load_stack`, `validate_stack`, `plan_stack`
(diff against a deployed stack), `dump_stack`, `starter_stack` and `Stack.router()` mirror the
infrastructure-as-code check / diff / apply workflow; the CLI gains `osr stack init | validate |
plan | apply` (`--registry`, `--against`, `--out`, `--route`). `examples/stack.yaml` is a complete
support-desk example.
- Hosted marketplace (`platform/api/osr_platform/registry.py`, tag `marketplace`): listings of kind
template, agent, skill, persona, tool, llm and prompt with slug, semver versions and changelog,
publisher, licence, tags, Markdown readme, price, installs and star ratings. Public browse and
detail (`GET /api/v1/registry`, `/registry/kinds`, `/registry/{slug}`, `/manifest?version=`,
`/reviews`), publisher lifecycle draft -> review -> published -> archived (`POST /registry`,
`PATCH`, `/submit`, `/versions`, `/archive`, `/unarchive`, `GET /me/listings`), installs and
purchases (`POST /registry/{slug}/install`, `GET /me/installs`, one-time Stripe checkout confirmed by
webhook), reviews (`PUT`/`DELETE /registry/{slug}/reviews`) and moderation under `/api/v1/admin/registry`
(`approve`, `reject`, `flags`, `unpublish`). Manifests are validated against the OCM / stack schemas;
template manifests may be sent as `{"yaml": "..."}`. Settings `OSR_PLATFORM_REGISTRY_AUTO_PUBLISH`
(free listings go live on submit) and `OSR_PLATFORM_REGISTRY_SEED` (seed the repository's skills,
the starter and example templates, a persona and prompts, all verified).
- Web: `/marketplace` (kind rail, search, filters, featured, pagination), `/marketplace/{slug}`
(about, use-it snippets, manifest, reviews with histogram, versions, install / buy / rate),
`/marketplace/publish` (four-step wizard, no code needed for prompts and personas) and
`Dashboard -> Marketplace` (my listings with status and reviewer notes, edit page with new-version
form, installed items). Marketplace links in the main navigation, footer, dashboard and sitemap.
User docs: `docs/MARKETPLACE.md` (site page `/docs/MARKETPLACE`).
- OCM cost block accepts `context_tokens`; `capability_from_target` emits it.
- `PlatformAPI.optional_principal` dependency (anonymous or authenticated) and
`Billing.listing_checkout_url` / `Billing.on_purchase` for one-time purchases.
Documentation site
- The hosted web app (`platform/web`) now renders the documentation itself as a static Next.js site at
`/docs`: `scripts/sync-content.mjs` snapshots the repository Markdown (`README`, `CHANGELOG`,
`CONTRIBUTING`, `SECURITY`, `docs/**`, `deploy/README.md`, `spec/ocm/README.md`,
`.claude/skills/*/SKILL.md`) into `content/`; `src/lib/docs/catalogue.ts` is
the single source of truth for sections, slugs and summaries; `src/lib/docs/render.ts` renders with
remark/rehype (GFM, GitHub-compatible heading ids, Shiki highlighting, Mermaid, relative-link
rewriting). Pages get a collapsible sidebar, scroll-spy table of contents, breadcrumbs, reading time,
code copy buttons, previous/next links and an "Edit on GitHub" link. New pages: every Agent-Skills
package (`/docs/skills/`, with its frontmatter), `/docs/deploy`, `/docs/ocm`,
`/docs/contributing`, `/docs/security-policy`.
`/docs/search.json` feeds documentation pages and headings into the Ctrl/Cmd+K palette; the sitemap
lists every page. The docs no longer depend on the platform API being up.
- The site is organised for end users of the hosted platform, the Python SDK and the pip package:
sections Start, Hosted platform, Python SDK, Self-hosting, Concepts, Security, Agent skills and
Releases, with a section tab bar and audience paths on the index. Internal material
(`docs/PLATFORM_PLAN.md`, `docs/GO_TO_MARKET.md`, `docs/BRAND.md`, `docs/sales/`,
`platform/README.md`) is no longer published on the site and stays in the repository.
- `docs/PLATFORM.md`: platform guide for end users (authentication, `/api/v1/route` fields and
constraints, the `/api/v1/estimate` quote, execution, the OpenAI-compatible endpoint, feedback,
plans and quotas, organizations and roles, organization SSO, tenants, the marketplace, the MCP
server, dashboard pages, privacy, error handling). `docs/MARKETPLACE.md` is published under Hosted
platform. `docs/GUIDE.md` lists every `osr` subcommand grouped by task.
- REST API reference generated from the platform's OpenAPI document: `/docs/api` (authentication
schemes, base URL, conventions, endpoint groups) and one page per tag (`/docs/api/routing`,
`/docs/api/openai`, `/docs/api/public`, `/docs/api/account`, `/docs/api/workspaces`,
`/docs/api/auth`, `/docs/api/billing`, `/docs/api/mcp`, `/docs/api/marketplace`) with parameters,
request and response schemas, curl and JSON examples; operations are searchable from the palette.
Operator-only `/api/v1/admin/*` paths are left out whatever tag they carry.
- Version menu in the docs top bar built from `CHANGELOG.md` (current release, unreleased changes,
links to each release section and PyPI).
- `.claude/skills/osr-platform`: Agent-Skills package for the hosted platform (API, web app, the
documentation site and `azd` deployment); root `AGENTS.md` and `.github/copilot-instructions.md`
point coding agents at the gates, the easy-to-break rules and the skill for each task.
Platform API
- The OpenAPI document declares `bearer` and `apiKey` security schemes (replacing the per-operation
`authorization` / `x-api-key` header parameters), tag descriptions for every route group and a
richer info block. `platform/api/openapi.json` is a committed snapshot regenerated with
`python platform/api/scripts/export_openapi.py`; `--check` (run in CI and by
`test_openapi_snapshot_is_current`) fails on drift.
Routing SLM and self-improvement
- `learning.RouterSLM`: a self-contained small routing model (hashed dual encoder, temperature
calibration, target snapshots) that trains on `EvalRow`s, predicts a calibrated distribution and a
quality/cost/latency utility per candidate, learns online from outcomes, and saves to a compact JSON
file (`save` / `from_file`, `SLM_FORMAT`). `learning.distill_router()` compresses the whole ensemble
(rules, capability fit, learners, judge) into one SLM from its traces or propensities.
`learning.SLMStrategy` puts it into the ensemble (weight `OSR_WEIGHTS_SLM`) with online updates;
`Router` accepts it like any other strategy and `osr --slm model.json route` loads one from the CLI.
- `adapters.ModelCatalogue` / `ModelCard`: live model catalogue from OpenRouter (prices, context,
modalities, tool support) and Hugging Face model cards (downloads, likes, `model-index` benchmarks ->
`quality_from_benchmarks`, `quality_from_popularity`), merged per model id and persisted; `targets()`
filters by quality/price into `RouteTarget`s (`card_to_target` replaces injection-risky third-party
descriptions), `cost_benchmark()`, `frontier()` (Pareto), `to_markdown()`; fail-soft `refresh()`.
- `adapters.WebKnowledge`: web-search discovery with keyless providers (`huggingface_search`,
`duckduckgo_search`) and `brave_search` when `BRAVE_API_KEY` is set; `fetch_bytes` / `fetch_json` /
`fetch_page_text` / `html_to_text` (https-only, byte-capped, risk-scored pages); a cached hit list
that `discover_models()` feeds into the catalogue.
- `eval.DatasetCollector` / `DatasetSource` / `KNOWN_SOURCES`: collect routing datasets from the
Hugging Face datasets-server (`fetch_hf_rows`, `iter_hf_rows`, `collect_dataset`) into a JSONL
cache, plus `rows_from_feedback()` (outcomes -> labelled rows) and `synthetic_rows()` (ontology
seed prompts scored by capability fit); `corpus()` dedupes and caps, `split()` is a deterministic
holdout. `DEFAULT_SOURCES` are the publicly served pairwise battle sets `routellm-battles`
(RouteLLM GPT-4-judged battles) and `arena-55k` (LMArena human preference); the RouterBench and
RouterEval presets stay in `KNOWN_SOURCES` flagged `gated` (the Hub does not serve their rows
without authentication). `from_pairwise_row()` turns a battle into an `EvalRow` (ties make both
sides acceptable); `model_quality()` fits a Bradley-Terry model over the battles and returns each
model's win probability against the field, which the catalogue applies as an `arena` quality
prior. `ARENA_TIERS` / `tier_of()` / `tier_model_map()` fold the retired arena model names onto
the operator's tiers (`osr collect --tier frontier=llm-frontier --tier mid=llm-mid --tier
small=llm-small`, also on `osr improve`); `DatasetCollector.derive()` remaps an existing cache
offline so tiered corpora never need a second download. `collect()` keeps partial pages when the
Hub rate-limits mid-way and never replaces a fuller cache with a shorter one; `fetch_bytes` retries
429/5xx with exponential backoff honouring `Retry-After` (`OSR_SLM_FETCH_RETRIES`,
`OSR_SLM_FETCH_BACKOFF_S`, `OSR_SLM_FETCH_BACKOFF_MAX_S`) and pages pause `OSR_SLM_PAGE_PAUSE_S`.
- `adapters.fetch_leaderboard_quality()`: Open LLM Leaderboard results (IFEval, BBH, MATH, GPQA, MuSR,
MMLU-Pro) per model via the datasets-server; `ModelCatalogue.apply_quality()` maps them (and the
arena win rates) onto cards by exact id or `model_key()` (case/variant-insensitive tail), ranking
sources popularity < catalogue < arena < leaderboard so a measured prior is never downgraded;
`refresh(leaderboard=True)`, `targets(measured=True)` and `osr catalogue --leaderboard
--quality-from DATA_DIR --measured`; `osr slm train --catalogue catalogue.json --source NAME` trains
on measured, priced catalogue models and on selected caches only.
- `adapters.attach_chat_handlers()`: give every LLM target a `chat_handler` on one OpenAI-compatible
client (`metadata.model` names the upstream model, or a default). The CLI does this at startup when
`OSR_LLM_BASE_URL` is set (`OSR_LLM_API_KEY`, `OSR_LLM_API_KEY_ENV`, `OSR_LLM_MODEL`), so the
container's `/v1/chat/completions` executes against OpenAI, Azure OpenAI, vLLM, Ollama, OpenRouter
or LiteLLM instead of answering 502. `huggingface_search()` limits model hits to `text-generation`
and falls back to per-keyword queries (the Hub matches repo names, not natural language);
`fetch_json()` returns `None` for an empty 200 body (DuckDuckGo does that under load).
- Deployment: `OSR_SLM` (Docker entrypoint, Helm `config.slm` -> `/config/slm.json`) loads a routing
SLM into the served ensemble; `OSR_LLM_BASE_URL` / `OSR_LLM_MODEL` are declared in the image.
- `learning.SelfImprover` / `ImprovementReport`: the closed loop - refresh catalogue, discover models
and datasets on the web, gather evidence (feedback, collected datasets, synthetic), train a
challenger on the train split, calibrate it on the holdout, and promote it over the champion only
when holdout accuracy improves by `OSR_SLM_MIN_GAIN`; JSONL history, `run(interval_s)` loop.
- CLI: `osr catalogue`, `osr collect`, `osr slm train|eval|predict|info`, `osr improve` and the global
`--slm` option; `settings.SLMSettings` (`OSR_SLM_*`) and `WeightSettings.slm`.
- Transformer encoders for the SLM. `learning.AttentionEncoder` is a pure-Python transformer block
(hashed token embeddings, sinusoidal positions, multi-head self-attention, residual, attention
pooling, hand-derived gradients) that `ContrastiveRouter(attention=...)` adds to the hashed query
encoder so tokens can condition on each other; opt in with `OSR_SLM_ENCODER=attention`
(`OSR_SLM_ATTENTION_HEADS`, `_HEAD_DIM`, `_MAX_TOKENS`, `_VOCAB`); the block is persisted in the
model file (`core.encoder`, `core.attention`) and restored by `from_file`. `learning.EmbeddingFeaturizer`
/ `load_embedder()` append a frozen pretrained sentence-transformer embedding (extra `embeddings`, or
any `Callable[[list[str]], list[list[float]]]`) as dense features, so `W` learns a linear head on top
of it; `RouterSLM(embedder=...)`, `OSR_SLM_EMBEDDER=` / `OSR_SLM_EMBEDDER_SCALE`, the file
records the embedder name and `from_file(embedder=...)` takes a custom one back.
- Self-operation: `learning.Autopilot` runs `SelfImprover` cycles inside a live process on a schedule
and on drift (`learning.DriftMonitor`, Page-Hinkley over the success indicator and quality of every
outcome the router learns from), rate-limited by a minimum gap, hot-swapping an accepted challenger
into the served `SLMStrategy` and rewriting the model file; failures are recorded, never fatal.
`Router.observers` is the outcome hook it (and any drift monitor or metric) plugs into.
`osr serve --autopilot [--autopilot-interval S --cache-dir DIR --catalogue FILE --source NAME
--search QUERY --tier TIER=TARGET --offline --min-rows N]`, `create_app(router, autopilot=...)`
exposes `autopilot` in `GET /stats` and `POST /autopilot/cycle`; settings
`OSR_SLM_AUTOPILOT_INTERVAL_S`, `_MIN_GAP_S`, `_DRIFT_THRESHOLD`, `_DRIFT_MIN_N`, `_REMEMBER`;
container / Helm: `OSR_AUTOPILOT=1`, `OSR_AUTOPILOT_ARGS`, `OSR_CACHE_DIR`, `OSR_CATALOGUE`.
`status()["busy"]` says a (possibly multi-minute) cycle is in flight. `DatasetCollector.corpus()` /
`cached()` skip the improver's own `improve-history.jsonl` and any non-row line in the cache dir -
the second cycle used to fail with `EvalRow.__init__() missing 'text'` when the log shared the dir.
- Routing datasets: `osr collect` knows ten public sources verified against the datasets-server -
the 2024-25 LMArena releases (`arena-100k`, `arena-140k`, `ppe-human`, `webdev-arena`), `mt-bench-human`,
`reward-bench`, `ultrafeedback` (per-model judge scores) and `routellm-gpt4` (RouteLLM's own "is the
cheap model good enough" labels), next to `routellm-battles` and `arena-55k`; gated ones stay listed but
off by default. Parsers understand the newer layouts (typed conversation turns, `winner` labels, per-model
score lists) and carry the code / language / math / hard-prompt tags into `context`; `tier_of()` has
name rules and a parameter-count fallback for the 2024-26 model generations (`gpt-4o-mini` -> mid,
`gemini-2.5-pro` -> frontier, `llama-3.1-8b` -> small ...) and `DatasetSource.tiers` folds every side
onto your catalogue at parse time (`--tier` no longer needs an exhaustive model table). Collection is
resumable: a `.meta.json` records the raw offset, a run cut short by a 429 keeps its rows and the
next `osr collect` continues from there (`DatasetCollector.progress()`); retries back off up to five
times / 60 s. `corpus()` fills `OSR_SLM_MAX_ROWS` round-robin across sources so one large dataset cannot
crowd the others out.
- SLM training is faster and generalises better. With numpy installed (extra `fast`, now in the serve
image) `ContrastiveRouter.fit()` runs the same SGD on dense arrays - about 10x faster (18 000 rows in
under a minute) - and inference projects through a dense mirror of `W`; `OSR_SLM_BACKEND=auto|numpy|python`
picks the arithmetic, both paths seed identically and produce the same model file (tested to 1e-9), so a
pure-Python deployment reads a numpy-trained model unchanged. The hashed featurizer caches token hashes.
The model gains a learned per-target logit bias (`B`, the base rate a unit-norm dot product cannot
express; older files load with none), inverse-time learning-rate decay (`OSR_SLM_LR_DECAY`, default 2) and
regularised defaults (`epochs` 4, `l2` 1e-3): on the 18 000-row mixed corpus the previous defaults
overfit below the constant "always the mid tier" baseline (0.62 vs 0.64 on a random holdout); the new
ones sit at 0.64-0.66 with holdout loss 0.98-1.00 vs 1.11 and are stable across seeds.
- The SLM no longer harms mixed catalogues. Scored rows train a softmax over the *scored population* (the
targets any row of the corpus scores, plus `OSR_SLM_NULL_TARGETS` learned "none of these" logits) instead
of the whole catalogue, so a corpus of LLM battles leaves the embeddings of tools, skills and agents it
never compared untouched (they used to be driven to p=0.0005 and any SLM took `examples/eval_dataset.jsonl`
from 0.83 to 0.43; it is 0.80 now). `SLMStrategy` / `ContrastiveStrategy` abstain on targets the model
has never compared (`ContrastiveRouter.B` records them; `RouterSLM.predict_proba()` with no explicit
candidates ranks only those), score the rest centred (best 1.0, mean 0.5, so a flat softmax is "no
preference" rather than a boost) and scale confidence by the share of the request's candidates they know.
Chosen on three leave-one-source-out suites (PPE 0.561, MT-Bench 0.508, WebDev Arena 0.506 - see
`docs/ROADMAP.md` v0.4).
- `eval.load_dataset()` raises `ConfigurationError` naming the file and line for a line that is not JSON, not
an object or has no text (it used to surface as a `TypeError` traceback), and `osr catalogue | collect | slm
| improve` print every SDK error as one `error:` line with its path / line / preset / backend detail and exit
1. `ContrastiveRouter(backend=...)` with an unknown backend is a `ConfigurationError` too.
Agentic core (PLATFORM_PLAN Phase 0)
- `signals.EventSignal`, `WorkflowSignal`, `parse_event()`, `EventInfo` and the `EVENT_*` lexicons:
textless requests (`context["event"]`, `context["workflow"]`) get domain / action signals so events
route to workflows and agents; rules match `event:` globs and `workflow: true`.
- `strategies.SessionAffinityStrategy` / `SessionState`: keep multi-turn conversations on the target
that owns the session unless quality drops; `Router.route(..., exclude=[...])` and fallback
candidates; `PLAN_ROLES` is kind-aware (agent / workflow plans need no persona); per-call pricing
on targets (`cost.usd_per_call`, `RouteTarget.cost_per_call`, `estimated_cost()`); `observe` hook.
- Proxy: `osr/auto:` presets (`cheap`, `fast`, `quality`, `private`, `agentic`, `llm`),
`osr` request block (exclude, objective, plan, fallbacks), `X-OSR-App` attribution and
`X-OSR-Target` / `X-OSR-Request-Id` response headers; metadata carries alternatives and cost.
- Executors: `adapters.http_handler()`, `mcp_tool_handler()` and queue-backed targets
(`Queue` protocol, `InMemoryQueue`, `queue_handler()`, `PendingResult`) so agents, tools and human
queues execute through the same plan runner.
- Open Capability Manifest: `spec/ocm/` (JSON Schema + README), `opensmartroute.ocm`
(`validate_capability`, `target_from_capability`, `capability_from_target`, `load_capabilities`,
`bind_endpoint`), `load_targets()` accepts manifests and mixed catalogues, `osr validate` and
`osr export-ocm`.
- Routing Audit and savings: `eval.routing_audit()` / `AuditReport` / `load_audit_log()` replay
logged traffic against a catalogue (savings vs baseline, policy violations, route mix);
`osr audit logs.jsonl --baseline ... --markdown|--json [--min-savings]`;
`enterprise.SavingsLedger` telemetry sink (`RouterBuilder.with_savings()`, `EnterpriseRouter.savings`)
with per-request baseline-vs-routed entries and `SavingsReport.to_markdown()`.
- CLI: `osr route --constraint KEY=VALUE` (hard constraints) and `--kinds a,b`; `--event`, `--session`,
`--exclude`.
- Platform: `GET /api/v1/savings`, `GET /api/v1/signals`, `GET /api/v1/admin/pql` (product-qualified
leads); web `/roi` calculator, `/compare/{openrouter,litellm,portkey,diy}` pages and the dashboard
savings view.
- Docs: sales enablement kit under `docs/sales/` (discovery guide, demo script, Routing Audit,
pilot charter, ROI calculator, security pack, battlecards, pricing and proposal, case study and QBR).
Research track (every previously `Research` roadmap row now has code, tests and a module reference)
- `learning.ContrastiveRouter` and `ContrastiveStrategy`: RouterDC-style contrastive training on
(query, target) pairs. Softmax cross-entropy over `q . e_t / tau` against a row's *acceptable set*
(`acceptable_set(row, slack)`, every target within `slack` of the best score), so several right
answers are not penalised; `fit(objective="distilled")` trains against Zooter-style reward-distilled
`soft_labels(scores, temperature)` instead of one-hot labels; hashed encoders, pure-Python SGD,
online updates from outcomes, `state()/load()`.
- `learning.PolicyGradientStrategy`, `decision_reward()`, `decision_regret()` and `RegretReport`:
end-to-end REINFORCE routing (Router-R1 / RLCascadeRouter) on hashed request features with an EWMA
baseline and entropy bonus, trained on the *decision* utility rather than a quality prediction;
`decision_regret(rows, targets, choose, predict)` reports decision regret next to prediction MAE so
the two losses can be compared on the same dataset.
- `eval.headroom`: `routing_headroom()` splits oracle minus best-single into the label-noise floor and
measurable headroom; `target_diversity()` (pairwise disagreement, winner entropy / share);
`min_catalogue(rows, targets, fraction)` (smallest subset keeping a fraction of the oracle);
`scaling_curve()` (oracle / best-single / headroom against catalogue size);
`learnability_by_difficulty()` (per-difficulty-bucket headroom and noise floor, the "least learnable
where most valuable" check for agentic pools).
- `strategies.MemoryRouter`, `MemoryTier`, `MemoryItem`, `ImportanceGate`, `RecallResult`: BudgetMem-style
memory-tier routing. Tiers carry capacity, write / read cost per token and latency; a hashed logistic
gate decides what is worth writing and learns from `feedback(used_ids, recalled)`; value decays per
`tick()`, lowest-value residents are demoted when a tier is full; `recall(query, budget_tokens)`
fills a token budget by relevance x value.
- `strategies.ModalityStrategy`, `ModalityEscalation`, `EscalationResult`, `request_modalities()`:
penalise text-only targets when a request carries images / screens / audio / video / files that a
text surrogate (OCR, caption, transcript) only describes; text-first escalation keeps a Beta
posterior on "the surrogate sufficed" per modality set and calls the multimodal target first when
that probability is low.
- `strategies.SemanticCache`, `SemanticCacheStrategy`, `CacheEntry`, `CacheHit`: a semantic cache as a
`DESTINATION` target. `SemanticCache.target()` serves near-duplicate answers (cosine over the router's
embedder, TTL, LRU bound, source-based invalidation); the strategy scores only cache targets from the
hit similarity and learns per-similarity-band hit quality from outcomes.
- `strategies.AnnotatorPool`, `AnnotatorSkill`, `QuorumPlan`, `quorum_accuracy()`,
`HumanRoutingStrategy`: QUORUM-style routing among `HUMAN` targets. Per-domain annotator accuracy is
estimated by Dawid-Skene EM over labels (with gold labels and direct quality reports as evidence,
prior seeded from `quality_prior`); `select_quorum(domain, target_accuracy, budget)` picks the
cheapest majority-vote quorum that reaches the target accuracy.
- `strategies.SpeculativeCascade`, `SpeculativeResult`, `expected_mode_costs()`: speculative
draft / strong cascades. A Beta acceptance posterior per complexity bucket decides between
`draft_only`, `speculate` (both in parallel, strong cancelled when the draft is accepted) and
`strong_only` by expected cost + latency under the request's `Objective`.
- `learning.HistoryTargetStrategy`, `HistoryTargetModel` and `history_vector()`: multi-turn routing
on history-target joint embeddings (MTRouter). A logistic model over `h * e_t` (recency-weighted
hashed history and current text, catalogue target embedding) with shared weights and a per-target
bias learns that the same follow-up routes differently depending on the conversation so far;
inductive for unseen targets; `context["last_target"]` earns an incumbent bonus that becomes a
penalty when `context["last_failed"]` is set; `state()/load()/merge()`, persisted by `AutoLearner`.
- `errors.deprecated(name, since=, removal=, replacement=)` and `errors.OpenSmartRouteDeprecationWarning`
implement the public API deprecation policy documented in CONTRIBUTING.md.
- `signals.VerbalisedDifficultySignal` and `parse_difficulty()`: a small model's verbalised
difficulty (`0.7`, `7/10`, "hard") from `context["difficulty"]` or a callable blends into
`complexity` and raises `reasoning_need`. `signals.DraftResponseSignal` and `draft_features()`
derive hedging, self-correction, query overlap and length ratio from a cheap draft answer.
- `signals.semantic_entropy()`, `semantic_clusters()`, `response_uncertainty()` (meaning clusters over
sampled answers, hedging, optional P(True) judge), `signals.UncertaintyGate` (a `Cascade` quality
gate / `MixtureOfAgents` trigger built on them) and `signals.EventTrigger` / `TriggerRule`
(threshold rules that fire named actions).
- `math.DirichletProbe` (evidential Dirichlet head with digamma loss and KL regulariser, pure Python
SGD, `predict()` returns probabilities and epistemic uncertainty `K/S`) and
`strategies.HiddenStateStrategy(state_fn, targets, dim)` that routes on host hidden states and
lowers its confidence on unfamiliar states.
- `math.EnergyModel` (per-target ridge fit of `Wh = e0 + e_in * prompt + e_out * output`, gCO2 via a
grid factor) and `math.HardwareProfile` / `hardware_profile()` priors for A100, H100, L4, RTX 4090,
16-core CPU and an edge NPU.
- `strategies.expand_elastic(parent, [BudgetVariant(...)])` creates `@` siblings of an
elastic model; `strategies.TokenBudgetStrategy` scores budget fit (truncation risk vs unused
capacity).
- `strategies.EdgeCloudStrategy`: Thompson posterior of edge success per complexity bucket, decode
time against the latency SLO, upload penalty for the cloud tier and a hard exclusion when the
request's data boundary forbids a public target.
- `strategies.AuctionStrategy`, `AuctionResult`, `default_bid()`: error-aware reverse auction where
each bidder's claimed success is corrected by its observed bias; second-price payment.
- `strategies.ProtocolPolicy`, `ProtocolRule`, `DEFAULT_RULES`, `failure_risk()`: risk / budget /
task-type rules choose `single`, `cascade`, `aggregate`, `debate` or `handoff`; a per-protocol
ledger reports which protocol paid.
- `strategies.SelfEscalation` and `wrap_stream()`: Bayesian competence posterior updated per streamed
chunk with an optimal-stopping rule; `SelfEscalation.for_target()` seeds the prior from
`quality_prior`.
- `strategies.MixtureOfAgents`, `majority_vote()`, `AggregateResult`: call the top-k alternatives when
the decision is unsure and the budget allows, aggregate by semantic majority (or a supplied
synthesiser) and emit one `Outcome` per participant.
- `learning.UserAdaptiveStrategy` (per-user Beta posteriors shrunk toward neighbour users and the
global posterior; `observe_user()` warm start), `learning.SkillAffinity` (profile-bucket skill
relevance for `select_skill_set(relevance=)`), `profile_vector()`, `profile_bucket()`.
- `learning.MixtureCureModel` (Weibull mixture-cure on cumulative trajectory risk with censoring, grid
MLE) and `learning.HandoffPolicy` (permanent hand-off on eventual-failure probability or horizon
hazard; releases the task's `TaskPins` pin).
- `opensmartroute.discovery`: `schema_match()` / `extract_entities()` / `SchemaAwareStrategy`
(coverage of required `input_schema` properties by typed entities in the request),
`CachePreservingSelector` (per-session prefix-stable tool ordering with lazy eviction and
`prefix_hit_ratio()`), `SkillGraph` (`requires` / `conflicts` / `composes` edges plus learned
co-usage; `compose()` returns a dependency order).
- `adapters.recommend_servers()`, `ServerCard`, `ServerRecommendation`: rank MCP servers for a task
with BM25 + hashing-cosine fusion over server cards and hard constraint filters.
- `adapters.load_semantic_router_config()` / `SemanticRouterImport`: import a vLLM semantic-router
`config.yaml` (categories, model scores, reasoning flags, pricing, system prompts) as targets and
rules.
- `load_skills` exposes `osr-requires`, `osr-conflicts` and `osr-composes` frontmatter as
`metadata["requires" | "conflicts" | "composes"]` for `SkillGraph`.
- `tests/test_roadmap_research.py` covers every item above; `tests/public_api.json` lists the new
names.
Decorator SDK
- `opensmartroute.sdk`: `ComponentRegistry` plus the decorators `@strategy`, `@signal`,
`@policy_rule`, `@middleware`, `@telemetry` and `@target` / `@tool` / `@skill` / `@agent`. Plain
functions are adapted through `FunctionStrategy`, `FunctionSignal` and `FunctionMiddleware`;
classes are registered as factories. `registry.router()` / `registry.builder()` assemble a
`Router` / `RouterBuilder` from everything declared; `include("pkg.mod[:hook]")` and
`discover()` (entry-point group `opensmartroute.plugins`) load plugins. The process-wide registry
is exported as `opensmartroute.components` and the top-level decorators bind to it.
- `RouterBuilder.with_components(registry, targets=, strategies=, signals=, policy=, middleware=,
telemetry=)` wires a registry into an existing builder.
- `strategies.default_strategies(seed=, settings=, state_dir=)` builds the default ensemble.
Settings
- `opensmartroute.settings`: immutable `Settings` grouped into `RoutingSettings`,
`PolicySettings`, `RulesSettings`, `CapabilitySettings`, `BanditSettings` and `WeightSettings`;
`configure()`, `get_settings()`, `Settings.from_env()` (`OSR__` overlay with typed
parsing and `ConfigurationError` on bad values) and `Settings.env_keys()`. `Router`,
`RouterBuilder`, `Policy`, `RulesStrategy`, `CapabilityStrategy` and `BanditStrategy` accept
`settings=`; every previously hardcoded threshold, weight and scale now resolves from it.
- `Router.weight_of(strategy)` reports the effective ensemble weight.
- `osr settings [--json]` prints the effective tunables, their `OSR_*` keys and which are overridden.
Agent skills
- `.claude/skills/`: eight Agent-Skills packages (`osr-routing-catalogue`, `osr-decorator-sdk`,
`osr-enterprise-builder`, `osr-evaluation`, `osr-security-hardening`, `osr-integrations`,
`osr-deploy-serve`, `osr-contributing`) that teach coding agents such as Claude Code how to use and
extend the library. They are valid `TargetKind.SKILL` targets: `tests/test_skills.py` loads them
with `load_skills`, checks the routing hints against the signal ontology and asserts the router
fills the plan's skill slot with the right package for a representative request.
- `osr skills [ROOT] [--json]` validates and lists SKILL.md packages (default root
`branding.SKILLS_DIR` = `.claude/skills`); `osr --skills ROOT ...` adds them to any routing command.
Documentation coverage
- `docs/REFERENCE.md`: generated API reference listing every module under `src/opensmartroute` and
every exported name with its kind, signature and one-line summary; re-exports link to their
definition. Produced by `scripts/api_reference.py` (`write` | `check` | `report`), stdlib only.
- Every exported class, function, constant and type alias now carries a one-line summary (docstring,
or a same-line comment for constants). `tests/test_docs.py` fails on an undocumented name, a stale
reference, a module missing from the reference, or a `docs/*.md` file not linked from the README.
Policy composition
- `Policy` is an ordered chain of `PolicyRule` callables (`enabled`, `allow_list`, `deny_list`,
`allowed_kinds`, `region`, `DataBoundaryRule`, `pii`, `tenant`, `cost_budget`, `latency_slo`,
`input_tokens`, `context_window`, `tools`, `modalities`, `LanguageRule`, `JailbreakRule`).
`Policy.with_rules(*rules)` appends rules (subclasses that override `check` are folded in as a
single rule); `Policy.rule_names` and `default_rules()` expose the chain.
Branding
- `opensmartroute.branding`: single source of truth for brand-derived names (`BRAND`, `PACKAGE`,
`CLI`, `ENV_PREFIX`, `ERROR_CODE_PREFIX`, `METADATA_PREFIX`, `STATE_DIR`, `ENTRY_POINT_GROUP`)
with `env_key()`, `error_code()`, `metadata_key()`, `logger()`, `user_agent()` and `version()`.
Error codes, `osr-*` frontmatter keys, the HTTP `User-Agent`, MCP `clientInfo`, the OpenAI proxy
alias and the FastAPI title/version derive from it.
Learning
- Judge-score calibration: `LLMJudgeStrategy(calibrate=True, min_fit=20, calibrator=)` keeps an
`IsotonicCalibrator` per judge, pairs each raw judge score with the served target's observed
outcome and re-fits on every `update()`; `Router.learn()` forwards outcomes to the judge even
when it is only used for escalation. Raw scores stay visible in the rationale (`raw=`); state
round-trips through `state()` / `load()`.
- `learning.warm_start_from_matrix(rows, strategies, weight=)`: offline full-information reward
matrix `(prompt, {target: reward})` replayed into every learner (LinUCB ridge per arm, IRT,
Thompson posteriors, task table, Markov rewards) before going online.
- `MarkovStrategy(state_from="auto" | "task_type" | "domain", decay=)`: conversation state is the
learned task type joined with the domain (not only the domain); per-request state memory so an
outcome credits the state that was scored. `MarkovChain(decay=)` forgets transition counts,
`RoutingMDP(decay=)` keeps exponentially weighted rewards.
- Federated merge on every learner: `MarkovChain.merge`, `RoutingMDP.merge`, `MarkovStrategy.merge`,
`BanditStrategy.merge`, `TaskTableStrategy.merge`; `learning.merge_learners(local, remote)` pairs
same-named strategies across replicas. `AutoLearner.refresh()` implements the writer / reader
pattern for replicas sharing one `StateStore`.
- Cascade steps feed every learner: `CascadeStep.cost_usd`, `CascadeResult.total_cost_usd`,
`CascadeResult.outcomes(request)` (one per-step `Outcome`, rejected steps are failures) and
`Router.learn_cascade(request, result)`. Learners keep their per-request memory while
`Outcome.step` is set so multi-step trajectories credit every step.
- Strategies condition on the plan role: `Router._build_plan` tags sub-route signals with
`signals.extra["role"]`; `BanditStrategy` contexts, `TaskTableStrategy` rows (`skill:` plus
`family:skill:`), `MarkovStrategy` states and every learner's request memory are keyed by
role (`strategies.base.role_of` / `memory_key`), so a target's record as a skill or persona never
leaks into its record as the primary answerer. `TaskTableStrategy` now learns from role outcomes.
Economics
- Energy and carbon as cost dimensions: `RouteTarget.unit_energy` (`cost["wh_per_1k_tokens"]`),
`RouteTarget.unit_carbon` (`cost["gco2_per_1k_tokens"]`, or energy x `cost["gco2_per_wh"]`) and
`Objective(energy=, carbon=)` weights applied to the log-normalised values in the utility.
Resilience
- `AutoLearner.load()` quarantines corrupt or incompatible learner state instead of failing: the file
is renamed `*.corrupt-` (or the store key copied under `quarantine/`), the learner starts blank
and `AutoLearner.quarantined` lists `(strategy, reason)`. A failing `StateStore.get` is recorded,
not raised.
- `FileAuditSink` resumes its hash chain from the last line on disk across restarts and gains
`FileAuditSink.verify(path) -> (ok, n, first_problem)` detecting edited, removed or unparseable lines.
- `LLMJudgeStrategy` filters judge output item by item: non-dict items, unparseable / NaN scores and
hallucinated or injected target ids are skipped without discarding the rest of the ranking;
non-string judge returns are a failure mode with zero confidence.
- `scripts/bench.py --scale` sweeps 16 / 64 / 256 / 1024 targets with and without retrieval narrowing
and reports p50 / p95 / p99; `docs/ARCHITECTURE.md` documents the measured envelope.
- `tests/test_resilience.py`: audit-chain verification, judge JSON failure modes, state-corruption
recovery (files and stores), multi-replica hand-off.
Deployment
- `deploy/Dockerfile` (non-root, multi-stage, `OSR_*` environment to `osr serve`), `deploy/entrypoint.sh`
and the Helm chart `deploy/helm/opensmartroute` (ConfigMap-driven catalogue, hardened pod security
context, HPA, PDB, NetworkPolicy, optional PVC with a multi-replica warning).
Roadmap exit criteria
- `eval.criteria`: every offline-measurable exit criterion in `docs/ROADMAP.md` as a simulation that
drives the real `Router` / learners and returns a `CriterionResult` - `cold_start_ratio()` (v0.5),
`multi_round_vs_best_single()` (v0.6), `match_at_1_at_scale()` with `synthetic_tool_catalogue()`
(v0.7), `conformal_coverage()`, `knapsack_never_exceeds_cap()` and `ope_within_live_ci()` (v0.8),
plus `bootstrap_ci()` and `run_all()`. `scripts/exit_criteria.py [--full] [--json]` runs them and
exits non-zero when one fails.
- `osr eval --multi-round [--threshold] [--max-rounds]` compares `MultiRoundExecutor` against the
best single target on a dataset whose rows carry per-target `scores`.
- Cold-start exploration: `Router(explore_rate=, explore_min_samples=, explore_seed=)` and
`RoutingSettings.explore_rate` / `explore_min_samples` occasionally serve the least-seen viable
candidate until it has enough outcomes; propensities include the exploration mass and
`signals.extra["explored"]` marks the request.
- `MultiKnapsackBandit(on_capped="release" | "abstain")` and `capped(scores)`: when every arm would
breach a hard cap the bandit can abstain instead of releasing the cheapest arm.
- `MultiRoundExecutor(failures_before_switch=)` forwards the switch policy to `ProgressRouter`.
- Thread safety: `Router.lock` (re-entrant) guards strategy scoring, every learner update, task
pins, request memory, retrieval narrowing and the exploration RNG; the LLM judge is scored outside
it. `AutoLearner(lock=)` shares that lock (`EnterpriseRouter` / `RouterBuilder` pass it), snapshots
are deep-copied and sequence-numbered so a slow `save()` never overwrites a newer one, and a
separate I/O lock orders writers against `load()` / `refresh()`. `SemanticCache`, `MemoryRouter`,
`AnnotatorPool` and `ModalityEscalation` lock their own state.
- `SpeculativeCascade(concurrency=, draft_timeout_s=, history_size=)`: bounded worker pool sized for
concurrent speculative runs, draft timeout, draft exceptions escalate to the strong target
(`details["draft_error"]` / `["draft_timeout"]`), late strong failures are swallowed after an accepted
draft, `draft_failures` / `strong_failures` counters and `stats()`.
- `ModalityEscalation.run` escalates when the text target raises (`details["reason"]`) instead of failing.
- `MultiRoundExecutor`: rounds whose handler raised become `Round(error=...)` and the loop continues
on another target; one judged learn per round (no provisional + correction double count);
`deadline_ms`, `return_best`, `credit_task`, `RoundResult.stop_reason` / `best` / `errors` /
`total_cost_usd`; `arun()` for coroutine handlers and an `async` judge. `ProgressRouter.run_step`
(`learn=`) routes via `router.route`, records a failed step before re-raising, estimates step cost
from the target's unit cost when the handler reports none; `arun_step` twin.
- `tests/test_e2e_scenarios.py`: real-scenario suite with no stubs - the shipped `examples/`
catalogue and rules driven end to end through an in-process OpenAI-compatible provider fleet over
HTTP (PII boundary, tenant deny lists and data boundaries, persona plans, metrics, hash-chained
audit, learner state surviving a restart), a provider outage opening and recovering a circuit
breaker, the FastAPI service over an `EnterpriseRouter` (OpenAI proxy, feedback, stats, error
mapping), the `osr` CLI (`route`, `eval --frontier`, `--min-accuracy` gate, `targets`, `stats`),
`ProgressRouter` / `MultiRoundExecutor` agentic loops, a real MCP server subprocess over stdio, and
24 concurrent requests through the full middleware stack.
- `EnterpriseRouter.credit_task()`: delivers the delayed task reward through the router and persists
the re-credited learner state; `MultiRoundExecutor(EnterpriseRouter)` now closes the task-credit loop.
- Execution: a primary target without a handler (an instructions-only skill or persona) runs on the
plan's `llm` slot - its instructions are disclosed in the system prompt, the model answers, and the
model is credited with a `role="llm"` outcome - instead of failing with "has no handler".
- Execution enforces the PII boundary set up by `GuardMiddleware(redact=True)`: targets whose
constraints allow PII receive the original text (`context["pii_restored"]`), every other target
keeps the placeholders (`context["pii_redacted"]`), and the mapping never reaches a handler.
- `create_app()` accepts an `EnterpriseRouter` (middleware, health, telemetry and audit apply to every
HTTP request; `/stats` includes the health snapshot) and maps `SecurityError` / `ValidationError`
to 400, `NoRouteError` to 422, `TargetUnavailableError` to 503 and `ExecutionError` to 502.
- `DomainActionSignal` detects arithmetic and unit conversions (`17% of 2,450`, `12 * 4`,
`5 miles to km`, percent / calculate / square root ...) as the `math` domain, so calculator-style
tools and `max_complexity` math rules fire; lexicon keywords made of symbols (`%`) now match.
- `examples/targets.yaml`: the public cloud models and the research agent carry
`constraints.pii_allowed: false`, making PII a hard policy stop instead of only a rule preference.
Hosted platform (`platform/`, separate from the zero-dependency SDK): `platform/api` (package
`osr-platform-api`, FastAPI) and `platform/web` (`osr-platform-web`, Next.js 16)
- `osr_platform.app.create_platform_app()` / `osr-platform-api`: the routing API as a service:
`/api/v1/route` (decision, signals, ranked breakdown, policy rejections; `plan` / `execute`),
`/feedback`, `/targets`, `/catalogue`, `/usage`, `/keys`, `/tenants`, `/stats`, `/audit`, the
OpenAI-compatible `/v1/models` + `/v1/chat/completions` (`model: "auto"` or a pinned target), and
`/api/v1/docs` serving the repository Markdown (README, CHANGELOG, `docs/`) rendered to HTML with a
table of contents and Mermaid blocks as JSON. `OSR_PLATFORM_WEB_URL` / `OSR_PLATFORM_CORS_ORIGINS`
point `/` and Stripe redirects at the web app and allow it as a browser origin.
- Web app: landing page with a live catalogue, documentation (sidebar, table of contents, Mermaid),
pricing, a playground (decision view with ranked utility bars, signals, policy rejections, plans,
execution; OpenAI-compatible chat with `auto` or pinned model), signup / sign-in (key shown once,
stored in the browser only) and a dashboard (overview with usage chart, API keys, usage by target
and endpoint, tenants editor, audit trail with chain verification, plan and billing). Next.js route
handlers proxy `/api/*`, `/v1/*` and `/openapi.json` to `OSR_API_URL`, so the browser only talks to
the web origin; server components render from the API at request time.
- API keys (`osr_live_...`, SHA-256 at rest, `Authorization: Bearer` or `X-API-Key`), plans
(free / pro / enterprise with per-minute and per-day quotas, key and tenant caps, feature gates;
`OSR_PLATFORM_PLAN_OVERRIDES`), usage metering per key / endpoint / target with tokens and cost,
admin API (`X-Admin-Token`) and an optional Stripe checkout + signed-webhook billing hook that is
inert until `OSR_PLATFORM_STRIPE_*` is configured. State is SQLite (WAL, with a rollback-journal
fallback for SMB mounts).
- Editions: `OSR_PLATFORM_EDITION=community` runs the core `Router`; `enterprise` runs `RouterBuilder`
with auto-learning persistence, health circuit breakers, `GuardMiddleware`, metrics and a
hash-chained `FileAuditSink`, and unlocks tenants (per-tenant data boundary / region / deny lists /
cost caps applied as hard constraints), `/stats` and `/audit` (filtered per account).
- `OSR_PLATFORM_PROVIDERS` maps targets to OpenAI-compatible providers (Azure OpenAI, OpenAI, vLLM,
Ollama ...) through `adapters.openai_compat`; targets without a provider stay routable as decisions.
- `platform/api/Dockerfile` and `platform/web/Dockerfile` (non-root, health checks; the API keeps a
`/data` volume), `infra/` Bicep (Container Apps `osr-api` + `osr-web`, Azure Files, ACR, Log
Analytics, Azure OpenAI with gpt-4.1 family deployments), `azure.yaml` for `azd up`, and the
`Platform` workflow (API tests, web lint/typecheck/build, two-container smoke, OIDC `azd` deploy).
Reference deployment: `rg-osr-prod` in Sweden Central.
- Public catalogue and rankings, measured on the deployment rather than curated: `GET /api/v1/models`
(model cards with capabilities, constraints, pricing per 1k / 1M tokens, executable flag, requests,
tokens, cost, success rate, latency, domains and week-over-week trend), `GET /api/v1/models/{id}`
(daily series, health, effort siblings), `GET /api/v1/rankings?days=&domain=` (share of tokens and
requests, trend, per-domain leaders) and `GET /api/v1/stats/public` (headline figures). Usage rows
now store `request_id`, `model`, `domain` and `complexity` (additive SQLite migration) so every
figure is reproducible from the same table that meters billing.
- `GET /api/v1/activity`: newest-first per-account request log with `endpoint` / `target` / `key`
filters and `before=` cursor paging.
- `POST /v1/chat/completions` extensions: `models` (candidate list; the router scores only those and
falls back down the ranking when an upstream call fails, up to three times), `stream: true`
(`text/event-stream` chunks, `[DONE]` terminator) and an `osr` object (`objective`, `constraints`,
`tenant`, `plan`, `fallbacks`). The `opensmartroute` metadata gained `plan`, `fallback_from`,
`cost_usd` and `latency_ms`; responses carry `X-Request-Id`, `X-OSR-Target`, `X-OSR-Confidence`.
- Web: `/models` explorer (search, domain, executable, kind and sort), `/models/{id}` pages with traffic
chart, routed-domain bars, capabilities, constraints, examples and copyable call snippets for the
live base URL, `/rankings` leaderboard (window, metric and domain filters), dashboard `/activity`
with expandable request rows and feedback snippet, playground compare mode (up to four targets in
parallel with cheapest / fastest badges and fallback markers; `?model=` deep link from model pages),
and a landing page fed by the public stats (hero figures, most-routed targets, domain leaders,
candidates + streaming sample, three-step getting started). Sitemap lists models and rankings.
- Platform single sign-on: `GET /api/v1/auth/providers`, `GET /auth/{provider}/start?next=` and
`POST /auth/{provider}/callback` (PKCE, HMAC-signed state, GitHub / Google / Microsoft / GitLab presets
and custom OIDC issuers via `OSR_PLATFORM_SSO_*`), `osr_sess_` browser sessions with 30-day TTL
(`GET /auth/sessions`, `POST /auth/logout?everywhere=`, `DELETE /auth/sessions/{id}`), linked
identities (`GET /auth/identities`, `DELETE /auth/identities/{provider}`; refuses to remove the only way
in), `/api/v1/me` reports `via` and `identities`, `/api/v1/info` lists `sso` providers. New SQLite
tables `identities` and `sessions` (additive migration).
- Web: "Continue with GitHub / Google / ..." on login and signup, `/auth/callback` exchange page (shows the
first API key once when the account is created), `/dashboard/account` (profile, linked identities,
browser sessions with revoke and sign out everywhere). Shared UI toolkit under `components/ui`
(Radix-based button variants, dialog, dropdown menu, tooltip, select, switch, progress, avatar, table,
sonner toasts) and a Recharts chart kit under `components/charts` (`TimeBars`, `TimeArea`,
`RankedBars`, `Donut`, `Sparkline`) with one palette and shared tooltip/legend/empty states.
Redesigned header (compact nav, Ctrl/Cmd+K catalogue search, account menu), light hero with a prompt
box that opens the playground pre-filled (`/playground?q=`), rankings charts (share of requests,
traffic by domain), dashboard usage stacked bars and quota progress, and a table view on `/models`.
- Platform users, workspaces and roles (B2C and B2B): new SQLite tables `users`, `memberships`, `invites`
and `sso_connections`, `accounts.kind` (`personal` / `organization`) and `accounts.slug`; existing
databases are migrated on start (every account becomes a personal workspace owned by its user).
`POST /signup` and first SSO sign-in create the user plus a personal workspace; `GET/POST /workspaces`,
`POST /workspaces/{id}/switch`, `PATCH/DELETE /workspace`, `GET /workspace/members`,
`PATCH/DELETE /workspace/members/{user_id}` (owner > admin > member, last owner protected),
`POST/DELETE /workspace/invites` (link shown once, 14-day TTL, seats = `plan.max_members`: free 3, pro 10,
enterprise 500), `GET /auth/invites/{token}` and `POST /auth/invites/{token}/accept`. Key and tenant
mutations need admin; API keys act as owner of their workspace. `/me` adds `user`, `role`, `workspaces`;
usage and activity rows carry `user_id`. Organization SSO (enterprise plan feature `sso`):
`GET/PUT/DELETE /workspace/sso` stores the company IdP (Google, Microsoft, GitLab, GitHub or any OIDC
issuer, allowed email domains, default role), served as provider `org-` with just-in-time
membership; `GET /auth/discover?email=` returns the sign-in options for an email domain.
- Web: workspace switcher in the dashboard sidebar, `/dashboard/members` (invite, roles, remove, pending
invitations), `/dashboard/workspace` (create organization with its first key, rename/slug, organization
SSO connection, leave), `/invite/[token]` acceptance page, and the sign-in page discovers company SSO
from the typed email ("Continue with ").
- Custom domains: Bicep parameters `webAliasDomains`, `apiCustomDomain`, `customDomainCertificates`
(managed certificates, TXT validation for the apex), outputs for the static IP and domain verification
id; `scripts/domains.ps1` configures Cloudflare DNS, redirects (`.com` to `.ai`, `www` to apex) and
SSL settings and binds the domains in two provision phases (see `platform/README.md`).
### Changed
- Web: the UI toolkit under `platform/web/src/components/ui` is now the shadcn/ui `new-york` kit
(`components.json`, `radix-ui` umbrella package, `class-variance-authority`, `cmdk`, `vaul`, `tw-animate-css`)
with the brand tokens mapped onto the kit's CSS variables in `globals.css`. Header (navigation menu with
grouped product/resources panels, mobile sheet, command search), footer, dashboard shell (sidebar), docs
sidebar (collapsible sections, mobile sheet) and version menu (dropdown) are rebuilt on it. Hand-rolled
tables, filter chips, selects, switches, search inputs, stat cards, accordions and sliders across the
marketing pages, models explorer/catalogue, marketplace, rankings, playground, estimator, ROI
calculator and dashboard pages use the kit primitives (`DataTable`, `StatCard`, `FilterChip`,
`SearchInput`, `Select` wrapper, `Switch`, `Progress`, `Accordion`, `Slider`). The per-scope
`@radix-ui/react-*` packages, `cn` and `next-themes` dependencies are removed.
- `Router(slot_quality_floor=, narrow_above=, narrow_to=)` and
`RouterBuilder.with_retrieval(narrow_above=, narrow_to=)` default to `None` (= settings) instead
of literals; `Router.calibrator` defaults to `TemperatureScaler(settings.routing.softmax_temperature)`
and plan-slot margins use it instead of a private softmax.
- `RouterBuilder.with_auto_learning()`, `with_health()` and `with_queue_awareness()` no longer
inject hardcoded weights; the defaults come from `WeightSettings`.
- `MultiKnapsackBandit`: the sliding-window spend meter is O(1) per step (running sums over a
bounded deque); shadow prices are updated in budget units (`avg / B - 1`) so a budget expressed in
tokens no longer blows the penalty up; the "release the cheapest arm" fallback only triggers when
*every* arm is blocked, not when utilities happen to be negative.
- Documentation: the README is a short overview (pitch, install, quick start, what it routes, how it
decides, production, security, documentation index, roadmap); the detailed walkthroughs (targets,
execution, real providers, learning, decorator SDK, enterprise builder, research-track modules,
CLI, routing latency, repository layout) moved to `docs/GUIDE.md`. `docs/ROADMAP.md` marks every
research-track row as implemented with its module; `docs/RESEARCH.md` and `docs/MATH.md` describe
the corresponding techniques and formulas (sections 10 to 16).
### Fixed
- `PageHinkley` accumulated `x - mean - delta`, so a perfectly stationary quality stream alarmed
after `threshold / delta` samples and `AutoLearner` reset healthy targets; the tolerance now has the
textbook sign (`+ delta`) and a constant stream never drifts.
- `Bandit.mean` is abstract and `LinUCB.mean()` returns the intercept estimate instead of a stub.
- `OpenAICompatClient` derives its `User-Agent` from the package version instead of a literal.
- `enterprise.ops.ABTest.observe` no longer divides by zero when the control arm has no samples yet.
- `server.py`: request bodies were parsed as query parameters (HTTP 422) because
`from __future__ import annotations` stringified the locally defined pydantic models; removed.
- `execute()` re-raised an `ExecutionError` from a provider handler (upstream 4xx, malformed body)
without recording a failed outcome, so learners and health never saw those failures.
- `EnterpriseRouter.learn()` with auto-learning bypassed the router's feedback store, task-credit
buffer and pins (only the strategies were updated); `RouterBuilder.with_feedback()` no longer
double-records through a learner sink.
- `CacheMiddleware` keyed only on text / constraints / objective, so a pinned (`candidates=`) or
`plan=True` call could be served another call's cached decision; per-call routing options are now
part of the key.
## [0.4.0] - 2026-09-04
### Added
Execution and plans
- `Router.run()`, `AsyncRouter.run()` and `EnterpriseRouter.run()` route and execute the resulting
plan through the new `opensmartroute.execution` module: the `skill` slot's handler runs as a
pre-processor (may return a rewritten `RouteRequest`, a `str` attached as `context["skill_output"]`,
or `None`), persona / skill / primary `instructions` are composed into `context["system"]`, the
primary handler (LLM, skill or agent harness) is called, and one `Outcome` per participant is
recorded and learned from. Returns `ExecutionResult(response, text, target_id, steps,
system_prompt, effective_request, outcomes)`.
- `RouteTarget.instructions` (persona prompt, skill body or agent standing instructions, disclosed
only when the target is part of the plan), `RouteTarget.effort` (reasoning-effort variant) and
`RouteTarget.family` (siblings share breaker and budget state).
- `Outcome.task_id`, `Outcome.role` and `Outcome.step` so delayed, task-level rewards can be joined
to every call in a trajectory and plan slots learn separately from the primary.
- `RouteRequest.profile` (user / tenant attributes) and `RouteRequest.history` (session turns) feed
`ProfileSignal` and `HistorySignal`.
- `Router(slot_quality_floor=0.5)`: a persona / skill slot is only filled when its best candidate's
quality estimate clears the floor.
Strategies and learning
- `TaskTableStrategy` (static task-type table with Wilson intervals; `fit_task_table()` from outcomes),
`DeferStrategy` (learning-to-defer for `TargetKind.HUMAN`), `EffortStrategy` and `decide_effort()`
(think vs. no-think), `ProgressRouter` and `MultiRoundExecutor` (progress-guided step routing and a
Router-R1 style route / execute / judge loop), `CascadePlanner` with `BeliefTracker`
(cascade as a finite-horizon MDP or POMDP with a stop action).
- `learning.TaskCredit` (uniform / discounted / last / blended credit assignment from a terminal
task reward), `learning.TaskPins` (admission-time pinning), `ExampleMiner`, `RequestMemory`,
`SimilarityFallback`, `target_embedding()`, `nearest_targets()` and `warm_start()` for cold-start
targets.
- `math.calibration`: `TemperatureScaler`, `IsotonicCalibrator`, `ConformalCalibrator` (split
conformal candidate sets with a coverage guarantee). `Router(calibrator=, conformal=)` exposes
calibrated confidence and `RouteDecision.candidate_set`.
- Forgetting (`decay < 1`) in `IRTModel` and `BradleyTerry`; `BradleyTerry.merge()` for federated
sufficient statistics; Sherman-Morrison rank-1 updates and surfaced posterior variance in `LinUCB`.
Signals
- `signals.ontology.TaskOntology` (families, types, subtypes with seed templates) and
`signals.models` (`HashedFeaturizer`, `HashedClassifier`, `HashedRegressor`, `synthesize_dataset()`)
behind `TaskTypeSignal`, `LearnedDifficultySignal`, `ReasoningNeedSignal`, `OutputLengthSignal`,
`SensitivitySignal`; weights ship as JSON (`SignalModelBundle`) and load with
`Router(extractors=learned_extractors(SignalModelBundle.from_file(path)))` or `osr --models path`.
- `osr train --out models.json [--from rows.jsonl] [--synth] [--gadget detector.json]`.
Catalogue interop and scale
- `opensmartroute.retrieval`: `BM25Index`, `DenseIndex`, `Retriever`, `rrf()` (reciprocal-rank fusion),
`select_skill_set()` (submodular skill-set selection under a token budget), `tool_search_target()` and
`execute_tool_target()` meta-tools. `Router(retriever=, narrow_above=500, narrow_to=50)` switches to
retrieve-then-rank for large catalogues.
- `adapters.mcp`: `tools_from_mcp()`, `connect_mcp()` (stdio JSON-RPC), `enrich_description()`,
`sign_manifest()` / `verify_manifest()` (HMAC-SHA256 or Ed25519), `description_risk()`;
`osr mcp-manifest sign|verify`.
- `adapters.a2a`: `fetch_agent_card()`, `agent_from_card()`, `skills_from_card()`, `a2a_handler()`.
- `adapters.frameworks`: `langgraph_node()`, `langgraph_condition()`, `maf_router_executor()`,
`openai_tool_spec()`, `route_and_execute()`.
- `adapters.personas`: `load_personas()` / `persona_target()` from Markdown, JSON, CSV and Copilot
`*.agent.md` / `*.chatmode.md` files.
- Agent-harness adapters: `AgentHarness`, `HarnessResult`, `CallableHarness`, `HTTPHarness`
(408 / 429 / 5xx map to `TargetUnavailableError`), `SubprocessHarness`, `harness_handler()`.
- SKILL.md loader (`load_skill`, `load_skills`, `skill_from_markdown`) for
[agentskills.io](https://agentskills.io/specification) folders; works with or without PyYAML.
- OpenAI-compatible proxy in the HTTP server: `POST /v1/chat/completions` (`model: "auto"` routes,
executes and learns) and `GET /v1/models`; `GET /healthz`.
Evaluation
- `eval.baselines`: random, cheapest, best-prior, most-expensive, static task table, oracle,
`multi_sample_oracle()` and `noise_floor()`; `osr eval --baselines`.
- `eval.robustness`: `repeat_flip_rate()`, `paraphrase_robustness()`, `profile_swap_fairness()`,
`coreset()`, `diversity()`; `osr eval --robustness`.
- `eval.frontier`: `frontier3()`, `hypervolume()`, `ablation_report()`; `osr eval --frontier3 --ablation`.
- `eval.ope`: IPS, SNIPS and doubly-robust off-policy estimates with weight clipping and effective
sample size; `osr ope logs.jsonl`.
- `eval.datasets`: presets for RouterBench, LLMRouterBench, RouterEval, xRouteBench and RouterXBench;
`osr eval --preset`.
- `calibration_report()` (ECE, Brier, reliability bins); `osr eval --calibration`.
- `osr eval --min-accuracy X` exits with status 2 below the threshold; CI uses it as the routing
accuracy gate.
Enterprise
- `enterprise.stores`: `RedisStateStore`, `SQLStateStore` (DB-API 2.0; PostgreSQL, SQLite, MySQL),
`VersionedStateStore` (schema envelope with forward migrations), `EncryptedStateStore`
(AES-256-GCM, key rotation; `crypto` extra), `BatchedStateStore` (write-behind), `NamespacedStateStore`.
`RouterBuilder.with_state_store()` persists learner state through any of them.
- `enterprise.ops`: `ShadowMiddleware` and `ABTest` (shadow or A/B routing judged by Wald's SPRT),
`FairShareMiddleware` (dominant-resource fairness across tenants), `InflightTracker` and
`QueueAwareStrategy` (Erlang-C / Kingman wait estimates from live in-flight counts).
`RouterBuilder.with_shadow()`, `.with_fair_share()`, `.with_queue_awareness()`,
`.with_calibration()`, `.with_retrieval()`, `.with_router_options()`.
- `opensmartroute.settings`: frozen, environment-overridable `Settings` (`OSR__`) for
every tunable constant; `opensmartroute.branding` centralises package identifiers.
- `deploy/`: non-root reference `Dockerfile`, `entrypoint.sh` and a Helm chart (Deployment, Service,
Ingress, HPA, ConfigMap). Releases publish `ghcr.io/isathish/opensmartroute:`.
Security
- `security.gadget.GadgetDetector` (learned confounder-gadget classifier; `InputGuard(learned=True)`),
`security.injection.injection_risk()` and `inspect_injection()`, `security.limits.ResourceLimiter`
and `ResourceLimitMiddleware` (per-task caps on steps, tool calls, depth, tokens, cost, wall-clock),
`security.provenance.OriginPolicy` (sensitive parameters of state-changing tools must originate
from the user turn), `security.safety.run_safety_suite()` (red-team routing cases);
`osr safety [--learned-guard]` exits with status 2 on any failure.
Project
- Public API snapshot test (`tests/public_api.json`, `tests/test_public_api.py`): adding or removing an
exported name fails CI until the snapshot is updated deliberately.
- Release automation: `scripts/release.py` (version consistency, changelog sections, release
preparation), `Prepare release` and `Release` workflows (release PR, tag, GitHub release with
changelog notes, PyPI trusted publishing with PEP 740 attestations, SLSA build provenance, GHCR
image), CodeQL for Python and workflows, Dependabot for actions and Python dependencies.
- CI matrix covers CPython 3.10 to 3.13 on Linux plus 3.12 on Windows, checks formatting, builds and
smoke-tests the container image.
- `examples/end_to_end.py`, `examples/skills/sql-reporting/SKILL.md`; `instructions` on the example
personas and skills.
### Changed
- `Router.execute()` / `AsyncRouter.execute()` keep returning the raw handler response but now go
through the plan-aware executor. `EnterpriseRouter` gains `execute()` (returns `ExecutionResult`).
- `load_targets` and `load_rules` are exported from the package root.
- Project metadata: repository URLs point at `isathish/OpenSmartRoute`; classifiers declare
CPython 3.13 and `Typing :: Typed`; the sdist no longer ships brand assets and fonts.
### Fixed
- `config.load_document` and the eval dataset loader read files as UTF-8 explicitly; on Windows the
locale default (cp1252) failed on the shipped `examples/targets.yaml`.
## [0.3.0] - 2026-09-04
### Added
- `opensmartroute.adapters`: stdlib `OpenAICompatClient` (timeouts, exponential backoff with
`Retry-After`, typed error mapping, key never logged) with `judge_fn`, `embedder`, `chat_handler`
glue; lazy `sentence_transformers_embedder` and `OpenTelemetryTelemetry`.
- `opensmartroute.config`: `load_targets` / `load_rules` / `load_document` with `ConfigurationError`
reporting file, entry and reason. `TargetRegistry.from_file` now delegates to it.
- `scripts/bench.py` — reproducible routing-latency benchmark; `scripts/brand_build.py` — logo system
generator with WCAG 2.2 audit and platform-standard exports.
- Brand identity: rounded-geometric wordmark (Quicksand) and Poppins text, vendored under OFL; light
and dark palettes verified ≥ 4.5:1 text / ≥ 3:1 graphics.
- `docs/ROADMAP.md` with per-release exit criteria; `docs/RESEARCH.md` rewritten as a survey with a
formal problem statement, taxonomy, systems comparison and idea→module map.
### Fixed
- `HealthPolicy` consumed a rate-limit token for **every** admissible candidate on every route; it now
only checks availability, and `EnterpriseRouter` reserves one token for the chosen target.
- `/feedback` endpoint dropped `complexity`, so IRT never learned from HTTP feedback.
### Changed
- `cli._load_rules` (private, but used by examples) replaced by public `config.load_rules`.
- Removed the earlier gradient ring / wordmark SVGs in favour of the generated system.
## [0.2.0] - 2026-09-04
### Added
- `opensmartroute.math`: Thompson/UCB1/LinUCB/ε-greedy bandits, cost-aware Lagrangian wrapper,
IRT (2PL) model, Bradley–Terry + Elo, Markov chain + routing MDP (value iteration),
EWMA/Welford, Page–Hinkley and ADWIN-lite drift, Wilson interval, Pareto/TOPSIS,
Erlang-C / Little's law / Kingman queueing.
- `opensmartroute.learning`: `IRTStrategy`, `PreferenceStrategy`, `LinUCBStrategy`,
`MarkovStrategy`, `AutoLearner` (fan-out + drift detection + atomic persistence).
- `opensmartroute.realtime`: circuit breaker, token-bucket rate limit, rolling budget,
`HealthRegistry`, `HealthPolicy`, `HealthStrategy`.
- `opensmartroute.enterprise`: `RouterBuilder`, `EnterpriseRouter`, middleware chain
(`CacheMiddleware`, `TenantMiddleware`, `TimeoutMiddleware`), telemetry ports
(`LoggingTelemetry`, `MetricsTelemetry`), `StateStore` ports, hash-chained `FileAuditSink`.
- `opensmartroute.security`: `InputGuard` (confounder-gadget detection), `Redactor`,
`GuardMiddleware`, `sanitize_for_prompt`, `load_secret`.
- `opensmartroute.aio.AsyncRouter`.
- Typed exception hierarchy in `opensmartroute.errors`; `py.typed` marker.
- Pinned rules (`pin: true`) and `primary: false` plan-only targets.
- CI (ruff, mypy, bandit, pytest, eval gate), SECURITY.md, CONTRIBUTING.md.
### Changed
- Lexicon matching is now whole-word (regex `\b`) — fixes false positives like `sql` in `australia`.
- Cost/latency normalisation is log-scaled; default `Objective` weights lowered.
- `NoRouteError` now derives from `OpenSmartRouteError` and carries `details["rejections"]`.
## [0.1.0] - 2026-09-04
- Initial release: core types, registry, signals, policy, rules/capability/similarity/bandit/
llm-judge/cascade strategies, router with plans, feedback store, eval harness, CLI, FastAPI server.
Predates the repository history: the code is the initial commit tagged `v0.2.0`.
[Unreleased]: https://github.com/isathish/OpenSmartRoute/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/isathish/OpenSmartRoute/compare/v0.5.0...v1.0.0
[0.5.0]: https://github.com/isathish/OpenSmartRoute/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/isathish/OpenSmartRoute/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/isathish/OpenSmartRoute/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/isathish/OpenSmartRoute/releases/tag/v0.2.0
[0.1.0]: https://github.com/isathish/OpenSmartRoute/tree/v0.2.0
---
# Roadmap
Status values: **Shipped** (in a tagged release), **Complete** (every item on `main` and the exit
criterion measured and met; ships with the next tag), **In progress** (code on `main`, part of the
exit criterion still open), **Planned**, **Research** (tracked, not scheduled). Where a criterion is
open, the section names exactly what is missing and what would close it.
Versioning follows SemVer. Each release has an **exit criterion** that must be met before tagging.
Items are ordered by dependency, not by preference. Dates are deliberately absent; the project ships
when the criteria are met.
This document is rebuilt from two inputs: (a) an engineering audit of the `v0.3.0` code base
(what is implemented, what is heuristic, what is missing; section 0), and (b) a literature scan of LLM /
agent / tool routing up to **September 2026** (summarised as design principles in section 0.3, cited per
item below, catalogued in [RESEARCH.md](https://opensmartroute.ai/docs/RESEARCH.md)). Every planned item names the paper or
concept it operationalises so the *why* survives the *what*.
`v0.4.0` shipped first implementations across the v0.4 to v0.9 themes and `v0.5.0` the research track,
the routing SLM and autopilot, estimates and the MCP server, the CLI sign-in flow and end-to-end tracing
(see [CHANGELOG.md](https://opensmartroute.ai/docs/changelog.md)).
The research track that followed (verbalised difficulty, draft-response and hidden-state
representations, elastic budgets, personalisation, self-escalation, hand-off, aggregation, protocol
selection, schema-aware and cache-preserving discovery, skill graphs, MCP server recommendation,
semantic-router import, energy models, edge/cloud and auctions) is implemented on `main` with tests;
each row below names the module. The per-version sections keep their exit criteria; an item is only
**Shipped** when its code is released, and a version is only closed when its exit criterion has been
measured and published.
---
## 0. Where we were: engineering audit of v0.3.0
### 0.1 What exists (and how it works)
| Layer | Implemented | Mechanism |
|---|---|---|
| Signals | `LengthSignal`, `LanguageSignal`, `ModalitySignal`, `ComplexitySignal`, `DomainActionSignal`, PII / jailbreak regexes | **Lexicon + regex heuristics**, <1 ms, zero deps. Nothing is learned. |
| Policy | `Policy.check/filter` | Hard constraints before scoring: enable, allow/deny, kind, region, boundary, PII, tenant, cost/latency/token SLO, modality, language, jailbreak ≥ 0.5 → safety/human only |
| Strategies | `Rules` (pin/prefer/avoid), `Capability`, `Similarity` (hashing embedder, optional sentence-transformers), `Bandit` (Beta–Bernoulli TS per domain), `LLMJudge`, `Cascade` (executor, not a scorer) | Ensemble $\hat q=\sum_k w_k\kappa_k s_k / \sum_k w_k\kappa_k$; utility $U=w_q\hat q-w_c\tilde c-w_\ell\tilde\ell$ with log-min-max normalisation and a hard quality floor; confidence = softmax margin at fixed $\tau=0.1$ |
| Learning | `IRTStrategy` (2PL, online SGA), `PreferenceStrategy` (Bradley–Terry / Elo per domain), `LinUCBStrategy` (disjoint LinUCB on a 8+|domains| signal vector), `MarkovStrategy` (Dirichlet-smoothed chain + value-iteration MDP), `AutoLearner` (fan-out, Page–Hinkley drift, atomic JSON persistence) | Learners are silent until $n \ge n_0$ ($\kappa=\min(1,n/n_0)$) so declarative strategies dominate at cold start |
| Maths | `ThompsonBeta` (with decay), `UCB1`, `LinUCB`, `EpsilonGreedy`, `CostAwareBandit` (Lagrangian dual ascent), `IRTModel`, `BradleyTerry`, `Elo`, `MarkovChain`, `RoutingMDP`, `EWMA`, `Welford`, `PageHinkley`, `WindowDrift`, Wilson, ECE, Pareto, TOPSIS, Erlang‑C, Little, Kingman | Pure Python, deterministic, serialisable |
| Real-time | `CircuitBreaker`, `TokenBucket`, `Budget`, `HealthRegistry/Policy/Strategy` | Non-consuming checks in policy; one token reserved for the chosen target only |
| Enterprise | `RouterBuilder`, `EnterpriseRouter`, middleware (`Guard`, `Tenant`, `Cache`, `Timeout`), `StateStore` (memory, file), `Telemetry` (logging, metrics), `AuditSink` (hash-chained JSONL) | Ports are ABCs; only in-process / file adapters ship |
| Security | `InputGuard` (size, token estimate, head/tail naturalness gadget score), `sanitize_for_prompt`, `Redactor`, `GuardMiddleware`, `load_secret` | Threat model T1–T10 in [SECURITY.md](https://opensmartroute.ai/docs/SECURITY.md) |
| Adapters | `OpenAICompatClient` (stdlib, retries, typed errors), `judge_fn`, `embedder`, `chat_handler`, agent harnesses (`CallableHarness`, `HTTPHarness`, `SubprocessHarness` → `HarnessResult`), SKILL.md loader (`load_skills`, agentskills.io frontmatter → `RouteTarget`), sentence-transformers, OpenTelemetry | No connection pooling |
| Execution | `execution.execute/aexecute`, `Router.run`, `AsyncRouter.run`, `EnterpriseRouter.run` | Plan-aware: skill pre-processor → persona/skill/primary `instructions` composed into `context["system"]` → primary handler; one `Outcome` per participant with `role` and shared `task_id`; slots gated by `slot_quality_floor` |
| Eval | `evaluate`, `cost_quality_frontier`, `area_under_frontier`, `expected_calibration_error` | Offline, single-draw labels, no counterfactual estimation |
| Surface | `Router`, `AsyncRouter`, FastAPI `create_app`, `osr` CLI, `config.load_targets/load_rules`, `FeedbackStore` (+ RouteLLM preference export) | |
### 0.2 What was missing (from the audit) and where it landed
| Gap | Where it bit | Roadmap home | Status |
|---|---|---|---|
| All signals are lexicon heuristics; no learned difficulty / task-type / reasoning-need classifier | `signals/__init__.py` | v0.4 | Shipped in 0.4.0 (`signals.models`, `signals.ontology`) |
| No **static task-type table** baseline or "always-strongest" baseline in `osr eval` | `eval/__init__.py` | v0.4 | Shipped in 0.4.0 (`eval.baselines`, `TaskTableStrategy`) |
| Single-draw oracle in eval; no paraphrase-robustness or pool-diversity metrics | `eval/__init__.py` | v0.4 | Shipped in 0.4.0 (`multi_sample_oracle`, `eval.robustness`) |
| Confidence uses fixed $\tau$; ECE computed but never used to **recalibrate**; no abstention / set-valued output | `router.py::_softmax_margin` | v0.4, v0.8 | Shipped in 0.4.0 (`math.calibration`, `RouteDecision.candidate_set`) |
| Target = one model; **reasoning effort / think-mode** is not a routable dimension | `core/types.py` | v0.5 | Shipped in 0.4.0 (`RouteTarget.effort`, `EffortStrategy`) |
| No personalisation: user / tenant profile does not condition scores | `core/types.py`, strategies | v0.5 | Shipped in 0.4.0 (`RouteRequest.profile`, `ProfileSignal`) |
| `Cascade` runs outside the ensemble and its steps are not fed back to learners; no "stop" action; fixed order | `strategies/cascade.py` | v0.6 | Shipped in 0.4.0 (`CascadePlanner`, `BeliefTracker`) |
| No trajectory step index; learners treat each outcome as immediate, so **delayed, task-level reward** is not joined | `core/types.py::Outcome`, `learning/` | v0.6 | Shipped in 0.4.0 (`Outcome.step`, `TaskCredit`, `TaskPins`) |
| Plan slots emit outcomes but strategies do not condition on role | `strategies/`, `learning/` | v0.6 | Shipped in 0.4.0 (`signals.extra["role"]`, role-keyed learner memory: `strategies.base.role_of` / `memory_key`) |
| No MCP / A2A import; no retrieve-then-rank narrowing for large catalogues; `SimilarityStrategy` is flat top-k | `adapters/` | v0.7 | Shipped in 0.4.0 (`adapters.mcp`, `adapters.a2a`, `retrieval`) |
| Only in-memory / file `StateStore`; per-outcome writes; no schema version in saved state | `enterprise/__init__.py`, `learning/__init__.py` | v0.7, v0.8 | Shipped in 0.4.0 (`enterprise.stores`) |
| IRT / BT / LinUCB / Markov have **no forgetting**; only `ThompsonBeta` decays | `math/irt.py`, `math/preference.py`, `math/bandits.py`, `math/markov.py` | v0.8 | Shipped in 0.4.0 (`decay=` / `discount=` on every learner) |
| `CostAwareBandit` handles one budget; no multi-resource knapsack, no shadow-audit stream | `math/bandits.py` | v0.8 | Shipped in 0.4.0 (`MultiKnapsackBandit`) |
| Latency is a static `latency_ms`; queue state / TTFT is not modelled although Erlang-C exists | `realtime/`, `math/decision.py` | v0.8 | Shipped in 0.4.0 (`enterprise.ops.QueueAwareStrategy`) |
| No off-policy evaluation (IPS / DR), no shadow mode, no A/B harness | `eval/` | v0.8 | Shipped in 0.4.0 (`eval.ope`, `enterprise.ops.ShadowMiddleware`) |
| No learning-to-defer objective for `TargetKind.HUMAN` | strategies | v0.8 | Shipped in 0.4.0 (`DeferStrategy`) |
| `bandits._inverse` is a naive Gauss-Jordan inverse (can go singular); posterior variance not surfaced in `StrategyScore` | `math/bandits.py` | v0.8 | Shipped in 0.4.0 (Sherman-Morrison updates, variance) |
| Energy / carbon not a cost dimension | `core/types.py::cost` | v0.8 | Shipped in 0.4.0 (`unit_energy`, `unit_carbon`, `Objective(energy=, carbon=)`) |
| Gadget detector is heuristic; no origin / provenance check on tool parameters in plans; no federated learning of router weights | `security/` | v0.9 | Shipped in 0.4.0 (`GadgetDetector`, `OriginPolicy`, `merge()` on every learner + `learning.merge_learners`) |
| Untested: cascade path, audit-chain verification, judge JSON failure modes, state-corruption recovery, multi-replica consistency | `tests/` | every release | Shipped in 0.4.0 (`tests/test_resilience.py`, `test_cascade_*`; `FileAuditSink.verify`, `AutoLearner.quarantined` / `refresh`) |
### 0.3 Evidence-driven principles (literature, 2025–2026)
These findings change defaults, not just features. Each is cited in [RESEARCH.md](https://opensmartroute.ai/docs/RESEARCH.md).
1. **Beat the boring baselines first.** Under unified evaluation many routers, including commercial ones,
fail to reliably outperform simple baselines, and a large router-to-oracle gap remains (LLMRouterBench,
Jan 2026). A static *task-type → model* table captured 21 of 29 routable questions in one study
("Most of the LLM Routing Gap Is Task Type", Aug 2026). ⇒ `osr eval` must always report
*always-strongest*, *always-cheapest* and *static task table* alongside any learned strategy, and a
`TaskTableStrategy` becomes a first-class, cheapest-possible learner.
2. **The oracle is noisy.** 12–36 % of the reported oracle gap on open-model pools is single-draw label
noise that no single-commit router can recover ("How Much of the Routing Gap Is Real?", Jul 2026).
⇒ evaluation adopts a multi-sample oracle protocol and reports a *reproducible headroom* figure.
3. **Curate, don't pile.** Larger ensembles show diminishing returns versus careful curation
(LLMRouterBench); hierarchic social entropy saturates below ~10 behaviourally distinct actors
("When is Routing Meaningful?", Jul 2026). ⇒ ship a diversity / coreset report for catalogues.
4. **Robustness is a metric.** Surface-form paraphrases should route identically; kNN-style routers
collapse under perturbation while prompted routers stay stable (same paper). ⇒ a paraphrase
robustness score in `osr eval`.
5. **Route the task, not the call.** Agentic workflows have one delayed, task-level outcome; per-call
routers mis-attribute feedback (TRACE-Router, Jul 2026; MTRouter, ACL 2026; ProgRouter, EMNLP 2026).
⇒ `Outcome` gains `task_id`; admission-time routing with pinned backend; terminal reward.
6. **Escalation is an optimal-stopping problem**, not a threshold (Bayesian self-escalation, Aug 2026;
TACIT-Switch, Aug 2026; RLCascadeRouter, Aug 2026). ⇒ cascade becomes an MDP with a *stop* action.
7. **Control risk, not just cost.** Conformal / distribution-free calibration gives set-valued routing
with abstention and provable mis-routing bounds (RACER, Feb 2026; RouteNLP, ACL 2026 industry;
CR², May 2026). ⇒ conformal thresholds replace hand-tuned `escalate_below`.
8. **Budgets are knapsacks and the world drifts.** Multi-resource constraints, shadow-audit streams,
pessimistic reward / optimistic cost, hard meters before commitment (Drift-Aware Sparse Routing,
Sep 2026). ⇒ generalise `CostAwareBandit`; add forgetting to every learner.
9. **Latency is queue state, not a constant** (Latency-aware routing with a TTFT estimator, Jul 2026;
HW-Router, DAC 2026). ⇒ wire `math.decision` queueing into `HealthStrategy`.
10. **Discovery at scale is retrieve-then-rank.** In-context selection collapses from Match@1 0.85 → 0.12
as registries grow to thousands of models / agents / tools / skills; retrieve-then-rank crosses over
at N≈500 (Enrich-Retrieve-Rank, Aug 2026); hybrid BM25 + dense with RRF cut MCP tool tokens by 99 %
in production (SCOUT, Aug 2026). ⇒ two-stage catalogue narrowing before scoring.
11. **Skill sets are submodular knapsacks** (Best Prefix Selection, Aug 2026). ⇒ `RoutePlan` skill slots
are selected as a set under a token budget, not top‑k.
12. **Reasoning effort is a routable dimension** (Think When Needed, SIGIR 2026; SCX Router predicts
task type, difficulty, reasoning mode and output length before generation, Sep 2026). ⇒ targets
carry `effort` variants; signals predict *whether to think*.
13. **Personalisation matters exactly when it changes the answer** (GMTRouter, EMNLP 2026; SkillFeed
profile-conditioned skill routing, Aug 2026; xRouteBench personalised tasks). ⇒ profile-conditioned
scoring with counterfactual tests.
14. **Routing data is fragmented and private.** Federated router training improves the accuracy–cost
frontier over client-local routers ("Federate the Router", Jan 2026). ⇒ mergeable learner state.
15. **The control plane is an attack surface.** Beyond confounder gadgets: indirect prompt injection via
tool content (ROPE, Aug 2026), unsigned MCP manifests, and recognition-without-enforcement gaps.
⇒ origin checks on state-changing tool parameters inside routed plans.
---
## v0.3: Real integrations (Shipped)
**Goal:** route to real providers with the same code that runs in tests.
| Item | Status | Notes |
|---|---|---|
| OpenAI-compatible HTTP client (stdlib, retries, typed errors) | Shipped | `adapters.OpenAICompatClient`; works with OpenAI, Azure, vLLM, Ollama, LiteLLM, OpenRouter, Groq |
| Judge / embedder / handler glue for the client | Shipped | `judge_fn`, `embedder`, `chat_handler` |
| sentence-transformers embedder | Shipped | `adapters.sentence_transformers_embedder` (extra: `embeddings`) |
| OpenTelemetry telemetry adapter | Shipped | `adapters.OpenTelemetryTelemetry` |
| Public config loaders with validation errors | Shipped | `config.load_targets / load_rules` |
| Rate-limit tokens consumed only for the chosen target | Shipped | bug fix in `HealthPolicy` / `EnterpriseRouter` |
**Exit criterion:** an end-to-end test routes, escalates to an LLM judge, and executes against an
in-process OpenAI-compatible server with zero mocks of the transport. Met.
---
## v0.4: Learned signals and honest evaluation (Shipped)
**Goal:** replace lexicon heuristics with trained, still-tiny models, without adding torch to the core,
and make `osr eval` hard to fool.
Ordering rationale: principle 1 (baselines) and 2 (noisy oracle) come **first** because they decide
whether anything in v0.5+ is worth building.
### 0.4a Evaluation harness
| Item | Status | Notes |
|---|---|---|
| Mandatory baselines in `osr eval`: always-strongest, always-cheapest, random, **static task-type table**, oracle | Shipped | `osr eval --baselines`; `eval.baselines` reports gap-to-oracle and the label-noise floor (LLMRouterBench 2601.07206; "Most of the routing gap is task type" 2608.23023) |
| Multi-sample oracle protocol | Shipped | `multi_sample_oracle()` and `noise_floor()` decompose the gap into *label-noise floor* vs *recoverable* (2607.03436) |
| Run-to-run variance | Shipped | `repeat_flip_rate()` in `osr eval --robustness` (5 % run-to-run flips observed in 2608.23023) |
| Paraphrase-robustness score | Shipped | `paraphrase_robustness()`: deterministic surface-form rewrites, % identical decisions (2607.09197) |
| Pool-diversity report | Shipped | `eval.robustness.diversity()` and greedy k-center `coreset()` |
| Latency-aware frontier | Shipped | `eval.frontier.frontier3()` and `hypervolume()`; `osr eval --frontier3` |
| Dataset adapters | Shipped | `eval.datasets` presets: `routerbench`, `llmrouterbench`, `routereval`, `xroutebench`, `routerxbench`; `osr eval --preset` |
| Component-level ablation report | Shipped | `ablation_report()` drops one strategy or extractor at a time; `osr eval --ablation` |
### 0.4b Learned signals (zero core deps)
| Item | Status | Notes |
|---|---|---|
| `TaskTableStrategy` | Shipped | per-(task type x target) success table with Wilson intervals; `fit_task_table()` from outcomes |
| Task-type classifier | Shipped | `TaskTypeSignal`: multinomial logistic regression on hashed n-grams over `signals.ontology.TaskOntology` (SCX Router 2609.02292); weights ship as JSON; falls back to `DOMAIN_LEXICON` |
| Difficulty classifier | Shipped | `LearnedDifficultySignal`; training on RouterBench / LLMRouterBench outcomes via `osr train --from` |
| Reasoning-need signal | Shipped | `ReasoningNeedSignal` (Think When Needed 2601.18146); consumed by `EffortStrategy` |
| Expected-output-length signal | Shipped | `OutputLengthSignal` regression head |
| Verbalised-difficulty retrieval | Shipped | `signals.VerbalisedDifficultySignal`: reads a small model's verbalised difficulty (number, `7/10`, or words) from `context["difficulty"]` or a callable, blends it into `complexity` and raises `reasoning_need`; `parse_difficulty()` (VDAR-Router 2607.18098) |
| Training CLI `osr train --from feedback.jsonl` | Shipped | produces the `SignalModelBundle` JSON; `--gadget` also trains the gadget detector |
| Cold-start synthetic supervision | Shipped | `synthesize_dataset()` generates labelled prompts per ontology node (TRouter 2604.09377; SCX); `osr train --synth` |
### 0.4c Calibration
| Item | Status | Notes |
|---|---|---|
| Temperature / isotonic fitting of `confidence` | Shipped | `TemperatureScaler`, `IsotonicCalibrator`; `osr eval --calibration` reports ECE, Brier and reliability bins |
| Judge-score calibration | Shipped | `LLMJudgeStrategy(calibrate=True, min_fit=20)` keeps an `IsotonicCalibrator` per judge instance, pairs each raw judge score with the served target's observed `quality`/`success` and re-fits on every outcome; raw scores are kept in the rationale (`raw=`) once the mapping is active; calibrator state is persisted with `state()/load()` |
| Confidence exposed per strategy | Shipped | posterior variance surfaced for LinUCB and IRT |
**Exit criterion:** on `examples/eval_dataset.jsonl` and one public suite, learned signals beat the
static task table by a margin larger than the measured label-noise floor; `osr eval` reports ECE <= 0.10;
paraphrase robustness >= 0.9; core dependency count remains **zero**.
**Measurement (published with 0.5.0, partially met).** Command:
`osr -t [--slm slm.json] eval --baselines --calibration --robustness --limit 1000`.
The public suites are the human-preference battles relabelled onto three tiers (`llm-small`,
`llm-mid`, `llm-frontier`) by `osr collect`; the SLM bundle was trained on Arena-55k + RouteLLM
battles and the suites below are held out.
| Suite | Router (learned) | Declarative | Static task table | Random | Cheapest | Paraphrase | ECE raw | ECE held-out isotonic |
|---|---|---|---|---|---|---|---|---|
| `examples/eval_dataset.jsonl` (n=30, 17 targets) | 0.833 | 0.833 | 0.233 | 0.133 | 0.067 | 0.867 | 0.245 | 0.261 (n=15) |
| MT-Bench human (n=1000) | 0.519 | 0.421 | 0.541 | 0.441 | 0.372 | 0.973 | 0.316 | 0.050 |
| PPE human (n=1000) | 0.561 | 0.418 | 0.651 | 0.445 | 0.174 | 0.898 | 0.279 | 0.036 |
| WebDev Arena (n=1000) | 0.533 | - | 0.670 | 0.391 | 0.000 | 0.872 | 0.253 | 0.060 |
- Met: learned signals beat the declarative router by +10 to +14 pp on every held-out suite and beat
random / cheapest / best-prior everywhere; zero core dependencies; paraphrase robustness >= 0.9 on
two of three public suites. With a calibrator fitted on half the suite, held-out ECE is 0.04-0.06
(isotonic) and 0.05-0.13 (temperature), i.e. the <= 0.10 target is met **after** fitting.
- Not met: on the public suites the static task table (which, with no task-type labels, degenerates to
"always the best single tier") still wins by 2-14 pp, because a pairwise preference between two
models is a weak label for a three-tier decision; the label-noise floor is not measurable on these
rows (one sample per prompt, `noise_floor()` needs `samples`); raw, unfitted ECE is 0.25-0.32.
- What this means: ship the router with a fitted `TemperatureScaler`/`IsotonicCalibrator` (see
`calibration_report()["held_out"]`), and use multi-sample suites (RouterBench presets) to beat the
task table by more than the noise floor - that measurement is tracked under v1.0 leaderboard runs.
**Re-measurement (0.5.x SLM recipe, leave-one-source-out).** The routing SLM now trains on ten public
sources, so each suite below was held out in turn and the SLM trained on the other nine (same command,
same targets, `--limit 1000`). The recipe change that matters for mixed catalogues - a corpus of model
battles no longer pushes down tools, skills and agents it never compared, and the strategy abstains on
them - takes `examples/eval_dataset.jsonl` *with* the SLM from 0.43 to 0.80 (0.83 without it; the old
recipe made any SLM harmful there). On the tier suites the numbers are unchanged within noise: PPE 0.561,
MT-Bench 0.508, WebDev Arena 0.506 (declarative 0.42 / 0.42 / -, static table 0.651 / 0.541 / 0.670);
held-out isotonic ECE 0.03 / 0.07 / 0.07. The conclusion stands: learned signals beat the declarative
router by +9 to +14 pp, and a pairwise battle remains too weak a label to beat "always the best tier".
---
## v0.5: Target representations, effort and personalisation (Complete)
**Goal:** learn what a target is good at from its own outcomes, so `examples` and `quality_prior` are
derived, not hand-written, and widen "target" to cover *how hard the model should think* and *for whom*.
### 0.5a Representations
| Item | Status | Notes |
|---|---|---|
| Target embeddings from outcome logs | Shipped | `learning.target_embedding()`: centroid of description, capability lexicon and examples; `nearest_targets()` routes to **unseen** targets via neighbours (UniRoute / EmbedLLM) |
| Offline reward-matrix warm start | Shipped | `learning.warm_start_from_matrix(rows, strategies, weight=)`: replays a full-information `(prompt, {target: reward})` matrix into every learner before going online (LinUCB ridge per arm, IRT ability, Thompson posteriors, task table, Markov rewards); `weight < 1` shrinks the offline evidence toward the prior (OrcaRouter 2605.30736) |
| Automatic `examples` mining | Shipped | `learning.ExampleMiner` promotes high-quality outcomes into `target.examples`, de-duplicated by cosine |
| Cold-start via IRT warm-up | Shipped | `learning.warm_start()` copies shrunk IRT ability, Thompson posteriors and Bradley-Terry strengths from neighbours (IRT-Router) |
| Target similarity to fallback ordering | Shipped | `learning.SimilarityFallback` |
| Query-response mixed representation | Shipped | `signals.DraftResponseSignal`: hedges, self-corrections, query overlap and length ratio of a cheap draft (`context["draft"]` or a drafter callable) become `extra["draft_uncertainty"]` and lift `complexity` (JiSi 2601.01330) |
| Hidden-state Dirichlet router | Shipped | `math.DirichletProbe` (evidential Dirichlet head, digamma loss, KL regulariser, pure Python SGD) and `strategies.HiddenStateStrategy(state_fn, targets, dim)`; epistemic uncertainty `K/S` becomes the strategy confidence so an unfamiliar state defers to the other strategies (ProbeDirichlet, RouterXBench 2602.11877) |
### 0.5b Effort as a routable dimension
| Item | Status | Notes |
|---|---|---|
| `Target.effort` variants | Shipped | `RouteTarget.effort` in `{none, low, medium, high}` with `RouteTarget.family` grouping siblings for breaker / budget purposes |
| Think-vs-non-think decision | Shipped | `EffortStrategy` / `decide_effort()` match `ReasoningNeedSignal` to effort level (Think When Needed 2601.18146; When to Think Deeply 2606.06745) |
| Token-budget-aware routing to elastic models | Shipped | `strategies.expand_elastic(parent, [BudgetVariant(...)])` turns one elastic model into `@` siblings sharing `family` and handler; `TokenBudgetStrategy` scores the fit between `token_budget` and the tokens the answer needs (truncation risk vs unused capacity) (Nemotron Elastic 2511.16664, Star 2605.07182) |
### 0.5c Personalisation
| Item | Status | Notes |
|---|---|---|
| `RouteRequest.profile` | Shipped | opaque user / tenant profile features; `ProfileSignal` hashes them into the LinUCB context |
| Few-shot user adaptation | Shipped | `learning.UserAdaptiveStrategy`: per-user x target Beta posteriors shrunk toward neighbour users (cosine over hashed profile vectors) and the global posterior, `w = n / (n + kappa)`; `observe_user()` warm-starts from logged interactions; `state()/load()` (GMTRouter 2511.08590) |
| Counterfactual personalisation test | Shipped | `eval.robustness.profile_swap_fairness()` measures how often the decision changes with the profile (SkillFeed methodology 2608.28241) |
**Exit criterion:** adding a new target with *only* `id`, `kind` and `cost` reaches >= 90 % of its
hand-configured routing accuracy after 200 outcomes; effort routing reduces tokens >= 30 % at equal
quality on a reasoning benchmark. **First half met:** `eval.criteria.cold_start_ratio()` strips the
`legal` expert down to `id`/`kind`/`cost` next to seven configured experts and a generalist, streams
mixed traffic through the real `Router` with `explore_rate=0.1`, and measures 1.000 x the
hand-configured accuracy after 200 outcomes for the newcomer (0.000 before learning; 1 643 requests,
180 of them exploratory). Making this pass required cold-start exploration in the router
(`Router(explore_rate=, explore_min_samples=)`), because a target with no description or examples is
never ranked first and so never earns an outcome. Reproduce with `python scripts/exit_criteria.py`.
**Effort-token half met** in `eval.criteria.effort_token_savings()`: one reasoning model exposed through
`expand_elastic()` as `reasoner@fast` (`effort="low"`) and `reasoner@think` (`effort="high"`), 2 000
rows carrying per-sibling `scores` *and* `tokens` (60 % plain lookups / rewrites where both siblings
score 0.90, 40 % proof / analysis prompts where the fast sibling drops to 0.50 - the pattern Think When
Needed and When to Think Deeply report for live thinking / non-thinking pairs). The real `Router` with
`default_strategies() + EffortStrategy() + TokenBudgetStrategy()` sends 59 % of the rows to the fast
sibling and spends **61.6 %** of the always-think tokens (1 234 vs 2 003 per row; oracle 1 231) at
quality 0.900 vs 0.900. `EvalRow.tokens` is the new dataset field and `osr eval DATASET --effort` runs the
same comparison on rows collected from live thinking / non-thinking pairs; the synthetic figure is the
roadmap claim until a live pair is published from the hosted platform.
---
## v0.6: Multi-step, multi-turn and agentic routing (Complete)
**Goal:** treat routing as a sequence, not a single pick; align the unit of learning with the unit of
supervision (the task).
### 0.6a Data model
| Item | Status | Notes |
|---|---|---|
| `Outcome.task_id`, `Outcome.step`, `Outcome.role` | Shipped | populated by `Router.run()`; `learning.TaskCredit` joins a terminal task-level reward to every call in the trajectory (TRACE-Router 2607.22465) |
| Delayed-feedback bandit updates | Shipped | `TaskCredit` buffers outcomes per task and credits on completion with uniform / discounted / last / blended schemes (Joulani et al. 2013) |
| Session history in `RouteRequest.history` | Shipped | `HistorySignal` turn-level features; `learning.HistoryTargetStrategy` scores each target by a logistic model over the history-target joint embedding `h * e_t` (recency-weighted hashed history, catalogue target embedding, shared weights + per-target bias, online SGD), with an incumbent bonus that flips to a penalty when `context["last_failed"]` is set; `state()/load()/merge()` (MTRouter 2604.23530) |
### 0.6b Strategies
| Item | Status | Notes |
|---|---|---|
| Admission-time task routing | Shipped | `learning.TaskPins` pins a task to its first target and releases on failure or after `max_steps` (TRACE-Router; MTRouter) |
| Cascade as MDP with a *stop* action | Shipped | `CascadePlanner(mode="mdp")`: finite-horizon expected value over candidate orderings (RLCascadeRouter 2608.15817; Dekoninck 2024). `CascadeResult.outcomes()` / `Router.learn_cascade()` turn every executed step into a per-step `Outcome` (rejected = failure with the gate score, accepted = success) that every learner consumes |
| POMDP self-verification cascade (AutoMix) | Shipped | `CascadePlanner(mode="pomdp")` with `BeliefTracker` |
| Bayesian self-escalation | Shipped | `strategies.SelfEscalation` keeps a Beta competence posterior updated per streamed chunk (hedges, refusals, self-corrections, repetition, overrun) and stops when `loss(stop) < loss(continue)`; `wrap_stream(chunks, monitor)` cuts any text iterator at the escalation point; `SelfEscalation.for_target()` seeds the prior from `quality_prior` (2608.24087) |
| Permanent-handoff policy from censored teacher signals | Shipped | `learning.MixtureCureModel` (Weibull mixture-cure on cumulative risk, grid MLE with censoring) and `learning.HandoffPolicy(threshold, hazard_threshold, horizon_steps, fallback_target, pins)`: hands a task over once and releases its `TaskPins` pin (TACIT-Switch 2608.27911) |
| Progress-guided step routing | Shipped | `ProgressRouter`: step index, failures, budget spent and last target condition each step's route (ProgRouter 2608.25992; 2511.06190) |
| Routing / aggregation switch | Shipped | `strategies.MixtureOfAgents(k, confidence_below, budget_usd, aggregator)`: calls the top-k alternatives when the decision is unsure and the budget allows, aggregates by semantic majority vote (`majority_vote`) or a supplied synthesiser, and emits one `Outcome` per participant (JiSi 2601.01330; Mixture-of-Agents) |
| Multi-round executor (Router-R1) | Shipped | `MultiRoundExecutor`: route, execute, judge, refine loop with bounded rounds |
| Plan execution | Shipped | `Router.run()` / `execution.py`: skill pre-processes, persona and skill `instructions` set the system prompt, primary answers; per-participant outcomes; `slot_quality_floor` |
| Conversation-state MDP from real sessions | Shipped | `MarkovStrategy(state_from="auto")` keys transitions and rewards on the learned task type (ontology leaf from the task-type classifier) joined with the domain, `"task_type"` / `"domain"` force one axis; per-request state memory so the outcome credits the state that was scored; `decay=` forgets stale sessions |
| Collaboration-protocol selection | Shipped | `strategies.ProtocolPolicy`: `failure_risk()` from decision confidence, signals and response uncertainty; an ordered `ProtocolRule` table (risk, budget, task type) picks `single / cascade / aggregate / debate / handoff`; per-protocol ledger reports which protocol actually paid (2608.14927) |
**Exit criterion:** on tau-bench style agentic tasks, task-level routing beats the best single target on
the accuracy-latency frontier; on multi-hop QA the executor beats the best single target at <= 60 % of
its cost, reproducibly via `osr eval --multi-round`; cascade outcomes are consumed by every learner.
**Multi-round half met** on the synthetic scored dataset in `eval.criteria.multi_round_vs_best_single()`
(1 000 rows, small / medium / large targets with per-row scores): `MultiRoundExecutor` with
`failures_before_switch=1` reaches quality 0.911 (best single target `large`: 0.900, oracle 0.912) at
**49.8 %** of the best single target's cost, 1.8 rounds on average. `osr eval DATASET --multi-round`
runs the same comparison on any dataset whose rows carry `scores`. Cascade outcomes are consumed by
every learner (`Router.learn_cascade()`, tested).
**tau-bench half met** in `eval.agentic.task_routing_frontier()`: 1 000 synthetic tasks of 3-5 steps
(customer support, coding, travel, finance; 30 % reasoning-heavy steps) with a per-step success /
latency table for three agents - `fast-agent` (400 ms, 0.97 on plain steps, 0.45 on hard ones),
`balanced-agent` (900 ms, 0.96 / 0.72) and `strong-agent` (2 000 ms, 0.94 / 0.90) - so no single agent
dominates per step, the shape tau-bench reports across model tiers. Each agent is a `CallableHarness`
behind `harness_handler()`; the real `ProgressRouter` (escalate on failure, switch after two) routes every
step with `EffortStrategy` and the auto-learning quartet, `TaskCredit` credits the task result back, and
both policies get one retry per step on common random numbers. Routed accuracy **0.987** vs 0.977 for the
best single agent (`strong-agent`) at **45.4 %** of its latency (3.9 s vs 8.5 s per task; 67 % of steps
go to `fast-agent`, 33 % to `strong-agent`). `AgentTask` / `load_agentic_tasks()` define the JSONL
format and `osr eval TASKS.jsonl --agentic` runs the same comparison on a table collected from live
harnesses; the synthetic frontier is the roadmap claim until a live one is published.
---
## v0.7: Catalogue interop and discovery at scale (Complete)
**Goal:** stop hand-writing `targets.yaml` for things that already describe themselves, and keep routing
accurate when the catalogue has thousands of entries.
### 0.7a Import
| Item | Status | Notes |
|---|---|---|
| MCP tool catalogue import to `TargetKind.TOOL` | Shipped | `adapters.mcp.tools_from_mcp()` (offline `tools/list`), `connect_mcp()` (stdio JSON-RPC), `enrich_description()` at import time (Enrich-Retrieve-Rank 2608.22695) |
| MCP *server* recommendation | Shipped | `adapters.recommend_servers(task, servers, k, constraints, allowed_auth)`: BM25 + hashing-cosine fusion over `ServerCard` text (name, description, tool names, tags) with hard filters on latency, region, data boundary and auth kind; rationale names the matched tools (Task2MCP / T2MRec 2604.17234; ComplexMCP 2605.10787) |
| A2A agent cards to `TargetKind.AGENT` | Shipped | `adapters.a2a.fetch_agent_card()`, `agent_from_card()`, `skills_from_card()`, `a2a_handler()` |
| Agent-harness adapters to `TargetKind.AGENT` | Shipped | `CallableHarness`, `HTTPHarness`, `SubprocessHarness` with a common `HarnessResult` and `task_id`-carrying outcomes (2607.11399; SWE-Router 2607.00053) |
| Skill packages to `TargetKind.SKILL` | Shipped | `load_skills()` reads agentskills.io `SKILL.md` folders |
| Persona catalogue import | Shipped | `adapters.personas.load_personas()` from Markdown, JSON, CSV and Copilot agent / chatmode files; persona choice learned from plan-level outcomes (MasRouter) |
| OpenAI-compatible **proxy mode** | Shipped | `POST /v1/chat/completions` with `model: "auto"` routes, executes and learns; `GET /v1/models` |
| LangGraph / Microsoft Agent Framework node | Shipped | `adapters.frameworks.langgraph_node()`, `langgraph_condition()`, `maf_router_executor()` |
| vLLM Semantic Router / gateway parity | Shipped | `adapters.load_semantic_router_config(path_or_dict)` imports a semantic-router `config.yaml` (categories, `model_scores`, `use_reasoning`, `model_config.pricing`, `system_prompt`) as `RouteTarget`s plus category `Rule`s, so an existing gateway config routes through OpenSmartRoute unchanged (2603.04444) |
### 0.7b Two-stage selection
| Item | Status | Notes |
|---|---|---|
| Retrieve-then-rank narrowing | Shipped | `retrieval.Retriever`: BM25 + hashing-dense retrieval with reciprocal-rank fusion, on above `narrow_above` (default 500) targets (Enrich-Retrieve-Rank; SCOUT 2608.23992) |
| `tool_search` / `execute_tool` meta-tools | Shipped | `retrieval.tool_search_target()` and `execute_tool_target()` expose the router as two MCP tools (SCOUT) |
| Field-aware / schema-aware tool matching | Shipped | `discovery.schema_match(request, target)` extracts typed entities (emails, URLs, dates, times, paths, ids, numbers, currency, quoted strings) and scores coverage of the tool's required `input_schema` properties; `discovery.SchemaAwareStrategy` fuses it with the other strategies (SchemaRouter 2608.21375) |
| Cache-preserving tool routing | Shipped | `discovery.CachePreservingSelector(prefix_size, evict_after)` keeps a per-session ordered tool prefix byte-stable across turns, appends new tools and evicts only after inactivity; `prefix_hit_ratio()` reports cache stability (CacheRouter 2608.22708) |
| Submodular skill-set selection | Shipped | `retrieval.select_skill_set()`: greedy benefit minus redundancy under a token budget (Best Prefix Selection 2608.19993) |
| Profile-conditioned skill routing | Shipped | `learning.SkillAffinity`: Beta posterior per (profile bucket, skill); `relevance(profile, skills)` plugs into `select_skill_set(relevance=)` (SkillFeed 2608.28241) |
| Skill graphs / composition | Shipped | `discovery.SkillGraph`: `requires` / `conflicts` / `composes` edges from `SKILL.md` frontmatter (`osr-requires`, `osr-conflicts`, `osr-composes`) plus learned co-usage; `compose(seeds)` closes under dependencies, resolves conflicts by priority and returns a dependency order (2606.18051; CaSKG 2608.25500) |
### 0.7c Shared state
| Item | Status | Notes |
|---|---|---|
| Redis / Postgres `StateStore` | Shipped | `enterprise.stores.RedisStateStore`, `SQLStateStore` (DB-API 2.0), `BatchedStateStore` for write coalescing, `NamespacedStateStore` |
| State schema version and migration | Shipped | `VersionedStateStore` stamps `{"_schema": n}` and runs forward migrations; refuses to load newer state |
**Exit criterion:** a fresh install routes to an MCP server's tools with no YAML written; with 5 000
synthetic tools Match@1 stays within 5 points of the 50-tool figure. **Met.**
`eval.criteria.match_at_1_at_scale()` builds 50 and 5 000 verb-object-qualifier tools
(`synthetic_tool_catalogue()`), routes 200 paraphrased prompts through `RouterBuilder.with_retrieval()`
and measures Match@1 1.000 -> 1.000 (drop 0.000).
---
## v0.8: Operations, risk control and economics (Complete)
**Goal:** make the learners safe to leave running: bounded risk, bounded budgets, honest under drift,
measurable without live traffic.
### 0.8a Risk control and abstention
| Item | Status | Notes |
|---|---|---|
| Conformal escalation thresholds | Shipped | `math.calibration.ConformalCalibrator` (split conformal) behind `RouterBuilder.with_calibration()` (RouteNLP 2604.23577) |
| Set-valued routing with abstention | Shipped | `RouteDecision.candidate_set` = smallest nested set with mis-routing risk <= alpha (RACER 2603.06616; CR2 2605.12001) |
| Learning-to-defer for `TargetKind.HUMAN` | Shipped | `DeferStrategy` (Madras 2018; Mozannar and Sontag 2020; Verma 2023) |
| Uncertainty features from the response | Shipped | `signals.semantic_entropy()` / `response_uncertainty()` (meaning clusters over sampled answers, hedging, optional P(True) judge); `signals.UncertaintyGate` is a drop-in `Cascade` quality gate and `MixtureOfAgents` trigger; `signals.EventTrigger` fires named actions from threshold rules (Kuhn 2023, Kadavath 2022; 2607.13048) |
### 0.8b Non-stationarity and budgets
| Item | Status | Notes |
|---|---|---|
| Forgetting in every learner | Shipped | `decay < 1` for `ThompsonBeta`, `IRTModel`, `BradleyTerry`, `MarkovChain` (row counts) and `RoutingMDP` (exponentially weighted rewards); `discount < 1` for `LinUCB` with an undiscounted ridge floor (D-UCB, Garivier and Moulines 2011); `MarkovStrategy(decay=)` wires both Markov knobs |
| Multi-knapsack `CostAwareBandit` | Shipped | `math.bandits.MultiKnapsackBandit`: several resources (USD, tokens, GPU-seconds, energy), one shadow price per resource by dual ascent, hard sliding-window meters checked **before** commitment, learned optimistic costs, pessimistic rewards, bounded `audit` stream of every pruned arm (Drift-Aware Sparse Routing 2609.00662; Badanidiyuru 2013) |
| Tenant fairness under shared budgets | Shipped | `enterprise.ops.FairShareMiddleware`: dominant-resource fairness over a sliding window |
| Numerically stable LinUCB | Shipped | Sherman-Morrison rank-1 updates; surfaced posterior variance |
### 0.8c Latency, hardware and energy
| Item | Status | Notes |
|---|---|---|
| Queue-aware latency estimate | Shipped | `enterprise.ops.InflightTracker` + `QueueAwareStrategy` (Erlang-C / Kingman) (Latency-aware routing 2607.18253) |
| Energy / carbon as cost dimensions | Shipped | `RouteTarget.cost["wh_per_1k_tokens"]` / `["gco2_per_1k_tokens"]` (or `gco2_per_wh` grid intensity) read by `unit_energy` / `unit_carbon`; `Objective(energy=, carbon=)` weights them in the utility, log-normalised like money; `MultiKnapsackBandit` meters them as resources |
| Hardware-aware cost | Shipped | `math.EnergyModel` fits `Wh = e0 + e_in * prompt + e_out * output` per target by ridge regression from measured samples and converts to gCO2 with a grid factor; `math.HardwareProfile` / `hardware_profile()` give priors for A100, H100, L4, RTX 4090, CPU and NPU when no meter exists; feeds `wh_per_1k_tokens` / `gco2_per_1k_tokens` (2608.28044; HW-Router 2608.14575) |
| Edge-cloud token-aware routing | Shipped | `strategies.EdgeCloudStrategy`: targets tagged `metadata["tier"] = edge | cloud`; edge success is a Thompson posterior per complexity bucket, penalised by decode time against the latency SLO; cloud is penalised by upload time and excluded when the request's data boundary forbids it (Pro-Router 2608.28726, RelayLLM 2601.05167) |
### 0.8d Offline and online evaluation
| Item | Status | Notes |
|---|---|---|
| Off-policy evaluation | Shipped | `eval.ope`: IPS / SNIPS / doubly-robust with weight clipping and effective sample size; `osr ope` (Dudik et al. 2011); `RouteDecision.propensities` logs the softmax routing policy so `LoggedDecision.from_decision` needs no extra plumbing |
| Shadow mode and A/B harness | Shipped | `enterprise.ops.ShadowMiddleware` and `ABTest` with Wald's SPRT for promotion |
| Federated learner merge | Shipped | `merge()` on `ThompsonBeta`, `LinUCB`, `IRTModel`, `BradleyTerry`, `MarkovChain` / `RoutingMDP`, `BanditStrategy` and `TaskTableStrategy` combine sufficient statistics without sharing prompts; `learning.merge_learners(local, remote)` pairs same-named strategies across replicas ("Federate the Router" 2601.22318). `AutoLearner.refresh()` is the writer / reader pattern for replicas that share one `StateStore` |
| Market / auction strategy | Shipped | `strategies.AuctionStrategy`: targets bid `(claimed success, price)`; claims are corrected by each bidder's observed bias and realised rate, the highest corrected surplus wins and pays the second price (EA-RAM 2608.12719) |
**Exit criterion:** conformal thresholds hit their nominal mis-routing rate +/- 2 pp on held-out data;
the knapsack bandit never exceeds a hard budget in a 10^6-step simulation with drift; IPS estimates of a
held-out strategy are within the bootstrap CI of its live result. **Met** (`python scripts/exit_criteria.py --full`):
- `conformal_coverage()`: `ConformalCalibrator(alpha=0.1)` fitted on 1 000 router propensities over
graded mixed-domain prompts covers 0.909 of 1 000 held-out labels (gap 0.9 pp, mean set size 1.2).
- `knapsack_never_exceeds_cap()`: `MultiKnapsackBandit(on_capped="abstain")` over 10^6 steps with reward
means reshuffled every 10^5 steps; peak sliding-window spend / cap = 0.9998 (USD) and 0.9996 (tokens),
zero abstentions, mean reward 0.75. Getting there fixed two real defects: the "release the cheapest
arm" fallback could breach the cap (peak 1.0165), and shadow prices exploded for token-scale budgets.
- `ope_within_live_ci()`: 2 000 decisions logged under a cost-heavy softmax policy; IPS 0.812, SNIPS
0.816 and DR 0.816 all fall inside the live value's 95 % bootstrap CI [0.798, 0.832] (live 0.815,
n_eff 992).
---
## v0.9: Security hardening of the control plane (Complete)
**Goal:** close the gaps listed in [SECURITY.md](https://opensmartroute.ai/docs/SECURITY.md) that heuristics cannot.
| Item | Status | Notes |
|---|---|---|
| Learned gadget / confounder detector | Shipped | `security.gadget.GadgetDetector`; `InputGuard(learned=True)`; trained via `osr train --gadget` (Rerouting LLM Routers) |
| Origin policy for tool parameters | Shipped | `security.provenance.OriginPolicy`: sensitive parameters of state-changing tools must originate from the user turn (ROPE 2608.27496) |
| Signed MCP manifests | Shipped | `adapters.mcp.sign_manifest()` / `verify_manifest()` (HMAC-SHA256 or Ed25519); `osr mcp-manifest` (2601.23132) |
| Resource-amplification limits | Shipped | `security.limits.ResourceLimiter`: per-task caps on steps, tool calls, depth, tokens, cost and wall-clock (Beyond Max Tokens 2601.10955) |
| Safety-routing regression suite | Shipped | `security.safety.run_safety_suite()`; `osr safety --learned-guard` gates CI (When Safety Routing Breaks 2609.01455) |
| Encrypted state at rest | Shipped | `enterprise.stores.EncryptedStateStore` (AES-256-GCM, key rotation) |
| External review pack | Complete | [SECURITY_REVIEW.md](https://opensmartroute.ai/docs/SECURITY_REVIEW.md): scope, trust boundaries, evidence table with reproduction commands, reviewer questions, known gaps, review log |
**Exit criterion:** red-team suite passes in CI (met); published threat-model delta (met:
[SECURITY.md, "Threat-model delta: 0.3 -> 0.4"](https://opensmartroute.ai/docs/SECURITY.md#threat-model-delta-03---04)); a published
review pack that lets an independent party run the review without a briefing (met:
[SECURITY_REVIEW.md](https://opensmartroute.ai/docs/SECURITY_REVIEW.md)).
The original wording also required the independent report itself. That cannot be produced from inside
the repository, so it is now the adoption item it really is: tracked in the v1.0 readiness table below
and closed when a third-party report is added to the review log in `SECURITY_REVIEW.md`.
---
## v1.0: Stable API (Complete)
**Goal:** freeze the public surface.
- All `opensmartroute.*` public symbols frozen; the snapshot test (`tests/public_api.json`) already
fails on any added or removed name. Deprecation policy: **shipped** in
[CONTRIBUTING.md, "Public API and deprecation policy"](https://opensmartroute.ai/docs/contributing.md#public-api-and-deprecation-policy)
with `errors.deprecated()` / `OpenSmartRouteDeprecationWarning` as the mechanism.
- Documented performance envelope: **shipped** in [ARCHITECTURE.md](https://opensmartroute.ai/docs/ARCHITECTURE.md#performance-envelope)
(p50 / p95 / p99 for 16 / 64 / 256 / 1024 targets, with and without two-stage narrowing;
`python scripts/bench.py --scale` reproduces it).
- Security review of the control plane against `docs/SECURITY.md` threat model: the review pack is
**shipped** ([SECURITY_REVIEW.md](https://opensmartroute.ai/docs/SECURITY_REVIEW.md)); the independent report is an adoption item
(table below).
- Reference deployments: **shipped** as library (`pip install`), sidecar (`deploy/Dockerfile`, `osr serve`)
and central control plane (`deploy/helm/opensmartroute`; hardened pod security, HPA, PDB, NetworkPolicy,
ConfigMap-driven catalogue) - see [deploy/README.md](https://opensmartroute.ai/docs/deploy.md); **hosted platform** on
Azure Container Apps with Azure OpenAI (`platform/`, `infra/`, `azd up`; community and enterprise
editions, API keys, plans, metering, tenants, audit) - see [platform/README.md](https://github.com/isathish/OpenSmartRoute/blob/main/platform/README.md).
- Public leaderboard runs with reproducible configs: **shipped** -
[examples/leaderboard/results](https://opensmartroute.ai/docs/leaderboard-results.md) holds the 0.5.0 run
(learned router vs declarative, static task table and baselines on MT-Bench human, PPE human and
WebDev Arena, with robustness and held-out calibration; suite-level hold-out, seed 0, SLM SHA-256
recorded) produced by the recipe in [examples/leaderboard](https://opensmartroute.ai/docs/leaderboard.md). Listing
on RouterArena, LLMRouterBench and xRouteBench is a submission to those projects and is tracked below.
**Exit criterion (settled with 1.0.0).** 1.0 means what the repository can prove: a frozen public API with a
deprecation path, a published performance envelope, reference deployments, the security controls with a
red-team suite in CI and a review pack, a published leaderboard run, and two consecutive releases without a
breaking change. Adoption evidence - an independent security report, accepted leaderboard listings, two
production users - needs a third party, so it is tracked and reported but does not gate the version.
Nothing is typed by hand: `python scripts/release.py readiness` computes the table below from the
repository (snapshot and tests present, envelope section, deployment files, review pack and CI safety
suite, published run, non-breaking release steps in `CHANGELOG.md` verified against the tagged
`tests/public_api.json`, review-log rows in `SECURITY_REVIEW.md`, `accepted` rows under *Listings* in
the results page, rows in [ADOPTERS.md](https://github.com/isathish/OpenSmartRoute/blob/main/ADOPTERS.md)); `release.py check` refuses any `1.x` version
while a **release** row is open, and the *Release* workflow prints the whole table in its summary.
| Readiness item | Kind | How it is verified | Status |
|---|---|---|---|
| Public API frozen with a deprecation path | release | `tests/test_public_api.py` fails on any removed or renamed name; `errors.deprecated()` | Met |
| Performance envelope published | release | [ARCHITECTURE.md](https://opensmartroute.ai/docs/ARCHITECTURE.md#performance-envelope), `scripts/bench.py --scale` | Met |
| Reference deployments (library, sidecar, control plane, hosted) | release | `deploy/`, `platform/`, live at opensmartroute.ai | Met |
| Security controls, red-team suite in CI, threat-model delta, review pack | release | v0.9 above | Met |
| Leaderboard run published with reproducible config | release | [examples/leaderboard/results](https://opensmartroute.ai/docs/leaderboard-results.md) | Met |
| Two consecutive releases with no breaking change | release | 0.4.0 -> 0.5.0 and 0.5.0 -> 1.0.0, additions only against the tagged `tests/public_api.json` | Met |
| Independent security report | adoption | A row in [SECURITY_REVIEW.md, "Review log"](https://opensmartroute.ai/docs/SECURITY_REVIEW.md#7-review-log) | Open |
| Leaderboard listings accepted | adoption | An `accepted` row under [results, "Listings"](https://opensmartroute.ai/docs/leaderboard-results.md#listings) linking the published run | Open |
| Two independent production users | adoption | Two rows in [ADOPTERS.md](https://github.com/isathish/OpenSmartRoute/blob/main/ADOPTERS.md) (named, or anonymised with consent) | 0 of 2 |
What closes each adoption row:
- **Security report** - a third party runs [SECURITY_REVIEW.md](https://opensmartroute.ai/docs/SECURITY_REVIEW.md) and adds a row to
its review log with the report link; findings are fixed or accepted first.
- **Listings** - submit the 0.5.0 run (commit, SLM SHA-256, results page) to RouterArena,
LLMRouterBench and xRouteBench; record the entry URL and `accepted` in the results page.
- **Production users** - two organisations agree to be listed in `ADOPTERS.md`.
After 1.0 the public API changes only through the deprecation policy: a removal needs a
`deprecated()` warning for at least one minor and a `### Removed` entry, which the readiness check
reads as a breaking step and the snapshot test refuses without an explicit snapshot update.
---
## Research track
Every question below now has a runnable answer in the tree; the table records where, so the
question can be re-asked against a real catalogue and dataset rather than argued about.
| Question | Answer in the code | Related work |
|---|---|---|
| Is the remaining router-to-oracle gap real or label noise on *our* pools? | `eval.headroom.routing_headroom(rows, targets)`: oracle vs best single target, split into the label-noise floor (`noise_floor`) and the measurable, recoverable headroom | 2607.03436; LLMRouterBench 2601.07206 |
| Does a static task table already capture most of the value for typical enterprise catalogues? | `eval.headroom.learnability_by_difficulty` buckets rows by oracle-minus-best-single gap; `TaskTableStrategy` is a mandatory `osr eval --baselines` row, so the table's share of the headroom is reported per run | 2608.23023 |
| How few, and how different, must targets be before routing beats the best single model? | `eval.headroom.target_diversity` (mean pairwise disagreement, winner entropy / share) and `min_catalogue(rows, targets, fraction)` (smallest subset keeping 95 % of the oracle) | 2607.09197 |
| Contrastive router training on (query, target) pairs | `learning.ContrastiveRouter` (softmax cross-entropy over `q·e_t / τ` against the *acceptable set* of a row, `acceptable_set(row, slack)`) and `ContrastiveStrategy` with online updates from outcomes | RouterDC (NeurIPS 2024); RouterDC-style objectives in LLMRouter |
| Reward-model-distilled routing labels | `learning.soft_labels(scores, temperature)` and `ContrastiveRouter.fit(objective="distilled")`: train against the softened reward distribution instead of one-hot labels | Zooter |
| Can the whole ensemble be compressed into one small routing model? | `learning.RouterSLM` (hashed dual encoder + temperature calibration + target snapshots in one JSON file) and `distill_router(router, texts)`, which trains it on the router's own propensities; `SLMStrategy` puts the student back in the ensemble and keeps learning online; `osr slm train/eval/predict` | RouteLLM matrix-factorisation router; Zooter; RouterDC |
| Does a router keep improving on its own from live data, without regressing? | `learning.SelfImprover`: champion/challenger cycle over feedback rows (`eval.rows_from_feedback`), Hub datasets (`eval.DatasetCollector`, RouterBench / RouterEval) and synthetic rows; challenger is calibrated on the holdout and promoted only above `OSR_SLM_MIN_GAIN`; JSONL history; `osr improve` | RouterBench 2403.12031; RouterEval 2503.10657 |
| Where do current prices and quality priors come from at scale? | `adapters.ModelCatalogue` (OpenRouter prices, Hugging Face `model-index` benchmarks and popularity → `quality_prior`, Pareto `frontier()`, `cost_benchmark()`) and `adapters.WebKnowledge` (Hugging Face / DuckDuckGo / Brave discovery, risk-scored page text); `osr catalogue` | LLMRouterBench 2601.07206 cost-quality frontier |
| Model-level scaling: does accuracy keep rising with more targets? | `eval.headroom.scaling_curve(rows, targets, sizes)`: oracle / best-single / headroom for random sub-catalogues of each size, exposing diminishing returns | RouterEval; LLMRouterBench diminishing returns |
| Intra-generation escalation vs pre-generation routing | `strategies.SelfEscalation` + `wrap_stream` (stop mid-stream when `loss(stop) < loss(continue)`); compare against `Cascade` on the same dataset | 2608.24087 |
| Routing as an RL problem end-to-end (Router-R1, RLCascadeRouter) vs predict-then-optimise | `learning.PolicyGradientStrategy` (REINFORCE on hashed request features, EWMA baseline, entropy bonus; reward is the *decision* utility `decision_reward`) and `learning.decision_regret(rows, targets, choose, predict)` which reports decision regret next to prediction MAE so the two losses can be compared directly | 2608.15817 |
| Is routing learnable where it is most valuable (web / SWE agents)? | `eval.headroom.learnability_by_difficulty(rows, targets, buckets)`: per-difficulty-bucket oracle, best single, headroom and noise floor; the hardest bucket answers the question for a given pool | 2608.06171; SWE-Router 2607.00053; Agent-as-a-Router 2606.22902 |
| Memory-tier routing for agents | `strategies.MemoryRouter`: tiers with capacity, write/read cost and latency; learned `ImportanceGate` decides what to write, value decays per tick, lowest-value residents are demoted, `recall(query, budget_tokens)` fills a token budget by relevance | BudgetMem 2602.06025; Gated-Memory Routing 2609.00237 |
| Multimodal / modality escalation | `strategies.ModalityStrategy` (penalises text-only targets when the request carries images / audio / video / files that only a text surrogate describes) and `ModalityEscalation` (text-first with a Beta posterior on "surrogate sufficed", escalates to the multimodal target when confidence is low) | LatentRouter 2605.11301; modality escalation 2607.05438; CUA routing 2603.12823 |
| Mechanism design when providers are strategic | `strategies.AuctionStrategy`: bias-corrected claims, second-price payment | EA-RAM 2608.12719; strategy auctions 2602.02751; coalition pricing 2608.07532 |
| Semantic caching as a target | `strategies.SemanticCache.target()` is a `DESTINATION` whose handler serves near-duplicate answers; `SemanticCacheStrategy` scores it from the hit similarity and learns per-band hit quality from outcomes | GPTCache; vLLM semantic router 2603.04444 |
| Routing among human annotators / experts | `strategies.AnnotatorPool` (Dawid-Skene EM over per-domain annotator accuracy), `select_quorum(domain, target_accuracy, budget)` (greedy majority-vote quorum via `quorum_accuracy`) and `HumanRoutingStrategy` for `HUMAN` targets | QUORUM 2608.27974 |
| Speculative / draft-based cascades | `strategies.SpeculativeCascade`: runs draft and strong in parallel when the learned acceptance posterior says speculation is cheaper than either alone (`expected_mode_costs`), cancels the strong call when the draft is accepted | speculative cascades; Differential Reasoning Router 2608.30224 |
Open follow-ups (not scheduled): learned energy/carbon priors per provider region, and replacing the
hashed encoders in `ContrastiveRouter` / `PolicyGradientStrategy` with a real embedding model behind
`Embedder` when a dependency is acceptable.
---
## Out of scope
- Model serving, quantisation, or inference optimisation (use vLLM / TGI).
- Protocol translation between provider APIs (use LiteLLM / aisix in front).
- Token-level / layer-level routing inside a model (MoE, early exit, hybrid attention).
- Prompt management / versioning.
- Being a chat UI.
---
# Contributing to OpenSmartRoute
Thanks for helping build an open, intelligent route to the right decision.
## Ground rules
- **Core stays dependency-free.** Anything needing torch, numpy, FastAPI, etc. goes behind an
optional extra or into `opensmartroute-contrib-*`.
- **Every strategy is inductive.** It must work for a target it has never seen, using only
the target's declared capabilities/examples.
- **Every decision is explainable.** New strategies return a `rationale` string.
- **Every learner is persistable.** Implement `state()` / `load()`.
- **No raw request text in logs.** Hash + length only.
## Workflow
```bash
python3.12 -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1
pip install -e '.[dev]'
ruff format src tests scripts
ruff check src tests scripts && mypy && bandit -q -r src -c pyproject.toml
pytest -q
osr -t examples/targets.yaml -r examples/rules.yaml eval examples/eval_dataset.jsonl --min-accuracy 0.85
osr -t examples/targets.yaml safety --learned-guard
python scripts/release.py check
```
CI runs the same commands on Linux (Python 3.10 to 3.13) and Windows (3.12), then builds the wheel,
sdist and container image. Pull requests must keep:
- eval accuracy >= 0.85 on `examples/eval_dataset.jsonl` (`--min-accuracy` exits non-zero otherwise);
- the safety-routing suite green (`osr safety --learned-guard`);
- the public API snapshot (`tests/public_api.json`) unchanged, or updated deliberately in the same PR
with a CHANGELOG entry;
- `pyproject.toml`, `opensmartroute.__version__` and the top CHANGELOG section in agreement
(`scripts/release.py check`);
- every exported name with a one-line summary (a docstring, or a same-line comment for constants)
and `docs/REFERENCE.md` regenerated (`python scripts/api_reference.py`; `tests/test_docs.py`
fails on a stale reference or an undocumented name; `... report` lists the gaps).
Every user-visible change gets a bullet under `## [Unreleased]` in `CHANGELOG.md`
(Keep a Changelog format: Added / Changed / Deprecated / Removed / Fixed / Security).
If you touch the hot path (`signals`, `policy`, `router`, any `Strategy.score`), run
`python scripts/bench.py` before and after and paste both tables in the PR.
If you touch brand assets, edit `scripts/brand_build.py` and run it with `--export`; never edit
the generated SVG/PNG files by hand.
## Public API and deprecation policy
The public surface is every name in the `__all__` of the packages listed in `tests/public_api.json`
plus the keyword arguments of `Router.__init__`. `tests/test_public_api.py` fails on any addition or
removal, so every change to that surface is a deliberate, reviewed diff of the snapshot.
- **Additions** are allowed in any minor release and get a CHANGELOG `Added` bullet.
- **Behaviour changes** to an existing public symbol need a `Changed` bullet; if the old behaviour
can be kept behind a parameter, keep it as the default for one minor.
- **Removals and renames** go through a deprecation: the old symbol keeps working for at least one
minor release, calls `opensmartroute.errors.deprecated(name, since=, removal=, replacement=)`
(which emits `OpenSmartRouteDeprecationWarning`, a `DeprecationWarning` subclass), is listed
under `Deprecated` in the CHANGELOG, and is deleted no earlier than the `removal` version named in
the warning, with a `Removed` bullet. Before 1.0 the minimum notice is one minor; from 1.0 on it is
two minors, and removals happen only in a major release.
- **State formats** (`state()` / `load()` payloads, `VersionedStateStore` schema) are versioned; a
newer library must load older state, and loading newer state raises rather than guessing.
- **Settings** (`OSR__`) follow the same rules as symbols; renamed keys are read under
both names for the deprecation window.
- `tests/public_api.json` is regenerated from the actual `__all__` lists, never edited by hand, and
the regenerating PR must show only additions unless it also carries the matching `Removed` entry.
## Adding a strategy
1. Subclass `opensmartroute.strategies.Strategy`, set `name`, implement `score()`.
2. If it learns, implement `update(outcome)` and, if it has a model, `model.state()/load()`.
3. Add a unit test with a deterministic seed.
4. Add a row to the strategy table in `README.md` and the idea-to-module map in `docs/RESEARCH.md`.
## Adding an adapter
Adapters live in `src/opensmartroute/adapters/`. Anything needing a third-party package must be
imported lazily inside the function/class and raise `ConfigurationError` naming the extra to install.
Test transports against a real in-process server (see `tests/test_adapters.py`), not by mocking
`urllib`.
## Adding math
Put it in `src/opensmartroute/math/`, cite the source in the docstring, include the formula,
and add a numerical test against a known value.
## Adding or editing an agent skill
Skills live in `.claude/skills//SKILL.md` and follow the Agent-Skills format the SDK loads
with `load_skills`. The directory name must equal `name`; every fact in the body must be
verifiable against the code (tests assert the packages load and route). Keep `osr-domains` to
ontology names (`opensmartroute.signals.ontology.DOMAIN_LEXICON` or `general`) and omit
`osr-actions` for broad skills. Validate with `osr skills` and run `tests/test_skills.py`; when a
skill's behaviour changes, update the matching request in `REPRESENTATIVE` too. Every skill is
published on the documentation site at `/docs/skills/`; no web change is needed.
## Editing documentation
The documentation site in `platform/web` (`/docs/`; see "Documentation site" in
`platform/README.md`) is for end users of the hosted platform, the Python SDK and the pip package. It
renders `docs/*.md` (except `docs/sales/`), the root `README`/`CHANGELOG`/`SECURITY`/this file,
`deploy/README.md`, `spec/ocm/README.md`, the skills, and a REST API reference generated from
`platform/api/openapi.json`. Internal documents (`docs/PLATFORM_PLAN.md`, `docs/GO_TO_MARKET.md`,
`docs/BRAND.md`, `docs/sales/`, `platform/README.md`) stay in the repository and are linked from
`README.md` only. Keep one H1 per file, use H2/H3 for structure (they become the table of contents and
the search index), give fenced code a language and use relative links to other Markdown files. A new
`docs/*.md` needs a link in `README.md` (`tests/test_docs.py`) and, when it is user-facing, an entry in
`platform/web/src/lib/docs/catalogue.ts`; run `npm run build` in `platform/web` to preview
(`npm run dev` for live reload). `docs/REFERENCE.md` is generated; never edit it by hand. After changing
a platform route or model, regenerate the OpenAPI snapshot with
`python platform/api/scripts/export_openapi.py` (CI runs `--check`). Documentation may only name things
that exist: `tests/test_docs_claims.py` verifies every `osr --flag` and every `OSR_*` variable
mentioned in the Markdown against the CLI parser and the code, and
`platform/api/tests/test_platform_docs_claims.py` verifies documented endpoints against the app's routes,
`/dashboard/` paths against the web app, the site catalogue against `docs/` and the CHANGELOG
sections (date, body, compare link). Coding agents should start from `AGENTS.md`.
## Commit style
Conventional Commits (`feat:`, `fix:`, `docs:`, `perf:`, `refactor:`, `test:`, `chore:`).
## Releasing
Releases are cut by GitHub Actions; nobody edits version numbers or tags by hand.
1. **Prepare.** Run the *Prepare release* workflow (Actions tab, `workflow_dispatch`) with a bump
(`major` / `minor` / `patch`) or an explicit version. It runs `scripts/release.py prepare`, which
bumps `pyproject.toml` and `__version__`, turns `## [Unreleased]` into a dated section and rewrites
the compare links, then opens a pull request labelled `release`.
2. **Review.** The PR diff is the release: version bump plus CHANGELOG. CI runs on it like any other PR.
3. **Merge.** Merging a `release`-labelled PR into `main` triggers the *Release* workflow, which
re-verifies version consistency, runs the test suite, builds the sdist and wheel (with build
provenance attestations), creates the annotated tag `vX.Y.Z` and the GitHub release with the
CHANGELOG section as notes, publishes to PyPI through trusted publishing (no API token) and pushes
`ghcr.io/isathish/opensmartroute:X.Y.Z` and `:latest`.
The *Release* workflow can also be dispatched manually on `main` when the tree is already at a
releasable version (used for the first release, or to retry a failed publish; it refuses to run if the
tag already exists).
One-time repository setup for maintainers:
- PyPI: add a trusted publisher for project `opensmartroute` (owner `isathish`, repository
`OpenSmartRoute`, workflow `release.yml`, environment `pypi`).
- GitHub: create the `pypi` environment (optionally with required reviewers); allow Actions to create
pull requests (Settings > Actions > General) so *Prepare release* can open the PR. Optionally add a
fine-grained `RELEASE_TOKEN` secret (contents and pull-requests write) so CI runs on the release PR
even though it was opened by automation.
- Tags for historical versions so compare links resolve: `git tag -a v0.3.0 -m "opensmartroute 0.3.0"`.
`scripts/release.py` has no dependencies beyond the standard library and is covered by
`tests/test_release_tooling.py`.
**1.0.0.** `python scripts/release.py readiness` evaluates the v1.0 readiness table in
[docs/ROADMAP.md](https://opensmartroute.ai/docs/ROADMAP.md) from the repository. *Release* rows (frozen API, performance envelope,
reference deployments, security review pack, published leaderboard run, two consecutive non-breaking
releases verified against the tagged `tests/public_api.json`) block any `1.x` version in `release.py check`;
*adoption* rows (an independent report row in `docs/SECURITY_REVIEW.md`, an `accepted` listing in
`examples/leaderboard/results/README.md`, two rows in `ADOPTERS.md`) are tracked and printed but never
block. The *Release* job prints the table in its summary on every run.
## Code of conduct
We follow the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).