【拯救 5.6 Sol(1.5)】codex首包卡死问题调查和解法

MotorwaySouth 2026-07-15 01:23 1


本文探讨的问题:



  • codex卡死在thinking,没有报错,就是看上去完全禁止了

  • 反代使用5.6会卡住,停止再继续就没问题了

  • codex模型执行完工具就卡在那里





太长不读:如果出现上述问题,按照下列优先级:





  • 1.能ouath登录就ouath登录(几乎一定能解决)




  • 2.实在不能ouath登录,cpa或者sub2api配置允许websockets。并在config.toml[model_providers.名字]下配置supports_websockets = true(几乎一定能解决)。




  • 3.new-api + cpa:new-api 至今不支持 WebSocket。需要自行合并 PR #5062 。然后开启cpa配置允许websockets。并在config.toml配置上述字段(几乎一定能解决)




  • 4.无论如何也不想打开ws:反代客户端需要存在「首包超时→同号重连」的逻辑(详见下文),如果没有,需要二开cpa或sub2api(不得已为之)




下面是原理和一些简单发现。大晚上写东西可能有疏漏,太困了。但大差不差了


请求OpenAI的两种模式




  • HTTP/SSE 模式(逐请求)

    每一次模型调用都是一次全新的独立请求:重新建连、把整段上下文发上去,等 OpenAI 应答,应答以 SSE(流式)一个事件一个事件地吐回来。下一次模型调用话,从头来一遍。




  • WebSocket 模式(一段会话复用一条连接)

    会话开始时只握手一次,把连接升级成一条长期存活的双向管道;此后每一轮对话都只是往这条已经打开的管道里发几个帧,应答也是帧返回。




HTTP/SSE 模式


一次 /v1/responses 请求"在 CPA↔OpenAI 这一跳的生命周期如下:


sequenceDiagram
participant C as CPA(发起方,uTLS 客户端)
participant O as OpenAI(上游)

Note over C,O: ① TCP 三次握手(建立可靠字节通道)
C->>O: SYN
O->>C: SYN-ACK
C->>O: ACK

Note over C,O: ② TLS 握手(uTLS 伪装 Chrome 指纹,建立加密通道)
C->>O: ClientHello
O->>C: ServerHello + 证书
C->>O: 密钥交换 + Finished

Note over C,O: ③ 发送 HTTP 请求(把整段对话上下文发上去)
C->>O: POST /v1/responses<br/>请求头 + 请求体(整段上下文,可达 ~90K token)

rect rgb(90, 30, 30)
Note over C,O: ④ 等待响应头 —— 首包卡死在这一步
Note right of O: OpenAI 已受理请求,但后端<br/>迟迟不开始产出任何字节<br/>健康:数百 ms;卡死:可能几十分钟或永久不返回
O-->>C: (长时间一个字节都不回)
end

Note over C,O: ⑤ 响应头终于到达,客户端的 http.Do() 才返回
O->>C: 200 OK + SSE 响应头

Note over C,O: ⑥ SSE 流式返回事件(首包一到,后续正常)
O->>C: event: response.created
O->>C: event: reasoning / output delta …
O->>C: event: [DONE]

请求流程:一条连接,从 TCP 握手(①)→ TLS 握手(②)→ 发出 HTTP 请求(③)→ 阻塞等待响应头(④) → 收到响应头(⑤)→ SSE 逐事件流式返回(⑥)。整个过程里,唯一会长时间卡死的是第 ④ 步。



  • 需要注意的是:本机网络问题,无法访问openai服务器会在①②③环节中断,体现到codex中就是 reconnect。


为什么会卡住?


以CPA为例:在 CPA 源码里,第 ③④⑤ 步对应一次调用 httpClient.Do(httpReq)internal/runtime/executor/codex_executor.go:1121):这个调用发出请求、并阻塞到响应头到达才返回。


关键在于这个 HTTP 客户端在"等响应头"这一步没有任何超时:




























位置 事实 后果
helps.NewUtlsHTTPClient(ctx, cfg, auth, 0) 构造时 timeout=0 不设 client.Timeout
utls_client.go(transport) 未设 ResponseHeaderTimeout 传输层不给"等响应头"设上限
结果 Do() 在第 ④ 步可无限期阻塞 上游卡多久就等多久

所以"首包卡死"就是第 ④ 步的无限阻塞。


那么到这里锅就可以确定了,openai收到请求不干事,死活不发响应头。这就是造成一系列问题的根本原因


我们在测试中进行了日志跟踪,"看起来是卡死"其实有两种可能:


























