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。