Skip to content

feat: codex无重启,无另外调整,纯模型栏切换官模与第三方api#1641

Open
121103qwq wants to merge 3 commits into
BigPizzaV3:mainfrom
121103qwq:codex/model-provider-routing
Open

feat: codex无重启,无另外调整,纯模型栏切换官模与第三方api#1641
121103qwq wants to merge 3 commits into
BigPizzaV3:mainfrom
121103qwq:codex/model-provider-routing

Conversation

@121103qwq

@121103qwq 121103qwq commented Jul 24, 2026

Copy link
Copy Markdown

新增“按模型自动路由供应商”功能,使 ChatGPT 官方模型与第三方 API 模型可以同时出现在 Codex 模型栏中,并仅通过切换模型自动选择对应供应商。

  • 模型存在于某个第三方供应商的 modelList:走该供应商
  • 模型未匹配任何第三方供应商:走 ChatGPT 官方登录
  • 模型同时存在于多个供应商:取供应商列表中靠前的那个,被遮蔽的供应商写入诊断日志(中转 API 转售同名模型是常态,不按配置错误处理;调整供应商顺序即可改变胜出者)
  • 第三方请求使用对应供应商的 API Key
  • 官方请求保留 Codex 提供的官方认证
  • 各供应商自己的「上游协议」开关照常生效,Responses 与 Chat Completions 供应商可在模型栏中混用
  • 无需 CC Switch,也无需手动切换当前供应商

新增“按模型自动路由供应商”独立开关,默认关闭

  • Codex 固定连接本地 127.0.0.1:57321/v1
  • 合并官方模型与第三方模型目录(/models 与 Codex 自身的模型选择器返回同一套集合)
  • 保留各模型单独配置的上下文窗口,例如 mimo-v2.5-pro[1M]
  • 复用现有 Responses / Chat Completions 协议转换
  • 开关开启时切换或编辑供应商不会把 Codex 踢下 router

复审后补充(ade7d2a2fa95e8

  • 路由模型目录合并官方模型,修正 /models 只返回第三方模型、与生成的 router catalog 不一致的问题
  • 重复模型名由报错改为「列表顺序第一个胜出」+ 诊断日志;relay_profile_for_model 因此不再可能失败,返回值由 Result 收敛为 Option
  • relay_switch 在开关开启时改走 router profile,并跳过会把 router 实时配置回填进上一个 profile 的逻辑
  • 新增 crates/codex-plus-core/tests/model_routing.rs(8 个端到端测试)及 /models、供应商切换的接口级测试
  • 新增 CODEX_PLUS_OFFICIAL_BASE_URL,用于把官方分支指向自建网关或测试上游(未设置即真实 ChatGPT 后端)

@BigPizzaV3

Copy link
Copy Markdown
Owner

审查结论:当前不建议合并,主要有以下问题需要先确认:

  1. 官方模型可能不会出现在模型列表中
    model_catalog.rsmodel_routing_catalog_value() 只收集第三方 profile 的 model_list,普通官方 profile 会被排除。但 read_codex_model_catalog() 在开启 model routing 后会直接使用这个结果,而 launcher 的模型目录接口也调用了它。这样可能出现实际 router catalog 尝试合并官方模型,但 /models 返回的模型列表只包含第三方模型,和 PR 描述的“官方模型与第三方模型同时显示”不一致。

建议让 routing catalog 同步合并 models_cache.json 或生成的 router catalog 中的官方模型,并补一个接口级测试,确认 /models 同时返回官方和第三方模型。

  1. 缺少核心路由的端到端测试
    目前测试覆盖了 profile 匹配、router 配置生成和 catalog 写入,但没有验证:
  • 匹配第三方模型时使用正确的 API Key;
  • 未匹配模型时正确转发官方 Authorization;
  • /responses/responses/compact 官方路径;
  • 重复模型的错误响应;
  • 实际 /models 返回的完整模型集合。
  1. CI 状态
    当前 PR 没有 GitHub CI,状态为 UNSTABLE。建议补齐 Windows、macOS x64、macOS arm64 构建检查。

我本地验证了 relay_config 100 个测试、launcher 71 个测试、protocol_proxy 46 个测试、TypeScript 检查和 release build,基础构建均通过;但上述路由行为仍缺少真实覆盖。

@BigPizzaV3

Copy link
Copy Markdown
Owner

Codex++ maintainer review note

这个方向有价值,但当前 PR 改动覆盖模型目录、relay config、protocol proxy、settings 和前端开关,而且没有 CI/check 结果;暂时不建议直接合并。

请补充以下内容后再复审:

  1. 针对官方模型、单一第三方模型、多个供应商重复模型名的端到端/集成测试;
  2. 明确该功能开启/关闭时不会破坏现有 per-profile provider 行为;
  3. 说明与现有 CC Switch / provider sync / model_catalog_json 逻辑的边界;
  4. 触发一轮 PR build artifacts 或贴出本地验证命令与结果。

The feature is useful, but it touches model catalog, relay config, protocol proxy, settings, and UI without CI results. Please add coverage and clarify compatibility boundaries before merge.

121103qwq and others added 2 commits July 26, 2026 23:37
Addresses the review on BigPizzaV3#1641.

- model_catalog: the routing catalog only listed third-party models, so
  /models hid the official ones even though Codex's generated
  model_catalog_json had both. Merge the official slugs back in from
  model-catalogs/codex-plus-router.json (falling back to
  models_cache.json before the first launch writes it) and expose them as
  their own source entry.
- relay_switch: switching or editing a provider while routing is on wrote
  that profile over the router provider, and backfilled the router's live
  config.toml onto the profile it replaced. Apply the router profile
  instead and skip the backfill.
- protocol_proxy: make the official base URL overridable via
  CODEX_PLUS_OFFICIAL_BASE_URL and expose the settings+headers entry point
  so the routing decision can be driven end to end from tests.

New coverage: tests/model_routing.rs drives /v1/responses against
loopback upstreams for matched third-party models (correct API key),
suffixed model ids, unmatched models (official Authorization forwarded,
x-api-key withheld), /responses and /responses/compact, duplicate model
bindings, missing/placeholder ChatGPT login, and routing turned off.
tests/model_catalog.rs asserts the /models payload with routing on and
off; tests/relay_switch.rs asserts the switch flow in both states.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Relay APIs commonly resell the same model, so several providers declaring
e.g. claude-sonnet-4-6 is a normal setup rather than a misconfiguration.
relay_profile_for_model used to bail out and fail the whole request; it
now takes the first match in relay_profiles order — the same one the model
catalog already collapses the duplicate slug onto — and records the
shadowed providers as model_routing.duplicate_model_binding in the
diagnostic log so an unexpected choice stays traceable. Reordering the
provider list is how the user picks the winner.

The function can no longer fail, so it returns Option instead of Result.

Also adds an end-to-end test that the per-provider upstream protocol
toggle still applies under routing: a Chat Completions provider gets
/v1/chat/completions with a converted `messages` body and its own key,
while other providers stay on Responses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@121103qwq

Copy link
Copy Markdown
Author

已按复审意见补齐,新提交 ade7d2a2fa95e8

1. 官方模型没有出现在模型列表(已确认并修复)

问题成立。model_routing_catalog_value() 只收集第三方 profile 的 model_list,而 launcher 写给 Codex 的 model_catalog_jsonmodel-catalogs/codex-plus-router.json)里是「官方 + 第三方」的合集 —— 两边不一致,/models 这一侧只返回第三方模型。

