From c9b68787acbde93d59548ce00806745c110ae430 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 11 Feb 2026 23:55:19 -0800 Subject: [PATCH] Handle response.incomplete --- codex-rs/codex-api/src/sse/responses.rs | 11 +++++ codex-rs/core/tests/suite/client.rs | 60 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/codex-rs/codex-api/src/sse/responses.rs b/codex-rs/codex-api/src/sse/responses.rs index d7ba3383b74..11e4f9de34c 100644 --- a/codex-rs/codex-api/src/sse/responses.rs +++ b/codex-rs/codex-api/src/sse/responses.rs @@ -254,6 +254,17 @@ pub fn process_responses_event( "response.failed event received".into(), ))); } + "response.incomplete" => { + let reason = event.response.as_ref().and_then(|response| { + response + .get("incomplete_details") + .and_then(|details| details.get("reason")) + .and_then(Value::as_str) + }); + let reason = reason.unwrap_or("unknown"); + let message = format!("Incomplete response returned, reason: {reason}"); + return Err(ResponsesEventError::Api(ApiError::Stream(message))); + } "response.completed" => { if let Some(resp_val) = event.response { match serde_json::from_value::(resp_val) { diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index b7b161aa86f..ed6912daae0 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -37,6 +37,8 @@ use codex_protocol::user_input::UserInput; use core_test_support::load_default_config_for_test; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed_with_tokens; +use core_test_support::responses::ev_message_item_added; +use core_test_support::responses::ev_output_text_delta; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_once; use core_test_support::responses::mount_sse_once_match; @@ -1787,6 +1789,64 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn incomplete_response_emits_content_filter_error_message() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = MockServer::start().await; + + let incomplete_response = sse(vec![ + ev_response_created("resp_incomplete"), + ev_message_item_added("msg_incomplete", "partial content"), + ev_output_text_delta("continued chunk"), + json!({ + "type": "response.incomplete", + "response": { + "id": "resp_incomplete", + "object": "response", + "status": "incomplete", + "error": null, + "incomplete_details": { + "reason": "content_filter" + } + } + }), + ]); + + let responses_mock = mount_sse_once(&server, incomplete_response).await; + + let TestCodex { codex, .. } = test_codex() + .with_config(|config| { + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await?; + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "trigger incomplete".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }) + .await?; + + let error_event = wait_for_event(&codex, |ev| matches!(ev, EventMsg::Error(_))).await; + assert!( + matches!( + error_event, + EventMsg::Error(ref err) + if err.message + == "stream disconnected before completion: Incomplete response returned, reason: content_filter" + ), + "expected incomplete content filter error; got {error_event:?}" + ); + + assert_eq!(responses_mock.requests().len(), 1); + + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn azure_overrides_assign_properties_used_for_responses_url() { skip_if_no_network!();