Client LLM et Responses API
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
- Client niveau session :
ModelClient::newreçoit auth manager, provider info, thread id, HTTP client factory, etc. — 13 paramètres — et rassemble cet état stable entre turns dansArc<ModelClientState>.codex-rs/core/src/client.rs:413-460 - Session niveau turn :
ModelClientSessionest créée parnew_session, porte un cacheWebsocketSessionet unturn_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 - Entrée streaming :
ModelClientSession::streamrépartit selonWireApi; la Responses API passe par WebSocket (si disponible) → en cas d'échec, fallback HTTP ; tous les chemins retournent unResponseStream.codex-rs/core/src/client.rs:1792-1843 - Compaction distante : le même
ModelClientexposecompact_conversation_history, qui appelle/responses/compactpour 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-528 — force_http_fallback, désactive WS définitivement + vide le cache + reporte en télémétrie.codex-rs/core/src/client.rs:944-962 — current_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-108 — run_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.
// 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.
// 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 ProviderAuthScope — agent_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.
// 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_fallbackutiliseAtomicBool::swappour désactiver WS définitivement, etstore_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
ModelClientSessionexige explicitement unnew_sessionpar turn ; une réutilisation inter-turns ferait fuiter le sticky-routing tokenx-codex-turn-statedu 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_historyrenvoie un Vec vide directement siprompt.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_authpasse aussi parcurrent_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.