跳转到正文
ThinkWatch
开始输入以搜索文档。

更新日志

ThinkWatch 发布说明

ThinkWatch 企业版、ThinkWatch Lite 与 ThinkWatch Core 各版本的发布说明,按时间倒序排列。可通过 RSS 订阅。

最新版本

2026年10月

  1. ThinkWatch Lite v2026.10.18 GitHub ↗

    升级须知:内置 core 升级到 0.72.0,控制面协议仍为 47,运行 0.71.0 的远程服务器可以继续连接。请求记录和设置均保留。

    修复:工具调用的参数中,网址紧跟在中文等非 ASCII 字符之后(如「文档见:https://…」)时,回答不再中途断开。此前工具调用审查(默认处于观察档)在回答流式输出时遇到这类文字会出错,Codex 报 stream disconnected before completion,请求记录为被网关内部的错误中断。网关自身代码在请求中出错时,请求的错误信息现在会写明出错的原因和位置。

    出站脱敏(替换档):回答中回显的私钥还原后为真正的换行,不再是字面的 \n。工具调用参数中的值在流式回答、完整回答、重放和 WebSocket 连接中都能正确还原,参数仍可解析。Codex 的自由格式工具输入(如 apply_patch)在流式输出时即可还原;WebSocket 上被切在两条消息之间的占位符也能还原。客户端以 \u00e9、\/ 等转义写法发送的值,存档的请求中同样显示上游收到的占位符。

  2. ThinkWatch 企业版 v3.7.1 EN GitHub ↗

    Tool-call inspection no longer crashes on a URL written right after a non-ASCII character, which cut streamed answers off, and redacted values come back exactly as the caller sent them. The thinkwatch-core crates move from v0.71.0 to v0.72.0.

    Fixed#

    • A tool call with a URL right after a non-ASCII character no longer breaks the answer. Tool-call inspection, on in observe mode by default, looks for URLs in the arguments of the tool calls an upstream returns. When a non-ASCII character stood directly before a URL, as in 文档见:https://…, finding the URL’s scheme cut that character in two and the gateway panicked. A streamed answer stopped partway, with no error event and no end of stream, and the request was logged with status 499 and client_cancelled, as if the client had left; on the Responses WebSocket the whole connection closed. A whole answer was refused with a 500 whose body is Service panicked, and logged with status 499 as cancelled before the response. No tool-call event was written for that call. Rust’s panic message (… is not a char boundary …, at tw-guard’s tools/net.rs) went to standard error, and for a whole answer the gateway’s log also has a Service panicked error. Affected since 3.0.0.

    • Redacted values come back exactly as the caller sent them. With outbound redaction in enforce mode, a value is now remembered as it reads rather than as the request’s JSON wrote it, and is escaped for each place it goes back into. A value holding characters that JSON writes with an escape, such as the line breaks of a private key, a quote or a backslash, came back wrong:

      • In a streamed answer, put back into text (the answer’s own, or the input of a freeform tool such as Codex’s apply_patch), a private key arrived with \n in place of each line break.
      • In a whole answer, including one served from the cache, such a value inside a tool call’s arguments, which are JSON written into a JSON string, was escaped once instead of twice, so the arguments the client received did not parse, or parsed to a different value.

      A placeholder split across two pieces of a freeform tool’s streamed input (response.custom_tool_call_input.delta) also reached the client as written; it is now put back.

    Changed#

    • thinkwatch-core crates (tw-bedrock, tw-breaker, tw-dialect, tw-guard) v0.71.0 → v0.72.0. Only tw-guard changes.
  3. ThinkWatch Core v0.72.0 EN GitHub ↗

    This release fixes a crash that cut off streamed answers whose tool calls had a URL right after a non-ASCII character, records the cause when the gateway’s code fails during a request, and makes redacted values come back exactly as they were in every kind of answer.

    Upgrade notes

    • The control-plane protocol version stays at 47; ThinkWatch Lite releases that include 0.71.0 connect to 0.72.0 and the other way round.
    • The request store’s schema and the configuration are unchanged; nothing is cleared.
    • New message code: gw.internal_panic.
    • For crates that embed the gateway: tw_guard::redact::replace::restore_json is removed (restore now escapes each value as deeply as it sits), and the ledger (Ledger::table, replacements, placeholder_of) holds decoded values. New: replace::Form, json_escaped, apply_text, restore_text, stream::Restorer::text, Restorer::fresh_as, Restorer::rebook and sse::FrameRestorer::rebook.

    Tool calls with a URL after non-ASCII text no longer break the stream. Tool-call inspection looks for URLs in a tool call’s arguments while they stream, and it is on in observe mode by default. Finding a URL’s scheme cut a multibyte character in two when one stood right before the URL, such as Chinese text followed by a full-width colon and a link, and the gateway’s code panicked: the client’s stream ended with a transport error and the request was recorded as interrupted by an internal error. The scheme is now found byte by byte. Masking credentials in URLs on configuration lines had the same flaw and is fixed too. The fault dates from 0.58.0.

    Internal errors say where and why. When the gateway’s code panics during a request, the request’s outcome now carries the panic’s message and its place in the source (gw.internal_panic), and the panic is logged; before, it only said the request was interrupted by an error inside the gateway. Data that the standard library prints into some panic messages is left out.

    Redacted values come back exactly. A value found in a request is now remembered as it reads, not as it was written in the request’s JSON, and is put back in the form each place needs:

    • A private key echoed in a streamed answer comes back with real line breaks instead of \n.
    • A value inside a tool call’s arguments, which are JSON text inside JSON, is escaped for every level it sits in, however deep, in streamed answers, whole answers, replays and WebSocket frames, so the arguments still parse.
    • Codex’s freeform tool input (such as apply_patch) is restored while it streams, including a placeholder cut across two pieces.
    • Over WebSocket, a placeholder cut across two frames is restored, also when the client sends a new value while the answer streams; what is still held when the upstream disconnects goes through the same renaming, reply plugins and tool-call inspection as the rest of the answer.
    • The same value seen in a request’s JSON and in the decoded text a plugin reads now has one placeholder number, and a stored request shows the placeholder upstream received even when the client wrote the value with escapes such as \u00e9 or \/. Bodies with nothing to restore are passed on unchanged, byte for byte.
  4. ThinkWatch Lite v2026.10.17 GitHub ↗

    升级须知:内置 core 升级到 0.71.0(控制面协议 47),远程服务器须同时升级到 0.71.0 才能连接。请求记录和设置均保留。此前的自定义出站脱敏规则一律按区分大小写的正则表达式匹配,升级后改为按原样查找文字、不区分大小写;依赖正则的规则仍能加载,但不再命中,原本替换的内容会原样发出。请逐条打开自定义脱敏规则,将「匹配方式」改为「正则」,需要时打开「区分大小写」。

    出站脱敏:不再查找请求中的字段名;自定义规则也不再查找协议要求原样保留的标识,如条目和调用的 id、类型、角色、工具名、模型名,内置规则照常查找。此前自定义规则的文字出现在 Codex 随历史发回的 id 中时,会替换掉 id 的一部分,上游随即拒绝该请求及此后的每一轮对话。私钥规则只在同一段文字中查找密钥块的结尾,不再把一条消息中的开头和后续消息中的结尾连成一段、将其间内容整段替换。

    自定义脱敏规则新增「匹配方式」:「包含」按原样查找文字,. * 等字符没有特殊含义;「正则」按正则表达式匹配。旁边的「区分大小写」默认关闭。测试框按选中的方式试,规则列表和安全日志详情注明每条规则的匹配方式。

  5. ThinkWatch 企业版 v3.7.0 EN GitHub ↗

    Outbound redaction keeps away from the structure of a request: it no longer looks inside field names, and custom rules leave alone the identifiers a protocol needs as they are, so a rule can no longer break the ids a client sends back with its history. A custom redaction rule now says whether it looks for its text as written or for a regular expression, and whether case matters. The thinkwatch-core crates move from v0.70.0 to v0.71.0.

    Read before upgrading#

    • Custom redaction rules saved before 3.7.0 now match their text as written, ignoring case. A custom rule in security.redact takes two new optional keys, match (contains or regex) and case_sensitive. A rule without match is looked for as plain text, in which ., *, \d and the like have no special meaning; a rule without case_sensitive ignores case. Until now every custom redaction rule was a case-sensitive regular expression, and no rule saved by 3.0 to 3.6 carries either key, including the rules an earlier upgrade converted from the old security.pii_redactor_patterns setting. A rule written as a regular expression still loads and nothing reports it, but it stops matching: what it used to replace is sent upstream as it is and, with audit.body_redact_pii on, stored unmasked. Nothing is converted automatically. Once every replica runs 3.7.0, open each custom redaction rule on the console’s security page, set Match by to Regex for one written as a regular expression, and tick Case-sensitive where case mattered. A policy written by a script or through PATCH /api/admin/settings needs "match": "regex" and "case_sensitive": true on each such rule. Settings still in the format from before 3.0.0, converted on the first start of 3.7.0, become case-sensitive regex rules and keep matching as before.
    • Upgrade every replica before editing redaction rules, and do not roll back afterwards. 3.6 refuses a redaction policy with keys it does not know. A 3.6 instance that reads a policy in which any custom rule has match or case_sensitive logs Unreadable guard policy and runs redaction on the factory policy: observe mode, the built-in rules as shipped, no custom rules, so nothing is replaced on the wire. Any change to outbound redaction saved from the 3.7.0 console, even a mode switch, writes match on every custom rule. During a rolling upgrade, edit redaction rules only once no 3.6 instance is left; to roll back to 3.6 after editing them, first remove match and case_sensitive from every custom rule in security.redact.
    • Redaction no longer searches field names, and custom rules skip protocol identifiers. No rule looks inside a JSON key any more. Custom rules also leave alone the values of id, call_id, item_id, tool_use_id, tool_call_id, toolUseId, previous_response_id, response_id, type, role, name, namespace, model, status, object and phase. Built-in rules still search those values, and every other value is searched by every rule as before, including fields such as user_id or aws_access_key_id inside tool arguments. A custom rule meant to catch a value under one of those keys, such as a tool name or a model name, no longer does. The same holds for masking stored bodies (audit.body_redact_pii).
    • The private-key rule replaces less. The header of a key block and its end line are now looked for within the same text (see Fixed). A header in one message and an end line in another are no longer replaced, along with everything between them, as one key.

    Added#

    • Match by and Case-sensitive for custom redaction rules. The custom rule dialog for outbound redaction offers Contains and Regex, as the content filter’s does, and a Case-sensitive checkbox. Contains looks for the text exactly as written; leading and trailing spaces count. The rule list says how each rule matches, such as “Contains x, ignoring case” or “Regex x, case-sensitive”. Trying a sample in the dialog uses the same settings: POST /api/admin/security/redact/test takes match and case_sensitive. Code points remain a content filter matcher and are refused for redaction.

    Fixed#

    • Custom redaction rules no longer break the ids a client sends back. Rules ran over the whole request body, field names and identifiers included, so a custom rule could match inside an id: Codex sends ids such as fco_ followed by a hexadecimal UUID, and a rule whose text can be spelled in hexadecimal turned one into fco_…<<TW_SECRET_1>>…. The upstream rejected the request, and since the history is sent again on every turn, every later turn of that conversation as well. See Read before upgrading for what is no longer searched.
    • Private keys stay within one text. The private-key rule looked for the end of a key block anywhere after its header. A header without an end in one message, such as a file read halfway or code that spells the marker, joined up with an end line in a later message, and everything in between, earlier turns included, was replaced by a single placeholder.

    Changed#

    • thinkwatch-core crates (tw-bedrock, tw-breaker, tw-dialect, tw-guard) v0.70.0 → v0.71.0.
    • The Licensor in LICENSE is now the project’s owner, named in the file, instead of “ThinkWatch Contributors”; the Business Source License 1.1 terms are unchanged.
    • A first pull request to this repository asks its author to sign the Contributor License Agreement (CLA.md) once, by a comment on the pull request; CONTRIBUTING.md explains how.
  6. ThinkWatch Core v0.71.0 EN GitHub ↗

    This release keeps outbound redaction away from the structure of a request, so a rule can no longer break the ids a client sends back, and lets a custom redaction rule look for its text as written or by a regular expression, with or without telling capital letters from small ones.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) goes from 46 to 47. ThinkWatch Lite connects only to a core with the same protocol version: ThinkWatch Lite 2026.10.16 includes 0.70.1 (protocol 46) and does not connect to 0.71.0. A server used with it stays on 0.70.1 until the app is updated to a release that includes 0.71.0; sudo twcore upgrade --version 0.70.1 --restart switches a server back.
    • The request store’s schema is unchanged; nothing is cleared.
    • Configuration: a custom redaction rule takes two new optional keys, match (contains or regex) and case_sensitive. A custom redaction rule without match is now looked for as written and ignores case; before, every custom redaction rule was a case-sensitive regular expression. Such a rule still loads and is not reported anywhere; it simply stops matching, and what it used to replace is sent as it is. Check every custom redaction rule before upgrading: one written as a regular expression needs match: regex, and case_sensitive: true to keep telling cases apart. Existing configurations load unchanged; a configuration that uses match or case_sensitive on a redaction rule does not load in 0.70.x.
    • New message codes: config.rule_match_bad, security.redact_match_bad.
    • Changed in the protocol:
      • CustomRuleSave and SecurityTestRequest: match applies to redaction rules too (contains or regex; contains when not given); new case_sensitive (off when not given).
      • Matcher: contains and regex gain case_sensitive. A custom redaction rule is shown as contains or regex; a tool-call rule reports true unless its pattern starts with (?i).
      • RuleSnapshot gains case_sensitive; a custom redaction rule’s snapshot now carries matching.

    Redaction leaves the request’s structure alone. Rules were run over the whole request body, field names and identifiers included, so a custom rule matched inside the ids a client sends with its history: Codex sends ids such as fco_ followed by a hexadecimal UUID, and a rule whose text could be spelled in hexadecimal turned one into fco_…<<TW_SECRET_1>>…. The upstream rejected the request because the id contained characters an id cannot have, and since the history is sent again on every turn, every later turn of that conversation was rejected too. No rule looks inside field names any more. Custom rules also leave alone the values a protocol needs exactly as they are: id, call_id, item_id, tool_use_id, tool_call_id, toolUseId, previous_response_id, response_id, type, role, name, namespace, model, status, object and phase. Built-in rules still search those values, since a key or a number cannot be mistaken for a hexadecimal id, and every other value is searched by every rule as before, including fields such as user_id or aws_access_key_id inside tool arguments.

    Private keys stay within one text. The private-key rule looked for the end of a key block anywhere after its first line. A header without an end in one message, such as a file read halfway or code that spells the marker, joined up with an end line in a later message, and everything in between, earlier turns included, was replaced by a single placeholder. The header and the end are now looked for within the same text.

    Match by. A custom redaction rule now says how it matches, as content rules already do: contains looks for the text exactly as written, so ., * and the like have no special meaning, and regex takes a regular expression. A rule matches regardless of case unless it sets case_sensitive: true. Code points remain a content-filter matcher only and are refused for redaction. The security log records how a custom redaction rule matched and whether it told cases apart when it found something.

  7. ThinkWatch 企业版 v3.6.0 EN GitHub ↗

    A key shown once can go straight into ThinkWatch Lite: the console’s key screens offer Add to ThinkWatch Lite, and the Configuration Guide has a tab for it. Every user, not only administrators, now sees the gateway address an administrator configured, and the model listings name only the models the calling key may use.

    Read before upgrading#

    • Model listings follow the calling key. GET /v1/models and GET /v1beta/models listed every routed model whatever the key; they now list only the models a request with that key is let through for (see Fixed). A client that lists models with a narrowed key sees fewer of them, and a key whose allow-list and owner’s roles have nothing in common lists none. No setting, schema, Helm value or Redis key changes.
    • general.public_host must be a host name or an IP address. Saving it used to accept anything without / or ://, such as gateway.example.com:8443, and the Configuration Guide then built a broken URL from it. Such a value is now refused when saved, and one already stored counts as not set: the guide uses the host the console is open at instead. If yours carries a port, move the port to general.public_port.
    • The console’s Content-Security-Policy allows frame-src thinkwatch:. The web image’s nginx now sends frame-src 'self' thinkwatch:, which Add to ThinkWatch Lite needs to hand its link to the app (see Added). A policy of your own in front of the console needs the same directive, or the button does nothing.

    Added#

    • “Add to ThinkWatch Lite” for a key shown once. The new-key and rotated-key dialogs have an Add to ThinkWatch Lite button next to Copy; at the end of first-run setup it is on its own line under the key. It opens a thinkwatch://import link with this gateway and the key, and ThinkWatch Lite (2026.10.16 or later) opens its New upstream dialog with them filled in; the upstream is created when the user confirms it there. The link is built in the browser from the key on screen and the gateway’s public address, and is sent nowhere. It is opened in a hidden frame, so a browser without Lite installed keeps the page, and the key on it, instead of showing an error page in its place. The button is unavailable, with the reason beside it, for a key without the ai_gateway surface, for a user whose roles do not grant ai_gateway:use (a key is always its creator’s, and the gateway would refuse its every request), and for a gateway whose public address Lite does not accept. Lite takes https://, and plain http:// only on this machine or a private network: private IPv4 ranges (10/8, 172.16/12, 192.168/16, 100.64/10, 169.254/16), private IPv6 ranges (fc00::/7, fe80::/10), and names with no dot or ending in .local, .lan, .internal, .intranet, .corp, .home.arpa or .localhost. Elsewhere, set general.public_protocol to https where the gateway is served over TLS. Once clicked, a line under it links to the download page. Phones get Copy only.
    • The Configuration Guide has a ThinkWatch Lite tab, first among the AI gateway clients: what Lite does, and a button that opens the new-key dialog filled in for it (named “ThinkWatch Lite”, AI gateway only). Users without api_keys:create are told how to get a key and add it by hand.
    • GET /api/gateway/info on the console port gives any signed-in user the configured parts of the gateway’s public address: {"public_protocol": "https", "public_host": "gateway.example.com", "public_port": 443, "gateway_port": 3000}. public_protocol, public_host and public_port are general.public_protocol, general.public_host and general.public_port, each null when not set (or when the stored value is not one the setting accepts); gateway_port is the port the gateway listens on. The console fills in what is not set from the address it is open at, the browser’s scheme and host, and uses gateway_port when no public port is set. The server reads no request headers for it.

    Fixed#

    • The Configuration Guide shows the configured gateway address to every user. It read the address from GET /api/admin/settings/system, which needs settings:read: everyone else got 403 and an address built from the browser’s host and port 3000, whatever general.public_* said. It now reads GET /api/gateway/info. Until that has loaded, and if it cannot be read, the examples use the browser’s scheme and host with port 3000, as before.
    • GET /v1/models and GET /v1beta/models list only the models the calling key may use: the key’s allowed_models intersected with what its owner’s roles grant, matched exactly or by prefix, the check a request for the model meets. They listed every routed model, and a request for one outside that scope was then refused. A model named in that allow-list is listed when a route serves it, so a key narrowed to gpt-4o lists gpt-4o when the route is a prefix such as the gpt- route of a provider with no routes of its own.
    • Saving general.public_host checks it (see Read before upgrading): PATCH /api/admin/settings answers 400, naming what the setting takes, for a value with a scheme, a port, a path, a user or anything else beyond a host name or an IP address. In Settings, the field’s save marker turns to an error that gives this message.

    Changed#

    • The Public Gateway URL hint in Settings says where an empty protocol or host comes from (the address the console is open at) and that port 0 is the gateway’s listening port, and names Add to ThinkWatch Lite among what uses the address.
  8. ThinkWatch Lite v2026.10.16 GitHub ↗

    内置 core 升级到 0.70.1,控制面协议仍是 46,连接 0.70.0 或 0.70.1 的远程 core 照常可用。请求记录、报文和设置都保留,不清空。

    导入链接:打开链接直接进入「新建上游」对话框并填好设置,停在连接或账号一步,不再弹单独的确认框,每一项都可以查看和修改后再创建。链接能设置对话框里各种服务能设置的内容:服务类型、认证方式、地址、协议与密钥、OAuth 与 AWS 访问密钥、请求头、转发客户端身份、并发上限、余额来源、手动添加的模型、启用的模型、模型规格、计费方式与价格。ChatGPT 与 Z.ai / BigModel 账号的链接不带凭据,打开后停在登录一步。链接不能设置出站代理、AWS profile、环境变量、别名、路由和默认上游,也不接受鉴权类请求头。每一步都写明请求和密钥将发往的主机;点击连接一步的「下一步」或开始登录之前,不写入配置,也不发出任何请求。含有当前版本不认识的参数时,显示「无法导入这条链接」并可检查更新;为旧版本生成的链接照常可用。公司内网的 http 地址(例如 ThinkWatch 企业版网关)可以导入,对话框会注明不加密。ThinkWatch 企业版 3.6.0 在新建的密钥旁提供「添加到 ThinkWatch Lite」。

    上游:自定义上游的「高级设置」中有「余额来源」。设置项多的步骤改为滚动,不再挤压:编辑时「检测连接」回到连接一节,结果完整显示;高级设置、OAuth 与 AWS 字段、ChatGPT 账号页恢复自然排列,接口地址、请求头、会话令牌、Access Token 与出站方式下的说明也恢复了。

    插件:全部停用时,升级后插件列表照样显示每个插件的名称、说明、权限与设置,不再只剩文件名。

  9. ThinkWatch Core v0.70.1 EN GitHub ↗

    This release lists disabled plugins in full again after a core upgrade.

    Upgrade notes

    • The control-plane protocol version stays at 46; ThinkWatch Lite releases that include 0.70.0 connect to 0.70.1 and the other way round.
    • The request store’s schema and the configuration are unchanged; nothing is cleared.
    • No new message codes.

    Disabled plugins. When no plugin is enabled, core does not start the plugin runtime, and the plugin list took each plugin’s name, description, permissions, request kinds, scope and settings from a cache that every core upgrade invalidates. With every plugin disabled, which is how the bundled default plugins ship, nothing rebuilt it, so after an upgrade the list showed each plugin by its id only. Core now reads these from the plugin’s approved file without starting the runtime when the cache has nothing; only the hooks a plugin exports are unknown until it runs. What is read this way is used for display only: enabling a plugin, trying it, saving its code and approving a changed file still compile it first.

  10. ThinkWatch Lite v2026.10.15 GitHub ↗

    内置 core 仍是 0.70.0(控制面协议 46),连接 0.70.0 的远程 core 照常可用。请求记录、报文和设置都保留,不清空。

    新建上游:默认窗口里每一步都不用滚动,只有模型表这类长列表在自己的框里滚。第一步的服务排成四列,搜索框在步骤条右边。ThinkWatch、Sub2API、New API / One API 只用 API 密钥,OAuth 只留给自定义;已经用 OAuth 的上游照旧可选。请求头、转发客户端身份、代理不可用时、并发上限收进「高级设置」,有设置时自动展开。点「下一步」即检测连接:通过后进入「模型」,顶部显示响应时间、发现的模型数,服务报告余额时也显示余额;没通过就留在这一步,写明原因,可以重试或仍然继续。

    账号登录:ChatGPT 的两种方式并排给出,浏览器登录可以复制链接,也可以用设备码。Z.ai / BigModel 的认证方式与站点在同一行,BigModel(中国大陆)排在前面并默认选中。登录后直接进入「模型」,顶部显示登录的账号,ChatGPT 账号还显示最紧的额度窗口;回到账号一步可以重新登录。

    编辑上游:布局与规则和新建一致,「检测连接」放在底部左侧,结果写在旁边。

  11. ThinkWatch Lite v2026.10.14 GitHub ↗

    内置 core 升级到 0.70.0(控制面协议 46)。连接远程 core 的,服务器要一并升级到 0.70.0;这一版不连 0.69.0 的服务器,2026.10.13 及更早的版本不连 0.70.0。请求记录、报文和设置都保留,不清空。

    新建上游:第一步按模型厂商、平台与中转、本机与自定义三组列出服务,各带标志和一句介绍,可以搜索;新增 OpenRouter、ThinkWatch、Sub2API、New API / One API。第二步只问所选服务需要的:认证方式按服务给出,只有一种时不显示;服务已经决定的地址、协议等直接填好或不再显示。ChatGPT 与 Z.ai / BigModel 账号在对话框内登录,说明照旧;登录链接可以复制,到常用的浏览器中打开;ChatGPT 也可以用设备码登录。连接远程 core 时 ChatGPT 只用设备码,验证网址和设备码都能复制。登录后接着设置模型与计费。

    编辑上游:按地址认出服务(Sub2API、New API、ThinkWatch 按余额来源认出),显示它的标志和名称,规则与新建一致;已经设置的值始终显示。经登录写入的 Z.ai / BigModel 上游显示为「账号登录」,可以重新登录。

    余额:上游列表的「额度 / 计费」一列显示服务报告的余额:密钥的额度与剩余、最紧的时间窗口、账户余额或已用金额,悬停可看全部窗口和确切数字。来源包括 OpenRouter、DeepSeek、Moonshot、Sub2API、New API / One API 与 ThinkWatch 企业版。企业版的密钥设了自己的限额时显示密钥的限额与用量,没设时显示账号的限额与所有密钥合计的用量,并注明是账号的。检测连接的结果里也显示余额,上游菜单中可以刷新余额;读不到时悬停可看原因,不影响请求转发。

  12. ThinkWatch 企业版 v3.5.0 EN GitHub ↗

    Limits hold where they used to leak: route caps are enforced, a limits store that cannot be read follows security.rate_limit_fail_closed, a refused model or tool no longer spends a request, a team-scoped role no longer limits the gateway it does not open, and cached answers count their tokens. A client can ask the gateway what room its API key has left (GET /v1/usage), and an API key’s own limits are edited in the console. The legacy monthly token quota is gone, and the thinkwatch-core crates move from v0.69.0 to v0.70.0.

    Read before upgrading#

    • Route caps are enforced. A route’s RPM cap and TPM cap (model_routes.rpm_cap / tpm_cap, set in the route editor) were saved but never applied. They now are, per route over a sliding minute, the TPM cap in weighted tokens like every other token limit (counted after the response). A route at either cap is skipped and the model’s next route serves the request; when every route left is at its cap, the request gets 429 with Retry-After, labelled route:requests/1m or route:tokens/1m. Such a request has passed the caller’s own limits and counts on them, as a request every upstream fails does. Review the caps already set on your routes: a cap that did nothing until now takes effect on upgrade. Routes with a cap write new Redis keys, ratelimit:{route:<route_id>}:ai_gateway:route:….
    • A limits store that cannot be read follows security.rate_limit_fail_closed. When the database failed while a request’s rate limits and budgets were being loaded, the request ran with no limits at all; when Redis failed while a budget was being read, the budget read as unspent. Both now follow the setting, as a Redis failure in the rate limiter always has, and so do the route caps: off (the default), the request goes on and a warning with the error is logged (gateway_limits_load_fail_open_total, lifecycle_budget_fail_open_total); on, the request is refused, labelled limits_unavailable, budget_unavailable or rate_limiter_unavailable (on the AI gateway, 429 with Retry-After: 30). With the setting off, a Redis error used to fail every AI-gateway request anyway, with 502 Quota exceeded, at the legacy quota check (see Changed); such a request now goes on, as the setting says. Turn the setting on to keep refusing requests while Redis is down.
    • A role granted at team scope no longer limits gateway requests. Such a grant administers that team and has never opened a model or an MCP tool at the gateways, but its rate limits and budgets were applied to every gateway request of the user who held it. A role’s limits now apply exactly where its gateway grant does: roles assigned globally, and roles attached to a team the user is a member of. A user who held a limiting role only at team scope is no longer held to its limits; assign the role globally, attach it to the team, or set a user limit if they were meant.
    • Answers from the response cache count their tokens. A cache hit counted toward request limits but added no tokens to token limits and budgets, so repeating a cached request got round them. It now adds the tokens the cached answer records, weighted like any answer (all of its input as plain input). The cost a cache hit reports stays 0. Callers that lean on the cache reach their token limits and budgets sooner.
    • Each API key and each user get day and month usage counters in Redis. An AI-gateway request made with a key now also counts on the key’s usage:{user:<owner id>}:api_key_lineage:<lineage id>:daily:<date> and …:monthly:<month> and on its owner’s usage:{user:<owner id>}:user:<owner id>:daily:<date> and …:monthly:<month>, hashes that expire two periods after their last write. That is one more Redis script per request once it passes the rate limits, and one when its tokens are counted, after the call or when it is answered from the response cache. The counters start empty, so the usage that GET /v1/usage reports counts from the upgrade on. No setting, database schema or Helm value changes.

    Added#

    • GET /v1/usage tells a client what room its API key has left. Called on the gateway port with a gateway API key, as a model request is, it answers about one of two subjects, named in scope:

      • "key" when the key has limits of its own on the AI gateway: limits lists only the key’s limits, each with used counted for the key, and usage is the key’s own (across its rotations).
      • "user" when it has none: limits lists its owner’s effective limits (roles, including those a team grants, merged most-restrictive, then the user’s overrides), each with used counted for everything the owner does, and usage is the owner’s total over all of their keys. With no limits on the owner either, limits is empty.
      {
        "scope": "key",
        "usage": {"requests_today": 12, "tokens_today": 48210,
                  "requests_month": 340, "tokens_month": 1290455,
                  "cost_usd_month": 3.82},
        "limits": [
          {"scope": "key", "kind": "tokens", "window": "daily", "window_secs": null,
           "limit": 100000, "used": 48210, "resets_at": "2026-10-12T00:00:00Z"},
          {"scope": "key", "kind": "requests", "window": "1m", "window_secs": 60,
           "limit": 60, "used": 4, "resets_at": null}
        ],
        "expires_at": "2026-12-31T00:00:00Z"
      }

      usage holds the requests the rate limits let through and the weighted tokens limits count, answers from the response cache included, for the UTC day and month, and the cost this month from the request log (null without ClickHouse). Every limit’s used is read from the counter that refuses requests, and the one with the least left comes first. window is a rate limit’s sliding window (1m, 5m, 1h, 5h, 1d, 1w, with its length in window_secs) or a budget’s calendar period (daily, weekly, monthly, with its end in resets_at, UTC). Limits on the MCP gateway are not listed. expires_at is the key’s expiry or the end of its rotation grace period, whichever comes first. A key a model request would refuse gets the same 401 or 403, and 503 means the key’s limits or their counters could not be read, whichever way security.rate_limit_fail_closed is set. Calling it charges no limit, writes no request log row and is not a use of the key: last_used_at stays as it was, so polling does not keep an idle key from its inactivity timeout. Whoever holds a key without limits of its own sees its owner’s totals and limits; a key handed to someone else should carry limits of its own. The console’s Configuration Guide lists the endpoint with the other gateway endpoints.

    • An API key’s own limits are edited in the console. The key’s edit dialog has a Limits tab: rate limits (requests or weighted tokens over 1m, 5m, 1h, 5h, 1d or 1w) and budgets (daily, weekly or monthly weighted tokens), each with what it has used, added, changed and removed through the existing limits endpoints. They follow the key across rotations. Reading needs rate_limits:read and changing rate_limits:write, in a scope that covers the key, as before; without write access the tab is read-only, and without read access the dialog is unchanged.

    Fixed#

    • A request the key may not make no longer spends a request limit. The rate-limit check, which counts the request, ran before the model check on the AI gateway and before the tool check on the MCP gateway, so a refused model or tool used up the caller’s requests limits. Access is now checked first (after the budget check, which counts nothing).
    • Route caps are applied, limits and budgets that cannot be read follow the fail-closed setting, team-scoped roles no longer limit and cached answers count their tokens: see Read before upgrading.
    • Comments that described a limits cache, a limits:changed subscriber and team-level budget caps, none of which exist, now say what happens: limits are read from the database on every request, nothing subscribes to limits:changed, and teams carry no limits of their own.

    Changed#

    • The legacy monthly token quota is removed. Every request counted its tokens into quota:{<user or key>:<model>}:used:<YYYY-MM> in Redis against a limit nothing could set, so it never refused anything. The gateway no longer reads or writes these keys; existing quota:* keys can be deleted. Monthly limits are budgets. Its metric, gateway_quota_overflow_total, is gone too: the Grafana overview panel that plotted it shows routes at their cap (gateway_route_capped_total) instead.
    • The security.rate_limit_fail_closed hint in Settings names everything the setting now covers.
    • thinkwatch-core crates (tw-bedrock, tw-breaker, tw-dialect, tw-guard) v0.69.0 → v0.70.0. The four crates do not change beyond their version.
  13. ThinkWatch Core v0.70.0 EN GitHub ↗

    This release reads each upstream’s balance: the money left on a key, the quota of a relay key and its time windows, a key’s limit and spend on OpenRouter, or the usage and limits a ThinkWatch Enterprise gateway reports for a key. It also records which upstreams were written by signing in to a Z.ai or BigModel account.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) goes from 45 to 46. ThinkWatch Lite connects only to a core with the same protocol version: ThinkWatch Lite 2026.10.13 includes 0.69.0 (protocol 45) and does not connect to 0.70.0. A server used with it stays on 0.69.0 until the app is updated to a release that includes 0.70.0; sudo twcore upgrade --version 0.69.0 --restart switches a server back.
    • The request store’s schema is unchanged; nothing is cleared.
    • Configuration: an upstream takes two new optional keys, balance: and signed_in:. Existing configurations load unchanged and read balances where the address is recognized.
    • New message codes: control.balance_none, gw.balance.rejected, gw.balance.status, gw.balance.unrecognized, gw.balance.timeout, gw.balance.connect, gw.balance.request_failed.
    • Changed in the protocol:
      • ProviderView gains balance (the latest reading), balance_setting (what the configuration says) and signed_in; ProviderInput gains balance; ProviderTestResult gains balance.
      • New types Balance, BalanceQuota, BalanceWindow, BalanceScope, Spent, SpentPeriod, BalanceSetting.
      • New event balance_updated and endpoint POST /providers/{name}/balance/refresh: 404 control.balance_none for an upstream with no balance to read, 502 with the reason when the source could not be determined because a request failed.

    Balances. An upstream’s balance: says where its balance is read from, with the upstream’s own key and proxy: openrouter, deepseek, moonshot, sub2api, newapi (New API and One API) or thinkwatch (a ThinkWatch Enterprise gateway); off reads nothing. The default, auto, recognizes OpenRouter, DeepSeek and Moonshot by their address; any other address, except Anthropic’s, OpenAI’s and Gemini’s own, is asked once whether it is a Sub2API relay, a New API relay or a ThinkWatch Enterprise gateway, and the answer is kept in memory only. ChatGPT and Z.ai accounts and Bedrock upstreams have no balance; their quota windows are reported as before.

    A reading is one or more of: money left on the key (a wallet, in its own currency), a total quota with what has been used of it, time windows with their limits, use and reset time, and the money already spent. Per source:

    • OpenRouter: the key’s own limit, if it has one, and what the key spent this month (or in total), with the key’s expiry. The account’s credits need a management key and are not read.
    • DeepSeek and Moonshot: the account balance, in its currency.
    • Sub2API: the key’s quota and its 5-hour, daily and weekly windows, or its subscription’s windows, or the wallet balance; what was spent today is the amount actually charged.
    • New API and One API: the key’s quota and use, in the unit the site displays (US dollars, yuan or tokens, read from the site’s public status); a site that does not say is marked as an unknown unit rather than dollars.
    • ThinkWatch Enterprise: the key’s limits and usage when the key has limits of its own, otherwise the account’s limits and its total over all of its keys, each limit saying which.

    Balances are read at start, at most once a minute after requests through that upstream end, every ten minutes otherwise, and on request; Check connection also reads the balance and runs the recognition for auto, waiting at most five seconds for it. A failed read keeps the previous reading and says why; reading a balance never affects forwarding. Requests go only to the upstream’s own address, through its proxy, and redirects are not followed.

    Z.ai and BigModel sign-in. An upstream written by signing in to a Z.ai or BigModel account now carries signed_in: zai or signed_in: bigmodel, and ProviderView.signed_in says so, so an app can show it as an account sign-in. Signing in again sets it on upstreams written before this release. Saving the upstream with the same key keeps it; replacing or removing the key clears it.

  14. ThinkWatch 企业版 v3.4.0 EN GitHub ↗

    Conversations with reasoning models behind Chat-format upstreams keep their reasoning from turn to turn, Gemini’s tool loops keep their thought signatures through a conversion, and a tool call that a Chat-format model writes into its answer text reaches the caller as a tool call. The thinkwatch-core crates move from v0.68.0 to v0.69.0.

    Read before upgrading#

    • Converted answers from Chat-format upstreams can carry tool calls that arrived as text before. Self-hosted and relayed Chat-format models (DeepSeek, GLM, Qwen, Llama and others) sometimes write a tool call into content in the template they were trained on instead of answering with tool_calls. On a route that converts the request to the Chat format, such a call to a tool the request defined now reaches the caller as a structured tool call, which the caller executes, where it used to be a tagged piece of prose the caller showed and did nothing with. Tool-call inspection sees these calls like any other and refuses or cuts them under the same rules; the request log’s response body records them as tool calls, and a recovered call ends the answer with the tool-use stop reason instead of stop. A request that defined no tools, a name the request did not define, and a bare JSON object in prose stay text. A Chat caller on a Chat-format route is forwarded as sent and unchanged. No setting, schema, API route, Helm value or Redis key changes.

    Fixed#

    • Gemini thought signatures are not counted as answer text. When a request’s usage has to be estimated (usage_estimated), the thoughtSignature on a Gemini thought or function call — an opaque blob of up to several kilobytes — was counted as output text; it is left out now, as the other formats’ reasoning signatures were.

    Changed#

    • thinkwatch-core crates (tw-bedrock, tw-breaker, tw-dialect, tw-guard) v0.68.0 → v0.69.0. Only tw-dialect changes:
      • Reasoning from a Chat-format upstream round-trips (tw-dialect). Reasoning a Chat-format upstream returned (reasoning_content, or reasoning as OpenRouter and vLLM call it) was dropped from the next turn’s request on every route, so a Claude Code or Codex conversation with tools against DeepSeek, which requires the reasoning of a tool-calling turn on the following requests of that turn, lost it every turn. Such reasoning is now carried in the caller’s own format with a tw1.ch. signature, read back on the next turn, and written as reasoning_content only to a Chat-format upstream; to an upstream of another format it is left out as before, and reasoning another vendor signed is still not sent to a Chat-format upstream. Consecutive assistant messages are merged before a request is written in the Chat format, so a Responses caller’s reasoning, text and tool call of one turn arrive as one message. The reasoning history a request carries back counts as its input, so the input tokens of those turns grow by it, as the upstream charges them.
      • Gemini thought signatures on function calls are kept (tw-dialect). Gemini with thinking puts a turn’s thought signature on the first functionCall part, and the conversion only read signatures from thought parts, so a converted Gemini tool loop lost the signature and with it the model’s reasoning continuity. The signature on a function call is now carried through a conversion and written back on the functionCall part; a call with no real signature gets the skip_thought_signature_validator placeholder as before. A Gemini request forwarded as sent that carries a signature from another route on a functionCall part keeps the part with the placeholder, where the part was removed and the tool loop broke.
      • Tool calls written into a Chat-format upstream’s answer text are recognised (tw-dialect): see Read before upgrading. In this edition the answer assembled for the request log and the cache of a Chat caller’s stream forwarded as sent to a Chat-format upstream keeps such a call as the text the caller received, rather than the structured call the conversion would have made of it.
  15. ThinkWatch Lite v2026.10.13 GitHub ↗

    内置 core 升级到 0.69.0(控制面协议 45)。连接远程 core 的,服务器要一并升级到 0.69.0;这一版不连 0.68.0 的服务器,2026.10.12 及更早的版本不连 0.69.0。请求记录、报文和设置都保留,不清空。

    新建上游改为先选服务类型:账号登录一组在前,API 接入一组在后,顶部可以搜索;选定后直接进入「连接」,名称、地址和协议已填好。

    路由规则新增「时段」条件:按星期和起止时间设定窗口,一条规则可以有多个窗口,可以跨夜;按 core 所在机器的本地时间判断。路由图上每个进行中的请求用各自的颜色沿整条路径移动。

    会话中的每一轮显示模型上下文窗口的占用,缓存读取单独一段;点开一轮可看到系统提示、工具定义、此前对话与最后一条用户消息各占多少(估算值)。

    所有黄色提醒都可以「忽略」,有新情况时再次出现;设置中的「重新显示」可以全部恢复。菜单栏不再因额度紧张或用完变色。

    网关:Chat 格式上游(DeepSeek、Kimi、GLM)的推理内容在格式转换后仍能回传;Gemini 函数调用上的思考签名得以保留;模型写在正文里的工具调用会被识别为正常的工具调用;压缩的请求体(gzip、deflate、zstd)与压缩的上游响应都能处理;请求排队等待密钥并发或速率名额时 15 秒后发送保活。

    去掉了无需知道的提示:账号框不再显示凭据有效期,凭据换发写回成功不再提示。

  16. ThinkWatch Core v0.69.0 EN GitHub ↗

    This release keeps a model’s reasoning intact across API formats, recognises tool calls that a Chat-format model writes into its answer text, reads compressed request bodies and compressed upstream answers, keeps a queued request’s connection alive, lets routing rules match the time of day, and tells, for every turn of a session, how full the model’s context window was and what filled it.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) goes from 44 to 45. ThinkWatch Lite connects only to a core with the same protocol version: ThinkWatch Lite 2026.10.12 includes 0.68.0 (protocol 44) and does not connect to 0.69.0. A server used with it stays on 0.68.0 until the app is updated to a release that includes 0.69.0; sudo twcore upgrade --version 0.68.0 --restart switches a server back.
    • The request store’s schema is unchanged; nothing is cleared.
    • Configuration: a routing rule’s when takes a new optional time condition (see below). Existing configurations load unchanged.
    • New message codes: gw.request.unsupported_encoding, gw.request.bad_encoding, gw.request.decoded_body_over_limit, engine.rule_time_syntax, engine.time_syntax.
    • Changed in the protocol:
      • ConditionField gains time.
      • TurnView gains context_window.
      • New endpoint GET /sessions/{session_id}/turns/{turn_id}/context (→ TurnContext).

    Reasoning across API formats. When the gateway converts between formats, a model’s reasoning now survives the round trip in the cases that used to lose it:

    • A Chat-format upstream’s reasoning_content (DeepSeek, Kimi, GLM and others) is carried to the client in its own format and sent back to that upstream on the next turn, so a conversation with tool calls keeps the reasoning the model requires. Reasoning from a Chat upstream is never sent to an upstream of another vendor, and reasoning_content is written only for reasoning that came from a Chat upstream, since OpenAI’s own API refuses unknown fields. Consecutive assistant items from a Responses client (reasoning, text, tool calls) are folded into one Chat message.
    • A Gemini model’s thoughtSignature on a functionCall part is read from upstream answers and from client history, carried through Anthropic and Responses clients, and written back on the functionCall part when the next turn reaches Gemini; the skip placeholder is used only when there is no real signature. When a Gemini client receives a streamed answer, a function call carries its signature and a thought part carries its text and signature together.
    • The cross-format test matrix now covers reasoning for every client and upstream pair.

    Tool calls written into the answer text. Some Chat-format models write a tool call into their text instead of the structured tool_calls field. When the request defined tools and the answer has no structured call, the gateway recognises <tool_call>{…}</tool_call>, DeepSeek’s own tool-call block markers, GLM’s <arg_key>/<arg_value> form and <function=NAME><parameter name="k"> blocks, truncated endings included, and turns each into an ordinary tool call with a generated id. Only names the request defined are converted; bare JSON in prose is never touched. Streaming answers buffer from the opening marker to its close; text before the marker streams as usual. Recovered calls go through tool-call inspection like any other. A Chat client talking to a Chat upstream in the same format is passed through unchanged. A program that embeds the dialect crate and only forwards a stream can keep such text as text with Session::keep_text_calls, so a copy it records matches what its client received.

    Compressed bodies. A request body sent with Content-Encoding: gzip, deflate or zstd is decoded on every route before it is read; the size limit applies to the decoded size (gw.request.decoded_body_over_limit), an encoding the gateway does not decode is refused with gw.request.unsupported_encoding, and a body that fails to decode with gw.request.bad_encoding. A compressed upstream answer, whole or streamed, reaches the client decoded. Traffic figures still count the bytes as they crossed the network.

    Keepalive while queued. A streamed request waiting for its key’s concurrency slot or for room in its rate window now receives the stream’s 200 and keepalive comments after 15 seconds, as a request waiting on an upstream already did; a limit that then refuses the request ends the stream with the same error the plain refusal would carry. The no-response timeout still starts only when the upstream request is sent.

    Routing by time of day. A rule’s when can hold time, one or more windows in the core machine’s local time, written as [days ]HH:MM-HH:MM: days are mon…sun or ranges such as mon-fri and fri-mon, omitted for every day; the start is inclusive, the end exclusive, 24:00 allowed; an end earlier than the start is an overnight window counted on the day it starts. A malformed value is refused when the configuration loads and when a rule is saved (engine.rule_time_syntax). The dry run evaluates it at the current time and names the time it saw (fri 17:30) when the condition did not match.

    Context window per turn. Every turn of a session reports the context window of the model that answered it, from the model specs known now. GET /sessions/{session_id}/turns/{turn_id}/context estimates, over the stored request body, how many tokens the system prompt, the tool definitions, the earlier history and the last user message took, with the measured input and cache-read tokens beside them, using the same estimator the gateway uses to count tokens locally. When the body is no longer stored, the answer says so instead of estimating.

  17. ThinkWatch Lite v2026.10.12 GitHub ↗

    升级须知:内置 core 升级到 0.68.0(控制协议 44),连接远程 core 时服务器也要同时升级到 0.68.0:这一版连不上 0.67.1 的服务器,2026.10.11 及更早的版本也连不上 0.68.0。首次启动时请求记录、保存的报文和安全日志会被清空。

    概览:概览页在一屏之内分为六张卡片:Token、费用、缓存命中、请求、首 token 与流量,每张给出数值和与上一个同等长度区间相比的变化。缓存命中显示输入中缓存读取、新输入与缓存写入的构成,其余卡片带有随时间变化的小图。指向任意一张小图时,所有小图同时标出同一时刻,数值显示在指针所在的卡片上。首 token 显示中位数与 95 分位,没有请求的时段以淡虚线连接。卡片上方的提示条列出需要处理的事:失败的请求、被切断的工具调用、上游的问题和无法计价的请求,没有时不显示。下方的明细表按模型、上游或密钥列出请求、token、费用、缓存命中与首 token,按模型或上游列出生成速度。

    流量:每个请求发往上游与从上游收到的数据量按网络上实际传输的大小记录,同时记录经由的代理。请求详情的「时间线」显示「出口」「上传」「下载」,流量页中悬停 token 数时显示上传与下载。「上游 › 代理」新增「流量 · 24 小时」一列,悬停时显示 7 天与 30 天的流量。

    内容:请求详情的「内容」分为「解析」与「原始报文」两种视图,请求进行中时都实时更新。「解析」以对话形式显示,回答随流式输出逐字出现,思考与工具调用的参数同样实时显示。「原始报文」显示请求行、状态行、报文头和正文,流式返回的每个事件依次追加,「跟随最新」使最新内容保持在视野内;「客户端一侧」与「上游一侧」分别显示客户端和上游与网关往来的报文,格式转换或插件改写请求时两者不同。密钥按保存时的方式替换。安全规则命中的位置在两种视图中都以安全日志的颜色标出,收起的部分中有命中时带一个圆点。

    安全日志:每条记录可在原处展开,列出规则命中的每一处及其前后文、命中时的规则,以及具体的处置:密钥被替换成的占位符、被切断的工具调用和客户端收到的提示,或客户端收到的拒绝。请求详情「时间线」中的安全记录同样可以展开。「导出」可将所选时段导出为 JSON 或 CSV。

    Codex 使用 ChatGPT 账号:已完成的轮次记为完成,并记录 token 与费用。Codex 读到回答结尾后立即断开连接,这些轮次此前被记为取消;较长的会话此前也不记录 token。

    上游:部分 OpenAI 兼容的中转和 Gemini 格式的上游在每一块流式数据中都写有 “error”: null,此前会被当成上游报错,请求在第一块就失败并转到下一个上游,转换格式的流也会报错并丢失正文;现在不再误判。

    路由:路由图上,进行中的请求沿其路径从密钥流向上游。

    菜单栏:费用只写数值,不再带「≥」「~」记号,与 token 数一样简短($41.20);点开的菜单中仍会标出含估算或只是下限的费用。

    修复:在概览页按空格键不再把窗口滚到空白处。

  18. ThinkWatch 企业版 v3.3.1 EN GitHub ↗

    Streams are logged as they ended. A caller that hangs up once it has read the whole answer, as Codex does, finished the request; an error the upstream or Bedrock reports partway through a stream is logged with the status the same error has as an answer, and counts against the route only when that answer would; a Responses WebSocket turn ends at its last event. The thinkwatch-core crates move from v0.67.1 to v0.68.0.

    Read before upgrading#

    • Request logs and error rates change for streams. Codex streams that were logged as cancelled (499) are logged as finished (200) and billed on the reported usage. Streams the upstream failed partway are logged with a 4xx or 5xx status_code instead of 200, so error rates that count status_code ≥ 400 include them; client_status keeps the 200 the caller received. No configuration, database schema, API or Helm value changes.

    Fixed#

    • A caller that hangs up on the answer’s last event finished the request. Codex closes the connection as soon as it has read response.completed, without waiting for the stream to end, and an upstream can end its stream a while after that event: the ChatGPT Codex backend often does, and so can a relay in front of it. A stream forwarded in its own format whose caller left in that window was logged as cancelled (499, stream_outcome: client_cancelled), though the caller had read the whole answer. Once the answer’s last event has gone out — Responses response.completed, response.incomplete or response.failed, Anthropic message_stop, Chat [DONE] — a caller that leaves is logged as if the stream had run to its end: 200, billed on the usage the upstream reported, or, after response.failed, the upstream’s failure (see the next entry). Converted streams were not affected: their last event is written once the upstream’s stream has ended.
    • An error the upstream reports partway through a stream fails the request. An upstream that has answered 200 can still report an error in the stream: an Anthropic error event (an overloaded_error arrives this way), Responses response.failed, an error in a Chat or Gemini chunk. When the stream then ended normally, the request was logged as a success (200) and counted as one by the route’s circuit breaker. It is now logged as the error it would have been as an answer, with what the upstream said: stream_outcome upstream_error, client_status 200 (the status the caller’s response went out with), and the status_code and error_type that answer would have had, read off the error’s own type or code. A rate limit or a spent quota is 429 (UpstreamRateLimited); a context too long or another invalid request 400, an unknown model 404, an overload 529 or 503 (ProviderHttpError); an error that names no status 502 (ProviderError). The route’s health and circuit breaker treat it as they treat that answer: a 5xx, a 429 or a refused credential counts against the route, so a route whose streams keep failing this way opens its breaker, while a request the upstream refuses, such as one past the model’s context window, does not. This holds for streams forwarded as sent and converted ones, and for a caller that leaves after the error. Billing is unchanged: such a request is billed on the usage the upstream reported.
      • Error rates that count status_code ≥ 400 now include these requests.
      • The rows of other streams that failed after the response went out as 200 — broken off, cut by tool-call inspection, refused by the upstream before its first byte — carry client_status too.
    • An exception Bedrock reports partway through a stream is logged as the same exception as an answer. Bedrock reports a failure in a stream it has started as an exception event: throttlingException when it throttles partway through, validationException, internalServerException, serviceUnavailableException and others. Each was logged as the stream breaking off in transit (502, error_type transport) and counted against the route’s circuit breaker. Each is now logged with the status the desktop gateway gives that exception and with what Bedrock said: throttling 429 (UpstreamRateLimited), an invalid request 400, a model timeout 504, the service unavailable 503, any other exception 500 (ProviderHttpError). Access denied is logged as a refused credential (UpstreamAuthError), without Bedrock’s words, which name the IAM principal, as for a 403 answer. The route’s health and circuit breaker treat it as they treat that answer (see the previous entry), so a request Bedrock refuses as invalid no longer counts against the route. What the caller receives is unchanged, and a damaged event-stream frame is still logged as transport 502.
    • A Responses WebSocket turn ends at its last event. On GET /v1/responses over a WebSocket, a turn lasted until the upstream’s stream ended, and the connection took the next turn only then, so an upstream that keeps its stream open after response.completed held up every following turn by that long. The next turn now starts at once.

    Changed#

    • thinkwatch-core crates (tw-bedrock, tw-breaker, tw-dialect, tw-guard) v0.67.1 → v0.68.0. tw-dialect changes what the gateway does, tw-guard only adds to its API, and tw-bedrock and tw-breaker do not change:
      • Long usage objects are read (tw-dialect). The usage reader skipped a usage object longer than 8 KB. The ChatGPT Codex backend’s usage carries an attribution breakdown that grows with the conversation, so behind an upstream that passes it on, a longer Codex session’s requests were billed on an estimate (usage_estimated) instead of the reported counts. Usage objects up to 1 MiB are now read. One cut across chunks is read on from where it stopped, nothing nested inside a usage object is counted as a second usage, and a stream that arrives in chunks shorter than the "usage" key no longer loses it.
      • A null error in a stream chunk is not an error (tw-dialect). Some OpenAI-compatible relays and Gemini-format upstreams write "error": null in every streamed chunk, and the Chat and Gemini stream readers took each such chunk for an error the upstream reported. A stream converted to another format ended in an error and lost its text, and for such a stream forwarded as sent, no assembled answer was cached or captured as the response body. A Gemini caller’s tool result carrying "error": null and no output went to an upstream of another format as a failed one. A null error is now ignored, and a Chat chunk’s error without a message reaches the caller of a converted stream as a fixed sentence instead of the whole chunk, which could hold answer text. The gateway’s own check for an error partway through a stream (see Fixed) ignores a null error as well.
      • tw_dialect::convert::ends_answer recognises an answer’s last event. The gateway uses it to tell a caller that left after the whole answer from one that left partway, and to end a WebSocket turn (see Fixed).
      • tw-guard adds, for the desktop gateway’s security log, the locate module, content::places, content::places_text and RulePlaces, content::Rule::draw, Ledger::placeholder_of, RuleSet::custom_pattern, tools::Rule::find_all, and the fields pattern, arguments, capped, places and path on tools::wall::Verdict. Secret redaction, the content filter and tool-call inspection find, replace, refuse and cut exactly what they did before. Verdict::arguments is a flagged tool call’s arguments with redacted values restored; the gateway does not log it, and its tool-call audit entries keep the fields they had.
  19. ThinkWatch Core v0.68.0 EN GitHub ↗

    This release counts the network traffic between the gateway and its upstreams, lets a running request’s content be watched live, records where each security hit was and what was done about it, and fixes Codex on a ChatGPT account (its finished turns were recorded as cancelled and its longer sessions had no usage) and upstreams that write "error": null in every streamed chunk.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) goes from 43 to 44. ThinkWatch Lite connects only to a core with the same protocol version: ThinkWatch Lite 2026.10.11 includes 0.67.1 (protocol 43) and does not connect to 0.68.0. A server used with it stays on 0.67.1 until the app is updated to a release that includes 0.68.0; sudo twcore upgrade --version 0.67.1 --restart switches a server back.
    • The request store’s schema goes to 27: the request history, stored payloads and the security log are cleared on the first start. Recorded requests have no traffic figures, and recorded security events have no detail.
    • No configuration key changes and no new message codes.
    • Changed in the protocol:
      • HistoryRow loses bytes and gains sent_bytes, received_bytes and egress. RequestFinished carries sent_bytes and received_bytes instead of bytes; RequestFailed and RequestCancelled carry them as optional (absent when no attempt reached an upstream). RequestRouted and in-flight requests carry egress; each attempt in the routing view carries proxy. ReplayOriginal.bytes becomes received_bytes.
      • Summary, CostBucket and CostBucketGroup gain sent_bytes and received_bytes; CostBucket also gains its token counts and the first-token ttft_p50_ms, ttft_p95_ms and ttft_samples of its bucket. CostDim gains egress (the direct group is named "").
      • New endpoints GET /latency/client (first-token latency by key) and GET /request/{id}/live (live content, an event stream; 404 control.request_not_running once the request has ended).
      • RequestDetail gains heads, upstream_request_body and upstream_response_body. response_body is now what the client received; for a converted request the upstream’s own answer is in upstream_response_body.
      • SecurityEventView gains session, sent_model, direction, locations, more_locations, rule_snapshot and outcome_detail. The live SecretsFound items, ContentMatched and ToolCallFlagged carry the same detail as detail.

    Traffic. Each request records how much it sent to upstreams and received from them: request and response bodies as they crossed the network, after the gateway’s own compression and before any decoding, summed over every attempt of a failover. It also records the proxy it went out through (egress), or none for a direct connection. The totals are in the summary, per time bucket, and grouped by model, upstream, key or proxy.

    Live content. GET /request/{id}/live streams a running request’s request and response — request and status lines, headers and bodies, the client’s side and each upstream attempt’s side — first everything so far, then each piece as it passes. Secrets are replaced and credentials masked exactly as in what is written to disk. Finished requests keep the headers, and the upstream’s side of the bodies when it differs from the client’s (a converted format, or a request with parts removed for the upstream).

    First-token latency over time. Every time bucket carries the median and 95th percentile of its first-token times, and first-token latency can be grouped by key.

    Security log detail. Every security event now says where each hit was and what exactly was done:

    • Every place the rule matched, in order, up to 50 per event (with a count of the rest): which part of the request or answer (system prompt, a message with its index and role, a tool call or tool result with the tool’s name, the answer text), its JSON path in the client’s own format, and up to 80 characters of context on either side.
    • The rule as it was when it matched: a built-in rule’s id and name with the core version that evaluated it, or a custom rule’s pattern and how it matches, so the entry stays readable after the rule is changed or deleted.
    • What was done: the placeholders a replaced credential was sent as; for a cut tool call, the tool, its arguments and the sentence the client received; for a refused request, the refusal; for removed characters, how many segments were removed.
    • The session the request belonged to and the model that was sent.

    Contexts and matches are redacted exactly like stored payloads, so no secret is written in clear. The detail is kept in the request log and expires with it. Changes to the guard settings are not written to the security log.

    Codex on a ChatGPT account.

    • Codex stops reading as soon as it has response.completed and closes the connection, without waiting for the stream to end; the ChatGPT backend often ends its stream a little later. Core reported the ending only when the upstream stream ended, so a client closing first was recorded as a cancellation, and most Codex turns appeared cancelled. Once the answer’s last frame has reached the client, a request whose client then leaves is recorded as finished, with its usage and cost.
    • The ChatGPT backend’s usage carries a per-item attribution that grows with the conversation. Usage objects were only read up to 8 KB, so longer sessions recorded no tokens and no cost. They are now read up to 1 MiB.

    "error": null in a stream. Some OpenAI-compatible relays and Gemini-format upstreams write "error": null in every streamed chunk. Core took such a chunk for an error reported by the upstream: the request failed at its first chunk and moved to the next upstream, a converted stream ended with an error and lost its text, reply plugins left those chunks untouched, and a Gemini tool result carrying "error": null was marked as failed. A null error is now ignored everywhere. An error that carries no message is recorded with a fixed sentence instead of the chunk it came in, which could hold answer text.

  20. ThinkWatch 企业版 v3.3.0 EN GitHub ↗

    Requests converted for Claude now use prompt caching, and Codex works through routes that convert its requests to another format: its tools, its history and its compactions arrive. A Responses request that only OpenAI can read — one that continues a conversation kept on OpenAI’s servers, or carries a compaction OpenAI wrote — now goes to a route that speaks Responses instead of being refused. The thinkwatch-core crates move from v0.65.0 to v0.67.1.

    Read before upgrading#

    • Converted requests to Claude are cached, and billed as cache traffic. A request converted for an Anthropic-format upstream, or for a Claude model on Bedrock that AWS lists for prompt caching (Claude 3.5 Sonnet v2, Claude 3.7 Sonnet, and Claude 4.5 and later), from a client that marks no cache breakpoints — Codex, Chat and Gemini clients, and an Anthropic client whose request goes to Bedrock without cache_control — now gets them at the end of the tools, the system prompt and the last two user turns, with the 5-minute lifetime.
      • The upstream bills the prefix a request writes at its cache-write rate (1.25× input at Anthropic) and what the next turns read back at its cache-read rate (0.1×). ThinkWatch prices and counts them the same way: cost, budgets and tokens rate limits weigh a cache write by the model’s cache_write_weight and a read by its cache_read_weight, 1.25× and 0.1× its input weight when unset. Set them on the model to match what your upstream charges.
      • A conversation of several turns costs less. A one-off request with a long prompt costs up to a quarter more on its input.
      • Prompt token counts (input_tokens on the log rows, quotas) include cached input in full, as before.
      • Requests that mark their own breakpoints, such as Claude Code’s, and requests forwarded in their own format are unchanged.
      • An upstream that refuses the added breakpoints — a 400 that names cache_control, cachePoint or prompt caching, as an Anthropic-compatible endpoint that does not know them answers — gets the request once more without them, and later requests to it for that model leave them out from the start. Each instance remembers this until a provider or model is changed or it restarts. Breakpoints a client marked itself are never taken out, and the probes that check a route’s API format go without breakpoints.
    • Rolling back with Codex sessions. A Codex session that compacted through a converted route on 3.3.0 carries a compaction that 3.2.1 refuses, so after a rollback, or on a 3.2.1 instance during the rollout, it cannot continue and needs a new session.

    Nothing else needs action: no setting, schema, API route or Redis key changes.

    Fixed#

    • Responses requests that only OpenAI can read. A Responses request that continues a conversation kept on OpenAI’s servers (previous_response_id, conversation, prompt, background), points at a stored item (item_reference) or carries a compaction OpenAI wrote was refused with 400, even on a route that speaks Responses and would have forwarded it as sent: every request was decoded for its usage estimate, and one that could not be decoded was refused. Codex sends such a compaction in every request after compacting a session through an OpenAI upstream, so the session could not go on; and a WebSocket turn naming a response other than the connection’s last one, meant to go upstream as sent, was refused too. These requests now go to the model’s routes that speak Responses, and only a model with none refuses them, with the reason. Their input estimate counts the request’s text.

    Changed#

    • thinkwatch-core crates (tw-bedrock, tw-breaker, tw-dialect, tw-guard) v0.65.0 → v0.67.1. Only tw-dialect, the format conversion, changes:
      • Prompt caching for Claude on converted requests (see Read before upgrading).
      • Codex tools. For some models Codex declares every tool in an additional_tools input item and sends no top-level tools. Those items were dropped, so a route converting to Anthropic, Chat, Gemini or Bedrock sent no tools and Codex could not read files or run commands. Every tool now arrives, and tool calls come back under the names and kinds Codex declared. Local shell calls, tool search, reasoning-effort changes and agent messages in the history are converted instead of dropped.
      • Codex compaction on converted routes. Codex compacts a long session through a provider named OpenAI by asking for exactly one compaction item, which a converted route could not give, so compacting failed. The upstream now writes a handoff summary, Codex receives it as its compaction, and later requests carry it back. The summary request is billed like any other. The summary travels in the item’s encrypted_content (tw1.c.…) base64-encoded, not encrypted; with audit.body_redact_pii on, captured bodies have it redacted like the rest of the body. OpenAI’s own compactions still cannot be read by an upstream of another format, and are refused saying so.
      • System messages in mid-conversation stay in place. A system or developer message after the conversation has started was added to the system prompt on a converted route. It now stays where it was given: a developer message for a Responses upstream, and a user turn wrapped in <system-reminder> for Anthropic, Chat, Gemini and Bedrock upstreams. The system prompt stays the same from one turn to the next, which is what keeps the prompt cache working.
      • verbosity (Chat) and text.verbosity (Responses) carry across the conversion to GPT-5 and later models. Other models still leave it out.
  21. ThinkWatch Lite v2026.10.11 GitHub ↗

    升级须知:内置 core 升级到 0.67.1(控制协议 43),连接远程 core 时服务器也要同时升级到 0.67.1:这一版连不上 0.66.0 的服务器,2026.10.10 及更早的版本也连不上 0.67.1。首次启动时请求记录和保存的报文会被清空。流开头太慢时换上游的设置由无响应超时取代:配置中仍写有 failover.next_on_slow_start 或 failover.stream_start_wait_secs 时,网关进入安全模式,按提示一键修复即可删除这两项。转发给 Claude 的转换请求开始使用提示缓存:写入缓存按缓存写入单价计费,命中按低得多的缓存读取单价计费,两者都计入请求的费用。

    无响应与中止:「设置 › 故障转移」新增「无响应超时」,默认 300 秒。上游在这段时间内没有发出任何内容时,请求自动转到下一个上游;没有可用的上游时直接向客户端返回超时错误,不再无限等待。回答已经开始输出后超时,以客户端能识别的错误结束本次回答。心跳不算内容。这一设置取代原来的「开头超时时转到下一个上游」。流量页中,进行中的请求可在菜单中「中止请求」,会话可「中止会话」以中止其中所有进行中的请求;请求详情中标明「手动中止」或「无响应超时」及等待的时长。

    Codex:经过转换格式的路由(转发给 Claude、GLM、DeepSeek 等)时,Codex 的工具全部保留。此前 Codex 对部分模型以另一种方式声明工具,这些工具被丢弃,Codex 因此提示无法读写文件或执行命令。历史记录、长会话的上下文压缩以及对话中途的指令现在也都能完整转发。转发给 Claude 的请求开始使用缓存,长会话的费用明显降低。

    上游:在上游的编辑对话框中,「模型」一节也可以手动添加模型,适用于任何上游,新建上游时同样可用;手动添加的模型标有「手动」,随上游一起保存。

    流量:过长的上游、模型和密钥名称不再撑宽表格,超出部分以省略号显示,鼠标悬停时显示完整名称;行菜单中的名称同样处理。

    ChatGPT 账号:「在其他设备上登录」恢复可用。此前 OpenAI 修改了登录响应中一个字段的格式,导致登录报错中断。

  22. ThinkWatch Core v0.67.1 EN GitHub ↗

    This release fixes two problems in 0.67.0’s handling of Codex and Claude: a compaction summary could be stored without its secrets masked, and requests converted for older Claude models on Bedrock could be refused because of the cache breakpoints 0.67.0 adds.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) stays at 43: a server can be upgraded from 0.67.0 on its own.
    • The request store’s schema is unchanged: upgrading from 0.67.0 keeps the request history.
    • The configuration format is unchanged.
    • Message codes are unchanged from 0.67.0.

    Compaction summaries are masked before they are stored. The summary 0.67.0 gives Codex when a converted route compacts a session travels base64-encoded in the item’s encrypted_content, which the masking of stored payloads could not see into. A key or other secret that appeared in the conversation could reach the stored request bodies inside it. Summaries are now decoded, masked with the same rules as the rest of the payload, and put back in place before anything is written to disk.

    Cache breakpoints for Claude on Bedrock follow AWS’s list. 0.67.0 added prompt-cache breakpoints to every converted request for a Claude model on Bedrock, including models Bedrock does not cache. They now go only to the Claude models AWS lists as supporting prompt caching: Claude 3.5 Sonnet v2, Claude 3.7 Sonnet, and Claude 4.5 and later. When an Anthropic-format or Bedrock upstream refuses a request because of breakpoints the gateway added, the request is sent once more without them, and that upstream and model get none from then on. Breakpoints a client marks itself are never removed.

  23. ThinkWatch Core v0.67.0 EN GitHub ↗

    This release stops requests from hanging on an upstream that answers and then goes quiet, lets a running request or a whole session be aborted, and makes Codex work through routes that convert its requests to another format: its tools, history, compactions and mid-conversation instructions now all arrive. Requests converted for Claude now use prompt caching. Signing in to a ChatGPT account on another device works again.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) goes from 42 to 43. ThinkWatch Lite connects only to a core with the same protocol version: ThinkWatch Lite 2026.10.10 includes 0.66.0 (protocol 42) and does not connect to 0.67.0. A server used with it stays on 0.66.0 until the app is updated to a release that includes 0.67.0; sudo twcore upgrade --version 0.66.0 --restart switches a server back.
    • The request store’s schema goes to 26: the request history and stored payloads are cleared on the first start. Recorded attempts could hold an outcome that no longer exists.
    • failover.next_on_slow_start and failover.stream_start_wait_secs are removed. A configuration that still has either fails to load and core starts in safe mode; the safe-mode repair offers to delete them. The new failover.idle_timeout_secs (default 300) replaces them.
    • Requests converted for Claude are cached. Prompt-cache writes are billed at the provider’s cache-write rate (25% above input for Anthropic) and reads at its cache-read rate (a tenth of input), and both are recorded and priced from the price table. Clients that mark their own breakpoints, such as Claude Code, are unchanged.
    • Changed in the protocol:
      • New endpoints POST /request/{id}/abort and POST /sessions/{id}/abort (→ Aborted { requests }).
      • Attempts gain the outcomes idle_timeout (with the time waited) and aborted; a request aborted by the user is recorded with the failure source aborted.
      • FailoverView gains idle_timeout_secs and loses next_on_slow_start and stream_start_wait_secs; the slow_start outcome is gone.
    • Message codes, compared with 0.66.0:
      • New: gw.upstream.idle_timeout, gw.upstream.idle_timeout_mid_stream, gw.request.aborted, control.request_not_running, control.session_not_running.
      • Removed: gw.slow_start, config.slow_start_too_short.

    No response. An upstream that accepts a request and then sends nothing used to keep it forever: there is deliberately no overall timeout, and the only check, moving on when a stream was slow to start, was off by default and never applied to the last upstream. Now, when failover.idle_timeout_secs (300 seconds by default, 30 to 3600) pass without any content — text, reasoning or a tool call; keep-alive pings do not count — the gateway gives up on that upstream. Before anything has reached the client, the request moves to the next upstream and the silent one counts as a failure for its pause; when none is left, the client gets a timeout error in its own format instead of waiting. After content has started, the answer ends with an error event the client understands. A whole (non-streamed) answer counts the same time until it arrives. A streamed answer is held for at most 15 seconds; after that the client receives its response headers and keep-alive comments, so it does not give up while the gateway is still able to move to the next upstream.

    Abort. A running request, or every running request of a session, can be aborted from the control plane. The upstream call is dropped at once, the client gets an error, the request is recorded as aborted by the user, and the upstream is not set aside.

    Codex through converted routes.

    • For some models Codex sends no top-level tools and declares every tool in an additional_tools input item. Those items were dropped, so a route converting to Anthropic, Chat, Gemini or Bedrock sent no tools at all, and Codex reported that it could not read files or run commands. Every tool now arrives, and tool calls come back to Codex under the names and kinds it declared.
    • Local shell calls, tool search, mid-session reasoning-effort changes and agent messages in the history are converted instead of dropped. text.verbosity reaches GPT-5 and later models on Chat upstreams.
    • Compacting a long session works on converted routes: the upstream writes a handoff summary, Codex receives it as its compaction, and later requests carry it back. OpenAI’s own encrypted compactions still cannot be read by other upstreams and are refused clearly.
    • Developer messages that arrive in the middle of a conversation stay where they are instead of joining the system prompt, so the system prompt stays the same from turn to turn and caching keeps working.

    Prompt caching for Claude. A request converted for Claude from a client that marks no cache breakpoints — Codex, Chat and Gemini clients — now gets them on the tools, the system prompt and the last two user turns, so each turn reads what the previous one cached. This applies to Anthropic-format upstreams and to Claude models on Bedrock.

    ChatGPT account on another device. OpenAI’s device-code answer now gives its polling interval as text ("5"), which stopped the sign-in with “could not be read”. Numbers written as text are now accepted there and in the other sign-in answers that carry durations.

  24. ThinkWatch Lite v2026.10.10 GitHub ↗

    升级须知:内置 core 升级到 0.66.0(控制协议 42),连接远程 core 时服务器也要同时升级到 0.66.0:这一版连不上 0.65.0 的服务器,2026.10.9 及更早的版本也连不上 0.66.0。请求记录保留。手动添加的模型现在会在读取配置时校验:含 * 或 ?、前后有空格、重复或超过 256 个字符的条目会被拒绝,网关进入安全模式并指出是哪个上游的哪个模型;此前的手动模型清单接受这类条目。普通的模型 ID 不受影响。

    上游:可以手动添加模型,在上游的模型列表中点「添加模型…」。上游实际提供、但没有出现在它返回的列表中的模型(例如账号后端隐藏的新模型、只列出部分模型的中转站),添加后与列出的模型一样参与路由:故障转移、别名、指定模型的路由规则、密钥的模型范围、客户端看到的模型列表、路由试算和测速都包括在内。手动添加的模型标有「手动」,可以从列表中移除,移除后可在提示中撤销;概要中与上游列出的模型分开计数。上游不返回模型列表时,同一个对话框取代原来的手动模型清单。

    菜单栏:「菜单栏」设置新增第四档「不显示」,选中后菜单栏不再显示图标。从访达或聚焦搜索再次打开 ThinkWatch Lite 即可显示主窗口,网关在后台照常运行。

    MCP:修改客户端配置(例如在 ~/.codex/config.toml 中加一行)后,此前已经发现的远程 MCP 服务器等内容不再作为新发现重复报告,只报告真正新出现的内容。

  25. ThinkWatch Core v0.66.0 EN GitHub ↗

    This release lets an upstream’s models be added by hand. An upstream that answers /v1/models but leaves out a model it serves — an account backend that hides newer models, a relay that lists only some — can now be given that model, and requests for it are routed there like for any listed model.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) goes from 41 to 42. ThinkWatch Lite connects only to a core with the same protocol version: ThinkWatch Lite 2026.10.9 includes 0.65.0 (protocol 41) and does not connect to 0.66.0. A server used with it stays on 0.65.0 until the app is updated to a release that includes 0.66.0; sudo twcore upgrade --version 0.65.0 --restart switches a server back.
    • The request store’s schema is unchanged: upgrading from 0.65.0 keeps the request history.
    • providers[].models is now checked when the configuration loads. An entry that is blank, has spaces around it, contains * or ?, appears twice, or is longer than 256 characters is refused, and core starts in safe mode with an error naming the upstream and the model; fix or remove the entry. Before, such entries were accepted as written. A configuration that never wrote models, or wrote plain model ids, loads unchanged.
    • Changed in the protocol:
      • New endpoint PUT /provider-manual-models (ManualModelsSave { provider, models, base_version } → ConfigWritten): replaces one upstream’s models added by hand, in the order given, and leaves the rest of its entry alone. An empty list removes the key.
      • ModelRow gains manual (the model was added by hand) and listed (the upstream’s own list has it).
    • Message codes, compared with 0.65.0:
      • New: config.manual_model_blank, config.manual_model_padded, config.manual_model_wildcard, config.manual_model_duplicate, config.manual_model_too_long.

    Models added by hand. providers[].models used to be a fallback, read only when the upstream did not answer /v1/models. It now always counts: its models are offered by that upstream next to the ones the upstream lists, and are the whole list when the upstream lists none. A model on both lists appears once. Everything that asks which models an upstream has treats them the same way: routing and failover, aliases, routing rules that name a model, a key’s allowed models, the gateway’s /v1/models, the dry run and the speed test. models_only still limits what is used. Changing the list takes effect on reload, without asking the upstream for its list again.

  26. ThinkWatch 企业版 v3.2.1 EN GitHub ↗

    The Helm chart and the Compose file now pull the published images: they named ghcr.io/thinkwatch/…, which this project does not publish to, so a default install could not pull. A TOTP code now logs in once, and log rows are no longer lost while ClickHouse is unreachable. Five reads from ClickHouse returned wrong or missing data and now return what is stored: the SLO latency percentiles, a route’s latency history, the times on traces and log bodies, a trace’s app events, and the log lists for a range written with a T; and a failed read is now an error or a logged warning instead of an empty answer. The server also reads from ClickHouse 26.9 and later, the console shows its own version again, and the dependencies move to current releases, several of them for security advisories. The thinkwatch-core crates move from v0.62.0 to v0.65.0, which makes outbound redaction and the content filter faster without changing what they find.

    No setting, schema or API route changes, and instances of 3.2.0 and 3.2.1 can run side by side during a rollout.

    Read before upgrading#

    • Image names in your own files. A values file or --set flag of your own that names ghcr.io/thinkwatch/think-watch-server or …-web keeps overriding the chart’s new default, and a copy of deploy/docker-compose.yml keeps the old name: change them to ghcr.io/thinkwatchproject/…. A pinned image tag has no v: 3.2.1, not v3.2.1.

    Fixed#

    • Image names. The Helm chart (image.server.repository, image.web.repository) and deploy/docker-compose.yml named ghcr.io/thinkwatch/think-watch-server and …-web, which this project does not publish to and which refuses anonymous pulls, so helm install with the default values and docker compose pull failed. Releases are published to ghcr.io/thinkwatchproject/think-watch-server and …-web, and both files now name them. The tag examples in the chart’s README and values-production.yaml.example wrote v0.2.0; published tags have no v, and the examples now say 3.2.1.
    • SLO latency percentiles. GET /api/admin/slo reported p50, p95 and p99 latencies next to zero: the percentiles kept the column’s integer type and were read as floating-point numbers. They now give the latencies in milliseconds.
    • Route latency history. The latency sparkline on the routing page (GET /api/admin/models/{model_id}/route-history) was always empty: the query’s column types differed from the ones the server read, and the failure fell back to no data. It now shows each minute’s p50 and p95, and a gap for a minute whose requests all failed before a latency was recorded.
    • Times on traces and log bodies. The trace view and the gateway and MCP log body views put the month name where the minutes belong (2026-10-06T04:October:52), because %M in ClickHouse’s formatDateTime is the month name. They now read 04:31:52.
    • App events on traces. A trace never listed app log events: the query that finds them compared a text column with a time, ClickHouse refused it, and the refusal showed as no events.
    • ClickHouse 26.9 and later. ClickHouse 26.9 compresses query results with ZSTD unless the client names a method, and the server’s client reads only LZ4, so every read failed with incorrect magic number. The server now names LZ4 on every query, which every ClickHouse version supports. Query results are also checked against the types the server reads them into, so a query whose types drift fails instead of returning garbage; every query was checked against ClickHouse 26.8 and 26.9.
    • Log rows kept through a ClickHouse outage. The log writer is meant to keep a batch that ClickHouse did not take and retry it on the next tick, up to 1000 entries per table. A batch that failed while being sent — ClickHouse restarting or unreachable, the usual case — had already been emptied, so its gateway, MCP, audit, access or app log rows were lost while the server logged retaining N entries for retry. They are now kept and retried. A batch that ClickHouse stored but whose answer never arrived is written a second time.
    • Time ranges on the log lists. On the gateway, MCP, audit, app and access log lists, a from written YYYY-MM-DDTHH:MM:SS, the form the API’s error message suggests, dropped every row of that day from items while total still counted them: the list compared the times as text. The console sends YYYY-MM-DD HH:MM:SS and was not affected.
    • ClickHouse failures no longer read as no data. The trace view, the usage-license meter (/api/admin/usage-license) and the per-period series and previous-period totals of the usage and cost statistics turned a failed ClickHouse query into an empty or zero answer, with nothing in the log. They now answer 500 and log the error. The dashboard’s request and active-key tiles and the call counts on the MCP server list still answer without the part that failed, so the page loads, and now log a warning naming the query.
    • The console’s version. The sidebar of the web console showed v0.0.0 in every released web image, because the image build could not see the version file it reads. It now shows the release.

    Security#

    • A TOTP code logs in once. A code accepted at login could log in again, with the password, for as long as it stayed valid: up to 90 seconds with the step of clock drift allowed either side, so a code read over a shoulder or a shared screen was enough. The last time step accepted for each user is now kept in Redis (totp_last_step:<user id>, for three minutes), and a code whose step is not later is refused, the code that turned TOTP on included. Logging in twice within one 30-second step needs the next code; one step of clock drift is still accepted. A refused replay is audited as auth.totp_failed with "replayed": true. Instances of 3.2.0 do not check, so this holds once the rollout is done.
    • rand 0.10.3 (RUSTSEC-2026-0097) and xxhash-rust 0.8.19 (GHSA-6g2r-675j-hx59). Neither affects how ThinkWatch uses the crate: it installs no log logger that draws random numbers, and it hashes with xxh3’s default secret only.
    • The web console’s build tools: brace-expansion 2.1.7 and 5.0.12, and ip-address 10.7.3 (Dependabot alerts on dependencies of eslint and the shadcn CLI). None of them is part of the console that is served.

    Changed#

    • thinkwatch-core crates (tw-bedrock, tw-breaker, tw-dialect, tw-guard) v0.62.0 → v0.65.0. Secret scanning and the content filter do much less work per request: they search bytes directly, skip plain ASCII where no rule can match it, and lowercase text in runs. Core tests each fast path against the code it replaced on randomly generated text, so they find exactly what they found before.
    • The bundled ClickHouse (Helm clickhouse.image, deploy/docker-compose.yml) moves from 26.3 to 26.8, both long-term-support releases. An upgrade restarts it on 26.8 with its data. ClickHouse does not promise that an older version reads what a newer one wrote, so to roll back to 3.2.0 later, keep clickhouse.image at 26.8.
    • Dependencies move to current releases, among them sqlx 0.9, clickhouse 0.15, tower-http 0.7, utoipa 6, jsonwebtoken 11, argon2 0.6, aes-gcm 0.11, p256 0.14 and totp-rs 6. Password hashes, encrypted secrets, issued tokens, request signatures and TOTP codes from 3.2.0 keep verifying, and what 3.2.1 writes verifies on 3.2.0; known-answer tests cover each.
    • Tests now hold two things that could otherwise break unnoticed: a login for an address with no account still spends the time of a password check (so its timing does not tell which addresses have accounts), and a column added to a ClickHouse log table comes with a default (without one, the previous release’s inserts into that table fail during a rolling upgrade or after a rollback).
    • The server image is built with Rust 1.99, and the web image serves from nginx 1.31 (was 1.29), built with Node 24.21 and pnpm 12. Building from source needs Rust 1.94.1 or newer (was 1.85).
  27. ThinkWatch Lite v2026.10.9 GitHub ↗

    升级须知:内置 core 升级到 0.65.0(控制协议 41),连接远程 core 时服务器也要同时升级到 0.65.0:这一版连不上 0.64.x 的服务器,2026.10.8 及更早的版本也连不上 0.65.0。请求记录保留;首次启动时为请求库补建索引,记录较多时约需一秒。

    菜单栏与托盘菜单:「打开主界面」固定为第一项,不随菜单里其他内容的多少移动。原来的标题和三个大号数字换成「今日」一栏:大字显示今日 token,下方一行是请求数、失败数和费用,右侧是按小时统计的 token 小图;顶行显示网关状态和生成速度,连接远程 core 时还显示服务器名称。上游报告的每个额度窗口各占一行,并写明多久后重置。「复制网关地址」右侧显示所复制的地址;有新版本时,「安装新版本」排在所在一组的第一项。

    性能:流量页在列表有数千条请求时打开所需的时间大幅缩短,占用的内存大幅减少,请求持续到达时也保持流畅。启动画面只需加载应用的一小部分即可出现,各页面在打开时才加载。实时更新和概览图表占用的 CPU 更少,窗口隐藏或最小化时不再重绘。网关处理大请求的速度提高约四倍,处理时不再拖慢同时在流式输出的其他回答。长会话的对话视图只获取新增的轮次,不再每次重新获取整段对话。macOS 上监听客户端配置文件时,不再因主目录下任意文件的变化而唤醒应用。连接远程 core 时,概览的各项数据改为并行获取。

    修复:转发的请求体保留客户端原有的字节和键顺序。此前删除 metadata.user_id,以及路由规则或别名修改模型、输出上限或推理设置时,会重写整个请求体,所有对象的键都按字母顺序重排。某一天保存的报文超过空间上限时,每小时的清理会删除当天全部报文(包括正在查看的会话),现在只删除当天最早的报文。最新 500 个会话之前的会话可以重新打开。「配置文件」和「版本历史」对话框读取失败时显示错误和「重试」,不再一直显示加载中。启动时菜单栏图标、托盘图标或窗口创建失败时,以提醒的形式报告,网关继续运行,不再直接退出。路由试算中轮询组各成员的份额改由网关计算给出,本轮不参与的成员不显示份额。Windows 上进行中请求状态点的光圈现在会显示动画。延迟、日期和 token 数在各处统一写法:延迟如 1182ms,日期如 10-08,token 数如 1.5k。

  28. ThinkWatch Core v0.65.0 EN GitHub ↗

    This release makes the gateway do much less work per request, keeps large requests from holding up other streams, fixes two ways stored request bodies and sessions could go missing, and stops forwarded bodies from having their keys reordered. On macOS, watching configuration files no longer wakes the process for every file change in the home folder.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) goes from 40 to 41. ThinkWatch Lite connects only to a core with the same protocol version: ThinkWatch Lite 2026.10.8 includes 0.64.1 (protocol 40) and does not connect to 0.65.0. A server used with it stays on 0.64.1 until the app is updated to a release that includes 0.65.0; sudo twcore upgrade --version 0.64.1 --restart switches a server back.
    • The request store’s schema is unchanged: upgrading from 0.64.1 keeps the request history. The first start on an existing store adds three indexes, which takes about a second for a store of 270,000 requests and grows the file by about 15%.
    • The configuration format is unchanged.
    • Changed in the protocol:
      • GET /sessions/{id}/transcript takes TranscriptQuery (from_turn, optional: only the turns from that index on). A client written for protocol 40 that sends null is refused.
      • Transcript gains total_turns and settled_turns, the number of leading turns that will not change any more; asking from settled_turns next time returns only what is new.
      • DryRunCandidate gains share: the part of a load-balance group’s requests the candidate gets with the current weights and factors. It is null outside load-balance groups and for members sitting the round out.
    • Message codes are unchanged from 0.64.1.

    Less work per request. With the default settings, a 2 MiB request from Claude Code used to cost about 45 ms of processing before its first byte went upstream, and an 8 MiB one about 180 ms; they now cost about 11 and 46 ms. The body is parsed once instead of three times, the checks for known client requests and for keys in the text search bytes directly, the content filter skips plain ASCII, and the scan that finds keys to redact no longer copies the body just to note what it found. Bodies of 1 MiB or more are analysed on a thread of their own, so answers streaming on the same worker no longer pause: the longest gap between relayed chunks while 8 MiB requests arrived fell from about half a second to under 40 ms.

    Forwarded bodies keep the client’s bytes. Taking out metadata.user_id (sent by Claude Code on every request) and applying a routing rule or alias that changes the model, output limit or thinking used to rewrite the whole body with every object’s keys in alphabetical order, tool schemas and tool inputs included. Only the changed fields are now edited; everything else reaches the upstream as the client sent it.

    Stored bodies and sessions.

    • Body clean-up no longer deletes the current day. When one day held more than body_max_bytes of bodies, the hourly clean-up emptied that day’s folder, including the session being looked at. Older days are still removed whole; the current day loses its oldest requests until it fits.
    • A session older than the newest 500 opens again; it used to be reported as not found.
    • The session list, the last-seen time of each client and the routing page’s counts use indexes instead of reading every request: the session list went from about 180 ms to about 14 ms on a store of 270,000 requests.
    • A session’s conversation is built once and then extended with new requests, instead of rereading every stored body each time: 830 ms → 6 ms for a 300-turn session.
    • Requests are recorded on a thread of their own with a queue separate from the app’s live updates, so a slow app window can no longer cause requests to go unrecorded.

    File watching on macOS. Watching a folder used to deliver every change anywhere below it, so watching the home folder for ~/.claude.json woke the process several times a second. Folders are now watched with kqueue and only their own entries count, with the files of interest watched directly for changes in place. Idle over 30 seconds, the watch plan ThinkWatch Lite uses went from 105 wake-ups to 4. Linux and Windows are unchanged.

  29. ThinkWatch Lite v2026.10.8 GitHub ↗

    升级须知:内置 core 升级到 0.64.1,控制协议仍为 40,可与 0.64.0 的远程 core 互相连接;请求记录保留。

    网关:超过 2 MiB 的请求不再被拒绝。此前对话中贴入几张截图后,请求体超过 2 MiB,之后的每次请求(包括压缩请求)都返回 413,会话无法再压缩到上限以下。网关的上限现为原定的 256 MiB,超过上限的请求按客户端自己的错误格式返回 413。请求体在来源地址和密钥检查通过之后才读取。

    更新:每日检查发现的新版本会列入通知(灰色),通知设为系统通知时同时弹出系统通知。此前只弹出一次系统通知,在通知中心清除后只剩菜单项。点击该通知打开更新窗口;发现更新的版本时,旧版本的通知随之替换。

  30. ThinkWatch Core v0.64.1 EN GitHub ↗

    This release lifts a 2 MiB limit on request bodies that the gateway never meant to have. A conversation holding a few inline screenshots passed 2 MiB and was refused from then on, compaction requests included, so the session could not be compacted back under the limit (#295).

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) stays at 40: ThinkWatch Lite 2026.10.7 connects to 0.64.1, and a server can be upgraded on its own.
    • The request store’s schema is unchanged: upgrading from 0.64.0 keeps the request history.
    • The configuration format is unchanged.
    • Message codes, compared with 0.64.0:
      • New: gw.request.body_declared_too_large, gw.request.body_over_limit, gw.request.body_unreadable.
      • Removed: gw.request.body_too_large, replaced by the first two: one for a declared length over the limit, which names the length the client declared, and one for a body found over the limit while reading, which has no size to name.

    Request size. The gateway’s only limit on a request body is now its own 256 MiB. The body used to be read by the web framework before the gateway saw the request, under the framework’s default 2 MiB limit: anything larger got a plain-text 413 Failed to buffer the request body, was not in the client’s error format, and left no record.

    The body is now read only after the source-address check, the key check and the POST check pass, so a connection that is not allowed in can no longer make the gateway hold its body in memory first. A request whose Content-Length is already over the limit is refused without reading the body. The limit is enforced on the bytes actually received, so a Content-Length that understates the body does not get around it. An oversized request is answered with 413 in the client’s own error format (request_too_large for Anthropic clients), and x-thinkwatch-error: request.

  31. ThinkWatch Lite v2026.10.7 GitHub ↗

    升级须知:内置 core 升级到 0.64.0(控制协议 40),连接远程 core 时服务器也要升级到 0.64.0;请求记录保留。opencode、Pi、oh-my-pi、Qwen Code、Grok Build 这几个把网关模型写进自己配置的客户端,升级后客户端页会提示更新一次,因为配置里现在还要写入每个模型的规格。

    客户端:接管 opencode、Pi、oh-my-pi、Qwen Code、Grok Build 时,除了模型名,还会写入每个模型的规格,取自价目表或上游页手动设置的值:上下文窗口,以及客户端用得上的输出上限、是否支持推理、是否支持图片输入。此前只写模型名,客户端一律使用自己的默认值,例如 Pi 中所有模型都按 128K 上下文且没有推理档位,opencode 不会自动压缩对话。只写入已知的项,其余仍由客户端使用自己的默认值。Qwen Code 和 Grok Build 不写输出上限,因为两者每次请求都会原样带上这个值;Qwen Code 也不写推理设置,因为它的推理设置会改变每次请求的内容。模型规格变化后(例如手动修改之后),客户端页会提示更新客户端配置,与模型清单变化时一样。

    上游:上游模型列表中的「规格…」新增「推理」和「图片输入」两项,可设为支持、不支持,或沿用价目表。

  32. ThinkWatch Core v0.64.0 EN GitHub ↗

    This release tells clients more about each model. The model listing now says, for every model it knows, how much it can write in one answer, whether it reasons and whether it takes images, besides its context window. An upstream’s model can have the last two set by hand, like the context window and output limit before.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) goes from 39 to 40. ThinkWatch Lite connects only to a core with the same protocol version: ThinkWatch Lite 2026.10.6 includes 0.63.0 (protocol 39) and does not connect to 0.64.0. A server used with it stays on 0.63.0 until the app is updated to a release that includes 0.64.0; sudo twcore upgrade --version 0.63.0 --restart switches a server back.
    • The request store’s schema is unchanged: upgrading from 0.63.0 keeps the request history.
    • The configuration format only gains fields (providers[].model_specs.*.reasoning and .image_input); every 0.63.0 configuration loads.
    • Changed in the protocol:
      • ModelRow gains reasoning, reasoning_source, image_input and image_input_source (SpecSource).
      • ModelSpecSave gains reasoning and image_input. An entry is removed when all four fields are empty. A client written for protocol 39 drops the two new fields when it saves a model spec.
    • Message codes, compared with 0.63.0:
      • New: config.model_spec_nothing_set.
      • Removed: config.model_spec_empty, replaced by config.model_spec_nothing_set because the sentence now names four fields.

    What the model listing carries. GET /v1/models adds three fields to each model in the OpenAI and Anthropic shapes, each only when it is known:

    • max_output_tokens, the most tokens one answer can have;
    • supports_reasoning, whether the model reasons;
    • input_modalities, ["text", "image"] or ["text"].

    The Gemini shape adds thinking. The values come from the same place as the context window: the upstream’s hand-written model spec first, then the price table. When neither says, the field is left out rather than guessed. Clients then use their own defaults: a client told “does not reason” hides its reasoning levels, and that is not something to guess. The price table now keeps “not stated” apart from “no” for reasoning, and reads image support from the dataset’s supports_vision.

    Reasoning and image input set by hand. providers[].model_specs takes reasoning and image_input (true or false) next to context_window and max_output_tokens, each overriding the price table on its own. PUT /provider-model-spec sets them. The speed test also uses the result: a model set by hand to reason gets room to reason in its probe request, as models the price table marks as reasoning already did.

  33. ThinkWatch 企业版 v3.2.0 EN GitHub ↗

    Redis Cluster and Redis over TLS now work, so managed Redis services can be used. Usage limits work as documented: token limits refuse, an API key’s limits count on that key, and a refused request counts against nothing. Server instances can start together against one database, a restart no longer clears captured bodies older than 30 days, and the Helm network policy allows the ports the database URLs name. The thinkwatch-core crates stay at v0.62.0.

    Read before upgrading#

    • Rate-limit windows start empty. Rate-limit counters move to new Redis keys (one hash per counter, tagged so that Redis Cluster can run them), and the counts from before the upgrade are not carried over: every window starts empty and fills from the first request after the upgrade. The old keys expire by themselves within two window lengths. Users’ budget counters are kept; API keys’ are not (see below).
    • Route health starts fresh. A route’s samples, circuit breaker and lifetime request count move to new keys for the same reason, so every route starts closed with nothing counted. The old lifetime counters never expire; redis-cli --scan --pattern 'route_health:[0-9a-f]*' | xargs redis-cli del removes them (the new keys start route_health:{).
    • An API key’s own budgets start again. 3.1.0 counted a key’s budget on its owner’s counter (budget:user:<user id>:…); 3.2.0 counts it on the key’s own (budget:api_key_lineage:<lineage id>:…), which starts at zero. A key with a monthly budget of its own can spend all of it again in the rest of the month.
    • Limits are looser while the rollout runs. Pods of 3.1.0 and 3.2.0 count rate limits and route health on different keys, so each sees only its own version’s requests: limits let more through, and circuit breakers can disagree, until the last 3.1.0 pod is gone.
    • Token limits start refusing. A tokens rate limit never refused a request before. It now does once its window is full, so a deployment with token limits will see 429s where it saw none.
    • A key’s limits no longer replace its owner’s. A rate limit or budget set on an API key used to take the place of the owner’s limit for the same window or period. Both now apply, each on its own counter: a key’s limits can narrow what its owner may do through that key, never widen it. A key given a higher limit than its owner to give it more room needs the owner’s limit raised instead.
    • rediss:// connects over TLS. 3.1.0 connected to a rediss:// URL over plain TCP. A rediss:// URL pointing at a port without TLS now fails at start: point it at the TLS port, or write redis://.
      • The certificate must name the host in a subjectAltName. One that names it only in its CN, which redis-cli accepts, is refused.
      • Cluster nodes that announce IP addresses their certificates don’t name can’t be reached.
    • budget_unavailable in dashboards. With Redis down and security.rate_limit_fail_closed on, a request that has a budget is refused as budget_unavailable, not rate_limiter_unavailable: budgets are checked first.
    • Upgrade with an ordinary rollout. Earlier versions don’t take the lock that now makes instances set up the schema one at a time, so don’t restart instances of the old version while the first one of this version starts.
    • No transaction-mode pooler in front of Postgres. Schema setup now holds a Postgres session-level advisory lock, which a pooler in transaction mode (PgBouncer pool_mode = transaction) can leave held, and every later start then waits for it: point DATABASE_URL at Postgres itself or at a pooler in session mode (the Helm chart’s README has the details).

    Fixed#

    • Token limits refuse requests. A tokens rate limit never refused anything, and stopped counting once a request would have taken it past its limit. A request is now refused once the window’s recorded usage reaches the limit, and every request’s tokens are recorded after it, even past the limit — a window can overshoot by what was in flight when it filled.
    • Several request limits at once. With two or more requests limits on a user (per minute and per hour, say), every request that passed was counted twice, and only one of the limits could refuse; with security.rate_limit_fail_closed on, every request was refused as rate_limiter_unavailable.
    • An API key’s limits count on that key. They were counted on its owner’s counter, which every key of the owner shared, and the usage the console reads for a key (/api/admin/limits/api_key/{id}/usage) was always 0. Each key now has counters of its own, rate limits and budgets alike, for the gateway and the MCP gateway, and its usage shows what it used.
    • A refused request counts against nothing. A request refused by a spent budget, or by one rate limit after another had passed, was still counted against the request limits. Budgets are now checked first and every rate limit in one step, so a refused request leaves every counter as it was.
    • Retry-After says when to retry. A 429 from the gateway’s own limits said Retry-After: 30 whatever the limit. It now gives the seconds until the window has room for another request, or until a spent budget’s period ends (the next midnight, Monday or 1st of the month, UTC). A spent budget also sends x-should-retry: false, so the OpenAI and Anthropic SDKs don’t retry it by themselves. The body stays in the caller’s API format.
    • Redis Cluster. The rate-limit, route-health and quota scripts touched keys of several hash slots, which a Redis Cluster refuses: on a cluster, rate limits silently stopped applying (or refused every request with security.rate_limit_fail_closed), circuit breakers never tripped, and cache invalidation reached one node only. Every key a script touches now shares a hash tag, pattern deletes scan every node, and the Helm chart’s README describes a redis-cluster:// URL.
    • Redis over TLS. The server was built without TLS for Redis: a rediss:// URL was used as plain TCP, so against a Redis that requires TLS — ElastiCache with in-transit encryption, Upstash, Azure Cache for Redis and Redis Cloud among them — the server did not start (Failed to connect to Redis: Protocol Error: Expected string.). rediss:// and rediss-cluster:// URLs now connect over TLS and check the certificate against the system’s CAs, as upstream HTTPS does. For a Redis whose certificate a private CA signed, REDIS_CA_CERT names a PEM file with that CA, which is then trusted alone; the Helm chart sets it from a Secret given in redis.caSecret. The chart’s README describes both.
    • Several instances starting at once. Server instances starting together against one database — a Helm replicaCount above 1, a rolling upgrade, an autoscaler adding pods — applied the schema side by side, and all but one could exit with Database migration failed: apply db/schema.sql: … deadlock detected (on an empty database: duplicate key value violates unique constraint "pg_extension_name_index"). With ClickHouse, the rollups that an instance fills from the logs when it finds them empty (cost_rollup_hourly, provider_health_5m, mcp_server_call_counts) could be filled by each of them, counting every request once per instance on the cost pages, the dashboard and the MCP server list. Instances now set up Postgres and ClickHouse one at a time, under Postgres advisory locks: the others wait, logging Another instance is setting up the database schema; waiting for it to finish, then find it done. An instance that dies holding a lock releases it with its connection. The Helm chart’s startup probe allows a pod 125 s to start instead of 35 s, set in startupProbe (the chart’s README says when to raise it), and an attempt to reach a ClickHouse that doesn’t answer gives up after 5 s instead of the system’s TCP timeout.
    • Captured bodies kept as long as configured. With ClickHouse and audit.body_retention_days above 30, every server start could clear the captured request and response bodies older than 30 days (gateway_logs.request_body / response_body, mcp_logs.tool_arguments / tool_result; the rows themselves stayed). The start-up table setup set those columns’ TTL to 30 days each time, and ClickHouse applies a TTL to the data already stored as soon as it is set, before the server put the configured TTL back a moment later. The setup now gives these columns a TTL only when it creates them, so a restart leaves the configured one in place. Bodies already cleared cannot be recovered. The log tables’ own TTLs (data.retention_days_*) were not affected.
    • Helm network policy and databases on other ports. With networkPolicy.enabled, the server could reach PostgreSQL only on 5432, Redis on 6379 and ClickHouse on 8123, whatever their externalUrl said, so a database on another port was blocked — Azure Cache for Redis over TLS (6380), a managed Postgres or a ClickHouse on a port of its own: the server could not start, or started without writing to ClickHouse. The allowed ports now follow postgres.externalUrl, redis.externalUrl and clickhouse.externalUrl: every port a URL names, and the client’s default for its scheme where it names none. networkPolicy.extraEgress adds egress rules as written, for ports no URL names (Redis Cluster nodes announcing other ports, an upstream or MCP server on a port other than 443). The chart’s README describes both. Port 9000, ClickHouse’s native protocol, is no longer allowed: the server reaches ClickHouse over plain HTTP only (an http:// URL; HTTPS is not supported). An S3 endpoint on 9000 (RustFS, MinIO) configured outside the chart needs a rule in networkPolicy.extraEgress.
  34. ThinkWatch Lite v2026.10.6 GitHub ↗

    升级须知:内置 core 升级到 0.63.0(控制协议 39),连接远程 core 时服务器也要升级到 0.63.0;请求记录保留。手动编辑的配置中,策略组若把同一个上游写了两次,或写了不是上游的名字,将无法加载,安全模式会指出是哪个策略组和成员。密钥的并发上限现在计入流式回答的全过程,达到上限的密钥排队会比以前多。

    策略组:轮询策略组的成员可以设权重(1 到 100),并选择分配依据:按比例、按速度、按稳定性或按速度和稳定性。速度看首 token 时间,稳定性看最近的成功率,两者都在权重的基础上调整;测量不足的上游按中等水平计。进行中的对话留在原来的上游,并计入它的份额。路由试算列出每个成员的权重、速度、成功率和占比,并能预告下一个请求发往哪个上游。

    上游:可以为上游设并发上限。进行中的对话等待空位,其他请求转到下一个上游;所有上游都满时请求会等待,等不到则以明确的提示失败。上游模型列表中的「规格…」可以手动设置模型的上下文窗口和输出上限,用于价目表中没有或数值不准的中转站,手动值有标记并取代价目表的值。体检发现的问题(回答中的模型名不同、报告的输入偏高或偏低、缓存读取偏少)改为直接标在上游所在行,单独的「体检」标签已去掉。

    故障转移设置:新增「开头超时时转到下一个上游」(默认关闭),流式回答在等待时间内没有任何内容时转到下一个可用的上游,最后一个上游照常等待。新增「最多等待空位」(默认 30 秒),即一个请求等待已满的上游或密钥的分钟、小时上限合计的最长时间。

    密钥:可以为密钥设用量上限,按分钟、小时、天、周或月限制请求数、token 数或费用(美元),所有上限都要满足;token 上限可选是否计入缓存读取;天、周、月分别在本地零点、周一和每月 1 日重置。密钥对话框显示每条上限的用量和重置时间,并列出这把密钥可用但没有价格的模型,这些模型按 0 计费。达到上限的密钥在列表中有标记,用到 80% 和达到上限时各有一条通知。

    流量:尝试链显示因开头超时被放弃的上游及其可能已计费的输入 token、因并发已满被跳过的上游,以及等待空位的时间。因密钥用量上限或所有上游都已满而被拒绝的请求有对应的标注。Responses 的 WebSocket 连接上,每一轮现在都是一条单独的请求,带有用量和费用。

    其他:菜单栏的菜单改为跟随应用中选择的外观,不再随菜单栏的着色变化,深色模式下配浅色壁纸时不再弹出浅色菜单。

  35. ThinkWatch Core v0.63.0 EN GitHub ↗

    This release gives routing more to go on and keys a budget. Load-balance groups can weigh their members and favour faster or more reliable upstreams; a stream that is slow to start can move to the next upstream; an upstream can be capped at a number of concurrent requests; an upstream’s model can have its context window and output limit set by hand; and a gateway key can have usage limits per minute, hour, day, week or month. Each turn on a Responses WebSocket now counts as a request of its own.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) goes from 38 to 39. ThinkWatch Lite connects only to a core with the same protocol version: ThinkWatch Lite 2026.10.5 includes 0.62.0 (protocol 38) and does not connect to 0.63.0. A server used with it stays on 0.62.0 until the app is updated to a release that includes 0.63.0; sudo twcore upgrade --version 0.62.0 --restart switches a server back.
    • The request store’s schema is unchanged: upgrading from 0.62.0 keeps the request history.
    • The configuration format only gains fields, and every 0.62.0 configuration loads, with one exception: a group that lists the same upstream twice, or lists a name that is not an upstream, is now refused when the configuration loads (it always was when saved through the control plane). Such a configuration starts in safe mode, which names the group and the member.
    • url-test ordering can change: its samples now measure the time from sending a request to an upstream to the first content of the answer (see below).
    • A gateway key’s max_concurrent now also counts streamed answers until they end; before, a request left the count when the response started. A key at its limit therefore queues more than before.
    • Responses WebSocket connections are recorded one row per turn instead of one row per connection.
    • Changed in the protocol:
      • Groups: GroupView gains weights (every member’s weight for a load-balance group, empty otherwise) and balance_by (BalanceBy); GroupInput takes both (weights missing = all 1). The dry-run’s DryRunCandidate gains weight, ttfb_ms, success_rate and balance_factor; DryRunResult gains balance_by.
      • Upstreams: ProviderView and ProviderInput gain max_concurrent. ModelRow gains context_window_source, max_output_tokens and max_output_tokens_source (SpecSource: price_table or manual). New: PUT /provider-model-spec (ModelSpecSave → ConfigWritten).
      • Failover: FailoverView gains next_on_slow_start and slot_wait_secs.
      • Attempts: AttemptView gains queued_ms, skipped (ServeSkip::Busy) and usage (AttemptUsage); AttemptOutcome gains slow_start.
      • Keys: ClientView gains limits (KeyLimitView) and unpriced_models; KeyInput gains limits (KeyLimitInput; LimitPer, LimitMeasure). New event: Event::KeyLimitAlert.
      • A client written for protocol 38 drops weights and limits when it saves a group or a key.
    • Message codes, compared with 0.62.0:
      • New: config.key_limit_cache_reads, config.key_limit_cost_too_small, config.key_limit_duplicate, config.key_limit_empty, config.key_limit_not_positive, config.key_limit_retention, config.key_limit_two_measures, config.model_spec_blank_model, config.model_spec_empty, config.model_spec_wildcard, config.model_spec_zero, config.provider_concurrency_range, config.slow_start_too_short; control.group.balance_not_load_balance, control.group.weight_not_load_balance, control.group.weight_not_member, control.group.weight_out_of_range; engine.group_balance_not_load_balance, engine.group_unknown_upstream, engine.group_upstream_twice, engine.group_weight_not_load_balance, engine.group_weight_out_of_range; gw.busy_all, gw.busy_upstream, gw.key_limit.cost_per_period, gw.key_limit.cost_rolling, gw.key_limit.requests_per_period, gw.key_limit.requests_rolling, gw.key_limit.tokens_per_period, gw.key_limit.tokens_rolling, gw.slow_start, gw.ws.upstream_closed.
      • None removed.

    Weights and distribution in load-balance groups. A member of a load-balance group may carry a weight: providers: [{ name: relay, weight: 7 }, official].

    • A weight is a whole number from 1 to 100; a plain name is 1 and is written back as a plain name, so existing configurations stay as they are. Weights are refused on other group types.
    • Requests are spread by smooth weighted round-robin, so 7 : 3 interleaves instead of arriving in runs. A request that stays on an upstream to continue its conversation counts toward that upstream’s share, so a weight is the long-run share of requests. Paused and full members sit out; when all are, all take part.
    • groups[].balance_by adjusts the weights automatically: latency gives faster upstreams more, health gives upstreams that fail less more, latency-health both; weights (the default) uses the weights alone. The factors multiply the weights. An upstream without enough measurements counts as average, so a new one is tried without being flooded, and a flaky one still gets the occasional request, so its recovery is noticed.
    • The dry-run shows each member’s weight, time to first content, success rate and factor, and predicts the next upstream exactly as the gateway will choose it.
    • WebSocket upgrades are now ordered the same way as HTTP requests.

    What speed means. The samples behind url-test and the latency factor now measure the time from sending a request to an upstream to the first content of its answer, for streamed answers, wherever the upstream sits in the list of candidates. Before, they ran from the moment the request reached the gateway, so they included waits and earlier failed attempts, and they ended at a point that depended on the upstream’s position. url-test uses the startup connection test until an upstream has three real samples; the latency factor uses real samples only. Each upstream also keeps a recent success rate (its last 50 attempts within 30 minutes, with at least 5), classified like the failures that pause an upstream.

    Moving on from a slow start. With failover.next_on_slow_start, a streamed answer that has sent no content after stream_start_wait_secs is cancelled and the next upstream is asked, provided one can take the request at that moment; otherwise the current upstream keeps streaming. The last upstream always waits. The slow upstream is not paused. The abandoned attempt appears with the outcome slow_start and the input tokens the upstream may have billed. It is off by default; with it on, stream_start_wait_secs must be at least 5, and 30 or more suits models that think before they write.

    A concurrency limit per upstream. providers[].max_concurrent caps the requests sent to an upstream at once, for relays and accounts that limit concurrency.

    • A conversation that is using its upstream’s cache waits for a free slot, then moves on. Other requests skip a full upstream at once.
    • When every upstream that could serve a request is full, it waits for the first free slot. If none frees in time and no attempt reached an upstream, the answer is a 429 with Retry-After; otherwise the client gets the last real failure.
    • failover.slot_wait_secs (default 30, 0 for no wait) is the total a request waits, for upstream slots and for a key’s per-minute and per-hour limits together.
    • Waiting is not a failure: it neither pauses an upstream nor counts against it.

    Model specs set by hand. providers[].model_specs gives a model’s context window and output limit on that upstream, field by field, for relays whose models the price table does not know or gets wrong. The values replace the price table’s wherever core reports or uses them: the model listing in every API shape, the control plane’s model rows, aliases, the re-routing of a conversation that outgrows its context window, and the output limit filled in when a request is converted to the Anthropic format. PUT /provider-model-spec sets or clears one model’s entry.

    Usage limits per gateway key. clients[].limits lists limits that must all pass: { per: minute | hour | day | week | month, requests | tokens | cost: N }.

    • Minute and hour are rolling windows; day, week (from Monday) and month follow the machine’s local time.
    • Tokens are uncached input, cache writes and output; a token limit with cache_reads: true counts cache reads too. Cost is in US dollars, at least 0.01; models without a price and upstreams with billing: free count as 0.
    • A request in flight reserves its estimate, and its recorded usage replaces the estimate when it ends. A request that never reached an upstream counts for nothing.
    • Totals are rebuilt from the request store at startup, and when a period’s start moves, such as after a time-zone change. The store must keep rows for the whole period (retention.row_days at least 1, 7 or 31 days).
    • A day, week or month limit that is used up answers 429 with Retry-After until the reset and x-should-retry: false, so the official SDKs don’t retry it; OpenAI-format bodies carry insufficient_quota. A minute or hour limit waits within the request’s wait, then answers 429 with the exact time to retry.
    • The control plane reports each limit’s usage and reset time, and the models a key with a cost limit can reach that have no price. Event::KeyLimitAlert fires at 80% and when a limit is reached.

    WebSocket turns. Each response.create on a Responses WebSocket is now a request of its own: it has a row with the turn’s usage and cost, takes its own key permit and upstream slot only while it runs, is checked against the key’s limits, and adds to the upstream’s speed and success samples. A refused or busy turn is answered with response.failed and the connection stays usable. An upstream that closes in the middle of a turn fails it. A Realtime connection is still one row, now carrying the usage of its responses, which counts against the key’s limits when the connection closes.

    Groups list each upstream once. Loading the configuration refuses a group that lists an upstream twice, which made failover try it again and gave it extra turns in load-balance, and a member that is not an upstream, which used to drop out of the group silently.

  36. ThinkWatch Lite v2026.10.5 GitHub ↗

    升级须知:内置 core 升级到 0.62.0(控制协议 38),连接远程 core 时服务器也要升级到 0.62.0;请求记录保留。密钥的模型范围现在在上游未报告模型列表时、WebSocket 连接(如 Codex)上以及故障转移的每个备选上都会生效,此前经由这些情况使用范围外模型的客户端会被拒绝。

    模型别名:上游页新增「别名」标签。别名是同一个模型的另一个名字,列出它在各上游的名称后,提供其中任一名称的上游都能以自己的名称服务它,彼此互为备选。同一个 Claude 模型在官方、Bedrock、Vertex、OpenRouter 上名称不同时,标签页和新建对话框会建议合并,标签上的圆点表示有建议。也可以从上游的模型列表中新建别名。对话框中列出每个上游收到的名称,别名占用其他上游的模型名或被已接入的客户端特殊对待时会提示。客户端的模型列表中能看到别名,回答中带的是客户端请求的模型名。密钥的模型范围允许某个上游模型时,列出它的别名也一并允许。

    路由:规则可以转发到指定模型,即某个上游及其一个模型,按顺序尝试备选,模型名按所写发出。为上游模型写的模型条件同样匹配列出它的别名。流量页标出经由别名或指定模型发出的请求,并显示每次尝试发出的模型。

    Claude Desktop:接管遵循当前版本 Claude Desktop 的模型名规则,包括 1M 的写法。没有上游提供 Claude 模型时,接管会询问 Claude Desktop 使用哪个上游模型,并在它自己的密钥上添加一条路由规则;取消接管时删除这条规则。

    其他:工具调用检查新增内置规则「读写 ThinkWatch 自己的数据」,拦截路径或命令指向 ThinkWatch 数据目录(所有上游密钥都存放在这里)的工具调用,仅提及该目录的文档不算。发给客户端的模型列表带上各模型的上下文窗口,Claude Desktop、Hermes 等据此控制对话长度。地址末尾自带 API 版本的上游(如智谱的 …/api/paas/v4、方舟的 …/api/v3)不再被多加一段 /v1。系统通知中所有内置工具调用规则都显示名称,此前有两条显示内部 id。

  37. ThinkWatch 企业版 v3.1.0 EN GitHub ↗

    The thinkwatch-core crates move from v0.59.0 to v0.62.0. Two changes reach the gateway: tool-call inspection gains a built-in rule for ThinkWatch’s own data directory, and an upstream whose base URL already ends in an API version is no longer sent a second one.

    Read before upgrading#

    • The new tool-call rule thinkwatch-data is on and set to cut off, like the other high-risk built-in rules. A deployment whose tool-call inspection is in enforce mode starts cutting off answers whose tool call reads or changes one of the paths below; one in observe mode only records them. Set the rule to record, or switch it off, on the security page to keep the previous behaviour.

    Added#

    • Tool-call inspection: thinkwatch-data (Read or change ThinkWatch’s own data). A tool call whose path or command points into ThinkWatch’s data directory (~/.thinkwatch, %APPDATA%\ThinkWatch, /var/lib/thinkwatch, /etc/thinkwatch) — where the desktop gateway keeps every upstream key and its own protection settings. Text that only mentions the directory, such as a document being edited, does not count. The security page lists it with the other built-in rules.

    Fixed#

    • Upstream base URLs that end in their own API version (…/api/paas/v4, …/api/v3) now receive requests under that version instead of a second /v1 appended to it, which returned 404. Requests and connection tests both use it.

    Changed#

    • thinkwatch-core crates (tw-bedrock, tw-breaker, tw-dialect, tw-guard) v0.59.0 → v0.62.0.
  38. ThinkWatch Core v0.62.0 EN GitHub ↗

    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.

  39. ThinkWatch Lite v2026.10.4 GitHub ↗

    升级须知:内置 core 仍为 0.61.0,连接远程 core 时服务器无需改动;请求记录保留。

    Linux:AppImage 不再自带 Wayland 库。此前包里带着构建所用系统(Ubuntu 22.04)的 Wayland 库,并且优先于系统自带的那一份加载,在 Arch 等图形组件较新的发行版上,窗口可能一直空白。现在改用系统自带的 Wayland 库。在 Wayland 桌面上,应用改为直接以 Wayland 运行,不再经由 X11(XWayland)。

    感谢 LINUX DO 的 sunnysab(https://linux.do/u/sunnysab),他在打包这个应用时找到了这个问题的修法。

    内部改动:Tauri 升级到 2.12.1。

  40. ThinkWatch Lite v2026.10.3 GitHub ↗

    升级须知:内置 core 升级到 0.61.0(控制协议 36 升至 37),连接远程 core 时服务器也要升级到 0.61.0;请求记录保留。更新到 2026.10.2 后因 config.yaml 的 client_probes: 下仍有 passthrough 或 route 而显示「core 未运行」的,更新到这一版后会显示出错的行,并可一键修复。

    配置文件有误时不再只显示「core 未运行」:界面写明出错的文件、行号、原因和那一行的原文。core 以安全模式运行,客户端的请求暂不转发,配置仍可修改和回滚。

    新增一键修复:取值不在可选范围内的改回默认值,不认识的字段删除。修复前列出要改的各处,修复前的版本保存在版本历史中。外部编辑被拒、网关仍按上一版本运行时,提示中同样提供一键修复。安全模式下保存了可用的配置后,网关自动恢复转发。

    取值不在可选范围内、字段名拼错这两类最常见的错误,改用界面语言说明。core 仍无法启动时,界面显示 core 给出的原因。

  41. ThinkWatch Core v0.61.0 EN GitHub ↗

    This release makes safe mode work for the case it exists for: a configuration that does not load. Core serves the control plane anyway, says which line is wrong, and can repair the two commonest mistakes in one step. Before, a safe-mode core exited on the same error as a normal start, and the desktop app could only say that core had stopped.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) goes from 36 to 37. ThinkWatch Lite connects only to a core with the same protocol version. ThinkWatch Lite 2026.10.2 includes 0.60.0 (protocol 36) and does not connect to 0.61.0. A server used with it stays on 0.60.0 until the app is updated to a release that includes 0.61.0; sudo twcore upgrade --version 0.60.0 --restart switches a server back.
    • The request store’s schema is unchanged (25): upgrading from 0.60.0 keeps the request history.
    • The configuration format is unchanged.
    • Changed in the protocol:
      • New: GET /config/repair (ConfigRepairPlan → ConfigRepair: base_version and a list of ConfigFix) and POST /config/repair (RepairConfig: ConfigRepairRequest { base_version } → ConfigWritten). A web view may call both.
      • ConfigFix has kind (unknown_value or unknown_field), field (a path such as providers[0].protocol), line, value (masked) and now (the default the field goes back to, when it is a single value).
      • In safe mode, Status.config_rejected is set from the start when the configuration does not load, and Status.gateway_addr is null as before.
    • Message codes, compared with 0.60.0:
      • New: config.unknown_variant (field, value, expected), config.unknown_field (field, expected) and control.config_not_repairable.
      • A value outside its choices and an unknown field used to come as config.unparsable with serde’s sentence in detail; they now use the two new codes. Other parse errors still use config.unparsable.

    Safe mode with a configuration that does not load. twcore serve --safe used to load the configuration before anything else and exit when it did not load. It now serves the control plane with a stand-in configuration: the control key comes from the file, so a client that reads it from the same file can connect, and everything else is a default. The data plane does not start.

    • Status.config_rejected says from the start which line is wrong, in the same form as for an edit that was refused while running: the stage, the message, the line and the line itself with secrets masked.
    • Saving a configuration that loads (through the control plane, a rollback, a repair, or by editing the file) swaps it in as usual. A client then restarts core in normal mode.
    • A file in which the control key cannot be found, such as one that is not valid YAML, still makes serve --safe exit, since nobody could connect to the control plane.

    One-click repair. GET /config/repair lists what a repair would change, and POST /config/repair makes those changes and writes the file.

    • Only two kinds of mistake are repaired, and each repair removes one key. A value outside its choices, such as titling: passthrough, goes back to the field’s default. An unknown field, such as a misspelled name, is removed. A section left empty is removed with its last key. Comments, layout and every other key are kept.
    • A repair is offered only when the repaired configuration loads. A syntax error, a missing field and an error in the configuration as a whole (such as two upstreams with the same name) are left to the user.
    • The repair is computed again from the file on disk when it is applied, and refused with 409 when the file has changed since the plan. It goes through the same path as any other write, so the previous version is in the history and can be rolled back.
  42. ThinkWatch Lite v2026.10.2 GitHub ↗

    升级须知:内置 core 升级到 0.60.0(控制协议 36),连接远程 core 时服务器也要升级到 0.60.0;请求记录保留。「辅助请求」改为两档,在旧版中把某一类设为「原样放行」或「交给路由」的,config.yaml 的 client_probes: 下会有 passthrough 或 route,core 无法启动、应用进入安全模式,直到把它们改成 forward 或删掉那一行;没有改过的不受影响。发布文件名统一为 ThinkWatch-Lite-<版本>-<darwin|windows|linux>-<架构>…,已安装的应用照常更新。

    辅助请求改为「本地应答」与「转发」两档:「转发」取代「原样放行」和「交给路由」。转发的辅助请求和普通请求一样经过路由规则,规则里的「辅助请求」条件无需另外设置即可匹配,例如把生成标题分流到费用更低的上游。规则对话框会指出条件里设为本地应答、不会进入路由的类别;试算对转发的辅助请求照常计算规则。

    Windows 新增绿色版:x64、ARM64 各一个 zip,解压后运行「ThinkWatch Lite.exe」即可,不需要管理员权限。数据保存在程序旁的 data 文件夹,与安装版互不影响。自动更新同样只需确认一次,不会弹出管理员授权。

    Windows 上同一时间只运行一个 ThinkWatch Lite:打开另一个位置的程序时会说明情况,并可选择停止运行中的程序、改为启动此程序,运行中的程序会等进行中的请求结束再退出。系统通知不再依赖开始菜单快捷方式;thinkwatch:// 链接和开机启动始终指向正在运行的程序。从 Windows 设置卸载时,先还原被接管的客户端;勾选「同时删除数据」会删除配置、API 密钥和请求记录(以前这一项不会删除这些数据),清理未完成时会提示。由另一个 ThinkWatch Lite 接管的客户端会单独标出,需在接管它的程序中还原。缺少 WebView2 运行时会提示并给出下载入口。在缩放过的屏幕上,界面和系统对话框按显示器缩放显示,不再发虚。

    修复:在新版之后运行旧版时,不再清空请求记录:旧版这一次不记录,记录留给新版。

  43. ThinkWatch Core v0.60.0 EN GitHub ↗

    This release gives client helper requests two modes instead of three: answered locally, or forwarded. A forwarded helper request goes through the routing rules like any other request and carries its class, so a rule with when.intent takes effect as soon as it is written. An older core no longer clears a request history written by a newer one.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) goes from 35 to 36. ThinkWatch Lite connects only to a core with the same protocol version. ThinkWatch Lite 2026.10.1 includes 0.59.0 (protocol 35) and does not connect to 0.60.0. A server used with it stays on 0.59.0 until the app is updated to a release that includes 0.60.0; sudo twcore upgrade --version 0.59.0 --restart switches a server back.
    • The request store’s schema is unchanged (25): upgrading from 0.59.0 keeps the request history.
    • client_probes takes intercept or forward. passthrough and route are gone, and a configuration that still sets either does not load. Change them to forward, or delete the line: health checks and warm-ups default to intercept, titles, topic detection and suggestions to forward. A configuration that never set client_probes is not affected.
    • Changed in the protocol:
      • ProbeMode is intercept | forward.
      • DryRunOutcome no longer has passthrough. A dry run of a forwarded helper request evaluates the rules and ends in route, deny, no_match or unavailable, like any other request.
      • RouteSave.route_probes is removed. Saving a route no longer changes client_probes.
    • Message codes, compared with 0.59.0: control.unknown_probe_class is removed, along with route_probes. No code is added and no sentence changes.

    Helper requests. Clients such as Claude Code send requests of their own: health checks, warm-ups, session titles, topic detection and suggestions. passthrough and route both sent such a request through the routing rules. They differed only in whether it carried its class, and so in whether a when.intent condition could match it. The names suggested that passthrough skipped routing, and the dry run said so too: it stopped before the rules and reported passthrough, while the gateway evaluated the rules for that request like for any other.

    • forward replaces both. The request carries its class, so when: { intent: titling } or when: { intent: assistant_internal } matches it without any other setting.
    • intercept is unchanged: the gateway answers the request itself, nothing is sent upstream, and no rule is evaluated. A when.intent condition never matches a class set to intercept.
    • Defaults are unchanged in effect: health checks and warm-ups are answered locally; titles, topic detection and suggestions are forwarded.

    Request history across versions. Core rebuilt the request store, bodies included, whenever its schema differed from the build’s own, in either direction. An older core opening a store written by a newer one therefore cleared a history the newer version would have read again: after installing an older release over a newer one, or with a second copy of the app on the same machine that has not updated yet. Now only an older store is rebuilt. A newer one is left as it is: core records nothing while it runs, forwarding is unaffected, /storage reports recording: false, and the newer version opens the store unchanged later.

  44. ThinkWatch Lite v2026.10.1 GitHub ↗

    升级须知:内置 core 升级到 0.59.0(控制协议 35),连接远程 core 时服务器也要升级到 0.59.0;首次启动会清空请求记录,配置、密钥与上游不受影响;「隐藏字符」与「输出长度」两项防护已移除,在旧版中改过其中任何一项的(隐藏字符不是「观察」或关掉了其中一条规则,输出长度不是「关闭」或上限不是 100,000 字符),更新前先改回出厂设置,否则 core 无法启动、应用进入安全模式,直到删掉 config.yaml 中 security: 下的 hidden_text: 与 output_limit: 两段(连同其下缩进的行)。

    新增「插件」页:JavaScript 插件在请求发往上游之前改写请求,在回答交给客户端之前改写回答。插件在 core 内的沙箱中运行,不能访问文件、网络和环境变量,只看得到授予它的部分,也看不到真实的密钥:识别出的密钥在插件运行前换成占位符,运行后再换回。每个插件是一个文件,设置项、适用范围和出错时的处理方式都写在其中;编辑器分「设置」和「代码」两页、一并保存,添加插件也用同一个编辑器。插件可以在最近的请求上试运行,可以拖动调整运行顺序,可以查看日志与统计;流量页标出被插件改动过的请求,请求详情可对照改动前后。只有能改动工具调用的插件,在安装、打开、更改代码和确认文件变更时需要在系统对话框中确认。自带「指定回答语言」与「WSL 路径转换」两个插件,默认关闭。

    会话新增「对话」页,按轮还原整段对话:消息、折叠的思考、工具调用及其结果、图片;无法显示的部分注明原因。

    安全防护由五项合并为三项:出站脱敏、工具调用审查、内容过滤。隐藏字符改由内容过滤的内置规则处理,删掉这些字符而不再拒绝请求;输出长度限制移除。内容规则可以拒绝请求、删除命中的内容或只记录,也可以按码位匹配;自定义脱敏规则可以命名占位符;新增邮箱与中国大陆手机号两条内置脱敏规则,默认关闭;工具调用审查新增两条规则:把凭据发往其服务商以外的主机,以及把本地文件上传到远程主机。

    请求与回答报文存储时已替换掉识别出的密钥;回答最多保存 4 MiB(原为 256 KiB),报文存储总量默认 5 GiB(原为 2 GiB);超过 7 天、90 天或 2 GiB 的保留设置现在生效。上游返回错误(如 400)的请求现在记为失败,原因取自上游的返回。

    修复:Windows 上不再需要 Visual C++ 运行库才能启动网关;包含大量疑似密钥的请求不再拖慢网关。

  45. ThinkWatch 企业版 v3.0.0 EN GitHub ↗

    The request guards — outbound redaction, tool-call inspection and the content filter — now share their rule model with the desktop gateway: the same policy shape, built-in rule catalog, validation, rule view and sample trial, from thinkwatch-core. Each guard has three modes (off, observe, and one named for what it does: replace, cut off, enforce), and the content filter can delete what a rule matches as well as refuse or record it, and match by code point. Hidden characters become content filter rules, and the per-model output length guardrail becomes a cap on the output tokens a request may ask for. Settings saved by an earlier version are converted at the first start: read the first section before deploying. It is a major release because setting keys, console API routes and the models table change; the conversion keeps each deployment’s behaviour.

    Read before upgrading#

    • Back up system_settings and models first, and plan for no way back to 2.2. The upgrade converts the guard settings and drops a column (next item). Version 2.2 cannot run on the converted database: its settings are gone, so it would start on its seeded defaults, and it can no longer read the models table. Take a copy before deploying:

      pg_dump --data-only --table=system_settings --table=models "$DATABASE_URL" > thinkwatch-2.2-guards.sql
    • Stop every replica of 2.2 before the first 3.0 one starts. A 2.2 replica still running when 3.0 converts finds its settings gone (it then filters and redacts nothing) and cannot rebuild its router; one that restarts writes its seeded defaults back. 3.0 does not convert those a second time — it removes them at its next start and logs a warning — but the 2.2 replica runs on them until then. With the Helm chart (release thinkwatch, namespace thinkwatch here):

      kubectl -n thinkwatch scale deployment/thinkwatch-server --replicas=0
      kubectl -n thinkwatch wait --for=delete pod \
        -l app.kubernetes.io/name=think-watch,app.kubernetes.io/component=server --timeout=5m
      helm upgrade thinkwatch deploy/helm/think-watch -n thinkwatch   # with your usual values
      kubectl -n thinkwatch scale deployment/thinkwatch-server --replicas=<replicas you run>

      The last command matters when autoscaling.enabled is on: the chart then leaves the replica count alone, and it stays at 0. With Docker Compose, docker compose stop server before pulling and starting the new image.

    • The old guard settings are converted at the first start, and behave as before. security.content_filter_patterns, security.hidden_text, security.pii_redactor_patterns and security.tool_inspection become security.content, security.redact and security.inspect_tools, and are deleted, in one transaction during the boot migration. The conversion is recorded in security.legacy_converted (when, which version, what it converted); with that record present, old keys or the old column that show up again are removed, never converted, so the policies in force are not overwritten. A content rule identical to a built-in rule becomes that rule, switched on, any other a custom rule; a list with rules in it runs in enforce mode with every built-in rule it did not name switched off. The four seeded PII patterns become the built-in rules for the same data (cn-resident-id, bank-card, email, cn-mobile-phone), any other pattern a custom rule whose label is its old placeholder prefix. Whatever the old runtime was skipping (a rule that did not compile, a built-in rule id it did not know, an output_guardrails value it could not read) is left out, each with a warning in the start-up log. A model’s output_guardrails length cap becomes max_output_tokens (below), and the column is dropped. To see what was converted, read the three keys from Settings or system_settings afterwards; the start-up log lists them too.

    • Placeholders are written <<TW_EMAIL_1>>, not {{EMAIL_1}}. The label of a custom rule is upper case letters, digits and underscores (an old prefix is converted: REDACTED-SSN → REDACTED_SSN); the built-in identity number and bank card rules use ID_NUMBER and CARD_NUMBER. Anything that looked for the old form in answers or logs needs the new one.

    • Redaction searches the whole request, not only the user’s messages: the system prompt, earlier answers and tool-call arguments are redacted too. Base64 payloads (images, files, signatures) are still left alone. Rules run on the request as it is sent, as JSON, where a custom pattern’s match ends at a quote or a backslash: a pattern written to match across a " in the decoded text needs rewriting.

    • Built-in credential rules start replacing on deployments that were redacting. API keys and tokens with a known prefix, private keys, JWTs and connection-string passwords are built-in rules that ship switched on. A deployment whose PII list had patterns in it runs redaction in enforce mode after the upgrade, so these values are now replaced as well. Switch the ones you do not want off on the console’s security page. With an empty PII list, redaction converts to observe mode: it records what it finds and changes nothing.

    • The built-in rules that replace the seeded PII patterns are stricter. An identity number has to have a real province code, a real date of birth and a matching check digit; a card number a known network’s prefix and length and a valid Luhn digit, and published test card numbers do not count. Numbers the old regexes took for them — any 18 digits, any 16 — are no longer replaced. A deployment that relied on the looser match can add its old regex back as a custom rule.

    • Tool calls are judged as the client receives them, and two built-in rules are new. Inspection now reads a tool call converted to the caller’s format and with redacted values restored — what the client would run — where it used to read the placeholders. The new secret-to-unknown-host rule cuts (in enforce mode) a call that sends a recognised API key or private key to a host that is neither local nor the key’s own provider; upload-file-to-host records a call that uploads a local file to an outside host. A deployment running tool-call inspection in enforce mode starts cutting the first; add it to disable if that is not wanted.

    • “Warn” and “log” are one action now, “record only”, and hidden characters are content filter rules: unicode-tags and bidi-controls, plus zero-width and private-use, which ship off. security.hidden_text: block converts to those two rules refusing, warn and log to recording, off to switching them off. Recording writes an audit event: hidden_text: log used to reach only the application log, and now writes gateway.content_flagged.

    • A deployment with no content rules starts recording. An empty content filter list converts to observe mode with the built-in rules that ship on, so requests matching them (ignore previous instructions and the like) write gateway.content_flagged events where 2.2 wrote nothing. Nothing on the wire changes.

    • The output length guardrail is replaced by a model’s maximum output tokens — check each model’s after upgrading. A cap of N bytes on the answer converts to ceil(N / 4) output tokens, stored as converted. The answer is no longer measured or cut: a request asking for more tokens than the cap is lowered to it, in whichever field its API uses (a Chat request that sets both max_tokens and max_completion_tokens has both lowered), and the upstream stops there; one asking for less keeps its own. A request that sets no limit is held to the cap only when the cap is within what the gateway knows the model’s family to take (32,000 tokens for Claude models, 8,192 for others); a Chat request is then given max_completion_tokens on OpenAI’s own endpoint, whose reasoning models refuse max_tokens, and max_tokens elsewhere. Above that figure, the request goes out without a limit rather than with one the model could refuse, and the model’s own default applies: a 100,000-byte cap on a non-Claude model converts to 25,000 tokens, so its requests that set no limit are not capped at all. To hold every request to a cap, have the clients send a limit, or set the cap to that family figure or below. Reasoning (thinking) tokens count towards the cap on the APIs that bill them as output, so a cap that fit an answer can cut short a model that thinks first. An Anthropic request with extended thinking has its thinking budget lowered below the cap too, or thinking turned off when the cap is 1,024 tokens or less, the smallest budget Anthropic takes. The model API’s output_guardrails field is gone: a request that still sets one (anything but null or []) is refused with 400, so a script cannot believe answers are still capped. max_output_tokens (1 to 2147483647, null for no limit) replaces it.

    • A new installation observes by default. Every guard starts in observe mode, with only the built-in rules that rarely misfire switched on (personal data such as e-mail addresses and phone numbers ships off). Nothing is refused, replaced or deleted until a guard is switched to its third mode.

    • A content filter refusal is 403, with the error type of the caller’s API (permission_error for OpenAI-style APIs). Keyword and regex rules used to refuse with 400. In gateway_logs, its error_type is PolicyBlocked, where it was TransformError.

    • A rule that deletes text changes what is stored. The request a content rule stripped goes upstream, is redacted and is captured as the stripped one: the audit log’s request body is what was sent, not what the caller typed.

    • Changing a guard policy takes settings:write and the guard’s own permission: pii_redactor:write for security.redact, content_filter:write for security.content and security.inspect_tools, through PATCH /api/admin/settings. 2.2 checked settings:write on the server and the guard permission in the console; both are checked on the server now. Trying a sample takes pii_redactor:read or content_filter:read. Reading the policies — GET /api/admin/security, which the console’s security page loads — takes settings:read. The seeded admin and super_admin roles hold all of them.

    • Console API changes. GET /api/admin/security lists each guard’s mode and every rule, and POST /api/admin/security/{guard}/test tries a sample; they replace /api/admin/settings/content-filter/test, /content-filter/presets, /pii-redactor/test, /tool-inspection/rules and /tool-inspection/test, which are gone.

    • Audit events. Every rule that matches writes one event per request: gateway.content_flagged, gateway.content_stripped and gateway.content_blocked; gateway.redaction_flagged and gateway.redaction_replaced; gateway.tool_call_flagged and gateway.tool_call_blocked as before. gateway.hidden_text_flagged and gateway.hidden_text_blocked are gone; hidden characters are content events. No event carries the request’s text: a content event names the rule, the outcome, how many matches and whether they were in a tool result; a redaction event names the rule and counts the values and their occurrences, and only a built-in rule’s lists a few in masked form (sk-an…7f9c) — a custom rule’s values are not written at all. A request writes at most 20 events per guard (the rules that changed it, or matched most, first), each with the number of rules that matched (rules_in_request). With audit.body_redact_pii on, captured bodies are redacted with the outbound redaction rules, built-in ones included, whatever the redaction mode.

    • Metrics renamed. gateway_hidden_text_total is gone: hidden characters count in gateway_content_matched_total{outcome,custom} with every content rule. pii_pattern_invalid_total{pattern} is now guard_policy_invalid_total{guard} (a stored policy with a rule that does not compile; the rule is left out), next to guard_policy_unreadable_total{guard} (a stored policy that cannot be read; the guard runs on its factory policy). Redaction counts values in gateway_redaction_found_total{kind,outcome}.

    Added#

    • Deleting what a content rule matches. A content rule can refuse the request, delete the matched text from the caller’s messages and tool results and send the rest, or only record. Text deleted joins back what it separated, so the request is checked again afterwards.
    • Code point rules. A content rule can match characters by code point (U+200B, U+E0000–U+E007F), for invisible characters a keyword cannot be written for.
    • Every rule visible and switchable, built-in and custom, in each guard, with what it does in the third mode and what it did out of the box; a sample can be tried against one rule, an unsaved one, or all of them.

    Changed#

    • Core crates at ThinkWatch-Core v0.59.0. tw-dialect, tw-guard, tw-breaker and tw-bedrock move from v0.55.0; the shared guard model described above comes with them.

    Fixed#

    • A credential in a matched tool call no longer reaches the audit log. The excerpt of a tool call that inspection cut or recorded is masked with the redaction rules before it is written; 2.2 stored the matched arguments as they were, a key the model wrote in them included.
    • A request’s audit events and its log row carry the same id when the caller sends no x-trace-id. The log row of a request that went through used to carry a second, unrelated id.
  46. ThinkWatch Core v0.59.0 EN GitHub ↗

    This release keeps a plugin’s configuration in the plugin’s own file. What a plugin does when it fails, which requests it handles and the values of its settings are written in the manifest of plugins/<id>.js, which has to be plain data, and config.yaml keeps only the plugin’s id, file, approved hash and whether it is on. Saving a plugin is one endpoint that tells a change of data from a change of code, and only plugins that can rewrite tool calls need a confirmation in the app. On Windows, twcore.exe no longer needs the Visual C++ Redistributable.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) goes from 34 to 35. ThinkWatch Lite connects only to a core with the same protocol version. No ThinkWatch Lite release includes 0.58.0. ThinkWatch Lite 2026.10.0 includes 0.57.1 (protocol 31) and does not connect to 0.59.0. A server used with it stays on 0.57.1 until the app is updated to a release that includes 0.59.0; sudo twcore upgrade --version 0.57.1 --restart switches a server back.
    • The request store’s schema is unchanged (25): upgrading from 0.58.0 keeps the request history. A server upgrading from 0.57.x also gets 0.58.0’s changes, so the 0.58.0 upgrade notes apply as well: the history is cleared once, and a configuration that still sets security.hidden_text or security.output_limit does not load.
    • A plugin’s manifest has to be plain data.
      • The file declares it as export const manifest = { … }. A manifest exported any other way, such as export { manifest }, does not load.
      • It may hold objects, arrays, strings, numbers, true, false and null. Keys are names or quoted strings. Trailing commas are fine, and so is a minus sign before a number.
      • Expressions, function calls, references to variables, spreads, computed keys, getters and methods, and template literals with ${…} are not data. Neither is a manifest that the module’s code changes after declaring it.
      • Such a plugin does not load. gw.plugin.manifest_not_data_at gives the line and column; gw.plugin.manifest_not_data is used when there is no position.
    • The manifest holds the plugin’s configuration.
      • on_error ("reject", the default, or "skip") is a manifest field.
      • match is the scope the plugin runs with. It used to be a suggestion that filled in the configured scope at install.
      • Settings take value in place of default: it is the value the plugin gets. A setting still written with default does not load; rename the field to value. A setting without value gets its type’s empty value ("", 0 or false).
    • Plugin entries in config.yaml hold only id, file, sha256 and enabled.
      • Entries written by 0.58.0 may still have on_error, scope and settings. These are ignored whatever they contain, so the configuration still loads, and they are removed the next time core writes the plugins section.
      • Their values are not carried over. To keep them, write them into the plugin’s manifest, or set them again in the app.
      • rewrite and confirmed are now reserved: a configuration with a plugin of either id does not load.
    • Plugins installed by 0.58.0.
      • The two default plugins are replaced with this release’s versions on first start and take this release’s setting values. One that was on is turned off, because this core cannot read what its 0.58.0 version asked for; turn it on again in the app.
      • A plugin of your own whose manifest uses default, or is not plain data, does not load until its file is fixed and the change approved. While it is on, it refuses the requests it covers: on_error defaults to reject, and a 0.58.0 manifest cannot set it.
    • Changed in the protocol:
      • Removed: PUT /plugins/{id}/source (ReplacePluginSource, PluginSourceReplace), UpdatePlugin, UpdatePluginConfirmed, PluginUpdate, and PluginView.settings (the values are in settings_schema[].value).
      • PUT /plugins/{id} is SavePlugin and PUT /plugins/{id}/confirmed is SavePluginConfirmed. Both take PluginSave: source, enabled and base_version.
      • New: POST /plugins/rewrite (PluginRewrite: PluginRewriteRequest → PluginSource), POST /plugins/confirmed (CreatePluginConfirmed) and POST /plugins/{id}/approve/confirmed (ApprovePluginFileConfirmed).
      • PluginCreate has only source, id, enabled and base_version. SettingSpecView.default is now value, and ManifestView has on_error. PluginView.on_error and PluginView.scope are what the plugin’s file says.
      • A web view may call CreatePlugin, SavePlugin and ApprovePluginFile. A client calls the three …Confirmed endpoints from native code after the user confirms, never from its web view.
    • Confirmations. A plugin that can rewrite tool calls (the reply.tool_calls permission) needs the user’s confirmation outside the web page to be installed, to be turned on, to have its code changed, or to have a change to its file approved. A plugin counts as one when its old or its new version has the permission, or when its old version cannot be read.
      • The plain endpoints refuse these with 403 control.plugin.needs_confirmation.
      • Nothing else needs a confirmation: changing only data, even of a plugin that can rewrite tool calls; turning a plugin off, deleting and reordering; and installing, turning on, changing or approving a plugin that cannot rewrite tool calls. In 0.58.0, every install, source replacement and file approval needed one, and so did changing the settings or scope of a plugin that can rewrite tool calls.
      • PUT /config, PATCH /config and rollback refuse to add, turn on or re-approve a plugin that can rewrite tool calls, with the same 403. They are open to the web view and used to bypass the rule. They have no confirmed variant: those changes go through the plugin endpoints.
    • Message codes, compared with 0.58.0:
      • New: gw.plugin.manifest_not_data and gw.plugin.manifest_not_data_at.
      • Removed: config.plugin.blank_pattern and config.plugin.setting_type. The configuration fields they checked are gone.
      • control.plugin.needs_confirmation has a new sentence that names the four actions. Its argument (plugin) is unchanged.

    Plugin files hold their configuration. The app changes on_error, match or a setting by rewriting the plugin’s source and saving it. POST /plugins/rewrite takes a source and the new values and returns the rewritten source; it reads and writes no file. POST /plugins/inspect reads the values back from a source.

    • Only the manifest’s bytes change. Every other byte of the file stays as it was.
    • Core writes the manifest in one fixed style: two-space indent, one manifest field per line, and nested objects and lists on one line when they fit in 80 columns. Comments inside the manifest are not kept.
    • In a rewrite request, on_error and scope are the values to end up with, and settings changes only the settings it names. Each scope entry is trimmed. A setting the plugin does not declare (gw.plugin.setting_unknown), a value of the wrong type (gw.plugin.setting_type) and an empty scope entry (control.plugin.blank_pattern) are refused with 400.
    • Core finds the manifest with a small tokenizer that skips strings, template literals, comments and regular expressions, so text that looks like a manifest inside them is not taken for it.
    • A plugin whose file changed, or that does not load, follows the on_error and match of its approved file. The manifest is data, so core reads them even when the plugin cannot run.

    Saving a plugin. PUT /plugins/{id} takes the whole source and whether the plugin is on. Core compares the source with the approved one:

    • Data-only: the bytes outside the manifest are identical, and the manifest differs only in on_error, match and the settings’ value.
    • Code change: anything else, including a change to the name, description, permissions, request kinds, or a setting’s type or label.
    • Same bytes: only enabled changes, and the files are not touched.

    When a save writes the file, the plugin file, its approved copy and its sha256 in config.yaml change together, with no moment in between when the file counts as changed: a plugin that is on keeps running across the save. A save is tied to the configuration version it read, and fails with 409 when the configuration has moved on. A file edited by hand still stops the plugin until the change is approved, even when only a value changed.

    Default plugins. reply-language and wsl-paths write on_error and their settings’ value in their manifests, in the style core writes, so changing a setting in the app changes one line of the file. The record of what core offered follows data-only saves and approvals, so a default whose settings, scope or on_error were changed still counts as unchanged code. A later version replaces it and keeps what was written in the file: on_error, match, and the values of the settings it still declares with the same type. A default whose code was changed is still left alone, and a new version that asks for more permissions or request kinds still comes back turned off.

    Windows. twcore.exe no longer needs the Visual C++ Redistributable; the C runtime is linked statically. The twcore.exe of 0.58.0 imported VCRUNTIME140.dll, which comes with that package and not with Windows, so the gateway did not start on a machine without it.

    Requests full of secrets. Outbound redaction no longer slows down when one request holds many distinct values that look like secrets: finding, replacing and restoring them, and hiding them from plugins, now take time in proportion to the request’s size instead of growing with the square of the number of values. The security log records at most the first 100 distinct values of a request, and every value is still replaced.

  47. ThinkWatch Core v0.58.0 EN GitHub ↗

    This release adds script plugins: JavaScript that changes requests before they go upstream and answers before they reach the client, run in a WebAssembly sandbox inside core. It also reads a session back as a conversation, stores request and answer bodies with recognized secrets already replaced, and records an error that an upstream answered as a failed request. The five security guards become three. Hidden characters are now content-filter rules, the output limit is gone, and tool-call inspection gains two rules against sending credentials and files to other hosts.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) goes from 31 to 34. ThinkWatch Lite connects only to a core with the same protocol version. ThinkWatch Lite 2026.10.0 includes 0.57.1 (protocol 31) and does not connect to 0.58.0. A server used with it stays on 0.57.1 until the app is updated to a release that includes 0.58.0; sudo twcore upgrade --version 0.57.1 --restart switches a server back.
    • The request store’s schema goes from 23 to 25: the security log has two new columns, and plugin runs have a table of their own. The request history is cleared when the store is rebuilt on first start.
    • security.hidden_text and security.output_limit are removed. A configuration that still has either key does not load: the app starts in safe mode, and twcore serve exits with the error.
      • Only configurations where one of these settings was changed are affected. A new configuration has no security section, and the app writes only values that differ from the factory ones. In ThinkWatch Lite 2026.10.0, that means:
        • Hidden characters set to Off or Enforce (factory: Observe), or one of its two rules (Unicode tag characters, bidirectional controls) turned off.
        • Output limit set to Observe or Enforce (factory: Off), or its limit changed from 100,000 characters.
      • Before upgrading, do one of the following:
        • set these back to their factory values in the app;
        • delete hidden_text: and output_limit:, with the lines indented under them, from the security: section of config.yaml.
      • Hidden characters are now checked by content-filter rules that follow the content filter’s mode (see Three guards).
    • Changed in the protocol:
      • Removed:
        • PUT /security/{guard}/limit (SetSecurityLimit, LimitSave);
        • OutputLimitDetail, HiddenItem, HiddenKind;
        • the events hidden_text_found and output_limited;
        • hidden_text and output_limit in Guard, SecurityDetail, SecurityView and SecurityCounts.
      • GET /sessions/{id}/transcript (SessionTranscript → Transcript, with TranscriptTurn, TranscriptMessage, TranscriptRole, TranscriptPart, TranscriptGap).
      • The plugin endpoints:
        • GET /plugins (Plugins → PluginView), POST /plugins/inspect (PluginInspect: PluginSource → PluginInspection), POST /plugins (CreatePlugin, PluginCreate), PUT /plugins/order (ReorderPlugins, PluginOrder);
        • PUT /plugins/{id} (UpdatePlugin, PluginUpdate), PUT /plugins/{id}/confirmed (UpdatePluginConfirmed), DELETE /plugins/{id} (DeletePlugin);
        • PUT /plugins/{id}/source (ReplacePluginSource, PluginSourceReplace), GET /plugins/{id}/source (PluginSourceDiff → PluginSourceView), POST /plugins/{id}/approve (ApprovePluginFile, PluginApprove);
        • POST /plugins/{id}/trial (TrialPlugin: PluginTrial → PluginTrialResult, TrialSide), GET /plugins/{id}/logs (PluginLogs → PluginLogEntry).
        • Supporting types: ManifestView, PluginStatus, PluginStats, PluginLastError, PluginLoadError, PluginScope, PluginHooks, PluginRunView, SettingSpecView, SettingValue, Permission, RequestKind, OnError, ReplyMode, SettingKind, PluginHook, PluginOutcome, PluginLogLevel.
      • Event::PluginFailed. RequestDetail has plugins and request_after_plugins, HistoryRow has plugin_changed, and ConfigOrigin has defaults.
      • TurnView has status.
      • The security types are now defined in tw-guard and keep their names. Changes:
        • SecurityRuleView and CustomRuleSave have label.
        • Matcher has email, cn-mobile-phone and builtin; RuleAction has strip; ContentMatch has codepoints.
        • SecurityTestRequest has label and action; SecurityTestResult has output and refused.
        • SecurityOutcome and SecurityOutcomeCounts have stripped; SecurityEventView has match and revealed.
        • The security counts in /summary are secrets, secrets_replaced, tool_calls, tool_calls_cut, content, content_blocked and content_stripped.
      • content_matched has match, action, outcome (ContentOutcome: recorded, stripped, blocked), count and revealed, and no longer blocked.
    • Message codes, compared with 0.57.1:
      • New:
        • 52 for plugins (config.plugin.*, control.plugin.*, gw.plugin.*). gw.plugin.reason only passes on what the plugin wrote.
        • gw.upstream.status_message.
        • Nine for the guards: config.rule_codepoints_bad, config.rule_label_bad, gw.content.refused_invisible_message, gw.content.refused_invisible_tool_result, security.bad_codepoints, security.bad_label, security.content_action_unknown, security.pattern_empty, security.unknown_guard.
        • Two test-only codes, which never reach a UI.
      • Renamed: gw.toolcall.cut → gw.toolcall.response_cut, gw.toolcall.blocked → gw.toolcall.response_withheld, gw.ws.toolcall_cut → gw.toolcall.connection_cut (its detail argument is now why). The sentences now say that the answer contained the call, not that an upstream returned it.
      • Removed: config.output_limit_range, gw.hidden_text.refused_message, gw.hidden_text.refused_tool_result, gw.output_limit.cut, gw.output_limit.withheld, security.guard_unknown, security.limit_range, security.no_custom_rules, security.no_limit, security.nothing_to_test, security.unknown_content_action. security.unknown_guard and security.content_action_unknown take the place of security.guard_unknown and security.unknown_content_action, with sentences that list the new guards and actions.
    • Building from source needs an LLVM clang that can compile C to WebAssembly, and its llvm-ar. The plugin sandbox compiles QuickJS while core builds. Apple’s clang cannot target WebAssembly; on macOS, brew install llvm. The README lists the other systems. The released binaries need nothing new.
    • Behavior worth knowing:
      • Compaction requests (/v1/responses/compact, /backend-api/codex/responses/compact) are screened by the content filter like generation requests. Token counts are still not screened.
      • Tool-call excerpts are masked before they reach the tool_call_flagged event, the security log and system notifications. A secret restored from a placeholder used to appear there in the clear.
      • Stored bodies no longer contain the secrets and personal numbers that the redaction rules recognize, whatever the redaction mode (see Stored bodies).
      • The retention settings now take effect. tw-store ran its own hourly cleanup with fixed limits (7 days of bodies, 90 days of rows, 2 GiB of bodies) beside the one that reads retention, so any setting above those limits was cut back every hour. That cleanup is removed. retention.body_max_bytes now defaults to 5 GiB (it was 2 GiB).
      • On first start, core adds its two default plugins to config.yaml, turned off, and writes their files to plugins/ beside it. That is one configuration version, recorded in the history as defaults.

    Session transcripts. GET /sessions/{id}/transcript reads a session’s stored bodies back as a conversation. For every turn it gives the messages that are new in that request, the answer, and what could not be shown.

    • Clients resend the whole history on every turn, so a turn shows only what its request added after the previous readable one.
    • Messages are compared by a fingerprint that ignores cache markers, reasoning signatures, key order and reasoning text. When the history was edited or compacted, the turn is marked restart and carries the whole history.
    • It reads Anthropic Messages, OpenAI Chat Completions and Responses, Gemini and Bedrock, streamed and whole, including answers converted from another format.
    • Tool-call ids in converted answers are matched to the ones the client recorded, so results pair with their calls.
    • A message holds text, reasoning, tool calls, tool results, images (media type and size only), and other blocks by their type name. System and developer messages in the middle of a conversation appear as system messages.
    • Gaps say what is missing: request_missing, request_truncated, response_missing, response_truncated, response_unreadable.
    • Every string is masked after decoding. Image data and reasoning signatures are never returned.
    • A sub-agent’s work is in its own session.

    Stored bodies. Bodies used to be stored as the client sent them and masked only when read. They are now redacted when twcore hands them to the store, off the forwarding path.

    • Every value the redaction rules recognize is taken out, whatever the mode, off included.
      • Under enforce, a request is stored with the placeholders the upstream received.
      • In the other modes, the values are masked.
      • The whole text then goes through the same masking the read side applies.
    • A value gets the same placeholder in every hop, in the stored request and in the stored answer.
    • Up to 4 MiB of each answer is kept (it was 256 KiB), the same as for requests. A request over 4 MiB is stored cut, with its original length recorded, so a replay refuses it instead of sending half the JSON.
    • Bodies waiting to be written are capped at 32 MiB as well as at 64 entries.
    • Where the stored copy shows:
      • The request detail shows the stored copy.
      • Whole-history search no longer finds text that occurs only inside a secret.
      • A replay sends the stored request, with placeholders or masked values where the secrets were.
      • The fixture export redacts again with every built-in rule.

    Failed upstream answers. When an upstream answered 4xx (or 3xx) and the gateway passed that answer on to the client, the request ended as finished with an empty error. It counted as successful everywhere. It now ends with RequestFailed, from source upstream. A request failed if and only if its error is set, in the traffic list, history search, the overview, sessions and the upstream check-up.

    • The recorded reason is what the upstream said: gw.upstream.status_message (upstream, status, message), read in the upstream’s format, masked, and capped at 500 characters. An empty body or an HTML page gives gw.upstream.status.
    • TurnView.status carries the upstream’s status code, so a failed turn can say what the upstream answered.
    • The client still gets the upstream’s answer byte for byte, and a client error still does not count against the upstream.

    Three guards. Outbound redaction, tool-call inspection and the content filter now share one model in tw-guard: policy shape, built-in catalogs, validation, rule views, and the logic behind “Test…”. The enterprise edition uses the same model.

    • Hidden characters are built-in content rules matched by code point. unicode-tags and bidi-controls are on; zero-width and private-use are off.
    • Actions. Content rules act with block, strip (new) or record when the filter is in enforce. The hidden-character rules strip, where the old guard refused the request.
      • Stripped text is removed from user messages and tool results.
      • The stripped body is what is redacted, recorded and sent on every hop.
      • For tag characters, events and the security log also show the decoded text (revealed).
    • Custom rules. Custom content rules can match code points (U+200B, U+E0000–U+E007F). Custom redaction rules can name their placeholder: label: PROJECT gives <<TW_PROJECT_1>>.
    • New redaction rules. email and cn-mobile-phone (Chinese mainland mobile numbers) are built in, off by default.
    • What is screened.
      • Compaction requests are screened, and so are response.create frames on the Responses WebSocket. Other WebSocket frames go through the code-point rules only.
      • Token counts, embeddings and legacy completions are not screened.
      • A refused request still leaves a failed row.
    • Output limit. It is removed.
    • Testing. “Test…” takes an action, and shows what would be sent and whether the request would be refused.

    Two tool-call rules. Placeholders are restored in answers, so an upstream could write a tool call that sends a restored credential somewhere. Tool calls are judged as the client will execute them, after restoration. Two built-in rules cover this:

    • secret-to-unknown-host (high: cut in enforce). It fires when a tool call makes an http(s) request with an API key or private key in its arguments that the redaction rules recognize, and the destination is neither local nor that credential’s own provider (an Anthropic key going to anthropic.com is fine). JWTs and connection strings do not count here.
    • upload-file-to-host (medium: recorded only). It fires when a tool call uploads a local file to a host that is not local: curl -T, --data @file, -F field=@file, --upload-file, --post-file.
    • The excerpt shows only scheme://host, so it never carries the credential.
    • Tool-call inspection starts in observe, where both rules only record.
    • The cut messages name the rule, not who produced the call, since a plugin can produce one too.

    Script plugins. Plugins are JavaScript modules that change requests before they go upstream and answers before they reach the client.

    • Sandbox. They run only inside core, in QuickJS compiled to WebAssembly and run by Wasmtime. They have no file system, network, environment, timers or module imports. A request hook gets a fresh instance for every call. An answer gets one instance, shared by its hooks and discarded when the answer ends.

    • Permissions. system, messages, tools, params, reply.text and reply.tool_calls. A plugin sees only the parts it was granted. Every edit is checked: keys, read-only fields and permissions. A request no plugin changed reaches the upstream byte for byte.

    • Placeholders. Plugins never see a real secret. Before a hook runs, values the redaction rules recognize are replaced with placeholders, in every redaction mode and with the same numbering as outbound redaction. Placeholders are put back afterwards, whoever wrote them. Tool-call inspection judges the call after restoration.

    • Request hooks.

      • They run after routing, once per upstream attempt, on the client’s request as the content filter left it.
      • Failover to another upstream starts again from that request, so an edit made for one upstream never reaches the next. Resends to the same upstream reuse the result.
      • A changed request is screened again. Only what the plugins added is reported, and only that can refuse the request.
      • reject(), an error under on_error: reject, or a refusal by the content filter refuses the whole request without failing over.
      • A plugin’s params.model renames what that upstream gets and never re-routes. The gateway key’s model list still applies (gw.plugin.model_not_allowed).
    • Request kinds. A plugin handles the kinds of request its manifest lists in requests, and conversations only when it lists none.

      • Conversations are Anthropic Messages, OpenAI Chat Completions and Responses, and Gemini, with their token counts and compaction.
      • Embeddings and legacy completions go only through plugins that declare them, and only their input text can change.
      • Other endpoints pass without any plugin.
    • Reply hooks. They run after format conversion and before tool-call inspection, so inspection sees what the plugin produced. They cover streamed, whole and converted answers, and the Responses WebSocket. Text comes in blocks or as a stream; tool calls come whole.

    • Management. The plugins: section of config.yaml lists plugins in the order they run.

      • A plugin runs only while the SHA-256 of plugins/<id>.js matches its approved sha256. A changed file stops it within seconds, until it is approved again.
      • Each plugin has on_error (reject or skip), a scope (clients, models sent, upstreams) and settings.
    • Confirmations. These need the user’s confirmation outside the web page:

      • installing a plugin, replacing its source, or approving a changed file;
      • turning on a plugin that can rewrite tool calls, or changing its settings or scope.

      UpdatePlugin refuses the second kind with 403 control.plugin.needs_confirmation. A client calls CreatePlugin, ReplacePluginSource, ApprovePluginFile and UpdatePluginConfirmed from native code after the user confirms, never from its web view. ThinkWatch Lite asks in a system dialog.

    • Default plugins. Two ship with core and are added turned off:

      • reply-language asks the model to answer in a chosen language.
      • wsl-paths converts drive paths in tool-call arguments between their WSL and Windows forms.

      A default that was deleted is not added back, and one that was changed is left alone. A new version that asks for more permissions or request kinds comes back turned off. The sandbox (about 7 MB of memory) starts only once a plugin is turned on.

    • Limits.

      • A request hook gets 200 ms of CPU and 128 MiB of memory. An answer gets 20 ms per call, 2 s in total and 64 MiB.
      • Output is capped at twice the input plus 1 MiB, and logs at 100 lines per call (longer lines are cut at 4 KiB). The source is capped at 1 MiB.
      • At most 32 plugin instances run on answers at once. Past that, a plugin follows its on_error.
    • Recording. Every run is recorded with its hook, outcome, CPU time and upstream attempt.

      • The request detail shows the runs and the body sent after plugins, which is stored with secrets replaced. History rows say when a plugin changed the request.
      • Failures raise plugin_failed.
      • Each plugin keeps its last 500 log lines in memory.
      • A plugin can be tried on a recorded request without contacting an upstream.

    Security fixes.

    • rustls 0.23.45 fixes RUSTSEC-2026-0285: TLS 1.3 handshake messages were accepted across encryption-level boundaries. Every upstream request goes through rustls.
    • The plugin sandbox ships with Wasmtime 49.0.2, which fixes RUSTSEC-2026-0325, -0326 and -0327. Those advisories were published against 49.0.1, which no release contained.
    • CI now checks the dependencies against RustSec advisories.
  48. ThinkWatch Lite v2026.10.0 GitHub ↗

    升级须知:内置 core 升级到 0.57.1(控制协议 31),连接远程 core 时服务器也要升级到 0.57.x;首次启动会清空请求记录,配置、密钥与上游不受影响;身份证号与银行卡号两条脱敏规则出厂开启,「观察」档下只记录;按输入 token 分流的规则改用网关的 token 计数估算,含多张图片的请求可能越过原先不会越过的阈值。

    流量页的搜索覆盖全部请求记录,可以继续向更早的记录搜索;搜索框中的开关可同时搜索请求与回答的内容,命中的片段显示在请求下方。

    上游页新增「体检」:各上游与服务同一模型的其他上游对照,列出失败率、回答中的模型名是否与发出的相同、报告的输入 token、提示缓存读取比例、首 token 时间与生成速度,每项附样本数,样本足够时才标出偏差。

    出站脱敏新增身份证号与银行卡号,号码结构与校验位都正确才算,占位符注明原来是哪一种。

    新增一键接管 Pi、oh-my-pi、Grok Build、Qwen Code 与 Hermes Agent,MCP 页列出它们的 MCP 服务器;只装了 DeepSeek Harness 桌面版时也能检测到;共用的 ~/.agents/skills 目录单独列出。

    还没有请求经过上游时,概览页显示「开始使用」清单,其他页面在适用之处提示下一步。

    不是 POST 的请求由网关直接回 404,不再转发给上游、记成失败的请求;Hermes Agent 启动时的本机服务检测因此不再出现在流量中。

  49. ThinkWatch Core v0.57.1 EN GitHub ↗

    This release answers requests other than POST in the gateway itself. Before, they were forwarded to an upstream and recorded as failed requests, so a client checking whether the gateway is a local model server sent those checks on to the upstreams.

    Upgrade notes

    • The control-plane protocol version is unchanged (31) and so is the request store’s schema (23). ThinkWatch Lite 2026.9.26 includes 0.56.0 (protocol 30) and does not connect to 0.57.x: a server used with it stays on 0.56.0 until the app is updated to a release that includes 0.57.1.
    • One message code is added: gw.method.not_forwarded.
    • A GET, HEAD, OPTIONS, PUT or DELETE to a path the gateway does not serve itself now gets a 404 from the gateway instead of whatever the upstream answered to an empty POST.

    Requests other than POST. Every upstream API the gateway forwards to takes POST, and the gateway always sent the upstream request as POST. Any other method that reached the catch-all route was sent on as an empty POST, recorded as a failed request and, with failover, tried on the next upstreams.

    • Hermes Agent sends five such GETs when it starts (/api/v1/models, /api/tags, /v1/props, /props, /version) to tell whether a server is LM Studio, Ollama, llama.cpp or vLLM.
    • An upstream that answered one of them with 200 made Hermes take the gateway for that kind of server.

    These requests now get a 404 in the client’s error format, after the key check, and are neither forwarded nor recorded. A request without a key still gets 401. /v1/models, /v1beta/models, /api/hello, /healthz and WebSocket upgrades are unchanged.

  50. ThinkWatch Core v0.57.0 EN GitHub ↗

    This release redacts Chinese resident ID numbers and bank card numbers out of the box, searches the whole request history including the text of requests and answers, and adds an upstream check-up: whether the model named in an upstream’s answers matches the one sent, how its reported input compares with other upstreams serving the same model, how much of its input comes from the prompt cache, and its time to first token and speed.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) is now 31. ThinkWatch Lite connects only to a core with the same protocol version, and has to regenerate its protocol types from this release to use it. A server used with ThinkWatch Lite 2026.9.26 or earlier (protocol 30) stays on 0.56.0 until the app is updated to a release that includes 0.57.0; sudo twcore upgrade --version 0.56.0 --restart switches a server back.
    • The request store’s schema goes from 22 to 23 (three new columns): the request history is cleared when the store is rebuilt on first start.
    • Two redaction rules are on by default: cn-resident-id and bank-card (see below). With outbound redaction in enforce, such numbers in a request are replaced before it leaves; in observe they are recorded. security.redact.disable turns either off.
    • Routing estimates input size the way token counting does. Rules on input_tokens now see one token per CJK character, 1,600 per image or file, and no thinking, instead of four bytes per token with images and files left out. A request with many images can now cross a size threshold it used to stay under.
    • Changed in the protocol:
      • POST /history/search (HistorySearchQuery → HistorySearchPage, with HistoryCursor, ContentHit, ContentSide, SearchStop).
      • GET /upstreams/health (UpstreamHealth, UpstreamCheckup, ModelConsistency, ModelPair, InputVsEstimate, InputForModel, RatioView, CacheReads, CacheForModel, CacheTally, MedianView).
      • Event::RequestStarted has input_estimate; RequestFinished, RequestFailed and RequestCancelled have answered_model (both left out when absent).
      • Matcher has cn-resident-id (born_since) and bank-card (networks: CardNetwork, CardPrefix); SecretKind has personal.
    • No message codes change.

    Chinese resident ID numbers and bank card numbers. Outbound redaction looked only for credentials. Two built-in rules now look for personal numbers, held to the same bar as the credential rules: a value must check out structurally, not merely look like one.

    • cn-resident-id: an 18-character resident ID number whose first two digits are a province-level code, whose birth date is a real date between 1900-01-01 and today, and whose check character (ISO 7064 MOD 11-2) is right. The old 15-digit numbers are not matched.
    • bank-card: a number whose prefix and length belong to UnionPay, Visa, Mastercard, American Express, JCB, Discover or Diners Club and which passes the Luhn check, written in one run or in groups of four separated by single spaces or hyphens (and American Express’s 4-6-5, Diners Club’s 4-6-4). The public test card numbers of Stripe, Braintree and Adyen are left alone.
    • A number inside a longer run of letters or digits is not matched, and neither is a number written as a JSON number (a tool call’s arguments, for example): replacing it would leave the body invalid JSON.
    • Placeholders say what was there: <<TW_ID_NUMBER_1>>, <<TW_CARD_NUMBER_1>>. They are restored in the answer as before, streams included. The security log shows only the last four characters.
    • Scanning a request body also got faster: the token scan no longer allocates per token.

    Searching the whole history. The Traffic page searched only the requests it had loaded. POST /history/search applies the same filter to every stored request, in pages that go back in time.

    • It matches the free text against the path, client, client hint, peer, upstream, model and error message, and takes the message codes whose translation matched, so a query in the app’s own language finds errors stored in English.
    • With content, it also searches, for requests whose bodies are still kept, what is new in each request: the last user turn (text and tool results) and the answer (text, and each tool call’s name and input). Clients that resend the whole conversation every turn therefore do not match every later request of a session. Each match comes with a short excerpt, with credentials masked.
    • Content search reads at most 500 requests’ bodies or 1.5 seconds per call, newest first, and says how far back it got, so a client can offer to search further.

    Upstream check-up. GET /upstreams/health reports, for every upstream over a window (the last 7 days by default), facts and sample sizes rather than verdicts:

    • requests, failures and cancellations;
    • whether the model named in its answers matches the model sent (after rule rewrites), with up to three example pairs. Names are compared after removing differences that are not a different model (vendor prefixes, dates and snapshot markers, Bedrock’s inference-profile and version forms, 4.5 against 4-5), never the family, size, generation or variant;
    • the median ratio of the input it reported to the gateway’s local estimate, overall and, for every model that other upstreams also served, next to the ratio of those other upstreams;
    • for later turns of a conversation sent to it within five minutes of the previous one, the share of input read from the prompt cache and how many such turns read none, also next to other upstreams serving the same model;
    • median time to first token and generation speed.
    • The answered model is read from the upstream’s own response in the same pass that reads usage (Bedrock’s responses do not name one). The estimate is the one routing already computes.

