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