修复方式:路由目录现在把官方模型合并回来。读 model-catalogs/codex-plus-router.json(首次启动尚未生成时回落到 models_cache.json),与第三方 profile 重名的 slug 归第三方所有、不重复出现,官方模型另外作为一个 source 暴露(type: "official_model_catalog")。这样 /models 与 Codex 自己的模型选择器看到的是同一套集合。

2. 重复模型名:改为「列表顺序第一个胜出」,不再报错

原来的实现在同一模型属于多个供应商时直接 bail!,整个请求失败。但中转 API 转售同一模型(多个站都挂 claude-sonnet-4-6)是常态而不是配置错误,硬失败等于把这类用户挡在门外。

现在 relay_profile_for_modelrelay_profiles 顺序里的第一个匹配 —— 与模型目录对重复 slug 去重后保留的那一个一致 —— 并把被遮蔽的供应商写进诊断日志(model_routing.duplicate_model_binding,含 model / selectedId / shadowed),选错时可追。用户通过调整供应商列表顺序来决定谁胜出。该函数因此不再可能失败,返回值从 Result 收敛为 Option

(更彻底的做法是模型别名 —— model_list 支持 别名=真实模型名,catalog 的 slug 用别名、发上游前改写回真名,这样多个站的同名模型能在模型栏里并列共存。catalog 格式里 slugdisplay_name 本来就是独立字段,实现是可行的。但那是一个语法扩展,本 PR 已经偏大,打算单开一个 PR 做。)

3. 端到端 / 接口级测试

crates/codex-plus-core/tests/model_routing.rs(新文件,8 个测试,直接调用真实 /v1/responses 入口,上游是回环 mock,断言真正发出去的请求):