2026年9月

  1. ThinkWatch 企业版 v2.2.0 EN GitHub ↗

    This release fixes authorization. The gateways never checked ai_gateway:use or mcp_gateway:use, so a user with no role, or with only roles that grant neither, could call every model and MCP tool; a role granted at the scope of one team widened model and tool access platform-wide; and an API key whose allow-list came out empty could call any model. The gateways now require the permission and count only the roles that grant it. The seeded team_manager role now works when granted at team scope, and seven permissions that nothing ever checked are retired. Some users lose gateway access on upgrade: read the first section before deploying. There is one manual database script and no schema, setting, environment variable or Helm value change.

    Read before upgrading#

    • Users without a role that grants gateway use lose gateway access. A request to the AI gateway is refused with 403 unless a role held by the key’s owner grants ai_gateway:use, and a request to the MCP gateway unless one grants mcp_gateway:use. The body names the missing permission, in the error format of the caller’s API. The built-in developer, admin, super_admin and team_manager roles grant both; viewer grants neither. Three groups of users are refused after the upgrade where 2.1.0 let them through:

      • users with no role at all;
      • users whose roles are only viewer, or custom roles without these actions;
      • users whose only gateway role is assigned at team scope (see the third item below).

      Find them before upgrading. This query lists every active user with a live key for a gateway that none of their roles will grant (it counts global assignments and roles attached to the user’s teams, which is what the gateways now read; it does not account for Deny statements):

      WITH grants AS (
        SELECT id AS role_id,
               jsonb_path_exists(policy_document,
                 '$.Statement[*] ? (@.Effect == "Allow").Action[*] ? (@ == "*" || @ == "ai_gateway:*" || @ == "ai_gateway:use")') AS ai,
               jsonb_path_exists(policy_document,
                 '$.Statement[*] ? (@.Effect == "Allow").Action[*] ? (@ == "*" || @ == "mcp_gateway:*" || @ == "mcp_gateway:use")') AS mcp
          FROM rbac_roles
      ),
      held AS (
        SELECT user_id, role_id FROM rbac_role_assignments WHERE scope_kind = 'global'
        UNION
        SELECT tm.user_id, tra.role_id
          FROM team_members tm JOIN team_role_assignments tra USING (team_id)
      ),
      access AS (
        SELECT h.user_id, bool_or(g.ai) AS ai, bool_or(g.mcp) AS mcp
          FROM held h JOIN grants g USING (role_id) GROUP BY h.user_id
      )
      SELECT u.email, s.surface
        FROM api_keys k
        JOIN users u ON u.id = k.user_id
        CROSS JOIN LATERAL unnest(k.surfaces) AS s(surface)
        LEFT JOIN access a ON a.user_id = u.id
       WHERE k.is_active AND k.deleted_at IS NULL
         AND u.is_active AND u.deleted_at IS NULL
         AND NOT COALESCE(CASE s.surface WHEN 'ai_gateway' THEN a.ai
                                         WHEN 'mcp_gateway' THEN a.mcp END, false)
       GROUP BY u.email, s.surface
       ORDER BY u.email, s.surface;

      Give each of them developer (or a custom role with the actions) either at global scope (Users → edit the user’s roles, scope Global), or by attaching the role to a team they belong to (Teams → the team → Roles), which every member of that team inherits. The change takes effect within a minute; each user’s permissions are cached for 60 seconds.

      If SSO users are meant to use the gateway from their first sign-in, set Default Role for New Users (auth.default_role) in Settings to developer. It is empty by default, and it applies only to accounts created after it is set; existing users need the grant above.

    • A role that does not grant gateway use no longer widens model or tool access. Model and MCP tool scopes are now the union over the roles that grant ai_gateway:use / mcp_gateway:use only. A role without those actions (such as viewer) used to count as “unrestricted”, so adding it to a user limited to some models opened every model to them. Users relying on that lose the extra models.

    • A role assigned at team scope no longer grants gateway access. An assignment with scope team:<id> administers that team from the console (team roster, team limits); it no longer contributes models, MCP tools or gateway use, which it used to do for every request the user made, member of the team or not. Roles attached to a team itself (Teams → Roles), which every member inherits, still count. A user whose only gateway role was a team-scoped developer or team_manager needs that role at global scope, or attached to their team. The role assignment editor now says this under the scope picker.

    • An empty model allow-list allows nothing. A key whose allowed_models is [], or whose list shares no model with what its owner’s roles grant, used to call any model; it now calls none, as allowed_mcp_tools: [] already did on the MCP gateway. The console never saves [] (clearing the picker sends null), so only keys written through the API are affected. To find them: SELECT id, name, user_id FROM api_keys WHERE allowed_models = '{}' AND is_active AND deleted_at IS NULL; Set such a key’s list to null to leave it bounded by its owner’s roles only. Model entries still match by prefix, and a key narrowed to gpt-4o-mini under a role granting gpt-4o keeps gpt-4o-mini.

    • Run db/release_migrations/2026-09-30_retire_unchecked_permissions.sql once, after deploying. The server does not apply it. It adds teams:read to the team_manager role and removes the seven retired permissions (see Changed) from every role, system and custom. It is idempotent and changes no access for any other role, since nothing checked the removed keys. Without it, the server still starts and works, but:

      • team_manager keeps the old team:read it cannot use, so a team manager who is not a member of the team they manage still gets 403 opening it, its roster or its roles;
      • every start logs a warning listing the roles that still name retired permissions.

      Reset to defaults on a system role in the console has the same effect for that one role, but does not clean custom roles.

    • A team-scoped rate_limits:write now takes effect. It used to require global scope for every subject, so the seeded team_manager granted at team scope could not touch any limit. It now covers the rate limits and budget caps of users in that team and of the API keys they own, never the holder’s own user or keys; role subjects still need global scope. Anyone holding team_manager (or a custom role with rate_limits:write) at team scope can now change, lift or delete the limits of every member of that team, administrators included. Review team-scoped assignments of these roles before upgrading.

    Security#

    • Gateway use is checked, and a missing grant means no access. See the first three items above: a user with no role, or only roles without gateway use, could call every model and MCP tool; a viewer-style role widened a restricted user to every model; a role granted at the scope of any team, even one the user was not a member of, widened model and tool access platform-wide. An explicit Deny on ai_gateway:use or mcp_gateway:use now closes that gateway. Action wildcards (ai_gateway:*, *) grant it, as they already did for console permissions.
    • A key narrowed inside a prefix grant is no longer unrestricted. The key’s allow-list was intersected with its owner’s role grants entry by entry, as literal strings. A key limited to gpt-4o-mini under a role granting gpt-4o (a prefix) came out with an empty list, which the gateway read as “no restriction”, so the key could call every model. The intersection now keeps an entry of either side that the other side covers, by prefix for models and by <server>__* pattern for MCP tools, and an empty result allows nothing.

    Fixed#

    • The seeded team_manager role works at team scope. It granted team:read, but the team handlers check teams:read, so a team manager got 403 listing teams and opening the team, its roster or its roles. It now grants teams:read, and opening a team accepts teams:read scoped to that team, where it used to require global scope. Existing installations need the release migration above.
    • Bulk disable and delete work on API keys’ limits. The bulk disable and delete routes for rate-limit rules and budget caps rejected every row stored for an API key (api_key_lineage, the kind a key’s limits are stored under) as an unknown subject kind.
    • The console’s effective-permissions preview shows real gateway access. It now counts only roles that grant gateway use and skips team-scoped assignments, as the gateways do, where it used to count every assigned role.

    Changed#

    • Seven permissions that nothing checked are retired: team:read, team:write, logs:read_own, logs:read_team, audit_logs:read_own, audit_logs:read_team and audit_logs:read_all. Every log endpoint, audit logs included, is gated on logs:read_all at global scope, and no own- or team-filtered log view exists, so these grants never did anything. They are gone from the permission catalog, the role editor and the seeded roles. Roles that still name them load, and the server logs a warning at start instead of refusing to boot.
    • Deleting one rate-limit rule or budget cap is bound to the subject in the path. DELETE on /api/admin/limits/{kind}/{id}/rules/{rule_id} and /api/admin/limits/{kind}/{id}/budgets/{cap_id} now answers 404 when the rule or cap belongs to a different subject; it used to delete any row id once the path’s subject was authorized.
    • Creating or editing a key with an explicit allow-list checks it against gateway-granting roles only. An allowed_models or allowed_mcp_tools entry is refused with 400 when no role of the owner that grants the gateway covers it, so a user without a gateway role can no longer save a non-empty list.
  2. ThinkWatch 企业版 v2.1.0 EN GitHub ↗

    Amazon Bedrock becomes a provider you can run from the console. It authenticates with a Bedrock API key as well as access keys or the instance role, lists its models for import and for the route editor, and has a working Test Connection. Requests converted for Bedrock now keep their prompt-cache breakpoints and Claude’s thinking settings. The route editor takes any upstream model name, and a route the provider refused can be created anyway. ThinkWatch-Core moves to v0.55.0, whose Bedrock layer this edition now shares with the desktop gateway. No database, setting, environment variable or Helm value changes.

    Read before upgrading#

    • Test Connection with a saved provider’s secrets needs providers:update. A test that names a saved provider (provider_id, which is what the Edit dialog sends) used to take only providers:create. A custom role that has providers:create without providers:update can no longer test existing providers; the built-in admin role has both. A test with values typed into the request still takes providers:create. See Security below.
    • Refusing a route the provider does not serve answers model_not_served. POST /api/admin/models/{model_id}/routes still answers 400 when the import probe found no API the provider serves the model on, but error.type is now model_not_served, where it was bad_request. Scripts that matched on bad_request for this case need the new value. The request takes a new optional "force": true to create the route anyway.
    • error_type in gateway_logs for failed requests is always the error’s tag. An upstream HTTP error used to log its debug text, such as ProviderHttpError { status: 502, message: "…" }, and an upstream rate limit a cut-off UpstreamRateLimited { retry_after_secs: Some. They now log ProviderHttpError and UpstreamRateLimited, as the metric labels and streamed requests already did. Dashboards or log forwarder queries that grouped by those strings see them merge into one value each.
    • The server log now carries the upstream’s reply to a 401 or 403, as it already did for other upstream errors. For Bedrock that reply names the AWS account and the IAM principal. Callers still see only “Authentication failed with upstream”, and failover, breakers and gateway_logs treat these errors as before.
    • Prompt caching now works on Bedrock routes, and is billed at cache prices. Requests converted for Bedrock used to lose their cache_control breakpoints, so Claude on Bedrock re-read the whole prompt at full price every turn. Breakpoints now go out as Converse cachePoint blocks, to Claude and Nova models only (others reject them), and the cache reads and writes Bedrock reports come back into usage, one-hour writes included. Traffic with repeated prompts, such as Claude Code, costs much less on Bedrock routes than it did; writes cost a little more, at cache_write_weight / cache_write_1h_weight.
    • Claude’s thinking reaches Bedrock. A request converted for Claude on Bedrock now carries its thinking and effort settings, in the form Anthropic’s API uses, along with the sampling limits thinking imposes (temperature 1, no top_k, top_p at least 0.95). Thinking used to be dropped on the way to Bedrock. Other Bedrock models still get no thinking.

    Added#

    • Bedrock providers can authenticate with a Bedrock API key. Pick Bedrock API Key as the authentication mode and paste the key AWS generated. It is kept like any other provider’s API key, as an encrypted Authorization: Bearer header, and replaced in the provider’s Edit dialog. A Bedrock provider that sends an Authorization header is not SigV4-signed; one without it is signed as before, with access keys or the instance role. Use a long-term key: a short-term one expires within 12 hours.
    • Bedrock models can be imported, and are suggested in the route editor. A Bedrock provider now lists what it can be routed to: the region’s foundation models that can be invoked on demand and answer in text, and the inference profiles AWS defines, such as us.anthropic.claude-sonnet-4-5-20250929-v1:0. A model that is only served through an inference profile, as most current models are, is listed under its profiles’ ids and not its own. Whether the account may call a model is still checked one model at a time when it is imported. The provider’s credential needs bedrock:ListFoundationModels and bedrock:ListInferenceProfiles; the AmazonBedrockLimitedAccess policy a long-term API key is created with allows both.
    • Test Connection for Bedrock providers, however they authenticate: with an API key, access keys or the instance role. It lists the models above, so a wrong credential or a missing permission shows up before any traffic does.
    • Create anyway, for a route the provider refused. When the route editor’s save is refused because the provider does not serve the model, the error now offers Create anyway: the refusal can be stale, or be about the probe’s request rather than the model. The route is created, and its model_route.created audit row records the refusal it overrode in a new refusal_overridden key.

    Changed#

    • The route editor’s upstream model field takes any model name. For a provider that lists its models, the field used to turn into a list to pick from, so a model the listing leaves out could not be routed to from the console. It now suggests the provider’s models as you type, and takes whatever is typed.
    • Bedrock instance-role credentials are cached. They took three IMDSv2 round trips per request; they are now kept until five minutes before they expire, and a burst of requests at expiry makes one trip.
    • Chat Completions requests can switch reasoning with thinking. When a request is converted for another format, thinking.type (enabled / disabled, as DeepSeek, GLM and Kimi write it) now turns reasoning on or off; disabled wins over reasoning_effort.
    • Core crates at ThinkWatch-Core v0.55.0. tw-dialect, tw-guard and tw-breaker move from v0.43.0, and tw-bedrock joins them: SigV4 signing, eventstream unframing, Bedrock’s addresses, region checks and model catalog now come from core. Where credentials come from (provider keys, the instance role) stays in this repository. tw-breaker is unchanged.

    Fixed#

    • Bedrock providers could not be created or edited in the console. The region was checked as a URL, so saving failed with 400 Invalid URL. A Bedrock provider’s base_url is now checked as an AWS region such as us-east-1, and anything else is refused, since the host is built from it. A provider saved with a URL there never reached Bedrock; set its region in the Edit dialog.
    • Bedrock models the account may not call were imported anyway. Bedrock refuses such a model (model access not granted, or an IAM or organization policy that denies it) with a 403, and the import probe read every 403 as a credential problem that says nothing about the model. The route was created and failed on first use. Now, when the region’s control plane accepts the same credential, the refusal is recorded as the model’s, with AWS’s reason, and the model is skipped on import like any other refused one. Once access is granted, re-check the provider’s models.
    • A Bedrock provider whose stored secret key would not decrypt signed with an empty secret, and every request failed with AWS’s SignatureDoesNotMatch. It now refuses its requests with the reason until the keys are saved again, rather than falling back to the instance role, which would call AWS as a different identity. An access key ID saved without a secret is refused the same way.
    • A Bedrock model id that is an ARN could not be routed. Its / went into the request path unescaped, adding a path segment AWS could not route. It is now escaped.
    • A base URL ending in its version segment doubled it. A provider written as https://api.openai.com/v1 sent requests, and the model listing, to /v1/v1/… and got 404s. A trailing version segment (v1, v1beta, …) that the request path starts with is now written once. Base URLs without one are unchanged.
    • Typing into a provider’s API key field and clearing it again wiped the saved key on save. The field sent Bearer with nothing after it. A cleared field now keeps the saved key, as a blank one always did.

    Security#

    • Testing a connection with a saved provider’s secrets takes providers:update. The test fills each header left blank with the saved provider’s secret and sends it to the URL in the request, and providers:create was enough to ask for it. So a user allowed only to create providers could send any saved API key to a server of their own. A test that names a saved provider (provider_id) now takes providers:update, which lets a user point that provider elsewhere anyway; a test without one still takes providers:create. A user with providers:update alone can now use Test Connection in the Edit dialog, which used to answer 403.
  3. ThinkWatch Lite v2026.9.26 GitHub ↗

    MCP 页的服务器表只为这台电脑上检测到的客户端显示一列:客户端目录或 MCP 文件存在,或手动指定过位置。此前表格列出全部八个客户端,超出窗口宽度;未检测到的客户端现列在表格下方,装在别处的客户端可通过「指定位置…」选择。

    页面名称固定显示在窗口工具栏中侧栏开关的右侧。

    内置 core 仍为 0.56.0,配置与请求记录不受影响。

  4. ThinkWatch Lite v2026.9.25 GitHub ↗

    升级须知:内置 core 升级到 0.56.0(控制面协议 30)。远程服务器上的 core 也需升级到 0.56.0,新旧版本之间无法连接。请求记录保留。策略组不再有「会话粘滞」开关;此前关闭过该开关的配置中,对应策略组下有一行 session_affinity: false,core 0.56.0 不接受这样的配置,应用会以安全模式启动并指出该行,删除这一行即可。

    故障转移#

    • 上游开始回答后、给出任何内容之前在流中报错(如 Anthropic 的 overloaded_error、ChatGPT 账号额度用完时的 response.failed),此前错误会直接交给客户端。现在流式回答会先暂存到第一段内容到达;在此之前报错,请求改发路由中的下一个上游,已收到的部分原样转交。
    • 上游拒绝凭据(401、403)、余额不足(402)或没有该模型(404)时,同样改发下一个上游。400、422 只有在上游说明余额或额度用完、模型不可用时才改发,其余客户端错误照旧直接返回。没有下一个上游时,客户端收到最后一个上游的原始错误。
    • 上游失败后的暂停时长按其给出的原因确定:余额不足暂停 30 分钟;额度用完暂停到上游说明的重置时间(未说明时 1 小时);限流按上游要求的等待时间(最长 1 小时)。未说明原因的失败连续 3 次后暂停 1 分钟,之后每次加倍,最长 10 分钟。
    • 以上时长以及等待流式回答开头的时长(15 秒)都可在「设置 › 故障转移」中修改。

    对话与提示缓存#

    • 一段对话会留在回答它的上游(包括故障转移后接手的上游):同一轮内一律不换;之后的轮次在该上游的提示缓存仍有效时继续留下。负载均衡策略组只在新对话之间轮换。
    • 每一轮开始时定下的路由沿用到这一轮结束,按输入大小或图片写的规则不会再在智能体工作到一半时切换上游。
    • 请求详情的「路由」页会说明请求是否沿用了本轮路由、是否留在了上次回答的上游。

    导入链接#

    • 中转站或服务商可以提供一条链接(thinkwatch://import?… 或 https://thinkwat.ch/import#…),预填一个新上游的名称、接口地址、协议、API 密钥和模型清单。应用会显示这些设置以及请求和密钥将发往的主机,点「创建」之前不写入配置、不发出任何网络请求。一条链接只会新增一个上游,不能修改已有上游、请求头、代理、计价或路由;引用环境变量的密钥会被拒绝。参数说明和链接生成器见官网文档「导入链接」。

    计算 token 数#

    • Claude Code 每一轮都会请求计算 token 数,只有 Anthropic 格式的上游能回答。此前路由到其他格式的上游、或未实现该接口的中转时会失败;现在由网关自行估算,不向该上游发送任何内容,尝试链中显示为「本地估算」。

    更换账号后的推理内容#

    • 一个账号产生的推理内容只有该账号认可,换到另一个账号或密钥时请求会被拒绝。现在故障转移到其他账号后会去掉这些内容重新发送一次,这段对话之后的轮次也会提前去掉。
  5. ThinkWatch Core v0.56.0 EN GitHub ↗

    This release keeps a conversation on the upstream that holds its prompt cache and decides a turn’s route once, moves a request to the next upstream when a stream fails before its first content, sets an upstream aside for as long as the reason it gives calls for, answers token counts that no upstream can serve with a local estimate, and resends a request whose reasoning another account sealed.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) is now 30. ThinkWatch Lite connects only to a core with the same protocol version, and has to regenerate its protocol types from this release to use it. ThinkWatch Lite 2026.9.24 includes 0.55.2 (protocol 28) and does not connect to 0.56.0: a server used with it stays on 0.55.2 until the app is updated to a release that includes 0.56.0. sudo twcore upgrade --version 0.55.2 --restart switches a server back to 0.55.2.
    • The request store’s schema is unchanged (22): the request history is kept.
    • groups[].session_affinity is removed. A configuration that still sets it (only session_affinity: false was ever written) is refused; delete the line.
    • Changed in the protocol:
      • Removed: GroupView.session_affinity, GroupView.hurts_cache, GroupInput.session_affinity, DryRunResult.hurts_cache.
      • RequestRouted.affinity and RoutingView.affinity (AffinityView, Stay): whether the request kept its turn’s route and why it stayed with the upstream that answered before.
      • Overview.failover (FailoverView).
      • AttemptOutcome has estimated.
    • New in the configuration: the failover section (see Setting a failing upstream aside).
    • Message codes: new config.failover_range and gw.upstream.stream_opening_error; removed gw.count_tokens.bedrock_model.

    A conversation stays where its cache is. Routing rules used to be evaluated for every request, and only load-balance groups kept a session on one upstream. A rule on input_tokens or on images could therefore switch upstreams in the middle of an agent’s turn, and a failover moved the conversation away from the upstream holding its prompt cache for good.

    • A conversation is recognised by the client’s session header (x-claude-code-session-id, session_id, conversation_id and a few others) together with the content fingerprint, or by the fingerprint alone. A turn lasts from a user message until the next one; tool results belong to the turn.
    • The routing decision made at the start of a turn (rule, group, rewrites) holds for the rest of the turn. It is made again when the input approaches the smallest context window among the candidate models, where the price data gives one.
    • The next request goes first to the upstream that last answered the conversation, including one that took over after a failover: always within a turn, and across turns while its last answer read or wrote at least 1024 cached tokens less than five minutes ago. An upstream that is set aside, not among the candidates or in another group is not held to.
    • load-balance takes turns between new conversations only. session_affinity and the warnings that a group lowers the cache hit rate are gone, since no strategy drops a warm cache in the middle of a conversation any more.

    Errors at the start of a stream fail over. An upstream can answer 200, open a stream and report an error as its first event: Anthropic’s overloaded_error, the Codex backend’s response.failed when the usage limit is reached, Bedrock’s throttlingException, an error chunk from a Chat or Gemini upstream. The client had received nothing yet, and the error still reached it. A streamed answer is now held until its first content arrives, and an error before that point moves the request to the next candidate, as an error status does. What was read while waiting is passed on unchanged. The wait ends after failover.stream_start_wait_secs (15) or 1 MiB; the last candidate is not held.

    Setting a failing upstream aside. Failures are sorted by the reason the upstream gives.

    • 401, 403, 402 and 404 now move the request to the next candidate. A 400 or 422 does so only when its body names an insufficient balance, a used-up quota or an unavailable model; other 4xx still go to the client unchanged. The last candidate’s own 4xx reaches the client as the upstream wrote it.
    • An insufficient balance sets the upstream aside for no_balance_pause_secs (1800). A used-up quota sets it aside until the reset time the upstream names in the body, in its quota headers or in a GLM 429, and for quota_pause_secs (3600) when it names none. A rate limit with Retry-After (or Gemini’s retryDelay) sets it aside for that long, at most rate_limit_max_pause_secs (3600). A missing model moves on without counting against the upstream.
    • Failures without a stated reason pause the upstream after failures_to_pause (3) in a row, for pause_secs (60), doubling with each further pause up to max_pause_secs (600) and starting over after a success.
    • A request with a single candidate is never affected, and when every candidate is set aside they are all tried.

    Counting tokens. /v1/messages/count_tokens and Gemini’s :countTokens routed to an upstream of another format, or to a same-format upstream that answers 404 or 405, are answered by the gateway with an estimate of the system prompt, messages, tool calls and results, and tool definitions; nothing is sent to that upstream. The answer carries x-thinkwatch-local: 1, and the request is recorded as a local answer with no cost. A count does not fail over to another format or another model. Counting through an AWS Bedrock upstream still answers 501 not_supported, after which Claude Code counts precisely itself.

    Reasoning sealed by another account. Reasoning items carry content encrypted or signed for the account that produced them. After a failover from one account to another, the upstream refuses them (Responses’ invalid_encrypted_content, Anthropic’s invalid signature in a thinking block). The request is now sent once more without them, and the refused items are left out up front on the conversation’s later turns to that upstream.

  6. ThinkWatch Lite v2026.9.24 GitHub ↗

    升级须知:内置 core 升级到 0.55.2(控制面协议 28)。远程服务器上的 core 也需升级到 0.55.2,新旧版本之间无法连接。请求记录保留。经路由规则改写模型的请求,费用改按实际发出的模型计算,与此前显示的可能不同。

    Amazon Bedrock 上游#

    • 服务类型中新增 Amazon Bedrock:选择区域,凭据三选一:Bedrock API 密钥;访问密钥(临时凭证另填会话令牌);或 AWS profile,从 core 所在机器的 AWS 凭证文件读取。都可以写成 ${变量名};连接远程 core 时,变量和 profile 在服务器上读取。需要登录或执行命令才能取得密钥的 profile 无法使用,会说明原因。
    • 「检测连接」验证凭据,并列出该区域可用的模型:基础模型、us./global. 开头的推理配置,以及账号自己的应用推理配置。
    • 各种格式的客户端请求都转换为 Bedrock 的 Converse 接口,逐个签名发送;流式、用量统计、输出脱敏和工具调用检查照常工作,提示缓存断点和 Claude 的思考设置也会带过去。
    • Bedrock 只认它自己的模型 ID。客户端请求 Anthropic 的模型名(如 claude-sonnet-4-5)时,需要在路由中添加规则,把模型改为 Bedrock 的 ID。
    • 使用 VPC 端点或代理时,填写其地址和区域。
    • AWS 拒绝凭据时的原文会点名账号和 IAM 身份,客户端收到的是网关自己的说明。
    • 发往 Bedrock 上游的请求不能重放:重放按原样发送记录下的请求,而 Bedrock 只接受 Converse 格式。

    原来直连 Bedrock 的 Claude Code 和 Claude Desktop

    • Claude Code 打开 CLAUDE_CODE_USE_BEDROCK 时不读取网关地址,即使显示已接管,请求仍直连 Bedrock;Mantle、Google Cloud Agent Platform、Microsoft Foundry、Claude Platform on AWS 这几个开关也一样。现在接管 Claude Code 时,会在 ~/.claude/settings.json 中关闭这类开关,还原时按原样恢复。组织统一管理的配置打开的开关无法覆盖,此时拒绝接管并说明原因。之后开关再次打开时,配置检查会指出。
    • 接管确认中列出没有设置 Bedrock 模型的模型变量:这些请求会以 Anthropic 的模型名到达网关,需要添加路由规则。
    • 接管确认中说明网关里是否已有 Amazon Bedrock 上游,并提供「新建 Bedrock 上游…」,按客户端原来的设置预填区域、自定义地址和凭据,凭据只写成 ${变量名} 或 profile 名。客户端配置中明文保存的密钥不会被复制,说明中会指出它的位置。Claude Desktop 接管前正在使用的 Bedrock 配置也同样识别。
    • 接管前显示的改动中,settings.json 的 env 里的凭据(如 /setup-bedrock 写入的 Bedrock API 密钥和访问密钥)一律遮盖;写入空字符串的值显示为 ""。

    跨格式的网页搜索#

    • Claude Code 的每次网页搜索都是一个只有 Anthropic 格式上游才能执行的请求。此前发往 Bedrock、OpenAI 或 Gemini 上游时,搜索工具被丢弃,模型自己的回答被当成搜索结果返回。现在这类请求改发路由中的下一个上游;均无法发送时返回错误,并指明工具和上游。

    费用#

    • 经路由规则改写模型的请求,按实际发出的模型计价;流量页仍显示客户端请求的模型。
    • 长上下文价格按价格数据给出的阈值生效(Claude Sonnet 4 与 4.5 为 200K,GPT-5.4 及之后为 272K),缓存读写计入阈值。
    • 价格数据缺少的价格(如 1 小时缓存写入)会补上并标为估算;价格数据未列出的 Bedrock 模型借用同一模型的价格,同样标为估算。

    流中途出错#

    • 上游开始回答后在流中报告错误(如 Anthropic 的 overloaded_error、Responses 的 response.failed)时,请求记为失败而不是完成,并保留出错前上游报告的用量。
  7. ThinkWatch Core v0.55.2 EN GitHub ↗

    This release stops a Bedrock API key that is not valid from passing the connection check.

    Upgrade notes

    • The control-plane protocol version is unchanged (28) and so is the request store’s schema (22). ThinkWatch Lite 2026.9.23 includes 0.54.0 (protocol 27) and does not connect to 0.55.x: a server used with it stays on 0.54.0 until the app is updated to a release that includes 0.55.2.
    • No message codes change.

    Checking a Bedrock upstream. AWS answers a Bedrock API key that is not valid with AccessDeniedException (“Authentication failed: …”, “Invalid API Key format: …”), the same exception it uses for a valid credential that may not list models. The check took every AccessDeniedException on the model listing to mean the credential works, so a mistyped key passed with “The credential works, and it has no permission to list models”, and the background model fetch said the same. Only a refusal whose message says the identity is not authorized, which is how IAM denies an action, now counts as missing list permission; any other one reports the key as rejected. AWS’s message is only inspected and never passed on, since it names the account.

  8. ThinkWatch Core v0.55.1 EN GitHub ↗

    This release stops a request that requires a server-side tool, such as Claude Code’s web search, from being sent without it to an upstream of another format.

    Upgrade notes

    • The control-plane protocol version is unchanged (28) and so is the request store’s schema (22). ThinkWatch Lite 2026.9.23 includes 0.54.0 (protocol 27) and does not connect to 0.55.x: a server used with it stays on 0.54.0 until the app is updated to a release that includes 0.55.1.
    • New message codes: gw.convert.tool_unsendable and gw.convert.tools_unsendable.

    Requests that require a server-side tool. Claude Code runs each web search as a request that carries only the server-side web_search tool and forces the model to use it. Converted for an upstream of another format (AWS Bedrock, OpenAI, Gemini), the server tool was dropped, since only the provider it belongs to runs it: the model answered without searching, and Claude Code passed that answer on as the search results. When the tool a request forces cannot be sent in the upstream’s format, that attempt is no longer made. The next upstream of the route is tried, and if none can take the request the client gets a 400 that names the tool and the upstream. Server-side tools offered alongside other tools, which the model is free to leave unused, are still dropped and listed in the request’s details as before.

  9. ThinkWatch Core v0.55.0 EN GitHub ↗

    This release adds AWS Bedrock upstreams. Requests to them are converted to Converse and signed per request, and prompt-cache breakpoints and Claude’s thinking now survive that conversion. Each request is priced by the model it was actually sent as, long-context prices follow the thresholds the price data gives, and an error an upstream reports partway through a stream now counts as a failure.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) is now 28. ThinkWatch Lite connects only to a core with the same protocol version, and has to regenerate its protocol types from this release to use it. ThinkWatch Lite 2026.9.23 includes 0.54.0 (protocol 27) and does not connect to 0.55.0: a server used with it stays on 0.54.0 until the app is updated to a release that includes 0.55.0. sudo twcore upgrade --version 0.54.0 --restart switches a server back to 0.54.0.
    • The request store’s schema is unchanged (22): the request history is kept.
    • Changed in the protocol:
      • Protocol has bedrock.
      • ProviderInput.aws and ProviderView.aws (AwsKeys): the access keys, or the name of an AWS profile, and the region to sign for.
      • ProviderView.region and ProviderPreview.region: the region of a Bedrock upstream.
      • AttemptView.model: the model an attempt sent when a routing rule rewrote it.
    • New in the configuration: the protocol bedrock and providers[].aws. The 200K long-context threshold of a price sheet’s input_above_200k and output_above_200k now counts cache reads and writes (see Priced by the model that was sent).
    • New message codes:
      • config.credential.: bedrock_oauth, key_and_aws, bedrock_no_credential, aws_not_bedrock, aws_empty, aws_profile_and_keys, aws_empty_profile, aws_signed_header, aws_no_region, aws_bad_region, aws_region_mismatch
      • config.aws_profile.: no_home, unreadable, not_found, unsupported, no_keys
      • gw.upstream.: aws_token_expired, aws_profile_expired, bedrock_refused, bedrock_refused_unnamed, sign_failed, eventstream_broken, stream_exception, stream_error
      • gw.probe.aws_token_expired, gw.probe.bedrock_list_denied, gw.count_tokens.bedrock_model, gw.count_tokens.bedrock_upstream, control.replay_bedrock, l3.sign_failed

    AWS Bedrock upstreams. An upstream at https://bedrock-runtime.<region>.amazonaws.com is recognized as bedrock. It authenticates in one of three ways:

    • A Bedrock API key in key, sent as Authorization: Bearer.
    • AWS access keys in aws: access_key_id, secret_access_key and, for temporary credentials, session_token, each of which can be ${VAR}. Every request is signed with them (SigV4) once its body is final.
    • aws.profile: the access keys of a profile in the AWS credential files (~/.aws/credentials, ~/.aws/config, or the files AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE name) on the machine core runs on. The files are read again when they change, so a tool that refreshes temporary keys there needs no restart. Nothing is run to obtain a credential: a profile that signs in through IAM Identity Center, runs a credential_process or assumes a role is refused, naming the setting.

    Requests in every client format are converted to Converse and ConverseStream. The binary eventstream is turned into server-sent events as it arrives, so usage, the first token, output redaction and tool-call inspection work as they do for any other upstream. The anthropic-beta values Bedrock accepts for Claude are carried into the request body. The model list comes from the region’s control plane: the foundation models that can be invoked on demand, the inference profiles AWS defines (us.anthropic.claude-…, global.…), and the account’s application inference profiles, listed by the ARN they are invoked by. Checking the connection and the inference speed test go through the same signing. For a VPC endpoint or a proxy, write its address in base_url and the region in aws.region.

    • AWS’s text for a refused credential (HTTP 401 or 403) names the account and the IAM identity. The client receives the gateway’s own sentence instead, and AWS’s text is not recorded as the request’s error.
    • An expired session token is named as such; for a profile, the message says to refresh the profile.
    • An exception AWS sends inside a stream ends the request as a failure.
    • count_tokens for a model that only Bedrock upstreams serve is answered with 501 not_supported, the answer Claude Code’s guidance for gateways names; Claude Code then counts with a one-token request. Bedrock’s own CountTokens covers only some older Claude models on bedrock-runtime.
    • A replay to a Bedrock upstream is refused: a replay sends the recorded request as it was, and Bedrock takes only the Converse format, which no client sends.

    The code that speaks to Bedrock (signing, the eventstream, the endpoints and the model catalog) is the new layer-one crate tw-bedrock, which ThinkWatch Enterprise shares.

    Converse keeps cache breakpoints and thinking. The conversion to Converse used to drop cache_control, so a client that cached its prompt never hit the cache through Bedrock. Cache breakpoints now become cachePoint blocks (at most four, as Bedrock allows), with the one-hour TTL where the client asked for it, and cache reads and writes, one-hour writes included, are read back. Claude’s thinking and effort are sent in additionalModelRequestFields, and its reasoning comes back as reasoning content.

    Priced by the model that was sent. A routing rule can send a request as another model, and the upstream charges for the model it received. Each attempt now records the model it sent when a rule rewrote it (AttemptView.model), and a request is priced by the model of the attempt that served it. The row still names the model the client asked for. The list of unpriced models names the model a price has to be set for.

    • A Bedrock model the price data does not list borrows a price, always marked estimated: an inference profile from the model it is named after, and an Anthropic model on Bedrock from Anthropic’s own name for it.
    • Long-context tiers are read with the threshold the price data gives (200K for Claude Sonnet 4 and 4.5, 272K for GPT-5.4 and later), including their cache prices. A request’s input counts its cache reads and writes when the tier is chosen. The 272K tier was not recognized before, and a request with a warm cache was priced as a short one.
    • A price the data does not give is filled in, and the cost is marked estimated whenever it is used: a one-hour cache write costs twice the input price (it used to fall back to the five-minute price), and the missing cache prices of a long-context tier grow with its input price.

    Errors partway through a stream. An upstream can answer 200, stream part of an answer and then report an error in the stream: Anthropic’s overloaded_error, Responses’ response.failed, an error in a Chat or Gemini frame. Such a request was recorded as finished; it is now a failure, with the usage the upstream reported before the error.

    Listed models. GET /v1/models and GET /v1/models/:model no longer stamp each model with the time of the request. created and created_at are now the Unix epoch, since the gateway does not know when a model was released.

  10. ThinkWatch Lite v2026.9.23 GitHub ↗

    升级须知:内置 core 仍为 0.54.0,远程服务器上的 core 无需变动;请求记录保留。

    更新#

    • 更新后窗口保持更新前的状态:更新前只在菜单栏运行,更新后仍只在菜单栏;更新前窗口开着,更新后重新打开。此前每次更新后都会打开主窗口。各平台的应用内更新和 macOS 上的 Homebrew 升级都适用;提醒设为「系统通知」时,Homebrew 升级后也会发出「已更新到」的通知。
    • 从 2026.9.22 更新到这一版时尚不生效:2026.9.22 退出时不记录窗口状态,这一次仍会打开窗口,也不会发出「已更新到 2026.9.23」的通知。

    侧栏#

    • 侧栏展开时,悬停在某一页上不再弹出重复页名的说明。这一页的快捷键(⌘1 至 ⌘9,Windows 和 Linux 上为 Ctrl+1 至 Ctrl+9)在指针移到这一行或键盘聚焦到这一行时显示在行尾。侧栏收起时,页名和快捷键仍以悬浮说明显示。
  11. ThinkWatch Lite v2026.9.22 GitHub ↗

    升级须知:内置 core 升级到 0.54.0(控制面协议 27)。远程服务器上的 core 也需升级到 0.54.0,新旧版本之间无法连接。请求记录的存储格式有变化,升级后请求记录会清空;配置、密钥和上游不受影响。

    首 token 时间#

    • 延迟改为统计到回答的第一个 token,而不是响应头。流式响应的响应头通常一收到请求就返回,第一个 token 要等上游排队、读完输入之后才到。流量、概览和上游页显示的都是这一时间,响应头的时刻移到请求详情的明细中。

    生成速度#

    • 新增生成速度(token/秒),从第一个 token 算到回答结束。
    • 流量页:悬停在延迟上,显示首 token、总耗时、生成用时和生成速度。
    • 请求详情:顶部显示首 token、总耗时、生成速度、token、费用五项,比例条分为等待首 token 与生成两段。
    • 概览:新增「生成速度」一节,按模型和按上游给出中位数。
    • 上游页:最后一列显示首 token 与生成速度的中位数。
    • 无法如实测量的请求不给出速度:非流式响应、取消和失败的请求、生成不足半秒的请求,以及推理过程被隐藏且上游未报告推理 token 数的请求。

    菜单栏#

    • 菜单中的生成速度此前有误,一次非流式请求就可能把它推高到每秒数万 token,现已改用同一套测量。
    • 电脑睡眠跨过零点后,「今日」会一直停在前一天的 token 和费用,直到有新的请求或打开菜单。现在电脑醒来、修改时钟或时区时立即更新;流量页区分今天与更早请求的分界、密钥页的 24 小时用量也随之更新。

    路由#

    • 规则的模型条件、「模型改为」和试算中输入的模型名,不在建议列表中也会保留。此前列表收起时,建议中没有的名称(例如中转站自己的模型名、上游列表获取之后才发布的模型)会被清空,比建议短的名称会被换回之前选过的建议。
  12. ThinkWatch Core v0.54.0 EN GitHub ↗

    This release records when each response’s first token arrives and how fast the response is generated, and corrects the generation rate that GET /live reports.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) is now 27. ThinkWatch Lite connects only to a core with the same protocol version, and has to regenerate its protocol types from this release to use it. ThinkWatch Lite 2026.9.21 includes 0.53.0 (protocol 26) and does not connect to 0.54.0: a server used with it stays on 0.53.0 until the app is updated to a release that includes 0.54.0. sudo twcore upgrade --version 0.53.0 --restart switches a server back to 0.53.0.
    • The format of the request history changed (request store schema 22, from 21). On its first start, 0.54.0 replaces the request history and the stored request and response bodies of an earlier version with an empty store; the configuration is kept. Switching back to an earlier version empties it again.
    • Changed in the protocol: the new event RequestFirstToken { id, ttft_ms }, RequestFinished.tokens_per_sec, HistoryRow.ttft_ms and HistoryRow.tokens_per_sec, and the new endpoints GET /token-rate and GET /token-rate/provider, which return TokenRateView { model, p50, samples }. GET /latency and GET /latency/provider now report the time to the first token. No message code changed.

    First token. The headers of a streamed response usually come back as soon as the upstream receives the request. The upstream then queues the request and reads the prompt before it produces anything, and the time a user waits is the time to the first token, not to the headers. The gateway now reads each successful streamed response in the upstream’s format and reports the moment the first text, reasoning or tool call arrives; opening frames such as message_start and response.created do not count. A response that is not streamed arrives whole and has no first token. GET /latency and GET /latency/provider now give percentiles of this time, so responses that are not streamed are left out of them. url-test groups still pick the upstream with the fastest response headers.

    Generation speed. Each finished streamed request has a generation speed: the output produced after the first token, divided by the time from the first token to the end.

    • When the model reasoned before the first token without streaming its reasoning, those reasoning tokens were generated outside that time. They are subtracted when the upstream reports how many there were, as OpenAI and Gemini do. When it does not, as with Anthropic, the request has no speed rather than one several times too high.
    • Reasoning that is streamed as it happens is counted.
    • Responses that are not streamed, cancelled and failed requests, and generations shorter than half a second have no speed.

    GET /token-rate and GET /token-rate/provider give the median speed by model and by upstream, with the number of samples.

    The live rate. GET /live used to divide each request’s output by the time after its response headers. A response that is not streamed gets its headers only when generation is done, so that time was a few milliseconds, and one such request could push the rate to tens of thousands of tokens per second. The rate now combines the speeds of the requests that have one, weighted by their generation time.

  13. ThinkWatch Lite v2026.9.21 GitHub ↗

    升级须知:内置 core 升级到 0.53.0(控制面协议 26)。远程服务器上的 core 也需升级到 0.53.0,新旧版本之间无法连接。请求记录保留。上游看到的 User-Agent 改为 ThinkWatch 自己的;Kimi For Coding、百炼 Coding Plan 和只允许官方客户端的中转站,需要在该上游的连接设置中打开「转发客户端身份」。

    发给上游的内容#

    • 每个上游只收到请求本身和其接口需要的请求头。客户端的 User-Agent、SDK 信息、会话 ID、安装 ID 以及浏览器的 Cookie 不再转发。
    • 请求体中客户端自动填写的身份字段一并去掉,例如 Claude Code 的 metadata.user_id(含设备 ID、账号 ID 和会话 ID)。
    • 上游对话框的连接设置新增「转发客户端身份」,把客户端自己的 User-Agent 和身份原样发给该上游,用于只接受特定客户端的上游。默认关闭(Z.ai 登录建立的上游也是),ChatGPT 账号不提供。

    ChatGPT 账号#

    • 账号可用的新模型(如 GPT-6-Luna)会列入模型清单并可以使用;此前会报「No upstream serves model gpt-6-luna」。
    • ChatGPT 应用内 Codex 的请求不再把该应用的设备证明和安装 ID 转给 OpenAI。
    • 请求体改为 zstd 压缩发送,长对话上传更快。
  14. ThinkWatch Core v0.53.0 EN GitHub ↗

    This release changes what ThinkWatch sends to upstreams. Each upstream now receives the request and only the headers it needs; the client’s own identity stays with ThinkWatch unless an upstream asks for it. It also fixes two problems with ChatGPT account upstreams.

    Upgrade notes

    • The control-plane protocol version is now 26 (it was 25). The request store’s schema is unchanged (21). ThinkWatch Lite 2026.9.20 includes 0.52.1 (protocol 25) and does not connect to 0.53.0, so a server used with it stays on 0.52.x until the app is updated to a release that includes 0.53.0.
    • Upstreams now see ThinkWatch’s User-Agent instead of the client’s. An upstream that admits only certain clients, such as Kimi For Coding, Bailian Coding Plan or a relay restricted to official clients, needs the new provider field forward_client_identity: true.
    • New message code: config.credential.chatgpt_client_identity.

    Each upstream gets only what it needs. Client request headers used to reach the upstream almost unchanged. Only credentials, a few transport headers and, when a request was translated, the client format’s own protocol headers were removed. Claude Code’s User-Agent, x-app, x-stainless-* and session ID, Gemini CLI’s installation ID and a browser client’s Cookie and Origin all went through. Outbound headers now come from three sources only:

    • Set by ThinkWatch: its own User-Agent, and content-type when the body was translated.
    • The upstream’s configuration: the credential and the headers written there. OpenAI-Organization and OpenAI-Project belong here, because they belong to the upstream’s key.
    • The client’s request, only for the headers the upstream’s protocol uses:
      • Anthropic: anthropic-*, except anthropic-dangerous-direct-browser-access.
      • OpenAI: Idempotency-Key and X-Client-Request-Id.
      • Gemini: none.
      • Every protocol: content-type and accept when the body passes through unchanged.

    An Anthropic request without anthropic-version now gets the default one. When the body passes through unchanged, identity fields that clients fill in themselves are removed from it: Claude Code’s metadata.user_id, and the Codex installation ID and turn metadata in client_metadata.

    forward_client_identity. With this field on, an upstream also receives the client’s own User-Agent, x-app, originator, version, session_id and x-codex-* headers, and the body’s identity fields are kept. These are the client’s own values; nothing is made up. The field is off by default. A ChatGPT account upstream refuses it: that upstream always names ThinkWatch as the sender.

    ChatGPT account upstreams.

    • New models were missing from the list. The Codex backend leaves a model out of the list when its minimum client version is above the client_version that the caller reports. ThinkWatch reported 0.0.0, so a model such as gpt-6-luna, which needs Codex 0.155.0, was not listed, and requests for it were refused with “No upstream serves model …” even though the account can use it. ThinkWatch now reports 99999.0.0, so the list shows every model the account can use. Requests still name ThinkWatch as their origin.
    • The ChatGPT app’s identity is no longer forwarded. The Codex inside the ChatGPT desktop app adds an attestation generated by the app (x-oai-attestation), its installation ID and turn metadata. ThinkWatch forwarded them together with the token it had obtained itself, and on one account OpenAI revoked that token within seconds of the first such request.
      • From the client, the Codex backend now receives only x-codex-turn-state, x-openai-internal-codex-responses-lite and x-codex-beta-features.
      • The attestation, installation ID and turn metadata cannot be set in an upstream’s configured headers.
    • Request bodies are compressed. Requests to the Codex backend are compressed with zstd, which is what Codex does when it talks to that backend itself.
  15. ThinkWatch Lite v2026.9.20 GitHub ↗

    升级须知:内置 core 仍为 0.52.1,连接运行 0.52.1 的远程服务器不受影响,服务器无需改动。

    接管与还原:写入的只会是确认过的那份改动。确认框打开期间客户端改写了自己的配置时,不写入任何内容,对话框改为展示按当前文件重新计算的改动,核对后再次确认;接管、还原、复制或移除 MCP 服务器、把 WSL 改为 mirrored 网络都是如此。还原不再删除接管时新建、之后又被加入其他内容的配置段(Claude Code settings.json 中的 env、新建 opencode 配置中的 provider、DeepSeek Harness 的 refs);DeepSeek Harness 还原时不再报错「refs is not a scalar」,其凭据文件中已有自身写入的内容时保留 version。含有写成 { } 的空对象的配置文件,加入设置时不再多出一个逗号,此前 Claude Code 和 Claude Desktop 会拒绝读取这样的文件。TOML 文件中被修改的那一行保留行尾注释,从 JSON 客户端复制过来的 MCP 服务器中的 null 不再写成空字符串。配置旁的接管记录损坏或属于其他客户端时,接管会被拒绝(还原原本就会拒绝),此前会把网关地址和密钥记成原值,之后还原时又写回去。写入前展示的改动中不再出现密钥:还原时会显示用户自己的密钥,复制 MCP 服务器时会显示文件中所有服务器的环境变量和请求头。配置文件解析失败的提示不再抄出出错的那一行;写入失败时,不再在配置旁留下带网关密钥的临时文件。备份文件只有本人可读;配置所在目录是符号链接(例如指向 dotfiles 仓库)时,写入前会提示。Claude Desktop 在更换密钥后或切换到远程 core 时重新指向,模型列表会一并更新。Windows 上按程序名识别正在运行的客户端,不再把 TombRaider.exe 之类的程序认作 Aider。卸载时,有客户端未能还原则保留数据目录(其中有该客户端唯一的完整备份);删除数据前先停止 core;确认框也列出将要还原的 WSL 中的客户端。「更改路径…」对话框遇到离线的网络路径时不再卡住窗口。新安装时,数据目录按预期设为只有本人可读,其中的 config.yaml 保存着上游密钥。

    数字:统计数据读取失败时,概览显示「暂时取不到」,上游页显示「—」并提供重试,不再显示为 0 或一张空图。菜单栏的今日费用,以及密钥页、上游页的费用,与概览一样把实测、估算、无法计价分开,无法计价时不再显示为 $0。极小的金额显示为「<$0.0001」,不再是「$0.0000」;数字先取整再选单位,写作「10k」「1.0M」,不再是「10.0k」「1000k」。实时档中价格尚未算出的请求,使合计显示为下限「≥」,不再按 $0 计。会话中的一轮只有在补上价格就能算出费用时才标为无法计价;流量页「仅显示无法计价」不再列出失败和无用量的请求。远程连接断开时进行中的请求,会按服务器的记录更正,不再一直标为失败。

    时间范围:菜单栏的「今日」从这台电脑的零点算起,core 运行在其他时区的服务器上、以及夏令时切换当天也是如此。「9/8 至今」这类自定义区间在页面开着期间不再往后漂移。较长的自定义区间按天或按周作图,始终画到当前时刻,此前超过约四个月的区间画不到最近的数据。

    core 与远程连接:切换到远程 core 时,本机正在启动或正在重启的 core 也会停止,此前它会继续被拉起并占用网关端口。安全模式下的 core 显示为安全模式而不是运行中,并可从这里重新启动。远程连接无声断开时(例如电脑睡眠后、换了网络),45 秒内会被发现;服务器接受连接却一直不应答时,连接测试会超时。退出应用时会停止本机 core。

    提醒:安全规则拦截的工具调用、客户端配置中新出现的可疑内容,每次发生都会再次提醒,此前只提醒第一次,重启后也不再提醒。为免短时间内刷屏,同类提醒每五分钟最多弹出一次(拦截按上游分别计算),其间的次数合并成一条说明。等待一分钟再弹出的提醒,若其间网关已停止则不再弹出。「另有 N 项」不再重复计数。上游反复断开时,提醒标题不再随每个请求变长。「已更新到 …」遵守提醒设置。提醒列表和设置改为整份写入,保存中途退出也不会丢失;遇到本版本不认识的设置值(例如较新版本写入的)时,只重置那一项。

    菜单栏与托盘:颜色按最紧张的额度窗口显示,不再只看用量最高的那个。Windows 和 Linux 上,「检查更新」等菜单操作会用通知给出结果,core 返回的原因按界面语言显示。菜单刚被替换时的点击不再触发其他菜单项。core 不再应答时,菜单栏不再停止更新。

    MCP 与安全扫描:扫描范围增加 ~/.claude.json 中按项目配置的 MCP 服务器、Claude Code 中不经模型就执行命令的设置(statusLine、apiKeyHelper 等),以及 Codex 的 notify;会输出凭据的辅助命令只按规则扫描、不在清单中列出,其命令不会显示在界面上。应用启动后才出现的技能和目录同样会被监视,opencode.jsonc 的改动也会。地址中含有 localhost 字样的远程 MCP 服务器不再被当成本机的。Windows 换行或带 BOM 的 SKILL.md 可以正确读取。塞满隐藏字符的文件不再卡住扫描,每个文件的发现有上限,并说明扫描停在何处;指向 /dev/zero 之类特殊文件的链接也不再卡住扫描。钩子命令中的发现(包括隐藏字符)会显示在该钩子所在的行上。

    编辑:使用 Windows 换行的 config.yaml 打开时不再显示有未保存的修改,保存时保持原来的换行。「在配置文件中定位」在密钥和路由同名时也能选中正确的条目。编辑对话框按打开时的配置保存,其间在别处做的修改会以冲突提示,而不是被覆盖;快速连续开关多项时不再报「版本不一致」。价格和路由试算接受小数逗号。上游对话框中连接设置改动后,旧的检测结果会清除。密钥的模型范围在模型列表为空时,也能显示和删除手动输入的模型 ID,并与网关一样不区分大小写。保存路由时,辅助请求类别按编辑后的条件生效,取消勾选或删除的条件不再保留;max_tokens 为 0 时会被拒绝,而不是保存时被悄悄丢弃;复制出的兜底规则可以移到兜底规则之前。重放请求会发往报过价的那个上游。多个对话框不再基于过时的状态操作:测速等新报价返回后才能开始,在上游对话框中删除价目表会一并清除表单中的选择,在「关于」中检查更新不再覆盖其间修改的设置,关闭一条密钥轮换提示不再连带关闭其他提示,Z.ai 登录可接受地址以斜杠结尾的已有上游。日志保留设置的上限较小时,不再无法保存。在输入框中按 Ctrl/⌘+B 不再切换侧栏。Windows 上启动时选择连接、打开更新窗口不再有卡死的风险。

    速度:更新窗口和连接选择窗口只加载所需的一小部分代码,配置编辑器在打开时才加载。较大的 ~/.claude.json 写入前的改动可立即显示。流量页处理每个事件的工作量更少;macOS 菜单栏在菜单打开期间就地更新菜单项。

  16. ThinkWatch Lite v2026.9.19 GitHub ↗

    升级须知:内置 core 升级到 0.52.1。请求库格式不变,请求记录保留。连接远程服务器时,服务器上的 core 须为同一版本 0.52.1(2026.9.18 无法连接 0.52.x 的服务器),服务器和应用需一并更新。版本不一致时,应用会给出在服务器上执行的命令 sudo twcore upgrade —version 0.52.1 —restart。服务器上的 core 从 0.51.0 升级后,其请求记录同样保留。

    上游与代理:编辑对话框按配置中的原样填入各项设置。接口地址保留路径,此前路径会被截去,并提示地址中的凭据部分已隐去。API 密钥填入后默认隐藏,可点眼睛按钮显示,请求头中的鉴权头随之显示或隐藏;${变量名} 形式的引用始终显示;其他请求头按原样填入。OAuth 上游填入 Token 端点、Refresh Token、Client ID 和 Client Secret,其中的密钥默认隐藏;Access Token 一栏仅在凭据新填或有改动时出现。代理填入用户名和密码,密码默认隐藏。保存时提交完整设置,「已保存,留空即保持不变」「移除」「更换」等状态不再出现。接口地址以版本段结尾时(如服务商文档给出的 https://api.openai.com/v1),请求和连接检测不再发往 …/v1/v1/…,接口地址的说明也改为只需不含 /chat/completions、/messages 等接口路径。代理密码可写成 ${变量名} 引用,此前含 ${ 的密码会被拒绝。Z.ai 登录时填写已有 Z.ai 上游的名称,此前提示「这个名称已被占用」而无法继续,现在提示「将替换该上游的密钥」并可继续登录。

    路由:改写模型的规则在目标上游列出了模型时会被拒绝,提示「没有上游提供模型 …」;本应用为 Claude Desktop 使用非 Claude 上游、为 Antigravity CLI 和 DeepSeek Harness 使用其他服务商模型时建议的正是这类规则。现在准入判断、跳过无法处理请求的候选上游、按输入单价选上游的 cheapest 策略组和路由试算,都按实际发给各上游的模型判断;改写后的模型没有上游提供时,错误信息同时写明改写前后的模型。Codex 等使用 OpenAI Responses 或 Gemini 格式的客户端发出的请求,现在能识别出所属会话:开启会话粘滞(默认开启)的 load-balance 策略组此前把这类请求全部发给第一个上游,现在每个会话固定使用一个上游、各会话分散到各成员;流量页也会将其归入会话。

    客户端:客户端页的行菜单(右键或「…」按钮)新增「更改路径…」,用于配置文件不在默认位置的客户端,例如用 CLAUDE_CONFIG_DIR 移动过的 Claude Code、用 CODEX_HOME 移动过的 Codex;「需要手动配置」中的 Cursor、Antigravity CLI 等同样适用。对话框列出接管、MCP 管理和安全扫描读写的位置,三者一起更改,确认前列出每一处要更改的位置及其新旧路径;「恢复默认」可改回默认位置。在 MCP 页的列标题,或「技能与钩子」「发现」中的行上右键,也可打开同一对话框。已接管的客户端需先还原才能更改。更改只对本机生效,WSL 中的客户端仍使用默认位置;Claude Desktop 和 DeepSeek Harness 不能更改。

    远程连接:编辑远程连接时,已保存的密钥会填入对话框并默认隐藏,可点眼睛按钮显示,便于核对和复制。

    菜单栏与托盘:菜单自上而下依次为网关状态、提醒、今日、额度、进行中,其后为「打开主界面」、可切换上游的策略组、「复制网关地址」「复制默认密钥」,最后是「设置…」「连接」「检查更新…」和「退出 ThinkWatch Lite…」;Linux 托盘中「打开主界面」仍在最上方。「撤销上一次配置修改」已移除,配置修改仍可在应用内的版本历史中恢复。以时长命名的额度窗口(如 ChatGPT 账号可能出现的 30 天窗口,此前显示为 30d),在菜单、上游页和系统通知中改为「30 天」等写法。

  17. ThinkWatch Core v0.52.1 EN GitHub ↗

    This release fixes two routing problems: a routing rule that rewrites the model was judged by the model the client asked for, and requests in the OpenAI Responses and Gemini formats were never recognized as part of a conversation.

    Upgrade notes

    • The control-plane protocol version is unchanged (25) and so is the request store’s schema (21). ThinkWatch Lite 2026.9.18 includes 0.51.0 (protocol 24) and does not connect to 0.52.x: a server used with it stays on 0.51.0 until the app is updated to a release that includes 0.52.1.
    • New message codes: gw.model.no_upstream_rewritten and gw.model.not_allowed_rewritten.

    Rewriting the model. A rule that sets the model, for example to send the Claude-style names that Claude Desktop requires to a GLM or DeepSeek upstream, was refused with “No upstream serves model …” whenever the target upstream lists its models. Admission, skipping the candidates that cannot serve a request, and the cheapest group’s price comparison all looked at the model the client wrote, before any rule rewrote it. They now look at the model each upstream will actually be asked for, including a rewrite that applies to one upstream only (provider_would_be), so admission runs after the rules have decided. When a rewritten model is not served, the error names both models. The routing dry run judges the candidates the same way.

    Conversations in the Responses and Gemini formats. Requests from Codex and other clients that use the OpenAI Responses or Gemini format were never assigned to a session, because the fingerprint read only the Anthropic and Chat Completions fields. A load-balancing group with session affinity, which is the default, sent all of them to its first upstream, and the traffic view did not group them into sessions. The fingerprint now also reads instructions and input, and systemInstruction and contents, and uses the request’s prompt_cache_key when it has one. Codex sends one per conversation, and Codex conversations started in the same repository open with identical text.

  18. ThinkWatch Core v0.52.0 EN GitHub ↗

    This release hands ThinkWatch Lite the upstream and proxy settings as they are written in the configuration, so its edit dialogs can show them, and stops repeating the version segment when an upstream’s base URL ends with it.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) is now 25. ThinkWatch Lite connects only to a core with the same protocol version, and has to regenerate its protocol types from this release to use it. ThinkWatch Lite 2026.9.18 includes 0.51.0 (protocol 24) and does not connect to 0.52.0: a server used with it stays on 0.51.0 until the app is updated to a release that includes 0.52.0. sudo twcore upgrade --version 0.51.0 --restart switches a server back to 0.51.0.
    • The request store’s schema is unchanged (21). Upgrading from 0.50.x or 0.51.0 keeps the request history.
    • Changed in the protocol: ProviderView.base_url, ProviderView.key (now a string), HeaderView.value, the new OAuthView.refresh and OAuthView.client_secret, and ProxyView.auth (which replaces has_auth) carry the settings as written; base_url_masked, SecretView and HeaderView.masked are gone. ProviderInput.base_url is required, ProviderInput.key is an optional string and every HeaderInput has a value; ProxyInput.auth is optional. SecretChange and ProxyAuthInput are gone, and so is the message code control.pass_has_expansion.

    Settings as written. The upstream and proxy views used to mask what they showed. The base URL was cut down to its origin, so https://bedrock-mantle.us-east-1.api.aws/v1 was shown without /v1, with a note that credentials had been hidden when there were none. API keys and header values were masked, and OAuth refresh tokens, client secrets and proxy credentials were left out. The app could not fill its edit dialogs in, and every save carried “keep what is saved” states instead of values. The views now carry the settings as they are written in config.yaml, and a save carries the whole definition. The access token of an OAuth credential stays out of the view, and an OAuth credential can still be kept as it is on save: while a dialog is open the gateway may renew the token and write back a new refresh token. GET /config already returned the file unmasked. Diagnostics bundles and logs still mask.

    Proxy passwords from the environment. A proxy password may be a ${NAME} reference, as the configuration reference shows (pass: ${PROXY_PASSWORD}). A password containing ${ used to be refused.

    Base URLs that end with a version segment. A base URL written up to its version segment, as provider docs give it (https://api.openai.com/v1) and as the configuration reference says, got every request and every connection check sent to .../v1/v1/...: the client’s path (/v1/chat/completions) was appended as it was. When the base URL’s path ends with the version segment the request path starts with (v1, v2, v1beta, …), that segment is now used once. Other segments are joined as before.

  19. ThinkWatch Lite v2026.9.18 GitHub ↗

    升级须知:内置 core 升级到 0.51.0。请求库格式有变,新版 core 首次启动时会清空请求记录、报文和安全日志,用量和费用等统计从零开始;配置保留。连接远程服务器时,服务器上的 core 须为同一版本 0.51.0(2026.9.17 无法连接 0.51.0 的服务器),服务器和应用需一并更新。版本不一致时,应用会给出在服务器上执行的命令 sudo twcore upgrade —version 0.51.0 —restart。服务器上的 core 从 0.49.x 升级后首次启动时,同样会清空其请求记录。

    客户端:Claude Desktop 可通过其第三方推理模式一键指向网关,完成后需完全退出 Claude Desktop 再重新打开,出现登录页时选择通过网关继续;此模式下的对话与原有对话分开保存,由组织统一管理的 Claude Desktop 无法接管。DeepSeek Harness 可一键指向网关,其网页版和桌面版的请求经由网关发出,联网搜索仍直接访问 DeepSeek;MCP 页列出其 MCP 服务器,扫描覆盖其补丁文件和技能。客户端页以 Antigravity CLI 取代 Gemini CLI,提供手动配置步骤和专用密钥;不再列出 Gemini CLI,已配置好的 Gemini CLI 不受影响;MCP 页列出 Antigravity CLI 的 MCP 服务器,扫描覆盖其钩子、MCP 配置、技能和 subagent。

    opencode:修复接管后无法选择 ThinkWatch 模型的问题,接管时写入所需的 SDK 包和该密钥可用的模型,适用于 opencode 1.x 与 2.x;上游或路由变化后,客户端页提示更新 opencode 的模型列表。MCP 页可读取 opencode 2.x 的 MCP 写法(mcp.servers),扫描覆盖 opencode.jsonc。

    Codex:还原后,接管期间开启的会话仍可打开,并直接连接 OpenAI;接管前的说明写明接管前后的会话分开列出。

    WSL:Windows 上可接管 WSL 中的 Claude Code 和 Codex,适用于 WSL 1 和使用 mirrored 网络模式的 WSL 2,均经 127.0.0.1 访问网关,网关仍只监听本机。WSL 2 使用 NAT 网络时,客户端页可将其改为 mirrored 网络模式并重启 WSL;mirrored 网络模式需要 Windows 11 22H2 及以上。切换到远程 core 时,WSL 中的客户端可一并改为指向服务器。

    上游:GLM Coding Plan(Z.ai、BigModel)的上游在上游页、菜单栏和托盘中显示 5 小时与每周额度及重置时间,积分制套餐显示剩余积分;额度用完时发送通知。老套餐中 Z.ai 自有 MCP 工具的每月调用次数不予显示。

    流量:请求详情显示 DeepSeek Harness 请求附带的会话日志及其大小。此类请求发往 DeepSeek 以外的上游前,网关会去除会话日志及 DeepSeek Harness 的其他扩展内容。

    网关:支持 Claude Desktop 第三方模式的启动检查,并在长时间无输出时保持流式连接;/v1/files 不再转发至上游;设置监听的网卡尚未出现时,网关先只监听本机,网卡出现后自动加入。

  20. ThinkWatch Core v0.51.0 EN GitHub ↗

    This release stops counting the MCP tool calls of the older GLM Coding Plans as a quota, marks only the weekly window when a model request gets business code 1310, and tells the app at once when a GLM key turns out to have no plan.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) is now 24. ThinkWatch Lite connects only to a core with the same protocol version, and has to regenerate its protocol types from this release to use it. ThinkWatch Lite 2026.9.17 includes 0.49.0 (protocol 22) and no released version of the app includes 0.50.0, so a server used with the app stays on 0.49.x until the app is updated to a release that includes 0.51.0. sudo twcore upgrade --version 0.49.1 --restart switches a server back to 0.49.1.
    • The request store’s schema is unchanged (21). Upgrading from 0.50.0 keeps the request history. Upgrading from 0.49.x or earlier replaces the request history and the stored bodies with an empty store on the first start, as 0.50.0 does; the configuration is kept. Switching back to 0.49.x empties it again.
    • Changed in the protocol: the quota window monthly is gone, and a QuotaSeen with empty windows means the upstream’s quota was taken down. No endpoint, type or message code changed.

    MCP calls are not a quota. The quota endpoint of the older GLM Coding Plans also reports a count (TIME_LIMIT) of calls to Z.ai’s own MCP tools: search-prime, web-reader and zread. Those calls do not go through the gateway, so the count says nothing about the requests it forwards. Core no longer reads it: there is no monthly window, and a used-up count no longer marks the upstream used up or emits QuotaExhausted. credits are reported only for the windows of credit-based plans (CREDIT_LIMIT).

    1310 is the weekly quota. A 429 with business code 1310 on a model request marks the weekly window used up, whatever its message says. 1308 still marks the 5-hour window. For 1316 to 1321 the window still comes from the message when it names the 5-hour or weekly window; otherwise it is the fuller of the two known windows, or the weekly one when neither is known.

    A key without a plan. When a GLM key is found to have no plan, core clears the quota it had stored for that upstream and emits a QuotaSeen with empty windows, so the app removes the quota at once instead of on its next read of /quota.

  21. ThinkWatch Core v0.50.0 EN GitHub ↗

    This release lets Claude Desktop’s third-party mode use the gateway, cleans the extensions DeepSeek Harness adds to its requests before they reach an upstream other than DeepSeek, reads the GLM Coding Plan quota of Z.ai and BigModel upstreams, and recognises requests from Antigravity CLI. A gateway bound to a network interface now starts even when that interface is not there yet.

    Upgrade notes

    • The format of the request history changed (request store schema 21, from 20). On its first start, 0.50.0 replaces the request history and the stored request and response bodies of an earlier version with an empty store; the configuration is kept. Switching back to an earlier version empties it again.
    • The control-plane protocol version (CONTROL_API_VERSION) is now 23. ThinkWatch Lite connects only to a core with the same protocol version, and has to regenerate its protocol types from this release to use it. ThinkWatch Lite 2026.9.17 includes 0.49.0 (protocol 22) and does not connect to 0.50.0: a server used with it stays on 0.49.x until the app is updated to a release that includes 0.50.0. sudo twcore upgrade --version 0.49.1 --restart switches a server back to 0.49.1.
    • New in the protocol: QuotaWindow.credits (a new type, QuotaCredits { total, used, remaining }), the quota window monthly, and session_log_bytes on RequestStarted and HistoryRow. New message code: gw.files.unsupported.
    • /v1/files and /files now answer 404 on the gateway instead of being forwarded to an upstream.

    Claude Desktop. Claude Desktop in third-party inference mode, and the Claude Code engine it embeds, expect a few things of a gateway:

    • HEAD /api/hello, the probe Desktop sends at startup, is answered with 200 by the gateway itself, without a key and without reaching an upstream. It used to be refused with 401.
    • The client gives up on a stream that is silent for about five minutes. When a client speaking Anthropic Messages has received nothing for 15 seconds, the gateway writes event: ping, so a converted upstream that is reasoning or queued at length no longer looks like a dead connection. The timer runs from the last bytes the client received, not from the upstream’s, because conversion drops the upstream’s own keep-alive comments. Pings go only between frames, are not recorded in the stored body or counted as output, and are never sent to clients of other formats. After ten minutes without a byte from the upstream the gateway stops pinging, so a half-open upstream connection ends on the client’s timer instead of lasting forever.
    • /v1/models answers Anthropic clients in the Anthropic shape: type, display_name, created_at, has_more, first_id and last_id, and, for models whose id reads as Claude, anthropic_family_tier, which Desktop uses to resolve aliases such as sonnet. The OpenAI fields stay on the objects, because clients that send only x-api-key are also read as Anthropic.

    DeepSeek Harness. DeepSeek Harness sends DeepSeek’s extensions to whichever base URL it is given: top-level dsh_* fields, among them a session log of the whole conversation of up to 8 MiB per request, role: system entries in messages, tool_addition and tool_removal blocks with defer_loading tools, thinking enabled without a budget, and x-deepseek-harness-* headers. A request is recognised by its User-Agent or a dsh_* field. To DeepSeek’s official endpoint it goes through unchanged. To any other upstream, every request from it, including count_tokens, has the dsh_* fields removed, its system entries appended to system in order, the tool change blocks and defer_loading removed and listed on the hop as a conversion lists what it drops, and a thinking without a budget rewritten to what the model accepts. The x-deepseek-harness-* headers are not forwarded, and the tool changes beta is taken out of anthropic-beta, with every line of a header sent on several lines kept. session_log_bytes gives the size of the session log a request carried, so the app can show that it carried one. /v1/files and /files answer 404: a file uploaded to the upstream picked at that moment cannot be used by a later request routed to another, and the 404 makes DeepSeek Harness send images inline.

    GLM Coding Plan quota. An upstream whose base_url host is api.z.ai or open.bigmodel.cn is read as a GLM Coding Plan upstream. Its responses carry no quota headers, so GET /quota asks the account’s quota endpoint for each enabled one that is due (at most once a minute, waiting up to 5 seconds), and traffic to one asks at most every 5 minutes; failures back off from 30 seconds to 5 minutes. Windows are 5h, weekly and monthly (the monthly MCP calls of the older plans), told apart by their length, and a reset time beyond a window’s length is dropped. Credit-based plans report credits with the three numbers the endpoint gives. A 429 carrying a used-up quota code marks the window used up and emits QuotaExhausted. A key without a plan is left alone for an hour, and a key that had a quota needs two no-plan answers in a row before it is believed, because a transient server error returns the same code.

    Antigravity CLI. Antigravity CLI sends the Go genai SDK’s default user agent. A user agent that starts with google-genai-sdk and carries Google’s internal Go toolchain (gl-go/ followed by cl/) is now reported as the client antigravity-cli. Programs built on the SDK with a public Go release are not.

    Listening on an interface that is not there yet. When listen.gateway.bind names a network interface that does not exist or is offline, or an address that is not on this machine yet, twcore used to exit at startup. The gateway now listens on 127.0.0.1 only and says why in the listen status (gw.listen.no_such_nic, gw.listen.nic_offline), asks the system again every three seconds, adds the interface once it appears, moves to its new address when it changes, and drops back to loopback when it goes away. This lets the desktop app on Windows bind the gateway to the WSL adapter, which appears only once WSL has started and changes address on every restart. A loopback that cannot be bound is still fatal at startup.

  22. ThinkWatch Core v0.49.1 EN GitHub ↗

    This release changes only the server guide, docs/server.md and its Chinese version, so that it describes the desktop app’s side of a remote connection as the app behaves. The binaries behave as in 0.49.0.

    Upgrade notes

    • The control-plane protocol version is unchanged (22) and so is the request store’s schema (20). A server on 0.49.0 does not need to upgrade, and ThinkWatch Lite 2026.9.17, which includes 0.49.0, connects to 0.49.0 and 0.49.1 alike.

    Connecting from the desktop app. The guide names the settings section Connection and the Add remote connection dialog, which also asks for a name. Test connection tests without saving, Save and switch tests before it saves and asks before it switches, and Save stores the connection untested; switching to a remote connection always tests it first and stays on the current connection if the test fails.

    Which version a server runs. The app connects only to a core that speaks its control-plane protocol version, which in practice means the core version the app includes, and the latest release can be newer than that. The guide therefore installs and switches with --version <version>, notes that twcore upgrade --version also installs an older release, and says that recent versions of the app (2026.9.17 and later) show sudo twcore upgrade --version <version> --restart with the version they need when the versions differ. It also says that twcore upgrade leaves the data alone, while a version whose request store has another schema starts with an empty request history.

    While connected to a remote core. The guide adds that the app signs ChatGPT accounts in with a device code only, next to the three things a remote connection cannot do.

  23. ThinkWatch Lite v2026.9.17 GitHub ↗

    升级须知:内置 core 升级到 0.49.0。请求库格式有变,新版 core 首次启动时会清空请求记录、报文和安全日志,用量和费用等统计从零开始;配置保留。连接远程服务器时,服务器上的 core 须为同一版本 0.49.0(2026.9.16 无法连接 0.49.0 的服务器),服务器和应用需一并更新。版本不一致时,应用会给出在服务器上执行的命令 sudo twcore upgrade —version 0.49.0 —restart,服务器上现有的版本较新或较旧均适用。服务器上的 core 从 0.47.0 升级后首次启动时,同样会清空其请求记录。

    界面改版:各页面按统一设计重做,页头给出本页概况,加载时显示骨架,读取失败时可重试;密钥、上游、安全规则等的启停可在随后的提示中撤销。macOS 上主窗口的侧栏改为半透明。

    概览:模型、缓存、延迟、上游各行和失败标记均可点击,打开按所点内容筛选的流量页,模型按名称精确匹配。模型排行中,「无法计价」「无用量」和确为零的费用分开显示;模型有无法计价的请求时,点击其费用即可查看这些请求。延迟一节改为与所选时间范围一致,此前固定为当天。

    流量:页头显示进行中和失败的请求数,以及近 30 分钟每分钟的请求量,点击失败数即只显示失败的请求。进行中的请求逐秒显示已运行时间,并从开始即归入所属会话。选定上游前即被规则拒绝的请求和无可用上游的请求现在也会记录,标为「规则拒绝」「无可用上游」,并计入失败数;请求详情的「路由」列出路由、命中规则、策略组、参数改写和拒绝请求的规则及原因。token 一栏改为输入合计(含缓存读取与写入)→ 输出。

    客户端与密钥:客户端页页头按状态计数,「需要手动配置」和「未检测到」两组可以折叠。密钥页中每把密钥显示近 24 小时逐小时的请求量,有请求进行中时标为「请求中」。

    上游:各行显示服务商图标和近 24 小时逐小时的请求量,有失败的时段标红;行菜单新增「查看流量」。ChatGPT 账号的邮箱和订阅类型改为从已保存的凭据中读取,无需联网,各处显示一致;登录失效后仍显示账号,且不再查询其额度。订阅类型的名称与 Codex 一致。ChatGPT 登录完成时显示所登录的账号,Z.ai 登录完成时不再偶尔缺少账号名。

    路由:页面顶部新增链路图,显示请求从密钥经路由、策略组到上游的去向;悬停表格行或节点时高亮经过的路径,进行中的请求按 core 的路由决定点亮路径。各条规则显示最近 7 天的命中次数,7 天内未命中的标为「未命中」,链路图中从路由出发的连线随请求量加粗;请求记录不足 7 天时按实际记录的时长标注,如「5 小时命中」。

    安全与 MCP:安全页页头显示各档位的防护数,以及所选时段内的确切命中次数和按处置的分类;日志按天分组;单击规则行即可打开,此前需双击。MCP 页页头汇总各级发现和扫描时间;点击服务器行可查看它在各客户端中的配置,不一致的字段会标出。

    设置:左侧新增分节目录,随滚动定位,有未保存修改的一节带标记;切换页面后未保存的修改仍在。页头显示当前连接、网关地址和版本。卸载改为确认对话框,列出将执行的操作并逐项报告结果,有步骤未成功时在标题中说明。

    命令面板:按 ⌘K 打开(Windows 与 Linux 上 ⌘ 为 Ctrl,下同),可跳转页面,执行新建、测速、路由试算、切换连接等操作,并可搜索上游、密钥、路由、客户端、设置项和已加载的请求等。⌘1 至 ⌘9 切换页面,⌘R 现在刷新当前页面的数据,在输入框外按 ? 显示快捷键一览。

    菜单栏:菜单中「进行中」下的已运行时间改由 core 计算,连接远程 core 时不再受两台机器时钟差异的影响。英文界面下,macOS 菜单栏菜单中「额度」和「今日」两部分的文字不再重叠;Windows 与 Linux 托盘中的本机连接改称 This computer。

  24. ThinkWatch Core v0.49.0 EN GitHub ↗

    This release states how far back the route and rule hit counts reach, and reports the signed-in account in the status of a ChatGPT sign-in. Release pages now begin with a summary of the release and list the downloads for each platform with the commands for a server installation.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) is now 22. ThinkWatch Lite connects only to a core with the same protocol version, so a server has to run the core version that the app includes. ThinkWatch Lite 2026.9.16 includes 0.47.0 (protocol 20) and does not connect to 0.49.0: a server used with it stays on 0.47.0 until the app is updated to a release that includes 0.49.0. sudo twcore upgrade --version 0.47.0 --restart switches a server back to 0.47.0.
    • GET /summary/routes returns an object, RouteStats { covered_since_ms, routes }, instead of an array of RouteHits; routes holds what the array held.
    • ChatgptLoginStatus.plan is removed. ChatgptLoginStatus.account carries the email and the plan.
    • The request store’s schema is unchanged (20), so 0.48.0 and 0.49.0 keep each other’s request history. Upgrading from 0.47.0 or an earlier version empties the request history on the first start, as 0.48.0 does.

    How far back the hit counts reach. GET /summary/routes counts the stored requests, so its counts reach back only as far as the stored history. On a new installation, after an upgrade that emptied the request history, or when requests are kept for fewer days than the window, the oldest stored request is later than the start of the window, and before it a rule without hits is unknown rather than unused. covered_since_ms is the later of the window’s start and the time the oldest stored request started; when it is set, it lies within the window. It is null when the stored history covers no part of the window: the store holds no request, or the oldest one started at or after the end of the window.

    The account after a ChatGPT sign-in. The status of a ChatGPT sign-in (GET /chatgpt/login/{id}) reports the account it signed in to in account, with the email and the plan. It is the same block as ProviderView.oauth.account and is read from the same access token, the one the sign-in saves in the configuration, so the sign-in and the upstream report the same account.

    Release pages. A release is titled “ThinkWatch Core” followed by its version. Its page begins with a summary of the release when one is written, followed by a table of the files for each platform, the commands that install the version on a Linux server and switch an existing installation to it, how to verify a download against its .sha256, and the list of pull requests. The pages of 0.47.0 and 0.48.0 carry the same sections.

    Documentation. The README describes installing the prebuilt binaries, the Linux install script and running twcore as a service on a server, and lists all of the crates, grouped by dependency order, including the three that ThinkWatch Enterprise depends on (tw-dialect, tw-guard and tw-breaker).

  25. ThinkWatch Core v0.48.0 EN GitHub ↗

    This release reports more of what core knows about each request: the session and the route from the moment a request starts, a snapshot of running requests that a client can replay, and how often each route and rule was used. It also names the account signed in on a ChatGPT account upstream, counts the requests in each cost group that have no price or no usage, and gives the security log totals for the whole window.

    Upgrade notes

    • The control-plane protocol version (CONTROL_API_VERSION) is now 21. ThinkWatch Lite connects only to a core with the same protocol version, so a server has to run the core version that the app includes. ThinkWatch Lite 2026.9.16 includes 0.47.0 (protocol 20) and does not connect to 0.48.0: a server used with it stays on 0.47.0 until the app is updated to a release that includes 0.48.0. sudo twcore upgrade --version 0.47.0 --restart switches a server back to 0.47.0.
    • The format of the request history changed (request store schema 20). On its first start, 0.48.0 replaces the request history and the stored request and response bodies of an earlier version with an empty store; the configuration is kept. Switching back to an earlier version empties it again.
    • GET /in-flight returns an object, InFlight { now_ms, requests: [{ id, events }] }, instead of an array of start events. RequestStarted.session_fp is replaced by session. ChatgptUsage.email and ChatgptUsage.plan are removed; the signed-in account is in ProviderView.oauth.account.

    Session and route from the start. The gateway assigns a request to a session when the request starts (the same conversation, within 30 minutes of its previous request), and RequestStarted carries that session id, the one the request is stored under. The first routing phase now finishes before the start event, so RequestStarted also carries the route, the rule, the strategy group and the rules that rewrote the request. RequestRouted carries the final record, including rewrites from the second phase and denied_by for a rule that denied the request there. The stored routing record has the same fields and is written from the start event, so a request that ends before routing completes still shows the route and rule it matched.

    Requests a rule decided. A request that a rule denies in the first routing phase, or that none of the upstreams a rule chose can serve, used to produce no events and no stored row. It now produces start, routed and failed events and a row, and counts among the requests and failures in /summary; a denial in the second phase now also produces the routed event. Requests that fail before a rule decides (authentication, model admission, no matching rule) are still not recorded.

    Live views. GET /in-flight returns core’s clock and, for each running request, the events seen so far in order, so that a client connecting while requests are running can replay them and compute elapsed time from core’s timestamps. The running requests in GET /live add elapsed_ms, session, route, rule, group and upstream.

    Rule hit counts. GET /summary/routes reports, for a time window (today by default), each route’s requests, failures and last request, and for each rule the requests it decided, the requests it applied to, their failures and the last one. The counts come from the stored routing records, not from the current configuration.

    Cost groups and the security log. Every group returned by GET /summary/buckets/by reports unpriced_requests and no_usage_requests, so a group with no cost can be told apart as unpriced, missing usage or free. GET /security/events adds total and by_outcome (recorded, replaced, cut, blocked): the counts for everything the query’s guard and time window match, the same on every page.

    ChatGPT account upstreams. ProviderView.oauth.account gives the email and plan of the account signed in on a ChatGPT account upstream. Both are read from the access token core already keeps with the credential: nothing is requested from the network, nothing new is stored, and the account and user ids in the token are not exposed. The plan is reported as the backend names it, including plans that core does not list.

    Releases. A release is now published by a single job once every platform has been built and each file matches its SHA-256. Previously the first platform to finish created the release, and made it the latest, while the others were still building, and each build job added another copy of the release notes. The server guide, docs/server.md, states that ThinkWatch Lite keeps a remote connection’s key in a private file in its data directory, and applies to the app on macOS, Windows and Linux. The crates carry homepage and documentation links, and twcore --help begins with the binary’s description.

    Tests. Tests no longer hand core a port that was just released, which could make tests that bind ports, such as those of the remote control port, fail intermittently on Linux.

  26. ThinkWatch Lite v2026.9.16 GitHub ↗

    升级须知:代理配置现在会拒绝拼错的字段名(例如把 type 写成 typ)。以往这类字段会被忽略,升级后配置将无法通过校验、网关不会启动;手动编辑过代理配置的,请在升级前核对一遍。

    新增远程连接:应用可以连接运行在另一台机器上的 core,例如 Linux 服务器。在「设置 → 连接」中添加远程连接,填写地址、端口和密钥,测试通过后即可切换;服务器端的安装与配置见 ThinkWatch-Core 仓库的 docs/server.md。同一时间只连接一个 core,连接远程时本机 core 停止,本机数据保留。切换时可选择同时将本机已接管的客户端改为指向服务器。

    连接不上时界面照常可用:启动后最多等待约 8 秒,之后显示「未连接」页,可重试、编辑连接或切换到本机;运行中断线时页面转为只读并提示一次,恢复后自动收起。菜单栏新增「连接」子菜单;启动时按住 Option 键,或连续两次启动未完成时,会先显示连接选择。

    远程模式下:客户端页和 MCP 页检查和修改的是这台电脑上的客户端配置;设置页分为本机应用与服务器配置两组;ChatGPT 账号只能用设备码登录;诊断包不提供。

    应用与 core 之间的控制通道改为加密握手,本机与远程使用同一把密钥(写在配置文件中)。客户端接管、MCP 管理和客户端配置扫描改由应用自身执行。

  27. ThinkWatch Core v0.47.0 EN GitHub ↗

    This release adds a remote control port, through which ThinkWatch Lite on another machine can manage a core running on a server, together with an install script, a systemd unit and twcore upgrade for running twcore as a service on Linux. ThinkWatch Lite 2026.9.16 includes this version.

    Upgrade notes

    • A misspelled field under proxies (for example typ: for type:) is now a configuration error; it used to be ignored. A configuration that contains one does not pass twcore check, and core does not start with it.
    • Every connection to the control plane now begins with a Noise handshake, and control requests no longer use a bearer token. HTTP clients such as curl cannot call the control plane directly; twcore call sends a request through the handshake.
    • Client adoption, MCP editing and the client configuration scan moved to ThinkWatch Lite, which runs on the machine where those files are. The twcore scan and twcore clients subcommands and the related control-plane endpoints and events are removed; POST /clients/{id}/key remains.

    Control key. listen.control.key in config.yaml holds the key, and twcore serve adds one to a configuration that has none. Every control transport (the unix socket, the loopback port on Windows and the remote port) starts with a Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s handshake keyed by it, and a wrong key is refused before any HTTP. twcore control-key prints the key; --rotate replaces it and closes the connections made with the previous one. The key is masked in GET /config, in the configuration history and in diagnostic bundles, and it cannot be changed through the control plane.

    Remote control port. listen.control.remote opens a TCP port for ThinkWatch Lite on another machine, in addition to the local channel. twcore init writes the section disabled, with a random port from 20000–32000. twcore remote enable [--bind B] [--port N] [--allow CIDR], twcore remote disable and twcore remote show change and report it, and a running core applies a change within a second. A source outside allow_from (the private ranges by default; loopback is not added automatically) is closed before the handshake, and a source with five failed handshakes within 60 seconds is ignored for 60 seconds. Remote connections cannot shut core down, download diagnostics or change listen.control.

    Server deployment. scripts/install.sh installs twcore on Linux (x86_64, aarch64) as a systemd service, with a thinkwatch system user, data in /var/lib/thinkwatch and environment variables in /etc/thinkwatch/env; it checks the SHA-256 and never starts or restarts the service. Releases now include twcore-<target>.tar.gz for both Linux targets, holding the binary, twcore.service and LICENSE. twcore upgrade [--check] [--restart] [--version X.Y.Z] replaces a separately installed twcore with a release from GitHub after checking its SHA-256 and running it once; it does not replace the copy inside ThinkWatch Lite, which the app updates itself. The guide is docs/server.md.

    Configuration reference. docs/config.md (and docs/config.zh-CN.md) describes every section and field of config.yaml. The field tables and the lists of built-in rules are generated from the code, and a test fails when the manual and the code disagree.

    Fix. On Linux, changing listen.gateway.bind between all and a single address on the same port now takes effect without a restart; it used to be reported as a port in use by another program.

  28. ThinkWatch Lite v2026.9.15 GitHub ↗

    升级须知:请求记录会清空。上游请求头中如写有 {{client}},升级前请先删除,该写法已不再支持,否则配置无法通过校验、网关不会启动。

    新增 Linux 版本(x86_64 与 aarch64 的 AppImage),支持应用内更新。可用一行命令安装:curl -fsSL https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest/download/install.sh | sh

    安全页新增三项防护:请求中的隐藏字符(标签字符、双向控制符)、内容规则(内置规则可逐条启停和改处置,另可添加「包含」或「正则」自定义规则)、输出长度上限。三项均可选择关闭、仅记录或拦截,命中会记入安全日志、请求详情和概览。

    安全加固:转发时不再跟随上游的重定向,避免上游凭据被带到其他地址;WebSocket 请求中的网关密钥不再发往上游;配置、请求记录等含密钥的文件在创建时即只允许本人读写;工具调用审查可识别更多 SSE 写法;窗口权限按需收紧。

    提醒改为按当前状态核对:应用启动或重新连接网关时,已存在的凭据失效、配置未通过校验等问题会补发提醒,已解决的会自动收起,同一问题不会重复提醒。系统通知和界面中的错误原因均显示为中文。

    其他:重放请求与正常转发使用同一出站设置;菜单栏的进行中请求和生成速率改由网关统计;网关已在运行时重新打开窗口不再播放启动动画;上游表单的请求头列表第一行显示密钥所用的认证头。

  29. ThinkWatch 企业版 v2.0.0 EN GitHub ↗

    Callers 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_required before 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-key and the TOTP status/setup/verify-setup calls. Setting up TOTP releases the session straight away. API keys are not affected: gateway, MCP and console tw- key traffic keeps working. While the setting is on, POST /api/auth/totp/disable is 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 and Retry-After are unchanged. Code that reads the error type needs updating:

      • Chat Completions and Responses keep the {"error": {"message", "type", …}} shape, but type is 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) and server_error (5xx). The old tags are gone: rate_limited, policy_blocked, provider_http_error, provider_error, provider_timeout, transform_error, network_error and auth_error. A policy block is now permission_error with 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.failed event, where before it got a Chat-style error frame that SDKs skip, so the stream just stopped. An Anthropic client gets an error event whose type follows the status.
      • The error_type field in gateway_logs and the metric labels are unchanged.
    • 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.
    • 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. models gains three weights, cache_read_weight, cache_write_weight and cache_write_1h_weight. When a weight is unset, it is input_weight times 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_tokens in the log is still the whole input. The log detail gains cache_read_tokens, cache_write_tokens and cache_write_1h.
    • 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, persona and chinese; they used to be basic, strict and chinese. 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: true in their detail and count in gateway_usage_estimated_total. A request with no answer at all is still billed at zero.

    • Clients that leave early are now logged. In 1.x a client that disconnected before its response existed left no gateway_logs row 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:

    • models gains three nullable columns: cache_read_weight, cache_write_weight and cache_write_1h_weight (ALTER TABLE … ADD COLUMN IF NOT EXISTS, CHECK (>= 0)).
    • system_settings gets an auth.default_role row, 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}:generateContent and :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 with alt=sse, and as Gemini’s JSON array without it. :countTokens and :embedContent are refused with 400.

    • The Responses API over a WebSocket. Connect to GET /v1/responses with Upgrade: websocket.

      • Each response.create frame 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 with previous_response_id, even with store: false, which is how Codex works, and against any upstream format.
      • A new counter, gateway_responses_ws_connections_total, counts connections.
    • More places to put an API key. Gateway keys are also accepted in x-api-key (Anthropic SDKs), x-goog-api-key and ?key= (Gemini SDKs), as well as Authorization: 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 found gains revealed, the ASCII that the hidden tag characters spell.

    • auth.default_role can 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.com are now recognised, and the check reads the parsed host. A relay URL such as https://relay/api.openai.com no 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. A tw1. 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-guard and tw-breaker at 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.
  30. ThinkWatch Core v0.46.0 GitHub ↗
  31. ThinkWatch Core v0.45.0 GitHub ↗
  32. ThinkWatch Core v0.44.0 GitHub ↗
  33. ThinkWatch Core v0.43.0 GitHub ↗
  34. ThinkWatch Core v0.42.0 GitHub ↗
  35. ThinkWatch 企业版 v1.1.0 EN GitHub ↗

    The gateway stops rebuilding every request as a chat-shaped message. A request whose route speaks the caller’s own format goes out as the caller sent it; one that crosses formats is converted by ThinkWatch-Core, the same layer the desktop edition uses. Tools, tool choice, system prompt blocks, metadata and cache_control now reach the upstream, where they used to be dropped. Two new checks guard what goes in and out: tool calls an upstream returns, and invisible characters in what a caller sends.

    Read before upgrading#

    • Anthropic routes record more prompt tokens for the same work. Prompt tokens now count the same for every upstream: plain input plus cache reads and cache writes, which is OpenAI’s definition. Anthropic’s own input_tokens leaves the cached part out, so on those routes prompt tokens, cost and budget use all go up. The price model still charges every prompt token at one rate.
    • Requests in the upstream’s own format are forwarded as sent. Every field the caller sends reaches the upstream, along with the caller’s anthropic-beta and anthropic-version headers. Only the model name changes, and PII is swapped for placeholders. An OpenAI-compatible upstream that rejects fields it does not know may now refuse requests that used to succeed, because those fields were stripped before. Try your upstreams with the clients you actually run.
    • Two checks are on by default, and neither blocks anything yet.
      • Tool-call inspection starts in observe mode.
      • The hidden-character check starts in warn mode.
      • Both write audit events, so expect new entries in the audit log and in anything subscribed to it.
      • Nothing is refused until you switch to enforce or block.
    • The response cache starts cold. The cache key now covers the whole request, so entries written by 1.0.2 are never hit again. They expire on their own.
    • One PII value gets one placeholder. Within a request, the same e-mail address is {{EMAIL_1}} wherever it appears. It used to get a new number each time, so a model saw one person as several. Saving a PII pattern now also requires the placeholder prefix to be letters, digits or underscores.

    Added#

    • Tool-call inspection (security.tool_inspection).
      • Why: an upstream writes the response, so it can hand the caller a tool call the model never made, such as bash("curl … | sh") appended to an ordinary answer. An agent set to auto-approve then runs it.
      • Rules: a built-in set of dangerous-command rules. Each can be switched off or given a different action, and you can add your own.
      • Modes: off, observe (records hits and changes nothing on the wire) and enforce.
      • Enforce on a stream: the stream is cut at the frame that would complete a matching call, and the refusal arrives in the caller’s format.
      • Enforce on a whole response or a cache hit: refused with 403 (policy_blocked). A refused answer is neither cached nor billed.
      • Audit and metrics: every hit is an audit event (gateway.tool_call_flagged or gateway.tool_call_blocked) and counts in gateway_tool_call_flagged_total.
      • Admin API: GET /api/admin/settings/tool-inspection/rules and POST /api/admin/settings/tool-inspection/test.
      • Console: a card on the security page, plus a sandbox tab.
    • Hidden-character check (security.hidden_text: off, log, warn or block).
      • What it looks for:
        • Unicode tag characters, which carry an instruction invisibly into the model’s context;
        • bidirectional overrides, which make text read differently on screen than it is.
      • Where: the caller’s messages and the tool results inside them. The system prompt and the model’s own turns are not checked.
      • Not flagged: zero-width joiners (emoji), the zero-width non-joiner (Persian) and Cyrillic.
      • Actions: warn writes gateway.hidden_text_flagged; block refuses with 403 and writes gateway.hidden_text_blocked.
      • Console: a card on the security page.
    • Tool-call arguments get their PII back. A model asked to e-mail a@example.com used to call the tool with {{EMAIL_1}} as the address.

    Changed#

    • One pipeline for /v1/chat/completions, /v1/messages and /v1/responses. A same-format request is forwarded as sent. A cross-format request is converted, and the gateway logs what the target format cannot carry.
    • Content filtering and PII detection read tool results too. That is where an injected instruction, or customer data pulled in by a tool, usually sits.
    • Usage is read off the upstream’s own bytes. A streamed response is no longer held in memory for an accounting pass at the end.
    • Chat streams are billed on the upstream’s actual usage. They are always sent asking for it. A caller who did not ask for the usage chunk still does not get one. 1.0.2 estimated the count for these.
    • Streams send their headers at once. A caller who leaves while the upstream is still thinking is recorded as cancelled.
    • Connectivity tests use the live encoder. A route’s test request is built by the same encoder as real traffic, so a passing test means forwarding works.
    • The web console loads data through TanStack Query.
      • Signing out, including from another tab, clears everything cached.
      • After a change, screens refresh in place.
      • Polling pauses while the tab is hidden.
    • Core crates come from one pinned tag (ThinkWatch-Core v0.40.0), declared once at the workspace root.

    Fixed#

    • Requests lost their tools, tool choice and non-text content on the way upstream (ThinkWatch-Core#50). Claude Code’s system prompt, sent as an array, was dropped whole, and so was every cache_control breakpoint. Each cached prefix was billed as full-price input.
    • The response cache could serve the wrong answer. Its key covered only model, messages and max_tokens, so two requests that differed only in tools, response_format, seed and so on shared one entry.
    • A tripped route stayed out until its Redis key expired, roughly four cooldowns. It now gets probed once the cooldown is over, and a success closes it.
    • The dashboard showed every AI provider’s breaker as Closed. It now shows the real state of the provider’s routes, reporting the worst one.
    • The PII “try patterns” endpoint misreported labels. A pattern named CUSTOM_EMAIL was reported as CUSTOM.
    • :latest could point at a main build rather than the release, which is what happened for v1.0.2’s server image. Only the release workflow sets :latest now.
    • The web image hung for six hours. Its frontend was built under QEMU for arm64, where Node crashed and the step never returned. It is now built once, natively. The image contents are unchanged.

    Security#

    • Refreshed the web console’s lockfile to clear 56 Dependabot alerts (1 critical, 23 high). All were transitive, and none of them reached the shipped bundle.
  36. ThinkWatch Core v0.41.0 GitHub ↗
  37. ThinkWatch Lite v2026.9.14 GitHub ↗

    上游的 API 密钥和请求头中的 ${变量名} 现在可以读取用户配置的环境变量。macOS 上读取登录 shell 中的变量,~/.zshrc 等文件中 export 的变量都会生效,此前从程序坞或访达打开时读不到;Windows 上每次启动网关都重新读取系统设置中的环境变量。代理相关的变量和 PATH 不会被读取。修改变量后重新打开应用即可生效。

    上游表单中,API 密钥与请求头的说明改为写明读取系统环境变量,请求头可一键插入网关密钥名称和 Access Token。

    熔断改用与企业版共用的状态机,行为不变:连续失败三次后熔断 60 秒,再放行一次探测;所有上游都不可用时照常放行,只有一个上游时不熔断。

  38. ThinkWatch Lite v2026.9.13 GitHub ↗

    自动检查发现新版本时改为发送系统通知,点按通知后再打开更新窗口,不再自行弹出。更新窗口只显示版本号,不再显示更新说明。

  39. ThinkWatch Core v0.40.0 GitHub ↗
  40. ThinkWatch Lite v2026.9.12 GitHub ↗

    Windows:网关监听选择「局域网」时,可以选用系统中的网卡(如「以太网」),此前保存会失败。快捷键改用 Ctrl(Ctrl+F 搜索、Ctrl+B 收起源列表),字号与字体按 Windows 调整,「访达」「菜单栏」等说法改为 Windows 对应的名称。安装版只运行安装目录中的网关,「关于」中显示的路径也已修正。

    开机启动的说明改为一句话,直接写明开启后的效果。

  41. ThinkWatch Core v0.39.0 GitHub ↗
  42. ThinkWatch Core v0.38.0 GitHub ↗
  43. ThinkWatch Core v0.37.0 GitHub ↗
  44. ThinkWatch Lite v2026.9.11 GitHub ↗

    修复 2026.9.10 中所有修改操作都会失败的问题:保存设置、新建或编辑上游、接管客户端等操作会提示控制面需要凭据,本版恢复正常。

    Windows:启动时不再弹出网关的命令行窗口;客户端页中的配置文件路径改为 Windows 的写法,Zed 的配置位置以及 Continue、Gemini CLI 的手动配置步骤按 Windows 给出。

    报错信息改为按界面语言显示,系统或外部服务返回的原文除外。

  45. ThinkWatch Core v0.36.0 GitHub ↗
  46. ThinkWatch Core v0.35.0 GitHub ↗
  47. ThinkWatch Core v0.34.0 GitHub ↗
  48. ThinkWatch Lite v2026.9.10 GitHub ↗

    首次提供 Windows 版,分 x64 与 arm64 两个安装程序,在发布页下载。Windows 版未做代码签名,首次运行时 SmartScreen 会拦截,选择「更多信息」→「仍要运行」即可。

    新增 Z.ai / BigModel 账号登录:在上游的服务类型中选择「Z.ai / BigModel 账号(登录)」,在浏览器中授权后,账号下会生成一把 API 密钥并写入这个上游。

    客户端页中的 Codex 一行同时对应 Codex 命令行和 ChatGPT 桌面应用内置的 Codex,二者共用同一份配置;接管时一并说明,ChatGPT 桌面应用需要重启才会生效。

    修复:更新窗口底部的按钮被窗口边缘截断;在接管对话框中展开「完整改动」后,内容超出对话框。

  49. ThinkWatch Core v0.33.0 GitHub ↗
  50. ThinkWatch Core v0.32.0 GitHub ↗
  51. ThinkWatch Core v0.31.0 GitHub ↗
  52. ThinkWatch Core v0.30.0 GitHub ↗
  53. ThinkWatch Core v0.29.0 GitHub ↗
  54. ThinkWatch Lite v2026.9.9 GitHub ↗

    升级须知:2026.9.8 及更早的版本无法在应用内安装这一版,需从发布页下载磁盘映像(DMG)手动安装一次,此后的版本可照常在应用内更新;通过 Homebrew 安装的执行 brew upgrade 即可。

    推理测速支持全部类型的上游。ChatGPT 账号上游此前无法测速,只会返回一句英文提示,现在与其余上游一样发送探测请求并测量首 token 时间,失败原因按界面语言显示。ChatGPT 账号不接受输出上限,费用预估中的上限显示为「不限」、金额显示为「按实际用量」,合计随之标为无法计算;会推理的模型在探测时留出推理所需的输出额度,避免上限过小导致没有可见的输出。

    发布内容只保留磁盘映像及其校验值,应用内更新改为下载与手动安装同一份磁盘映像。

  55. ThinkWatch Core v0.28.0 GitHub ↗
  56. ThinkWatch Lite v2026.9.8 GitHub ↗

    升级须知:升级后本机的请求记录会清空。部分旧设置项已删除,配置文件中仍有这些项时网关无法启动,应用会进入安全模式;删除这些项后重新启动即可恢复。用 ChatGPT 账号登录的上游写入的「billing: subscription」属于这一类。

    界面支持英文,默认跟随系统语言,也可在「设置」中选择;菜单栏、系统通知和错误信息使用同一种语言。外观可选浅色、深色或跟随系统。

    导航重新整理为概览、流量、客户端、密钥、上游、路由、安全、MCP 和设置。

    流量页将请求与会话合为一张表,可按会话归组;每条请求标出发出它的密钥、应用和来源机器,请求详情中的 JSON 会格式化显示。概览的实时图显示最近十分钟,切换页面后保留所选的时间范围。

    路由页以密钥为中心重新设计,规则按顺序编辑;试算会给出请求使用的路由,不经过任何规则的请求也会如实说明。

    密钥单独成页,显示完整密钥并可复制,标出由哪个客户端接管生成,可安全地更换;每把密钥可设置可见模型和并发上限,全局并发限制已删除。客户端页用一张表列出各客户端的状态和所用密钥,未检测到的客户端也给出手动配置方法;「全部还原」此前只还原 opencode,现在会还原所有已接管的客户端。

    安全页只保留出站脱敏和工具调用审查两项全局防护,匹配规则可查看和自定义,并新增安全日志;MCP 扫描单独成页。

    计费方式只分按量计费和不计费,ChatGPT 等订阅账号也按价目表计算费用。上游行可直接展开模型列表,缺失的列表会自动获取;ChatGPT 账号上游显示邮箱和套餐,也可在另一台已登录的设备上输入代码完成登录。

    菜单栏改为原生样式,显示今日 token 和今日费用,菜单中可查看额度、进行中的请求和提醒。提醒只保留一个总开关,可标为已读;不再监测磁盘空间。

    网关监听的访问范围、网卡、端口与放行网段移入「设置」,保存后生效;日志保留时长也可在「设置」中调整。启动时显示启动画面,网关就绪后进入主界面,各页面随数据变化实时更新。

  57. ThinkWatch Core v0.27.0 GitHub ↗
  58. ThinkWatch Core v0.26.0 GitHub ↗
  59. ThinkWatch Core v0.25.0 GitHub ↗
  60. ThinkWatch Core v0.24.1 GitHub ↗
  61. ThinkWatch Core v0.24.0 GitHub ↗
  62. ThinkWatch Core v0.23.0 GitHub ↗
  63. ThinkWatch Core v0.22.0 GitHub ↗
  64. ThinkWatch Core v0.21.0 GitHub ↗
  65. ThinkWatch Core v0.20.0 GitHub ↗
  66. ThinkWatch Core v0.19.0 GitHub ↗
  67. ThinkWatch Core v0.18.0 GitHub ↗
  68. ThinkWatch Core v0.17.2 GitHub ↗
  69. ThinkWatch Core v0.17.1 GitHub ↗
  70. ThinkWatch Core v0.17.0 GitHub ↗
  71. ThinkWatch Core v0.16.0 GitHub ↗
  72. ThinkWatch Core v0.15.0 GitHub ↗
  73. ThinkWatch Core v0.14.1 GitHub ↗
  74. ThinkWatch Core v0.14.0 GitHub ↗
  75. ThinkWatch Core v0.13.0 GitHub ↗
  76. ThinkWatch Core v0.12.0 GitHub ↗
  77. ThinkWatch Core v0.11.1 GitHub ↗
  78. ThinkWatch Core v0.11.0 GitHub ↗
  79. ThinkWatch Core v0.10.0 GitHub ↗
  80. ThinkWatch Core v0.9.1 GitHub ↗
  81. ThinkWatch Core v0.9.0 GitHub ↗
  82. ThinkWatch Core v0.8.2 GitHub ↗
  83. ThinkWatch Core v0.8.1 GitHub ↗
  84. ThinkWatch Core v0.8.0 GitHub ↗
  85. ThinkWatch Core v0.7.0 GitHub ↗
  86. ThinkWatch Lite v2026.9.7 GitHub ↗

    通知改为原生 macOS 通知:同一条会就地更新,过期的会自己撤回,点开直接跳到对应的页面。

  87. ThinkWatch Lite v2026.9.6 GitHub ↗

    可以用 ChatGPT 账号新建上游:在浏览器中完成登录后,订阅额度即可供各类客户端使用。登录前会说明相关风险;账号的额度与额度重置卡在该上游的菜单中查看和使用。

    不同格式之间的请求可以互相转换:使用 Anthropic、OpenAI Chat、OpenAI Responses 或 Gemini 格式的客户端,可以使用其中任何一种格式的上游。请求详情中会显示转换情况与未能保留的字段。

    新增提醒:订阅额度用完、登录失效、上游拒绝凭据、代理不通、磁盘空间不足等情况记录在工具栏的提醒列表中,需要处理的会弹出系统通知。每一类提醒可在「设置」中选择弹出通知、仅在应用内显示或关闭。

    上游页重新设计:上游、代理与价目表分为三个标签,新建与编辑均在对话框中完成;上游可以设置自定义价目表与额外的请求头。

    请求列表会标出被取消的请求、部分失败的请求与缺少用量的请求。

  88. ThinkWatch Core v0.6.0 GitHub ↗
  89. ThinkWatch Core v0.5.0 GitHub ↗
  90. ThinkWatch Core v0.4.0 GitHub ↗
  91. ThinkWatch Core v0.3.0 GitHub ↗
  92. ThinkWatch Core v0.2.2 GitHub ↗
  93. ThinkWatch Core v0.2.1 GitHub ↗
  94. ThinkWatch Core v0.2.0 GitHub ↗
  95. ThinkWatch Core v0.1.2 GitHub ↗
  96. ThinkWatch Lite v2026.9.5 GitHub ↗

    设置里的「自动检查新版本」只保留开关;检查改为每天一次。

  97. ThinkWatch Lite v2026.9.4 GitHub ↗

    安装包的窗口重新设计:打开磁盘映像即可看到拖放方向和首次打开的说明。签名改为固定的证书,此后通过 Homebrew 升级不再提示签名者变化。

  98. ThinkWatch Lite v2026.9.3 GitHub ↗

    有新版本时弹出更新窗口。网页下载安装的,按一次即可完成更新,并会等正在进行的请求结束后再重启;Homebrew 安装的,窗口给出更新命令,可一键复制。更新检查默认开启,可在「设置」中关闭。

  99. ThinkWatch Lite v2026.9.2 GitHub ↗

    安装包改为磁盘映像:打开后把应用拖进「应用程序」即可。

  100. ThinkWatch Core v0.1.1 GitHub ↗
  101. ThinkWatch Lite v2026.9.1 GitHub ↗

    这一版会在有新版本时告诉你。检查默认关闭,在「设置」里打开。

  102. ThinkWatch Lite v2026.9.0 EN GitHub ↗

    The first build. An unsigned arm64 .app with the gateway inside it.

  103. ThinkWatch Core v0.1.0 GitHub ↗
  104. ThinkWatch 企业版 v1.0.2 EN GitHub ↗

    The shared gateway layer moves out into its own repository, and OIDC learns to accept an ID token whose aud carries more than the client ID. Mostly a fix release otherwise — the deploy and auth items below are the ones worth reading before you upgrade.

    Added#

    • OIDC_ADDITIONAL_TRUSTED_AUDIENCES — a comma-separated allowlist of extra ID-token audiences to trust. Some IdPs put something besides the client ID in aud; Zitadel includes the parent project ID, and openidconnect’s stock verifier rejects every non-client-ID audience, so login failed outright. Leave it unset and verification is byte-for-byte what it was. Set, it widens exactly one check: the extra audiences on a multi-audience token are matched against this list as exact strings — no wildcards, no prefixes. The client ID must still appear in aud regardless. Documented in .env.example. Thanks to @DaniW42 for the report and the patch (#12).
    • Automatic upstream protocol detection — the gateway learns an upstream’s dialect and relearns it when the upstream changes, instead of relying on a static guess. Protocol is now a property of the route.
    • Reverse-proxy identification — the server recognizes its own reverse proxy, which is what makes the dashboard WebSocket and the API docs work behind nginx.

    Changed#

    • azp is validated whenever it is present, per OIDC Core 3.1.3.7 step 5, which has no audience-count precondition. Previously it was only checked when a multi-audience token made it mandatory, so a single-audience token naming this client with azp pointing at a different client was accepted — the IdP stating plainly that the token was authorized for somebody else. This can reject a token that 1.0.1 accepted. If your IdP sets azp to something other than your client ID, logins will start failing and the message will say so; that is the IdP to fix, not this setting.
    • The shared gateway layer now comes from ThinkWatch-Core as a git dependency rather than living in this tree. Six crates — types, protocol, provider, resilience, crypto, and the JSON secret envelope — are one implementation shared with the desktop edition instead of two copies drifting apart. No runtime or API change; it affects you only if you build from source, where the build now needs network access to resolve that dependency. The published images are unaffected.
    • Stored secrets are redacted on read and decrypted on use. Provider credentials no longer travel in plaintext through code paths that merely display or list them.

    Fixed#

    • Deploy — nginx broke the API docs three separate ways; the dashboard WebSocket wasn’t proxied; the prod stack’s healthchecks were wrong; and the dev stack could overwrite production’s ClickHouse credentials. The last one is the reason to read this list.
    • Auth — the console logged itself out after every token refresh. Rate-limit counters could be created without an expiry and sit in Redis forever.
    • Models — the gateway no longer offers models the upstream refuses, no longer imports models it won’t serve, and reports every dialect that was refused rather than only the last. Provider base_url trailing slashes are normalized.
    • Analytics — spend is attributed to a person, not a UUID.
    • Audit — a syslog forwarder formatting fix that had CI red on clippy 1.98 (#11, thanks @DaniW42). Two more instances of the same lint, plus a result_large_err false positive in the MCP lifecycle stages, measured rather than boxed: the Ok variant is 296 bytes against the Err’s 152, so boxing saves nothing and adds an allocation.
    • UI — the brand mark stays inside the collapsed sidebar rail.

    Contributing#

    CONTRIBUTING.md and a PR template now state the branch contract that had only lived in docs/operations/release.md: routine work targets dev, and main is the release line. A workflow comments on PRs opened against main from anything other than dev or a hotfix/* branch, because GitHub pre-fills the base with the default branch and walks contributors into it.

    Added#

    • (nothing yet)

    Changed#

    • (nothing yet)

    Fixed#

    • (nothing yet)

    Removed#

    • (nothing yet)

    Security#

    • (nothing yet)

2026年5月

  1. ThinkWatch 企业版 v1.0.1 EN GitHub ↗

    Release-pipeline validation. No product change — the published binary, REST surface, MCP wire shapes, and audit semantics are identical to v1.0.0. Operators pinning 1.0.0 have no reason to bump; those tracking :latest move forward.

    Changed#

    • Release workflow — arm64 image builds now run on a native arm64 runner (ubuntu-24.04-arm) instead of QEMU emulation. v1.0.0’s server image build took 1h24m; this should drop to ~10 min. Multi-arch manifest assembled by a new merge job via docker buildx imagetools create.
    • Node 24 opt-in — workflow sets FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true so all actions/* + docker/* run on Node 24 ahead of GitHub’s 2026-06-02 forced cutover.
  2. ThinkWatch 企业版 v1.0.0 EN GitHub ↗

    Stability commitment. No code delta since 0.5.0 — this tag marks the point at which the API surface becomes a SemVer commitment.

    Changed#

    • Versioning policy — from this tag onwards every breaking change (REST routes, MCP wire shapes, audit-row JSON keys, database schema, public Rust APIs in published crates) requires a major bump. Operators chasing the :latest tag on the GHCR images can do so without surprise.

    Notes#

    • Docker images cut at this tag receive :latest for the first time — the release workflow suppresses :latest on 0.x and pre-release tags. Pin the version in production rather than tracking :latest unless you have a controlled rollback path.
  3. ThinkWatch 企业版 v0.4.0

    看见每一个字节

    • 每一次 Gateway 和 MCP 调用都持久化 request / response body
    • 落盘前 PII 脱敏;按列 TTL 让「热 body」和「冷元数据」分别衰减
    • S3 兼容的 Body 卸载 —— AWS S3、MinIO、Ceph 或内置 RustFS
    • 基于布隆过滤器索引的跨行子串搜索
    • audit:read_bodies 独立权限控制 Body 查看器
    • MCP 下游 SSE —— 客户端可订阅 tools/call 事件时间线
    • 按模型路由:Auto / Manual 两种模式 + 拖拽式流量分配条
    • 模型级开关 —— 暂停单个模型而不影响整个 Provider
    • Pre-call 预算检查直接拒绝已超额请求
    • allowed_models 在 Anthropic Messages 与 OpenAI Responses 同样强制

    全量请求 / 响应体审计#

    ThinkWatch 把自己定位成 AI 流量的「堡垒机」—— 但每行审计只有元数据。Token 数、延迟、成本。用户实际问了什么、模型实际答了什么,没有记录。合规团队没法复盘事故、没法证明「PII X 被发送过」,甚至分不清两个几乎一模一样的请求。

    v0.4.0 补齐这段。gateway_logs 新增 request_body + response_body(外加字节数与 body_capture_status 维度);mcp_logs 对应新增 tool_arguments + tool_result。ZSTD(6) 压缩把冷存储成本压在可控区间;六个动态配置开关(逐字段开关 + 全局 body_max_bytes)让运维不重启就能调整范围。缓存命中的响应现在也会写出完整审计行,不再有「黑洞」。

    按列 TTL + S3 卸载#

    「热证据」和「冷元数据」的保留形状完全不同。按列 TTL 让你可以在同一张表里同时保留近 30 天的 body 和数年的 token / 成本行,不必为这种交叉支付 ClickHouse 的代价。

    对于超过 body_max_bytes 的载荷 —— 200k 上下文 prompt、读取整个文件的 MCP 工具、Base64 图片 —— body 会卸载到 S3 而不是被截断。后端通过六个环境变量配置(S3_ENDPOINT_URL、S3_BUCKET 等),可对接 AWS S3、MinIO、Ceph,或 Docker Compose 中内置的 RustFS 容器。对象 key 带日期前缀(bodies/<table>/yyyy/mm/dd/...),便于配置 Bucket 生命周期规则独立老化。Body Viewer 端点透明解引用 s3:// 单元格,控制台完全感知不到卸载存在。

    布隆过滤器子串搜索#

    捕获的 body 列附带 tokenbf_v1(512, 3, 0) 索引。审计员查询「哪些会话提到了 API key XYZ」或「哪些工具调用碰过 /etc/passwd」时,不再需要扫描每个 granule —— 布隆过滤器在前置阶段就排除了绝大多数行。索引大小约 50 KB / granule,第一次回答真实合规问题时就回本了。

    新增独立权限 audit:read_bodies 控制 Body Viewer 与底层 /api/admin/{gateway,mcp}/logs/{id}/body 端点 —— 原始载荷作为「行为意图」的法律证据保留,但读取需要单独授权。

    MCP 下游流式响应#

    tools/call 在客户端侧此前只支持缓冲:哪怕上游已经发出 progress 通知,网关也会吞掉时间线,只返回一个 JSON。v0.4.0 基于 Accept 头协商 —— 包含 text/event-stream 的客户端会逐事件接收上游响应(notifications/progress、中间块、最终结果)作为独立 SSE 事件下发。仅 JSON 的客户端保留原缓冲形态,向后兼容。审计管线无论哪种姿态都拿到完整事件时间线。

    按模型路由#

    模型路由配置围绕管理员真实的心智模型重新组织:Auto(latency-cost / balanced / latency-only)或 Manual(peer 间拖拽式流量分配条)。优先级 / 分层故障转移概念被移除;替换为 nginx 风格的扁平 peer 集 + 熔断器旁路。每个模型可以覆盖全局策略;仪表盘暴露 routing-projection,让 UI 在你提交前先告诉你「你的手动配置将花费 $X;自动则是 $Y」。

    三个新端点支撑 UI:

    • PATCH /api/admin/model-routes/batch-weights —— 拖拽条的提交端点
    • GET /api/admin/models/{id}/routing-projection —— 当前 vs Auto 流量分配 + 预计 $/1M tokens
    • GET /api/admin/models/{id}/route-history —— 60 桶 1 分钟 p50/p95,喂给延迟迷你折线图

    决策日志为每个请求记录所选 Provider + 原因 + 任何回退;每行 gateway_logs 标记真实的 upstream_model,事故复盘不留歧义。

    Pre-call 预算强制#

    预算上限此前只在 Post-flight 触发:月度配额已经为零的失控客户端仍能发出请求,只有事后扣账才会注意到 —— 而那时上游 token 已经被烧掉了。v0.4.0 增加 check_budget 生命周期阶段,在上游调用之前运行。「你的配额已耗尽,停止发送」这类稳态场景现在直接被关在门外;既有的跨阈值告警继续覆盖并发突发的竞态。

    allowed_models 全表面强制#

    每个 API 密钥的 allowed_models 白名单现在在 OpenAI Chat Completions、Anthropic Messages、OpenAI Responses 上一致强制。同一套强制被接入共享生命周期管线,未来新增表面自动继承 —— 没有任何 API 表面可以绕过模型白名单。

    引擎内#

    新的共享生命周期管线用统一的 Surface trait 把 AI 网关与 MCP 网关的请求路径合流。check_limits、check_access、check_budget、record_usage 现在作为对称应用的阶段同时存在于两侧,消除了「缓冲、流式、短路」三条分支之间长期存在的漂移。MCP 的 proxy.rs 与 Gateway 的 proxy.rs 都被切分为子模块;前端 1667 行的 dashboard 组件也被拆解为更聚焦的子组件。

  4. ThinkWatch 企业版 v0.3.0

    MCP,按用户的方式

    • 按用户的上游凭证——每位开发者以自己的身份连接 GitHub、Linear、Slack
    • Dynamic Client Registration 一键 OAuth 接入
    • MCP Store 双语模板市场(Linear OAuth 模板开箱预置)
    • 分步注册向导 + 鉴权模式感知的编辑表单
    • 三层上游身份解析(JWT + userinfo + discovery)
    • /connections 页面提供逐凭证 Test Connection
    • 按用户的工具目录——不同用户根据上游权限看到不同工具集
    • MCP 响应缓存按 (user, account_label) 隔离——无跨用户串扰
    • 安全评审加固 —— SSRF、缓存隔离、限流、审计

    按用户的上游凭证#

    MCP 网关不再假装所有用户都是同一个上游服务账号。每位开发者以自己的身份——通过 OAuth 或 PAT——连接 GitHub、Linear、Slack 以及任何启用了 MCP 的服务。上游审计轨迹终于端到端贯通:工单分配给真实的人、GitHub Issue 由实际提交者创建。逐密钥账号覆盖(per-key account override)让同一用户可同时维护多个身份(个人 + 工作 GitHub),并指定某个 API 密钥使用哪一个。

    一键 OAuth 接入#

    在注册向导中粘贴 MCP 服务器 URL,ThinkWatch 自动与上游 OAuth 服务器完成 Dynamic Client Registration,运行鉴权探测,在安装时收集 OAuth 客户端凭证,并引导你完成授权页面。匿名探测的 401/403 被正确识别为 auth_required(在目录卡片上显示琥珀色状态指示),而不是硬性失败——部分受保护的服务器也能干净地注册。

    MCP Store#

    网关内置双语模板注册表。模板自带合理默认值、所需 OAuth scope,以及面向终端用户的说明。Linear OAuth 模板开箱预置,更多主流服务陆续加入。Display Label 让多次安装的模板区分清晰:个人 GitHub 安装与工作 GitHub 安装显示为独立卡片,而不是两个无法分辨的条目。

    分步注册向导#

    MCP 服务器注册重构为分步引导式向导,鉴权模式不同呈现不同界面。编辑表单拒绝保存与当前鉴权模式不匹配的字段;工具调用与安装错误以易懂的语言展示并给出可执行的下一步建议,而不是原始 JSON-RPC 错误码。

    三层上游身份解析#

    当上游 MCP 服务器使用 OAuth 时,ThinkWatch 通过三层策略解析上游用户身份:先解析 JWT,回落到 OAuth userinfo 端点,再回落到 issuer discovery。解析得到的 subject 作为缓存和审计的关键键值,让共用同一个 MCP 服务器的两位用户在下游严格分离。

    逐凭证 Test Connection#

    /connections 页面为每个凭证提供 Test Connection 按钮。在正式提交前先验证你的 OAuth/PAT 是否真的能连上上游服务器;结果是结构化的(auth_ok / auth_required / unreachable / tool_call_ok),不再是单一红绿点——你能精确知道哪一步出了问题。

    安全加固#

    经过一次内部安全评审,MCP 路径新增:探测 URL 的 SSRF 防护;响应缓存按 (user, account_label) 隔离,OAuth/PAT 数据绝不跨用户泄露;每一跳网关都启用限流;工具调用边界发出结构化审计;管理员防误操作护栏(粘贴时验证静态 Token、阻止明显错误的凭证组合)。

2026年4月

  1. ThinkWatch 企业版 v0.2.0

    团队、消费管控与可编程 API

    • 团队——按业务单元隔离密钥、预算与分析数据
    • 限流与预算上限,fail-closed 强制执行,含消费告警
    • RBAC v2——自定义角色、权限历史、一键导入/导出
    • Token 迁移至 HttpOnly Cookie,XSS 无法再窃取会话
    • 完整管理 API,含 OpenAPI 文档与 API 密钥认证
    • WebSocket 驱动的实时看板

    团队#

    用户和 API 密钥现在可以组织到团队中。每个团队拥有独立的分析看板视图、独立的用户列表,以及分配 API 密钥时独立的作用域。结构化的作用域选择器使成员管理更加清晰。

    消费管控#

    全新的限额引擎在每个层级强制执行限流与预算上限——按用户、按 API 密钥、按 Provider、按 MCP 服务器。限额直接内嵌在各编辑对话框的专属面板中,无需跳转到单独的配置页面。当预算耗尽时,网关采用 fail-closed 机制——无静默超支。可设置预算告警阈值,在达到硬性上限前提前预警。流式请求计费准确;支持输入输出 token 价格不对称的模型加权计费。

    RBAC v2#

    角色系统经过全面重构。内置角色与自定义角色统一到同一张表中。自定义角色可在 CodeMirror 编辑器中完整编辑,支持从现有角色克隆作为起点,并追踪完整的权限变更历史,清楚记录谁在什么时候做了什么修改。角色支持以 JSON 格式导出和导入,便于跨环境保持一致性。角色授权现在感知作用域——在团队层级授予的角色仅在该团队内生效。

    安全加固#

    访问令牌和刷新令牌已从 localStorage 迁移到 HttpOnly Cookie,JavaScript 可见的会话面完全归零。XSS 攻击无法窃取 Token。刷新端点将每个令牌绑定到来源客户端 IP,被盗 Cookie 无法跨网络重放。WebSocket 看板连接现在使用一次性票据,而非长期有效凭证。

    管理 API#

    网关现在开放了以 API 密钥认证的可编程管理接口。完整的 OpenAPI 规范与网关一同部署,可将密钥和用户的生命周期管理集成到自有工具链或 CI 流水线,无需打开控制台操作。

  2. ThinkWatch 企业版 v0.1.0

    公开预览

    • 多格式 AI API 网关(OpenAI、Anthropic、Gemini、Bedrock)
    • MCP 网关,含命名空间隔离与工具级 RBAC
    • ClickHouse 驱动的审计日志,支持多通道转发
    • 首次运行初始化向导与内置配置指南

    ThinkWatch 现已进入公开预览阶段。此次首个标记版本包含双端口架构(网关 :3000,控制台 :3001)、虚拟 API 密钥生命周期管理、滑动窗口限流、熔断器,以及统一日志检索。

    Helm Chart 与 Distroless 容器镜像均已发布,同时支持 Docker Compose 自托管部署。