Skip to content

feat(codex): 支持 Bot 独立配置速度档位 - #1820

Merged
deepcoldy merged 4 commits into
deepcoldy:masterfrom
stormluke:codex/bot-service-tier
Oct 12, 2026
Merged

deepcoldy merged 4 commits into
deepcoldy:masterfrom
stormluke:codex/bot-service-tier

Conversation

@stormluke

@stormluke stormluke commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1819

变更

Codex Bot 可独立配置新会话的速度档位,避免多个 Bot 共用原生配置时无法分别选择 Fast 等档位。保存和启动前按实际 binary、环境及模型验证加速能力,无法确认时拒绝配置。

Bot serviceTier → 新会话快照 → 最终会话 CLI → 能力校验 → TUI / RPC / App runner
  • serviceTier 支持继承、显式 default、priority、ultrafast;/config 与 Dashboard 可读写。Dashboard 只提供当前模型目录声明的加速档位。
  • 新会话创建时冻结档位;已有会话及旧数据不回填 Bot 新配置;Fork 按目标 Bot 重新快照。陈旧整行写入保留已冻结值,有意清除落盘 inherit。
  • /cli 预选与普通会话统一按最终 CLI 处理:非 Codex 清除档位,已冻结为 Codex 的会话保留档位。首次启动前修改 Bot CLI 不再导致会话永久拒启;解析发生在持久层回填之后。
  • 三条执行路径透传:TUI 根级 -c service_tier=...;RPC 与 App runner 的 thread/start / thread/resume 顶层 serviceTier。恢复不重发 model/provider/effort。
  • 两个 Codex 适配器与配置、启动校验共用能力目录探测。通用模型目录继续缓存 Aiden 的专用探测结果,保留其他 CLI 的 slug-only 契约。

验证

  • 修复前,首次启动前改换 CLI 的 default、priority、ultrafast 三个回归用例均触发 Session service tier ... is not supported by CLI;修复后通过,且验证 inherit 写盘、重新解析和冻结 Codex 档位保留。
  • 本轮相关 21 个测试文件:1656 passed,1 skipped。覆盖配置读写、SQLite 会话快照、Dashboard、TUI 参数、RPC start/resume、App runner、陈旧对象回填及非 Codex 回归。
  • bun run build、git diff --check:通过。构建包含主工程、脚本及测试 mock 类型检查和 Dashboard bundle。
  • head 7fce3ba9 的 GitHub CI:构建、三个单测分片、Bun 全量测试和 glibc/musl/darwin 二进制检查全部通过;network-boundary 通过。

实际回归命令:

bun run test \
  test/bot-config-store.test.ts test/bot-registry.test.ts \
  test/codex-app-runner.integration.test.ts test/codex-coco-detect-models.test.ts \
  test/codex-effort-wiring.test.ts test/codex-rpc-engine.test.ts test/codex-service-tier-catalog.test.ts \
  test/command-handler.test.ts test/dashboard-bot-defaults-cliid.test.ts \
  test/dashboard-bot-defaults-model-picker.test.ts test/dashboard-bot-payload.test.ts \
  test/dashboard-cli-options-models.test.ts test/dashboard-ipc.test.ts test/fork-session.test.ts \
  test/model-catalog-json.test.ts test/model-catalog.test.ts test/session-store.test.ts \
  test/codex-service-tier.test.ts test/deferred-suspend.test.ts \
  test/topic-directive-header.test.ts test/traex-detect-models.test.ts

兼容性

公共配置、session store、worker init 和 Dashboard 增加可选字段,无数据库迁移。parseDebugModelsJson 保持 slug-only API,/api/cli-options/models 保留旧三字段并可选追加 modelCapabilities;CoCo、TraeX、Claude 等回归已覆盖。变更不增加平台特定路径或进程启动方式,本地验证环境为 Linux。

未重启 live daemon,未做飞书真实会话 E2E 或 macOS 真机验证。wrapper 网关保守地只暴露 slug,未 pin 模型时加速档位仍需显式指定模型;已有 RPC thread 的真实 resume 行为继续留待真机确认。

UI

Bot Defaults 的速度档位下拉框

@deepcoldy

Copy link
Copy Markdown
Owner

