Skip to main content

claude_codex/providers/codex/
mod.rs

1pub mod auth;
2pub mod chat_completions;
3pub mod client;
4pub mod compaction;
5pub mod continuation;
6pub mod count_tokens;
7pub(crate) mod events;
8pub mod images;
9pub mod native;
10pub mod request_summary;
11pub mod search;
12pub mod transcription;
13pub mod translate;
14pub mod websocket;
15
16use async_trait::async_trait;
17use axum::Json;
18use axum::body::Body;
19use axum::response::{IntoResponse, Response};
20use bytes::Bytes;
21use http::StatusCode;
22use std::sync::Arc;
23use std::time::{Duration, Instant};
24
25use crate::anthropic::error::json_error;
26use crate::anthropic::schema::{CountTokensResponse, MessagesRequest};
27use crate::anthropic::sse::parse_sse_events;
28use crate::config;
29use crate::logging::create_logger;
30use crate::monitor::usage_from_anthropic_sse;
31use crate::provider::{CliHandlers, Provider, RequestContext};
32use crate::registry;
33use crate::request_identity::ConversationIdentity;
34use crate::retry::{compute_backoff_delay, sleep};
35
36use self::auth::token_store::file_store;
37use self::client::CodexHttpClient;
38use self::compaction::{
39    CompactionAttempt, abort_compaction_attempt, activate_compaction, apply_compaction_replay,
40    begin_compaction, request_compaction, store_compaction,
41};
42use self::continuation::{
43    ContinuationReservation, abort_continuation_for_owner, continuation_candidate_for_owner,
44    record_continuation_for_owner,
45};
46use self::count_tokens::count_translated_tokens;
47use self::translate::accumulate::accumulate_response_with_traffic;
48use self::translate::live_stream::LiveStreamTranslator;
49use self::translate::model_allowlist::{
50    assert_allowed_model, full_lane_web_search_model, resolve_model_request_with_config_override,
51    uses_responses_lite,
52};
53use self::translate::reducer::finish_metadata_from_upstream;
54use self::translate::request::{
55    TranslateOptions, has_hosted_web_search, is_compact_messages_request, translate_request,
56};
57
58const MAX_RETRYABLE_LIVE_STREAM_RETRIES: u32 = 10;
59const MAX_EMPTY_COMPLETION_RETRIES: u32 = 10;
60const EMPTY_CODEX_COMPLETION_DETAIL: &str = "empty_codex_completion";
61const LIVE_STREAM_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(15);
62use self::translate::stream::translate_stream_bytes_with_traffic;
63
64// ---------------------------------------------------------------------------
65// Provider
66// ---------------------------------------------------------------------------
67
68pub(crate) fn clear_session_compaction(session_id: &str) {
69    compaction::clear_compaction(session_id);
70}
71
72pub struct CodexProvider {
73    client: Arc<CodexHttpClient>,
74}
75
76impl Default for CodexProvider {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl CodexProvider {
83    pub fn new() -> Self {
84        Self {
85            client: Arc::new(CodexHttpClient::new()),
86        }
87    }
88}
89
90impl CodexProvider {
91    async fn handle_messages_inner(
92        &self,
93        body: MessagesRequest,
94        ctx: RequestContext,
95        conversation_identity: Option<ConversationIdentity>,
96    ) -> Response {
97        let message_id = format!("msg_{}", uuid::Uuid::new_v4().to_string().replace('-', ""));
98        let want_stream = body.stream;
99        let model = body.model.as_deref().unwrap_or("gpt-5.6-sol");
100
101        let mut resolved =
102            resolve_model_request_with_config_override(model, !body.bypass_provider_model_override);
103        if let Err(e) = assert_allowed_model(&resolved.model) {
104            return json_error(
105                StatusCode::BAD_REQUEST,
106                "invalid_request_error",
107                format!(
108                    "Model \"{model}\" resolves to unsupported model \"{}\"",
109                    e.model
110                ),
111            );
112        }
113        if search::is_standalone_search_request(&body) {
114            if let Some(monitor) = ctx.monitor.as_ref() {
115                monitor.model_resolved(&ctx.req_id, &resolved.model);
116            }
117            let (search_request, query) = match search::build_search_request(
118                &body,
119                &resolved.model,
120                ctx.session_id.as_deref(),
121            ) {
122                Ok(request) => request,
123                Err(error) => {
124                    return json_error(
125                        StatusCode::BAD_REQUEST,
126                        "invalid_request_error",
127                        error.to_string(),
128                    );
129                }
130            };
131            let log = create_logger("codex");
132            let started_at = Instant::now();
133            log.info(
134                "codex_standalone_search_started",
135                Some(serde_json::Map::from_iter([
136                    ("reqId".to_string(), serde_json::json!(&ctx.req_id)),
137                    ("model".to_string(), serde_json::json!(&resolved.model)),
138                    ("stream".to_string(), serde_json::json!(want_stream)),
139                ])),
140            );
141            if let Some(monitor) = ctx.monitor.as_ref() {
142                monitor.upstream_started(&ctx.req_id);
143            }
144            let search_response = match self.client.post_search(&search_request, &ctx).await {
145                Ok(response) => response,
146                Err(error) => {
147                    log.warn(
148                        "codex_standalone_search_failed",
149                        Some(serde_json::Map::from_iter([
150                            ("reqId".to_string(), serde_json::json!(&ctx.req_id)),
151                            ("model".to_string(), serde_json::json!(&resolved.model)),
152                            ("status".to_string(), serde_json::json!(error.status)),
153                            (
154                                "ms".to_string(),
155                                serde_json::json!(started_at.elapsed().as_millis()),
156                            ),
157                        ])),
158                    );
159                    return map_codex_error_to_response(&error);
160                }
161            };
162            log.info(
163                "codex_standalone_search_completed",
164                Some(serde_json::Map::from_iter([
165                    ("reqId".to_string(), serde_json::json!(&ctx.req_id)),
166                    ("model".to_string(), serde_json::json!(&resolved.model)),
167                    (
168                        "resultCount".to_string(),
169                        serde_json::json!(search_response.results.as_ref().map(Vec::len)),
170                    ),
171                    (
172                        "ms".to_string(),
173                        serde_json::json!(started_at.elapsed().as_millis()),
174                    ),
175                ])),
176            );
177            let input_tokens = search::search_request_input_tokens(&search_request);
178            let output_tokens = search::search_response_output_tokens(&search_response);
179            if let Some(monitor) = ctx.monitor.as_ref() {
180                monitor.usage_updated(&ctx.req_id, Some(input_tokens), Some(output_tokens));
181            }
182            return search::anthropic_search_response(
183                &search_response,
184                &query,
185                &message_id,
186                model,
187                want_stream,
188                input_tokens,
189                ctx.traffic.as_deref(),
190            );
191        }
192        let use_responses_lite = apply_model_lane_for_request(&mut resolved.model, &body);
193        if let Some(monitor) = ctx.monitor.as_ref() {
194            monitor.model_resolved(&ctx.req_id, &resolved.model);
195        }
196
197        let mut translated = match translate_request(
198            &body,
199            TranslateOptions {
200                session_id: ctx.session_id.clone(),
201                service_tier: resolved.service_tier.clone(),
202                model: resolved.model.clone(),
203                use_responses_lite,
204            },
205        ) {
206            Ok(t) => t,
207            Err(e) => {
208                return json_error(
209                    StatusCode::BAD_REQUEST,
210                    "invalid_request_error",
211                    e.to_string(),
212                );
213            }
214        };
215
216        let compact_boundary = is_compact_messages_request(&body);
217        let server_compaction_enabled = config::codex_server_compaction();
218        let mut compaction_attempt = None;
219        if !server_compaction_enabled && let Some(session_id) = ctx.session_id.as_deref() {
220            compaction::clear_compaction(session_id);
221        }
222        if server_compaction_enabled
223            && compact_boundary
224            && let Some(session_id) = ctx.session_id.as_deref()
225        {
226            let attempt = begin_compaction(session_id, &translated.model);
227            compaction_attempt = Some(attempt);
228            log_compaction_event(
229                "server_compaction_triggered",
230                &ctx,
231                translated.input.len(),
232                None,
233            );
234            if let Some(monitor) = ctx.monitor.as_ref() {
235                monitor.compaction_started(&ctx.req_id);
236            }
237            let mut compaction_ctx = ctx.clone();
238            compaction_ctx.monitor = None;
239            match request_compaction(self.client.as_ref(), &translated, &compaction_ctx).await {
240                Ok(native_history) => {
241                    if store_compaction(session_id, attempt, native_history) {
242                        log_compaction_event(
243                            "server_compaction_completed",
244                            &ctx,
245                            translated.input.len(),
246                            None,
247                        );
248                    } else {
249                        log_compaction_event(
250                            "server_compaction_failed",
251                            &ctx,
252                            translated.input.len(),
253                            Some("compaction state was superseded or exceeded the in-memory limit"),
254                        );
255                    }
256                }
257                Err(error) => {
258                    abort_compaction_attempt(Some(session_id), Some(attempt));
259                    log_compaction_event(
260                        "server_compaction_failed",
261                        &ctx,
262                        translated.input.len(),
263                        Some(&error.to_string()),
264                    );
265                }
266            }
267        } else if server_compaction_enabled
268            && !compact_boundary
269            && let Some(replay) = apply_compaction_replay(ctx.session_id.as_deref(), &translated)
270        {
271            translated = replay.request;
272            compaction_attempt = Some(replay.attempt);
273        }
274
275        // Check continuation
276        let previous_response_id_enabled = config::codex_previous_response_id();
277        let continuation = continuation_candidate_for_owner(
278            conversation_identity.as_ref(),
279            &translated,
280            previous_response_id_enabled,
281        );
282        let turn_id = continuation.turn_id();
283        let configured_transport = config::codex_transport();
284        let transport = configured_transport.as_str();
285        let upstream_started_at = Instant::now();
286        let log = create_logger("codex");
287        let req_id = ctx.req_id.clone();
288        log.info(
289            "codex_upstream_request_started",
290            Some(serde_json::Map::from_iter([
291                ("reqId".to_string(), serde_json::json!(&req_id)),
292                ("transport".to_string(), serde_json::json!(transport)),
293                ("model".to_string(), serde_json::json!(&resolved.model)),
294                ("stream".to_string(), serde_json::json!(want_stream)),
295                (
296                    "responsesLite".to_string(),
297                    serde_json::json!(use_responses_lite),
298                ),
299                (
300                    "previousResponseIdEnabled".to_string(),
301                    serde_json::json!(previous_response_id_enabled),
302                ),
303                (
304                    "hasPreviousResponseId".to_string(),
305                    serde_json::json!(continuation.candidate().previous_response_id.is_some()),
306                ),
307                (
308                    "inputDeltaCount".to_string(),
309                    serde_json::json!(continuation.candidate().input_delta.as_ref().map(Vec::len)),
310                ),
311                ("turnId".to_string(), serde_json::json!(turn_id)),
312            ])),
313        );
314
315        // Post to upstream with continuation
316        let client = self.client.clone();
317        if let Some(monitor) = ctx.monitor.as_ref() {
318            monitor.upstream_started(&ctx.req_id);
319        }
320        if want_stream {
321            let stream_request = translated.clone();
322            let response = live_stream_response(
323                client,
324                message_id,
325                model,
326                ctx,
327                stream_request,
328                continuation,
329                LiveStreamCompaction {
330                    compact_boundary,
331                    attempt: compaction_attempt,
332                },
333                configured_transport,
334            )
335            .await;
336            log.info(
337                "codex_upstream_response_ready",
338                Some(serde_json::Map::from_iter([
339                    ("reqId".to_string(), serde_json::json!(&req_id)),
340                    ("transport".to_string(), serde_json::json!(transport)),
341                    (
342                        "status".to_string(),
343                        serde_json::json!(response.status().as_u16()),
344                    ),
345                    (
346                        "ms".to_string(),
347                        serde_json::json!(upstream_started_at.elapsed().as_millis()),
348                    ),
349                ])),
350            );
351            return response;
352        }
353
354        let request_continuation = continuation.clone();
355        let mut continuation = Some(continuation);
356        let mut attempt = 0_u32;
357        let upstream = loop {
358            let response = match client
359                .post_codex_for_owner(&translated, &ctx, continuation.as_ref())
360                .await
361            {
362                Ok(r) => r,
363                Err(e) => {
364                    log.warn(
365                        "codex_upstream_request_failed",
366                        Some(serde_json::Map::from_iter([
367                            ("reqId".to_string(), serde_json::json!(&req_id)),
368                            ("transport".to_string(), serde_json::json!(transport)),
369                            ("status".to_string(), serde_json::json!(e.status)),
370                            (
371                                "origin".to_string(),
372                                serde_json::json!(format!("{:?}", e.origin)),
373                            ),
374                            ("error".to_string(), serde_json::json!(&e.message)),
375                            (
376                                "ms".to_string(),
377                                serde_json::json!(upstream_started_at.elapsed().as_millis()),
378                            ),
379                        ])),
380                    );
381                    abort_compaction_attempt(ctx.session_id.as_deref(), compaction_attempt);
382                    abort_continuation_for_owner(&request_continuation);
383                    return map_codex_error_to_response(&e);
384                }
385            };
386            if !is_empty_codex_success_completion(&response.body) {
387                break response;
388            }
389            // A successful terminal event with no output would translate into
390            // an empty end_turn; retry with full context instead.
391            let error = empty_buffered_completion_error();
392            drop_live_continuation_for_retry(&mut continuation);
393            if attempt >= MAX_EMPTY_COMPLETION_RETRIES {
394                abort_compaction_attempt(ctx.session_id.as_deref(), compaction_attempt);
395                abort_continuation_for_owner(&request_continuation);
396                return map_codex_error_to_response(&error);
397            }
398            let delay = compute_backoff_delay(attempt, None);
399            if delay.exceeds_budget {
400                abort_compaction_attempt(ctx.session_id.as_deref(), compaction_attempt);
401                abort_continuation_for_owner(&request_continuation);
402                return map_codex_error_to_response(&error);
403            }
404            attempt += 1;
405            sleep(delay.wait_ms).await;
406        };
407        log.info(
408            "codex_upstream_response_received",
409            Some(serde_json::Map::from_iter([
410                ("reqId".to_string(), serde_json::json!(&req_id)),
411                ("transport".to_string(), serde_json::json!(transport)),
412                ("status".to_string(), serde_json::json!(upstream.status)),
413                (
414                    "bodyBytes".to_string(),
415                    serde_json::json!(upstream.body.len()),
416                ),
417                (
418                    "ms".to_string(),
419                    serde_json::json!(upstream_started_at.elapsed().as_millis()),
420                ),
421            ])),
422        );
423
424        if want_stream {
425            let estimated_input_tokens = count_translated_tokens(&translated);
426            let sse_bytes = match translate_stream_bytes_with_traffic(
427                &upstream.body,
428                &message_id,
429                model,
430                estimated_input_tokens,
431                ctx.traffic.as_deref(),
432            ) {
433                Ok(b) => b,
434                Err(e) => {
435                    abort_compaction_attempt(ctx.session_id.as_deref(), compaction_attempt);
436                    abort_continuation_for_owner(&request_continuation);
437                    return map_codex_failure_to_response(&format!(
438                        "Stream translation error: {e}"
439                    ));
440                }
441            };
442            if let Some(monitor) = ctx.monitor.as_ref() {
443                let (input_tokens, output_tokens) = usage_from_anthropic_sse(&sse_bytes);
444                monitor.stream_progress(
445                    &ctx.req_id,
446                    sse_bytes.len() as u64,
447                    count_sse_events(&sse_bytes),
448                    input_tokens,
449                    output_tokens,
450                );
451            }
452            update_continuation_from_upstream(
453                ctx.session_id.as_deref(),
454                &request_continuation,
455                compaction_attempt,
456                &translated,
457                &upstream.body,
458                upstream.socket_id,
459                compact_boundary,
460            );
461
462            let headers = [
463                (http::header::CONTENT_TYPE, "text/event-stream"),
464                (http::header::CACHE_CONTROL, "no-cache"),
465                (http::header::CONNECTION, "keep-alive"),
466            ];
467            (headers, sse_bytes).into_response()
468        } else {
469            match accumulate_response_with_traffic(
470                &upstream.body,
471                &message_id,
472                model,
473                ctx.traffic.as_deref(),
474            ) {
475                Ok(json) => {
476                    if let Some(monitor) = ctx.monitor.as_ref() {
477                        monitor.usage_updated(
478                            &ctx.req_id,
479                            json.pointer("/usage/input_tokens").and_then(|v| v.as_u64()),
480                            json.pointer("/usage/output_tokens")
481                                .and_then(|v| v.as_u64()),
482                        );
483                    }
484                    update_continuation_from_upstream(
485                        ctx.session_id.as_deref(),
486                        &request_continuation,
487                        compaction_attempt,
488                        &translated,
489                        &upstream.body,
490                        upstream.socket_id,
491                        compact_boundary,
492                    );
493                    (StatusCode::OK, Json(json)).into_response()
494                }
495                Err(e) => {
496                    abort_compaction_attempt(ctx.session_id.as_deref(), compaction_attempt);
497                    abort_continuation_for_owner(&request_continuation);
498                    map_codex_failure_to_response(&format!("Accumulation error: {e}"))
499                }
500            }
501        }
502    }
503}
504
505#[async_trait]
506impl Provider for CodexProvider {
507    fn name(&self) -> &'static str {
508        "codex"
509    }
510
511    fn supported_models(&self) -> Vec<String> {
512        let mut models: Vec<String> = registry::CODEX_MODELS
513            .iter()
514            .map(|m| m.to_string())
515            .collect();
516        for m in registry::CODEX_MODELS {
517            models.push(format!("{m}-fast"));
518        }
519        models.sort_unstable();
520        models.dedup();
521        models
522    }
523
524    fn cli(&self) -> &'static dyn CliHandlers {
525        &CODEX_CLI
526    }
527
528    async fn handle_messages(&self, body: MessagesRequest, ctx: RequestContext) -> Response {
529        self.handle_messages_inner(body, ctx, None).await
530    }
531
532    async fn handle_messages_with_conversation_identity(
533        &self,
534        body: MessagesRequest,
535        ctx: RequestContext,
536        conversation_identity: Option<ConversationIdentity>,
537    ) -> Response {
538        self.handle_messages_inner(body, ctx, conversation_identity)
539            .await
540    }
541
542    async fn handle_count_tokens(&self, body: MessagesRequest, ctx: RequestContext) -> Response {
543        let model = body.model.as_deref().unwrap_or("gpt-5.6-sol");
544        let mut resolved =
545            resolve_model_request_with_config_override(model, !body.bypass_provider_model_override);
546        if let Err(e) = assert_allowed_model(&resolved.model) {
547            return json_error(
548                StatusCode::BAD_REQUEST,
549                "invalid_request_error",
550                format!(
551                    "Model \"{model}\" resolves to unsupported model \"{}\"",
552                    e.model
553                ),
554            );
555        }
556        let use_responses_lite = apply_model_lane_for_request(&mut resolved.model, &body);
557        if let Some(monitor) = ctx.monitor.as_ref() {
558            monitor.model_resolved(&ctx.req_id, &resolved.model);
559        }
560
561        let translated = match translate_request(
562            &body,
563            TranslateOptions {
564                session_id: None,
565                service_tier: resolved.service_tier.clone(),
566                model: resolved.model.clone(),
567                use_responses_lite,
568            },
569        ) {
570            Ok(t) => t,
571            Err(e) => {
572                return json_error(
573                    StatusCode::BAD_REQUEST,
574                    "invalid_request_error",
575                    e.to_string(),
576                );
577            }
578        };
579
580        let tokens = count_translated_tokens(&translated);
581        if let Some(monitor) = ctx.monitor.as_ref() {
582            monitor.usage_updated(&ctx.req_id, Some(tokens), None);
583        }
584        (
585            StatusCode::OK,
586            Json(CountTokensResponse {
587                input_tokens: tokens,
588            }),
589        )
590            .into_response()
591    }
592}
593
594/// Picks the upstream model and lane for a request. Hosted web_search must
595/// run on the full Responses API (the lite lane rejects hosted tools), and
596/// lite-only models like gpt-5.6-luna don't exist there, so such requests
597/// are upgraded to a full-lane model. Returns whether to use the lite lane.
598fn apply_model_lane_for_request(model: &mut String, body: &MessagesRequest) -> bool {
599    if has_hosted_web_search(body) {
600        *model = full_lane_web_search_model(model).to_string();
601        return false;
602    }
603    uses_responses_lite(model)
604}
605
606fn count_sse_events(bytes: &[u8]) -> u64 {
607    String::from_utf8_lossy(bytes).matches("event:").count() as u64
608}
609
610fn log_compaction_event(
611    event: &str,
612    ctx: &RequestContext,
613    input_items: usize,
614    error: Option<&str>,
615) {
616    let mut fields = serde_json::Map::new();
617    fields.insert("reqId".into(), serde_json::json!(ctx.req_id));
618    fields.insert("inputItems".into(), serde_json::json!(input_items));
619    if let Some(error) = error {
620        fields.insert("error".into(), serde_json::json!(error));
621        create_logger("codex").warn(event, Some(fields));
622    } else {
623        create_logger("codex").info(event, Some(fields));
624    }
625}
626
627fn abort_request_state(
628    session_id: Option<&str>,
629    continuation: &ContinuationReservation,
630    compaction_attempt: Option<CompactionAttempt>,
631) {
632    abort_compaction_attempt(session_id, compaction_attempt);
633    abort_continuation_for_owner(continuation);
634}
635
636struct LiveRequestStateCleanup {
637    continuation: ContinuationReservation,
638    session_id: Option<String>,
639    compaction_attempt: Option<CompactionAttempt>,
640    armed: bool,
641}
642
643impl LiveRequestStateCleanup {
644    fn new(
645        continuation: ContinuationReservation,
646        session_id: Option<String>,
647        compaction_attempt: Option<CompactionAttempt>,
648    ) -> Self {
649        Self {
650            continuation,
651            session_id,
652            compaction_attempt,
653            armed: true,
654        }
655    }
656
657    fn abort(&mut self) {
658        if self.armed {
659            abort_request_state(
660                self.session_id.as_deref(),
661                &self.continuation,
662                self.compaction_attempt,
663            );
664            self.armed = false;
665        }
666    }
667
668    fn disarm(&mut self) {
669        self.armed = false;
670    }
671}
672
673impl Drop for LiveRequestStateCleanup {
674    fn drop(&mut self) {
675        if self.armed {
676            abort_request_state(
677                self.session_id.as_deref(),
678                &self.continuation,
679                self.compaction_attempt,
680            );
681        }
682    }
683}
684
685enum LiveStreamStart {
686    Response(Response),
687    Retry {
688        error: client::CodexError,
689        full_context_retry_attempted: bool,
690    },
691}
692
693#[derive(Clone, Copy)]
694struct LiveStreamCompaction {
695    compact_boundary: bool,
696    attempt: Option<CompactionAttempt>,
697}
698
699#[allow(clippy::too_many_arguments)]
700async fn live_stream_response(
701    client: Arc<CodexHttpClient>,
702    message_id: String,
703    model: &str,
704    ctx: RequestContext,
705    request_body: translate::request::ResponsesRequest,
706    continuation: ContinuationReservation,
707    compaction: LiveStreamCompaction,
708    transport: config::CodexTransport,
709) -> Response {
710    let model = model.to_string();
711    let request_continuation = continuation.clone();
712    let mut cleanup = LiveRequestStateCleanup::new(
713        request_continuation.clone(),
714        ctx.session_id.clone(),
715        compaction.attempt,
716    );
717    let mut attempt = 0_u32;
718    let mut continuation = Some(continuation);
719
720    loop {
721        let upstream_events = match transport {
722            config::CodexTransport::Http => {
723                client
724                    .stream_codex_http_events_for_owner(&request_body, &ctx)
725                    .await
726            }
727            config::CodexTransport::WebSocket => {
728                client
729                    .stream_codex_websocket_events_for_owner(
730                        &request_body,
731                        &ctx,
732                        continuation.as_ref(),
733                    )
734                    .await
735            }
736            config::CodexTransport::Auto => {
737                client
738                    .stream_codex_auto_events_for_owner(&request_body, &ctx, continuation.as_ref())
739                    .await
740            }
741        };
742        let upstream_events = match upstream_events {
743            Ok(events) => events,
744            Err(err) if err.origin == client::CodexErrorOrigin::Http => {
745                cleanup.abort();
746                return map_codex_error_to_response(&err);
747            }
748            Err(err) if retryable_live_start_codex_error(&err) => {
749                let dropped = drop_live_continuation_for_retry(&mut continuation);
750                if dropped && is_missing_previous_response_error(&err) {
751                    attempt += 1;
752                    continue;
753                }
754                if attempt >= MAX_RETRYABLE_LIVE_STREAM_RETRIES {
755                    cleanup.abort();
756                    return map_codex_error_to_response(&err);
757                }
758                let delay = compute_backoff_delay(attempt, err.retry_after.as_deref());
759                if delay.exceeds_budget {
760                    cleanup.abort();
761                    return map_codex_error_to_response(&err);
762                }
763                attempt += 1;
764                sleep(delay.wait_ms).await;
765                continue;
766            }
767            Err(err) => {
768                cleanup.abort();
769                return map_codex_error_to_response(&err);
770            }
771        };
772
773        match live_stream_response_once(
774            upstream_events,
775            message_id.clone(),
776            &model,
777            ctx.clone(),
778            request_continuation.clone(),
779            request_body.clone(),
780            compaction,
781        )
782        .await
783        {
784            LiveStreamStart::Response(response) => {
785                cleanup.disarm();
786                return response;
787            }
788            LiveStreamStart::Retry {
789                error,
790                full_context_retry_attempted,
791            } => {
792                // The incremental HTTP reader performs its own bounded
793                // pre-semantic retries so it can stop immediately when the
794                // consumer disappears. Do not multiply that exhausted retry
795                // loop by the provider-level WebSocket retry policy.
796                if error.origin == client::CodexErrorOrigin::Http {
797                    cleanup.abort();
798                    return map_codex_error_to_response(&error);
799                }
800                let dropped = drop_live_continuation_for_retry(&mut continuation);
801                if full_context_retry_attempted && client::is_continuation_retry_error(&error) {
802                    cleanup.abort();
803                    return map_codex_error_to_response(&error);
804                }
805                if dropped && is_missing_previous_response_error(&error) {
806                    attempt += 1;
807                    continue;
808                }
809                if attempt >= MAX_RETRYABLE_LIVE_STREAM_RETRIES {
810                    cleanup.abort();
811                    return map_codex_error_to_response(&error);
812                }
813                let delay = compute_backoff_delay(attempt, error.retry_after.as_deref());
814                if delay.exceeds_budget {
815                    cleanup.abort();
816                    return map_codex_error_to_response(&error);
817                }
818                attempt += 1;
819                sleep(delay.wait_ms).await;
820            }
821        }
822    }
823}
824
825fn provider_retry(
826    upstream_events: &websocket::CodexWebSocketEventStream,
827    error: client::CodexError,
828) -> LiveStreamStart {
829    let full_context_retry_attempted = upstream_events.used_full_context_retry();
830    upstream_events.mark_provider_retry_handoff();
831    LiveStreamStart::Retry {
832        error,
833        full_context_retry_attempted,
834    }
835}
836
837#[allow(clippy::too_many_arguments)]
838async fn live_stream_response_once(
839    mut upstream_events: websocket::CodexWebSocketEventStream,
840    message_id: String,
841    model: &str,
842    ctx: RequestContext,
843    request_continuation: ContinuationReservation,
844    request_body: translate::request::ResponsesRequest,
845    compaction: LiveStreamCompaction,
846) -> LiveStreamStart {
847    let estimated_input_tokens = count_translated_tokens(&request_body);
848    let mut translator = LiveStreamTranslator::with_estimated_input_tokens(
849        message_id,
850        model.to_string(),
851        estimated_input_tokens,
852    );
853    let mut upstream_sse_body = Vec::new();
854    // Keep protocol framing private until real output makes a transparent retry unsafe.
855    // Every branch that consumes pending_chunk returns, so it is never flushed twice.
856    let mut pending_chunk = Vec::new();
857    let mut generation_started = false;
858
859    while let Some(item) = upstream_events.recv().await {
860        let payload = match item {
861            Ok(payload) => payload,
862            Err(err) => {
863                if retryable_live_start_codex_error(&err) {
864                    return provider_retry(&upstream_events, err);
865                }
866                abort_request_state(
867                    ctx.session_id.as_deref(),
868                    &request_continuation,
869                    compaction.attempt,
870                );
871                return LiveStreamStart::Response(map_codex_error_to_response(&err));
872            }
873        };
874        if !generation_started && codex_generation_event(&payload) {
875            if let Some(monitor) = ctx.monitor.as_ref() {
876                monitor.generation_started(&ctx.req_id);
877            }
878            generation_started = true;
879        }
880        append_upstream_sse_payload(&mut upstream_sse_body, &payload);
881        let (chunk, terminal) = match translate_live_stream_payload(&mut translator, &payload, None)
882        {
883            Ok(result) => result,
884            Err(message) => {
885                if retryable_live_start_payload(&payload, &message) {
886                    let lower_message = message.to_ascii_lowercase();
887                    let status = websocket::event_error_status(&payload).unwrap_or_else(|| {
888                        let error = payload.get("error").or_else(|| {
889                            payload.get("response").and_then(|value| value.get("error"))
890                        });
891                        let overloaded = error.is_some_and(|error| {
892                            error.get("code").and_then(|value| value.as_str())
893                                == Some("overloaded_error")
894                                || error.get("type").and_then(|value| value.as_str())
895                                    == Some("overloaded_error")
896                        });
897                        if payload.get("type").and_then(|value| value.as_str())
898                            == Some("codex.rate_limits")
899                            || lower_message.contains("rate limit")
900                        {
901                            429
902                        } else if overloaded || lower_message.contains("overloaded") {
903                            529
904                        } else {
905                            503
906                        }
907                    });
908                    return provider_retry(
909                        &upstream_events,
910                        client::CodexError {
911                            status,
912                            message: message.clone(),
913                            detail: Some(message),
914                            retry_after: retry_after_from_live_payload(&payload),
915                            origin: client::CodexErrorOrigin::WebSocket,
916                        },
917                    );
918                }
919                abort_request_state(
920                    ctx.session_id.as_deref(),
921                    &request_continuation,
922                    compaction.attempt,
923                );
924                return LiveStreamStart::Response(map_codex_failure_to_response(&message));
925            }
926        };
927        pending_chunk.extend_from_slice(&chunk);
928        if terminal
929            && is_codex_success_terminal_event(&payload)
930            && !translator.has_semantic_output()
931        {
932            return provider_retry(&upstream_events, empty_live_completion_error());
933        }
934        if translator.has_semantic_output() && !pending_chunk.is_empty() {
935            record_live_stream_downstream_capture(&ctx, &pending_chunk);
936            record_live_stream_progress(&ctx, &pending_chunk);
937            if terminal {
938                update_continuation_from_upstream(
939                    ctx.session_id.as_deref(),
940                    &request_continuation,
941                    compaction.attempt,
942                    &request_body,
943                    &upstream_sse_body,
944                    upstream_events.socket_id(),
945                    compaction.compact_boundary,
946                );
947                return LiveStreamStart::Response(single_live_stream_response(pending_chunk));
948            }
949            return LiveStreamStart::Response(remaining_live_stream_response(
950                upstream_events,
951                translator,
952                pending_chunk,
953                ctx,
954                request_continuation,
955                request_body,
956                upstream_sse_body,
957                compaction,
958            ));
959        }
960        if terminal {
961            update_continuation_from_upstream(
962                ctx.session_id.as_deref(),
963                &request_continuation,
964                compaction.attempt,
965                &request_body,
966                &upstream_sse_body,
967                upstream_events.socket_id(),
968                compaction.compact_boundary,
969            );
970            if pending_chunk.is_empty() {
971                return LiveStreamStart::Response(empty_live_stream_response());
972            }
973            record_live_stream_downstream_capture(&ctx, &pending_chunk);
974            record_live_stream_progress(&ctx, &pending_chunk);
975            return LiveStreamStart::Response(single_live_stream_response(pending_chunk));
976        }
977    }
978
979    provider_retry(
980        &upstream_events,
981        client::CodexError {
982            status: 0,
983            message: "WebSocket connection closed before terminal Codex response event".to_string(),
984            detail: Some(websocket::WEBSOCKET_MISSING_TERMINAL_DETAIL.to_string()),
985            retry_after: None,
986            origin: client::CodexErrorOrigin::WebSocket,
987        },
988    )
989}
990
991fn empty_live_completion_error() -> client::CodexError {
992    client::CodexError {
993        status: 503,
994        message: "Codex completed without producing output".to_string(),
995        detail: Some(EMPTY_CODEX_COMPLETION_DETAIL.to_string()),
996        retry_after: None,
997        origin: client::CodexErrorOrigin::WebSocket,
998    }
999}
1000
1001fn codex_generation_event(payload: &serde_json::Value) -> bool {
1002    !matches!(
1003        payload.get("type").and_then(|value| value.as_str()),
1004        Some("codex.rate_limits" | "keepalive") | None
1005    )
1006}
1007
1008fn translate_live_stream_payload(
1009    translator: &mut LiveStreamTranslator,
1010    payload: &serde_json::Value,
1011    traffic: Option<&crate::traffic::TrafficCapture>,
1012) -> Result<(Vec<u8>, bool), String> {
1013    let chunk = translator.accept(payload, traffic)?;
1014    let terminal = is_codex_terminal_event(payload) || translator.is_finished();
1015    Ok((chunk, terminal))
1016}
1017
1018fn record_live_stream_downstream_capture(ctx: &RequestContext, chunk: &[u8]) {
1019    let Some(traffic) = ctx.traffic.as_ref() else {
1020        return;
1021    };
1022    for event in parse_sse_events(chunk) {
1023        let Ok(data) = serde_json::from_str::<serde_json::Value>(&event.data) else {
1024            continue;
1025        };
1026        traffic.write_json_event(
1027            "050-downstream-event",
1028            &serde_json::json!({
1029                "event": event.event.as_deref().unwrap_or("message"),
1030                "data": data,
1031            }),
1032        );
1033    }
1034}
1035
1036fn record_live_stream_progress(ctx: &RequestContext, chunk: &[u8]) {
1037    if let Some(monitor) = ctx.monitor.as_ref() {
1038        let (input_tokens, output_tokens) = usage_from_anthropic_sse(chunk);
1039        monitor.stream_progress(
1040            &ctx.req_id,
1041            chunk.len() as u64,
1042            count_sse_events(chunk),
1043            input_tokens,
1044            output_tokens,
1045        );
1046    }
1047}
1048
1049fn single_live_stream_response(chunk: Vec<u8>) -> Response {
1050    event_stream_response(futures_util::stream::once(async move {
1051        Ok::<Bytes, std::io::Error>(Bytes::from(chunk))
1052    }))
1053}
1054
1055fn empty_live_stream_response() -> Response {
1056    event_stream_response(futures_util::stream::empty::<Result<Bytes, std::io::Error>>())
1057}
1058
1059#[allow(clippy::too_many_arguments)]
1060fn remaining_live_stream_response(
1061    mut upstream_events: websocket::CodexWebSocketEventStream,
1062    mut translator: LiveStreamTranslator,
1063    first_chunk: Vec<u8>,
1064    ctx: RequestContext,
1065    request_continuation: ContinuationReservation,
1066    request_body: translate::request::ResponsesRequest,
1067    mut upstream_sse_body: Vec<u8>,
1068    compaction: LiveStreamCompaction,
1069) -> Response {
1070    let (tx, rx) = tokio::sync::mpsc::channel::<Result<Bytes, std::io::Error>>(64);
1071    tokio::spawn(async move {
1072        if tx.send(Ok(Bytes::from(first_chunk))).await.is_err() {
1073            abort_request_state(
1074                ctx.session_id.as_deref(),
1075                &request_continuation,
1076                compaction.attempt,
1077            );
1078            return;
1079        }
1080        let mut heartbeat = tokio::time::interval(LIVE_STREAM_HEARTBEAT_INTERVAL);
1081        heartbeat.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1082        heartbeat.tick().await;
1083        loop {
1084            let item = tokio::select! {
1085                biased;
1086                _ = tx.closed() => {
1087                    abort_request_state(
1088                        ctx.session_id.as_deref(),
1089                        &request_continuation,
1090                        compaction.attempt,
1091                    );
1092                    return;
1093                }
1094                item = upstream_events.recv() => item,
1095                _ = heartbeat.tick() => {
1096                    let chunk = translator.ping_chunk(ctx.traffic.as_deref());
1097                    if !chunk.is_empty() {
1098                        record_live_stream_progress(&ctx, &chunk);
1099                        if tx.send(Ok(Bytes::from(chunk))).await.is_err() {
1100                            abort_request_state(
1101                                ctx.session_id.as_deref(),
1102                                &request_continuation,
1103                                compaction.attempt,
1104                            );
1105                            return;
1106                        }
1107                    }
1108                    continue;
1109                }
1110            };
1111            let Some(item) = item else {
1112                break;
1113            };
1114            match item {
1115                Ok(payload) => {
1116                    append_upstream_sse_payload(&mut upstream_sse_body, &payload);
1117                    let (chunk, terminal) = match translate_live_stream_payload(
1118                        &mut translator,
1119                        &payload,
1120                        ctx.traffic.as_deref(),
1121                    ) {
1122                        Ok(result) => result,
1123                        Err(message) => {
1124                            abort_request_state(
1125                                ctx.session_id.as_deref(),
1126                                &request_continuation,
1127                                compaction.attempt,
1128                            );
1129                            let chunk = translator.error_chunk(
1130                                &message,
1131                                "api_error",
1132                                ctx.traffic.as_deref(),
1133                            );
1134                            if !chunk.is_empty() {
1135                                record_live_stream_progress(&ctx, &chunk);
1136                                let _ = tx.send(Ok(Bytes::from(chunk))).await;
1137                            }
1138                            return;
1139                        }
1140                    };
1141                    if !chunk.is_empty() {
1142                        record_live_stream_progress(&ctx, &chunk);
1143                        if tx.send(Ok(Bytes::from(chunk))).await.is_err() {
1144                            abort_request_state(
1145                                ctx.session_id.as_deref(),
1146                                &request_continuation,
1147                                compaction.attempt,
1148                            );
1149                            return;
1150                        }
1151                    }
1152                    if terminal {
1153                        update_continuation_from_upstream(
1154                            ctx.session_id.as_deref(),
1155                            &request_continuation,
1156                            compaction.attempt,
1157                            &request_body,
1158                            &upstream_sse_body,
1159                            upstream_events.socket_id(),
1160                            compaction.compact_boundary,
1161                        );
1162                        return;
1163                    }
1164                }
1165                Err(err) => {
1166                    abort_request_state(
1167                        ctx.session_id.as_deref(),
1168                        &request_continuation,
1169                        compaction.attempt,
1170                    );
1171                    let chunk =
1172                        translator.finish_after_closed_completed_tool_call(ctx.traffic.as_deref());
1173                    if !chunk.is_empty() {
1174                        record_live_stream_progress(&ctx, &chunk);
1175                        let _ = tx.send(Ok(Bytes::from(chunk))).await;
1176                        return;
1177                    }
1178                    let error_type = codex_stream_error_type(&err);
1179                    let chunk = translator.error_chunk(
1180                        codex_error_message(&err),
1181                        error_type,
1182                        ctx.traffic.as_deref(),
1183                    );
1184                    if !chunk.is_empty() {
1185                        record_live_stream_progress(&ctx, &chunk);
1186                        let _ = tx.send(Ok(Bytes::from(chunk))).await;
1187                    }
1188                    return;
1189                }
1190            }
1191        }
1192
1193        abort_request_state(
1194            ctx.session_id.as_deref(),
1195            &request_continuation,
1196            compaction.attempt,
1197        );
1198        let chunk = translator.finish_after_closed_completed_tool_call(ctx.traffic.as_deref());
1199        if !chunk.is_empty() {
1200            record_live_stream_progress(&ctx, &chunk);
1201            let _ = tx.send(Ok(Bytes::from(chunk))).await;
1202            return;
1203        }
1204        let chunk = translator.error_chunk(
1205            "Upstream event stream closed before terminal Codex response event",
1206            "api_error",
1207            ctx.traffic.as_deref(),
1208        );
1209        if !chunk.is_empty() {
1210            record_live_stream_progress(&ctx, &chunk);
1211            let _ = tx.send(Ok(Bytes::from(chunk))).await;
1212        }
1213    });
1214
1215    let stream = futures_util::stream::unfold(rx, |mut rx| async {
1216        rx.recv().await.map(|item| (item, rx))
1217    });
1218    event_stream_response(stream)
1219}
1220
1221fn append_upstream_sse_payload(buffer: &mut Vec<u8>, payload: &serde_json::Value) {
1222    let text = payload.to_string();
1223    for line in text.lines() {
1224        buffer.extend_from_slice(b"data: ");
1225        buffer.extend_from_slice(line.as_bytes());
1226        buffer.push(b'\n');
1227    }
1228    buffer.push(b'\n');
1229}
1230
1231fn event_stream_response<S>(stream: S) -> Response
1232where
1233    S: futures_util::Stream<Item = Result<Bytes, std::io::Error>> + Send + 'static,
1234{
1235    let headers = [
1236        (http::header::CONTENT_TYPE, "text/event-stream"),
1237        (http::header::CACHE_CONTROL, "no-cache"),
1238        (http::header::CONNECTION, "keep-alive"),
1239    ];
1240    (headers, Body::from_stream(stream)).into_response()
1241}
1242
1243fn empty_buffered_completion_error() -> client::CodexError {
1244    client::CodexError {
1245        status: 503,
1246        message: "Codex completed without producing output".to_string(),
1247        detail: Some(EMPTY_CODEX_COMPLETION_DETAIL.to_string()),
1248        retry_after: None,
1249        origin: match config::codex_transport() {
1250            config::CodexTransport::Http => client::CodexErrorOrigin::BufferedHttp,
1251            _ => client::CodexErrorOrigin::BufferedWebSocket,
1252        },
1253    }
1254}
1255
1256/// True when the buffered upstream body ended in a successful terminal event
1257/// without ever producing semantic output (text, thinking, tool, web search).
1258fn is_empty_codex_success_completion(upstream_sse: &[u8]) -> bool {
1259    use self::translate::reducer::{ReducerEvent, TERM_COMPLETED, TERM_DONE};
1260
1261    let Ok(events) = self::translate::reducer::reduce_upstream_bytes(upstream_sse) else {
1262        return false;
1263    };
1264    let mut saw_success_terminal = false;
1265    for event in &events {
1266        match event {
1267            ReducerEvent::TextDelta { text, .. } if !text.is_empty() => return false,
1268            ReducerEvent::ThinkingStart { .. }
1269            | ReducerEvent::ToolStart { .. }
1270            | ReducerEvent::WebSearch { .. } => return false,
1271            ReducerEvent::Finish { terminal_type, .. }
1272                if terminal_type == TERM_COMPLETED || terminal_type == TERM_DONE =>
1273            {
1274                saw_success_terminal = true;
1275            }
1276            _ => {}
1277        }
1278    }
1279    saw_success_terminal
1280}
1281
1282fn is_codex_terminal_event(payload: &serde_json::Value) -> bool {
1283    matches!(
1284        payload.get("type").and_then(|v| v.as_str()),
1285        Some("response.completed")
1286            | Some("response.incomplete")
1287            | Some("response.done")
1288            | Some("response.failed")
1289            | Some("response.error")
1290            | Some("error")
1291    )
1292}
1293
1294fn is_codex_success_terminal_event(payload: &serde_json::Value) -> bool {
1295    matches!(
1296        payload.get("type").and_then(|v| v.as_str()),
1297        Some("response.completed") | Some("response.done")
1298    )
1299}
1300
1301fn retryable_live_start_codex_error(err: &client::CodexError) -> bool {
1302    if err.origin == client::CodexErrorOrigin::WebSocketHandshake {
1303        if err.detail.as_deref() == Some(websocket::WEBSOCKET_PROXY_TUNNEL_REJECTED_DETAIL) {
1304            return false;
1305        }
1306        return err.status == 0 || matches!(err.status, 429 | 500 | 502 | 503 | 504 | 529);
1307    }
1308    if err.detail.as_deref() == Some(websocket::WEBSOCKET_KEEPALIVE_FAILURE_DETAIL) {
1309        return true;
1310    }
1311    matches!(err.status, 429 | 500 | 502 | 503 | 504 | 529)
1312        || (err.status == 0 && retryable_live_message(codex_error_message(err)))
1313}
1314
1315fn is_missing_previous_response_error(err: &client::CodexError) -> bool {
1316    matches!(
1317        err.detail.as_deref(),
1318        Some("previous_response_not_found")
1319            | Some(websocket::WEBSOCKET_CONTINUATION_SOCKET_MISSING_DETAIL)
1320    )
1321}
1322
1323fn drop_live_continuation_for_retry(continuation: &mut Option<ContinuationReservation>) -> bool {
1324    if continuation
1325        .as_ref()
1326        .and_then(|reservation| reservation.candidate().previous_response_id.as_deref())
1327        .is_none()
1328    {
1329        return false;
1330    }
1331
1332    if let Some(reservation) = continuation.as_ref() {
1333        *continuation = Some(reservation.full_context_retry());
1334    }
1335    true
1336}
1337
1338fn retryable_live_message(message: &str) -> bool {
1339    let lower = message.to_ascii_lowercase();
1340    [
1341        "overloaded",
1342        "rate limit",
1343        "you can retry your request",
1344        "temporarily unavailable",
1345        "timed out",
1346        "connection closed",
1347        "connection reset",
1348        "broken pipe",
1349        "epipe",
1350    ]
1351    .iter()
1352    .any(|needle| lower.contains(needle))
1353}
1354
1355fn retryable_live_start_payload(payload: &serde_json::Value, _message: &str) -> bool {
1356    events::classify_event_failure(payload).is_some_and(|failure| failure.retryable())
1357}
1358
1359fn retry_after_from_live_payload(payload: &serde_json::Value) -> Option<String> {
1360    events::classify_event_failure(payload).and_then(|failure| failure.retry_after)
1361}
1362
1363fn codex_stream_error_type(err: &client::CodexError) -> &'static str {
1364    match err.status {
1365        429 => "rate_limit_error",
1366        529 => "overloaded_error",
1367        _ if codex_error_message(err)
1368            .to_lowercase()
1369            .contains("overloaded") =>
1370        {
1371            "overloaded_error"
1372        }
1373        _ => "api_error",
1374    }
1375}
1376
1377#[allow(clippy::too_many_arguments)]
1378fn update_continuation_from_upstream(
1379    session_id: Option<&str>,
1380    continuation: &ContinuationReservation,
1381    compaction_attempt: Option<CompactionAttempt>,
1382    request_body: &translate::request::ResponsesRequest,
1383    upstream_body: &[u8],
1384    socket_id: Option<u64>,
1385    compact_boundary: bool,
1386) {
1387    match finish_metadata_from_upstream(upstream_body) {
1388        Ok(Some(finish)) if finish.continuation_eligible => {
1389            if compact_boundary {
1390                activate_compaction(
1391                    session_id,
1392                    compaction_attempt,
1393                    &request_body.model,
1394                    &finish.output_items,
1395                );
1396            }
1397            record_continuation_for_owner(
1398                continuation,
1399                request_body,
1400                finish.response_id.as_deref(),
1401                socket_id,
1402                &finish.output_items,
1403            );
1404        }
1405        _ => {
1406            abort_compaction_attempt(session_id, compaction_attempt);
1407            abort_continuation_for_owner(continuation);
1408        }
1409    }
1410}
1411
1412// ---------------------------------------------------------------------------
1413// Error mapping
1414// ---------------------------------------------------------------------------
1415
1416fn map_codex_error_to_response(err: &client::CodexError) -> Response {
1417    let message = codex_error_message(err);
1418    if is_context_window_overflow(message) {
1419        return map_codex_failure_to_response(message);
1420    }
1421    if err.detail.as_deref() == Some(EMPTY_CODEX_COMPLETION_DETAIL) {
1422        return json_error(StatusCode::SERVICE_UNAVAILABLE, "api_error", &err.message);
1423    }
1424
1425    match err.status {
1426        401 => json_error(
1427            StatusCode::UNAUTHORIZED,
1428            "authentication_error",
1429            err.detail.as_deref().unwrap_or("Authentication failed"),
1430        ),
1431        403 => json_error(
1432            StatusCode::FORBIDDEN,
1433            "permission_error",
1434            err.detail.as_deref().unwrap_or("Permission denied"),
1435        ),
1436        429 => {
1437            let response = json_error(
1438                StatusCode::TOO_MANY_REQUESTS,
1439                "rate_limit_error",
1440                &err.message,
1441            );
1442            if let Some(retry_after) = err.retry_after.as_deref() {
1443                ([(http::header::RETRY_AFTER, retry_after)], response).into_response()
1444            } else {
1445                response
1446            }
1447        }
1448        status @ (400..=599) => {
1449            let response = json_error(
1450                StatusCode::from_u16(status).unwrap_or(StatusCode::BAD_GATEWAY),
1451                if status == 529 {
1452                    "overloaded_error"
1453                } else {
1454                    "api_error"
1455                },
1456                codex_error_message(err),
1457            );
1458            if let Some(retry_after) = err.retry_after.as_deref() {
1459                ([(http::header::RETRY_AFTER, retry_after)], response).into_response()
1460            } else {
1461                response
1462            }
1463        }
1464        _ => json_error(
1465            StatusCode::BAD_GATEWAY,
1466            "api_error",
1467            codex_error_message(err),
1468        ),
1469    }
1470}
1471
1472fn map_codex_failure_to_response(message: &str) -> Response {
1473    if is_context_window_overflow(message) {
1474        json_error(StatusCode::PAYLOAD_TOO_LARGE, "request_too_large", message)
1475    } else {
1476        json_error(StatusCode::BAD_GATEWAY, "api_error", message)
1477    }
1478}
1479
1480fn is_context_window_overflow(message: &str) -> bool {
1481    message.to_ascii_lowercase().contains("context window")
1482}
1483
1484fn codex_error_message(err: &client::CodexError) -> &str {
1485    if err.status == 0 {
1486        err.message.as_str()
1487    } else {
1488        err.detail.as_deref().unwrap_or("Upstream error")
1489    }
1490}
1491
1492// ---------------------------------------------------------------------------
1493// CLI
1494// ---------------------------------------------------------------------------
1495
1496pub(crate) struct CodexCli;
1497
1498impl CliHandlers for CodexCli {
1499    fn login(&self) -> Result<(), anyhow::Error> {
1500        anyhow::bail!(
1501            "Codex auth is provided by the Codex CLI. Run `codex login` to create ~/.codex/auth.json; this proxy reads it directly."
1502        )
1503    }
1504
1505    fn device(&self) -> Result<(), anyhow::Error> {
1506        anyhow::bail!(
1507            "Codex auth is provided by the Codex CLI. Run `codex login` to create ~/.codex/auth.json; this proxy reads it directly."
1508        )
1509    }
1510
1511    fn status(&self) -> Result<(), anyhow::Error> {
1512        let store = file_store();
1513        let stored = store.load_auth()?;
1514        match stored {
1515            Some(auth) => {
1516                println!(
1517                    "Account: {}",
1518                    auth.account_id.as_deref().unwrap_or("(none)")
1519                );
1520                println!("{}", format_expiry(auth.expires, now_ms()));
1521                println!("Storage: {} (Codex CLI)", store.auth_path());
1522                Ok(())
1523            }
1524            None => anyhow::bail!(
1525                "No Codex credentials. Run `codex login` to create {}",
1526                store.auth_path()
1527            ),
1528        }
1529    }
1530
1531    fn logout(&self) -> Result<(), anyhow::Error> {
1532        println!(
1533            "Codex credentials are managed by the Codex CLI; run `codex logout` to remove them"
1534        );
1535        Ok(())
1536    }
1537}
1538
1539pub(crate) static CODEX_CLI: CodexCli = CodexCli;
1540
1541// ---------------------------------------------------------------------------
1542// CLI helpers
1543// ---------------------------------------------------------------------------
1544
1545fn now_ms() -> u64 {
1546    std::time::SystemTime::now()
1547        .duration_since(std::time::UNIX_EPOCH)
1548        .unwrap_or_default()
1549        .as_millis() as u64
1550}
1551
1552fn format_expiry(expires: u64, now: u64) -> String {
1553    let remaining = (i128::from(expires) - i128::from(now)).div_euclid(1000);
1554    let iso = time::OffsetDateTime::from_unix_timestamp_nanos(i128::from(expires) * 1_000_000)
1555        .ok()
1556        .and_then(|dt| {
1557            let fmt = time::format_description::parse_borrowed::<2>(
1558                "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z",
1559            )
1560            .ok()?;
1561            dt.format(&fmt).ok()
1562        })
1563        .unwrap_or_else(|| "invalid".to_string());
1564    format!("Expires: {iso} (in {remaining}s)")
1565}
1566
1567// ---------------------------------------------------------------------------
1568// Tests
1569// ---------------------------------------------------------------------------
1570
1571#[cfg(test)]
1572mod tests {
1573    use futures_util::{SinkExt, StreamExt};
1574    use http_body_util::BodyExt;
1575    use tokio::net::{TcpListener, TcpStream};
1576    use tokio_tungstenite::tungstenite::Message;
1577
1578    use super::*;
1579
1580    fn live_test_request(text: &str) -> translate::request::ResponsesRequest {
1581        translate::request::ResponsesRequest {
1582            model: "gpt-5.6-sol".to_string(),
1583            instructions: None,
1584            input: vec![translate::request::ResponsesInputItem::Message {
1585                role: "user".to_string(),
1586                content: vec![translate::request::ResponsesContentPart::InputText {
1587                    text: text.to_string(),
1588                }],
1589            }],
1590            tools: None,
1591            tool_choice: None,
1592            store: false,
1593            stream: true,
1594            parallel_tool_calls: true,
1595            include: None,
1596            client_metadata: None,
1597            service_tier: None,
1598            prompt_cache_key: None,
1599            text: translate::request::ResponsesText {
1600                verbosity: None,
1601                format: None,
1602            },
1603            reasoning: None,
1604        }
1605    }
1606
1607    fn live_test_context(session_id: &str) -> RequestContext {
1608        RequestContext {
1609            req_id: format!("request-{session_id}"),
1610            session_id: Some(session_id.to_string()),
1611            session_seq: None,
1612            provider: "codex".to_string(),
1613            traffic: None,
1614            monitor: None,
1615            passthrough: None,
1616        }
1617    }
1618
1619    fn authenticated_live_test_client(base_url: String) -> Arc<CodexHttpClient> {
1620        let client = CodexHttpClient::new_for_test(
1621            reqwest::Client::builder().no_proxy().build().unwrap(),
1622            base_url,
1623            1_000,
1624            1_000,
1625            0,
1626        );
1627        client
1628            .auth_manager()
1629            .set_test_auth(auth::token_store::StoredAuth {
1630                access: "test".to_string(),
1631                refresh: String::new(),
1632                expires: u64::MAX,
1633                account_id: Some("acct".to_string()),
1634            });
1635        Arc::new(client)
1636    }
1637
1638    async fn next_live_websocket_request(
1639        websocket: &mut tokio_tungstenite::WebSocketStream<TcpStream>,
1640    ) -> serde_json::Value {
1641        loop {
1642            match websocket.next().await {
1643                Some(Ok(Message::Ping(payload))) => {
1644                    websocket.send(Message::Pong(payload)).await.unwrap();
1645                }
1646                Some(Ok(Message::Text(text))) => return serde_json::from_str(&text).unwrap(),
1647                other => panic!("unexpected WebSocket request frame: {other:?}"),
1648            }
1649        }
1650    }
1651
1652    async fn emit_live_event(
1653        websocket: &mut tokio_tungstenite::WebSocketStream<TcpStream>,
1654        event: &serde_json::Value,
1655    ) {
1656        websocket
1657            .send(Message::Text(event.to_string()))
1658            .await
1659            .unwrap();
1660    }
1661
1662    fn upstream_sse(events: &[serde_json::Value]) -> Vec<u8> {
1663        let mut bytes = Vec::new();
1664        for event in events {
1665            bytes.extend_from_slice(format!("data: {event}\n\n").as_bytes());
1666        }
1667        bytes
1668    }
1669
1670    #[test]
1671    fn terminal_only_completed_upstream_is_empty_completion() {
1672        let body = upstream_sse(&[serde_json::json!({
1673            "type": "response.completed",
1674            "response": {"id": "resp_1", "status": "completed", "incomplete_details": null, "usage": {"input_tokens": 5, "output_tokens": 0}}
1675        })]);
1676        assert!(is_empty_codex_success_completion(&body));
1677    }
1678
1679    #[test]
1680    fn terminal_only_done_upstream_is_empty_completion() {
1681        let body = upstream_sse(&[serde_json::json!({
1682            "type": "response.done",
1683            "response": {"id": "resp_1", "usage": {}}
1684        })]);
1685        assert!(is_empty_codex_success_completion(&body));
1686    }
1687
1688    #[test]
1689    fn empty_message_item_is_empty_completion() {
1690        let body = upstream_sse(&[
1691            serde_json::json!({
1692                "type": "response.output_item.added",
1693                "output_index": 0,
1694                "item": {"type": "message", "id": "msg_1"}
1695            }),
1696            serde_json::json!({
1697                "type": "response.output_item.done",
1698                "output_index": 0,
1699                "item": {"type": "message"}
1700            }),
1701            serde_json::json!({
1702                "type": "response.completed",
1703                "response": {"id": "resp_1", "usage": {}}
1704            }),
1705        ]);
1706        assert!(is_empty_codex_success_completion(&body));
1707    }
1708
1709    #[test]
1710    fn upstream_with_text_is_not_empty_completion() {
1711        let body = upstream_sse(&[
1712            serde_json::json!({
1713                "type": "response.output_item.added",
1714                "output_index": 0,
1715                "item": {"type": "message", "id": "msg_1"}
1716            }),
1717            serde_json::json!({
1718                "type": "response.output_text.delta",
1719                "output_index": 0,
1720                "delta": "hello"
1721            }),
1722            serde_json::json!({
1723                "type": "response.output_item.done",
1724                "output_index": 0,
1725                "item": {"type": "message"}
1726            }),
1727            serde_json::json!({
1728                "type": "response.completed",
1729                "response": {"id": "resp_1", "usage": {}}
1730            }),
1731        ]);
1732        assert!(!is_empty_codex_success_completion(&body));
1733    }
1734
1735    #[test]
1736    fn upstream_with_tool_call_is_not_empty_completion() {
1737        let body = upstream_sse(&[
1738            serde_json::json!({
1739                "type": "response.output_item.added",
1740                "output_index": 0,
1741                "item": {"type": "function_call", "call_id": "call_1", "name": "Read", "arguments": ""}
1742            }),
1743            serde_json::json!({
1744                "type": "response.output_item.done",
1745                "output_index": 0,
1746                "item": {"type": "function_call", "call_id": "call_1", "name": "Read", "arguments": "{}"}
1747            }),
1748            serde_json::json!({
1749                "type": "response.completed",
1750                "response": {"id": "resp_1", "usage": {}}
1751            }),
1752        ]);
1753        assert!(!is_empty_codex_success_completion(&body));
1754    }
1755
1756    #[test]
1757    fn terminal_only_incomplete_upstream_is_not_empty_completion() {
1758        let body = upstream_sse(&[serde_json::json!({
1759            "type": "response.incomplete",
1760            "response": {"id": "resp_1", "incomplete_details": {"reason": "max_output_tokens"}, "usage": {}}
1761        })]);
1762        assert!(!is_empty_codex_success_completion(&body));
1763    }
1764
1765    #[test]
1766    fn upstream_without_terminal_event_is_not_empty_completion() {
1767        assert!(!is_empty_codex_success_completion(&upstream_sse(&[])));
1768    }
1769
1770    fn request_with_tools(tools: serde_json::Value) -> MessagesRequest {
1771        serde_json::from_value(serde_json::json!({
1772            "model": "gpt-5.6-luna",
1773            "messages": [{"role":"user", "content":"find it"}],
1774            "tools": tools
1775        }))
1776        .unwrap()
1777    }
1778
1779    #[test]
1780    fn web_search_requests_leave_lite_lane_and_upgrade_luna() {
1781        let body = request_with_tools(serde_json::json!([
1782            {"type":"web_search_20250305", "name":"web_search"}
1783        ]));
1784        for (resolved, expected) in [
1785            ("gpt-5.6-luna", "gpt-5.6-sol"),
1786            ("gpt-5.6-sol", "gpt-5.6-sol"),
1787            ("gpt-5.6-terra", "gpt-5.6-terra"),
1788            ("gpt-5.4", "gpt-5.4"),
1789        ] {
1790            let mut model = resolved.to_string();
1791            let lite = apply_model_lane_for_request(&mut model, &body);
1792            assert!(!lite, "{resolved} with web_search must use the full lane");
1793            assert_eq!(model, expected);
1794        }
1795    }
1796
1797    #[test]
1798    fn requests_without_web_search_keep_model_and_lite_lane() {
1799        let body = request_with_tools(serde_json::json!([
1800            {"name":"Bash", "input_schema":{}}
1801        ]));
1802        for (resolved, lite_expected) in [
1803            ("gpt-5.6-luna", true),
1804            ("gpt-5.6-sol", true),
1805            ("gpt-5.4", false),
1806        ] {
1807            let mut model = resolved.to_string();
1808            let lite = apply_model_lane_for_request(&mut model, &body);
1809            assert_eq!(model, resolved, "model must not change without web_search");
1810            assert_eq!(lite, lite_expected);
1811        }
1812    }
1813
1814    #[test]
1815    fn generation_timing_ignores_control_events() {
1816        assert!(!codex_generation_event(&serde_json::json!({
1817            "type": "codex.rate_limits"
1818        })));
1819        assert!(!codex_generation_event(&serde_json::json!({
1820            "type": "keepalive"
1821        })));
1822        assert!(codex_generation_event(&serde_json::json!({
1823            "type": "response.created"
1824        })));
1825    }
1826
1827    #[test]
1828    fn live_stream_progress_records_terminal_usage() {
1829        let monitor = crate::monitor::MonitorHandle::new(10);
1830        monitor.request_started(
1831            "request",
1832            None,
1833            None,
1834            crate::monitor::EndpointKind::Messages,
1835        );
1836        let ctx = RequestContext {
1837            req_id: "request".to_string(),
1838            session_id: None,
1839            session_seq: None,
1840            provider: "codex".to_string(),
1841            traffic: None,
1842            monitor: Some(monitor.clone()),
1843            passthrough: None,
1844        };
1845        let chunk = b"event: message_delta\ndata: {\"type\":\"message_delta\",\"usage\":{\"input_tokens\":12,\"output_tokens\":48}}\n\n";
1846
1847        record_live_stream_progress(&ctx, chunk);
1848
1849        let state = monitor.snapshot();
1850        assert_eq!(state.active[0].input_tokens, Some(12));
1851        assert_eq!(state.active[0].output_tokens, Some(48));
1852    }
1853
1854    #[tokio::test]
1855    async fn live_stream_response_emits_downstream_frames_before_terminal_event() {
1856        use http_body_util::BodyExt as _;
1857
1858        let body = request_with_tools(serde_json::json!([]));
1859        let request_body = translate_request(
1860            &body,
1861            TranslateOptions {
1862                session_id: None,
1863                service_tier: None,
1864                model: "gpt-5.6-sol".to_string(),
1865                use_responses_lite: true,
1866            },
1867        )
1868        .unwrap();
1869        let ctx = RequestContext {
1870            req_id: "incremental-http".to_string(),
1871            session_id: None,
1872            session_seq: None,
1873            provider: "codex".to_string(),
1874            traffic: None,
1875            monitor: None,
1876            passthrough: None,
1877        };
1878        let (tx, rx) = tokio::sync::mpsc::channel(8);
1879        tx.send(Ok(serde_json::json!({"type": "keepalive"})))
1880            .await
1881            .unwrap();
1882        tx.send(Ok(serde_json::json!({
1883            "type": "response.output_item.added",
1884            "output_index": 0,
1885            "item": {"type": "message", "id": "msg_up"}
1886        })))
1887        .await
1888        .unwrap();
1889        tx.send(Ok(serde_json::json!({
1890            "type": "response.output_text.delta",
1891            "output_index": 0,
1892            "delta": "first"
1893        })))
1894        .await
1895        .unwrap();
1896
1897        let (rx, _) = websocket::CodexWebSocketEventStream::pending(rx);
1898        let continuation = ContinuationReservation::for_owner_turn(None, None);
1899        let response = match live_stream_response_once(
1900            rx,
1901            "msg_test".to_string(),
1902            "claude-opus-4-8",
1903            ctx,
1904            continuation,
1905            request_body,
1906            LiveStreamCompaction {
1907                compact_boundary: false,
1908                attempt: None,
1909            },
1910        )
1911        .await
1912        {
1913            LiveStreamStart::Response(response) => response,
1914            LiveStreamStart::Retry { error, .. } => panic!("unexpected retry: {error}"),
1915        };
1916        let mut body = response.into_body();
1917        let first = tokio::time::timeout(Duration::from_millis(200), body.frame())
1918            .await
1919            .expect("initial downstream frame must be available immediately")
1920            .unwrap()
1921            .unwrap()
1922            .into_data()
1923            .unwrap();
1924        let first = String::from_utf8(first.to_vec()).unwrap();
1925        assert!(first.contains("event: message_start"));
1926        assert!(first.contains("event: ping"));
1927        assert!(first.contains("event: content_block_start"));
1928        assert!(first.contains("event: content_block_delta"));
1929
1930        tx.send(Ok(serde_json::json!({
1931            "type": "response.output_text.delta",
1932            "output_index": 0,
1933            "delta": "second"
1934        })))
1935        .await
1936        .unwrap();
1937        let second = tokio::time::timeout(Duration::from_millis(200), body.frame())
1938            .await
1939            .expect("text delta must arrive before the terminal event")
1940            .unwrap()
1941            .unwrap()
1942            .into_data()
1943            .unwrap();
1944        assert!(
1945            String::from_utf8(second.to_vec())
1946                .unwrap()
1947                .contains("event: content_block_delta")
1948        );
1949
1950        for payload in [
1951            serde_json::json!({
1952                "type": "response.output_item.done",
1953                "output_index": 0,
1954                "item": {"type": "message"}
1955            }),
1956            serde_json::json!({
1957                "type": "response.completed",
1958                "response": {
1959                    "id": "resp_1",
1960                    "status": "completed",
1961                    "incomplete_details": null,
1962                    "usage": {"input_tokens": 1, "output_tokens": 1}
1963                }
1964            }),
1965        ] {
1966            tx.send(Ok(payload)).await.unwrap();
1967        }
1968        drop(tx);
1969        while let Some(frame) = body.frame().await {
1970            frame.unwrap();
1971        }
1972    }
1973
1974    #[test]
1975    fn supported_models_includes_fast_variants() {
1976        let provider = CodexProvider::new();
1977        let models = provider.supported_models();
1978        assert!(models.contains(&"gpt-5.6-sol".to_string()));
1979        assert!(models.contains(&"gpt-5.6-sol-fast".to_string()));
1980        assert!(models.contains(&"gpt-5.6-terra".to_string()));
1981        assert!(models.contains(&"gpt-5.6-luna".to_string()));
1982        assert!(models.contains(&"gpt-5.4".to_string()));
1983        assert!(models.contains(&"gpt-5.4-mini".to_string()));
1984    }
1985
1986    #[test]
1987    fn format_expiry_with_future_expiry() {
1988        // 2100-01-01T00:00:00Z in ms
1989        let expires = 4102444800000;
1990        let now = 4102444790000; // 10s before
1991        let output = format_expiry(expires, now);
1992        assert!(output.starts_with("Expires: 2100-01-01T00:00:00.000Z (in "));
1993        assert!(output.ends_with("s)"));
1994    }
1995
1996    #[test]
1997    fn format_expiry_with_past_expiry() {
1998        // 2000-01-01T00:00:00Z in ms
1999        let expires = 946684800000;
2000        let now = 946684810000; // 10s after
2001        let output = format_expiry(expires, now);
2002        assert!(output.starts_with("Expires: 2000-01-01T00:00:00.000Z (in -"));
2003    }
2004
2005    #[tokio::test]
2006    async fn live_upstream_status_and_retry_after_are_preserved() {
2007        let err = client::CodexError {
2008            status: 422,
2009            message: "invalid request".to_string(),
2010            detail: Some("invalid request".to_string()),
2011            retry_after: Some("7".to_string()),
2012            origin: client::CodexErrorOrigin::WebSocketHandshake,
2013        };
2014        let response = map_codex_error_to_response(&err);
2015        assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
2016        assert_eq!(
2017            response.headers().get(http::header::RETRY_AFTER).unwrap(),
2018            "7"
2019        );
2020    }
2021
2022    #[tokio::test]
2023    async fn statusless_codex_error_returns_source_message() {
2024        let err = client::CodexError {
2025            status: 0,
2026            message: "WebSocket connect error: HTTP error: 502 Bad Gateway".to_string(),
2027            detail: None,
2028            retry_after: None,
2029            origin: client::CodexErrorOrigin::WebSocket,
2030        };
2031
2032        let response = map_codex_error_to_response(&err);
2033        assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
2034
2035        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
2036            .await
2037            .unwrap();
2038        let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
2039        assert_eq!(
2040            body.pointer("/error/message").and_then(|v| v.as_str()),
2041            Some("WebSocket connect error: HTTP error: 502 Bad Gateway")
2042        );
2043    }
2044
2045    #[tokio::test]
2046    async fn empty_live_completion_maps_to_explicit_service_unavailable() {
2047        let err = empty_live_completion_error();
2048
2049        assert_eq!(err.status, 503);
2050        assert_eq!(err.detail.as_deref(), Some(EMPTY_CODEX_COMPLETION_DETAIL));
2051        assert_eq!(
2052            map_codex_error_to_response(&err).status(),
2053            StatusCode::SERVICE_UNAVAILABLE
2054        );
2055    }
2056
2057    #[test]
2058    fn live_start_statusless_websocket_handshake_error_is_retryable() {
2059        let err = client::CodexError {
2060            status: 0,
2061            message: "WebSocket connect timeout after 15000ms".to_string(),
2062            detail: None,
2063            retry_after: None,
2064            origin: client::CodexErrorOrigin::WebSocketHandshake,
2065        };
2066
2067        assert!(retryable_live_start_codex_error(&err));
2068    }
2069
2070    #[test]
2071    fn live_start_proxy_tunnel_rejection_is_not_retryable() {
2072        let err = client::CodexError {
2073            status: 0,
2074            message: "WebSocket proxy tunnel was rejected".to_string(),
2075            detail: Some(websocket::WEBSOCKET_PROXY_TUNNEL_REJECTED_DETAIL.to_string()),
2076            retry_after: None,
2077            origin: client::CodexErrorOrigin::WebSocketHandshake,
2078        };
2079
2080        assert!(!retryable_live_start_codex_error(&err));
2081    }
2082
2083    #[test]
2084    fn live_start_keepalive_failure_is_retryable() {
2085        let err = client::CodexError {
2086            status: 0,
2087            message: "WebSocket keepalive error: test write failed".to_string(),
2088            detail: Some(websocket::WEBSOCKET_KEEPALIVE_FAILURE_DETAIL.to_string()),
2089            retry_after: None,
2090            origin: client::CodexErrorOrigin::WebSocket,
2091        };
2092
2093        assert!(retryable_live_start_codex_error(&err));
2094    }
2095
2096    #[test]
2097    fn live_start_payload_retry_detection_covers_rate_limit_and_overload() {
2098        assert!(retryable_live_start_payload(
2099            &serde_json::json!({
2100                "type": "codex.rate_limits",
2101                "rate_limits": {"limit_reached": true}
2102            }),
2103            "rate limit reached",
2104        ));
2105        assert!(retryable_live_start_payload(
2106            &serde_json::json!({
2107                "type": "response.failed",
2108                "response": {"error": {"type": "overloaded_error", "message": "overloaded"}}
2109            }),
2110            "overloaded",
2111        ));
2112        assert!(!retryable_live_start_payload(
2113            &serde_json::json!({
2114                "type": "response.failed",
2115                "response": {"error": {"message": "bad request"}}
2116            }),
2117            "bad request",
2118        ));
2119    }
2120
2121    async fn run_live_failure_case(
2122        session_id: &str,
2123        event: serde_json::Value,
2124        expected_attempts: usize,
2125    ) -> StatusCode {
2126        let owner = ConversationIdentity::Main(session_id.to_string());
2127        continuation::clear_continuation_for_owner(Some(&owner));
2128        websocket::invalidate_codex_websocket_pool_owner(&owner);
2129        let request = live_test_request("one");
2130        let continuation = continuation_candidate_for_owner(Some(&owner), &request, true);
2131        let compaction_attempt = begin_compaction(session_id, &request.model);
2132
2133        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2134        let addr = listener.local_addr().unwrap();
2135        let server = tokio::spawn(async move {
2136            for _ in 0..expected_attempts {
2137                let (socket, _) = listener.accept().await.unwrap();
2138                let mut websocket = tokio_tungstenite::accept_async(socket).await.unwrap();
2139                let _ = next_live_websocket_request(&mut websocket).await;
2140                emit_live_event(&mut websocket, &event).await;
2141                drop(websocket);
2142            }
2143        });
2144        let client = authenticated_live_test_client(format!("http://{addr}/responses"));
2145        let response = tokio::time::timeout(
2146            std::time::Duration::from_secs(15),
2147            live_stream_response(
2148                client,
2149                "message".to_string(),
2150                &request.model,
2151                live_test_context(session_id),
2152                request.clone(),
2153                continuation.clone(),
2154                LiveStreamCompaction {
2155                    compact_boundary: false,
2156                    attempt: Some(compaction_attempt),
2157                },
2158                config::CodexTransport::WebSocket,
2159            ),
2160        )
2161        .await
2162        .expect("live failure case timed out");
2163        tokio::time::timeout(std::time::Duration::from_secs(5), server)
2164            .await
2165            .expect("live failure server timed out")
2166            .expect("live failure server failed");
2167
2168        assert!(!continuation::is_current_turn_for_owner(&continuation));
2169        assert!(!store_compaction(
2170            session_id,
2171            compaction_attempt,
2172            Vec::new()
2173        ));
2174        websocket::invalidate_codex_websocket_pool_owner(&owner);
2175        response.status()
2176    }
2177
2178    #[tokio::test]
2179    async fn dropping_live_stream_during_retry_backoff_aborts_request_state() {
2180        let _registry_guard = continuation::lock_continuation_registry_for_async_tests().await;
2181        let _pool_guard = websocket::lock_codex_websocket_pool_for_tests().await;
2182        let session_id = "live-retry-backoff-cleanup";
2183        let owner = ConversationIdentity::Main(session_id.to_string());
2184        continuation::clear_continuation_for_owner(Some(&owner));
2185        websocket::invalidate_codex_websocket_pool_owner(&owner);
2186        let request = live_test_request("one");
2187        let continuation = continuation_candidate_for_owner(Some(&owner), &request, true);
2188        let compaction_attempt = begin_compaction(session_id, &request.model);
2189
2190        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2191        let addr = listener.local_addr().unwrap();
2192        let (event_sent_tx, event_sent_rx) = tokio::sync::oneshot::channel();
2193        let (socket_closed_tx, socket_closed_rx) = tokio::sync::oneshot::channel();
2194        let server = tokio::spawn(async move {
2195            let (socket, _) = listener.accept().await.unwrap();
2196            let mut websocket = tokio_tungstenite::accept_async(socket).await.unwrap();
2197            let _ = next_live_websocket_request(&mut websocket).await;
2198            emit_live_event(
2199                &mut websocket,
2200                &serde_json::json!({
2201                    "type": "codex.rate_limits",
2202                    "rate_limits": {"allowed": false, "limit_reached": true}
2203                }),
2204            )
2205            .await;
2206            event_sent_tx.send(()).unwrap();
2207            drop(websocket);
2208            socket_closed_tx.send(()).unwrap();
2209        });
2210        let client = authenticated_live_test_client(format!("http://{addr}/responses"));
2211        let task_request = request.clone();
2212        let task_continuation = continuation.clone();
2213        let response_task = tokio::spawn(async move {
2214            let model = task_request.model.clone();
2215            live_stream_response(
2216                client,
2217                "message".to_string(),
2218                &model,
2219                live_test_context(session_id),
2220                task_request,
2221                task_continuation,
2222                LiveStreamCompaction {
2223                    compact_boundary: false,
2224                    attempt: Some(compaction_attempt),
2225                },
2226                config::CodexTransport::WebSocket,
2227            )
2228            .await
2229        });
2230
2231        event_sent_rx.await.unwrap();
2232        tokio::time::timeout(std::time::Duration::from_secs(1), socket_closed_rx)
2233            .await
2234            .expect("retry handoff did not close the abandoned attempt socket")
2235            .expect("retry handoff socket-close sender dropped");
2236        for _ in 0..16 {
2237            tokio::task::yield_now().await;
2238        }
2239        assert!(
2240            !response_task.is_finished(),
2241            "logical request must still be waiting in retry backoff"
2242        );
2243        response_task.abort();
2244        assert!(response_task.await.unwrap_err().is_cancelled());
2245
2246        assert!(!continuation::is_current_turn_for_owner(&continuation));
2247        assert!(!store_compaction(
2248            session_id,
2249            compaction_attempt,
2250            Vec::new()
2251        ));
2252        server.await.unwrap();
2253        websocket::invalidate_codex_websocket_pool_owner(&owner);
2254    }
2255
2256    #[tokio::test]
2257    async fn dropping_live_response_body_after_first_chunk_aborts_request_state() {
2258        let _registry_guard = continuation::lock_continuation_registry_for_async_tests().await;
2259        let _pool_guard = websocket::lock_codex_websocket_pool_for_tests().await;
2260        let session_id = "live-response-body-drop-cleanup";
2261        let owner = ConversationIdentity::Main(session_id.to_string());
2262        continuation::clear_continuation_for_owner(Some(&owner));
2263        websocket::invalidate_codex_websocket_pool_owner(&owner);
2264        let request = live_test_request("one");
2265        let continuation = continuation_candidate_for_owner(Some(&owner), &request, true);
2266        let compaction_attempt = begin_compaction(session_id, &request.model);
2267
2268        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2269        let addr = listener.local_addr().unwrap();
2270        let (socket_closed_tx, socket_closed_rx) = tokio::sync::oneshot::channel();
2271        let server = tokio::spawn(async move {
2272            let (socket, _) = listener.accept().await.unwrap();
2273            let mut websocket = tokio_tungstenite::accept_async(socket).await.unwrap();
2274            let _ = next_live_websocket_request(&mut websocket).await;
2275            emit_live_event(
2276                &mut websocket,
2277                &serde_json::json!({
2278                    "type": "response.output_item.added",
2279                    "output_index": 0,
2280                    "item": {"type": "message", "id": "msg-partial"}
2281                }),
2282            )
2283            .await;
2284            emit_live_event(
2285                &mut websocket,
2286                &serde_json::json!({
2287                    "type": "response.output_text.delta",
2288                    "output_index": 0,
2289                    "delta": "partial"
2290                }),
2291            )
2292            .await;
2293            while websocket.next().await.is_some() {}
2294            socket_closed_tx.send(()).unwrap();
2295        });
2296        let client = authenticated_live_test_client(format!("http://{addr}/responses"));
2297
2298        let response = tokio::time::timeout(
2299            std::time::Duration::from_secs(2),
2300            live_stream_response(
2301                client,
2302                "message".to_string(),
2303                &request.model,
2304                live_test_context(session_id),
2305                request.clone(),
2306                continuation.clone(),
2307                LiveStreamCompaction {
2308                    compact_boundary: false,
2309                    attempt: Some(compaction_attempt),
2310                },
2311                config::CodexTransport::WebSocket,
2312            ),
2313        )
2314        .await
2315        .expect("live response did not publish the first chunk");
2316        let mut body = response.into_body();
2317        tokio::time::timeout(std::time::Duration::from_secs(1), body.frame())
2318            .await
2319            .expect("first downstream chunk timed out")
2320            .expect("live response body ended before the first chunk")
2321            .expect("first downstream chunk failed");
2322        drop(body);
2323
2324        tokio::time::timeout(std::time::Duration::from_secs(1), socket_closed_rx)
2325            .await
2326            .expect("dropping the downstream body did not close the upstream socket")
2327            .expect("socket-close acknowledgement sender dropped");
2328        assert!(!continuation::is_current_turn_for_owner(&continuation));
2329        assert!(!store_compaction(
2330            session_id,
2331            compaction_attempt,
2332            Vec::new()
2333        ));
2334        server.await.unwrap();
2335        websocket::invalidate_codex_websocket_pool_owner(&owner);
2336    }
2337
2338    #[tokio::test]
2339    async fn stale_request_cleanup_preserves_newer_turn_and_compaction_attempt() {
2340        let _registry_guard = continuation::lock_continuation_registry_for_async_tests().await;
2341        let session_id = "stale-live-request-cleanup";
2342        let owner = ConversationIdentity::Main(session_id.to_string());
2343        continuation::clear_continuation_for_owner(Some(&owner));
2344        let request = live_test_request("one");
2345        let stale_continuation = continuation_candidate_for_owner(Some(&owner), &request, true);
2346        let stale_compaction = begin_compaction(session_id, &request.model);
2347        let stale_cleanup = LiveRequestStateCleanup::new(
2348            stale_continuation,
2349            Some(session_id.to_string()),
2350            Some(stale_compaction),
2351        );
2352
2353        let newer_continuation = continuation_candidate_for_owner(Some(&owner), &request, true);
2354        let newer_compaction = begin_compaction(session_id, &request.model);
2355        drop(stale_cleanup);
2356
2357        assert!(continuation::is_current_turn_for_owner(&newer_continuation));
2358        assert!(store_compaction(session_id, newer_compaction, Vec::new()));
2359        abort_request_state(
2360            Some(session_id),
2361            &newer_continuation,
2362            Some(newer_compaction),
2363        );
2364    }
2365
2366    #[tokio::test]
2367    async fn retry_exhaustion_aborts_live_request_state_after_eleven_attempts() {
2368        let _registry_guard = continuation::lock_continuation_registry_for_async_tests().await;
2369        let _pool_guard = websocket::lock_codex_websocket_pool_for_tests().await;
2370        let status = run_live_failure_case(
2371            "live-retry-exhaustion-cleanup",
2372            serde_json::json!({
2373                "type": "codex.rate_limits",
2374                "rate_limits": {
2375                    "allowed": false,
2376                    "limit_reached": true,
2377                    "primary": {"reset_after_seconds": 0}
2378                }
2379            }),
2380            11,
2381        )
2382        .await;
2383        assert_eq!(status, StatusCode::TOO_MANY_REQUESTS);
2384    }
2385
2386    #[tokio::test]
2387    async fn excessive_retry_after_aborts_live_request_state() {
2388        let _registry_guard = continuation::lock_continuation_registry_for_async_tests().await;
2389        let _pool_guard = websocket::lock_codex_websocket_pool_for_tests().await;
2390        let status = run_live_failure_case(
2391            "live-excessive-retry-after-cleanup",
2392            serde_json::json!({
2393                "type": "codex.rate_limits",
2394                "rate_limits": {
2395                    "allowed": false,
2396                    "limit_reached": true,
2397                    "primary": {"reset_after_seconds": 31}
2398                }
2399            }),
2400            1,
2401        )
2402        .await;
2403        assert_eq!(status, StatusCode::TOO_MANY_REQUESTS);
2404    }
2405
2406    #[tokio::test]
2407    async fn nonretryable_live_error_aborts_request_state() {
2408        let _registry_guard = continuation::lock_continuation_registry_for_async_tests().await;
2409        let _pool_guard = websocket::lock_codex_websocket_pool_for_tests().await;
2410        let status = run_live_failure_case(
2411            "live-nonretryable-cleanup",
2412            serde_json::json!({
2413                "type": "response.failed",
2414                "response": {
2415                    "status": "failed",
2416                    "error": {"message": "invalid request"}
2417                }
2418            }),
2419            1,
2420        )
2421        .await;
2422        assert_eq!(status, StatusCode::BAD_GATEWAY);
2423    }
2424
2425    #[tokio::test]
2426    async fn cancellation_while_replacement_startup_is_blocked_aborts_request_state() {
2427        let _registry_guard = continuation::lock_continuation_registry_for_async_tests().await;
2428        let _pool_guard = websocket::lock_codex_websocket_pool_for_tests().await;
2429        let session_id = "live-blocked-replacement-cleanup";
2430        let owner = ConversationIdentity::Main(session_id.to_string());
2431        continuation::clear_continuation_for_owner(Some(&owner));
2432        websocket::invalidate_codex_websocket_pool_owner(&owner);
2433        let request = live_test_request("one");
2434        let continuation = continuation_candidate_for_owner(Some(&owner), &request, true);
2435        let compaction_attempt = begin_compaction(session_id, &request.model);
2436
2437        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2438        let addr = listener.local_addr().unwrap();
2439        let (replacement_accepted_tx, replacement_accepted_rx) = tokio::sync::oneshot::channel();
2440        let (release_replacement_tx, release_replacement_rx) = tokio::sync::oneshot::channel();
2441        let server = tokio::spawn(async move {
2442            let (first_socket, _) = listener.accept().await.unwrap();
2443            let mut first_websocket = tokio_tungstenite::accept_async(first_socket).await.unwrap();
2444            let _ = next_live_websocket_request(&mut first_websocket).await;
2445            emit_live_event(
2446                &mut first_websocket,
2447                &serde_json::json!({
2448                    "type": "codex.rate_limits",
2449                    "rate_limits": {
2450                        "allowed": false,
2451                        "limit_reached": true,
2452                        "primary": {"reset_after_seconds": 0}
2453                    }
2454                }),
2455            )
2456            .await;
2457            drop(first_websocket);
2458
2459            let (_replacement_socket, _) = listener.accept().await.unwrap();
2460            replacement_accepted_tx.send(()).unwrap();
2461            let _ = release_replacement_rx.await;
2462        });
2463        let client = authenticated_live_test_client(format!("http://{addr}/responses"));
2464        let task_request = request.clone();
2465        let task_continuation = continuation.clone();
2466        let response_task = tokio::spawn(async move {
2467            let model = task_request.model.clone();
2468            live_stream_response(
2469                client,
2470                "message".to_string(),
2471                &model,
2472                live_test_context(session_id),
2473                task_request,
2474                task_continuation,
2475                LiveStreamCompaction {
2476                    compact_boundary: false,
2477                    attempt: Some(compaction_attempt),
2478                },
2479                config::CodexTransport::WebSocket,
2480            )
2481            .await
2482        });
2483
2484        tokio::time::timeout(std::time::Duration::from_secs(2), replacement_accepted_rx)
2485            .await
2486            .expect("replacement startup did not reach the blocked handshake")
2487            .expect("replacement startup acknowledgement sender dropped");
2488        response_task.abort();
2489        assert!(response_task.await.unwrap_err().is_cancelled());
2490        let _ = release_replacement_tx.send(());
2491        server.await.unwrap();
2492
2493        assert!(!continuation::is_current_turn_for_owner(&continuation));
2494        assert!(!store_compaction(
2495            session_id,
2496            compaction_attempt,
2497            Vec::new()
2498        ));
2499        websocket::invalidate_codex_websocket_pool_owner(&owner);
2500    }
2501}