Skip to content

G1|推理栈:Prompt 是合同,打出去是 Responses 客户端 ​

先别把 build_prompt 当成一次 HTTP ​

读完 B7,下一件错事是打开 Prompt 三块,以为赋值完就 POST /v1/chat/completions。然后 WS 被理解成「更快的 HTTP」,401 被理解成「再拼一次同样的 JSON」,Ollama 被理解成「把 ChatGPT 后端抄进 stub」。

判断先说清楚:B7 的合同是模型看见什么;这一章是这一枪怎么飞出去。 飞出去的不是一个 URL,是一只 turn-scoped 的 ModelClientSession:复用本轮的 WebSocket 和 sticky routing,走 Responses API(先 WS,不行再 HTTP SSE),失败按流 / 鉴权 / 换传输分流。Provider 只声明自己会什么,不假装自己是 Codex 后端。

附录 F 钉的是合同和 Event。G1 钉的是客户端会话。


1. ModelClient 跨 Turn,ModelClientSession 只活一轮 ​

两层不要焊成一个「全局 HTTP 单例」。

ModelClient 是 session-scoped(core/src/client.rs:236-244):auth、provider、thread id、以及「这轮已经 fallback 到 HTTP 了没有」。WS 一旦被本 session 判不健康,后面几轮继续走 HTTP,避免每轮再赌一次断线。

ModelClientSession 是 turn-scoped(:256-268)。注释写死:懒建 Responses WebSocket,本轮多枪复用;缓存上一份完整请求,好决定下一枪能不能只发增量;x-codex-turn-state 必须在本轮每枪回放,不准带到下一轮。run_turn 也写了同一句:WS 和 sticky routing 在这一轮里复用(session/turn.rs:414)。

所以 B7 那张失败表才禁止「每枪新建 ModelClientSession」。新建等于丢掉 sticky token,服务端会把续跑当成新 Turn 路由。

没有「session 客户端 / turn 会话」会怎样?要么每枪新开 TCP,prompt cache 和 sticky 全废;要么一只 session 活过两个 Turn,上一轮的 x-codex-turn-state 污染下一轮。合同在 Prompt,寿命在 Session。


2. 传输是 Responses,优先 WebSocket,失败才 HTTP SSE ​

ModelClientSession::stream(client.rs:1965-2004)按 provider 的 wire_api 走。WireApi::Responses 时:provider 还健康就先 stream_responses_websocket;WS 交出 FallbackToHttp,再切 HTTP Responses。两条路都在 codex-api/:endpoint/responses_websocket.rs 和 endpoint/responses.rs,事件归一化走 sse/。

这不是 Chat Completions 换个 path。请求体是 Responses 的 input / tools / instructions,流事件是 ResponseEvent,不是 choices[0].delta。codex-client 管 SSE 帧;codex-api 管「这一枪的 endpoint + 头 + 限流解析」(rate_limits.rs:22-25 读默认限流头家族)。

x-codex-turn-state、x-codex-turn-metadata、x-codex-routing-hint 写在 client.rs:151-155。它们是 本轮路由合同,不是给产品表面看的 trace。

没有 Responses 这一层会怎样?每个 provider 各写一套流解析,IDE 和 exec 看见的 Event 对不上;或者把限流头当普通 429 字符串,额度窗口和套餐快照全部丢。


3. 失败不是一种 retry:流、鉴权、换传输 ​

B7 已经分流「流 / 窗口 / 工具 / 额度」。G1 只补 枪还在飞的时候 谁重试。

responses_retry.rs:1 把这件事写成模块职责:Responses 请求的重试和传输降级。handle_retryable_response_stream_error(:51-58)吃的是正在用的那只 ModelClientSession,不是新开一只。采样和远程 compact v2 是两种 ResponsesStreamRequest(:21-24),重试预算分开算。

HTTP 路径上还有鉴权恢复。stream_responses_api(:1488-1503)从 AuthManager::unauthorized_recovery 拿状态机——B5 那只 manager 的 401 恢复,不是客户端自己再 login 一次。恢复失败才把错误抛回 Turn。

换传输是 session 级的:WS 判负之后 try_switch_fallback_transport,后面几轮默认 HTTP。不要在同一枪里 WS / HTTP 各打一遍完整 Prompt 当「双保险」。

没有这层分流会怎样?5xx、401、窗口满、工具 exit 2 进同一个 retry():要么把过期 token 打爆额度,要么把 compact 前的窗口错误当成「再 POST 一次」。


4. Provider 声明能力,不假装自己是 Codex 后端 ​

model-provider 决定 打到哪、会什么。远程 compact 是枚举:Unsupported 或 V2(model-provider/src/provider.rs:44-50)。OpenAI / 部分 Azure 声明 V2;普通自定义 URL 和 Ollama 默认不会。附录 C 展开压缩流;这里只要记住:能力是声明,不是把 ChatGPT 后端抄进 stub。

A1 的 uses_codex_backend() 仍是账号分叉。API Key、Bedrock、oss_provider 只换本机循环的出口。Apps、远程市场、云端任务不会因为你填对了 base URL 就出现。models-manager 刷新的是模型目录和 preset,不是第三套循环。

没有「能力声明」会怎样?Ollama 用户看见 compact 按钮,请求 404;或者所有自定义 URL 都被当成 Codex 后端,计量和 Apps 路由一起打穿。客户端抄的是「问 provider 会什么」,不是抄死一个 hostname。


5. 结语:带走一句话 ​

抄推理栈要抄 turn-scoped 的 Responses 客户端——本轮复用 WS 和 sticky token,失败按流 / 鉴权 / 换传输分流,能力由 provider 声明——不要把 Prompt 三块焊成一个 URL。