Skip to content

Releases: dddmiku/workbuddy2api

v2.1.12 修正循环保护的压制上限,让重发真正生效

Choose a tag to compare

@github-actions github-actions released this 20 Sep 13:07

变更

修复:循环保护的重发几乎没机会触发。

Devin 那把密钥反复断流,根因不是「没做重试」,而是重试的触发条件几乎不可能满足。推理侧的压制有两条上限:字符上限 40000、字节上限 2 MiB。线上上游把推理切成约 220 字节的小帧、每帧只带一两个字符(实测 124–222 字节/字符),于是 2 MiB 只折合一万多字符。缓冲会在 256 行窗口凑满、重复覆盖达标之前就被放行——客户端先看到一屏重复文本、再收到 422,同账号重发这条路径根本没走到。

48 小时里 39 次循环判定只有 13 次走到了重发,其余 26 次都从这里漏掉。

现在字节上限提到 16 MiB(覆盖 65536 字符 × 222 字节 ≈ 14 MB),字符上限提到 65536(实测判定点落在 9439–55777)。字符与时间上限成为真正的绑定条件,不再被字节上限抢先。

顺带修复:走 Chat Completions 的客户端拿不到运行约定。

Devin、narrafork 调用的是 /v1/chat/completions,而「别说完就停」这条运行约定此前只挂在 /v1/responses 上。结果模型回一句「让我先确认一下…」就结束本轮,客户端不会自动续跑,用户只能反复手动发「继续」。现在两条路径都会注入,并按请求体里实际的 tools/functions 声明判断是否带工具;纯对话不注入,Responses 路径不会重复注入。

部署

热更新即可,不需要重建容器:

docker compose pull
docker compose up -d

已知边界

保护仍是启发式:合法的重复短行可能误报,无换行的循环不在覆盖范围。业务本来就需要大量重复短行时,可以按密钥关闭这项保护。

压制期间每条在途请求最多占用 16 MiB 内存。峰值并发实测约 7 条在途请求,按每条上限计约 112 MB;即便 24 账号 × 单账号 3 在途全部占满(72 条)也在 1.2 GB 以内。

运行约定是提示层的缓解措施,不是启发式重试,也不能保证第三方模型在每次长会话里都完成任务。

v2.1.11 运行约定的 Chat 路径回归与文档

Choose a tag to compare

@github-actions github-actions released this 20 Sep 12:45

见 v2.1.12 说明;本版为上一版的回归测试与文档同步。

v2.1.10 运行约定覆盖 Chat 路径

Choose a tag to compare

@github-actions github-actions released this 20 Sep 12:37

见 v2.1.12 说明;本版修复运行约定只覆盖 Responses 路径的问题。

v2.1.9 正文重复输出保护与压缩阈值口径

Choose a tag to compare

@github-actions github-actions released this 20 Sep 06:01

变更

修复:正文里的重复输出现在也会被拦下。 此前保护只监控 reasoning_content,而线上观测到的「疯狂输出」发生在正文里:抓包里是「我执行。」重复 127230 行、唯一行只有 1 个,推理侧却只有 3736 字符、重复覆盖 23%,永远达不到阈值,于是既不中断也不重试。

正文现在单独一路计数,并用独立的错误码 upstream_output_loop 与推理侧的 upstream_reasoning_loop 区分开,调用方据此判断「模型在思考里打转」还是「重复正文正在刷屏」。正文比推理更保守:除共用的「窗口内不同项不超过 12、重复覆盖至少 95%」外,还要求出现最多的那一行占 75% 以上,避免两行或三行交替重复的表格、代码块被误截。

写出闸门同步扩到正文。正文从第一帧就压住,命中时客户端仍是零字节,可以整段丢弃并在同一账号上重发;出现超过 32 字符的长行、空行或第三种短行即判定不是短行循环并立即放行,正常回答通常只多等一个行尾。正文另有 8 秒独立时限与 32768 字符上限,没有换行的短回答不会被压到推理侧的 60 秒。命中后压住的那段重复正文在重发用尽时也直接丢弃,只写错误帧,避免用户先看到一屏重复输出再看到失败。

修正压缩阈值口径。 客户端模型目录里 deepseek-v4.1-flashauto_compact_token_limit 由 466666 提到 900000。466666 是早期按 1.5 倍输入估计缩小的保守值,而 1.5 倍已在网关侧退役;现在客户端按上游原值计数,900000 正好是 1000000 窗口的 9/10,与 Codex 内置默认口径一致,不再出现「远没到临界就压缩」。global:hy3 的 120000 保持不变:192000 窗口配 64000 最大输出,留出的余量是刻意的,不是同一个 1.5 倍造成的。

部署

热更新即可,不需要重建容器:

