From cfb02e0072a426cfa185ce320ecf444e40fb18dd Mon Sep 17 00:00:00 2001 From: caigee-cmd <210803857+caigee-cmd@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:09:53 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20v0.7.0=20=E5=8F=91=E7=89=88=E5=89=8D?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E4=B8=8E=20changelog=20=E7=B2=BE=E7=AE=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prompt-cache-and-stream-salvage.md | 8 ++++---- .../unreleased/qoder-in-process-bridge.md | 18 ++++++------------ .../qoder-inprocess-quota-aggregate.md | 7 ------- changelog/unreleased/qwenwork-provider.md | 10 ++++------ .../region-qualified-model-prefix.md | 10 ++++------ changelog/unreleased/runtime-quota-recovery.md | 10 ++++------ .../unreleased/trae-login-checkin-fixes.md | 4 ++-- .../workbuddy-waf-fingerprint-sanitization.md | 4 ++-- changelog/unreleased/worker-stream-timeout.md | 12 ++++++------ deploy/README.md | 3 +++ 10 files changed, 35 insertions(+), 51 deletions(-) delete mode 100644 changelog/unreleased/qoder-inprocess-quota-aggregate.md diff --git a/changelog/unreleased/prompt-cache-and-stream-salvage.md b/changelog/unreleased/prompt-cache-and-stream-salvage.md index d189d2c2..1638cbce 100644 --- a/changelog/unreleased/prompt-cache-and-stream-salvage.md +++ b/changelog/unreleased/prompt-cache-and-stream-salvage.md @@ -1,9 +1,9 @@ ### English -- Preserve and normalize prompt cache token metrics (`cache_read_tokens`, `prompt_cache_hit_tokens`, and `prompt_tokens_details`) across Trae, Qoder, and WorkBuddy providers in both streaming and non-streaming responses. Originally submitted by [@longlixiao-ong](https://github.com/longlixiao-ong) in #293. -- Gracefully salvage upstream streams that finish with a terminal `finish_reason` before EOF without emitting literal `[DONE]`, preventing artificial 502 incomplete stream errors. +- Report prompt-cache token usage for Trae, Qoder, and WorkBuddy, streaming and non-streaming. Thanks [@longlixiao-ong](https://github.com/longlixiao-ong) (#293). +- Streams that end with a `finish_reason` but no `[DONE]` no longer return a 502. ### 中文 -- 修复并标准化 Trae、Qoder 与 WorkBuddy 提供商在流式与非流式响应中的 Prompt 缓存 Token 指标(统一解析并透传 `cache_read_tokens`、`prompt_cache_hit_tokens` 与 `prompt_tokens_details`)。原稿由 [@longlixiao-ong](https://github.com/longlixiao-ong) 在 #293 提交。 -- 容错处理上游流式响应在带有终态 `finish_reason` 正常结束但未显式发送 `[DONE]` 的边界情况,自动补齐结束帧,避免下游误判为 502 流异常中断。 +- Trae、Qoder、WorkBuddy 流式与非流式均返回 Prompt 缓存 token 用量。感谢 [@longlixiao-ong](https://github.com/longlixiao-ong)(#293)。 +- 上游带 `finish_reason` 结束但未发 `[DONE]` 的流不再报 502。 diff --git a/changelog/unreleased/qoder-in-process-bridge.md b/changelog/unreleased/qoder-in-process-bridge.md index 4f70663a..ee9e838d 100644 --- a/changelog/unreleased/qoder-in-process-bridge.md +++ b/changelog/unreleased/qoder-in-process-bridge.md @@ -1,17 +1,11 @@ ### English -- Qoder accounts can now run fully in-process from a PAT, with no per-account Node+WASM worker. A new `qoder-pat-v1` credential format stores a Personal Access Token; the new in-process `Bridge` (`internal/providers/qoder/bridge.go`) performs the PAT→jobToken exchange, proactive and forced token refresh, `/user/status` + OpenAPI `/api/v2/quota/usage` quota resolution, and `/model/list` catalog fetch — all via the `qauth` package ported from EchoPing07/Qoder-2API-Go (MIT, see `internal/providers/qoder/qauth/NOTICE`). Chat signing continues to use the existing in-process COSY client. The runtime no longer spawns a Node worker for PAT-backed accounts, eliminating the per-account WASM process, the liveness watchdog, and the restart ladder for them. -- Check-in (签到) now runs entirely in Go. The OpenAPI `/sash/api/v1/me/campaigns` list + `/claim` calls are made directly with the bridge's `securityOauthToken` and a machine identity derived deterministically from the PAT (no `runtime-info` child process). Token refresh before the call and force-refresh on a 401 are handled by the bridge. The worker `/admin/checkin` endpoint is now a legacy fallback for accounts still on the `qoder-native-v1` blob. -- Existing Qoder accounts that still carry the legacy `qoder-native-v1` CLI blob (no PAT) keep working via the worker path until the operator re-logs them with a PAT. The console's PAT login path (`LoginPAT`) now accepts a Qoder PAT and stores it in the `qoder-pat-v1` format, after which the account stops spawning a worker. -- Global-region check-in now authenticates the campaign list/claim calls with the OpenAPI bearer (`/api/v1/jobToken/exchange`), matching the region's token requirement; CN keeps using the COSY `securityOauthToken`. A 401 now also invalidates the cached OpenAPI bearer before the single force-refresh retry. -- Fixed Qoder in-process chat requests losing the model's thinking/reasoning stream. `BuildPlainChatBody` now fills `chat_context` the way the reference Qoder client does: `chat_context.text`, `chat_context.extra.modelConfig` (`key` + `is_reasoning`) and `chat_context.extra.originalContent` carry the latest user prompt and the routed model identity. The gateway resolves the thinking channel from that block, so an empty `extra` made it answer with content only. `source`/`version`/`session_type` were also aligned to the official client (`1` / `"3"` / `"qodercli"`). Reasoning now streams back as `reasoning_content` (and as Anthropic `thinking_delta` on `/v1/messages`). -- Fixed Qoder in-process catalog parsing and model discovery. `ModelInfos` previously expected legacy worker fields (`id`, `mapped_key`) and dropped every entry from raw gateway `/model/list` responses (which only supply `key` and `display_name`). Models are now derived from `key` and `display_name` via `CanonicalModelID`, restoring Qoder model routing for in-process accounts. `catalog_source` now also merges in-process Qoder catalogs, and `manager_probe` triggers background model discovery for prober-less in-process accounts on startup. +- Qoder PAT accounts now run fully in-process (no per-account Node worker), including token refresh, model catalog, quota, and check-in. (#313) +- Quota headline sums plan + add-on + org packs; reasoning streams again for in-process chat. +- Upgrade: existing Qoder accounts keep the worker path until you re-login them with a PAT. ### 中文 -- Qoder 账号现在可以基于 PAT 完全进程内运行,不再为每个账号 spawn 一个 Node+WASM worker。新增 `qoder-pat-v1` 凭证格式存储 Personal Access Token;新增进程内 `Bridge`(`internal/providers/qoder/bridge.go`)负责 PAT→jobToken 交换、主动与强制 token 刷新、`/user/status` + OpenAPI `/api/v2/quota/usage` 配额解析、`/model/list` 目录拉取——全部经由从 EchoPing07/Qoder-2API-Go 移植的(MIT,见 `internal/providers/qoder/qauth/NOTICE`) `qauth` 包完成。chat 签名仍走既有进程内 COSY 客户端。运行时不再为 PAT 账号 spawn Node worker,从而消除每账号 WASM 进程、liveness 看门狗和重启阶梯。 -- 签到(check-in)现在完全在 Go 里跑。OpenAPI 的 `/sash/api/v1/me/campaigns` 列表 + `/claim` 调用直接用 bridge 的 `securityOauthToken` 和从 PAT 确定性派生的机器身份发起(不再 spawn `runtime-info` 子进程)。调用前的 token 刷新和 401 时的强制刷新由 bridge 处理。worker 的 `/admin/checkin` 端点降级为仍是 `qoder-native-v1` blob 的老账号的 legacy fallback。 -- 仍带着 legacy `qoder-native-v1` CLI blob(没有 PAT)的 Qoder 老账号在操作员用 PAT 重新登录前继续走 worker 路径。控制台的 PAT 登录路径(`LoginPAT`)现在接受 Qoder PAT 并以 `qoder-pat-v1` 格式存储,之后该账号就不再 spawn worker。 -- 国际版签到改用 OpenAPI bearer(`/api/v1/jobToken/exchange`)来发起 campaign 列表/领取请求,符合该区的 token 要求;国内版继续使用 COSY 的 `securityOauthToken`。401 时现在会先清掉缓存的 OpenAPI bearer 再做一次强制刷新重试。 -- 修复 Qoder 进程内 chat 请求丢失模型思考(reasoning)流的问题。`BuildPlainChatBody` 现在按官方 Qoder 客户端的方式填充 `chat_context`:`chat_context.text`、`chat_context.extra.modelConfig`(`key` + `is_reasoning`)与 `chat_context.extra.originalContent` 带上最新用户输入和路由到的模型标识。网关依据该块决定是否走思考通道,之前 `extra` 为空导致上游只回内容、不回思考。同时把 `source`/`version`/`session_type` 对齐官方客户端(`1` / `"3"` / `"qodercli"`)。现在思考过程会以 `reasoning_content` 流式返回(`/v1/messages` 入口则编码为 Anthropic `thinking_delta`)。 -- 修复 Qoder 进程内模型目录解析与路由丢失问题。`ModelInfos` 之前仅兼容旧版 worker 的 `id` / `mapped_key` 字段,导致直连网关 `/model/list` 的原始数据(仅有 `key` 和 `display_name`)被当成空字段全部跳过,池中 Qoder 账号模型列表为空并报 `model_not_available`。现已自动兼容网关原生 `key` 与 `display_name` 派生 `CanonicalModelID`;`catalog_source` 也同步支持将无 worker 的进程内 Qoder 账号合并入展示目录,且 `manager_probe` 在探针刷新时对这类账号自动触发目录加载。 +- Qoder PAT 账号改为完全进程内运行(不再每账号起 Node worker),token 刷新、模型目录、额度、签到均在进程内完成。(#313) +- 额度总览合计套餐 + 资源包 + 组织资源包;进程内对话恢复思考流输出。 +- 升级提示:老 Qoder 账号需用 PAT 重新登录才会切到进程内模式,否则继续走 worker。 diff --git a/changelog/unreleased/qoder-inprocess-quota-aggregate.md b/changelog/unreleased/qoder-inprocess-quota-aggregate.md deleted file mode 100644 index b1bbf836..00000000 --- a/changelog/unreleased/qoder-inprocess-quota-aggregate.md +++ /dev/null @@ -1,7 +0,0 @@ -### English - -- Qoder PAT accounts: the console quota headline now sums the plan quota, add-on pack, and organization resource package, matching the official client's usage view. Previously only the plan bucket was shown, so an account living on its add-on balance (e.g. after the plan ran out) displayed 0/0. - -### 中文 - -- Qoder PAT 账号:控制台额度总览现在会把套餐内额度、资源包与组织资源包三个桶求和显示,与官方客户端的用量页一致。此前只显示套餐内额度,套餐耗尽后依赖资源包的账号会错误显示为 0/0。 diff --git a/changelog/unreleased/qwenwork-provider.md b/changelog/unreleased/qwenwork-provider.md index 94e373b8..ea5a001f 100644 --- a/changelog/unreleased/qwenwork-provider.md +++ b/changelog/unreleased/qwenwork-provider.md @@ -1,11 +1,9 @@ ### English -- Add the QwenWork provider (`qwenwork`), an in-process adapter for the QwenWork desktop product. Two regions under one provider: `global` (qwenwork.ai, default) and `cn` (千问办公, qwenwork.cn). Browser login uses the single-use device authorization link; poll and token refresh run in-process, with refresh-token rotation stored automatically. Chat goes through the COSY-signed `agent_chat_generation` endpoint and the wrapped upstream SSE is relayed as OpenAI chunks, so `/v1/chat/completions`, `/v1/messages`, and `/v1/responses` all work, streaming and non-streaming, with multi-turn history and tool calls. The model catalog (`qwork-advanced`, `qwork-auto`, …) is fetched live from the upstream `qwork` list, and quota comes from the account-context API. Model-level errors ("Model is not available for this user") cool only that model; quota code 14018 maps to account quota. Credentials can also be imported from a qwenwork2api account export (`accessToken`/`refreshToken`). The CN region follows the desktop bundle configuration but has not yet been verified with a real CN account; international accounts are verified end-to-end. -- Console: new QwenWork International / 千问办公 国内版 account types in the add-account wizard. The authorization link is single-use: open it directly in a browser already signed in to the target account, and do not send it through chat tools. Models are addressable with the `qwenwork/` prefix (for example `qwenwork/qwork-advanced`). -- Protocol behavior was cross-checked against nostalgia296/qwenwork2api-makers (no license declared); the Go implementation is independent and copies no code. +- New QwenWork provider (`qwenwork/` prefix): browser login, chat on all three APIs with tools, live model list, and quota. +- CN region (千问办公) is not yet verified with a real account; Global is verified. ### 中文 -- 新增 QwenWork 供应商(`qwenwork`):QwenWork(千问办公)桌面产品的进程内适配器。同一供应商下两个 region:`global`(qwenwork.ai,默认)与 `cn`(qwenwork.cn)。浏览器登录使用一次性的设备授权链接;轮询与令牌刷新都在进程内完成,refresh_token 轮换后自动保存。对话走 COSY 签名的 `agent_chat_generation` 端点,上游包装 SSE 转换为 OpenAI 分块转发,因此 `/v1/chat/completions`、`/v1/messages`、`/v1/responses` 流式与非流式均可用,支持多轮历史与工具调用。模型目录(`qwork-advanced`、`qwork-auto` 等)实时取自上游 `qwork` 列表,额度来自 account-context 接口。模型级错误("Model is not available for this user")只冷却该模型;额度错误码 14018 映射为账号额度。也支持从 qwenwork2api 的账号导出(`accessToken`/`refreshToken`)导入凭证。国内版按桌面端配置实现,但尚未用真实国内版账号验收;国际版已端到端验证。 -- 控制台:添加账号向导新增 "QwenWork 国际版" 与 "千问办公 国内版" 两种类型。授权链接只能使用一次:请在已登录目标账号的浏览器窗口里直接打开,不要经聊天工具发送。模型可用 `qwenwork/` 前缀指定(例如 `qwenwork/qwork-advanced`)。 -- 协议行为参考并核对了 nostalgia296/qwenwork2api-makers(未声明许可证);Go 实现为独立编写,未复制其代码。 +- 新增 QwenWork(千问办公)供应商(`qwenwork/` 前缀):浏览器登录,三种 API 均可对话并支持工具调用,模型列表与额度实时获取。 +- 国内版尚未用真实账号验证;国际版已验证。 diff --git a/changelog/unreleased/region-qualified-model-prefix.md b/changelog/unreleased/region-qualified-model-prefix.md index 60d3e3e9..cc8f4058 100644 --- a/changelog/unreleased/region-qualified-model-prefix.md +++ b/changelog/unreleased/region-qualified-model-prefix.md @@ -1,11 +1,9 @@ ### English -- Region-qualified model IDs: use `workbuddy-cn/glm-5.3-flash` or `workbuddy-gl/glm-5.3-flash` to route a request to CN or Global accounts respectively. Applies to all multi-region providers (WorkBuddy, Qoder). -- A bare provider prefix (`qoder/…`, `workbuddy/…`) keeps its existing behavior: no region is pinned and the pool auto-picks across regions, so international-only setups keep working unchanged. Bare model names with no prefix still auto-route across regions as before. -- New optional `-auto/` qualifier (`qoder-auto/…`, `workbuddy-auto/…`): an explicit spelling of the bare-prefix behavior — it pins the provider family while letting the pool pick the region, useful for cross-region load spreading within one provider. It reuses the pool's session-affinity latch so a route sticks to one region until it thins out. +- Pin a region with `workbuddy-cn/…` or `workbuddy-gl/…` (also Qoder); `-auto/` picks the region automatically. +- Bare `qoder/…`, `workbuddy/…`, and unprefixed models behave as before. ### 中文 -- 区域限定模型 ID:使用 `workbuddy-cn/glm-5.3-flash` 或 `workbuddy-gl/glm-5.3-flash` 可将请求分别路由到 CN 或 Global 账户。适用于所有多区域 provider(WorkBuddy、Qoder)。 -- 不带区域限定的供应商前缀(`qoder/…`、`workbuddy/…`)行为不变:不锁定区域,由池跨区域自动选择,只有国际账号的部署不受影响。不带任何前缀的裸模型名也仍按原逻辑跨区域自动路由。 -- 新增可选的 `-auto/` 限定形式(`qoder-auto/…`、`workbuddy-auto/…`):与裸前缀行为一致的显式写法——固定供应商但不限定区域,由池自动选择区域,适合在单个 provider 内跨区域分摊负载。复用池的会话粘性机制,路由会粘在一个区域直到该区域候选不足才切换。 +- 可用 `workbuddy-cn/…`、`workbuddy-gl/…` 指定区域(Qoder 同理);`-auto/` 由池自动选区域。 +- 裸前缀 `qoder/…`、`workbuddy/…` 及无前缀模型行为不变。 diff --git a/changelog/unreleased/runtime-quota-recovery.md b/changelog/unreleased/runtime-quota-recovery.md index 482fbbe1..41b14aed 100644 --- a/changelog/unreleased/runtime-quota-recovery.md +++ b/changelog/unreleased/runtime-quota-recovery.md @@ -1,11 +1,9 @@ ### English -- Auto-refresh stale exhausted quota: the maintenance loop now re-fetches quota every 5 minutes, but only for accounts whose cached snapshot still reports `Exceeded`. A snapshot can go stale when upstream credits are replenished out-of-band (billing reset, add-on purchase) or when a flaky quota endpoint leaves an old `Exceeded=true` in place, so the account stayed out of routing until an operator manually refreshed it. Healthy accounts are never touched by this sweep. -- Quota error instead of "no accounts available": when every account that can serve a model is out of quota, the request now returns `429 insufficient_quota` rather than a generic "no accounts available", so the client gets the real cause. -- Back off quota probing for genuinely exhausted accounts: the maintenance sweep still starts at 5 minutes, but each account that keeps reporting `Exceeded` now steps through 5m → 5m → 5m → 15m → 30m → 1h → 3h → 6h → 12h → 24h instead of being re-fetched every five minutes forever. Provider balances reset on a daily cadence, so a permanently empty account no longer costs ~288 pointless quota calls a day, settling at roughly one probe per day. The ladder resets to the fast end as soon as a snapshot reports a non-zero balance, and a failed probe keeps its level instead of escalating a transient outage into an hours-long wait. +- Accounts marked out of quota are re-checked automatically, with backoff from 5 minutes up to 24 hours. +- When every eligible account is out of quota, requests return `429 insufficient_quota` instead of "no accounts available". ### 中文 -- 自动刷新 stale 的已耗尽配额:维护循环现在每 5 分钟重新拉取一次配额,但仅针对缓存快照仍为 `Exceeded` 的账号。快照可能因上游额度被外部恢复(计费重置、加购附加包)或配额接口抖动留下旧的 `Exceeded=true` 而变 stale,导致账号一直被路由排除,直到运维手动刷新才恢复。健康账号不会被此扫描触碰。 -- 配额耗尽返回配额错误而非「无可用账户」:当能服务某模型的所有账号都配额耗尽时,请求现在返回 `429 insufficient_quota`,而不是笼统的「无可用账户」,让客户端拿到真实原因。 -- 为真正耗尽的账号增加配额探测退避:维护扫描仍从 5 分钟起步,但连续报告 `Exceeded` 的账号会依次退避到 5m → 5m → 5m → 15m → 30m → 1h → 3h → 6h → 12h → 24h,而不是永远每 5 分钟重拉一次。供应商额度一般按天重置,长期为空的账号不会再每天产生约 288 次无效配额调用,最终稳定在每天约 1 次探测。一旦快照报告非零余额,退避立即回到最快档;探测失败则保持当前档位,不会把瞬时故障升级成数小时的等待。 +- 额度耗尽的账号会自动复查,间隔从 5 分钟逐步退避到 24 小时。 +- 所有可用账号都耗尽额度时,返回 `429 insufficient_quota`,而非「无可用账户」。 diff --git a/changelog/unreleased/trae-login-checkin-fixes.md b/changelog/unreleased/trae-login-checkin-fixes.md index e4faac89..9f514d01 100644 --- a/changelog/unreleased/trae-login-checkin-fixes.md +++ b/changelog/unreleased/trae-login-checkin-fixes.md @@ -1,7 +1,7 @@ ### English -- Trae browser login now completes on its own. The loopback listener only parked the authorization code and waited for a console poll to redeem it, but the console's poll window is far shorter than a real browser login (an SMS login can take minutes), so the poll expired first and the code was never exchanged. The callback now exchanges the code and stores the credential as soon as the browser reaches the loopback listener, so closing the browser or missing the poll window no longer costs the login. A pasted callback URL still works as a fallback and now actually redeems the parked code; a callback URL carrying only `loginTraceID` is accepted for that path; and the single-use code is exchanged exactly once even when the callback, a poll, and a paste overlap. +- Trae browser login now completes on its own, even for slow SMS logins; pasting the callback URL still works as a fallback. ### 中文 -- Trae 浏览器登录现在会自行完成。此前 loopback 监听只把授权码暂存下来,等待控制台轮询去换取令牌,但控制台的轮询窗口远短于真实浏览器登录(短信登录可能耗时数分钟),轮询先超时退出,授权码因此从未被换取。现在浏览器一到达 loopback 监听,回调就会立即换取授权码并写入凭据,关闭浏览器或错过轮询窗口都不再导致登录失败。粘贴回调 URL 仍可作为兜底,并且现在会真正换取已暂存的授权码;仅携带 `loginTraceID` 的回调 URL 也被接受用于该路径;即便回调、轮询与粘贴同时发生,单次使用的授权码也只会被换取一次。 +- Trae 浏览器登录现在可自动完成,短信登录较慢也不会失败;粘贴回调 URL 仍可兜底。 diff --git a/changelog/unreleased/workbuddy-waf-fingerprint-sanitization.md b/changelog/unreleased/workbuddy-waf-fingerprint-sanitization.md index 140cd4fd..e3381acc 100644 --- a/changelog/unreleased/workbuddy-waf-fingerprint-sanitization.md +++ b/changelog/unreleased/workbuddy-waf-fingerprint-sanitization.md @@ -1,7 +1,7 @@ ### English -- WorkBuddy request payloads now automatically sanitize client environment fingerprint phrases (such as `Main branch (you will usually use this for PRs)` injected by ZCode / Claude Code environments) before dispatching to upstream. Upstream Tencent WorkBuddy WAF detects these signatures and rejects requests with error `11128: Illegal API invocation from an unapproved channel`. The pre-dispatch sanitizer transparently rewrites these phrases into neutral equivalents across all message roles and structured text parts, preventing rejection without requiring users to discard their entire system prompt. +- WorkBuddy strips client fingerprint phrases (e.g. from Claude Code) before sending, avoiding upstream error `11128`. ### 中文 -- WorkBuddy 请求体在发送给上游前现已自动脱敏客户端环境签名特征词(例如 ZCode / Claude Code 环境自动注入的 `Main branch (you will usually use this for PRs)`)。腾讯 WorkBuddy 上游 WAF 会检测此类特征词并将请求拦截为 `11128: Illegal API invocation from an unapproved channel`。现在的预处理流水线会在发送前自动将所有消息角色和多段文本中的敏感短语透明重写为中性表述,避免请求被拦截,且无需强制丢弃完整的系统提示词。 +- WorkBuddy 发送前自动改写客户端特征语句(如 Claude Code 注入的内容),避免上游报 `11128`。 diff --git a/changelog/unreleased/worker-stream-timeout.md b/changelog/unreleased/worker-stream-timeout.md index b09c0207..f12d775e 100644 --- a/changelog/unreleased/worker-stream-timeout.md +++ b/changelog/unreleased/worker-stream-timeout.md @@ -1,11 +1,11 @@ ### English -- Stop streaming chat from hanging on a stuck upstream. A Qoder stream request whose first byte never arrives (the symptom was `qwen3.8-flash` requests with only an entry log line and no `chat reasoning resolved`) now fails fast after a 30-second TTFB deadline, classifies as `first_byte_timeout` / `unavailable` with failover, and is retried on the next eligible account instead of hanging the client until it gives up. The deadline is on the response head only — once headers arrive, the SSE body streams under the request's normal lifetime, so long thinking-model output is not cut off. -- Give the worker daemon its own upstream timeout. The worker's `fetch` to the Qoder gateway previously had no `AbortSignal`, so a stalled upstream held the worker's in-flight slot forever. It now aborts on a 30-second first-byte deadline, plus a mid-stream stall watchdog that fires only after 300 seconds of silence between chunks (it resets on every chunk, so a long but steady thinking stream is never truncated), returning a stable `first_byte_timeout` / `stream_total_timeout` error so the Go side can classify and fail over. Both are tunable via `QODER_STREAM_TTFB_MS` / `QODER_STREAM_TOTAL_MS`. -- Add a worker liveness watchdog to the maintenance loop. Health probes previously ran only at boot plus on manual console refresh, so a worker that stayed "alive" as a process but stopped serving `/health` was never noticed and kept being routed to. Every 30-second tick now issues one lightweight `/health` round-trip per worker account; three consecutive failures (~90s grace, enough to ride out a transient blip) restart the worker through the existing recovery path with backoff. A worker that answers but is still starting, rewarming, or waiting for login (`ready=false`) is treated as alive, so warming accounts are not rebooted. In-process accounts (WorkBuddy / Trae / Devin) are skipped and unaffected. +- Qoder streams with no first byte in 30s now fail over to the next account instead of hanging (`QODER_STREAM_TTFB_MS`). +- Worker aborts a stream only after 300s with no new chunk (`QODER_STREAM_TOTAL_MS`); long active streams are not cut. +- Unresponsive Qoder workers are detected and restarted automatically. ### 中文 -- 流式 chat 不再因上游卡死而无响应地挂起。Qoder 流式请求如果首字节迟迟不到(现象是 `qwen3.8-flash` 请求只有入口日志、没有 `chat reasoning resolved` 出口日志),现在会在 30 秒 TTFB 阈值后快速失败,归类为 `first_byte_timeout` / `unavailable` 并触发 failover,切到下一个可用账号重试,而不是把客户端挂到自行超时。该超时只针对响应头:一旦头部到达,SSE body 回到请求原本的生命周期,长思考模型的输出不会被截断。 -- 给 worker 守护进程加上自己的上游超时。worker 对 Qoder 网关的 `fetch` 此前没有 `AbortSignal`,上游停滞时会永久占用 worker 的并发槽。现在它在 30 秒首字节超时,以及"连续 300 秒没有新数据"的中途停滞看门狗(每收到一个 chunk 就重置,所以长但持续输出的思考流不会被截断)任一触发时 abort,返回稳定的 `first_byte_timeout` / `stream_total_timeout` 错误,让 Go 侧分类并 failover。两者均可通过 `QODER_STREAM_TTFB_MS` / `QODER_STREAM_TOTAL_MS` 调整。 -- 给维护循环加 worker liveness 看门狗。健康探针此前只在启动时和手动控制台刷新时跑,所以一个"进程还活着但 `/health` 不再响应"的 worker 永远不会被察觉,还会继续被路由命中。现在每 30 秒维护周期对每个 worker 账号发一次轻量 `/health` 探针;连续 3 次失败(约 90 秒容忍窗口,足以扛过瞬时抖动)就通过既有的恢复路径按退避重启该 worker。对 `/health` 有响应但仍在启动、rewarm 或等待登录(`ready=false`)的 worker 视为存活,不会误杀。进程内账号(WorkBuddy / Trae / Devin)被跳过,不受影响。 +- Qoder 流式请求 30 秒内无首字节会切换到下一个账号,不再挂起(`QODER_STREAM_TTFB_MS`)。 +- worker 仅在连续 300 秒无新数据时中止流(`QODER_STREAM_TOTAL_MS`),持续输出的长流不会被截断。 +- 无响应的 Qoder worker 会被自动发现并重启。 diff --git a/deploy/README.md b/deploy/README.md index ec2ec616..ed8c74b9 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -134,6 +134,9 @@ console; environment variables cannot replace it. | `QODER_MAX_RETRY_ACCOUNTS` | `4` | Maximum accounts attempted for one request (1-64) | | `QODER_SSE_DIAGNOSTIC_MODELS` | empty | Comma-separated Qoder model IDs for redacted SSE diagnostics in Runtime Logs; `*` enables all | | `QODER_WORKER_BASE_PORT` | `32100` | Internal child-runtime port range | +| `QODER_STREAM_TTFB_MS` | `30000` | Qoder stream first-byte (response head) deadline in ms, Go side and worker; min `5000`; timeout fails over to the next account | +| `QODER_STREAM_TOTAL_MS` | `300000` | Qoder worker mid-stream stall timeout in ms: aborts only after this long with no new chunk (resets per chunk, not a wall-clock cap); never below the TTFB value | +| `QODER_SIGNATURE_SECRET` | built-in | Override the COSY request-signing secret for in-process Qoder; leave unset unless upstream rotates it | | `QODER_PROXY_URL` | empty | Initial global outbound proxy: `http(s)://`, `direct`, or `none`; saved console settings take precedence | | `QODERCLI_JS` | image default | Pinned Qoder Global CLI bundle | | `QODERCNCLI_JS` | image default | Pinned Qoder CN CLI bundle |