Skip to content
OpenSmartRoute

Changelog

Release notes for every version.

CHANGELOG.md

All notable changes are documented here. Format follows Keep a Changelog; versions follow SemVer. Releases are cut by the Prepare release workflow (scripts/release.py), which turns the Unreleased section into a dated section.

Unreleased#

Added#

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-<name>, python -m osr_platform.services <name>, ports 8091-8101) on the API image (platform/api/osr_platform/services.py). The API is the gateway: with OSR_PLATFORM_<NAME>_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 <web>/auth/callback, and links the API generates (emails, invitations, device sign-in, Stripe return, alert hrefs) 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 <app>.internal.<domain> 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:<username> 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.<name> (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 <name>) behind the API gateway (OSR_PLATFORM_<NAME>_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/<page> 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/<slug>#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 <server> --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, <WEB_URL>/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/<vendor> 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 <platform>/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 <main id="main">.
  • 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/<name>, 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 EvalRows, 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 RouteTargets (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=<model> / 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 <name>.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:<variant> 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 <id>@<budget> 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_<GROUP>_<FIELD> 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:<task> plus family:skill:<fam>), 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-<ts> (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-<slug> 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 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_<GROUP>_<FIELD>) 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:<version>.

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.