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.10",
"version": "2026.10.11",
"type": "module",
"packageManager": "pnpm@11.13.0",
"scripts": {
Expand Down
22 changes: 22 additions & 0 deletions release-notes/2026.10.11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
**Upgrade notes:**
- The bundled core is now 0.67.0, with control-plane protocol 43. A remote server must be upgraded to core 0.67.0 together with the app: this version does not connect to a server on 0.66.0, and 2026.10.10 and earlier do not connect to 0.67.0.
- **The request history and stored payloads are cleared on the first start.**
- The failover settings for a slow-starting stream are replaced by the no-response timeout. A configuration that still has `failover.next_on_slow_start` or `failover.stream_start_wait_secs` makes the gateway start in safe mode; the repair it offers deletes them.
- Requests converted for Claude now use prompt caching: cache writes are billed at the cache-write rate and reads at the much lower cache-read rate, and both are priced and shown with the request.

**No response and abort:**
- **Settings › Failover** has a **No-response timeout**, 300 seconds by default. When an upstream sends no content for that long, the request moves to the next upstream; when none is left, the client gets a timeout error instead of waiting indefinitely. After an answer has started, it ends with an error the client understands. Keep-alive pings do not count as content. It replaces the switch for moving on when a stream is slow to start.
- A running request can be stopped with **Abort request** in its menu on the Traffic page, and every running request of a session with **Abort session**. The request's details show **Aborted** or **No response** with the time waited.

**Codex:**
- Through a route that converts to Claude, GLM, DeepSeek or another format, Codex keeps all its tools. For some models Codex declares its tools in a way that was dropped, and it then reported that it could not read files or run commands. Its history, compaction of long sessions and instructions in the middle of a conversation also arrive now.
- Requests converted for Claude are cached, so the turns of a long session cost much less.

**Upstreams:**
- Models can also be added by hand in an upstream's dialog, in **Models**, for any upstream and while creating one. They are marked **Manual** and saved with the upstream.

**Traffic:**
- Long upstream, model and key names no longer widen the table: they are cut with an ellipsis and shown in full on hover, also in the row menus.

**ChatGPT account:**
- Signing in on another device works again. OpenAI changed the format of a value in its sign-in answer, which stopped the sign-in with an error.
5 changes: 2 additions & 3 deletions scripts/shots/core/en/overview.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,13 @@
"default_route": "default",
"failover": {
"failures_to_pause": 3,
"idle_timeout_secs": 300,
"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
"slot_wait_secs": 30
},
"groups": [
{
Expand Down
4 changes: 2 additions & 2 deletions scripts/shots/core/en/status.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"api_version": 38,
"api_version": 43,
"clients": 4,
"config_path": "",
"gateway_addr": null,
Expand All @@ -14,5 +14,5 @@
"reachable": []
},
"uptime_secs": 0,
"version": "0.62.0"
"version": "0.66.0"
}
5 changes: 2 additions & 3 deletions scripts/shots/core/zh/overview.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,13 @@
"default_route": "default",
"failover": {
"failures_to_pause": 3,
"idle_timeout_secs": 300,
"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
"slot_wait_secs": 30
},
"groups": [
{
Expand Down
4 changes: 2 additions & 2 deletions scripts/shots/core/zh/status.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"api_version": 38,
"api_version": 43,
"clients": 4,
"config_path": "",
"gateway_addr": null,
Expand All @@ -14,5 +14,5 @@
"reachable": []
},
"uptime_secs": 0,
"version": "0.62.0"
"version": "0.66.0"
}
2 changes: 2 additions & 0 deletions scripts/shots/mock/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export const CORE: { [N in WebviewEndpoint]: Handler<N> } = {
Sessions: (req) => sessions(req.limit ?? 200),
SessionDetail: (_req, [id]) => ({ session: sessionView(id!) ?? notFound(`Session ${id}`), turns: turns(id!) }),
SessionTranscript: (req, [id]) => (sessionView(id!) ? transcript(id!, req.from_turn ?? 0) : notFound(`Session ${id}`)),
AbortRequest: refuse,
AbortSession: refuse,
SpeedQuote: refuse,
SpeedRun: refuse,
ReplayQuote: refuse,
Expand Down
30 changes: 15 additions & 15 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "thinkwatch-lite"
version = "2026.10.10"
version = "2026.10.11"
edition = "2024"
# **这个数决定依赖能升到哪一版。**edition 2024 的解析器只挑声明的 Rust
# 版本编得动的依赖:写 1.85 的时候,`cargo update` 一直停在旧的 time 和
Expand All @@ -26,12 +26,12 @@ license = "MIT"
# twcore 二进制必须和这里编译进去的协议镜像来自同一个 core 版本 —— 打包脚本正是
# 从 tw-api 锁到的 tag 去取二进制的(见 scripts/fetch-core.sh)。
[workspace.dependencies]
tw-api = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.66.0" }
tw-types = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.66.0" }
tw-yaml = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.66.0" }
tw-guard = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.66.0" }
tw-watch = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.66.0" }
tw-link = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.66.0" }
tw-api = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.67.0" }
tw-types = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.67.0" }
tw-yaml = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.67.0" }
tw-guard = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.67.0" }
tw-watch = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.67.0" }
tw-link = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.67.0" }

[lib]
name = "thinkwatch_lite_lib"
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,11 @@ webview_endpoints![
History,
HistorySearch,
RequestDetail,
AbortRequest,
Sessions,
SessionDetail,
SessionTranscript,
AbortSession,
// 测速、回放、试路由
SpeedQuote,
SpeedRun,
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/clients/desktop_rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,8 +477,8 @@ mod tests {
"security": {"redact": "observe", "inspect_tools": "observe", "content": "observe"},
"retention": {"body_days": 7, "row_days": 90, "body_max_bytes": 0, "body_bytes_now": 0},
"failover": {"failures_to_pause": 3, "pause_secs": 60, "max_pause_secs": 600, "no_balance_pause_secs": 1800,
"quota_pause_secs": 3600, "rate_limit_max_pause_secs": 3600, "stream_start_wait_secs": 15,
"next_on_slow_start": false, "slot_wait_secs": 30}
"quota_pause_secs": 3600, "rate_limit_max_pause_secs": 3600, "idle_timeout_secs": 300,
"slot_wait_secs": 30}
})
}

