AGENT

token-pool-codex-auth-stream-repair-2026-04-28

2026/04/28 19 min read AGENT AUTH TOKEN POOL CODEX AUTH STREAM REPAIR

Token Pool / Codex OAuth 断流、假健康与首包超时修复记录(2026-04-28)

结论

这次问题不是单一故障,而是三条错误链叠加:

  1. openai-codex-oauth 的真实上游错误是 429 usage_limit_reached,但网关没有稳定把它转成“带恢复时间的 unhealthy”。
  2. fallback 到 openaiResponses-custom 时,重试路径复用了第一次发给 Codex 的已转换请求体,跨 provider 后 body 形状错了,制造出假 401 / 假协议错误。
  3. 历史上的很多 504/524 并不是 Codex 上游真的返回 504,而是网关自己的“首包等待 30000ms”先把流切断了,而且之前没有正确读取当前 provider 的超时覆盖配置。

修复后,gpt-5.3-codexgpt-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

修复点:

  • 健康检查失败结果补 failureKindfailureStatusCode
  • 健康检查遇到 AUTH_ERROR 时立即摘除
  • Codex health check 默认模型固定为 gpt-5.3-codex
  • 对 Codex 健康检查使用 first-chunk stream probe,而不是弱 list-model 探测
  • codexopenaiResponses 之间视为协议可兼容 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.js
  • tests/provider-pool-manager-health.unit.test.js
  • tests/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/responses
  • model = gpt-5.3-codex
  • 结果:200

说明:

  • Codex 主链已经不再卡死在“429 后假健康 / 假 dead-end”状态

B. Public/main route 验证

生产 runtime 直测:

  • POST http://127.0.0.1:3301/v1/responses
  • model = gpt-5.3-codex -> 200
  • model = 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 首包门误杀

这次修复最值得记住的经验

  1. 429 不能只看 status code,要看语义字段

    • usage_limit_reached 和普通 burst rate limit 不是一回事
    • 前者应该进入 cooldown,最好带 recovery time
  2. “debug 里 healthy” 不等于真实健康

    • 如果 401/403 还靠累计 errorCount 才摘除,debug 会长期撒谎
  3. retry/fallback 绝不能复用上一个 provider 的已转换 request body

    • 这类 bug 会伪造出大量跨协议异常
    • 表面像 key 坏了,本质是 request shape 错了
  4. 网关自己的 timeout 门比上游 timeout 更危险

    • 如果 provider 已经显式配置了更大的首包等待时间
    • 网关必须读取当前 provider 的 runtime config,而不是只看全局默认值
  5. 远端 live 验证不能只看一条 200

    • 至少要同时验证:
      • direct provider route
      • main /v1/responses route
      • debug/provider snapshot
      • journal 中真实选中的 provider 与 error chain

后续运维建议

  1. 对所有 openaiResponses-custom 节点补 machine-readable 能力标签

    • 明确哪个节点支持 gpt-5.3-codex
    • 明确哪个节点需要大于 30s 的首包窗口
  2. 对 hard auth failure 建立自动熔断/自动 disable 策略

    • 例如连续一次明确 API key is disabled 即直接 isDisabled = true
    • 避免重复进入轮转池
  3. 对 quota recovery 建立 debug 可见字段

    • 例如在 provider debug 视图中直接显示 scheduledRecoveryTime
    • 让“冷却中”与“健康”一眼可分
  4. 保留这次排障顺序作为固定 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 误杀链路已完成治本修复。