你好,感谢这个 PR!以下是 botmux 自动评审的初步意见(最终以维护者审阅为准)。整体方向(创建即冻结、三链路 TUI/RPC/app-runner 透传、按真实 binary+环境+模型做能力证明且 fail-closed)我们认同,但在真实 codex 与边界场景上发现 1 个阻断问题和 2 个建议一并处理的问题。

🔴 P1(阻断):adopt / 外部 App Server 的「清除档位」被持久层无条件复活,外部 App Server 切换确定性拒启

session-store.ts 为保护冻结字段不被「拿旧整行对象的陈旧写入」抹掉,在 persistRow 里加了合并:

// src/services/session-store.ts:2150
if (session.serviceTier === undefined && durable.serviceTier !== undefined) {
  session = { ...session, serviceTier: durable.serviceTier };
}

它区分不了「陈旧写入(该保)」与「本次有意清除(该落盘)」。而两处切换都是先删再存:

// command-handler.ts:6196(切外部 App Server)、:6311(adopt)
delete current.session.serviceTier;
sessionStore.updateSession(current.session);

用真实 SQLite 对 default/priority/ultrafast 三种具体档位复现:delete → updateSession → 重新 init 后 getSession().serviceTier 全部仍是原具体值;updateSession 末尾的 Object.assign(session, durable) 还会把复活值写回活对象——那正是随后 forkWorker 读的对象。

外部 App Server 切换之后走 forkWorker,复活的档位经 worker-pool.ts:13226 进入 init,撞上 worker 守卫,任何「曾冻结过具体档位」的会话切外部 App Server 都会确定性拒启:

// src/worker.ts:15552
if (cfg.serviceTier !== undefined) throw new Error('existing App Server attachment cannot apply an explicit serviceTier');

(adopt 一侧因 forkAdoptWorker 的 init 不携带该字段,运行时侥幸不抛,但 durable 仍被写成具体档位的脏数据。)

建议修法:两处有意清除不用 delete(undefined),改为写入本 PR 已有的 'inherit' 哨兵——persistRow 只复活 undefined,'inherit' 能正常落盘;而 worker-pool(2383/2509 一带)已把 'inherit' 映射为 undefined,因此 init 不带档位、守卫通过;陈旧整行写入的既有保护与其测试都不受影响。另建议补一条行为测试:建具体档位会话 → 切换清除 → updateSession + 重新 init → 断言重载后运行时无档位。当前唯一相关护栏是 codex-effort-wiring 对源码字符串 delete …serviceTier 出现次数的断言(toHaveLength(2)),它测不到「是否真正落盘」,所以这个 bug 在现有全套测试下全绿。

🟠 P2-a:配了具体档位的 codex Bot,新话题启动前用 /cli 预选非 codex CLI 会被硬拒

buildNewSession(session-store.ts:2203)只按「Bot 是 codex」就在创建时冻结具体档位,与之后 /cli 预选了什么无关。首次 fork 时 sessionAgentConfig 两个分支(worker-pool.ts:2387、:2513)对非 codex CLI 直接 throw:

CLI selection rejected: claude-code cannot use serviceTier priority

但「新话题先 /cli <id> 选别的 CLI」是明确支持的跨 CLI 用法——同一 snapshot 分支还特意保证 Bot 的 model / reasoningEffort / modelBackendVariant 不泄漏给所选 CLI,唯独档位没做同构处理;且 /cli 已先回复 CLI selected,失败要拖到首次 fork 才以启动错误爆出。建议与 modelBackendVariant 同构:解析到非 codex 时置 'inherit' 并落盘,而非 throw。这两个新 throw 目前也没有行为测试覆盖。

🟠 P2-b:ultrafast 在当前真机 codex 的 service_tiers 词表中不存在,选项存不进去;且与推理档位标签冲突

在真机 codex-cli 0.160.0 上 codex debug models,7 个 visibility=list 模型(gpt-6.1-sol / gpt-6-astra / gpt-6-sol / gpt-6-luna / gpt-5.6-sol / gpt-5.6-terra / gpt-5.6-luna)的档位字段完全一致:

"service_tiers": [{ "id": "priority", "name": "Fast", "description": "2x speed, increased usage" }],
"additional_speed_tiers": ["fast"]

