背景
我在 Windows + Codex App/CLI + Cockpit Tools 切换 OAuth / API Key / API relay 的场景里复现到两个关联问题:
- 切到 API relay 后 Codex App 可能重新尝试
responses_websocket,表现为持续 Reconnecting。
- 切到 API 后 Codex App 和 Codex CLI 的 picker 会话历史为空,即使本地
sessions / state_5.sqlite 里实际还有历史。
本次基于最新 main 源码审查,HEAD 为:70942f79df8c847d4ffb5f76ad400adba870c472。
现象与根因判断
1. API Key/custom relay provider 写入的连接语义仍不适合 relay
源码位置:src-tauri/src/modules/codex_account.rs
write_api_provider_to_config_toml() 的 custom provider 分支会写:
model_provider = <provider_id>
[model_providers.<provider_id>]
wire_api = "responses"
requires_openai_auth = true
- 但没有写
supports_websockets = false。
write_api_key_provider_to_config_toml() 还会把 API Key provider 固定折叠为 codex_local_access,并同样写 requires_openai_auth = true,也没有 supports_websockets = false。
对 OpenAI 官方端点这可能还能工作,但对常见 API relay / compatible endpoint 来说,实际需要的是稳定的 custom provider bucket,并显式禁用 WebSocket。否则 Codex/App 会把该 provider 当成可走 responses_websocket 的 OpenAI 风格 provider,导致 relay 不支持 WebSocket 时反复重连。
本地实测可工作的投影形态是:
model_provider = "<stable_custom_provider_id>"
[model_providers.<stable_custom_provider_id>]
name = "..."
base_url = "https://relay.example.com/v1"
wire_api = "responses"
requires_openai_auth = false
supports_websockets = false
也就是说,不应把 relay 强行塞回 built-in openai bucket,也不应在 API Key relay 场景统一折叠到一个不表达真实 provider 身份的 runtime bucket。
2. session visibility repair 只修 provider bucket,漏掉 picker 可见性字段
源码位置:src-tauri/src/modules/codex_session_visibility.rs
当前 repair 的 SQLite 逻辑只统计/更新:
SELECT COUNT(*) FROM threads WHERE COALESCE(model_provider, '') <> ?1
UPDATE threads SET model_provider = ?1 WHERE COALESCE(model_provider, '') <> ?1
这能修复 threads.model_provider 不匹配的问题,但不能覆盖另一个实际会让 picker 空白的状态:历史 thread 已经在正确 provider bucket 下,但 has_user_event = 0,同时 first_user_message 非空。此时:
- repair 会认为
model_provider 已匹配,所以 rows_to_update = 0;
- 但 App/CLI picker 仍可能按
has_user_event / user-message 元数据过滤,结果历史为空。
所以现在的 repair 会出现“报告已修/无需修,但 picker 仍空”的假阴性。
3. 自动/手动 repair 的边界与文案存在漂移风险
src/pages/CodexAccountsPage.tsx 里 openApiSwitchVisibilityNotice() 会调用 runApiSwitchVisibilityRepair(),而 changelog 同时出现过“自动修复”和“切号后端不再自动执行历史会话可见性修复 / explicit repair action”的描述。无论最终交互是自动还是显式按钮,关键问题是当前 backend repair 只覆盖 provider,没覆盖 picker visibility metadata,因此 UI 层很容易给用户一个“repair 已运行”的错觉。
建议修法
建议把 Codex 切换后的 on-disk projection 当成一个原子结果维护:
-
API Key / API relay 模式
- 写
auth.json 的 API Key 语义。
- 写
config.toml:
model_provider = <account.api_provider_id 或稳定 custom provider id>。
[model_providers.<id>] base_url / wire_api = "responses"。
requires_openai_auth = false。
- relay/compatible endpoint 默认或可配置写
supports_websockets = false。
- 不要把 relay 塞进 built-in
openai bucket;否则 shared openai bucket 下无法禁用 WebSocket。
- 对多 provider/API 账号,避免统一折叠到
codex_local_access 导致历史 bucket 与真实连接 provider 不一致。
-
OAuth / ChatGPT 模式
- 切回 built-in
openai bucket。
- 清理 API relay 的 runtime provider 投影,避免残留 custom provider 影响 OAuth。
- 只保留 OAuth 所需的
auth.json token 投影。
-
session visibility repair 增强
- 在修
threads.model_provider 的同时,schema-aware 地检测 threads 表是否存在 has_user_event、first_user_message、thread_source 等列。
- 当
first_user_message 非空但 has_user_event != 1 时,也应计入 repair。
- 可考虑:
UPDATE threads
SET
model_provider = ?1,
has_user_event = CASE
WHEN COALESCE(first_user_message, '') <> '' THEN 1
ELSE has_user_event
END,
thread_source = CASE
WHEN COALESCE(thread_source, '') = '' AND COALESCE(first_user_message, '') <> '' THEN 'user'
ELSE thread_source
END
WHERE
COALESCE(model_provider, '') <> ?1
OR (COALESCE(first_user_message, '') <> '' AND COALESCE(has_user_event, 0) <> 1)
实际实现需要先 PRAGMA table_info(threads),兼容旧 schema,缺列时降级为只修 provider。
-
增加诊断指标,避免 repair 假阴性
active_provider_thread_count
user_message_thread_count
visible_user_event_thread_count
thread_source 分布
- 当
user_message_thread_count > 0 且 visible_user_event_thread_count == 0 时,应提示“历史存在但 picker 元数据不可见”,而不是显示“无需修复”。
-
增加回归测试
- API Key custom provider 写入应保留 stable provider id,并写入
requires_openai_auth = false / supports_websockets = false。
- OAuth 切回 openai 时应清理 API relay provider 投影。
state_5.sqlite 中 provider 已匹配但 has_user_event = 0 的 thread,repair 后应变为 picker 可见。
- repair summary 应能区分 provider rows updated 与 visibility metadata rows updated。
期望结果
- API relay 模式下 Codex App 不再走不支持的
responses_websocket,避免 Reconnecting。
- API/OAuth 来回切换后,App 和 CLI picker 看到的 provider bucket 与真实连接 provider 一致。
- 历史会话不仅在文件/SQLite 中存在,而且在 picker 中可见。
- repair 不再出现“执行成功但历史仍为空”的假阴性。
背景
我在 Windows + Codex App/CLI + Cockpit Tools 切换 OAuth / API Key / API relay 的场景里复现到两个关联问题:
responses_websocket,表现为持续Reconnecting。sessions/state_5.sqlite里实际还有历史。本次基于最新
main源码审查,HEAD 为:70942f79df8c847d4ffb5f76ad400adba870c472。现象与根因判断
1. API Key/custom relay provider 写入的连接语义仍不适合 relay
源码位置:
src-tauri/src/modules/codex_account.rswrite_api_provider_to_config_toml()的 custom provider 分支会写:model_provider = <provider_id>[model_providers.<provider_id>]wire_api = "responses"requires_openai_auth = truesupports_websockets = false。write_api_key_provider_to_config_toml()还会把 API Key provider 固定折叠为codex_local_access,并同样写requires_openai_auth = true,也没有supports_websockets = false。对 OpenAI 官方端点这可能还能工作,但对常见 API relay / compatible endpoint 来说,实际需要的是稳定的 custom provider bucket,并显式禁用 WebSocket。否则 Codex/App 会把该 provider 当成可走
responses_websocket的 OpenAI 风格 provider,导致 relay 不支持 WebSocket 时反复重连。本地实测可工作的投影形态是:
也就是说,不应把 relay 强行塞回 built-in
openaibucket,也不应在 API Key relay 场景统一折叠到一个不表达真实 provider 身份的 runtime bucket。2. session visibility repair 只修 provider bucket,漏掉 picker 可见性字段
源码位置:
src-tauri/src/modules/codex_session_visibility.rs当前 repair 的 SQLite 逻辑只统计/更新:
这能修复
threads.model_provider不匹配的问题,但不能覆盖另一个实际会让 picker 空白的状态:历史 thread 已经在正确 provider bucket 下,但has_user_event = 0,同时first_user_message非空。此时:model_provider已匹配,所以rows_to_update = 0;has_user_event/ user-message 元数据过滤,结果历史为空。所以现在的 repair 会出现“报告已修/无需修,但 picker 仍空”的假阴性。
3. 自动/手动 repair 的边界与文案存在漂移风险
src/pages/CodexAccountsPage.tsx里openApiSwitchVisibilityNotice()会调用runApiSwitchVisibilityRepair(),而 changelog 同时出现过“自动修复”和“切号后端不再自动执行历史会话可见性修复 / explicit repair action”的描述。无论最终交互是自动还是显式按钮,关键问题是当前 backend repair 只覆盖 provider,没覆盖 picker visibility metadata,因此 UI 层很容易给用户一个“repair 已运行”的错觉。建议修法
建议把 Codex 切换后的 on-disk projection 当成一个原子结果维护:
API Key / API relay 模式
auth.json的 API Key 语义。config.toml:model_provider = <account.api_provider_id 或稳定 custom provider id>。[model_providers.<id>] base_url / wire_api = "responses"。requires_openai_auth = false。supports_websockets = false。openaibucket;否则 shared openai bucket 下无法禁用 WebSocket。codex_local_access导致历史 bucket 与真实连接 provider 不一致。OAuth / ChatGPT 模式
openaibucket。auth.jsontoken 投影。session visibility repair 增强
threads.model_provider的同时,schema-aware 地检测threads表是否存在has_user_event、first_user_message、thread_source等列。first_user_message非空但has_user_event != 1时,也应计入 repair。实际实现需要先
PRAGMA table_info(threads),兼容旧 schema,缺列时降级为只修 provider。增加诊断指标,避免 repair 假阴性
active_provider_thread_countuser_message_thread_countvisible_user_event_thread_countthread_source分布user_message_thread_count > 0且visible_user_event_thread_count == 0时,应提示“历史存在但 picker 元数据不可见”,而不是显示“无需修复”。增加回归测试
requires_openai_auth = false/supports_websockets = false。state_5.sqlite中 provider 已匹配但has_user_event = 0的 thread,repair 后应变为 picker 可见。期望结果
responses_websocket,避免Reconnecting。