Changelog
User-visible changes to SemanticRouter.NET: the NuGet packages, the semantic-router tool, the container image and the desktop app. The format follows Keep a Changelog, and versions follow Semantic Versioning; before 1.0, a minor version can change behaviour. Each pull request with a user-visible change adds its line under Unreleased (CONTRIBUTING.md).
Unreleased
Added
- A gateway benchmark:
dotnet run -c Release --project benchmarks/SemanticRouter.Benchmarks -- gatewaymeasures what the router adds in front of a stub backend (time to first byte, total time, throughput and memory at 1, 8 and 32 clients),--soakwatches for drift, and--faultschecks failover, cooldowns, stalls, dropped connections, cancelled requests and two routers on one database. CI runs the short version on every pull request and the soak and faults nightly. IRoutingActivityStore.PurgeWithResultandRoutingActivityLog.PurgeWithResultsay whether purged activity is also gone from the store's files (ActivityPurgeResult), and the store interfaces gainIRoutingActivityStore.IdsBetweenandIRoutingFeedbackStore.Remove, for deleting what refers to purged activity. All are default interface methods: existing stores keep working. (#241)- Dashboard: Look up prices on a connector's group on the Models page fills in its cloud models' input, output and prompt-cache prices (a subscription's API prices) from OpenRouter's public model list, and Discover models fills them in for the cloud models it suggests. Models are matched by their provider's own id at the price of the provider the connector reaches; a gateway on another host gets the model maker's price, marked to check. Nothing is applied until Apply & save.
POST api/prices/lookupis the endpoint; the list is fetched only on request, kept for six hours, refused in offline mode unlessopenrouter.aiis allowed, anddoctorlists it as an egress destination. - Dashboard: a subscription model has API cache read price and API cache write price fields next to its API input and output prices, so its cached tokens are valued at what the API charges for them.
GET api/setupfor the dashboard's first-run setup: whether a model is still needed, whatsemantic-router initwould find on this machine (LM Studio, Ollama, and the claude, codex and copilot CLIs with whether they are signed in) and one-click provider presets (LM Studio, Ollama, OpenAI, Anthropic, OpenRouter, Google Gemini, Claude Code, Codex, GitHub Copilot). It only describes; nothing is enabled until the dashboard applies it. The machine policy, offline mode and the allowed endpoints leave out what they turn off (offline, no CLI is started to ask). Like a change, it is refused from other sites, so a page elsewhere cannot set off the CLI checks;?refresh=truereuses a look younger than 10 seconds, and a detection that fails still answers with the presets and the problem indetectionProblem.POST api/connectors/discoverdiscovers the models of a connector that is only in the dashboard's draft, held to the same checks as applying it (policy, what a CLI may run, where a stored secret may go). The audit log records where it asked (scheme://host:port, or a CLI's program).POST api/config/saveandPOST api/config/revert, andunsavedinGET api/status: a configuration applied without saving can be kept or undone, and the dashboard can say it is not saved.PUT api/config?baseVersion=N&instance=Xrefuses a change made against a version that is no longer running (409with who changed it, when and how), so two editors cannot silently undo each other.instance, a random id for this run of the router, is in every configuration envelope andGET api/status: version numbers start again at 1 on every start.- Validation errors from
PUT api/configandPOST api/config/validatecome withissuesthat name the field (Routes.1.Name,Connectors.local.Timeout), and a value that does not fit its field says the format it needs ("5m" is not a duration; use hh:mm:ss, e.g. 00:05:00) instead of "Expected a configuration document". - The
semantic-routertool and the desktop app redirect/to the dashboard (MapRouterDashboardRedirect; an application's own/endpoint wins). - The start-up banner has a Try it block: a request to send (curl, or
Invoke-RestMethodon Windows), where to watch it on the dashboard (#/live), and the Claude Code command as one line with its variables inline, as in the client guide. While no model is enabled it says so instead, with the link to the setup on the dashboard's Overview. - The container seeds a connector for
OPENAI_API_KEYandANTHROPIC_API_KEYon the start that creates the configuration file (deleting one sticks); the connector refers to the key as${env:…}, so the key never lands in the file (SEMANTIC_ROUTER_SEED_API_KEYS, on in the image). doctorchecks the Codex CLI's sign-in withcodex login status.- Desktop app: on the first start the dashboard opens on its Overview, where the setup is, and the welcome window says so. While the router has no enabled model, the tray menu says so and offers the setup, with one notification.
- README: a Quick start right under the introduction (who it is for, what to install, the commands, the desktop app, where to go next).
- Dashboard: Running configuration not saved in the top bar when the router runs a configuration that was applied without saving, with Save and Revert to saved for the admin (the More panel shows the source as "not saved").
- Dashboard: the save bar counts the draft's changes, and Review shows them next to the running configuration.
- Dashboard: Apply names the version the draft started from; when someone else applied a configuration meanwhile, a notice says who and when and offers Reload, Compare or Apply mine anyway. The refresh notices such a change too (a router restart included): it reloads a draft without changes quietly, and moves unapplied edits onto the new configuration when they touch other settings. Moving them is strict: a new order of model aliases, routes or models, or a list such as CLI arguments changed on both sides, is a conflict; only lists that are sets (utterances, capabilities and similar) combine.
- Dashboard: removing a route, model, connector, profile, tier, budget or memory source offers Undo; removing a connector that models use, or a profile that rules, virtual models, client mappings or budgets name, asks whether to remove those too.
- Dashboard: problems from Validate and Apply are shown next to their fields (the item opens, the first gets the focus), and the problem list links each one to its field on its page.
- Dashboard: model aliases can be reordered (the first matching glob wins).
- Dashboard: new Activity entries that arrive while the table holds still (auto-refresh off, a label being written, text selected) are counted, with Show.
- Dashboard: Set up SemanticRouter on the Overview while no model is enabled: add a connector (tools found on this machine first, then presets for LM Studio, Ollama, OpenAI, Anthropic, OpenRouter, Gemini and the CLIs), add models (Discover models, Apply & save), send a test prompt. Read-only users see that an admin needs to add a model.
- Dashboard: Add connector is a menu of presets and a blank connector, also shown on an empty Connectors page.
- Dashboard: Activity has a search box, and a click (or Enter) on a row opens the whole decision.
- Dashboard: Router learned has a small Settings form for the common
Tuningoptions. - Dashboard: Routes can be set to follow the conversation (
FollowsConversation). - Desktop app: the dashboard in the app window has an update banner when an update is ready, available or downloading (Restart to update, Not now), and About SemanticRouter under More. On macOS the app menu has About SemanticRouter and Check for Updates….
- Desktop app: a Settings window with everything the tray menu sets or does (the router's status and problems, Restart router, the background service, start at sign-in, where the dashboard opens, the log and the configuration folder), opened from the tray, from App settings under More on the dashboard in the app window, and from the macOS app menu (⌘,). Nothing needs the tray icon any more.
- Desktop app: on Linux (libnotify 0.7.10 or later) a click on a notification opens what it is about. A failure you caused (the background service, a restart, an update download) is also a notification.
semantic-router init --enable-cli claude,codexenables just the subscription CLIs named, without asking; the others found are added disabled. Without a terminal,initnow says that it skipped the questions and how to choose (--yes,--enable-cli,--no-cli).- Dashboard: Admin → Service for the
semantic-routertool: who can connect (this machine only, or other machines too, and the port, saved tosemantic-router.host.jsonand applied by a restart, with what serving other machines needs), Restart and Stop, and the background service (Install and hand over, Reinstall, Remove the service, its definition and logs). A restart happens in the same process; one onto a port that is taken goes back to the old address and says so. It says when--urls, an environment variable,Kestrel:Endpointsor a container decides where the router listens, and an application that hosts the router itself (or the desktop app) keeps its own controls. API:GET api/service,PUT api/service/listen,POST api/service/{restart|stop|install|reinstall|uninstall}, admin only and audited; hosts plug in throughIRouterServiceControl. - Documentation site: each video has a transcript (its narration, plus figures shown on screen that the narration doesn't say), a link to its captions file, and a download link that names the video. The build fails when a video is published without captions, unless
site/videos.jsonmarks it"speech": false. - Before the state database is migrated to a newer schema, the file as it was is kept as
semantic-router.db.v<old schema>.bak, so the previous release can be started on it again. Upgrading and going back describes both directions (#205). - Upgrades bring
semantic-router.jsonup to the current built-in template: on the first start of a new release, routes, utterances, model aliases and defaults that still have an earlier template's values get the current ones, and settings you changed are kept. The start prints what changed, and the configuration history records it as anupgradeentry. The file is stamped with"TemplateVersion"(RouterConfiguration.TemplateVersion). The sample evaluation set is replaced too while nobody has changed it (#204). - Desktop app:
SemanticRouter --update --restartapplies an update as Restart to update does, starting the new version (and the background service, if it ran the app) once the update is applied. CI uses it to test the restart on Windows (#212). - Dashboard: New defaults to review on the Overview after an upgrade to a newer built-in template kept settings you had changed that the template changed too: each with yours and the new default, Use the new default or Keep mine. It is also a notice under Needs a decision. API:
GET api/config/template-defaults,POST api/config/template-defaults/useand…/keep(#204).
Changed
- Dashboard: Admin opens on the new Service tab, and the Overview's "this machine only" note points there. The live event stream ends when the router stops, so an open Live view no longer holds up a restart.
- Usage (the Usage page, the digest and budgets) is priced at the catalog's current prices: changing a model's price, cache prices or API prices now changes its past usage too, instead of only requests served after the change. A model no longer in the catalog keeps its recorded cost.
IUsageLedger.Records(a default interface method) hands out the records to price; a custom ledger that does not implement it stays at the recorded prices. - Dashboard: the 19-link sidebar, which no longer fit a laptop screen or the desktop app's default window, is a narrow rail of five sections (Monitor, Routing, Providers, Gateway, Admin) with each section's pages as tabs under the top bar. Routing rules is the Rules tab and the gateway page the Settings tab. Report an issue (the Feedback page) sits at the foot of the rail, and the version, theme picker and Sign out moved under More. Every page keeps its address, so existing links and bookmarks still work.
- A request no model can serve because of the configuration (no model enabled, or none with the tier, capabilities, locality or profile it needs) gets
400 invalid_request_errorin the client's protocol instead of503, so the SDKs and Claude Code stop retrying, with what to do and where: "SemanticRouter has no model that can answer this (no model is enabled). Add one on the dashboard: http://localhost:5080/router#/overview". Failed or cooling-down models still give a retryable503, as do a prompt that could not be classified (the embeddings did not answer) and a request only models outside their active hours would fit ("no model is in its active hours right now"). - The "no model is enabled" warning and service note point to the dashboard's setup on Overview instead of the Models page or
semantic-router init. initreplaces a configuration that still holds the built-in template (as the router writes it on its first start) without--force; a file whose embeddings someone chose is not the template.initoutput: a one-line tier legend, no similarity-threshold note, the Claude Code command on one line (noexport) and only when a model is enabled; with none, the dashboard's setup. When a subscription CLI is all it found, the question says that without it the router has no model to use.doctor: no enabled model is a[FAIL](exit code 1), whose fix is a step that helps (the dashboard's setup, orinitwhile the configuration is still the template). Fix lines name the dashboard field beside the setting, e.g. Providers → Connectors → openai → API key.- An unreachable LM Studio or Ollama says how to start it (Developer → Start server,
ollama serve); other servers name the address setting and where it is on the dashboard. - A failed ONNX model download names huggingface.co,
HTTPS_PROXYandNetwork:CaBundle, and where to copy the file instead, and is logged without a stack trace. - A quieter console: ASP.NET Core's lines per request and the hosting start-up notes from
Warning, data protection fromError(the router's own key-ring line covers it); the router keeps its one line per request.Logging:LogLevelturns any of it back up. - The key-ring warning (unencrypted keys, macOS and Linux) appears only once the secrets file holds a key.
- An address that is in use is explained without a stack trace; when this configuration's own router holds that address, the message says so with its dashboard address.
- Links in the tool's output, the help text and the desktop app point to the documentation site instead of files in the repository.
deploy/compose.yamlruns the published imageghcr.io/siebe41/semantic-router:<version>; building from the checkout is a commented-out alternative.- The site's Get started (home page and top bar) goes to the README's Quick start; its Try it names the .NET 10 SDK and the Claude Code line.
- Dashboard: Apply & save is the primary action; Apply is now Apply only ("a restart undoes it").
- Dashboard: Discard, Use defaults (tiers, complexity rules), revert, reset, rollback and the other confirmations ask in the page instead of the browser's dialog; Discard says how many changes go and offers Undo.
- Dashboard: the top bar's health dot shows the router's overall state (red when failing or no model is enabled, amber when degraded or warming up), with a label, and a short word next to it on a phone.
- Dashboard: messages stack instead of replacing one another, are spoken once (errors as alerts, prefixed so), and "Not applied" stays until it is closed; the top bar and banner are rewritten only when they change.
- Dashboard: a button is marked busy while its action runs and cannot be sent twice.
- Dashboard: the read-only role sees forms as read-only (no add or remove buttons, no Send in the prompt tester), and is not asked before leaving.
- Dashboard: adding an item opens it with the focus in its name; after a removal the focus moves to the next item; a refresh keeps the focus on the button in use and keeps open tables open.
- Dashboard: actions that add an utterance or apply a learned change say "applied and saved" or "applied, not saved".
- Dashboard: a refused rename (a duplicate name,
@or.) says why. - Dashboard: Discover models asks a connector as the draft describes it (
POST api/connectors/discover), so a connector added a moment ago works before Apply; a reason every row shares is stated once above the list. - Dashboard: the Overview leads with setup, decisions, the week, the models and recent activity; "Runs on" and the runtime fold into About this router; one row of counters.
- Dashboard: Rules, Gateway and Profiles split basic from advanced settings; Profiles is four cards; Delete all activity has its own Stored activity card.
- Dashboard: settings show their defaults as placeholders instead of as values, with the limits validation uses; durations show in words ("15 min") and are flagged when not hh:mm:ss.
- Dashboard: capabilities, allowed/denied models and connectors are picked from lists; time zones have suggestions; a budget covers one model, connector or profile picked with Covers, and a new budget is valid.
- Dashboard: routing reasons, configuration sources and settings are in plain words, with the raw value in a tooltip; one format for money, counts, dates and tiers ("T2 · Coding"); consistent Remove, Use defaults and Download buttons.
- Dashboard: Label starts with no tier picked and warns when it is the tier the prompt was routed to.
- Desktop app: the tray's Check for updates is now Check for updates automatically, next to Check for updates now….
- Dashboard: links in running text are underlined, and a few theme colours are slightly stronger so status badges, faint text and links keep 4.5:1 on every surface, hovered rows included. A theme file is now checked against
--bg-hovertoo. - The
semantic-routertool and the desktop app don't start on a state database written by a newer release (after a downgrade), instead of running with budgets that count from zero and an audit log kept in memory. The message names the file, its schema and the copy to restore; the tool exits with code 2 (#205). - Windows: the state database, downloaded models, logs and the desktop app's WebView2 profile moved from the roaming
%APPDATA%\semantic-routerto%LOCALAPPDATA%\semantic-router, so a roaming or redirected profile neither copies them at every sign-out nor puts SQLite's write-ahead log on a file share. The configuration stays where it was. An existing install is moved on its next start; withSEMANTIC_ROUTER_HOMEeverything stays in that folder, anddoctorwarns when the database is on a network drive (#216). - The library packages depend on each other at exactly their own version:
SemanticRouter.Storage.Sqlite,SemanticRouter.Embeddings.OnnxandSemanticRouter.Connectors.Copilot0.6.0 needSemanticRouter.AspNetCore[0.6.0](not 0.6.0 or later), andSemanticRouter.AspNetCoreneedsSemanticRouter.Coreof its own version the same way. Mixing versions, which failed at run time with aMissingMethodException, now fails the restore (NU1107) or warns (NU1608); update all the SemanticRouter packages you use together.SemanticRouter.Connectors.CopilotneedsGitHub.Copilot.SDK1.0.22 or a later 1.0.x, since it uses an API the SDK marks experimental. (#238)
Fixed
- The container image reports its release's version. Released images were built as 0.1.0 whatever their tag, so the dashboard,
GET /router/api/status,semantic-router versionand the image's SBOM all named the wrong version. The image now also carries the OCI labelsorg.opencontainers.image.version,.revision,.sourceand.licenses, and the release fails unless the image, its labels and its SBOM name the tag's version. (#219) - Requests no longer wait for the SQLite state file. Usage, activity, feedback, warm caches, tool-call affinity and cooldowns are saved by one background writer, in batches; with SQLite state the gateway went from about 760 to 6,100 requests a second at 8 clients in the gateway benchmark, and p99 from 300 ms (busy-retry sleeps of 150 ms) to 6 ms. Stopping the router saves what is still waiting. A write that fails is counted (
semantic_router.state.dropped_writes) and the dashboard's status says how many were lost. Activity is pruned every tenth of its cap instead of every 100 entries, and activity ids are reserved 1,024 at a time, ahead of need. - The latency and prompt-cache figures of the latency-aware strategies are recomputed in the background. A request used to read a day of activity from the state file every 10 seconds while every other request waited for it.
- The in-memory warm prompt cache store is bounded (32,768 conversations) and no longer walks every entry on every write once it holds a few thousand.
- Identical prompts from unrelated requests no longer count as retries, so a monitor, a script or two sessions answering "continue" cannot get a healthy model left out of a route for a day. A retry is now the same prompt in the same conversation, before the conversation moved on; only the first repeat of a prompt gives feedback, and a short follow-up said again is not a retry.
- A backend that stops sending partway through a reply no longer holds the request open forever. The new connector setting
StallTimeout(default: the connector'sTimeout) is the time allowed between two pieces of a reply, for HTTP and CLI connectors. Before anything reached the client the next model answers; after, the client gets an error event in its protocol (a reply that is not streamed is broken off) and the model cools down. - Requests that end early are recorded. A client that goes away mid-reply leaves an activity entry marked cancelled and a usage row (what the backend reported so far, or the estimated input), without holding it against the model. A backend that drops the connection mid-reply is recorded as failed and cooled down, and the client gets an error event instead of a truncated reply; the router no longer logs it as an unhandled exception.
- Traffic of many unrelated requests (scripts, evaluations, a shared gateway) no longer slows the router down over its first half hour: the conversation and retry trackers walked every entry on every request once they held a few thousand. They now prune at most every 15 seconds and keep at most 32,768 conversations and 16,384 answers, the least recently used going first.
- Where ONNX Runtime cannot run (Intel Macs, which it has no build for, and macOS before 14),
initno longer sets up the in-process (Onnx) embeddings: it keepsHashingand says to run an embedding model in LM Studio or Ollama. The dashboard's Embeddings page anddoctorsay whyOnnxis not available there, and anOnnxchoice that cannot load is reported as an embedding problem with that reason instead of a stack trace on every request. The release checks that each desktop build ships the native libraries it needs. (#238) doctorand Network traffic name every host the downloads reach: Hugging Face's CDN (*.hf.co), which serves the ONNX model file through a redirect, andrelease-assets.githubusercontent.com, where desktop updates are now downloaded from. A failed model download names both Hugging Face hosts. (#241)- Delete all activity says when another reader of the state file (a second router sharing it) kept a copy of the deleted rows in its write-ahead log past the busy timeout (
"complete": falsefromDELETE api/activity), and that copy is removed after the router's next database operation instead of at the next prune. (#241) - Desktop app: the dashboard's own links (its rail and tabs) opened in the web browser instead of the app window in some setups. A link the web view asks to open in a new window now stays in the window when it is the router's, and the router counts as itself under localhost, 127.0.0.1 or [::1]. Each link handed to the browser is noted in the app's log.
- Changes made on top of the running configuration (an utterance added from the dashboard, learned changes and undoing them) no longer undo a configuration applied a moment earlier: they are made again on top of it, up to three times.
POST api/config/savesaves exactly the version it read, or answers409. - Adding an utterance from the dashboard, and applying or undoing a learned change, no longer saves configuration changes that were applied without saving: while the running configuration is unsaved they apply without saving too, and their answers say
"saved": false. doctorlabelled a CLI working directory it could not create as the CLI's--versionfailing; it now says what it is.- The tool's prerequisite is the .NET 10 SDK (installing a .NET tool needs it), not "runtime or SDK".
- Dashboard: unapplied edits survive adding an utterance from Evaluate or Activity, a learned change, Save or Revert (moved onto the new configuration, or kept with a notice), and signing in again.
- Dashboard: text typed in the JSON editor or the evaluation set is kept across pages and renders, counts as unsaved when leaving, and is not replaced by Show baseline or Load into editor without asking.
- Dashboard: two model aliases or rate limits with the same name are flagged instead of one silently disappearing; Add Claude Code defaults no longer overwrites targets you set.
- Dashboard: removing an item no longer opens the item that moves into its place.
- Dashboard: Usage and Trends that fail to load say why with Retry, instead of asking again on every render.
- Dashboard: an error page from a proxy shows what it said instead of only "HTTP 502".
- Dashboard: Discover on a model shows its result under the field.
- Dashboard: no page scrolls sideways on a phone (Usage, Profiles, Evaluate, Overview); stat tiles stay two abreast.
- Dashboard: chart axes count in whole numbers without repeated labels; the Evaluate sweep's "too high"/"too low" labels no longer overlap.
- Dashboard: Usage right-aligns its figures and folds an empty Feedback card to one line.
- Dashboard: a failed prompt-tester Send or a plan with no eligible model says why and where to fix it.
- Dashboard: Memory without sources no longer shows Fit, Rescan and Clear sharing, nor two sets of add buttons; Evaluate with an empty set says what to do on the page itself.
- Deleting activity (
DELETE api/activity, Delete all activity) could leave the deleted prompt previews instate.db-walwhen another connection was reading the database at that moment (the dashboard, a second router on the same file). It now waits up to the busy timeout for the reader to finish before emptying the log, and logs a warning when it cannot. - Dashboard accessibility (WCAG 2.2 AA, now checked with axe-core on every page, theme and width in CI): tables and the Live feed that scroll can be reached and scrolled from the keyboard; a model's enabled box is no longer inside its header's button and is big enough to hit; table columns without a visible heading have one for screen readers; the Feedback page's headings are in order; the charts are named with their figures; the Trends and Usage segmented buttons say which is pressed.
- Desktop app accessibility: the About, welcome and new Settings windows have headings, links that read as links, a logo hidden as decoration, and explanations read with their checkboxes; the update status and the router's problems are announced when they change; the focus moves to Restart to update after Check now finds one; the welcome window closes with Escape; the windows scroll instead of running off a short screen with large text; secondary text keeps full strength under high contrast; opening the dashboard window puts the focus in the page.
- Documentation site: the brand buttons' white text has at least 4.5:1 contrast in both themes, including on hover. The screenshots' alt text describes what they show (the memory map's colours included), the Mermaid diagrams have accessible titles and descriptions, links show page or section names instead of file names, and
semantic-router modelswrites "why:" instead of an arrow glyph. - Releases: a tag with a version suffix (
v1.0.0-rc.1) is published as a GitHub prerelease with prerelease desktop update feeds, which installed desktop apps do not take, and its image is not tagged:latest(#207). - A state database that can't be used (corrupt, locked, or newer than the router) was reported only in the log. It is now one of the configuration warnings in
GET api/statusand on the dashboard, anddoctorhas astateline that fails on it (#205). - An install set up with 0.1.0–0.2.1 kept that release's routing defaults after every upgrade: Claude Code's default model name stayed an explicit Opus request (
"*opus*": "family:opus", fixed in 0.3.0 for new installs only), and thequick-answersroute and later utterances never arrived. They now arrive on the first start of this release unless you changed those entries; if you did, set*opus*toautoyourself to have Claude Code's turns routed.initwithout--forcealso replaces an untouched template from any earlier release, as it says it will, instead of refusing (#204). - Desktop app: the Windows zip archives no longer carry 105 MB of native debug symbols, import libraries, the IIS module and XML documentation the app never uses (277 MB unpacked before, 176 MB now) (#217).
- Desktop app: an update no longer turns a background service installed with
semantic-router service installinto one that runs the app, and uninstalling the app on Windows no longer removes it. The app reads which program the service runs, and its Keep the router running in the background setting names that program when it is not the app (#214). - Windows: the dashboard's setup left out, without a word, a CLI installed only as an npm script wrapper (
codex.cmd), which the router does not start. It now lists it as found but not usable as installed, with where to get the.exe;init,doctorand the connector's error name that place too, and CLIs on Windows gives a Windows install for Claude Code, Codex and Copilot (#211). - Desktop app on macOS: macOS's web view carries out ⌘C, ⌘V, ⌘X, ⌘A and ⌘Z through the Edit menu, which the menu-bar app does not have, so pasting a key into the dashboard window could fail. The dashboard in the app window now handles these shortcuts itself (it does not copy from password fields). docs/desktop.md no longer describes an app menu that macOS does not show (#210).
- Desktop app: the tray icon is the app's pulse glyph without its dark tile, which nearly disappeared on a dark menu bar or taskbar. On macOS it is a template image that follows the menu bar's light, dark and highlighted look; on Windows an icon with every size the notification area uses (#215).
- A spending budget no longer slows down every request as the period's usage grows (62 ms per request at 20,000 requests a month, more than half a second at 200,000): the budget check keeps the period's totals in memory instead of reading every usage row, and the Usage page and digest add up usage in the database. Routers sharing a state file count each other's usage within 2 seconds. The state file's usage table is rebuilt once, on the first start after the update. (#238)
- Activity entries were lost: concurrent requests could get the same id (the same
X-Prompt-Route-Id), and two routers sharing a state file handed out the same ids, each replacing the other's entries without a word (about half the log). Every request now gets its own id, routers sharing a file take ids from a counter in it, and an id that is already stored is refused and logged instead of overwritten. Feedback ids too. Ids skip ahead after a restart. (#238)
Security
- The release builds, scans and pushes the container image in separate jobs. Trivy and Syft, which download their binaries and vulnerability database at run time, now run in a job without permissions or caches instead of next to a token that can push to the registry, and the push checks that both the image it loads and the image the registry then holds are the one the build made. (#223)
- CLI connectors leave the router's own settings and keys out of the child's environment:
SemanticRouter__…andROUTER_…variables, the provider keys the setup reads (OPENAI_API_KEYand others), and any variable holding a secret the router resolved.RemoveEnvironmentVariablesandPromptInputcount as what a CLI runs, so the admin API changes them only withAllowProcessSettings. (#238) - Gateway keys must have at least 16 characters (a shorter key in the configuration is refused, one from a reference is a warning), and an address that sends 20 wrong keys within 15 minutes gets
429withRetry-Afteruntil the oldest is 15 minutes old. (#238) - A repeated
TrustedGroupsHeaderis refused with403, as a repeated user header already was, so a client's own copy of the groups header cannot raise its role behind a proxy that appends to it. (#238) - Delete all activity (
DELETE api/activity) also deletes the feedback about the deleted entries (the judge's reason can quote the prompt and the answer), the prompt previews learned changes quote as evidence and the latest connector failure messages (a failed CLI run's output). A request still running, or waiting for the judge, when its activity is deleted is no longer stored again, and the judge drops it. (#241) - The dashboard's API answers, the activity export and the live event stream are sent with
Cache-Control: no-store, so a browser or the desktop app's web view no longer keeps prompt previews, the configuration or learned examples in its disk cache, where deleting activity could not reach them. The desktop app empties its web view's HTTP cache when its window first opens. (#241) - Retention (
State:Retention, capped by the machine policy'sMaxRetentionDays) is enforced at start-up, every hour and when it changes, not only on every 100th write, and the write-ahead log is truncated after a prune. The activity export, the feedback summary and tuning leave out rows past retention even before they are pruned. (#241) - An unreachable HTTP connector's log line no longer includes credentials written into its
BaseAddress(https://user:key@host), and such credentials, and key-like query values, are redacted wherever the router redacts keys. (#241) - Another account on the same machine is no longer trusted as "this machine": the router asks the operating system who opened each loopback connection (Linux, macOS, Windows), and another user is a remote client that needs a dashboard token or a gateway key and is not served by subscription CLI connectors. Root and LocalSystem still count as the router's own account.
SemanticRouter:Network:TrustOtherLocalAccountsrestores the old behaviour for a router that serves everyone who signs in to the machine. (#235) - Over the admin API, every value the router resolves is held to the secret-reference rules, not only header names that look secret, and an
${env:…}reference a setting did not have before is accepted only from this machine or as a provider preset's key for that provider's address. (#235) GET api/routes/health, which embeds every utterance, is refused from other sites and to the read-only role, and is recorded in the audit log, like the other endpoints that spend embedding calls. (#235)- The judge does not send a remote client's request and answer to a subscription CLI or Copilot connector that does not allow remote clients. (#235)
- Desktop app: the app window hands only
httpsandhttplinks to your browser. A link or navigation to any other scheme (file:, a network share,ms-msdt:,search-ms:,javascript:,data:or another program's protocol) is dropped and noted in the app's log instead of being passed to the operating system, and the app acts on a message from the page only while the window shows the router. (#238) - Desktop app: a release build ignores
SEMANTICROUTER_UPDATE_SOURCEunless the machine policy sets the newUpdates:AllowCustomFeed, and an update feed (from the policy or the variable) must be HTTPS or a folder; plain HTTP is accepted only to this machine, and other schemes are refused. (#238) - Desktop app: with
Updates:Publisherset on Windows, an update is also refused when its*.runtimeconfig.jsonor*.deps.jsonwould make the .NET host load code from outside the package (additionalProbingPaths,STARTUP_HOOKSand the host's other properties, paths that leave the app's folder) or it has a*.runtimeconfig.dev.json. (#238) - On Windows, a
SEMANTIC_ROUTER_HOMEorSEMANTIC_ROUTER_KEYS_DIRfolder the router creates outside your profile is readable only by your account, SYSTEM and Administrators, instead of inheriting permissions that usually let every user read it;doctorwarns when an existing configuration or key ring folder can be read byUsers,EveryoneorAuthenticated Users. (#238) - The in-process (
Onnx) embeddings no longer send usage data to Microsoft or write a device id to~/.cache/Microsoft/DeveloperTools/.onnxruntimeon Linux and macOS, offline mode included: the router setsORT_DISABLE_TELEMETRY=1in its own environment before ONNX Runtime loads (a value you set is kept), and the container image and the servicesservice installwrites set it too.doctorsays whether ONNX Runtime's telemetry is off. (#238)
0.4.0 - 2026-10-16
Added
- Desktop app: an Updates section in About SemanticRouter, and Check for updates now… in the tray menu. It shows whether the app is up to date and when it last checked, an update that is downloading, ready or available, and why a check failed or updates are off. It has Check now, Restart to update and the two update switches.
0.3.0 - 2026-10-16
Security
- Machine-wide policy that the user's configuration cannot override, read from
HKLM\Software\Policies\SemanticRouter, macOS managed preferences or/etc/semantic-router/policy.json: turn off connector types and CLI presets, limit endpoints and the hosts that count as local, require local-only, cap prompt storage and retention, make the dashboard read-only or turn it off, control desktop updates and turn features off. A policy that cannot be read stops the router. See docs/enterprise.md. (#142) initdoes not add, or ask about, CLIs that the machine policy turns off. (#146)- Offline mode (
Network:OfflinewithNetwork:AllowedHosts, orOfflinein the machine policy): connectors, embeddings and webhooks reach only this machine and the allowed hosts, CLI and Copilot connectors are off, and the ONNX model is never downloaded. (#147) - Remote gateway clients need a key: with no
Middleware:GatewayKeysthey get 401 (AllowUnauthenticatedRemoteClientsrestores the old behaviour), and a key reference that does not resolve no longer opens the gateway. The container image readsROUTER_GATEWAY_KEY. (#129) - Default limits for remote clients (
Middleware:RemoteRequestsPerMinute, 120) and CLI queues (MaxQueue, 16), andForwardedHeaders:KnownProxies/KnownNetworksto trustX-Forwarded-Foronly from named proxies. (#129) - Web pages cannot use the gateway through DNS rebinding or a foreign
Origin;/v1/models,count_tokensand/semantic-router/classifyfollow the same gateway-key rule as the chat endpoints. (#114) - Dashboard responses forbid framing (
frame-ancestors 'none',X-Frame-Options: DENY) and sendnosniffandno-referrer; the dashboard bundles its fonts and sends a strict Content-Security-Policy. (#114, #129) - The admin API cannot change what a CLI runs (program, arguments, environment, home directory) or which paths memory sources read, unless
Dashboard:AllowProcessSettingsis on. (#114, #144) - The read-only dashboard role can no longer trigger calls that cost money or scan the disk (evaluate, classify, embeddings test, discover, check, memory refresh); dashboard sessions expire after
Dashboard:SessionLifetime(12 hours), and wrong tokens are throttled per address. (#144) - Dashboard identity from an authenticating reverse proxy (
Dashboard:TrustedIdentityHeader, believed only fromKnownProxies), the real actor on every configuration change, and an append-only audit log (GET /router/api/audit, an Audit log page,State:Retention:AuditDays). (#150) - A
Metricsdashboard role (Dashboard:MetricsToken, orMetricsinTrustedIdentityRoles) that sees figures only, never prompt or reply text. Signing out ends the session on the server, and an admin can sign out every session (POST /router/api/session/revoke-all); sessions from before the upgrade sign in once more. (#161) - With
State:StorePrompts: Off, a failed CLI run's output and the judge's reason for a grade are not stored or logged either;Redactedmasks credentials in them. (#162) - Local-only routing holds in more cases: a failed classification stays local while any route is local-only, a conversation kept local stays local (including named cloud models), local-only prompts stay off remote embedding endpoints (
Embeddings:IsLocal), and the judge respects routing profiles. (#114) - Secrets: CLI environment variables and more header names (auth, cookie, password, credential, session, signature) are masked; masked or stored keys are never sent to a connector that now points somewhere else; the configuration history keeps no secrets; keyed requests do not follow redirects; webhook URLs are not logged. (#114, #116)
- Keys written into addresses (
user:key@, key-named query values) are masked in the admin API, the configuration history and every address the dashboard anddoctorshow. (#117, #129) - CLIs run in a working directory only the user can write to and start by full path; a Custom CLI's
{prompt}cannot become an option; the router's files are readable by their owner only. (#114) - The data-protection key ring is encrypted at rest: to a certificate (
DataProtection:Certificate) on any OS, with DPAPI forSEMANTIC_ROUTER_KEYS_DIRon Windows; otherwise owner-only permissions and a start-up warning.doctorsays how the keys are protected. (#149) - Stored prompts:
State:StorePrompts(Redactedby default, masking credentials in previews;On;Off),State:Retention, and a purge (DELETE /router/api/activity, "Delete all activity") that leaves nothing readable in the database files. (#129) - Webhooks carry no prompt or reply text unless
Notifications:IncludeDetailsis on, and the stored project label is a hash of the client'suser_id, never the id. (#144) - Prompted tool calls are harder to forge: a random fence label per run that never reaches the client, defused tool results,
PromptedToolParsing: Strict(opt-in), calls copied from a tool result kept as text in both modes, and a prompted-call count in the activity log. (#129, #152) - No outbound traffic you did not configure: no web fonts from other hosts, Claude Code started without its auto-updater and non-essential traffic, Codex with update checks and analytics off.
doctorlists every host the router will contact. (#129, #143) - Desktop updates:
Updates:Publisherrequires an update to be code-signed by a given publisher before it is applied, and the updater refuses feeds over plain HTTP other than loopback. (#142, #151) - Verifiable releases: build provenance attestations for every asset,
SHA256SUMS, CycloneDX SBOMs, a keyless cosign signature and attestations on the container image, andTHIRD-PARTY-NOTICES.txt. See docs/releases.md. (#145) - Release pipeline: a tag must be on
main, tests pass before anything publishes, the desktop app is built in jobs without secrets, and the image is built and scanned (Trivy) before the registry login. CodeQL and OpenSSF Scorecard run on the repository. (#129) - The container runs on a chiseled ASP.NET Core image with no shell, with both base images pinned by digest. (#129)
- NuGet restores are locked to committed
packages.lock.jsonfiles in CI, the release and the container build. (#148)
Added
- A "Cut a release" workflow (Actions > Cut a release): the repository owner enters a version, it checks CI, code signing and NuGet publishing, tags
main, and the release workflow publishes everything with the CHANGELOG section as the release notes. Only the owner can release, by this workflow or a pushed tag. See docs/releases.md. - Cache-aware stickiness: a conversation stays on the model with the warm prompt cache when that is cheaper than moving, and returns to it after a failover (
Middleware:Conversation:CacheLifetime). (#137) - Prompt-cache markers on Anthropic requests translated from OpenAI or Responses clients (
Connectors:{name}:PromptCaching,CacheTtl). (#139) - Cache reads and writes per activity entry, and a cache hit rate on the Usage page. (#113)
- Endpoints card on the Overview (host, state, health and latency per connector, with a Check button) and a Service card (where the router runs, the URLs for clients, its health). (#117, #121)
Network:CaBundle, a PEM file trusted in addition to the system store, for TLS-inspecting proxies. (#143)- Desktop app: notify-only updates (
Updates:Apply: Notifyin the machine policy, or "Install updates when quitting" in the tray), and managed deployment docs. (#151) PromptRouterMiddlewareOptions.InjectedTags; the Copilot app's injected context blocks are ignored when classifying. (#118)- Model names
semantic-router,claude-semantic-router,-cheapestand-best;/v1/modelsentries have adisplay_name. (#156) tool-callsbenchmark for prompted tool calling. (#152)- A
quick-answersroute (tier 2) in the starting routes, for short factual and explain questions, and small-refactor utterances insimple-code; the sample evaluation set has 116 prompts. (#160) - Dashboard Feedback page and a Report button on each Activity entry: they open a GitHub issue (misroute, bug or idea) with the decision line, the model that answered, the embedding settings and the router version filled in. The prompt is left out unless you add it, and a local-only prompt answered by a cloud model points to private security reporting instead. The misroute and bug forms ask for the router version and the decision line. (#164)
Changed
- Targets .NET 10 (LTS) instead of .NET 9, whose support ends on 10 November 2026: the packages and the tool need the .NET 10 runtime, and the container runs on ASP.NET Core 10. Building needs a 10.0.4xx SDK. (#115, #148)
initasks before enabling a subscription CLI connector (Claude Code, Codex); declined or non-interactive runs add it disabled.--yesrestores the old behaviour and--no-clileaves CLIs out. (#141)- Prompts go to Claude Code and Copilot CLIs on standard input;
PromptInput: "Argument"restores the old behaviour. (#114) - Model comparisons count each model's prompt-cache prices, from the share of input it reads from cache (
Selection:CacheShareWindow,CacheShareSamples). (#138) - Only the prose of a prompt is embedded, without pasted code or data (
EmbedProseOnly, on by default); local-only routes are still checked against the whole prompt. (#157) - The
inittemplate routes Claude Code's*opus*asauto;opus-pinnedpins Opus, and an exact alias wins over a wildcard. (#156) - Routing profile
Projectsglobs match the hasheduser:…project label, and dashboard sessions sign in once more after the upgrade. (#144) - Dependency updates, among them GitHub.Copilot.SDK 1.0.22. (#134, #135, #136)
Fixed
- Cached-token counts on streamed and translated replies, from the CLIs and in "saved by routing". (#113)
- After a failover, the serving model is timed from its own attempt, so latency ranking and endpoint health no longer count the failed attempts against it. (#154)
- Claude Code: a conversation keeps its key across turns (with
Middleware:Conversation:SessionHeader), the session-title request is housekeeping, and a JSONmetadata.user_idis not used as the project. (#156) - A user message that holds only injected context (such as the Copilot app's
<system_reminder>) no longer hides the person's message from the classifier. (#118) - "What can you help me with?" goes to smalltalk; classify and Try a prompt apply callouts; computed properties are no longer written into
semantic-router.json. (#157) - Dashboard layout: steady Live rows, no sideways scrolling on Live, long tokens on Activity. (#155)
- A warm prompt cache on an Anthropic connector counts for its
CacheTtl(or the client's ownttl), not alwaysMiddleware:Conversation:CacheLifetime; a tool call written inside a line of a tool result is kept as text when the model repeats it. (#162)
0.2.1 and earlier
See GitHub Releases.
