From 71cfcd450618508abee7bba166ec3ca0d25bd5d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 18:07:30 +0000 Subject: [PATCH] chore: v0.50.0 - Bump the workspace version to 0.50.0 - release-notes/0.50.0.md: Claude Desktop gateway support, DeepSeek Harness cleaning, GLM Coding Plan quota, Antigravity CLI recognition, listening on an interface that is not there yet; request store schema 20 -> 21 (history emptied on upgrade), CONTROL_API_VERSION 23 - Write client_sse() in the form newer clippy asks for (nonminimal_bool); same logic Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018r4jRvQk9vJq4w4NnUrP58 --- Cargo.lock | 34 ++++++++++++++++---------------- Cargo.toml | 2 +- crates/tw-dialect/src/convert.rs | 2 +- release-notes/0.50.0.md | 22 +++++++++++++++++++++ 4 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 release-notes/0.50.0.md diff --git a/Cargo.lock b/Cargo.lock index e74f5bb..aa2b43b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2492,7 +2492,7 @@ dependencies = [ [[package]] name = "tw-api" -version = "0.49.1" +version = "0.50.0" dependencies = [ "serde", "serde_json", @@ -2502,7 +2502,7 @@ dependencies = [ [[package]] name = "tw-breaker" -version = "0.49.1" +version = "0.50.0" dependencies = [ "serde", "serde_json", @@ -2510,7 +2510,7 @@ dependencies = [ [[package]] name = "tw-config" -version = "0.49.1" +version = "0.50.0" dependencies = [ "blake3", "libc", @@ -2534,7 +2534,7 @@ dependencies = [ [[package]] name = "tw-control" -version = "0.49.1" +version = "0.50.0" dependencies = [ "axum", "base64", @@ -2573,7 +2573,7 @@ dependencies = [ [[package]] name = "tw-dialect" -version = "0.49.1" +version = "0.50.0" dependencies = [ "serde", "serde_json", @@ -2581,7 +2581,7 @@ dependencies = [ [[package]] name = "tw-engine" -version = "0.49.1" +version = "0.50.0" dependencies = [ "serde", "serde_json", @@ -2594,7 +2594,7 @@ dependencies = [ [[package]] name = "tw-gateway" -version = "0.49.1" +version = "0.50.0" dependencies = [ "arc-swap", "async-stream", @@ -2638,7 +2638,7 @@ dependencies = [ [[package]] name = "tw-guard" -version = "0.49.1" +version = "0.50.0" dependencies = [ "base64", "regex", @@ -2651,7 +2651,7 @@ dependencies = [ [[package]] name = "tw-link" -version = "0.49.1" +version = "0.50.0" dependencies = [ "serde", "serde_json", @@ -2665,7 +2665,7 @@ dependencies = [ [[package]] name = "tw-observe" -version = "0.49.1" +version = "0.50.0" dependencies = [ "tokio", "tracing", @@ -2674,7 +2674,7 @@ dependencies = [ [[package]] name = "tw-pricing" -version = "0.49.1" +version = "0.50.0" dependencies = [ "arc-swap", "flate2", @@ -2687,14 +2687,14 @@ dependencies = [ [[package]] name = "tw-secret" -version = "0.49.1" +version = "0.50.0" dependencies = [ "thiserror", ] [[package]] name = "tw-store" -version = "0.49.1" +version = "0.50.0" dependencies = [ "bytes", "rusqlite", @@ -2711,7 +2711,7 @@ dependencies = [ [[package]] name = "tw-types" -version = "0.49.1" +version = "0.50.0" dependencies = [ "serde", "serde_json", @@ -2720,7 +2720,7 @@ dependencies = [ [[package]] name = "tw-watch" -version = "0.49.1" +version = "0.50.0" dependencies = [ "notify", "tempfile", @@ -2730,7 +2730,7 @@ dependencies = [ [[package]] name = "tw-yaml" -version = "0.49.1" +version = "0.50.0" dependencies = [ "saphyr-parser", "serde_yaml_ng", @@ -2740,7 +2740,7 @@ dependencies = [ [[package]] name = "twcore" -version = "0.49.1" +version = "0.50.0" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index 58018cb..7a96c17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ categories = ["network-programming", "web-programming::http-server"] # 二进制走 CalVer,crate 走 SemVer —— 两者是两回事,卖给不同的人。 # 这里是 crate 的版本。 -version = "0.49.1" +version = "0.50.0" [workspace.dependencies] # ── 内部 crate ─────────────────────────────────────────── diff --git a/crates/tw-dialect/src/convert.rs b/crates/tw-dialect/src/convert.rs index 7eff677..04da770 100644 --- a/crates/tw-dialect/src/convert.rs +++ b/crates/tw-dialect/src/convert.rs @@ -273,7 +273,7 @@ impl Session { /// 客户端收到的流是不是 SSE。Gemini 客户端不带 `alt=sse` 时是一个 JSON 数组 pub fn client_sse(&self) -> bool { - !(self.client == Dialect::Gemini && !self.shape.gemini_sse) + self.client != Dialect::Gemini || self.shape.gemini_sse } /// 转换后的响应的 Content-Type diff --git a/release-notes/0.50.0.md b/release-notes/0.50.0.md new file mode 100644 index 0000000..c024abf --- /dev/null +++ b/release-notes/0.50.0.md @@ -0,0 +1,22 @@ +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.