这次折腾 Codex Desktop,起因很简单:我想让它走第三方 OpenAI 兼容接口,比如 Fucheers / NewAPI,同时保留桌面端左侧那些旧聊天和 Projects。
听起来只是改个 API 地址,但实际一路踩了不少坑。最开始我以为是认证问题,后来怀疑是数据库问题,最后才发现,真正影响左侧聊天目录的,很可能是 Codex 内部的 provider 命名空间。
这篇文章记录整个排查过程,也整理一下最后能用的配置,以及后续怎么继续验证这些猜想。
1. 问题是怎么出现的
我最开始把 config.toml 改成了自定义 provider:
model_provider = "custom"
[model_providers.custom]
name = "openai"
base_url = "https://www.fucheers.top/v1"
wire_api = "responses"
这个配置的确能让 Codex 使用第三方接口。但很快出现了一个严重副作用:
左侧历史聊天几乎全没了,只剩几条新聊天。
更奇怪的是,项目有时还在,有时也会不见。于是第一反应很自然:是不是聊天记录没了?是不是数据库坏了?是不是账号换了?
后来反复验证后发现,都不是。
2. 聊天其实没有丢
整个过程中,我没有删除数据库,也没有清空任何聊天记录。主要改动只有两个文件:
C:\Users\XKH\.codex\config.toml
C:\Users\XKH\.codex\auth.json
但只要切换配置,左侧聊天列表就会变化。
这说明聊天记录大概率还在本地,只是 Codex Desktop 当前没有把它们展示出来。
这个判断很关键。因为如果一开始误以为“数据丢了”,就很容易去做迁移数据库、清缓存、删配置之类的高风险操作。实际这次更像是:数据还在,但被当前配置过滤掉了。
3. Projects 和 Chats 不是一回事
中途还有一个现象容易误导人:Projects 和 Chats 的表现不完全一致。
有一次配置里缺了 [projects] 部分,结果 Projects 消失了。恢复 [projects] 后,Projects 回来了,但旧聊天仍然没回来。
这说明 Codex Desktop 里至少有两套索引逻辑:
Projects 一套
Chats 一套
Projects 是否显示,和 config.toml 里的 [projects] 强相关。
但聊天是否显示,还受别的字段影响。
4. 真正关键的是 model_provider
最后反复对比后,最关键的规律出现了:
model_provider = "openai"
旧聊天回来。
model_provider = "custom"
旧聊天消失,只显示 custom 期间创建的少量新聊天。
这基本说明,左侧聊天列表很可能和当前 model_provider 绑定。
也就是说,Codex Desktop 不是简单地把本地所有聊天都列出来,而是会按当前 provider 或运行命名空间筛选。
旧聊天大概率属于:
provider = openai
而我切到:
provider = custom
之后,Desktop 就只展示 custom 这个命名空间下的聊天。
5. “是不是被当成两个用户了?”
这也是我后来想到的一个猜想。
因为现象看起来确实很像:切到 custom 后,旧聊天不见了;用 custom 创建的新聊天,又像是另一套聊天历史。于是很容易怀疑:
是不是 Codex 把它们当成了两个不同用户的数据,所以不互通?
现在看,这个猜想有一定道理,但表述上可以更精确一点。
它不像是两个 Windows 用户,也不像是两个真正的账号。更可能是:
同一个本地用户
同一个 ~/.codex
同一套本地数据库
但分成了不同 provider / runtime namespace
也就是说,openai 和 custom 不是两个用户,但在聊天列表筛选上,它们像两套不同的会话空间。
可以把它理解成:
provider = openai -> 显示旧聊天
provider = custom -> 显示 custom 期间创建的新聊天
所以“被当成两个用户”这个直觉是有价值的,只是底层原因更可能不是 user id,而是 provider id、runtime id、host id、account id 等字段组合出来的会话作用域。
目前最强的证据仍然是:
只改 model_provider,聊天列表就变化。
而只改 auth.json,更多影响的是认证和 401,不是聊天是否显示。
6. name = "openai" 没有用
我还试过一种看起来很合理的写法:
model_provider = "custom"
[model_providers.custom]
name = "openai"
base_url = "https://www.fucheers.top/v1"
wire_api = "responses"
直觉上会想:我把名字写成 openai,是不是 Codex 就会按 openai 处理?
结果并不会。
Codex 看的是 section 名,也就是这里的:
[model_providers.custom]
而不是:
name = "openai"
更进一步,如果尝试直接写:
[model_providers.openai]
Codex 会报错,因为 openai 是内置保留 provider,不能覆盖。
错误大意是:
model_providers contains reserved built-in provider IDs: openai
Built-in providers cannot be overridden.
这一步基本确认了:想保留旧聊天,就不能把 provider 切到 custom。
7. 正确方向:保留 openai,只改 base URL
后来查到官方手册里的推荐写法:
openai_base_url = "https://us.api.openai.com/v1"
也就是说,如果只是想让内置 OpenAI provider 指向代理、路由器或第三方兼容接口,不应该重新定义 provider,而应该这样写:
model_provider = "openai"
openai_base_url = "https://www.fucheers.top/v1"
这个配置的意义很重要:
provider 仍然是 openai
所以旧聊天还能显示
实际 API base URL 改成第三方接口
所以请求可以走 Fucheers / NewAPI
这一步解决了最核心的“左侧旧聊天消失”问题。
8. 中途踩过的几个坑
第一个坑是 requires_openai_auth = true。
这个字段看起来像是“我要用 OpenAI 认证”,但对第三方兼容接口来说,反而可能导致 401。因为它可能触发 Codex 使用官方 OpenAI 的认证方式,而第三方接口不一定接受。
所以最后配置里不保留它。
第二个坑是 disable_response_storage = true。
这个字段不适合当前目标。我的目标是保留 Desktop 历史和完整本地体验,不应该随便关闭 response storage。
第三个坑是 TOML 语法。
有一次 config.toml 某个 table header 写坏了,Codex 直接提示:
unclosed table, expected `]`
然后当前对话也无法继续,界面一直处在加载状态。
所以每次改完配置,都应该先用 TOML parser 验证:
@'
from pathlib import Path
import tomllib
p = Path(r'C:\Users\XKH\.codex\config.toml')
data = tomllib.loads(p.read_text(encoding='utf-8-sig'))
print("TOML_OK")
print("model_provider =", data.get("model_provider"))
print("openai_base_url =", data.get("openai_base_url"))
print("project_count =", len(data.get("projects", {})))
print("features =", data.get("features"))
'@ | python -
9. 最后一个问题:WebSocket 404
当旧聊天终于恢复后,又出现了另一个问题:
unexpected status 404 Not Found: Invalid URL (GET /v1/responses),
url: wss://www.fucheers.top/v1/responses
这个错误说明 Codex 在某些场景下试图走 Responses WebSocket:
wss://www.fucheers.top/v1/responses
但 Fucheers / NewAPI 这类兼容接口通常支持的是 HTTP/SSE Responses,不一定支持 Codex 所需的 WebSocket Responses endpoint。
这就导致了 404。
后来在本地 Codex 二进制里能看到两个 feature flag:
responses_websockets
responses_websockets_v2
于是最后加了一个很小、可逆的配置:
[features]
responses_websockets = false
responses_websockets_v2 = false
这样做的目的不是换 provider,而是在保留 built-in openai provider 的前提下,尽量让 Codex 不再走 WebSocket,而是回落到 HTTP/SSE。
10. 最终可用配置
最终核心配置大概是这样:
model_provider = "openai"
openai_base_url = "https://www.fucheers.top/v1"
model = "gpt-5.5"
model_reasoning_effort = "high"
persistence = "save-all"
personality = "pragmatic"
service_tier = "default"
[features]
responses_websockets = false
responses_websockets_v2 = false
验证结果应该类似:
TOML_OK
model_provider = openai
openai_base_url = https://www.fucheers.top/v1
features = {
responses_websockets = false,
responses_websockets_v2 = false
}
project_count = 8
改完后一定要完全退出并重启 Codex Desktop。只刷新窗口不一定够,因为 app-server 可能仍在使用旧配置。
11. 后续怎么验证这个猜想
目前最核心的猜想是:
Codex Desktop 的聊天列表不是简单展示所有本地聊天,
而是按 provider / runtime / account / host 等命名空间过滤。
其中 model_provider 至少是一个非常重要的筛选因素。
要进一步验证,可以做几组更有控制的实验。
实验一:在 custom 下创建一条带标记的新聊天
先切到:
model_provider = "custom"
然后新建一条聊天,标题或第一句话写得非常明显,例如:
CUSTOM_PROVIDER_TEST_2026_06_28
确认它在左侧列表里可见。
然后切回:
model_provider = "openai"
完全重启 Codex Desktop,再搜索或观察左侧列表。
如果这条 CUSTOM_PROVIDER_TEST_2026_06_28 消失,而旧 openai 聊天回来,就说明 custom 聊天确实被放进了另一套 namespace。
实验二:再切回 custom,看那条测试聊天是否回来
接着再次切回:
model_provider = "custom"
完全重启 Codex Desktop。
如果 CUSTOM_PROVIDER_TEST_2026_06_28 又出现,而旧 openai 聊天又消失,就能进一步证明:
聊天没有丢,只是当前 provider 决定了显示哪一组。
这会非常支持“provider namespace”猜想。
实验三:只改 auth.json,不改 model_provider
保持:
model_provider = "openai"
然后只替换或调整 auth.json,观察旧聊天是否还在。
如果旧聊天仍然显示,只是请求报 401 或认证失败,就说明:
auth.json 主要影响认证,不是聊天列表分组的核心条件。
这和之前的观察一致。
实验四:只改 name 字段,不改 section 名
测试:
model_provider = "custom"
[model_providers.custom]
name = "openai"
base_url = "https://www.fucheers.top/v1"
wire_api = "responses"
如果旧聊天仍然不回来,就说明 Codex 判断 provider 的依据不是 name,而是 section id,也就是:
custom
这也解释了为什么 name = "openai" 没有效果。
实验五:比较日志里的 provider 和 transport
可以查看 Codex 日志,重点找这些字段:
model_provider
provider
wire_api
transport
responses_http
responses_websocket
api.path="responses"
如果在 openai_base_url 方案下看到:
transport="responses_http"
POST https://www.fucheers.top/v1/responses
就说明请求已经通过 HTTP/SSE 走第三方接口。
如果看到:
wss://www.fucheers.top/v1/responses
说明仍然触发了 WebSocket,需要检查:
[features]
responses_websockets = false
responses_websockets_v2 = false
是否生效,并确认是否完全重启了 Codex Desktop。
实验六:检查本地数据库字段,但不要直接改
如果想进一步确认底层字段,可以只读方式查看本地 SQLite 表结构。
重点找类似字段:
provider
provider_id
model_provider
runtime
runtime_id
account_id
host_id
thread_id
conversation_id
注意,这一步只建议只读检查,不建议直接修改数据库。因为当前已经有不改数据库的解决方案,直接改 DB 风险太高。
如果数据库里确实能看到聊天和 provider/runtime/account 之类字段关联,就能进一步证明左侧列表为什么会分组显示。
12. 最后的理解
这次问题最后可以总结成两句话。
第一句:
Codex Desktop 的左侧聊天列表,很可能按 provider/runtime namespace 过滤。
所以切到 custom 后,旧的 openai 聊天不会显示。不是聊天丢了,也不是数据库坏了。
第二句:
第三方 OpenAI 兼容接口应该优先用 openai_base_url 接入,而不是自定义 openai provider。
这样才能保留 openai 这个 provider 命名空间,从而保留旧聊天。
如果第三方接口不支持 Codex 的 Responses WebSocket,再补上:
[features]
responses_websockets = false
responses_websockets_v2 = false
13. 给后来人的排查顺序
如果以后再遇到类似问题,我会按这个顺序查:
- 先备份
C:\Users\XKH\.codex\config.toml - 确认没有改数据库、没有删聊天
- 用 TOML parser 验证
config.toml - 确保
model_provider = "openai" - 用
openai_base_url指向第三方接口 - 不要写
[model_providers.openai] - 不要随便加
requires_openai_auth = true - 如果出现
wss://.../responses404,再关闭 Responses WebSocket - 完全重启 Codex Desktop
- 观察旧聊天、Projects、发送消息是否都正常
这次最大的教训是:表面上像是聊天丢了,实际上更像是 Codex 换了一个会话视角。把视角切回 openai,旧聊天就还在那里。