Skip to content

Client LLM et Responses API

源码版本rust-v0.145.0

codex regroupe tous les appels de modèle derrière la Responses API (/v1/responses). Cette couche est implémentée dans core/src/client.rs, avec pour rôle : prendre le prompt + la config modèle + l'auth, construire la requête, passer par WebSocket ou HTTP, parser le flux SSE en ResponseEvent, et remonter token usage, reasoning summary et tool call delta à la boucle de haut niveau. Le même ModelClient gère aussi les requêtes de compaction distante (compact_conversation_history), le prewarm d'auth, ainsi qu'un proxy de débogage indépendant responses-api-proxy.

Responsabilités

  1. Client niveau session : ModelClient::new reçoit auth manager, provider info, thread id, HTTP client factory, etc. — 13 paramètres — et rassemble cet état stable entre turns dans Arc<ModelClientState>. codex-rs/core/src/client.rs:413-460
  2. Session niveau turn : ModelClientSession est créée par new_session, porte un cache WebsocketSession et un turn_state: Arc<OnceLock<String>> pour réutiliser le sticky-routing token d'un même turn à travers plusieurs requêtes. codex-rs/core/src/client.rs:480-486
  3. Entrée streaming : ModelClientSession::stream répartit selon WireApi ; la Responses API passe par WebSocket (si disponible) → en cas d'échec, fallback HTTP ; tous les chemins retournent un ResponseStream. codex-rs/core/src/client.rs:1792-1843
  4. Compaction distante : le même ModelClient expose compact_conversation_history, qui appelle /responses/compact pour faire replier l'historique côté server et renvoyer une nouvelle liste d'ResponseItem. codex-rs/core/src/client.rs:538-547

Motivations de conception

La Responses API diffère des traditionnelles Chat Completions : elle supporte nativement le reasoning summary, la compaction côté server et les tool call delta en streaming. codex choisit de faire passer tous les appels de modèle par /v1/responses (WireApi::Responses est actuellement la seule variante), ce qui lui donne une logique de traitement de flux ResponseEvent unifiée. WebSocket est un transport optionnel : par rapport à HTTP SSE, il permet d'envoyer plusieurs requêtes incrémentales sur la même connexion (réutilisation du contexte en raisonnement multi-étapes d'un turn), et le sticky-routing token x-codex-turn-state garantit qu'un même turn est routé vers la même instance backend ; mais WS est complexe, donc force_http_fallback désactive WS définitivement et vide le cache, et en cas d'erreur la session entière retombe sur HTTP.

Le découpage en deux niveaux ModelClient / ModelClientSession : le premier est session-scoped (un seul pour toute la session Codex), le second est turn-scoped (un new_session() par turn), non réutilisable entre turns — sinon le token turn_state polluerait le routage d'un turn à l'autre. current_client_setup verrouille la résolution d'auth en un point unique, pour que prewarm (préconnexion en arrière-plan) et turn réel voient le même état auth/provider. La réutilisation du même client par compact_conversation_history est un point de détail : il appelle /responses/compact plutôt que /responses, mais les headers transport, auth et télémétrie utilisent le même builder, pour que les requêtes de compaction et les turns normaux soient indistinguables dans la supervision.

Fichiers clés

codex-rs/core/src/client.rs:253-272 — définition des structures ModelClient et ModelClientSession ; la doc du second exige un new_session par turn.codex-rs/core/src/client.rs:509-528force_http_fallback, désactive WS définitivement + vide le cache + reporte en télémétrie.codex-rs/core/src/client.rs:944-962current_client_setup, concentre la résolution auth + provider.codex-rs/codex-api/src/common.rs:74-119 — l'énum ResponseEvent, tous les types d'événements SSE : Created / OutputItemDone / Completed / ReasoningSummaryDelta, etc.codex-rs/responses-api-proxy/src/lib.rs:73-108run_main, tiny_http server de débogage qui forward vers upstream_url avec dump optionnel.

ModelClient::new a tellement de paramètres qu'il faut un #[allow(clippy::too_many_arguments)], mais chacun est nécessaire au scope session. create_model_provider(provider_info, auth_manager) convertit le provider info en provider concret (OpenAI / Bedrock / Ollama / LMStudio / Anthropic-style external), après quoi toute résolution d'auth passe par cette abstraction 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 est l'entrée cœur d'un turn. On regarde d'abord si wire_api vaut Responses (la seule option actuellement), puis responses_websocket_enabled() décide WS ou HTTP. Si le chemin WS retourne WebsocketStreamOutcome::FallbackToHttp, on appelle try_switch_fallback_transport pour basculer définitivement sur HTTP, puis on retombe sur stream_responses_api. Ainsi, un échec WS ne bloque pas le turn, on perd juste le bénéfice de réutilisation.

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 est l'entrée partagée par prewarm et turn. Il récupère auth() et api_provider() depuis le provider, puis résout le scope agent identity selon ProviderAuthScopeagent_identity_policy décide si l'auth ChatGPT peut être promue en agent identity, session_source décide du périmètre du scope. Il renvoie CurrentClientSetup, que build_api_transport consomme en lecture seule.

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,
    })
}

Flux de données

Limites et échecs

  • WS fallback définitif : force_http_fallback utilise AtomicBool::swap pour désactiver WS définitivement, et store_cached_websocket_session(WebsocketSession::default()) nettoie la connexion déjà établie. Une fois le fallback déclenché dans un turn, tous les turns restants de la session passent en HTTP, sans réessayer WS. codex-rs/core/src/client.rs:509-528
  • Pas de réutilisation de session entre turns : la doc de ModelClientSession exige explicitement un new_session par turn ; une réutilisation inter-turns ferait fuiter le sticky-routing token x-codex-turn-state du tour précédent vers le suivant, provoquant un routage erroné côté serveur. codex-rs/core/src/client.rs:260-272
  • Prompt vide, pas de compact : compact_conversation_history renvoie un Vec vide directement si prompt.input.is_empty(), sans gaspiller un appel réseau. codex-rs/core/src/client.rs:548-550
  • Prewarm d'auth cohérent avec le turn : prewarm_auth passe aussi par current_client_setup, pour que la préconnexion en arrière-plan et le turn réel voient exactement le même état auth/provider, et qu'une préconnexion ne soit pas évincée par un nouveau token. codex-rs/core/src/client.rs:979-981

Récapitulatif

ModelClient regroupe tous les appels de modèle derrière la Responses API, découpe un handle turn-scoped via new_session, et stream choisit entre WebSocket et HTTP, avec bascule définitive vers HTTP en cas d'échec. compact_conversation_history réutilise les mêmes headers transport et auth pour appeler /responses/compact. responses-api-proxy fournit un proxy de débogage indépendant. Cette couche ne participe pas au scheduling de la boucle : elle se contente de faire transiter le prompt et de renvoyer le flux d'ResponseEvent ; ce qui pilote vraiment la boucle, c'est Boucle principale de l'Agent. L'état d'auth vient du auth.json écrit par Login ChatGPT et authentification.