Skip to content

Cliente LLM y Responses API

源码版本rust-v0.145.0

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

  1. Cliente a nivel de sesión: ModelClient::new toma 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 en Arc<ModelClientState>. codex-rs/core/src/client.rs:413-460
  2. Session a nivel de turn: ModelClientSession se crea con new_session, mantiene una caché de WebsocketSession y un turn_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
  3. Entrada de streaming: ModelClientSession::stream dispatcha según WireApi; Responses API va por WebSocket (si está disponible) → fallback HTTP si falla; todas las rutas devuelven un ResponseStream. codex-rs/core/src/client.rs:1792-1843
  4. Compaction remota: el mismo ModelClient expone compact_conversation_history, que por el endpoint /responses/compact pide al server que pliegue el historial y devuelve la nueva lista de ResponseItem. 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-528force_http_fallback: deshabilita WS permanentemente + limpia caché + sube telemetry.codex-rs/core/src/client.rs:944-962current_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-108run_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.

rust
// 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.

rust
// 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.

rust
// 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_fallback usa AtomicBool::swap para deshabilitar WS permanentemente, y store_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 ModelClientSession exige llamar a new_session una vez por turn; reutilizarlo entre turns dejaría que el token de sticky-routing x-codex-turn-state del 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_history devuelve un Vec vacío directamente cuando prompt.input.is_empty(), sin gastar una llamada de red. codex-rs/core/src/client.rs:548-550
  • prewarm de auth consistente con turn: prewarm_auth también pasa por current_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.