Repository navigation
ThinkWatch Core 0.62.0
This release adds model aliases: one name for the same model across upstreams, each upstream receiving the name it uses for it. Routing rules can also pin requests to a particular upstream's model, answers carry the model name the client asked for, and the model listing tells clients each model's context window. Tool-call inspection gains a rule that guards ThinkWatch's own data directory, and an upstream URL that ends in its own API version no longer gets a second one.
Upgrade notes
- The control-plane protocol version (
CONTROL_API_VERSION) goes from 37 to 38. ThinkWatch Lite connects only to a core with the same protocol version: ThinkWatch Lite 2026.10.4 includes 0.61.0 (protocol 37) and does not connect to 0.62.0. A server used with it stays on 0.61.0 until the app is updated to a release that includes 0.62.0;sudo twcore upgrade --version 0.61.0 --restartswitches a server back. - The request store's schema is unchanged: upgrading from 0.61.0 keeps the request history.
- The configuration format only gains fields: a top-level
aliasestable, andtoin a routing rule may now be a list of pinned models. Every 0.61.0 configuration loads unchanged. - Answers to requests whose model was renamed on the way (by an alias, a rule's
set.model, a pinned model or a plugin) now carry the name the client asked for, as long as the upstream answered with the model it was sent. Before, they carried the upstream's name. - A key's
allowis now enforced in three places it used to be skipped: when no upstream reports a model list, on WebSocket connections (Responses and Realtime), and on every failover candidate, including pinned models. A client that was using a model outside its key'sallowthrough one of these gaps is now refused. - Changed in the protocol:
- New:
GET /aliases(AliasesView),POST /aliasesandPUT /aliases/{name}(AliasSave→ConfigWritten/AliasWritten),DELETE /aliases/{name},POST /alias-preview(AliasPreviewRequest→AliasPreview) andGET /aliases/{name}/usage(AliasUsage). RuleView.toandRuleInput.toare aRuleTarget: an upstream or group name as before, or a list ofPinnedModel { provider, model }.KnownModelgainsalias(the alias's models, when the entry is an alias) andaliases(the aliases that list a model);GET /modelslists aliases.ModelRowgainsaliases.- The dry-run result gains
candidate_models, oneDryRunCandidate { provider, sent_model, model_via }per candidate. ServeSkipgainsnot_allowed: a candidate skipped because the key may not use the model it would be sent.- Moved, with the endpoint names unchanged:
POST /providers/test→POST /provider-test,POST /providers/preview→POST /provider-preview,POST /proxies/test→POST /proxy-test,POST /plugins/inspect→POST /plugin-inspect,POST /plugins/rewrite→POST /plugin-rewrite,POST /plugins/confirmed→POST /plugin-confirmedandPUT /plugins/order→PUT /plugin-order. At the old paths they shadowed an upstream, proxy or plugin of the same name: an upstream namedtestorpreview, or a proxy namedtest, could not be edited or deleted.order,inspect,rewriteandconfirmedare now valid plugin ids.
- New:
- Message codes, compared with 0.61.0:
- New:
config.alias_blank_model,config.alias_chained,config.alias_duplicate,config.alias_empty_name,config.alias_no_models,config.alias_only_itself,config.alias_reserved,config.alias_wildcard;engine.pinned_blank_model,engine.pinned_duplicate_provider,engine.pinned_empty,engine.pinned_unknown_provider,engine.pinned_with_provider_would_be,engine.pinned_with_set_model;gw.model.alias_unserved,gw.model.alias_unserved_rewritten,gw.model.pinned_not_allowed,gw.model.pinned_not_offered,gw.plugin.alias_unserved,gw.ws.alias_unserved,gw.ws.alias_unserved_rewritten;control.replay_alias_unserved. - Removed:
config.plugin.reserved_idandcontrol.plugin.reserved_id, since no plugin id is reserved any more.
- New:
Model aliases. An alias is another name for the same model, written in a top-level aliases table as the alias and the names upstreams use for it, in order.
- An upstream serves an alias when it offers one of the listed names within its
models_only, and receives the first such name in the list. Every upstream that serves an alias takes part in failover for it, each receiving its own name. GET /v1/modelslists aliases next to the upstream models; the original names stay available.- An alias takes precedence over an upstream model of the same name that it doesn't list.
- A key's
allowand a rule'swhen.modelwritten for an upstream model name also cover the aliases that list it; written for an alias, they cover only the alias. set.modelmay name an alias. Prices, the upstream check-up and each recorded attempt use the name sent upstream; the request log keeps the name the client asked for.- Replay, the inference speed test and plugin renames resolve an alias for each upstream the same way.
- WebSocket connections follow the same rules as HTTP requests. Each
response.createon a Responses connection is sent under the connected upstream's own name for an alias (a pinned model or a phase-two rename as written), is checked against the key'sallowwith the same inheritance, and gets the rules'set.max_tokens,set.thinkinganddenyas evaluated for that request; one that can't be sent is answered withresponse.failedand the connection stays open. A Realtime connection (/v1/realtime) is routed by the model in its query string, which the key must be allowed to use and which reaches the upstream under its own name. Before, WebSocket connections were not checked againstallow, and the rules' parameter rewrites and model conditions did not apply to them. - The control plane manages aliases (
/aliases), previews a draft (which upstream would receive which name, upstream models of the same name it would take over, other upstreams' names for the same Claude model) and renames an alias together with the keys'allowentries and the rules'when.modelandset.modelthat name it. When the old name stays in the alias's list,allowandwhen.modelkeep it: the name of a listed model already covers the alias.
Pinned models. A routing rule's to may list upstreams with the model to send each, tried in order. The model is sent as written: aliases and set.model do not apply. A rule can so send one key's requests to one upstream's model even when an alias of the same name points elsewhere.
The model name in answers. When the model sent upstream differs from the one the client asked for and the upstream answered with the model it was sent, the answer carries the client's name: in all four API formats, streamed or not, and in the openai-model and x-openai-model headers. An answer naming a different model than the one sent is passed on unchanged, so a substitution by the upstream stays visible. The request log and the upstream check-up keep the upstream's own answer.
Context windows in the model listing. Each model in GET /v1/models carries its context window where the price table knows it: context_window, context_length and max_input_tokens, plus supports_1m on the Anthropic shape and inputTokenLimit/outputTokenLimit on the Gemini shape. Claude family tiers now include fable and mythos.
ThinkWatch's own data directory. Tool-call inspection has a new built-in rule, thinkwatch-data, for a tool call whose path or command points into ThinkWatch's data directory (~/.thinkwatch, %APPDATA%\ThinkWatch, /var/lib/thinkwatch, /etc/thinkwatch). Under enforce it cuts the response off. Text that only mentions the directory, such as a document being edited, does not count. The configuration reference now says what protects config.yaml on the machine.
Upstream URLs with their own version. An upstream whose base URL already ends in an API version (…/api/paas/v4, …/api/v3) receives requests under that version instead of a second /v1 appended to it, which used to return 404. Requests, connection checks and model-list fetches all use it.
Downloads
| Platform | Binary | Archive for server installation |
|---|---|---|
| Linux, x86_64 | twcore-x86_64-unknown-linux-gnu |
twcore-x86_64-unknown-linux-gnu.tar.gz |
| Linux, aarch64 | twcore-aarch64-unknown-linux-gnu |
twcore-aarch64-unknown-linux-gnu.tar.gz |
| macOS, Apple silicon | twcore-aarch64-apple-darwin |
— |
| Windows, x64 | twcore-x86_64-pc-windows-msvc.exe |
— |
| Windows, ARM64 | twcore-aarch64-pc-windows-msvc.exe |
— |
Each file is published with a .sha256 file beside it. A Linux archive contains twcore, the systemd unit twcore.service and LICENSE. ThinkWatch Lite includes its own copy of twcore; the files here are for running core separately, such as on a server.
Server installation
On Linux (x86_64 or aarch64), the install script sets up twcore as a systemd service. This installs 0.62.0:
curl -fsSL https://raw.githubusercontent.com/ThinkWatchProject/ThinkWatch-Core/main/scripts/install.sh | sudo sh -s -- --version 0.62.0An installation made with the script switches to 0.62.0 with:
sudo twcore upgrade --version 0.62.0 --restartConfiguration, the remote control port and connecting ThinkWatch Lite are described in docs/server.md.
Verifying a download
A .sha256 file holds the SHA-256 of the file followed by its name. With both files in the current directory, on Linux:
sha256sum -c twcore-x86_64-unknown-linux-gnu.tar.gz.sha256On macOS:
shasum -a 256 -c twcore-aarch64-apple-darwin.sha256On Windows, in PowerShell, the following prints True when the binary matches:
(Get-FileHash .\twcore-x86_64-pc-windows-msvc.exe).Hash -eq (Get-Content .\twcore-x86_64-pc-windows-msvc.exe.sha256).Split()[0]The install script and twcore upgrade check the SHA-256 themselves.
What's Changed
- Tool-call inspection guards ThinkWatch's own data directory; the docs say what protects config.yaml by @fylorn in #287
- fix(url): a base URL's own version takes the place of the client's /v1 by @fylorn in #288
- Model aliases and pinned models in routing rules, v0.62.0 by @fylorn in #290
Full Changelog: v0.61.0...v0.62.0