Expand Down
3 changes: 1 addition & 2 deletions src-tauri/src/notices/fixtures/snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,7 @@
"no_balance_pause_secs": 1800,
"quota_pause_secs": 3600,
"rate_limit_max_pause_secs": 3600,
"stream_start_wait_secs": 15,
"next_on_slow_start": false,
"idle_timeout_secs": 300,
"slot_wait_secs": 30
},
"price_sheets": []
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ThinkWatch Lite",
"version": "2026.10.10",
"version": "2026.10.11",
"identifier": "app.thinkwatch.lite",
"build": {
"beforeDevCommand": "pnpm dev",
Expand Down
2 changes: 2 additions & 0 deletions src/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ export const WEBVIEW_ENDPOINTS = [
"History",
"HistorySearch",
"RequestDetail",
"AbortRequest",
"Sessions",
"SessionDetail",
"SessionTranscript",
"AbortSession",
"SpeedQuote",
"SpeedRun",
"ReplayQuote",
Expand Down
9 changes: 9 additions & 0 deletions src/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ describe("从事件缝出一行", () => {
expect(rows.get(1)?.model).toBe("claude-sonnet-4-5");
});

/** 跑在 WebSocket 连接上的那一行说得出来:它不能单独中止(`abortable`) */
it("开始事件说得出是不是 WebSocket", () => {
const rows = new Map<number, RequestRow>();
applyEvent(rows, started({ method: "WS", path: "/v1/responses" }));
applyEvent(rows, started({ id: 2 }));
expect(rows.get(1)?.ws).toBe(true);
expect(rows.get(2)?.ws).toBeUndefined();
});

/** 会话是网关在开始的那一刻定的:正在跑的那一条已经在它的会话里,不用等落库 */
it("开始就带着会话", () => {
const rows = new Map<number, RequestRow>();
Expand Down
40 changes: 27 additions & 13 deletions src/generated/tw-api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
// Generated by tw-api (`tw_api::ts::export_all`). Do not edit by hand.

export const CONTROL_API_VERSION = 42;
export const CONTROL_API_VERSION = 43;

/**
* 手动中止(`POST /request/{id}/abort`、`POST /sessions/{id}/abort`)叫停了哪几个请求。
*
* **叫停是立刻的,结局随后到**:每个请求照常报一条 [`Event::RequestFailed`](`source` 是
* [`FailureSource::Aborted`]),通常在这条响应之前或紧跟着。叫停之后才跑完的不改。
*/
export type Aborted = {
/**
* 叫停的请求号,开始得早的在前
*/
requests: Array<number>, };

/**
* 一个账号上游登的是哪个账号。
Expand Down Expand Up @@ -219,10 +231,10 @@ suggestions: Array<AliasSuggestion>, };
/**
* 尝试链里一跳的结果。
*/
export type AttemptOutcome = "served" | "status" | "error" | "estimated" | "slow_start";
export type AttemptOutcome = "served" | "status" | "error" | "estimated" | "idle_timeout" | "aborted";

/**
* 放弃了的一跳([`AttemptOutcome::SlowStart`])上游可能已经收了钱的输入。
* 放弃了的一跳([`AttemptOutcome::IdleTimeout`])上游可能已经收了钱的输入。
*
* 上游在流开头报了的(Anthropic 的 `message_start`)是它报的数;没报的只有 `input`,是网关
* 估的(`estimated`,和 [`Event::RequestStarted`] 的 `input_estimate` 同一个数)。**输出不知道**:
Expand Down Expand Up @@ -266,11 +278,12 @@ outcome: AttemptOutcome,
*/
status?: number | null,
/**
* `error` 时的说明。和这一跳报给客户端的那条错误是同一句。`slow_start` 时说等了多久
* `error` 时的说明。和这一跳报给客户端的那条错误是同一句。`idle_timeout` 时说等了多久,
* `aborted` 时是 `gw.request.aborted`
*/
error?: Msg | null, ms: number,
/**
* 放弃了的这一跳(`slow_start`)上游可能已经收了钱的输入(见 [`AttemptUsage`])。估不
* 放弃了的这一跳(`idle_timeout`)上游可能已经收了钱的输入(见 [`AttemptUsage`])。估不
* 出来的(请求解不开)没有。别的结果都没有:接下请求的那一跳的用量在结局里
*/
usage?: AttemptUsage | null,
Expand Down Expand Up @@ -1501,7 +1514,7 @@ plugin_name: string,
request_id?: number | null, message: Msg, at_ms: number, } | { "kind": "events_dropped", id: number, count: number, at_ms: number, };

/**
* 上游失败之后停用多久、流开头最多等多久。和配置的 `failover` 一一对应,
* 上游失败之后停用多久、多久没有内容就不再等。和配置的 `failover` 一一对应,
* 没写的是默认值 —— **界面显示的就是真在用的数**,不是「空 = 默认」。
*/
export type FailoverView = {
Expand Down Expand Up @@ -1530,13 +1543,10 @@ quota_pause_secs: number,
*/
rate_limit_max_pause_secs: number,
/**
* 流式回答的开头最多等多少秒
*/
stream_start_wait_secs: number,
/**
* 等过 `stream_start_wait_secs` 还没有内容就换下一家(最后一家照常等)
* 上游多少秒没有内容就不再等它(无响应超时):从请求发出去起算,每来一段内容重新
* 计时。客户端还什么都没收到时换下一家,已经收到一部分的报错收尾
*/
next_on_slow_start: boolean,
idle_timeout_secs: number,
/**
* 一个请求合计最多等多少秒:等密钥的分钟、小时上限空出名额,和等满着(`max_concurrent`)
* 的上游空出位置,共用这一段。0 是不等
Expand All @@ -1547,7 +1557,7 @@ slot_wait_secs: number, };
* 一个请求失败在哪一方。和 HTTP 响应里的 `x-thinkwatch-error` 同一个词表,
* 另外多一个 `internal`。
*/
export type FailureSource = "auth" | "config" | "upstream" | "request" | "rate_limited" | "denied" | "internal";
export type FailureSource = "auth" | "config" | "upstream" | "request" | "rate_limited" | "denied" | "aborted" | "internal";

/**
* 新建或修改一个策略组时交过来的定义。
Expand Down Expand Up @@ -4935,10 +4945,12 @@ export const ENDPOINTS = {
TokenRateByProvider: { method: "GET", path: "/token-rate/provider", params: [], format: "json" },
UpstreamHealth: { method: "GET", path: "/upstreams/health", params: [], format: "json" },
RequestDetail: { method: "GET", path: "/request/{id}", params: ["id"], format: "json" },
AbortRequest: { method: "POST", path: "/request/{id}/abort", params: ["id"], format: "json" },
Fixture: { method: "GET", path: "/request/{id}/fixture", params: ["id"], format: "text" },
Sessions: { method: "GET", path: "/sessions", params: [], format: "json" },
SessionDetail: { method: "GET", path: "/sessions/{id}", params: ["id"], format: "json" },
SessionTranscript: { method: "GET", path: "/sessions/{id}/transcript", params: ["id"], format: "json" },
AbortSession: { method: "POST", path: "/sessions/{id}/abort", params: ["id"], format: "json" },
SpeedQuote: { method: "POST", path: "/speed/quote", params: [], format: "json" },
SpeedRun: { method: "POST", path: "/speed/run", params: [], format: "json" },
ReplayQuote: { method: "POST", path: "/replay/quote", params: [], format: "json" },
Expand Down Expand Up @@ -5058,10 +5070,12 @@ export type Endpoints = {
TokenRateByProvider: { req: Window; res: Array<TokenRateView> };
UpstreamHealth: { req: Window; res: UpstreamHealth };
RequestDetail: { req: null; res: RequestDetail };
AbortRequest: { req: null; res: Aborted };
Fixture: { req: null; res: string };
Sessions: { req: ListQuery; res: Array<SessionView> };
SessionDetail: { req: null; res: SessionDetail };
SessionTranscript: { req: TranscriptQuery; res: Transcript };
AbortSession: { req: null; res: Aborted };
SpeedQuote: { req: SpeedRunRequest; res: SpeedQuote };
SpeedRun: { req: SpeedRunRequest; res: Array<SpeedResult> };
ReplayQuote: { req: ReplayRequest; res: ReplayQuote };
Expand Down
Loading
Loading