形态 描述 卡在哪 证据
header-stall(首包/响应头卡死) 连 200 响应头都迟迟不发 第 ④ 步(等响应头) 就是它——逐帧日志显示选定上游后有 8.5 分钟空白才进入读流阶段,说明卡在读流之前,即 http.Do
body-stall(响应体卡死) 200 头秒到,SSE 流开了却卡在首个内容 第 ⑥ 步之内 零证据——健康请求 response.created 亚秒即到

所以这里引入了一个兜底性的解法:



  1. 给第 ④ 步套一个预算(first-event-timeout-seconds,现设 6 秒):把 ExecuteStream(含 http.Do)丢到 goroutine 上,一个预算同时覆盖住"等响应头 + 等首字节"。健康请求首字节 1~3 秒到,6 秒有充足余量;卡死则零字节。

  2. 超时→同号重连:一旦超时,丢弃这条卡住的请求、用同一个账号重新发起(first-event-retries,现设 4)。因为卡死是单个请求实例摇到的坏运气(可能是openai服务端排队?不确定),重发=重新摇一次骰子,实测多数一两次就中。


感兴趣的佬可自行二开更改cpa或sub2api,或提交pr,不确定上游是否会接受。前文说的很清楚了,最大的锅在openai


WebSocket 模式:为何直接绕过首包卡死


一次 WebSocket 会话的生命周期


sequenceDiagram
participant K as Codex
participant U as OpenAI(经 nginx / new-api / CPA 透传)

Note over K,U: 会话开始:一次性握手(TCP + TLS + HTTP Upgrade)
K->>U: GET /v1/responses<br/>Upgrade: websocket<br/>(key 藏在 Sec-WebSocket-Protocol 里)
U->>K: 101 Switching Protocols

Note over K,U: 此后整段会话复用这一条连接,双向收发"帧",不再有新的 HTTP 请求
K->>U: 帧:response.create(第 1 轮)
U->>K: 帧:response.created / delta … / done
K->>U: 帧:response.create(第 2 轮)
U->>K: 帧:… 响应事件 …
K->>U: 帧:response.create(第 N 轮)
U->>K: 帧:… 响应事件 …

Note over K,U: 每一轮都直接发帧,没有"新建 HTTP 请求 → 等响应头"这一步

WebSocket 的本质:握手一次,把 HTTP 连接"升级"成一条长期存活的双向管道;之后所有来回都是轻量的帧,在这条已建好的管道里流动。


纠正一个可能的误解:HTTP 并不是"每次请求都新建一条连接"——HTTP/1.1 keep-alive、HTTP/2 都能在同一条连接上跑多个请求。

































维度 HTTP/SSE WebSocket
一段会话里 每一轮对话 = 一次独立的请求-响应周期 整段会话 = 一条持久连接
"等响应头"这一步(第④步) 每一轮都要经历一次 只在会话开始的握手时有一次(返回 101 很快),之后每轮不再有
承载单位 每轮一个完整 HTTP 请求(含头、体、SSE 响应) 每轮只是若干"帧"
生产观测 每轮一条 gin 请求日志 一条连接只在开/关时记一次;多轮请求复用同一个 request_id


  • 由此可见为什么WebSocket能解决问题:让 HTTP 概率卡死的那一步(第 ④ 步"等响应头")在 HTTP 里是"每轮一次",在 WebSocket 里被压缩成"整段会话一次"。


系列文章:




  • 【拯救 5.6 Sol(1)】开箱即用、快速高效、减少上下文腐烂的Codex子代理实践




  • 【拯救 5.6 Sol(2)】让 Codex 又快又准的原生模型阅读工具

    编写中和开发中;类似claude code的read、grep、glob,但更加强大:允许直接原生读取pdf、word、PPT等多类型文件,自动处理转换多种编码,原生读取二进制视图…




  • 【拯救 5.6 Sol(3)】Codex 子代理机制完全解读

    编写中;对配置字段逐行解读



最新回复 (4)
  • James 07-15 01:28
    1

    来支持了,佬的上一篇只读代理思路很有启发

    蹲一下【拯救 5.6 Sol(2)】,能让codex不手搓读文件还是很好的

  • PositronCannon 07-15 01:31
    2

    又有新的干货了,第一时间赶来拜读

  • PositronCannon 07-15 01:33
    3

    佬,请问 WebSocket 会话的生命周期的这图是用的什么做的呀?mermaid吗?能这么规整吗

  • 南箕子 07-15 03:57
    4

    佬真的是跟 codex 如胶似漆啊,哈哈哈

* 帖子来源Linux.do
返回