Skip to content

[Bug] BYOK 首个 turn 静默回退系统 Auto 并扣费:预热 Query 携带过期 cliRuntimePolicy + get_model_policy 错误被吞 #5

Description

@oljh0

[Bug] BYOK 模型在桌面端首个 turn 静默回退系统 Auto 并产生扣费

环境

  • Qoder Desktop 0.1.6(Windows,C:\Program Files\Qoder\Qoder,app.asar 内 out/main/index.js)
  • 内置 worker runtime qoder-worker-runtime 1.1.40(app.asar.unpacked/node_modules/@qoder-ai/qoder-agent-sdk/dist/_worker/qoder-worker-runtime.obf.mjs,runtime-info.json:build 2026-09-01,commit 0ec47ef)
  • 自定义模型:BYOK zhipu/glm5.3-cp(早已配置并可正常使用)
  • 时间均为北京时间 2026-09-05

现象

应用启动后,用户已选择 BYOK 自定义模型并发送首个 turn。该 turn 实际全部运行在系统 Auto(及附属 Lite)上并按 Credits 扣费,界面无任何提示;从第二个 turn 起同一模型选择恢复正常(全部为 custom model 请求)。

复现步骤

  1. 配置一个 BYOK 自定义模型;
  2. 完全退出并重启 Qoder Desktop;
  3. 在既有会话中选择该 BYOK 模型,立即发送首个 turn;
  4. 观察:该 turn 的推理实际走系统 Auto;用量页出现 Auto 扣费;下一 turn 自动恢复 BYOK。

证据时间线(本地日志摘录)

宿主 main.log(AppData/Roaming/com.qoder.app.stable/logs/20260904-172431.*/main.log,UTC):

17:26:07.722Z ChatSession Query preparation completed {...}          ← 预热 Query 就绪
17:26:15.341Z ChatSession model policy frozen {isByok:true, modelSource:"user", ...}
17:26:15.342Z ChatSession prepared Query attached {...}              ← 冻结后 1ms 即接管,中间无 recycle
(直到下一 turn 才出现↓)
17:36:20.079Z ChatSession query recycled for CLI runtime policy {previousPolicy:"configured", nextPolicy:"bundled-only"}
17:36:21.295Z ChatSession Query open completed {querySessionMode:"resume", ...}   ← 此后 BYOK 正常

worker qodercli.log(run 2026-09-05T01-25-59-834+08-00-*,version=1.1.40):

01:26:16.390 ERROR [HeadlessSession] get_model_policy failed for main: Error: BYOK_EXECUTION_UNSUPPORTED
01:27:12~01:28:25  同类错误共 9 次(utility×4、web_fetch×2、suggestion×1、title×1、main×1)

该 turn 实际产生 5 个 Auto 主请求 + 8 个 Lite 附属请求,全部成功返回并计费;turn 正常完成,无失败提示。

根因(对本地安装包的静态分析)

两个缺陷叠加:

① 宿主(0.1.6,out/main/index.js):预热 Query 携带过期的 cliRuntimePolicy,且接管路径绕过回收检查。

  • 预热 Query 创建时 cliRuntimePolicy 取自 activeCliRuntimePolicy(),而此时 turn policy 尚未冻结(activeModelPolicy 为空)→ 落入默认值 "configured";BYOK 场景模型种子按设计写为 "auto"(resolveQuerySeed:BYOK → "auto")。
  • turn 到达时 deliver() 依次执行 freezeTurnModelPolicy() → recycleQueryForCliRuntimePolicy() → ensureOpen();但回收函数在 this.query 为空(预热会话尚未 attach)时直接短路返回,随后 attachPreparedQuery() 无条件接管该会话。于是 17:26:15.341→.342 的日志里"冻结 BYOK"与"接管"之间没有 recycle。
  • worker 随后发起 get_model_policy 控制请求;宿主 resolveSdkModel 校验的是会话创建时闭包捕获的 cliRuntimePolicy("configured")→ throw BYOK_EXECUTION_UNSUPPORTED。
  • 直到第二个 turn,this.query 已存在,回收条件才成立(previousPolicy:"configured" → nextPolicy:"bundled-only"),此后一切正常——这解释了"仅首个 turn 异常"。

② worker(qoder-worker-runtime 1.1.40):get_model_policy 失败被吞掉,静默回退到种子模型。

  • get_model_policy 处理函数的 catch 中,仅 abort/取消类错误(FWu() 判定)会被重抛;BYOK_EXECUTION_UNSUPPORTED 等其余错误仅记录日志后 return void。
  • SDK 模型路由 iZ.resolve() 先解析基础模型,再咨询 policy:if (!policy) return baseModel; —— policy 为空即静默使用种子模型继续执行(main → auto,附属用途 → Lite),turn 照常成功并计费。

与官方文档的矛盾

文档称 dynamic model 回调失败时没有自动 fallback,失败应使 query 失败(https://docs.qoder.com/cli/sdk/model-policy )。实际实现是 policy 仅具咨询性:失败 → 静默回退到种子模型并正常计费。这使用户的 BYOK 选择在无任何提示的情况下被替换为计费模型。

影响

  • 2026-09-05 01:26:1 个主 turn 内 5 次 Auto 计费请求(用量行:2min / Auto / 已扣费 6.42/6.39 credits / $0.12)。
  • 2026-09-03 22:17 ~ 09-04 03:26:同一代码路径的整会话回退,约 556.13 credits / $11.12。
  • 本质问题:未经用户授权的计费模型替换 + 无提示,且 BYOK 用户的第三方 API key 根本未被使用。

修复建议

  1. 宿主:attachPreparedQuery() 接管前校验预热会话的 queryCliRuntimePolicy 与当前 activeCliRuntimePolicy() 一致;不一致时走既有 recycle 路径(即把 this.query 为空但 preparedAgentSession 存在的情况纳入回收检查)。
  2. worker:get_model_policy 失败(至少 BYOK_EXECUTION_UNSUPPORTED 这类显式策略拒绝)应使当前推理/turn 失败或向用户明显报错,与文档声明的"无自动 fallback"保持一致,而不是静默回退到计费种子模型。
  3. 建议对 09-03 与 09-05 两次事故的受影响交易进行核验与返还。

附

  • 本地证据链完整:宿主/worker 双端日志、run 20260905T01-25-59-* 内 9 条 policy 错误、逐请求索引、以及上述反编译函数名均可复核。
  • 如需完整日志或进一步复现材料,可联系我。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions