Repository navigation
ThinkWatch v2.0.0
Images
ghcr.io/thinkwatchproject/think-watch-server:2.0.0ghcr.io/thinkwatchproject/think-watch-web:2.0.0
Helm
helm install think-watch ./think-watch-2.0.0.tgz \
--namespace thinkwatch --create-namespaceCallers now get errors in their own API's format, and an upstream that
refuses a request no longer takes a model's other routes down with it.
The gateway also speaks two more client protocols: Gemini, and the
Responses API over a WebSocket. Cached input is billed at cache prices,
and a request with no usage report is billed on an estimate instead of
at zero. The TOTP requirement, which never took effect before, is now
enforced. This is a major release because error bodies, the
content-filter preset ids and the TOTP behaviour all change in ways a
client or a script can notice.
Read before upgrading
-
Check
security.totp_requiredbefore you upgrade. In 1.x this
setting never took effect: it is stored as a boolean and was read as a
string, so it always read as off. From 2.0.0 it is enforced by the
server. Find out what it is set to:SELECT value FROM system_settings WHERE key = 'security.totp_required';
If it is
true, every console user without TOTP, super admins
included, is held at a TOTP setup screen on their next request, and
sessions that are already open are held too. Until they set up TOTP,
every console and admin endpoint answers 403
totp_enrollment_required, except/api/auth/me, logout,
register-keyand the TOTP status/setup/verify-setup calls. Setting
up TOTP releases the session straight away. API keys are not affected:
gateway, MCP and consoletw-key traffic keeps working. While the
setting is on,POST /api/auth/totp/disableis refused with 400. The
setting must now be a JSON boolean; a string such as"true"is
refused on save. -
Gateway error bodies follow each client API's own format. Status
codes andRetry-Afterare unchanged. Code that reads the error
typeneeds updating:- Chat Completions and Responses keep the
{"error": {"message", "type", …}}shape, buttypeis now
OpenAI's value for the status, not a ThinkWatch tag:
authentication_error(401),permission_error(403),
not_found_error(404),rate_limit_error(429),
invalid_request_error(other 4xx) andserver_error(5xx). The
old tags are gone:rate_limited,policy_blocked,
provider_http_error,provider_error,provider_timeout,
transform_error,network_errorandauth_error. A policy block
is nowpermission_errorwith status 403. - Anthropic Messages clients get Anthropic's body,
{"type": "error", "error": {"type", "message"}}, with Anthropic's
type names (rate_limit_error,overloaded_error,api_error, …). - Gemini clients get Google's body,
{"error": {"code", "message", "status"}}. - Once a stream has started, a Responses client gets a
response.failedevent, where before it got a Chat-style error
frame that SDKs skip, so the stream just stopped. An Anthropic
client gets anerrorevent whose type follows the status. - The
error_typefield ingateway_logsand the metric labels are
unchanged.
- Chat Completions and Responses keep the
-
Only upstream failures fail over or count against a route's
breaker. In 1.x every non-2xx except 401, 403 and 429 was retried on
the model's other routes and counted as a failure on each of them, so
one malformed request could open the breakers on all of a model's
routes.- Tried on another route and counted against this one: 5xx, 408,
429, 401 and 403 (the upstream refused the gateway's own
credential), timeouts, network errors and unreadable responses. - Returned to the caller straight away, and counted as the upstream
working: every other 4xx. - What the caller sees: such a 4xx comes back with its own status
and the upstream's reason. In 1.x it came back as a 502 after every
route had been tried. - An upstream 5xx comes back with the upstream's status (500, 503, …)
rather than a blanket 502. - An upstream timeout is now 504.
- Streams follow the same rule. A stream cut by tool-call inspection
no longer counts against the route.
- Tried on another route and counted against this one: 5xx, 408,
-
Cached input is billed at cache prices. In 1.x cache reads and
writes were billed, and debited from budgets and weighted rate limits,
as full-price input.modelsgains three weights,cache_read_weight,
cache_write_weightandcache_write_1h_weight. When a weight is
unset, it isinput_weighttimes Anthropic's ratio: 0.1× for a read,
1.25× for a write and 2× for a one-hour write. What this changes:- Traffic with many cache reads (Claude Code, for instance) costs much
less than it did. - Traffic that writes to the cache costs a little more.
- Older OpenAI models discount cache reads less (0.5× or 0.25×). Set
the weights on those models yourself. input_tokensin the log is still the whole input. The log detail
gainscache_read_tokens,cache_write_tokensandcache_write_1h.
- Traffic with many cache reads (Claude Code, for instance) costs much
-
Output length limits now apply to streams. In 1.x,
max_length
output guardrails checked only whole responses, so streamed answers
were never checked. Now the frame that would cross the limit is not
sent, and the stream ends with an error in the caller's format. A
response served from the cache is also checked against the limit in
force. If you set a limit, streamed answers that used to go through
can now be cut off. -
Content-filter preset groups are renamed. The groups are now
injection,personaandchinese; they used to bebasic,
strictandchinese. This matters only if you call the presets
endpoint by group id. Rules you have already added are copies and are
not affected. Other changes to the filter:- A rule with an empty pattern is now refused on save.
- Each text part of a message is scanned separately, so a pattern no
longer matches across two parts. - The engine is now shared with ThinkWatch-Core's
tw-guard. The
stored format and the admin API are unchanged.
-
Requests with no usage report are billed on an estimate. In 1.x
such a request was billed at zero. This happens when an upstream
ignores the request for usage, or when the caller leaves before the
final chunk arrives. The estimate is:- input: about four bytes of the request per token, not counting
images and files; - output: the answer that actually arrived.
Estimated rows carry
usage_estimated: truein their detail and count
ingateway_usage_estimated_total. A request with no answer at all is
still billed at zero. - input: about four bytes of the request per token, not counting
-
Clients that leave early are now logged. In 1.x a client that
disconnected before its response existed left nogateway_logsrow at
all. That covers leaving during auth, limits or routing, or while
waiting for a whole (not streamed) answer. Such a request now writes
one row: status 499,stream_outcome: client_cancelled,
cancelled_before: response, no tokens and no cost. Expect more 499
rows in dashboards and log forwarders. A new counter,
gateway_cancelled_before_response_total, counts them.
Database changes
Both apply on their own at startup, as every schema change does, and
both are additive:
modelsgains three nullable columns:cache_read_weight,
cache_write_weightandcache_write_1h_weight
(ALTER TABLE … ADD COLUMN IF NOT EXISTS,CHECK (>= 0)).system_settingsgets anauth.default_rolerow, seeded empty (no
role). Existing rows are left alone (ON CONFLICT DO NOTHING).
Neither is irreversible. A 1.1.0 server runs against the upgraded
database: it ignores the new columns and the new setting. What a 1.1.0
server cannot do is price cache tokens from the weights.
Added
-
Gemini clients. New endpoints:
POST /v1beta/models/{model}:generateContentand
:streamGenerateContent, also served under/v1/models/…;GET /v1beta/models, which lists models in Gemini's format.
These requests get the same limits, budgets, filters, routing with
failover, format conversion, inspection, billing and audit as every
other endpoint. A Gemini upstream gets the request as it was sent.
A stream comes back as SSE withalt=sse, and as Gemini's JSON array
without it.:countTokensand:embedContentare refused with 400. -
The Responses API over a WebSocket. Connect to
GET /v1/responses
withUpgrade: websocket.- Each
response.createframe is handled like a streamed
POST /v1/responses, with its own limits, routing, billing and
audit row. - Turns on one connection run in order. A refused turn fails with
response.failed, and the connection stays open. - The connection keeps its latest response. That lets a turn continue
from it withprevious_response_id, even withstore: false,
which is how Codex works, and against any upstream format. - A new counter,
gateway_responses_ws_connections_total, counts
connections.
- Each
-
More places to put an API key. Gateway keys are also accepted in
x-api-key(Anthropic SDKs),x-goog-api-keyand?key=(Gemini
SDKs), as well asAuthorization: Bearer. Headers are checked first.
A key given in the query string is never sent upstream. -
Hidden-text audit events show what the text says. Each item in
foundgainsrevealed, the ASCII that the hidden tag characters
spell. -
auth.default_rolecan be set. It is the role that newly
registered users and SSO users get. In 1.x, setting it through the
admin API reported success but changed nothing, because the setting
row did not exist. -
Model editor fields for the three cache weights. Each placeholder
shows the value used when the field is left empty.
Changed
- Default output length for upstreams that require
max_tokens. When
the caller sets none, the gateway now sends 32000 for Claude models and
8192 for other models. It used to send 4096, which cut Claude answers
short. - More upstreams count as the vendor's own endpoint. DeepSeek,
Moonshot, Zhipu/Z.ai, DashScope, xAI and*.amazonaws.comare now
recognised, and the check reads the parsed host. A relay URL such as
https://relay/api.openai.comno longer passes as official. Official
endpoints are stricter about request parameters, so the gateway drops
or renames some parameters before sending to them. - Hidden-text scanning uses ThinkWatch-Core's
tw-guard. Same
scope, same actions, and nothing is stripped. - Requests forwarded in their own format lose ThinkWatch's reasoning
signatures. Atw1.signature written by an earlier format
conversion is removed, because Anthropic rejects it. The upstream's
own signatures are kept. - Core crates:
tw-dialect,tw-guardandtw-breakerat
ThinkWatch-Core v0.43.0. The code only this edition used (at-rest
crypto, SigV4 signing, the gateway error type) moved into this
repository. It works the same, and stored secrets decrypt as before. - The server's SQL moved from the request handlers into repository
modules (catalog, dashboard, limits, log forwarding, identity,
access, MCP). Every statement is unchanged. New integration tests cover
these endpoints and pass on both the old and the new code. - CI runs on pull requests into
dev, including the whole
integration suite against Postgres, Redis and ClickHouse.
Fixed
- Revoking a user's default MCP connection always failed with a 500,
and the account could not be revoked. The newest remaining account is
now made the default.