LLM 客户端与 Responses API
codex 把所有模型调用都收口到 Responses API(/v1/responses)。这一层在 core/src/client.rs 实现,职责是拿 prompt + 模型配置 + auth,拼请求、走 WebSocket 或 HTTP、解析 SSE 流回 ResponseEvent,把 token usage、reasoning summary、tool call delta 都喂给上层循环。同一个 ModelClient 还负责远程 compaction 请求(compact_conversation_history)和 auth prewarm,以及一个独立的 responses-api-proxy 调试代理。
职责
- 会话级客户端:
ModelClient::new接 auth manager、provider info、thread id、HTTP client factory 等 13 个参数,把这些跨 turn 稳定的状态收进Arc<ModelClientState>。codex-rs/core/src/client.rs:413-460 - Turn 级 session:
ModelClientSession由new_session创建,持有一个WebsocketSession缓存和turn_state: Arc<OnceLock<String>>跨多次请求复用同 turn 的 sticky-routing token。codex-rs/core/src/client.rs:480-486 - 流式入口:
ModelClientSession::stream按WireApi分派,Responses API 走 WebSocket(若可用)→ 失败 fallback HTTP,所有路径都返回ResponseStream。codex-rs/core/src/client.rs:1792-1843 - 远程压缩:同一个
ModelClient上挂compact_conversation_history,通过/responses/compact端点让 server 端折叠历史,返回新ResponseItem列表。codex-rs/core/src/client.rs:538-547
设计动机
Responses API 与传统 Chat Completions 不同,它原生支持 reasoning summary、server-side compaction、streaming tool call delta 等能力。codex 选择把所有模型调用都走 /v1/responses 这一条路(WireApi::Responses 是目前唯一变体),换来统一的 ResponseEvent 流处理逻辑。WebSocket 是可选传输,相比 HTTP SSE 能让 codex 在同一连接上发多个增量请求(turn 内多步推理时复用上下文),并通过 x-codex-turn-state sticky-routing token 保证同一 turn 路由到同一后端实例;但 WS 复杂度高,所以 force_http_fallback 永久禁用 WS 并清缓存,出错后整个 session 都退回 HTTP。
ModelClient 与 ModelClientSession 的两层切分:前者 session-scoped(整个 Codex 会话用一份),后者 turn-scoped(每个 turn new_session() 一次),不能跨 turn 复用——否则 turn_state token 跨 turn 会污染路由。current_client_setup 把 auth 解析锁进单点,确保 prewarm(后台预连接)和真实 turn 看到一致的 auth/provider 状态。compact_conversation_history 复用同一 client 是个细节:它走 /responses/compact 而不是 /responses,但 transport 头部、auth 头、telemetry 全用同一套 builder,让 compaction 请求和正常 turn 在监控里看不出差异。
关键文件
codex-rs/core/src/client.rs:253-272 — ModelClient 与 ModelClientSession 的结构体定义,后者文档明确要求每个 turn 调一次 new_session。codex-rs/core/src/client.rs:509-528 — force_http_fallback,永久禁 WS + 清缓存 + 上 telemetry。codex-rs/core/src/client.rs:944-962 — current_client_setup,集中 auth + provider 解析。codex-rs/codex-api/src/common.rs:74-119 — ResponseEvent 枚举,Created / OutputItemDone / Completed / ReasoningSummaryDelta 等所有 SSE 事件类型。codex-rs/responses-api-proxy/src/lib.rs:73-108 — run_main,调试用 tiny_http server,转发到 upstream_url 并可选 dump。ModelClient::new 参数多到要 #[allow(clippy::too_many_arguments)],但每个都是 session-scoped 必需品。create_model_provider(provider_info, auth_manager) 把 provider info 转成具体 provider(OpenAI / Bedrock / Ollama / LMStudio / Anthropic-style external),之后所有 auth 解析都走这个 provider 抽象。
// core/src/client.rs:413-460 — ModelClient::new 装配 session-scoped 状态
pub fn new(
auth_manager: Option<Arc<AuthManager>>,
agent_identity_policy: AgentIdentityAuthPolicy,
thread_id: ThreadId,
provider_info: ModelProviderInfo,
session_source: SessionSource,
originator: String,
model_verbosity: Option<VerbosityConfig>,
enable_request_compression: bool,
include_timing_metrics: bool,
beta_features_header: Option<String>,
item_ids_enabled: bool,
concurrent_reasoning_summaries_enabled: bool,
attestation_provider: Option<Arc<dyn AttestationProvider>>,
http_client_factory: HttpClientFactory,
) -> Self {
let model_provider = create_model_provider(provider_info, auth_manager);
// ...
Self { state: Arc::new(ModelClientState { /* ... */ }), /* ... */ }
}stream 是 turn 内的核心入口。先看 wire_api 是否 Responses(目前唯一选项),再看 responses_websocket_enabled() 决定走 WS 还是 HTTP。WS 路径若返回 WebsocketStreamOutcome::FallbackToHttp 就调用 try_switch_fallback_transport 永久切到 HTTP,然后退回 stream_responses_api。这样 WS 失败不会让 turn 卡死,只是损失复用收益。
// core/src/client.rs:1792-1843 — ModelClientSession::stream 分派
pub async fn stream(
&mut self,
prompt: &Prompt,
model_info: &ModelInfo,
session_telemetry: &SessionTelemetry,
effort: Option<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
service_tier: Option<String>,
responses_metadata: &CodexResponsesMetadata,
inference_trace: &InferenceTraceContext,
) -> Result<ResponseStream> {
let wire_api = self.client.state.provider.info().wire_api;
match wire_api {
WireApi::Responses => {
if self.client.responses_websocket_enabled() {
match self.stream_responses_websocket(/* ... */).await? {
WebsocketStreamOutcome::Stream(stream) => return Ok(stream),
WebsocketStreamOutcome::FallbackToHttp => {
self.try_switch_fallback_transport(session_telemetry, model_info);
}
}
}
self.stream_responses_api(/* ... */).await
}
}
}current_client_setup 是 prewarm 和 turn 共享的入口。它从 provider 拿 auth() 和 api_provider(),再按 ProviderAuthScope 解析 agent identity scope——agent_identity_policy 决定是否允许 ChatGPT auth 自动升级成 agent identity,session_source 决定 scope 范围。返回 CurrentClientSetup,内部后续 build_api_transport 只读这个结构。
// core/src/client.rs:944-962 — 集中 auth + provider 解析
async fn current_client_setup(&self) -> Result<CurrentClientSetup> {
let auth = self.state.provider.auth().await;
let api_provider = self.state.provider.api_provider().await?;
let resolved_auth = self
.state
.provider
.api_auth_for_scope(ProviderAuthScope {
agent_identity_policy: self.agent_identity_policy,
session_source: self.state.session_source.clone(),
agent_identity_session_fallback: self.state.agent_identity_session_fallback.clone(),
})
.await?;
Ok(CurrentClientSetup {
auth,
api_provider,
api_auth: resolved_auth.auth,
agent_identity_telemetry: resolved_auth.agent_identity_telemetry,
})
}数据流
边界与失败
- WS fallback 永久:
force_http_fallback用AtomicBool::swap永久禁用 WS,且store_cached_websocket_session(WebsocketSession::default())清掉已建连。一旦 turn 内触发 fallback,session 剩余所有 turn 都走 HTTP,不再尝试 WS。codex-rs/core/src/client.rs:509-528 - turn session 不复用:
ModelClientSession文档明确要求每个 turn 调一次new_session,跨 turn 复用会让上一 turn 的x-codex-turn-statesticky-routing token 流到下一 turn,触发服务器侧路由错乱。codex-rs/core/src/client.rs:260-272 - prompt 空则跳过 compact:
compact_conversation_history在prompt.input.is_empty()时直接返回空 Vec,不浪费一次网络调用。codex-rs/core/src/client.rs:548-550 - auth prewarm 与 turn 一致:
prewarm_auth也走current_client_setup,确保后台预连接和真实 turn 看到的 auth/provider 状态完全一致,避免 prewarm 建连后又被新 token 顶掉。codex-rs/core/src/client.rs:979-981
小结
ModelClient 把所有模型调用收口到 Responses API,通过 new_session 切出 turn-scoped 句柄,stream 在 WebSocket 和 HTTP 之间二选一,失败永久切 HTTP。compact_conversation_history 复用同一套 transport 与 auth 头走 /responses/compact。responses-api-proxy 提供独立调试代理。这层不参与循环调度,只把 prompt 流过去、ResponseEvent 流回来,真正驱动循环的是 Agent 主循环。auth 状态来自 ChatGPT 登录与认证 落盘的 auth.json。