Skip to content

ThinkWatch Core 0.62.0

Choose a tag to compare

@github-actions github-actions released this 05 Oct 05:19
· 48 commits to main since this release
51ef803

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 --restart switches 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 aliases table, and to in 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 allow is 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's allow through one of these gaps is now refused.
  • Changed in the protocol:
    • New: GET /aliases (AliasesView), POST /aliases and PUT /aliases/{name} (AliasSave → ConfigWritten / AliasWritten), DELETE /aliases/{name}, POST /alias-preview (AliasPreviewRequest → AliasPreview) and GET /aliases/{name}/usage (AliasUsage).
    • RuleView.to and RuleInput.to are a RuleTarget: an upstream or group name as before, or a list of PinnedModel { provider, model }.
    • KnownModel gains alias (the alias's models, when the entry is an alias) and aliases (the aliases that list a model); GET /models lists aliases. ModelRow gains aliases.
    • The dry-run result gains candidate_models, one DryRunCandidate { provider, sent_model, model_via } per candidate.
    • ServeSkip gains not_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-confirmed and PUT /plugins/order → PUT /plugin-order. At the old paths they shadowed an upstream, proxy or plugin of the same name: an upstream named test or preview, or a proxy named test, could not be edited or deleted. order, inspect, rewrite and confirmed are now valid plugin ids.
  • 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_id and control.plugin.reserved_id, since no plugin id is reserved any more.

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/models lists 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 allow and a rule's when.model written for an upstream model name also cover the aliases that list it; written for an alias, they cover only the alias.
  • set.model may 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.create on 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's allow with the same inheritance, and gets the rules' set.max_tokens, set.thinking and deny as evaluated for that request; one that can't be sent is answered with response.failed and 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 against allow, 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' allow entries and the rules' when.model and set.model that name it. When the old name stays in the alias's list, allow and when.model keep 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.0

An installation made with the script switches to 0.62.0 with:

sudo twcore upgrade --version 0.62.0 --restart

Configuration, 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.sha256

On macOS:

shasum -a 256 -c twcore-aarch64-apple-darwin.sha256

On 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