Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "thinkwatch-lite",
"private": true,
"description": "Desktop app for a local AI API gateway on macOS, Windows and Linux",
"version": "2026.10.5",
"version": "2026.10.6",
"type": "module",
"packageManager": "pnpm@11.13.0",
"scripts": {
Expand Down
30 changes: 30 additions & 0 deletions release-notes/2026.10.6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
**Upgrade notes:**
- The bundled core is now 0.63.0, which speaks control-plane protocol 39. A remote server has to run core 0.63.0 too: this version does not connect to 0.62.0, and 2026.10.5 and earlier do not connect to 0.63.0.
- Request history is kept.
- A configuration edited by hand that lists an upstream twice in a strategy group, or a name that is not an upstream, no longer loads; safe mode names the group and the member.
- A key's concurrency limit now counts streamed answers until they end, so a key at its limit waits more often than before.

**Strategy groups:**
- Load-balance groups take a weight per member, from 1 to 100, and a choice of how to distribute: by ratio, by speed, by reliability, or both. Speed is the time to first token, reliability the recent success rate; both adjust the weights, and an upstream without enough measurements counts as average.
- Ongoing conversations stay on their upstream and count toward its share. The route test shows each member's weight, speed, success rate and share, and predicts the next upstream.

**Upstreams:**
- An upstream can have a concurrency limit. A conversation in progress waits for a free slot; other requests go to the next upstream, and when every upstream is full the request waits, then fails with a clear message.
- A model's context window and output limit can be set by hand under "Specs…" in the upstream's model list, for relays the price table does not know or gets wrong. A manual value is marked and replaces the price table's.
- Check-up findings (a different model name in answers, input reported high or low, few cache reads) now appear on the upstream's row; the separate Check-up tab is gone.

**Failover settings:**
- "Move to the next upstream when the start times out" (off by default): a streamed answer that has sent nothing within the wait moves to the next upstream, if one can take it. The last upstream keeps waiting.
- "Wait for a free slot at most" (30 seconds by default) is the total time a request waits for a full upstream or a key's per-minute or per-hour limit.

**Keys:**
- A key can have usage limits: requests, tokens or cost (USD) per minute, hour, day, week or month, all of which must hold. A token limit can count cache reads. Day, week and month reset at local midnight, on Monday and on the 1st.
- The key dialog shows each limit's usage and reset time, and lists the models the key can use that have no price; they count as $0. Keys at a limit are marked in the list, and a notification comes at 80% and when a limit is reached.

**Traffic:**
- The attempt chain shows an upstream given up for a slow start, with the input tokens it may have billed, upstreams skipped at their concurrency limit, and time spent waiting for a slot.
- Requests refused by a key's usage limit or because every upstream was full are labelled as such.
- On a Responses WebSocket connection, each turn is now a request of its own, with its usage and cost.

**Also:**
- The menu bar's menu follows the appearance chosen in the app instead of the menu bar's tint, so it no longer opens light in Dark Mode over a bright wallpaper.
4 changes: 4 additions & 0 deletions scripts/shots/core/en/keys.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"allow": null,
"default": true,
"key": "tw-m9EXAMPLEj7ak",
"limits": [],
"max_concurrent": null,
"name": "default",
"route": null
Expand All @@ -11,6 +12,7 @@
"allow": null,
"client": "claude-code",
"key": "tw-5aEXAMPLEqmg8",
"limits": [],
"max_concurrent": null,
"name": "claude-code",
"route": null
Expand All @@ -19,6 +21,7 @@
"allow": null,
"client": "codex",
"key": "tw-h5EXAMPLEyrqn",
"limits": [],
"max_concurrent": 4,
"name": "codex",
"route": "codex"
Expand All @@ -30,6 +33,7 @@
],
"client": "cursor",
"key": "tw-e6EXAMPLE6td6",
"limits": [],
"max_concurrent": null,
"name": "cursor",
"route": "cursor"
Expand Down
18 changes: 15 additions & 3 deletions scripts/shots/core/en/overview.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"allow": null,
"default": true,
"key": "tw-m9…j7ak",
"limits": [],
"max_concurrent": null,
"name": "default",
"route": null
Expand All @@ -34,6 +35,7 @@
"allow": null,
"client": "claude-code",
"key": "tw-5a…qmg8",
"limits": [],
"max_concurrent": null,
"name": "claude-code",
"route": null
Expand All @@ -42,6 +44,7 @@
"allow": null,
"client": "codex",
"key": "tw-h5…yrqn",
"limits": [],
"max_concurrent": 4,
"name": "codex",
"route": "codex"
Expand All @@ -53,6 +56,7 @@
],
"client": "cursor",
"key": "tw-e6…6td6",
"limits": [],
"max_concurrent": null,
"name": "cursor",
"route": "cursor"
Expand All @@ -63,32 +67,39 @@
"failover": {
"failures_to_pause": 3,
"max_pause_secs": 600,
"next_on_slow_start": false,
"no_balance_pause_secs": 1800,
"pause_secs": 60,
"quota_pause_secs": 3600,
"rate_limit_max_pause_secs": 3600,
"slot_wait_secs": 30,
"stream_start_wait_secs": 15
},
"groups": [
{
"balance_by": "weights",
"builtin": false,
"kind": "fallback",
"name": "main",
"providers": [
"anthropic",
"relay"
]
],
"weights": {}
},
{
"balance_by": "weights",
"builtin": false,
"kind": "cheapest",
"name": "budget",
"providers": [
"anthropic",
"relay"
]
],
"weights": {}
},
{
"balance_by": "weights",
"builtin": true,
"kind": "fallback",
"name": "__all__",
Expand All @@ -100,7 +111,8 @@
"deepseek",
"gemini",
"ollama"
]
],
"weights": {}
}
],
"listen": {
Expand Down
2 changes: 1 addition & 1 deletion scripts/shots/core/en/status.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
"reachable": []
},
"uptime_secs": 0,
"version": "0.61.0"
"version": "0.62.0"
}
9 changes: 5 additions & 4 deletions scripts/shots/core/oracle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@
# 钉住的 core 之后。截图页的配置类数据(上游、密钥、路由、安全规则、试算)
# 直接用这几份文件,所以它们必须是 core 真的答出来的,不是照着样子手写的。
#
# 用的是 src-tauri/Cargo.toml 钉住的那个 tag:从检出里 `git archive` 一份到临时
# 目录,放进 oracle.rs 跑一次。**不改那个检出。**要先在那边 `git fetch --tags`。
# 用的是 src-tauri/Cargo.toml 钉住的那个 tag(core 还没发版时临时钉的 rev 也认):从检出里
# `git archive` 一份到临时目录,放进 oracle.rs 跑一次。**不改那个检出。**要先在那边
# `git fetch --tags`。
set -euo pipefail