整份输出(约 600KB)里没有任何 ultrafast。因此:

  1. checkCodexServiceTierSupport(codex-service-tier-catalog.ts:74)用 serviceTiers.includes('ultrafast') 判定,目录里永远没有 → 对任何模型都返回 not_supported_by_model,Dashboard//config 保存 ultrafast 被 400 拒。UI/enum/i18n 提供了它,但在当前线上二进制上是结构性存不进去的值;测试里自造的 serviceTiers:['priority','ultrafast'] 假目录掩盖了这一点。
  2. 真实存在的第二档字段是 additional_speed_tiers: ["fast"],id 看起来是 fast,它不在 service_tiers 里、本 PR 也未解析——需要确认 Codex 真正的第二加速档走哪个字段、哪个 id。
  3. CLI 里 “Ultrafast” 实际是**推理档位(effort)**标签(Light/Minimal/Ultrafast 那一串),速度档位的展示名是 “Fast”;把速度档标成 “Ultrafast” 会语义错位。

PR 描述提到在 codex 0.162.1 上 gpt-6-astra 支持 priority + ultrafast,这与 0.160.0 的真机输出不一致(模型词表由运行时下发,我们无法复现你那个版本/账号的响应)。能否附一份 0.162.1 上 gpt-6-astra 的 debug models 原始 JSON 作为依据?在确认前建议:要么先移除 ultrafast(等真机二进制真正下发该 id 再加),要么让下拉选项也按当前 binary+所选模型的能力目录动态生成(只列实际可用档),并核实第二档是否应为 fast、展示名不要复用 effort 的 “Ultrafast”。

可选 follow-up(不阻断)

  • Bot 未 pin 模型时保存非 default 档位会因 model 为空直接 model_required——即便 codex 默认模型支持该档也开不了 Fast,建议解析默认模型或给出更明确提示。
  • 档位目录缓存对不同 env 的过期键不淘汰,且保存与启动并发时无在途去重(会各起一次 8s 的 debug models),可后续优化。
  • RPC thread/resume 顶层携带 serviceTier 目前只由 fake server 覆盖;对「已存在 thread」resume 是否接受变更档位,建议安排一次真机确认(与既有 model/effort 的 resume 漂移同源)。

验证:把本分支 rebase 到最新 master 后,bunx tsc --noEmit、bun run build 及 PR 相关全部单测通过;P1/P2 是另用真实 SQLite 与真机 codex 0.160.0 复现得到。以上为自动评审初步意见,具体取舍以维护者审阅为准,辛苦!

@stormluke

Copy link
Copy Markdown
Contributor Author

已在 e2b1b3f 处理完这三点:

  • P1:adopt / 外部 App Server 的有意清除改为落盘 serviceTier: 'inherit',不再被 stale whole-row 保护复活;补了具体档位清除后 updateSession + 重新 init 仍为 inherit 的真实 store 回归。
  • P2-a:/cli 预选阶段保留原档位,允许启动前在 Codex 和非 Codex 之间来回改选;首次真正 resolve 到非 Codex 时把会话档位写成 inherit,worker init 不带档位,同 CLI 选择仍保留原档位。
  • P2-b:codex debug models 解析并透出可选 modelCapabilities,Dashboard 按当前模型实际 service_tiers 生成 Fast/Ultrafast 选项。0.160 这类只有 priority 的目录不会显示 Ultrafast;0.162.1 下若模型目录声明 ultrafast 才显示。目录缺失、模型未知或旧后端时只显示继承/标准,未知 tier id 一律过滤。
  • 能力探测按当前 Bot 的已保存 runtime/path/env 进行,缓存 key 包含 runtime 与 env hash;env 同 key 换值、envPolicy 或 runtime 保存后会重新探测。wrapper 选择保守地不暴露未经真实 wrapper 链路证明的档位。

本地验证:评审相关 11 个测试文件 808 passed;bunx tsc --noEmit、bun run build、git diff --check 通过。远端 CI 已在 e2b1b3f 重新触发。

@deepcoldy

Copy link
Copy Markdown
Owner

