LLM-Client und Responses API
codex leitet alle Modellaufrufe über die Responses API (/v1/responses) zusammen. Diese Schicht ist in core/src/client.rs implementiert; ihre Aufgabe ist, aus Prompt + Modellkonfiguration + Auth einen Request zu bauen, über WebSocket oder HTTP zu senden, den SSE-Strom zu ResponseEvent zu parsen und Token-Usage, Reasoning Summary und Tool-Call-Deltas an die obere Schleife zu füttern. Derselbe ModelClient ist außerdem für Remote-Compaction-Requests (compact_conversation_history) sowie Auth-Prewarm zuständig und bringt einen eigenständigen responses-api-proxy-Debug-Proxy mit.
Verantwortlichkeiten
- Sitzungs-Client:
ModelClient::newnimmt Auth Manager, Provider Info, Thread-ID, HTTP-Client-Factory und weitere Parameter (insgesamt 13) und bündelt diesen turn-übergreifend stabilen Zustand inArc<ModelClientState>.codex-rs/core/src/client.rs:413-460 - Turn-Sitzung:
ModelClientSessionwird vonnew_sessionerzeugt; sie hält einenWebsocketSession-Cache sowieturn_state: Arc<OnceLock<String>>, um das Sticky-Routing-Token über mehrere Requests desselben Turns wiederzuverwenden.codex-rs/core/src/client.rs:480-486 - Streaming-Einstieg:
ModelClientSession::streamdispatcht nachWireApi; bei Responses API geht es über WebSocket (falls verfügbar) → bei Fehlschlag Fallback auf HTTP; alle Pfade liefern einenResponseStream.codex-rs/core/src/client.rs:1792-1843 - Remote-Kompression (compaction): Auf demselben
ModelClienthängtcompact_conversation_history, das den Server über den Endpunkt/responses/compactdie Historie falten lässt und eine neueResponseItem-Liste zurückliefert.codex-rs/core/src/client.rs:538-547
Entwurfsbeweggründe
Anders als klassische Chat Completions unterstützt die Responses API nativ Reasoning Summary, serverseitige Kompression (compaction) und Streaming von Tool-Call-Deltas. codex leitet daher alle Modellaufrufe über /v1/responses (WireApi::Responses ist derzeit die einzige Variante) und erhält im Gegenzug eine einheitliche ResponseEvent-Strom-Verarbeitungslogik. WebSocket ist ein optionaler Transport: Gegenüber HTTP-SSE erlaubt er, mehrere inkrementelle Requests innerhalb einer Verbindung (bei mehrstufigem Reasoning in einem Turn Wiederverwendung des Kontexts) und über das x-codex-turn-state-Sticky-Routing-Token das Routing an dieselbe Backend-Instanz innerhalb eines Turns. Weil WS aber komplex ist, deaktiviert force_http_fallback permanent WS und löscht den Cache; nach einem Fehler geht die gesamte Session auf HTTP zurück.
Die Zweiteilung von ModelClient und ModelClientSession: Ersterer ist session-scoped (eine Instanz pro Codex-Sitzung), letzterer turn-scoped (pro Turn einmal new_session()); er darf nicht turn-übergreifend wiederverwendet werden, sonst würde das turn_state-Token das Routing verschmutzen. current_client_setup bannt die Auth-Auflösung in einem einzelnen Punkt und stellt so sicher, dass Prewarm (Hintergrund-Vorverbindung) und realer Turn denselben Auth/Provider-Zustand sehen. Dass compact_conversation_history denselben Client nutzt, ist ein Detail: Es geht auf /responses/compact statt /responses, aber Transport-Header, Auth-Header und Telemetrie verwenden denselben Builder, sodass ein Compaction-Request und ein normaler Turn in der Beobachtung nicht unterscheidbar sind.
Wichtige Dateien
codex-rs/core/src/client.rs:253-272 — Strukturdefinition von ModelClient und ModelClientSession; letztere dokumentiert explizit, dass pro Turn einmal new_session aufzurufen ist.codex-rs/core/src/client.rs:509-528 — force_http_fallback: WS permanent deaktivieren + Cache löschen + Telemetrie-Event.codex-rs/core/src/client.rs:944-962 — current_client_setup, konzentriert Auth + Provider-Auflösung.codex-rs/codex-api/src/common.rs:74-119 — ResponseEvent-Enum, Created / OutputItemDone / Completed / ReasoningSummaryDelta und alle anderen SSE-Ereignistypen.codex-rs/responses-api-proxy/src/lib.rs:73-108 — run_main, ein kleiner tiny_http-Server für Debugging, der an upstream_url weiterleitet und optional dumped.ModelClient::new braucht so viele Parameter, dass #[allow(clippy::too_many_arguments)] nötig ist; aber jeder ist session-scoped unverzichtbar. create_model_provider(provider_info, auth_manager) wandelt die Provider Info in einen konkreten Provider um (OpenAI / Bedrock / Ollama / LMStudio / Anthropic-artig extern); danach läuft jegliche Auth-Auflösung über diese Provider-Abstraktion.
// 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 ist der Haupteinstieg innerhalb eines Turns. Zuerst wird geprüft, ob wire_api Responses ist (derzeit einzige Option), dann entscheidet responses_websocket_enabled() zwischen WS und HTTP. Liefert der WS-Pfad WebsocketStreamOutcome::FallbackToHttp, ruft es try_switch_fallback_transport auf und schaltet permanent auf HTTP, um dann auf stream_responses_api zurückzufallen. So lässt ein WS-Fehler den Turn nicht feststecken, nur der Wiederverwendungsgewinn geht verloren.
// 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 ist der gemeinsame Einstieg von Prewarm und Turn. Er holt vom Provider auth() und api_provider() und löst dann nach ProviderAuthScope die agent identity scope auf — agent_identity_policy entscheidet, ob ChatGPT-Auth automatisch zu agent identity upgegradet werden darf, session_source bestimmt die Scope-Reichweite. Zurück kommt CurrentClientSetup, das nachfolgende build_api_transport nur noch liest.
// 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,
})
}Datenfluss
Grenzen und Fehler
- WS-Fallback permanent:
force_http_fallbackschaltet perAtomicBool::swapWS dauerhaft ab und leert überstore_cached_websocket_session(WebsocketSession::default())die aufgebaute Verbindung. Einmal in einem Turn ausgelöst, gehen alle restlichen Turns der Session über HTTP, kein neuer WS-Versuch.codex-rs/core/src/client.rs:509-528 - Turn-Session nicht wiederverwendet: Die
ModelClientSession-Dokumentation verlangt pro Turn einmalnew_session; turn-übergreifende Wiederverwendung ließe dasx-codex-turn-state-Sticky-Routing-Token des vorigen Turns in den nächsten fließen und auf Serverseite Routing-Chaos auslösen.codex-rs/core/src/client.rs:260-272 - Bei leerem Prompt kein compact:
compact_conversation_historygibt beiprompt.input.is_empty()direkt einen leeren Vec zurück, ohne einen Netzwerkaufruf zu verschwenden.codex-rs/core/src/client.rs:548-550 - Auth-Prewarm und Turn konsistent:
prewarm_authläuft ebenfalls übercurrent_client_setup, damit Hintergrund-Vorverbindung und realer Turn exakt denselben Auth/Provider-Zustand sehen und Prewarm nach Aufbau nicht von einem neuen Token verdrängt wird.codex-rs/core/src/client.rs:979-981
Zusammenfassung
ModelClient bündelt alle Modellaufrufe auf die Responses API, teilt über new_session einen turn-scoped Handle ab und wählt in stream zwischen WebSocket und HTTP; bei Fehlschlag permanent auf HTTP. compact_conversation_history nutzt dieselben Transport- und Auth-Header über /responses/compact. responses-api-proxy stellt einen eigenständigen Debug-Proxy zur Verfügung. Diese Schicht betreibt keine Schleifenplanung, sondern lässt nur den Prompt hinein- und ResponseEvent herausfließen; die Schleife treibt eigentlich die Agent-Hauptschleife an. Der Auth-Zustand stammt aus der auth.json, die von ChatGPT-Login und Authentifizierung geschrieben wurde.