Token Pool 本地 auth 链悬挂与 Codex 请求假断流修复记录(2026-04-28)
结论
这次本地 Codex -> Token Pool 的“像断流一样直接停住”里,已经确认并修掉两条同一 auth 链上的确定性问题:
crs-key-auth在 key 不属于 CRS key store 时,会返回authorized: false。plugin-manager.executeAuth()又把所有authorized: false一律改写成handled: true。
组合结果是:
request-handler误以为“认证插件已经自己把响应写回去了”- 实际插件并没有
res.end() - 请求不会继续走
handleError(401) - 客户端只能一直等,直到自己超时或主动断开
这不是网络断流,而是 auth 链把请求挂住了。
在修掉 contract 后,又继续确认到第二个同链问题:
crs-key-auth会把所有 Bearer key 都当成 CRS key 来查库- 只要 key 不在
configs/crs-keys.json,它就会抢先拒绝 - 导致
default-auth根本没有机会验证configs/config.json中的REQUIRED_API_KEY
这会让“明明填的是本地配置里的 API key”仍然被误判为未授权。
现场症状
本地可稳定复现的异常现象是:
POST /openai/v1/responses带REQUIRED_API_KEY时,不立即返回 401,也不继续执行业务逻辑- 客户端一直等响应头,直到超时
- 日志里能看到:
[CRS Auth] Invalid client API key- 但没有紧接着的
[Request] END - 只有客户端超时后才出现
[Request] ABORT ... reason=request_aborted
单一路径追踪
1. 请求入口
src/handlers/request-handler.js
请求进入后先做:
pluginManager.executeAuth(req, res, requestUrl, currentConfig)
然后按返回值分支:
authResult.handled === true-> 直接returnauthResult.authorized === false->handleError(...401...)
2. CRS 认证插件
src/plugins/crs-key-auth/index.js
原逻辑里:
- 取到 Bearer key
- 在
configs/crs-keys.json查不到 - 直接返回
{ handled: false, authorized: false }
3. 插件管理器错误改写 contract
src/core/plugin-manager.js
原逻辑把:
- 插件返回
{ handled: false, authorized: false }
错误改写成:
{ handled: true, authorized: false }
4. request-handler 被误导
于是 request-handler 进入:
if (authResult.handled) return;
请求到此结束,但没有任何响应体被写回。
5. 客户端视角
客户端看到的不是明确 401,而是:
- 等不到首包
- 等不到结束
- 最后自己超时 / 中断
这就是“看起来像断流”的直接原因。
已修复内容
A. 修正 auth contract
文件:src/core/plugin-manager.js
修复后:
- 只有插件真的自己写回响应时,才返回
handled: true - 如果插件只是认证失败但没写回响应,则保留
handled: false, authorized: false - 让
request-handler统一走 401 收尾
B. 修正 CRS 插件的抢占式拒绝
文件:src/plugins/crs-key-auth/index.js
修复后:
- key 不在 CRS store 时,不再直接判死
- 改为返回
authorized: null - 让后续
default-auth继续验证REQUIRED_API_KEY
这让两套认证层可以按 README 描述的方式共存:
crs-key-auth处理公网/控制台分发 keydefault-auth处理本地配置的默认 API key
回归测试
新增 / 覆盖的回归测试在:
tests/plugin-manager.unit.test.js
已验证:
- 认证插件返回
authorized: false且未写响应时,handled不会被错误改成true - 认证插件真的已写响应时,
handled: true会被保留 - 前置 auth 插件返回
authorized: null时,后续 auth 插件仍可继续授权
执行结果:
node node_modules/jest/bin/jest.js tests/plugin-manager.unit.test.js --runInBand- 结果:
4 passed
本地 live 验证
部署动作
- 通过
POST http://127.0.0.1:3100/master/restart重启本地 worker - 当前重启后 worker pid:
42004
验证 1:不再悬挂
使用 REQUIRED_API_KEY 发送:
GET /openai/v1/modelsPOST /openai/v1/responses(故意发非法 JSON)
结果:
- 两个请求都在本地立即完成
- 不再出现“等不到响应头直到客户端超时”的旧现象
验证 2:日志证据
logs/app-2026-04-28.log 已出现新的明确链路:
[CRS Auth] Key not found in CRS store, deferring to next auth plugin- 紧跟
[Request] END ... status=500 ...
这说明请求已经不再卡在 auth 链,而是正常进入后续逻辑并被统一收尾。
验证 3:当前剩余错误已不属于 auth-hang
当前 GET /openai/v1/models 返回的 500 是:
OpenAI API Key is required for OpenAIResponsesApiService.
当前 POST /openai/v1/responses 故意发非法 JSON 返回的 500 是:
Invalid JSON in request body.
这两个错误都说明:
- 认证已经通过或正常落到后续阶段
- 剩下的是 provider/runtime 配置问题,不再是“请求悬挂”问题
本次涉及文件
代码修复:
E:\My Project\Token Pool\src\core\plugin-manager.jsE:\My Project\Token Pool\src\plugins\crs-key-auth\index.jsE:\My Project\Token Pool\tests\plugin-manager.unit.test.js
运行证据:
E:\My Project\Token Pool\logs\app-2026-04-28.log
当前状态
- deploy decision:
deployed(已重启本地 live runtime) - live verification:
passed(auth 链不再悬挂,请求可明确结束) - root-cause status:
confirmed and fixed - remaining follow-up: 本地
openaiResponses-custom的上游 OpenAI key/provider 配置仍需单独检查,但已不属于这次 auth-hang 根因
第二阶段追加记录:Responses 流错误被错误伪装成 200 SSE(2026-04-28 16:44 +08:00)
追加结论
在 auth-hang 修掉之后,又确认出另一条独立但同样会被用户体感成“断流/突然停住”的确定性问题:
- 对
POST /openai/v1/responses这类 stream 请求 - 如果上游在 首个 chunk 之前 就失败
- 本地
handleStreamRequest()会先写死:200Content-Type: text/event-stream
- 然后最终错误只能退化成:
text/event-stream头- 正文却是裸 JSON error
这会让下游客户端误判:
- 它收到的不是一个正常 HTTP error
- 也不是一个合法完成的 SSE/Responses 流
- 于是会表现成“像断流一样不对劲”,甚至进入自己的重试/超时逻辑
这不是 provider 自身协议问题,而是 Token Pool 本地 stream 头发送时机错误。
单一路径追踪
1. 旧行为
文件:src/utils/common.js
handleStreamRequest() 在真正拿到上游首个 chunk 之前,就先执行:
handleUnifiedResponse(res, '', true)
而 handleUnifiedResponse() 会立刻:
res.writeHead(200, { "Content-Type": "text/event-stream", ... })
2. 上游失败条件
这条链在本地 live 环境下稳定触发:
gpt-5.3-codex- 路由归一到
openai-codex-oauth - 上游 codex credential 返回
429 usage_limit_reached - 且
runtime-overrides.json已去掉openai-codex-oauth -> openaiResponses-customfallback
于是当前请求会在 没有任何首个流 chunk 的前提下直接失败。
3. 错协议收尾
因为响应头已经提前写成了 200 text/event-stream:
- 后续
handleError()已经不能再改成真实500 application/json - 客户端看到的就是“stream 头 + 非 stream body”
直接抓包的旧现场是:
STATUS 200Content-Type: text/event-stream- body:
{"error":{"type":"server_error","message":"[API Service] No healthy provider found in pool for openai-codex-oauth supporting model: gpt-5.3-codex", ...}}这就是导致 Codex/TUI 侧出现异常等待、重试或“像 turn 被截断”的直接协议层原因。
本次修复
文件:src/utils/common.js
修复动作是把 stream 头发送从“请求一进入 stream handler 就发送”改成:
- 只有 真正要写第一个 event/data chunk 时才发送
- 或真正正常结束、需要补 completion marker 时才发送
也就是:
- 不再在 pre-first-chunk 阶段提前
writeHead(200, text/event-stream) - 给 pre-first-chunk 失败保留正常 HTTP error 出口
回归测试
新增覆盖:
tests/stream-midflight-stall.unit.test.js
新增场景验证:
responses流在首个 chunk 之前失败时,不会提前写 SSE 头- 之后由
handleError()可以正确返回:429application/json- 标准 error payload
执行结果:
node node_modules/jest/bin/jest.js tests/stream-midflight-stall.unit.test.js --runInBand --silent- 结果:
11 passed
live 验证
验证 1:直接探针
请求:
POST http://127.0.0.1:3301/openai/v1/responses- body:
model: gpt-5.3-codexstream: true
修复后实测结果:
STATUS 500Content-Type: application/json- body:
{"error":{"type":"server_error","message":"[API Service] No healthy provider found in pool for openai-codex-oauth supporting model: gpt-5.3-codex", ...}}这说明本地 gateway 已经不再伪装成一个错误的 SSE 流。
验证 2:debug store
GET /openai/debug/requests?limit=5 现在记录为:
statusCode: 500finalStatus: failed
不再是之前那种:
statusCode: 200- 但 body 实际是 error JSON / stream contract 错配
验证 3:日志
logs/app-2026-04-28.log 中现在可以直接看到:
status=500No healthy provider found in pool for openai-codex-oauth supporting model: gpt-5.3-codex
说明错误已经被收敛成正常失败,而不是假流式完成。
当前剩余问题(已和本次根因分离)
修掉这条本地 stream contract bug 之后,剩余问题已经清楚分层:
openai-codex-oauth当前本地 credential 仍会命中429 usage_limit_reached- 因为
runtime-overrides.json已去掉openai-codex-oauth -> openaiResponses-customfallback,所以请求现在会 fail-fast 为正常500 codex exec仍然可能持续较久才退出,这一段从 debug/log 看已经不是 Token Pool 挂流,而是 Codex CLI 收到连续失败后的客户端侧重试/退避行为
换句话说:
- “本地 gateway 把错误伪装成断流”的 bug:
fixed - “本地 codex oauth credential 没额度”:
unfixed - “Codex CLI 在失败后会继续自己重试一段时间”:
client-side behavior observed
第二阶段涉及文件
E:\My Project\Token Pool\src\utils\common.jsE:\My Project\Token Pool\tests\stream-midflight-stall.unit.test.jsE:\My Project\Token Pool\logs\app-2026-04-28.log
第二阶段状态
- deploy decision:
deployed(已重启本地 live worker) - live verification:
passed(pre-first-chunk 失败已返回真实500 application/json) - root-cause status:
confirmed and fixed - remaining follow-up:
- 修通
openai-codex-oauth可用上游 credential,或恢复一个已验证健康的 fallback - 若仍要继续追
<turn_aborted>,下一步应转到 Codex CLI/TUI 的失败后重试与 interrupt 链,不再是这条 gateway stream bug
- 修通
第三阶段追加记录:Codex fallback 已接通,本地 codex exec 已真实返回 pong(2026-04-28 17:07 +08:00)
追加结论
第二阶段之后,剩余主链问题已经不是 stream contract,而是 本地 openai-codex-oauth 虽然仍会 429,但没有一个真实可用的本地 fallback credential 接上来。
本次继续确认出的确定性根因有两条:
configs/provider_pools.json中openaiResponses-custom绑定的是错误的上游 key,不是当前 Codex CLI 真正在用的 live public key。configs/config.json和configs/runtime-overrides.json里都去掉了openai-codex-oauth -> openaiResponses-custom的 fallback 链,所以即便openaiResponses-custom本身可用,Codex 模型请求也接不过去。
修正这两点后:
openai-codex-oauth命中 429 时,会正常切到openaiResponses-custom- 本地
codex exec已经通过http://127.0.0.1:3301/openai/v1真实返回pong
单一路径追踪
1. live 上游并不是整体不可用
用当前 Codex CLI 正在使用的 live auth key 直接请求:
POST https://key.tengokukk.com/openai/v1/responsesmodel = gpt-5.3-codex
可以稳定拿到 200 completed。
这说明:
- 上游
key.tengokukk.com/openai/v1/responses对gpt-5.3-codex是健康的 - 问题不在公网 gateway 整体
- 问题在本地
openaiResponses-custom绑定错 key
2. 本地错误 key 导致 fallback 名义存在也不可用
configs/provider_pools.json 中原先的 openaiResponses-custom.OPENAI_API_KEY 不是 C:\Users\ASUS-KL\.codex\auth.json 中的 live key。
所以即使请求理论上应该 fallback 到 openaiResponses-custom:
- 实际也会打到错误凭证
- 仍然无法形成可用接力
3. Codex 模型请求仍然先锚定到 openai-codex-oauth
src/services/service-manager.js 中的路由逻辑会把:
gpt-5.3-codex
先归一锚定到:
openai-codex-oauth
所以这条主路径真正要修通的不是“彻底绕开 codex family”,而是:
- 允许
openai-codex-oauth在 429/不可用时 - 通过协议兼容链切到
openaiResponses-custom
4. 协议兼容本来就是允许的
src/providers/provider-pool-manager.js 中 areProviderProtocolsCompatible() 已明确允许:
codexopenaiResponses
之间互为兼容协议。
因此这次不需要加新代码分支,只需要恢复被去掉的 fallback 配置,并把 openaiResponses-custom 绑到正确 key。
本次修复
A. 恢复本地 codex fallback 链
更新:
E:\My Project\Token Pool\configs\config.jsonE:\My Project\Token Pool\configs\runtime-overrides.json
补回:
openai-codex-oauth -> openaiResponses-custom
B. 把 openaiResponses-custom 绑定到当前 live Codex key
更新:
E:\My Project\Token Pool\configs\provider_pools.json
做法:
- 从
C:\Users\ASUS-KL\.codex\auth.json读取当前 liveOPENAI_API_KEY - 回填到本地
openaiResponses-custom.OPENAI_API_KEY
没有把 secret 写进文档或命令输出。
live 验证
验证 1:本地 /openai/v1/responses 已恢复成功
请求:
POST http://127.0.0.1:3301/openai/v1/responsesmodel = gpt-5.3-codex
结果:
statusCode = 200
/openai/debug/requests 与 logs/app-2026-04-28.log 显示:
- 先尝试
openai-codex-oauth - 命中
429 - 然后
Fallback activated (Chain): openai-codex-oauth -> openaiResponses-custom - 最终
status=200
这说明 fallback 主链已经真实接通。
验证 2:真实 codex exec 已返回 pong
执行:
codex exec --ephemeral --skip-git-repo-check --json -c 'model_provider="crs"' -c 'model_providers.crs.base_url="http://127.0.0.1:3301/openai/v1"' -C 'E:\My Project\Token Pool' '你好,只回复pong'
实测结果:
- 退出码:
0 - 事件流最后一条 agent message:
pong
这说明用户要的主路径:
Codex CLI -> 本地 Token Pool -> fallback -> 上游 responses
已经恢复可用。
继续排查:为什么 gateway 已经正常 500,Codex CLI 还是要拖很久才退
复现方式
为了构造一个确定性的快速失败路径,执行:
codex exec ... -c 'model="gpt-4o-mini"' ...
当前本地 openaiResponses-custom 不支持 gpt-4o-mini,所以 gateway 会立即返回正常 500。
现场证据
codex exec总耗时:35.97s
codex execJSONL 输出:- 连续出现
Reconnecting... 1/5 - 一直到
Reconnecting... 5/5 - 最终
turn.failed
- 连续出现
- 本地 gateway 日志:
- 从
2026-04-28T09:06:30.815Z到2026-04-28T09:06:55.389Z - 连续收到
30次POST /openai/v1/responses - 每次都在约几毫秒内结束为正常
500
- 从
工程级结论
这说明“gateway 已经正常失败,但 Codex 还拖很久才退出”的根因不在 gateway 卡住,而在 Codex CLI 自己的 reconnect / retry backoff 机制:
- 本地 gateway 没有悬挂
- 本地 gateway 没有假流
- 本地 gateway 每次都快速明确返回
500 - 但 Codex CLI 仍会把这类失败解释成“可重连/可重试”的暂时性错误,并继续进行 5 轮 reconnect
也就是说,这段长尾时间是:
client-side reconnect budget
不是:
gateway-side hang
第三阶段涉及文件
E:\My Project\Token Pool\configs\config.jsonE:\My Project\Token Pool\configs\runtime-overrides.jsonE:\My Project\Token Pool\configs\provider_pools.jsonE:\My Project\Token Pool\logs\app-2026-04-28.log
第三阶段状态
- deploy decision:
deployed(已重启本地 live worker) - live verification:
passed(本地codex exec已真实返回pong) - root-cause status:
openaiResponses-custom错绑上游 key:confirmed and fixedopenai-codex-oauth -> openaiResponses-customfallback 缺失:confirmed and fixed- Codex CLI 慢退出:
confirmed as client-side reconnect behavior
- remaining follow-up:
- 若要继续缩短失败退出时间,下一步应转向 Codex CLI / provider adapter 如何映射
500为“可重连错误”的判定逻辑,而不是继续改 Token Pool 的流收尾
- 若要继续缩短失败退出时间,下一步应转向 Codex CLI / provider adapter 如何映射
第四阶段追加记录:Codex CLI 为什么会把本地快速 500 判成 Reconnecting... 1/5(2026-04-28 17:25 +08:00)
追加结论
这一步已经确认:
- 本地 Token Pool 对失败请求并没有挂住
- 也没有继续伪装 stream 完成
- 它是在几毫秒内稳定返回正常
500
但 Codex CLI 仍然会把这类失败当作 turn-level reconnect / retry,于是用户看到:
Reconnecting... 1/5- 一直到
Reconnecting... 5/5
这条慢退出链的主因已经可以定性为:
- Codex CLI 内部把
/responses的非完成错误当成可重试的 turn stream failure
而不是:
- gateway hang
- gateway 假断流
- 本地 wrapper 注入
<turn_aborted>
本地确定性证据
1. 失败复现
执行:
codex exec ... -c 'model="gpt-4o-mini"' ...
当前本地 openaiResponses-custom 不支持 gpt-4o-mini,所以这是一个稳定的“立刻失败”探针。
2. CLI 表面行为
本地 JSONL 输出稳定出现:
Reconnecting... 1/5Reconnecting... 2/5Reconnecting... 3/5Reconnecting... 4/5Reconnecting... 5/5- 最终
turn.failed
3. Gateway 实际行为
同一时间窗内,本地 GET /openai/debug/requests?limit=80 显示:
- 共记录到
30次POST /openai/v1/responses - 时间窗约
25s - 每次都是正常
500 - 没有任何一条请求卡住等待完成
也就是说:
- 从 gateway 视角看,这是快速失败风暴
- 从 CLI 视角看,它把这风暴包装成 reconnect 链
来自 OpenAI 官方 openai/codex 的补充证据
官方 issue 中已经能看到同类模式:
-
VScode插件Codex在wsl中Ubuntu系统超时: Reconnecting...
issue #8814
日志显示:codex_core::codex: stream disconnected - retrying turn (1/5 in 207ms)...- 错误源是
codex_api::endpoint::responses
-
stream error: unexpected status 404 Not Found
issue #2851
即使是404,CLI 也会显示:retrying 1/5- 一直到
retrying 5/5
这两条证据说明:
- CLI 的 retry 不是只针对“纯网络断连”
- 连
unexpected status 404这类明确 HTTP 错误,也会先进 turn retry 流程
因此本地看到:
- gateway 已快速
500 - CLI 仍然
Reconnecting... 1/5
在行为上和官方已知模式是一致的。
工程级判断
从当前证据看,Codex CLI 的内部判定更接近:
- “本轮
/responses没有正常 completed”
而不是:
- “HTTP 已明确失败,所以立即终止且不重试”
换句话说,它更像是:
- turn stream failure classifier
而不是简单:
- HTTP status classifier
所以只要一轮 /responses 没能以它期望的完成路径结束,就可能走:
- retry/backoff
- reconnect UI
- 最终
turn.failed
对本地 Token Pool 的含义
这意味着:
-
你前面修的 gateway stream contract 是必要的
否则 CLI 会在错误输入上更加混乱。 -
但即使 gateway 现在已经“正常失败”,CLI 仍然可能继续重试
这是 client-side policy,不是 gateway-side hang。 -
如果以后要继续缩短失败退出时间,主战场不再是:
src/utils/common.js- gateway stream 收尾
而是:
- Codex CLI / upstream adapter 对
/responses错误的重试判定
第四阶段状态
- deploy decision:
not-needed(本阶段无新的 runtime patch) - live verification:
passed(CLI reconnect 行为已稳定复现并与 gateway 快速 500 对齐) - root-cause status:
- CLI 慢退出的 gateway-side hang 假设:
ruled out - CLI 将失败 turn 归类为 reconnect/retry:
confirmed with local evidence + official issue evidence
- CLI 慢退出的 gateway-side hang 假设:
- remaining follow-up:
- 若要进一步压缩失败退出时间,应继续追 Codex CLI 对
/responses失败的 retry classifier,而不是继续在 Token Pool 内追加 fallback/timeout 修补
- 若要进一步压缩失败退出时间,应继续追 Codex CLI 对
第五阶段追加记录:Codex CLI 对 /responses 失败形态的真实分类矩阵
本阶段目标
把“gateway 已经快速正常失败,为什么 Codex CLI 还会拖很久才退并显示 Reconnecting... 1/5”继续收敛到具体响应形态级别,而不是停留在“可能是客户端策略”这一级。
1. 先钉死本地 gateway 的真实失败响应
对当前稳定失败探针:
POST http://127.0.0.1:3301/openai/v1/responsesmodel = gpt-4o-miniAuthorization = Bearer sk-tengokukk-crs-badd296ef15f233a79ba0049
直接抓到的真实响应是:
HTTP/1.1 500 Internal Server ErrorContent-Type: application/json- body:
{"error":{"type":"server_error","message":"[API Service] No healthy provider found in pool for openaiResponses-custom supporting model: gpt-4o-mini","code":"server_error"}}这一步排除了一个常见误判:
- 这里不是 gateway 卡住不回
- 也不是 gateway 偷偷保持长连接
- 而是同步、明确、快速地返回了一个普通 JSON 500
2. 用可控 mock 逐个比较 Codex CLI 的反应
为了把 CLI 分类逻辑继续收窄,额外做了一个本地最小 mock /openai/v1 服务:
- 文件:
C:\Users\ASUS-KL\AppData\Local\Temp\codex-mock-responses.js - 通过
codex exec --json ... -c 'model_providers.crs.base_url="http://127.0.0.1:3399/openai/v1"'直接打这个 mock
重点不是 mock 自己长什么样,而是:同一条 Codex CLI 执行链,只改响应形态,其它条件不变。
3. 实验矩阵
A. 纯 HTTP JSON 错误
-
400 + application/json + invalid_request_error- 结果:不重连
- CLI 输出:直接
turn.failed - 耗时:约
11.47s
-
429 + application/json + rate_limit_error- 结果:不显示 Reconnecting
- CLI 输出:
exceeded retry limit, last status: 429 Too Many Requests - 耗时:约
11.49s
-
401 + application/json + authentication_error- 结果:重连 5 次
- CLI 输出:
unexpected status 401 Unauthorized: ... - 耗时:约
18.02s
-
404 + application/json + invalid_request_error- 结果:重连 5 次
- CLI 输出:
unexpected status 404 Not Found: ... - 耗时:约
17.94s
-
500 + application/json + server_error- 结果:重连 5 次
- CLI 输出:
We're currently experiencing high demand, which may cause temporary errors. - 耗时:约
37.26s
B. 200,但不是 response.completed 终止
-
200 + 普通 JSON completed body(非 SSE)- 结果:重连 5 次
- CLI 输出:
stream disconnected before completion: stream closed before response.completed
-
200 + SSE error event- 结果:重连 5 次
- CLI 输出:同样是
stream closed before response.completed
-
200 + SSE response.failed- 结果:重连 5 次
- CLI 输出:
stream disconnected before completion: mock sse_failed_event_200
这三条一起说明:
- 对 Codex CLI 来说,
/responses这条流式链如果没有落到它认可的response.completed终止路径,就会被归类成“未完成断流” - 即使 HTTP 是
200 - 即使已经显式发了
error或response.failed - 也还是会触发 turn retry / reconnect
C. 200 + response.completed
-
200 + SSE response.completed + response.status = completed- 结果:不重连
- CLI 输出:
turn.completed - exit code:
0
-
200 + SSE response.completed + response.status = failed + error- 结果:也不重连
- CLI 输出:仍然是
turn.completed - exit code:
0
这一条是本阶段最关键的新证据:
- Codex CLI 把
response.completed当成比response.status/response.error更高优先级的 turn 终止信号 - 也就是说,只要走到了
response.completed,它就直接把这轮当成“已完成” response.status = failed并不会让codex exec变成失败退出
4. 由此得到的确定性结论
现在可以把这条链完整收敛成下面这个判断:
-
当前本地 gateway 对 unsupported model 的失败响应是:
- 同步 JSON 500
- 不是 hang
- 不是假断流
-
Codex CLI 对
/responses的失败不是单纯按“HTTP 明确失败 -> 立刻退出”处理。 -
它内部至少存在两层更强的分类:
- 协议终止层:有没有它认可的
response.completed - 状态码策略层:某些 HTTP 错误被当成 turn retry / reconnect
- 协议终止层:有没有它认可的
-
从本地实测矩阵看:
400/429会较快失败,不走Reconnecting...401/404/500会进Reconnecting... 1/5200但没有response.completed也会进Reconnecting... 1/5
-
因此,当前这类“gateway 已经快速 500,但 CLI 仍然拖很久才退”的现象,根因已经不在 Token Pool:
- 它是 Codex CLI 对
/responses失败 turn 的内建 retry / reconnect classifier - 不再是 gateway stream contract、auth hang、provider fallback、或本地 wrapper 注入造成的
- 它是 Codex CLI 对
5. 对 gateway 还能不能继续“修掉它”的结论
这里也顺便得到一个非常重要的边界结论:
如果强行想让 Codex CLI 不重连,只剩两种方向:
- 返回
400/429这类它会快速失败的状态码 - 返回
200 + response.completed
但第 2 条会把本来应该失败的 turn 伪装成成功完成,因为 codex exec 会直接 turn.completed 并以 exit code 0 退出。
所以:
- 不能通过“更漂亮的 SSE 错误事件”同时获得“正确失败语义”与“零 reconnect”
- 对
401/404/500这种失败形态,CLI 的慢退出属于客户端策略,不是本地 gateway 还能继续靠协议补丁彻底消掉的问题
6. 本阶段结论
- deploy decision:
not-needed - live verification:
passed - root-cause status:
- gateway 快速失败但 CLI 仍慢退出:
confirmed client-side - 触发条件已缩小到具体响应形态:
confirmed
- gateway 快速失败但 CLI 仍慢退出:
- remaining follow-up:
- 若以后还要进一步缩短这类失败的退出时间,主战场只能是 Codex CLI 自身的
/responsesretry classifier,或者上游/路由层尽量避免把请求导向会产生401/404/500的失败路径
- 若以后还要进一步缩短这类失败的退出时间,主战场只能是 Codex CLI 自身的