更新一轮:新推的两个 commit(e2b1b3f + 31d6137)我们已在最新 master 上重新复审,上一轮的 1 个阻断 + 2 个建议项都已正确修复,没有新增问题,构建与相关测试通过,真实 SQLite 与真机 codex 0.160 的 debug models 也独立复验过:

  • P1:delete serviceTier 改为写 'inherit',adopt / 外部 App Server 清除后能正确落盘且不再拒启;陈旧整行写入的保护仍然有效(双向都验证了)。
  • P2-a:/cli 跨 CLI 预选改为静默置 inherit 并落盘,不再 throw,且补了跨 CLI / 同 CLI 两条行为测试。
  • P2-b:速度档下拉改为由真机能力目录(detectModelCapabilities → service_tiers)动态驱动,并按 bot 的 runtime/env 分缓存、在途去重;在 codex 0.160 上所有模型只会出现 priority,ultrafast 在该二进制上既不会渲染也无法保存,死值问题结构性消除。跨 CLI(coco/traex/claude/opencode)走旧的 slug 兜底,无回归;wrapper 选择只暴露 slug 也处理得很稳妥。

只剩一个可选的一致性小收尾(非阻断,不改也可以合),供你参考:

sessionAgentConfig 的非 snapshot 分支里还保留着全仓最后一处对档位的硬 throw:

// src/core/worker-pool.ts 非 snapshot 分支
if (serviceTier && !isCodexServiceTierCliId(ds.session.cliId ?? botCfg.cliId)) {
  throw new Error(`Session service tier ${serviceTier} is not supported by CLI ${...}`);
}

它只在一个很窄的窗口可达:会话以 codex + 具体档位创建后、首次 fork 之前,管理员在 Dashboard 把该 Bot 的 CLI 改成非 codex(只清 bot 配置、不动已建会话行)。首次 fork 走到这里时,上面的冻结块已经先把新 cliId 落盘,随后这个 throw 会让该会话此后每次 fork 都抛错,切回 codex 也因 cliId 已冻结而无效,只能 /new。同一路径上 reasoningEffort / modelBackendVariant 遇到不支持的 CLI 都是静默降级,snapshot 分支这次也已经改成静默置 inherit 了,只有这里还在 throw。

建议直接照搬同文件 snapshot 分支的写法(非 codex 且持有具体档位 → ds.session.serviceTier = 'inherit' 并 updateSession,去掉 throw),再补一条与新增 fork-session 用例对称的测试即可,改动约 4–6 行、风险很低。

是否处理这条都不影响合入评估;若你这次不方便,我们也可以按 follow-up 记录。以上仍为自动评审初步意见,最终以维护者审阅为准,感谢跟进!

@stormluke

stormluke commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

已在 7fce3ba 处理这条一致性问题,并检查了整个 PR 的改动:

  • 普通会话与 /cli snapshot 分支共用 sessionServiceTier:最终 CLI 为非 Codex 时落盘 inherit,worker init 不再携带档位,也不再因这条路径永久拒启。已冻结为 Codex 的会话仍保留原档位。
  • 统一在会话冻结和持久层回填完成后解析档位;回归覆盖三种具体档位、后续重新解析、冻结 Codex 保留,以及陈旧整行对象被 store 回填后的 Codex / 非 Codex 两种行为。
  • 两个 Codex 适配器复用现有能力目录加载器,移除重复的子进程探测、超时与解析逻辑;同时修复目录改造中 Aiden 成功结果绕过原有 TTL 缓存的问题,并补缓存过期回归。简化两处相同的错误码映射。本轮生产代码净减少 49 行。

最终验证:相关 21 个测试文件 1656 passed、1 skipped;bun run build(含主工程、脚本、测试 mock 类型检查及 Dashboard bundle)、git diff --check 通过。新 head 7fce3ba9 的 CI 38275787313 全部通过,包含三个单测分片、Bun 全量测试及 glibc/musl/darwin 二进制检查;network-boundary 同样通过。PR 描述同步为最终实现及实际回归命令。

本轮未重启 live daemon,未做飞书真实会话 E2E。

@deepcoldy
deepcoldy merged commit b8a2e64 into deepcoldy:master Oct 12, 2026
12 checks passed
@github-actions

Copy link
Copy Markdown

🚀 Released in v3.43.0

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

支持 Codex Bot 独立配置 service tier

2 participants