Skip to content

LLM 客户端与 Responses API

源码版本rust-v0.145.0

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 调试代理。

职责

  1. 会话级客户端:ModelClient::new 接 auth manager、provider info、thread id、HTTP client factory 等 13 个参数,把这些跨 turn 稳定的状态收进 Arc<ModelClientState>codex-rs/core/src/client.rs:413-460
  2. Turn 级 session:ModelClientSessionnew_session 创建,持有一个 WebsocketSession 缓存和 turn_state: Arc<OnceLock<String>> 跨多次请求复用同 turn 的 sticky-routing token。codex-rs/core/src/client.rs:480-486
  3. 流式入口:ModelClientSession::streamWireApi 分派,Responses API 走 WebSocket(若可用)→ 失败 fallback HTTP,所有路径都返回 ResponseStreamcodex-rs/core/src/client.rs:1792-1843
  4. 远程压缩:同一个 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。

ModelClientModelClientSession 的两层切分:前者 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-272ModelClientModelClientSession 的结构体定义,后者文档明确要求每个 turn 调一次 new_sessioncodex-rs/core/src/client.rs:509-528force_http_fallback,永久禁 WS + 清缓存 + 上 telemetry。codex-rs/core/src/client.rs:944-962current_client_setup,集中 auth + provider 解析。codex-rs/codex-api/src/common.rs:74-119ResponseEvent 枚举,Created / OutputItemDone / Completed / ReasoningSummaryDelta 等所有 SSE 事件类型。codex-rs/responses-api-proxy/src/lib.rs:73-108run_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 抽象。

rust
// 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 卡死,只是损失复用收益。

rust
// 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 只读这个结构。

rust
// 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_fallbackAtomicBool::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-state sticky-routing token 流到下一 turn,触发服务器侧路由错乱。codex-rs/core/src/client.rs:260-272
  • prompt 空则跳过 compact:compact_conversation_historyprompt.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/compactresponses-api-proxy 提供独立调试代理。这层不参与循环调度,只把 prompt 流过去、ResponseEvent 流回来,真正驱动循环的是 Agent 主循环。auth 状态来自 ChatGPT 登录与认证 落盘的 auth.json