Skip to content

LLM-Client und Responses API

源码版本rust-v0.145.0

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

  1. Sitzungs-Client: ModelClient::new nimmt Auth Manager, Provider Info, Thread-ID, HTTP-Client-Factory und weitere Parameter (insgesamt 13) und bündelt diesen turn-übergreifend stabilen Zustand in Arc<ModelClientState>. codex-rs/core/src/client.rs:413-460
  2. Turn-Sitzung: ModelClientSession wird von new_session erzeugt; sie hält einen WebsocketSession-Cache sowie turn_state: Arc<OnceLock<String>>, um das Sticky-Routing-Token über mehrere Requests desselben Turns wiederzuverwenden. codex-rs/core/src/client.rs:480-486
  3. Streaming-Einstieg: ModelClientSession::stream dispatcht nach WireApi; bei Responses API geht es über WebSocket (falls verfügbar) → bei Fehlschlag Fallback auf HTTP; alle Pfade liefern einen ResponseStream. codex-rs/core/src/client.rs:1792-1843
  4. Remote-Kompression (compaction): Auf demselben ModelClient hängt compact_conversation_history, das den Server über den Endpunkt /responses/compact die Historie falten lässt und eine neue ResponseItem-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-528force_http_fallback: WS permanent deaktivieren + Cache löschen + Telemetrie-Event.codex-rs/core/src/client.rs:944-962current_client_setup, konzentriert Auth + Provider-Auflösung.codex-rs/codex-api/src/common.rs:74-119ResponseEvent-Enum, Created / OutputItemDone / Completed / ReasoningSummaryDelta und alle anderen SSE-Ereignistypen.codex-rs/responses-api-proxy/src/lib.rs:73-108run_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.

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

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

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

Datenfluss

Grenzen und Fehler

  • WS-Fallback permanent: force_http_fallback schaltet per AtomicBool::swap WS dauerhaft ab und leert über store_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 einmal new_session; turn-übergreifende Wiederverwendung ließe das x-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_history gibt bei prompt.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_auth läuft ebenfalls über current_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.