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
34 changes: 17 additions & 17 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ───────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion crates/tw-dialect/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions release-notes/0.50.0.md
Original file line number Diff line number Diff line change
@@ -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.
Loading