Codex Handbook
codex-api/src/endpoint/responses_websocket.rs 1024 lines
use crate::auth::SharedAuthProvider;use crate::common::ResponseEvent;use crate::common::ResponseStream;use crate::common::ResponsesWsRequest;use crate::error::ApiError;use crate::provider::Provider;use crate::rate_limits::parse_rate_limit_event;use crate::sse::ResponsesStreamEvent;use crate::sse::process_responses_event;use crate::telemetry::WebsocketTelemetry;use codex_client::TransportError;use codex_client::maybe_build_rustls_client_config_with_custom_ca;use codex_utils_rustls_provider::ensure_rustls_crypto_provider;use futures::SinkExt;use futures::StreamExt;use http::HeaderMap;use http::HeaderName;use http::HeaderValue;use http::StatusCode;use serde::Deserialize;use serde_json::Value;use serde_json::map::Map as JsonMap;use std::sync::Arc;use std::sync::OnceLock;use std::time::Duration;use tokio::net::TcpStream;use tokio::sync::Mutex;use tokio::sync::mpsc;use tokio::sync::oneshot;use tokio::time::Instant;use tokio_tungstenite::MaybeTlsStream;use tokio_tungstenite::WebSocketStream;use tokio_tungstenite::connect_async_tls_with_config;use tokio_tungstenite::tungstenite::Error as WsError;use tokio_tungstenite::tungstenite::Message;use tokio_tungstenite::tungstenite::client::IntoClientRequest;use tokio_tungstenite::tungstenite::protocol::CloseFrame;use tracing::Instrument;use tracing::Span;use tracing::debug;use tracing::error;use tracing::info;use tracing::instrument;use tracing::trace;use tungstenite::extensions::ExtensionsConfig;use tungstenite::extensions::compression::deflate::DeflateConfig;use tungstenite::protocol::WebSocketConfig;use url::Url;struct WsStream {    tx_command: mpsc::Sender<WsCommand>,    rx_message: mpsc::UnboundedReceiver<Result<Message, WsError>>,    pump_task: tokio::task::JoinHandle<()>,}enum WsCommand {    Send {        message: Message,        tx_result: oneshot::Sender<Result<(), WsError>>,    },}impl WsStream {    fn new(inner: WebSocketStream<MaybeTlsStream<TcpStream>>) -> Self {        let (tx_command, mut rx_command) = mpsc::channel::<WsCommand>(32);        let (tx_message, rx_message) = mpsc::unbounded_channel::<Result<Message, WsError>>();        let pump_task = tokio::spawn(async move {            let mut inner = inner;            loop {                tokio::select! {                    command = rx_command.recv() => {                        let Some(command) = command else {                            break;                        };                        match command {                            WsCommand::Send { message, tx_result } => {                                let result = inner.send(message).await;                                let should_break = result.is_err();                                let _ = tx_result.send(result);                                if should_break {                                    break;                                }                            }                        }                    }                    message = inner.next() => {                        let Some(message) = message else {                            break;                        };                        match message {                            Ok(Message::Ping(payload)) => {                                if let Err(err) = inner.send(Message::Pong(payload)).await {                                    let _ = tx_message.send(Err(err));                                    break;                                }                            }                            Ok(Message::Pong(_)) => {}                            Ok(message @ (Message::Text(_)                            | Message::Binary(_)                            | Message::Close(_)                            | Message::Frame(_))) => {                                let is_close = matches!(message, Message::Close(_));                                if tx_message.send(Ok(message)).is_err() {                                    break;                                }                                if is_close {                                    break;                                }                            }                            Err(err) => {                                let _ = tx_message.send(Err(err));                                break;                            }                        }                    }                }            }        });        Self {            tx_command,            rx_message,            pump_task,        }    }    async fn request(        &self,        make_command: impl FnOnce(oneshot::Sender<Result<(), WsError>>) -> WsCommand,    ) -> Result<(), WsError> {        let (tx_result, rx_result) = oneshot::channel();        if self.tx_command.send(make_command(tx_result)).await.is_err() {            return Err(WsError::ConnectionClosed);        }        rx_result.await.unwrap_or(Err(WsError::ConnectionClosed))    }    async fn send(&self, message: Message) -> Result<(), WsError> {        self.request(|tx_result| WsCommand::Send { message, tx_result })            .await    }    async fn next(&mut self) -> Option<Result<Message, WsError>> {        self.rx_message.recv().await    }}impl Drop for WsStream {    fn drop(&mut self) {        self.pump_task.abort();    }}const X_CODEX_TURN_STATE_HEADER: &str = "x-codex-turn-state";const X_MODELS_ETAG_HEADER: &str = "x-models-etag";const X_REASONING_INCLUDED_HEADER: &str = "x-reasoning-included";const OPENAI_MODEL_HEADER: &str = "openai-model";const WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE: &str = "websocket_connection_limit_reached";const WEBSOCKET_CONNECTION_LIMIT_REACHED_MESSAGE: &str = "Responses websocket connection limit reached (60 minutes). Create a new websocket connection to continue.";pub struct ResponsesWebsocketConnection {    stream: Arc<Mutex<Option<WsStream>>>,    // TODO (pakrym): is this the right place for timeout?    idle_timeout: Duration,    server_reasoning_included: bool,    models_etag: Option<String>,    server_model: Option<String>,    telemetry: Option<Arc<dyn WebsocketTelemetry>>,}impl std::fmt::Debug for ResponsesWebsocketConnection {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        f.debug_struct("ResponsesWebsocketConnection")            .field("stream", &"<ws-stream>")            .field("idle_timeout", &self.idle_timeout)            .field("server_reasoning_included", &self.server_reasoning_included)            .field("models_etag", &self.models_etag)            .field("server_model", &self.server_model)            .field("telemetry", &self.telemetry.as_ref().map(|_| "<telemetry>"))            .finish()    }}impl ResponsesWebsocketConnection {    fn new(        stream: WsStream,        idle_timeout: Duration,        server_reasoning_included: bool,        models_etag: Option<String>,        server_model: Option<String>,        telemetry: Option<Arc<dyn WebsocketTelemetry>>,    ) -> Self {        Self {            stream: Arc::new(Mutex::new(Some(stream))),            idle_timeout,            server_reasoning_included,            models_etag,            server_model,            telemetry,        }    }    pub async fn is_closed(&self) -> bool {        self.stream.lock().await.is_none()    }    #[instrument(        name = "responses_websocket.stream_request",        level = "info",        skip_all,        fields(transport = "responses_websocket", api.path = "responses")    )]    pub async fn stream_request(        &self,        request: ResponsesWsRequest,        connection_reused: bool,        turn_state: Option<Arc<OnceLock<String>>>,    ) -> Result<ResponseStream, ApiError> {        let (tx_event, rx_event) =            mpsc::channel::<std::result::Result<ResponseEvent, ApiError>>(1600);        let stream = Arc::clone(&self.stream);        let idle_timeout = self.idle_timeout;        let server_reasoning_included = self.server_reasoning_included;        let models_etag = self.models_etag.clone();        let server_model = self.server_model.clone();        let telemetry = self.telemetry.clone();        let request_text = serialize_websocket_request(&request)?;        let current_span = Span::current();        tokio::spawn(            #[expect(                clippy::await_holding_invalid_type,                reason = "the guard serializes exclusive use of the websocket stream for the lifetime of the response stream"            )]            async move {                if let Some(model) = server_model {                    let _ = tx_event.send(Ok(ResponseEvent::ServerModel(model))).await;                }                if let Some(etag) = models_etag {                    let _ = tx_event.send(Ok(ResponseEvent::ModelsEtag(etag))).await;                }                if server_reasoning_included {                    let _ = tx_event                        .send(Ok(ResponseEvent::ServerReasoningIncluded(true)))                        .await;                }                let mut guard = stream.lock().await;                let result = {                    let Some(ws_stream) = guard.as_mut() else {                        let _ = tx_event                            .send(Err(ApiError::Stream(                                "websocket connection is closed".to_string(),                            )))                            .await;                        return;                    };                    run_websocket_response_stream(                        ws_stream,                        tx_event.clone(),                        request_text,                        idle_timeout,                        telemetry,                        connection_reused,                        turn_state.as_deref(),                    )                    .await                };                if let Err(err) = result {                    // A terminal stream error should reach the caller immediately. Waiting for a                    // graceful close handshake here can stall indefinitely and mask the error.                    let failed_stream = guard.take();                    drop(guard);                    drop(failed_stream);                    let _ = tx_event.send(Err(err)).await;                }            }            .instrument(current_span),        );        Ok(ResponseStream {            rx_event,            upstream_request_id: None,        })    }}/// Client for connecting to the Responses WebSocket endpoint for one provider.pub struct ResponsesWebsocketClient {    provider: Provider,    auth: SharedAuthProvider,}/// Close frame information captured by a handshake probe.#[derive(Debug, Clone, PartialEq, Eq)]pub struct ResponsesWebsocketClose {    /// WebSocket close code returned by the server.    pub code: String,    /// Human-readable close reason returned by the server.    pub reason: String,}/// Result of a handshake-only Responses WebSocket probe.#[derive(Debug, Clone, PartialEq, Eq)]pub struct ResponsesWebsocketProbe {    /// Redacted by callers before displaying or serializing support reports.    pub url: String,    /// HTTP status returned by the successful WebSocket upgrade.    pub status: StatusCode,    /// Whether the server reported reasoning support in the upgrade response.    pub reasoning_included: bool,    /// Whether the server returned a model catalog ETag in the upgrade response.    pub models_etag_present: bool,    /// Whether the server returned a server-selected model in the upgrade response.    pub server_model_present: bool,    /// Close frame received immediately after upgrade, when one arrives quickly.    pub immediate_close: Option<ResponsesWebsocketClose>,}impl ResponsesWebsocketClient {    /// Creates a Responses WebSocket client for an already-resolved provider and auth source.    pub fn new(provider: Provider, auth: SharedAuthProvider) -> Self {        Self { provider, auth }    }    #[instrument(        name = "responses_websocket.connect",        level = "info",        skip_all,        fields(transport = "responses_websocket", api.path = "responses")    )]    pub async fn connect(        &self,        extra_headers: HeaderMap,        default_headers: HeaderMap,        turn_state: Option<Arc<OnceLock<String>>>,        telemetry: Option<Arc<dyn WebsocketTelemetry>>,    ) -> Result<ResponsesWebsocketConnection, ApiError> {        let ws_url = self            .provider            .websocket_url_for_path("responses")            .map_err(|err| ApiError::Stream(format!("failed to build websocket URL: {err}")))?;        let mut headers =            merge_request_headers(&self.provider.headers, extra_headers, default_headers);        self.auth.add_auth_headers(&mut headers);        let (stream, _status, server_reasoning_included, models_etag, server_model) =            connect_websocket(ws_url, headers, turn_state.clone()).await?;        Ok(ResponsesWebsocketConnection::new(            stream,            self.provider.stream_idle_timeout,            server_reasoning_included,            models_etag,            server_model,            telemetry,        ))    }    /// Opens a WebSocket connection long enough to validate the upgrade response.    ///    /// The probe uses the same URL construction, headers, authentication, TLS,    /// and custom-CA path as a real Responses WebSocket connection, but it does    /// not send a request frame. After the HTTP 101 upgrade succeeds, it waits    /// briefly for an immediate server close frame so diagnostics can distinguish    /// a usable connection from a policy rejection that closes right away.    pub async fn probe_handshake(        &self,        extra_headers: HeaderMap,        default_headers: HeaderMap,        immediate_close_timeout: Duration,    ) -> Result<ResponsesWebsocketProbe, ApiError> {        let ws_url = self            .provider            .websocket_url_for_path("responses")            .map_err(|err| ApiError::Stream(format!("failed to build websocket URL: {err}")))?;        let mut headers =            merge_request_headers(&self.provider.headers, extra_headers, default_headers);        self.auth.add_auth_headers(&mut headers);        let (mut stream, status, reasoning_included, models_etag, server_model) =            connect_websocket(ws_url.clone(), headers, /*turn_state*/ None).await?;        let immediate_close = tokio::time::timeout(immediate_close_timeout, stream.next())            .await            .ok()            .flatten()            .transpose()            .map_err(|err| {                ApiError::Stream(format!("failed to read websocket probe event: {err}"))            })?            .and_then(immediate_close_from_message);        Ok(ResponsesWebsocketProbe {            url: ws_url.to_string(),            status,            reasoning_included,            models_etag_present: models_etag.is_some(),            server_model_present: server_model.is_some(),            immediate_close,        })    }}fn immediate_close_from_message(message: Message) -> Option<ResponsesWebsocketClose> {    let Message::Close(frame) = message else {        return None;    };    frame.map(close_frame_to_probe)}fn close_frame_to_probe(frame: CloseFrame) -> ResponsesWebsocketClose {    ResponsesWebsocketClose {        code: frame.code.to_string(),        reason: frame.reason.to_string(),    }}fn merge_request_headers(    provider_headers: &HeaderMap,    extra_headers: HeaderMap,    default_headers: HeaderMap,) -> HeaderMap {    let mut headers = provider_headers.clone();    headers.extend(extra_headers);    for (name, value) in &default_headers {        if let http::header::Entry::Vacant(entry) = headers.entry(name) {            entry.insert(value.clone());        }    }    headers}async fn connect_websocket(    url: Url,    headers: HeaderMap,    turn_state: Option<Arc<OnceLock<String>>>,) -> Result<(WsStream, StatusCode, bool, Option<String>, Option<String>), ApiError> {    ensure_rustls_crypto_provider();    info!("connecting to websocket: {url}");    let mut request = url        .as_str()        .into_client_request()        .map_err(|err| ApiError::Stream(format!("failed to build websocket request: {err}")))?;    request.headers_mut().extend(headers);    // Secure websocket traffic needs the same custom-CA policy as reqwest-based HTTPS traffic.    // If a Codex-specific CA bundle is configured, build an explicit rustls connector so this    // websocket path does not fall back to tungstenite's default native-roots-only behavior.    let connector = maybe_build_rustls_client_config_with_custom_ca()        .map_err(|err| ApiError::Stream(format!("failed to configure websocket TLS: {err}")))?        .map(tokio_tungstenite::Connector::Rustls);    let response = connect_async_tls_with_config(        request,        Some(websocket_config()),        false, // `false` means "do not disable Nagle", which is tungstenite's recommended default.        connector,    )    .await;    let (stream, response) = match response {        Ok((stream, response)) => {            info!(                "successfully connected to websocket: {url}, headers: {:?}",                response.headers()            );            (stream, response)        }        Err(err) => {            error!("failed to connect to websocket: {err}, url: {url}");            return Err(map_ws_error(err, &url));        }    };    let reasoning_included = response.headers().contains_key(X_REASONING_INCLUDED_HEADER);    let models_etag = response        .headers()        .get(X_MODELS_ETAG_HEADER)        .and_then(|value| value.to_str().ok())        .map(ToString::to_string);    let server_model = response        .headers()        .get(OPENAI_MODEL_HEADER)        .and_then(|value| value.to_str().ok())        .map(ToString::to_string);    if let Some(turn_state) = turn_state        && let Some(header_value) = response            .headers()            .get(X_CODEX_TURN_STATE_HEADER)            .and_then(|value| value.to_str().ok())    {        let _ = turn_state.set(header_value.to_string());    }    Ok((        WsStream::new(stream),        response.status(),        reasoning_included,        models_etag,        server_model,    ))}fn websocket_config() -> WebSocketConfig {    let mut extensions = ExtensionsConfig::default();    extensions.permessage_deflate = Some(DeflateConfig::default());    let mut config = WebSocketConfig::default();    config.extensions = extensions;    config}fn map_ws_error(err: WsError, url: &Url) -> ApiError {    match err {        WsError::Http(response) => {            let status = response.status();            let headers = response.headers().clone();            let body = response                .body()                .as_ref()                .and_then(|bytes| String::from_utf8(bytes.clone()).ok());            ApiError::Transport(TransportError::Http {                status,                url: Some(url.to_string()),                headers: Some(headers),                body,            })        }        WsError::ConnectionClosed | WsError::AlreadyClosed => {            ApiError::Stream("websocket closed".to_string())        }        WsError::Io(err) => ApiError::Transport(TransportError::Network(err.to_string())),        other => ApiError::Transport(TransportError::Network(other.to_string())),    }}#[derive(Debug, Deserialize)]struct WrappedWebsocketError {    code: Option<String>,    message: Option<String>,}#[derive(Debug, Deserialize)]struct WrappedWebsocketErrorEvent {    #[serde(rename = "type")]    kind: String,    #[serde(alias = "status_code")]    status: Option<u16>,    #[serde(default)]    error: Option<WrappedWebsocketError>,    #[serde(default)]    headers: Option<JsonMap<String, Value>>,}fn parse_wrapped_websocket_error_event(payload: &str) -> Option<WrappedWebsocketErrorEvent> {    let event: WrappedWebsocketErrorEvent = serde_json::from_str(payload).ok()?;    if event.kind != "error" {        return None;    }    Some(event)}fn map_wrapped_websocket_error_event(    event: WrappedWebsocketErrorEvent,    original_payload: String,) -> Option<ApiError> {    let WrappedWebsocketErrorEvent {        status,        error,        headers,        ..    } = event;    if let Some(error) = error.as_ref()        && let Some(code) = error.code.as_deref()        && code == WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE    {        return Some(ApiError::Retryable {            message: error                .message                .clone()                .unwrap_or_else(|| WEBSOCKET_CONNECTION_LIMIT_REACHED_MESSAGE.to_string()),            delay: None,        });    }    let status = StatusCode::from_u16(status?).ok()?;    if status.is_success() {        return None;    }    Some(ApiError::Transport(TransportError::Http {        status,        url: None,        headers: headers.map(json_headers_to_http_headers),        body: Some(original_payload),    }))}fn json_headers_to_http_headers(headers: JsonMap<String, Value>) -> HeaderMap {    let mut mapped = HeaderMap::new();    for (name, value) in headers {        let Ok(header_name) = HeaderName::from_bytes(name.as_bytes()) else {            continue;        };        let Some(header_value) = json_header_value(value) else {            continue;        };        mapped.insert(header_name, header_value);    }    mapped}fn json_header_value(value: Value) -> Option<HeaderValue> {    let value = match value {        Value::String(value) => value,        Value::Number(value) => value.to_string(),        Value::Bool(value) => value.to_string(),        _ => return None,    };    HeaderValue::from_str(&value).ok()}async fn run_websocket_response_stream(    ws_stream: &mut WsStream,    tx_event: mpsc::Sender<std::result::Result<ResponseEvent, ApiError>>,    request_text: String,    idle_timeout: Duration,    telemetry: Option<Arc<dyn WebsocketTelemetry>>,    connection_reused: bool,    turn_state: Option<&OnceLock<String>>,) -> Result<(), ApiError> {    let mut last_server_model: Option<String> = None;    send_websocket_request(        ws_stream,        request_text,        idle_timeout,        telemetry.as_ref(),        connection_reused,    )    .await?;    loop {        let poll_start = Instant::now();        let response = tokio::time::timeout(idle_timeout, ws_stream.next())            .await            .map_err(|_| ApiError::Stream("idle timeout waiting for websocket".into()));        if let Some(t) = telemetry.as_ref() {            t.on_ws_event(&response, poll_start.elapsed());        }        let message = match response {            Ok(Some(Ok(msg))) => msg,            Ok(Some(Err(err))) => {                return Err(ApiError::Stream(err.to_string()));            }            Ok(None) => {                return Err(ApiError::Stream(                    "stream closed before response.completed".into(),                ));            }            Err(err) => {                return Err(err);            }        };        match message {            Message::Text(text) => {                trace!("websocket event: {text}");                if let Some(wrapped_error) = parse_wrapped_websocket_error_event(&text)                    && let Some(error) =                        map_wrapped_websocket_error_event(wrapped_error, text.to_string())                {                    return Err(error);                }                let event = match serde_json::from_str::<ResponsesStreamEvent>(&text) {                    Ok(event) => event,                    Err(err) => {                        debug!("failed to parse websocket event: {err}, data: {text}");                        continue;                    }                };                if let Some(response_turn_state) = event.turn_state()                    && let Some(turn_state) = turn_state                {                    let _ = turn_state.set(response_turn_state);                }                let model_verifications = event.model_verifications();                let turn_moderation_metadata = event.turn_moderation_metadata();                if event.kind() == "codex.rate_limits" {                    if let Some(snapshot) = parse_rate_limit_event(&text) {                        let _ = tx_event.send(Ok(ResponseEvent::RateLimits(snapshot))).await;                    }                    continue;                }                if let Some(model) = event.response_model()                    && last_server_model.as_deref() != Some(model.as_str())                {                    let _ = tx_event                        .send(Ok(ResponseEvent::ServerModel(model.clone())))                        .await;                    last_server_model = Some(model);                }                if let Some(verifications) = model_verifications                    && tx_event                        .send(Ok(ResponseEvent::ModelVerifications(verifications)))                        .await                        .is_err()                {                    return Err(ApiError::Stream(                        "response event consumer dropped".to_string(),                    ));                }                if let Some(metadata) = turn_moderation_metadata                    && tx_event                        .send(Ok(ResponseEvent::TurnModerationMetadata(metadata)))                        .await                        .is_err()                {                    return Err(ApiError::Stream(                        "response event consumer dropped".to_string(),                    ));                }                match process_responses_event(event) {                    Ok(Some(event)) => {                        let is_completed = matches!(event, ResponseEvent::Completed { .. });                        let _ = tx_event.send(Ok(event)).await;                        if is_completed {                            break;                        }                    }                    Ok(None) => {}                    Err(error) => {                        return Err(error.into_api_error());                    }                }            }            Message::Binary(_) => {                return Err(ApiError::Stream("unexpected binary websocket event".into()));            }            Message::Close(_) => {                return Err(ApiError::Stream(                    "websocket closed by server before response.completed".into(),                ));            }            Message::Frame(_) => {}            Message::Ping(_) | Message::Pong(_) => {}        }    }    Ok(())}async fn send_websocket_request(    ws_stream: &WsStream,    request_text: String,    idle_timeout: Duration,    telemetry: Option<&Arc<dyn WebsocketTelemetry>>,    connection_reused: bool,) -> Result<(), ApiError> {    trace!("websocket request: {request_text}");    let request_start = Instant::now();    let result = tokio::time::timeout(        idle_timeout,        ws_stream.send(Message::Text(request_text.into())),    )    .await    .map_err(|_| ApiError::Stream("idle timeout sending websocket request".into()))    .and_then(|result| {        result.map_err(|err| ApiError::Stream(format!("failed to send websocket request: {err}")))    });    if let Some(t) = telemetry.as_ref() {        t.on_ws_request(            request_start.elapsed(),            result.as_ref().err(),            connection_reused,        );    }    result?;    Ok(())}fn serialize_websocket_request(request: &ResponsesWsRequest) -> Result<String, ApiError> {    serde_json::to_string(request)        .map_err(|err| ApiError::Stream(format!("failed to encode websocket request: {err}")))}#[cfg(test)]mod tests {    use super::*;    use crate::common::ResponseCreateWsRequest;    use codex_protocol::models::ContentItem;    use codex_protocol::models::ResponseItem;    use pretty_assertions::assert_eq;    use serde_json::json;    use std::collections::HashMap;    #[test]    fn direct_serialization_preserves_websocket_request_payload() {        let request = ResponsesWsRequest::ResponseCreate(ResponseCreateWsRequest {            model: "gpt-test".to_string(),            instructions: "Use the available tools.".to_string(),            previous_response_id: Some("resp-1".to_string()),            input: vec![ResponseItem::Message {                id: Some("msg-1".to_string()),                role: "user".to_string(),                content: vec![ContentItem::InputText {                    text: "hello".to_string(),                }],                phase: None,                metadata: None,            }],            tools: vec![json!({                "type": "function",                "name": "lookup",                "parameters": {"type": "object"}            })],            tool_choice: "auto".to_string(),            parallel_tool_calls: true,            reasoning: None,            store: false,            stream: true,            include: vec!["reasoning.encrypted_content".to_string()],            service_tier: Some("priority".to_string()),            prompt_cache_key: Some("cache-key".to_string()),            text: None,            generate: Some(false),            client_metadata: Some(HashMap::from([(                "traceparent".to_string(),                "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01".to_string(),            )])),        });        let previous_payload = serde_json::to_value(&request).expect("serialize previous payload");        let request_text =            serialize_websocket_request(&request).expect("serialize websocket request");        let wire_payload =            serde_json::from_str::<Value>(&request_text).expect("parse websocket request");        assert_eq!(wire_payload, previous_payload);    }    #[test]    fn websocket_config_enables_permessage_deflate() {        let config = websocket_config();        assert!(config.extensions.permessage_deflate.is_some());    }    #[test]    fn parse_wrapped_websocket_error_event_maps_to_transport_http() {        let payload = json!({            "type": "error",            "status": 429,            "error": {                "type": "usage_limit_reached",                "message": "The usage limit has been reached",                "plan_type": "pro",                "resets_at": 1738888888            },            "headers": {                "x-codex-primary-used-percent": "100.0",                "x-codex-primary-window-minutes": 15            }        })        .to_string();        let wrapped_error = parse_wrapped_websocket_error_event(&payload)            .expect("expected websocket error payload to be parsed");        let api_error = map_wrapped_websocket_error_event(wrapped_error, payload)            .expect("expected websocket error payload to map to ApiError");        let ApiError::Transport(TransportError::Http {            status,            headers,            body,            ..        }) = api_error        else {            panic!("expected ApiError::Transport(Http)");        };        assert_eq!(status, StatusCode::TOO_MANY_REQUESTS);        let headers = headers.expect("expected headers");        assert_eq!(            headers                .get("x-codex-primary-used-percent")                .and_then(|value| value.to_str().ok()),            Some("100.0")        );        assert_eq!(            headers                .get("x-codex-primary-window-minutes")                .and_then(|value| value.to_str().ok()),            Some("15")        );        let body = body.expect("expected body");        assert!(body.contains("usage_limit_reached"));        assert!(body.contains("The usage limit has been reached"));    }    #[test]    fn parse_wrapped_websocket_error_event_ignores_non_error_payloads() {        let payload = json!({            "type": "response.created",            "response": {                "id": "resp-1"            }        })        .to_string();        let wrapped_error = parse_wrapped_websocket_error_event(&payload);        assert!(wrapped_error.is_none());    }    #[test]    fn parse_wrapped_websocket_error_event_with_status_maps_invalid_request() {        let payload = json!({            "type": "error",            "status": 400,            "error": {                "type": "invalid_request_error",                "message": "Model does not support image inputs"            }        })        .to_string();        let wrapped_error = parse_wrapped_websocket_error_event(&payload)            .expect("expected websocket error payload to be parsed");        let api_error = map_wrapped_websocket_error_event(wrapped_error, payload)            .expect("expected websocket error payload to map to ApiError");        let ApiError::Transport(TransportError::Http { status, body, .. }) = api_error else {            panic!("expected ApiError::Transport(Http)");        };        assert_eq!(status, StatusCode::BAD_REQUEST);        let body = body.expect("expected body");        assert!(body.contains("invalid_request_error"));        assert!(body.contains("Model does not support image inputs"));    }    #[test]    fn parse_wrapped_websocket_error_event_with_connection_limit_maps_retryable() {        let payload = json!({            "type": "error",            "status": 400,            "error": {                "type": "invalid_request_error",                "code": "websocket_connection_limit_reached",                "message": "Responses websocket connection limit reached (60 minutes). Create a new websocket connection to continue."            }        })        .to_string();        let wrapped_error = parse_wrapped_websocket_error_event(&payload)            .expect("expected websocket error payload to be parsed");        let api_error = map_wrapped_websocket_error_event(wrapped_error, payload)            .expect("expected websocket error payload to map to ApiError");        let ApiError::Retryable { message, delay } = api_error else {            panic!("expected ApiError::Retryable");        };        assert_eq!(message, WEBSOCKET_CONNECTION_LIMIT_REACHED_MESSAGE);        assert_eq!(delay, None);    }    #[test]    fn parse_wrapped_websocket_error_event_without_status_is_not_mapped() {        let payload = json!({            "type": "error",            "error": {                "type": "usage_limit_reached",                "message": "The usage limit has been reached"            },            "headers": {                "x-codex-primary-used-percent": "100.0",                "x-codex-primary-window-minutes": 15            }        })        .to_string();        let wrapped_error = parse_wrapped_websocket_error_event(&payload)            .expect("expected websocket error payload to be parsed");        let api_error = map_wrapped_websocket_error_event(wrapped_error, payload);        assert!(api_error.is_none());    }    #[test]    fn merge_request_headers_matches_http_precedence() {        let mut provider_headers = HeaderMap::new();        provider_headers.insert(            "originator",            HeaderValue::from_static("provider-originator"),        );        provider_headers.insert("x-priority", HeaderValue::from_static("provider"));        let mut extra_headers = HeaderMap::new();        extra_headers.insert("x-priority", HeaderValue::from_static("extra"));        let mut default_headers = HeaderMap::new();        default_headers.insert("originator", HeaderValue::from_static("default-originator"));        default_headers.insert("x-priority", HeaderValue::from_static("default"));        default_headers.insert("x-default-only", HeaderValue::from_static("default-only"));        let merged = merge_request_headers(&provider_headers, extra_headers, default_headers);        assert_eq!(            merged.get("originator"),            Some(&HeaderValue::from_static("provider-originator"))        );        assert_eq!(            merged.get("x-priority"),            Some(&HeaderValue::from_static("extra"))        );        assert_eq!(            merged.get("x-default-only"),            Some(&HeaderValue::from_static("default-only"))        );    }}