场景 断言
命中第三方模型 请求落到该 profile 的 base_url,authorization: Bearer sk-beta,而不是调用方传入的官方 token
模型以 [1M] 后缀声明 仍然路由到该供应商
供应商设为 Chat Completions /v1/chat/completions,body 转成 messageswire_api == ChatCompletions,用该供应商自己的 Key;其他供应商仍走 Responses
未命中模型 打官方端点;转发 authorization / chatgpt-account-id / originator转发 x-api-key
/v1/responses/compact 命中 .../responses/compact,不被降级为 /responses
同一模型属于两个供应商 请求成功,落到列表里靠前的那个供应商,用它的 Key
官方登录缺失 / 占位符 分别报「请先在 Codex 中完成 ChatGPT 登录」「重新加载官方登录认证」
开关关闭 请求仍走当前 active profile,模型不参与选路

tests/model_catalog.rs:接口级断言 read_codex_model_catalog()/models 的数据源)——开关打开时同时返回官方与第三方模型且 sources 结构正确;关闭时保持原有 per-profile 行为,且残留的 router catalog 不会泄漏进去。

tests/relay_switch.rs:开关打开时切换/编辑供应商不会把 Codex 踢下 router;关闭后恢复写入所选 profile 的 provider。

src/settings.rs 单测另外覆盖了「普通官方 profile 不捕获模型」和「重复时胜出者跟随列表顺序」。

4. 顺带修掉的一个真问题:开关开启时切换供应商会破坏路由

relay_switch 之前完全不知道 model routing:

  • apply_selected_relay_profile 会把所选 profile 的 provider 写进 config.toml,覆盖掉 codex-plus-router,路由要等到下次启动才恢复;
  • backfill_profile_before_switch 会把 router 的实时 config.toml 回填进「上一个」profile,污染那个 profile 存的 provider 配置。

现在开关开启时改为走 model_router_profile() 重新落盘,并跳过回填;两条路径都有对应测试。

5. 与现有逻辑的边界

  • model_catalog_json:只有 profile.id == "codex-plus-router" 才走合并逻辑(build_model_router_catalog_json),普通 profile 仍是原来的 build_model_catalog_json。用户手写的 model_catalog_json 指针依然优先保留(preserves_user_model_catalog_json 未改动且通过)。
  • 上游协议开关:逐模型路由完全尊重每个 profile 自己的 protocol,Responses 和 Chat Completions 供应商可以在模型栏里混用,各走各的协议(见上表第 3 行)。官方模型固定走 Responses 到 ChatGPT 后端,不受该开关影响。
  • CC Switch 导入relay_profile_from_ccs 产出的 profile model_list 为空,所以刚导入的 CC Switch 供应商不会捕获任何模型,请求落到官方路由,直到用户给它填模型列表。本 PR 未改动导入路径。
  • provider sync:目标供应商取自 config.tomlmodel_provider,开启路由后即 codex-plus-router,因此会话历史会被标成 router 而不是实际上游。这是开启路由后的已知边界(本 PR 未改动 provider sync),需要归到具体供应商时可传显式 target。
  • 聚合供应商RelayMode::Aggregate 的 profile 被排除在选路之外(避免经同一本地端口递归),聚合模式与逐模型路由互斥。
  • 开关默认关闭,关闭时所有路径都回到原实现。

另外新增了 CODEX_PLUS_OFFICIAL_BASE_URL 覆盖官方 base URL(未设置即真实后端),路由测试靠它把官方分支指向回环上游。

6. 本地验证

cargo test --workspace                                    # 846 passed, 0 failed
cargo build --release                                     # Finished in 2m06s
npm run check                                             # tsc --noEmit 通过
node --experimental-strip-types --test "src/*.test.ts"    # 32 passed, 0 failed

分测试二进制:model_routing 8、model_catalog 9、relay_switch 8、relay_config 102、launcher 73、protocol_proxy 46、core lib 单测 199。

(本机 Node 是 v22.11.0,还没默认开启 TS 类型擦除,直接跑 npm testERR_UNKNOWN_FILE_EXTENSION;CI 的 Node 22 最新版没有这个问题,所以上面用 --experimental-strip-types 跑了同一批用例。)

7. CI

已在 fork 上跑完一轮 pr-build,三个平台全绿(2fa95e8):
https://github.com/121103qwq/CodexPlusPlus/actions/runs/30210705694

Job 结果
Windows artifacts(含 npm test / npm run check / cargo test --workspace / release build / NSIS 安装包) ✅ success
macOS DMG (x64) ✅ success
macOS DMG (arm64) ✅ success

产物:codex-plus-plus-windows-installer 20.1 MB、codex-plus-plus-windows-binaries 25.3 MB、codex-plus-plus-macos-x64-dmg 32.5 MB、codex-plus-plus-macos-arm64-dmg 31.6 MB。

(本 PR 面向上游的 checks 需要维护者点一次 “Approve and run”,所以上面这轮是在 fork 上用 workflow_dispatch 跑的同一个 pr-build.yml。)

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.

2 participants