Token Pool / Codex OAuth 断流、假健康与首包超时修复记录(2026-04-28)
结论
这次问题不是单一故障,而是三条错误链叠加:
openai-codex-oauth的真实上游错误是429 usage_limit_reached,但网关没有稳定把它转成“带恢复时间的 unhealthy”。- fallback 到
openaiResponses-custom时,重试路径复用了第一次发给 Codex 的已转换请求体,跨 provider 后 body 形状错了,制造出假 401 / 假协议错误。 - 历史上的很多
504/524并不是 Codex 上游真的返回 504,而是网关自己的“首包等待 30000ms”先把流切断了,而且之前没有正确读取当前 provider 的超时覆盖配置。
修复后,gpt-5.3-codex 与 gpt-5.4 都已经在生产 runtime 上完成 live 验证,返回 200。
现场症状
用户侧主要现象:
- Codex 经常“看起来 provider 还健康”,但请求一跑就断流。
- 一旦走
openai-codex-oauth,会出现自动断开、重连不上、或 fallback 后继续失败。 - debug/provider 视图里一些节点仍显示
isHealthy: true,与真实可用性不一致。 - 日志里可见历史
504/524,但并不稳定指向同一个上游。
已确认的根因
1. Codex OAuth 真实失败不是 token 过期,而是 quota 用尽
生产日志在 2026-04-28 14:16:04 CST 明确出现:
- provider:
openai-codex-oauth - model:
gpt-5.3-codex - upstream error:
429 usage_limit_reached - reset field:
resets_at
这说明主因不是 OAuth token 过期,而是当前 Codex 账号额度/计划限制触发了上游拒绝。
2. 429 的结构化错误体有时是 stringified JSON,导致恢复时间丢失
原逻辑只稳定处理对象形态的 error.response.data。当上游把错误体塞成字符串时:
usage_limit_reached识别不稳resets_at无法提取- 节点不会进入“带恢复时间的冷却态”
- 结果就是“看起来健康,实际下一跳仍然死”
3. 401/403 以前只累计 errorCount,不会立即摘除节点
这会导致:
- 坏 key 仍停留在轮转池里
- debug 视图仍可能显示
isHealthy: true - fallback 重试会继续选到同一类已确定坏掉的节点
这次明确确认的坏节点:
d0398338-6eb2-4c0d-b0d3-26e7060c68ec- customName:
soxio-responses-v1-b - 远端直测结果:
401 Invalid API key / API key is disabled
4. fallback 重试错误复用了第一次 provider 的 request body
原主链里,第一次请求如果已经从 OpenAI Responses 形态转换到了 Codex 形态:
- 后续 retry/fallback 仍沿用这份已经转换过的 body
- 当路由切到
openaiResponses-custom时,没有按新route.toProvider重建 body - 于是会把不适合 Responses 上游的 payload 继续发出去
这个 bug 会制造出大量“看上去像 key 坏了/协议坏了”的假象。
5. 历史 504/524 的具体超时点在网关首包门,不是 Codex 自身统一返回 504
生产日志明确有多次:
Upstream stream did not produce the first chunk within 30000ms.- 随后 routing runner 把节点按
524处理
关键点在于:
- 某些 provider 已经配置了更大的
STREAM_FIRST_CHUNK_TIMEOUT_MS - 但网关以前读的是全局
CONFIG - 没正确吃到当前选中 provider 的
service.config
结果就是“provider 明明配了 90s,网关还是 30s 先切流”。
代码修复摘要
A. src/core/routing-runner.js
修复点:
- 解析 stringified JSON 错误体
- 识别
usage_limit_reached - 提取
resets_at/resetsAt/ rate-limit header 401/403改为立即markProviderUnhealthyImmediately(...)429 usage_limit_reached改为markProviderUnhealthyWithRecoveryTime(...)
效果:
- quota 型失败不再伪装成普通 transient error
- hard auth failure 不再继续留在 healthy 池里
B. src/providers/provider-pool-manager.js
修复点:
- 健康检查失败结果补
failureKind与failureStatusCode - 健康检查遇到
AUTH_ERROR时立即摘除 - Codex health check 默认模型固定为
gpt-5.3-codex - 对 Codex 健康检查使用 first-chunk stream probe,而不是弱 list-model 探测
codex与openaiResponses之间视为协议可兼容 fallback
效果:
- 健康状态与真实可用性更接近
- Codex fallback 不再因为协议前缀误判被挡住
C. src/utils/common.js
修复点:
- 新增
buildProcessedRequestBodyForRoute(...) - 每次 retry/fallback 都按当前
route.toProvider重新:- 转换 request body
- 附加内部 metadata
- 应用 system prompt
- 应用 custom model 参数
- stream 首包等待改为读取
service.config || CONFIG
效果:
- 不再复用“上一跳 provider 的旧 payload”
- provider 自己的
STREAM_FIRST_CHUNK_TIMEOUT_MS终于真正生效
D. src/providers/openai/openai-responses-core.js
修复点:
- 补 401/403 上游 detail 日志
- 遇到协议/鉴权失败时,不再只打印“可能 key 失效”这种弱日志
效果:
- 后续排障时能更快区分:坏 key、协议不匹配、并发上限、上游规则拒绝
生产 runtime 配置修复
1. fallback chain
生产 runtime 的 configs/runtime-overrides.json 需要包含:
openai-codex-oauth -> openaiResponses-custom
若这一条覆盖缺失,即使 routing 代码已支持,也会再次出现:
- Codex 被 quota 冷却后
- fallback 不生效
- 直接报
No healthy provider found in pool for openai-codex-oauth
2. 清理坏节点
在生产 provider_pools.json 中已处理:
- UUID:
d0398338-6eb2-4c0d-b0d3-26e7060c68ec - customName:
soxio-responses-v1-b - 动作:
isDisabled = true,同时置为 unhealthy
原因:
- 已通过远端直测确认其上游返回
401 - 返回内容明确为
API key is disabled - 不应继续进入轮转池制造噪音
3. 给慢首包节点补显式超时
已给下面这个节点补上:
- UUID:
be1d6074-3a71-4702-ab53-d07d52512559 - customName:
soxio-responses-v1 - 配置:
STREAM_FIRST_CHUNK_TIMEOUT_MS = 90000
目的:
- 即便上游首个 token/事件比较慢,也不应再被 30s 默认门误杀
验证过程与证据
本地测试
已通过的针对性测试包括:
tests/routing-runner.unit.test.jstests/provider-pool-manager-health.unit.test.jstests/stream-midflight-stall.unit.test.js- 以及先前已通过的 fallback / service-manager / overlay 测试集
关键新增覆盖:
- stringified quota payload 也能提取 recovery time
- 401 auth failure 立即摘除
- startup health check 能直接打掉 auth-failing provider
- stream first chunk timeout 会优先读取所选 service 的 provider-level override
生产部署时间
- 服务:
token-pool-gateway-prod.service - 一次关键重启:
2026-04-28 14:22:41 CST - 后续 provider pool 调整重启:
2026-04-28 14:26:47 CST
live 验证结果
A. Codex 路由验证
生产 runtime 直测:
POST http://127.0.0.1:3301/openai-codex-oauth/v1/responsesmodel = gpt-5.3-codex- 结果:
200
说明:
- Codex 主链已经不再卡死在“429 后假健康 / 假 dead-end”状态
B. Public/main route 验证
生产 runtime 直测:
POST http://127.0.0.1:3301/v1/responsesmodel = gpt-5.3-codex->200model = gpt-5.4->200
说明:
- Codex 路由、Responses fallback、主入口三者已重新接通
C. 慢首包场景验证
针对 openaiResponses-custom 直连做了 6 次连续请求压测:
- 路径:
POST http://127.0.0.1:3301/openaiResponses-custom/v1/responses - 模型:
gpt-5.4 - 结果:6/6 成功,全部
200 - 单次耗时约:
1.9s ~ 2.7s
同时检查 journal:
- 6 次请求全部命中
be1d6074-3a71-4702-ab53-d07d52512559 - 没再出现
d0398338-6eb2-4c0d-b0d3-26e7060c68ec - 没再出现
Upstream stream did not produce the first chunk within 30000ms. - 没再出现
524/504首包门误杀
这次修复最值得记住的经验
-
429不能只看 status code,要看语义字段usage_limit_reached和普通 burst rate limit 不是一回事- 前者应该进入 cooldown,最好带 recovery time
-
“debug 里 healthy” 不等于真实健康
- 如果 401/403 还靠累计 errorCount 才摘除,debug 会长期撒谎
-
retry/fallback 绝不能复用上一个 provider 的已转换 request body
- 这类 bug 会伪造出大量跨协议异常
- 表面像 key 坏了,本质是 request shape 错了
-
网关自己的 timeout 门比上游 timeout 更危险
- 如果 provider 已经显式配置了更大的首包等待时间
- 网关必须读取当前 provider 的 runtime config,而不是只看全局默认值
-
远端 live 验证不能只看一条
200- 至少要同时验证:
- direct provider route
- main
/v1/responsesroute - debug/provider snapshot
- journal 中真实选中的 provider 与 error chain
- 至少要同时验证:
后续运维建议
-
对所有
openaiResponses-custom节点补 machine-readable 能力标签- 明确哪个节点支持
gpt-5.3-codex - 明确哪个节点需要大于 30s 的首包窗口
- 明确哪个节点支持
-
对 hard auth failure 建立自动熔断/自动 disable 策略
- 例如连续一次明确
API key is disabled即直接isDisabled = true - 避免重复进入轮转池
- 例如连续一次明确
-
对 quota recovery 建立 debug 可见字段
- 例如在 provider debug 视图中直接显示
scheduledRecoveryTime - 让“冷却中”与“健康”一眼可分
- 例如在 provider debug 视图中直接显示
-
保留这次排障顺序作为固定 SOP
- 先确认真实上游状态码与 body
- 再看 routing health semantics
- 再看 retry/fallback 是否复用了错误请求体
- 最后看网关本地 timeout / proxy / first chunk gate
本次涉及的关键节点 / 路径
- 生产 host:
170.106.179.226 - 生产 runtime:
/srv/token-pool-gateway-prod - 服务:
token-pool-gateway-prod.service - 坏节点(已禁用):
d0398338-6eb2-4c0d-b0d3-26e7060c68ec - 慢首包节点(已补超时):
be1d6074-3a71-4702-ab53-d07d52512559 - Codex 节点:
c52e1160-02ba-48ee-9ee8-978ffc062164
当前状态
- deploy decision:
deployed - live verification:
passed - 当前结论:这轮 Codex OAuth 断流 / 假健康 / fallback 假异常 / 首包 504 误杀链路已完成治本修复。