core=${1:?用法:oracle.sh <thinkwatch-core 的本地检出>}
here=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$here/../../.." && pwd)
tag=$(sed -n 's/^tw-api = .*tag = "\([^"]*\)".*/\1/p' "$root/src-tauri/Cargo.toml")
[ -n "$tag" ] || { echo "src-tauri/Cargo.toml 里找不到 tw-api 的 tag" >&2; exit 1; }
tag=$(sed -nE 's/^tw-api = .*(tag|rev) = "([^"]*)".*/\2/p' "$root/src-tauri/Cargo.toml")
[ -n "$tag" ] || { echo "src-tauri/Cargo.toml 里找不到 tw-api 的 tag 或 rev" >&2; exit 1; }

src=$(mktemp -d)
trap 'rm -rf "$src"' EXIT
Expand Down
4 changes: 4 additions & 0 deletions scripts/shots/core/zh/keys.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"allow": null,
"default": true,
"key": "tw-m9EXAMPLEj7ak",
"limits": [],
"max_concurrent": null,
"name": "default",
"route": null
Expand All @@ -11,6 +12,7 @@
"allow": null,
"client": "claude-code",
"key": "tw-5aEXAMPLEqmg8",
"limits": [],
"max_concurrent": null,
"name": "claude-code",
"route": null
Expand All @@ -19,6 +21,7 @@
"allow": null,
"client": "codex",
"key": "tw-h5EXAMPLEyrqn",
"limits": [],
"max_concurrent": 4,
"name": "codex",
"route": "codex"
Expand All @@ -30,6 +33,7 @@
],
"client": "cursor",
"key": "tw-e6EXAMPLE6td6",
"limits": [],
"max_concurrent": null,
"name": "cursor",
"route": "cursor"
Expand Down
18 changes: 15 additions & 3 deletions scripts/shots/core/zh/overview.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"allow": null,
"default": true,
"key": "tw-m9…j7ak",
"limits": [],
"max_concurrent": null,
"name": "default",
"route": null
Expand All @@ -34,6 +35,7 @@
"allow": null,
"client": "claude-code",
"key": "tw-5a…qmg8",
"limits": [],
"max_concurrent": null,
"name": "claude-code",
"route": null
Expand All @@ -42,6 +44,7 @@
"allow": null,
"client": "codex",
"key": "tw-h5…yrqn",
"limits": [],
"max_concurrent": 4,
"name": "codex",
"route": "codex"
Expand All @@ -53,6 +56,7 @@
],
"client": "cursor",
"key": "tw-e6…6td6",
"limits": [],
"max_concurrent": null,
"name": "cursor",
"route": "cursor"
Expand All @@ -63,32 +67,39 @@
"failover": {
"failures_to_pause": 3,
"max_pause_secs": 600,
"next_on_slow_start": false,
"no_balance_pause_secs": 1800,
"pause_secs": 60,
"quota_pause_secs": 3600,
"rate_limit_max_pause_secs": 3600,
"slot_wait_secs": 30,
"stream_start_wait_secs": 15
},
"groups": [
{
"balance_by": "weights",
"builtin": false,
"kind": "fallback",
"name": "主力",
"providers": [
"anthropic",
"relay"
]
],
"weights": {}
},
{
"balance_by": "weights",
"builtin": false,
"kind": "cheapest",
"name": "低价",
"providers": [
"anthropic",
"relay"
]
],
"weights": {}
},
{
"balance_by": "weights",
"builtin": true,
"kind": "fallback",
"name": "__all__",
Expand All @@ -100,7 +111,8 @@
"deepseek",
"gemini",
"ollama"
]
],
"weights": {}
}
],
"listen": {
Expand Down
2 changes: 1 addition & 1 deletion scripts/shots/core/zh/status.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
"reachable": []
},
"uptime_secs": 0,
"version": "0.61.0"
"version": "0.62.0"
}
1 change: 1 addition & 0 deletions scripts/shots/mock/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export const CORE: { [N in WebviewEndpoint]: Handler<N> } = {
UpdateProvider: refuse,
DeleteProvider: refuse,
ProviderModels: (_req, [name]) => providerModels(name!) ?? notFound(`Upstream ${name}`),
SetModelSpec: refuse,
RefreshProviderModels: (_req, [name]) => providerModels(name!) ?? notFound(`Upstream ${name}`),
RefreshStaleModels: () => ({ providers: [] }),

Expand Down
Loading
Loading