Cliente LLM y Responses API
codex canaliza todas las llamadas a modelo a través de Responses API (/v1/responses). Esta capa, en core/src/client.rs, toma prompt + configuración de modelo + auth, compone la petición, va por WebSocket o HTTP, parsea el flujo SSE de vuelta a ResponseEvent, y entrega token usage, reasoning summary y tool call delta al bucle superior. El mismo ModelClient también se ocupa de las peticiones de compaction remota (compact_conversation_history) y del prewarm de auth, además de un proxy de depuración independiente responses-api-proxy.
Responsabilidades
- Cliente a nivel de sesión:
ModelClient::newtoma auth manager, provider info, thread id, factory de HTTP client, etc., 13 parámetros en total, y los reúne como estado estable entre turns enArc<ModelClientState>.codex-rs/core/src/client.rs:413-460 - Session a nivel de turn:
ModelClientSessionse crea connew_session, mantiene una caché deWebsocketSessiony unturn_state: Arc<OnceLock<String>>para reutilizar el token de sticky-routing entre múltiples peticiones del mismo turn.codex-rs/core/src/client.rs:480-486 - Entrada de streaming:
ModelClientSession::streamdispatcha segúnWireApi; Responses API va por WebSocket (si está disponible) → fallback HTTP si falla; todas las rutas devuelven unResponseStream.codex-rs/core/src/client.rs:1792-1843 - Compaction remota: el mismo
ModelClientexponecompact_conversation_history, que por el endpoint/responses/compactpide al server que pliegue el historial y devuelve la nueva lista deResponseItem.codex-rs/core/src/client.rs:538-547
Motivación de diseño
Responses API, a diferencia del Chat Completions tradicional, soporta de forma nativa reasoning summary, compaction del lado server y streaming de tool call delta. codex eligió que todas las llamadas a modelo pasen por /v1/responses (WireApi::Responses es hoy la única variante), ganando una lógica unificada de tratamiento del flujo de ResponseEvent. WebSocket es un transporte opcional que, frente a HTTP SSE, permite a codex enviar varias peticiones incrementales sobre la misma conexión (al hacer multi-step reasoning dentro de un turn, reutiliza contexto), y mediante el token de sticky-routing x-codex-turn-state garantiza que el mismo turn se enrute a la misma instancia de backend; pero WS añade complejidad, así que force_http_fallback lo deshabilita de forma permanente y limpia la caché, y ante error toda la session retrocede a HTTP.
La separación en dos capas entre ModelClient y ModelClientSession: el primero es session-scoped (una instancia para toda la sesión Codex), el segundo es turn-scoped (un new_session() por turn) y no se reutiliza entre turns; de lo contrario, el token turn_state contaminaría el enrutado entre turns. current_client_setup lleva el candado de resolución de auth a un único punto, asegurando que el prewarm (preconexión en background) y el turn real vean un estado de auth/provider consistente. Que compact_conversation_history reutilice el mismo cliente es un detalle: va a /responses/compact y no a /responses, pero las cabeceras de transport, de auth y de telemetría usan el mismo builder, así que en monitorización las peticiones de compaction y los turns normales son indistinguibles.
Archivos clave
codex-rs/core/src/client.rs:253-272 — definición de struct de ModelClient y ModelClientSession; la documentación del segundo exige llamar a new_session una vez por turn.codex-rs/core/src/client.rs:509-528 — force_http_fallback: deshabilita WS permanentemente + limpia caché + sube telemetry.codex-rs/core/src/client.rs:944-962 — current_client_setup, centraliza la resolución de auth + provider.codex-rs/codex-api/src/common.rs:74-119 — enum ResponseEvent: Created / OutputItemDone / Completed / ReasoningSummaryDelta y todos los tipos de evento SSE.codex-rs/responses-api-proxy/src/lib.rs:73-108 — run_main, server tiny_http para depuración que reenvía a upstream_url con opción de dump.ModelClient::new tiene tantos parámetros que necesita #[allow(clippy::too_many_arguments)], pero cada uno es imprescindible a nivel de sesión. create_model_provider(provider_info, auth_manager) convierte el provider info en un provider concreto (OpenAI / Bedrock / Ollama / LMStudio / Anthropic-style external); toda la resolución de auth posterior pasa por esa abstracción de provider.
// core/src/client.rs:413-460 — ModelClient::new ensambla el estado 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 es la entrada central dentro del turn. Primero comprueba si wire_api es Responses (hoy la única opción), y luego mira responses_websocket_enabled() para ir por WS o por HTTP. Si la ruta WS devuelve WebsocketStreamOutcome::FallbackToHttp, llama a try_switch_fallback_transport para conmutar permanentemente a HTTP y luego retrocede a stream_responses_api. Así, un fallo de WS no atasca el turn; solo se pierde el beneficio de reutilización.
// core/src/client.rs:1792-1843 — dispatch de 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 es la entrada compartida por prewarm y turn. Toma del provider auth() y api_provider(), y según ProviderAuthScope resuelve el scope de agent identity: agent_identity_policy decide si se permite que el auth de ChatGPT suba automáticamente a agent identity, y session_source determina el alcance del scope. Devuelve CurrentClientSetup, que el build_api_transport interno solo lee.
// core/src/client.rs:944-962 — resolución centralizada de 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,
})
}Flujo de datos
Bordes y fallos
- WS fallback permanente:
force_http_fallbackusaAtomicBool::swappara deshabilitar WS permanentemente, ystore_cached_websocket_session(WebsocketSession::default())limpia la conexión ya establecida. Una vez que se dispara fallback dentro de un turn, todos los turn restantes de la session van por HTTP, sin reintentar WS.codex-rs/core/src/client.rs:509-528 - turn session no se reutiliza: la documentación de
ModelClientSessionexige llamar anew_sessionuna vez por turn; reutilizarlo entre turns dejaría que el token de sticky-routingx-codex-turn-statedel turn anterior se filtrara al siguiente, desordenando el enrutado del lado server.codex-rs/core/src/client.rs:260-272 - prompt vacío saltar compact:
compact_conversation_historydevuelve un Vec vacío directamente cuandoprompt.input.is_empty(), sin gastar una llamada de red.codex-rs/core/src/client.rs:548-550 - prewarm de auth consistente con turn:
prewarm_authtambién pasa porcurrent_client_setup, asegurando que la preconexión en background y el turn real vean exactamente el mismo estado de auth/provider, evitando que tras el prewarm llegue un token nuevo y lo descuarte.codex-rs/core/src/client.rs:979-981
Resumen
ModelClient canaliza todas las llamadas a modelo a Responses API; mediante new_session saca un handle turn-scoped, y stream elige entre WebSocket y HTTP, conmutando permanentemente a HTTP si falla. compact_conversation_history reutiliza el mismo transport y cabeceras de auth para ir a /responses/compact. responses-api-proxy ofrece un proxy de depuración independiente. Esta capa no participa en la orquestación del bucle: solo hace pasar el prompt y devolver el flujo de ResponseEvent. Lo que realmente impulsa el bucle es el Bucle principal del Agent. El estado de auth proviene del auth.json volcado por Login de ChatGPT y autenticación.