diff --git a/backend/crates/providers/openai/src/transport/websocket/pool/mod.rs b/backend/crates/providers/openai/src/transport/websocket/pool/mod.rs index 2e4e8e16d..9171b8450 100644 --- a/backend/crates/providers/openai/src/transport/websocket/pool/mod.rs +++ b/backend/crates/providers/openai/src/transport/websocket/pool/mod.rs @@ -36,7 +36,8 @@ const DEFAULT_MAX_CONNECTING: usize = 16; const DEFAULT_MAX_AGE: Duration = Duration::from_mins(55); const DEFAULT_MAINTENANCE_INTERVAL: Duration = Duration::from_secs(25); const DEFAULT_PING_INTERVAL: Duration = Duration::from_secs(25); -const DEFAULT_PING_TIMEOUT: Duration = Duration::from_secs(5); +// 心跳也覆盖正在生成的连接,给短时链路停顿留出恢复余量。 +const DEFAULT_PING_TIMEOUT: Duration = Duration::from_secs(30); pub(crate) const DEFAULT_STREAM_IDLE_TIMEOUT: Duration = Duration::from_secs(300); /// WebSocket 连接池。 @@ -67,9 +68,9 @@ pub struct CodexWebSocketPoolConfig { pub max_connecting: usize, /// 后台维护间隔;`None` 表示不启动后台任务。 pub maintenance_interval: Option, - /// idle socket 探活 ping 间隔;`None` 表示维护时只做过期清理。 + /// 池化连接的探活 ping 间隔(包括正在生成的连接);`None` 表示不主动 ping。 pub ping_interval: Option, - /// 发送 ping 后等待上游响应的超时时间;零值表示不校验 Pong deadline。 + /// 发送 ping 后等待任意入站帧的超时时间;零值表示不校验 Pong deadline。 pub ping_timeout: Duration, /// idle socket 无活动多久后视为失活。 pub liveness_timeout: Option, diff --git a/backend/crates/providers/openai/src/transport/websocket/pump.rs b/backend/crates/providers/openai/src/transport/websocket/pump.rs index f6985f94a..23022ea15 100644 --- a/backend/crates/providers/openai/src/transport/websocket/pump.rs +++ b/backend/crates/providers/openai/src/transport/websocket/pump.rs @@ -2,7 +2,7 @@ //! //! 每条上游 WebSocket 连接都由一个后台 pump 任务独占: //! - 持续读取 socket:一旦观察到 `Close` / EOF / 传输错误,立即把连接标记为 `closed`。 -//! - 自动回应上游 `Ping`,并按 `ping_interval` 主动 `Ping`;匹配的 `Pong` 必须在 deadline 内返回。 +//! - 自动回应上游 `Ping`,并按 `ping_interval` 主动 `Ping`;收到任意入站帧即解除本次心跳 deadline。 //! - 可选 `liveness_timeout`:长时间无任何入站活动时判定连接失活并退出。 //! //! 因此空闲连接的“死没死”在后台被实时感知;复用方只需零成本读取 [`PumpedWebSocket::is_closed`], @@ -57,7 +57,7 @@ impl PumpLogContext { pub(crate) struct PumpKeepalive { /// 主动 `Ping` 间隔;`None` 表示 pump 不主动 ping(仅被动读取 + 回应上游 ping)。 pub(crate) ping_interval: Option, - /// 主动 `Ping` 发出后等待匹配 `Pong` 的 deadline;`None` 表示不校验响应。 + /// 主动 `Ping` 发出后等待任意入站帧的 deadline;`None` 表示不校验响应。 pub(crate) ping_timeout: Option, /// 无入站活动多久后判定失活;`None` 表示只靠显式 close/传输错误发现死亡。 pub(crate) liveness_timeout: Option, diff --git a/deploy/README.md b/deploy/README.md index aeb05eb2c..d089cee8d 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -90,6 +90,10 @@ Compose 默认只绑定 `127.0.0.1`。从其他设备访问时,在应用前配 并设置 `X-Accel-Buffering: no` 和 `Cache-Control: no-cache, no-transform`。 反向代理仍需允许这些响应头生效;首个事件到达前的等待也需要足够的读取超时。 +OpenAI 上游池化 WebSocket 默认每 25 秒发送一次 Ping,发出后允许等待 30 秒; +收到 Pong 或其他入站帧即解除本次心跳截止,持续无响应则以 `pong_timeout` 关闭连接。 +此策略也覆盖正在生成的请求,与等待下一条上游消息的 `stream_idle_timeout_ms` 分别计时。 + 若 Codex 在压缩或长时间生成时出现 `error decoding response body`,这表示 客户端读取 HTTP 响应体失败。请结合网关请求诊断中的 `upstream.read.failed`、 `downstream.body.closed` 和反向代理日志判断断开位置,不能仅凭此消息认定是 JSON 格式错误。