[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 请求)。
复现步骤
- 配置一个 BYOK 自定义模型;
- 完全退出并重启 Qoder Desktop;
- 在既有会话中选择该 BYOK 模型,立即发送首个 turn;
- 观察:该 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 根本未被使用。
修复建议
- 宿主:
attachPreparedQuery() 接管前校验预热会话的 queryCliRuntimePolicy 与当前 activeCliRuntimePolicy() 一致;不一致时走既有 recycle 路径(即把 this.query 为空但 preparedAgentSession 存在的情况纳入回收检查)。
- worker:
get_model_policy 失败(至少 BYOK_EXECUTION_UNSUPPORTED 这类显式策略拒绝)应使当前推理/turn 失败或向用户明显报错,与文档声明的"无自动 fallback"保持一致,而不是静默回退到计费种子模型。
- 建议对 09-03 与 09-05 两次事故的受影响交易进行核验与返还。
附
- 本地证据链完整:宿主/worker 双端日志、run
20260905T01-25-59-* 内 9 条 policy 错误、逐请求索引、以及上述反编译函数名均可复核。
- 如需完整日志或进一步复现材料,可联系我。
[Bug] BYOK 模型在桌面端首个 turn 静默回退系统 Auto 并产生扣费
环境
C:\Program Files\Qoder\Qoder,app.asar内out/main/index.js)app.asar.unpacked/node_modules/@qoder-ai/qoder-agent-sdk/dist/_worker/qoder-worker-runtime.obf.mjs,runtime-info.json:build 2026-09-01,commit0ec47ef)zhipu/glm5.3-cp(早已配置并可正常使用)现象
应用启动后,用户已选择 BYOK 自定义模型并发送首个 turn。该 turn 实际全部运行在系统 Auto(及附属 Lite)上并按 Credits 扣费,界面无任何提示;从第二个 turn 起同一模型选择恢复正常(全部为 custom model 请求)。
复现步骤
证据时间线(本地日志摘录)
宿主
main.log(AppData/Roaming/com.qoder.app.stable/logs/20260904-172431.*/main.log,UTC):worker
qodercli.log(run2026-09-05T01-25-59-834+08-00-*,version=1.1.40):该 turn 实际产生 5 个 Auto 主请求 + 8 个 Lite 附属请求,全部成功返回并计费;turn 正常完成,无失败提示。
根因(对本地安装包的静态分析)
两个缺陷叠加:
① 宿主(0.1.6,
out/main/index.js):预热 Query 携带过期的cliRuntimePolicy,且接管路径绕过回收检查。cliRuntimePolicy取自activeCliRuntimePolicy(),而此时 turn policy 尚未冻结(activeModelPolicy为空)→ 落入默认值"configured";BYOK 场景模型种子按设计写为"auto"(resolveQuerySeed:BYOK →"auto")。deliver()依次执行freezeTurnModelPolicy()→recycleQueryForCliRuntimePolicy()→ensureOpen();但回收函数在this.query为空(预热会话尚未 attach)时直接短路返回,随后attachPreparedQuery()无条件接管该会话。于是 17:26:15.341→.342 的日志里"冻结 BYOK"与"接管"之间没有 recycle。get_model_policy控制请求;宿主resolveSdkModel校验的是会话创建时闭包捕获的cliRuntimePolicy("configured")→throw BYOK_EXECUTION_UNSUPPORTED。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。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 选择在无任何提示的情况下被替换为计费模型。
影响
修复建议
attachPreparedQuery()接管前校验预热会话的queryCliRuntimePolicy与当前activeCliRuntimePolicy()一致;不一致时走既有 recycle 路径(即把this.query为空但preparedAgentSession存在的情况纳入回收检查)。get_model_policy失败(至少BYOK_EXECUTION_UNSUPPORTED这类显式策略拒绝)应使当前推理/turn 失败或向用户明显报错,与文档声明的"无自动 fallback"保持一致,而不是静默回退到计费种子模型。附
20260905T01-25-59-*内 9 条 policy 错误、逐请求索引、以及上述反编译函数名均可复核。