docker compose pull
docker compose up -d

网关与面板镜像都已发布 v2.1.9。管理台「重复推理保护」开关现在覆盖推理与正文两条流,文案已同步更新。

已知边界

保护仍是启发式:合法的重复短行可能误报,无换行的循环不在覆盖范围,正文侧另有「占多数的一行」与空行两道防线来减少误截。业务本来就需要大量重复短行时,可以按密钥关闭这项保护。

压缩阈值写在客户端模型目录里,不由网关下发。若发现压缩时机仍不合适,先核对实际生效值——它是 auto_compact_token_limitcontext_window 九成的较小值。

v2.1.8 · 密钥累计用量与有效期

Choose a tag to compare

@github-actions github-actions released this 20 Sep 04:21

新增:密钥列表显示累计用量,密钥可设有效期

密钥列表此前看不到任何消耗,管理员没法判断哪把密钥在实际使用;而且每把密钥都是永久有效,
发出去的短期试用密钥也只能靠手动删除来收回。

密钥页新增两列:

  • 总用量:显示该密钥累计的 token 数(输入 + 输出),数据来自用量账本。账本是独立
    文件,面板在返回密钥列表时一并合并,浏览器不需要再多拉一次接口;账本不可用时该列显示
    0,密钥列表照常加载。
  • 有效期:显示「无限制」/「N 天后到期」/「已过期」。创建或编辑密钥时可以选择
    7 / 30 / 90 天、1 年或自定义时间,留空表示无限制。

既有密钥和新建密钥的默认都是无限制,这次升级不会给任何密钥加上到期时间,也不需要
迁移:密钥文件里没有 expires_at 字段就等于永久有效。到期后该密钥立即失效,网关返回
api_key_expired 而不是笼统的 invalid_api_key,调用方能区分「密钥过期」和「密钥不对」;
在管理台重新设置有效期即可继续使用。

有效期只影响鉴权,不改变密钥的启停、模型白名单或用量记录。修改时会拒绝过去的时间、
超过十年的时间和格式错误的值;只改名称时不会顺手清掉已设的有效期。

部署

本次同时更新网关与管理台,两者的镜像标签都是 v2.1.8

cd /opt/workbuddy2api
sudo docker compose pull
sudo docker compose up -d

已知边界

  • 「总用量」是账本累计值,与用量统计页口径一致;它不按密钥的有效期或日期过滤。
  • 有效期按网关时间判定,没有提前提醒;到期后需要管理员手动续期。
  • 删除密钥不会删除账本里该密钥的历史用量记录。

v2.1.7 · 压制窗口对齐真实循环判定点

Choose a tag to compare

@github-actions github-actions released this 20 Sep 02:54

修复:压制窗口对齐真实循环的判定点

v2.1.6 引入「命中重复推理后先截断、再在同一账号上重发」时,把流式压制窗口的上限设成
12000 字符。上线后线上日志显示,真实循环的判定点落在 18085–34341 字符之间(自上次实际
进展起算),于是循环在网关开始压制之前就已经写给了客户端,重发没有机会执行,线上仍然
返回 upstream_reasoning_loop

这一版把上限对齐实测数据:

  • 字符上限提到 40000,覆盖线上观测到的全部形态(最宽的一次 34341)并留出余量。
  • 新增 60 秒时间上限,覆盖「想得慢、每次只吐几个字」的流——那种情况字符数上不去,
    只靠字符上限会让客户端长时间收不到任何响应。
  • 字节上限提到 2 MiB,避免分帧很碎的流提前打开闸门。

出现正文、拒绝或工具进展时仍然立即放行,因为已经写给客户端的内容收不回来。重发成功时
调用方只会看到第二次的输出;重发后仍循环、或客户端已收到内容时,行为与之前一致。

部署

网关镜像随本 tag 发布,标签 v2.1.7latest 都已推送。线上用热更新完成切换,
当前对话未中断。

cd /opt/workbuddy2api
sudo docker compose pull wb2api
sudo docker compose up -d wb2api

已知边界

  • 压制窗口按已观测的循环形态标定:从零开始的循环最迟在 40000 字符前就会命中。循环若
    只在更靠后的位置才开始,闸门已经放行,只能按原有方式报错。
  • 重发上限仍是一次;持续循环会如实报错,不会无限重试。
  • 被丢弃的那一轮确实已经生成并计费,用量照实累计并标记为「用量未完整返回」。

v2.1.6 · 重复推理循环先截断再自动重试

Choose a tag to compare

@github-actions github-actions released this 20 Sep 02:32

修复:重复推理循环改为「先截断、再自动重试」

此前 DeepSeek 陷入重复短行推理循环时,网关直接中止请求并返回 upstream_reasoning_loop。用户在思考中途看到回合失败,只能自己重发。

循环是模型行为、不是账号故障,同一个账号再问一次通常就能拿到干净的一轮。现在命中保护后,网关会先在同一账号上重发一次,用户只会看到第二次的成功输出。

  • 重发只在客户端还没收到任何字节时进行。流式请求在「只有推理、还没有正文/拒绝/工具进展」的阶段把帧压在内存里,命中时整段丢弃;一旦出现正文、拒绝或工具进展就立即放行,此后命中只能照旧回报错误,因为写出去的收不回。
  • 压制有上限:推理累计到 12000 字符、或缓冲达到 256 KiB 时立即放行,正常长推理不会因为保护而延迟显示。
  • 被丢弃的那一轮确实已经生成并计费,用量照实累计,并把该请求标记为「用量未完整返回」,不伪装成完整账单。
  • 重发没能建立(上游连接失败或直接报错)时,失败会如实写回客户端,回合不会静默结束。
  • 非流式请求同样重发一次,客户端在读完前本来也是零字节。

重发固定落在同一账号,不换号、不冷却、不解除已有会话粘性。重发一次后仍循环、或客户端已收到内容时,行为与之前一致:非流式 Chat 与 Responses 返回 422,已开始的流式请求保留 200 并以错误事件收尾。

部署

网关镜像随本 tag 发布,标签 v2.1.6latest 都已推送。线上用热更新完成切换(新进程先接管监听、旧进程把在途请求跑完再退出),当前对话未中断。

cd /opt/workbuddy2api
sudo docker compose pull wb2api
sudo docker compose up -d wb2api

已知边界

  • 重发上限是一次。持续循环时仍会如实报错,不会无限重试。
  • 客户端已经收到正文后命中循环,无法再重发,只能按原有方式报错。
  • 保护本身仍是启发式:合法的重复短行可能误报,无换行的循环不在覆盖范围内,业务需要大量重复短行时可关闭对应密钥的开关。

v2.1.5 · 不带会话标识的客户端恢复账号粘性

Choose a tag to compare

@github-actions github-actions released this 20 Sep 01:38

修复:不带会话标识的客户端也能保持账号粘性

问题来自线上实测。同一个调用密钥下,Codex 那路的请求每轮都命中同一个账号、上游提示缓存稳定(hit 稳定在几十万),而 narrafork 那路(/v1/chat/completions)每一轮都换号,hit 恒为 0。

抓包定位到根因:narrafork 的请求体顶层只有 modelmessagesstreamstream_optionsmax_tokenstoolstool_choicereasoning_effort,既没有 conversation_id,也没有 metadataclient_metadataprompt_cache_key。网关的会话键提取依次读这些字段,全都读不到就返回空串,会话粘性于是完全不生效——每一轮都走普通轮换重新选号,同一对话的上下文缓存整段作废。

修复方式是在没有显式会话标识时,从请求正文派生一个对话级回退键:取第一条 user 消息的文本做哈希。对话开头在整个对话内不变,所以同一对话的后续轮次(末尾不断追加消息)保持同一个账号,不同对话各自绑定。用抓到的真实流量验证过这个前提:14 条请求正好聚成 2 个对话,一个首条 user 125 字符、8 条请求,另一个 32 字符、6 条请求,而消息数从 255 一路涨到 670,首条 user 文本始终不变。

几处边界是有意这样定的:

  • 显式会话标识仍然优先。Codex 之类会发 prompt_cache_key 的客户端行为完全不变。
  • 回退键只用于账号绑定,不参与上游关联头的聚合粒度。那部分在没有显式标识时仍按「对话轮」聚合,把回退键混进去会把粒度从轮级改成对话级,是另一件事。
  • 关掉粘性(session_sticky.enabled=false)时依然不建立任何绑定。
  • 首条 user 消息没有文本(纯图片等)时不伪造键,保持原有的无粘性行为。
  • 客户端裁剪历史把首条 user 删掉时键会变化、绑定重新分配,只影响缓存亲和;两条对话恰好以同样文本开头时会共用绑定,粒度仍细于原有的 metadata.user_id 回退。

部署

网关镜像随本 tag 发布,标签 v2.1.5latest 都已推送。线上用热更新完成切换(新进程先接管监听、旧进程把在途请求跑完再退出),当前对话未中断。

cd /opt/workbuddy2api
sudo docker compose pull wb2api
sudo docker compose up -d wb2api

已知边界

  • 回退键依赖客户端在后续轮次里保留首条 user 消息。这是主流 agent 客户端的常态,但不是协议保证。
  • 本次只改会话粘性的键来源,选号、冷却、用量记账与协议转换均未改动。