From 0136a731d73e681bbc7826c9cf7ae2ae09d89fe9 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Sun, 14 Jun 2026 13:41:59 -0700 Subject: [PATCH 1/7] Use input items for Responses Lite tools --- codex-rs/codex-api/src/common.rs | 9 +- codex-rs/codex-api/tests/clients.rs | 4 +- codex-rs/core/src/agent/control/spawn.rs | 3 +- codex-rs/core/src/client.rs | 24 +++++- codex-rs/core/src/client_common.rs | 3 +- codex-rs/core/src/client_common_tests.rs | 20 ++--- codex-rs/core/src/compact_remote.rs | 3 +- codex-rs/core/src/context_manager/history.rs | 9 +- codex-rs/core/src/image_preparation.rs | 3 +- codex-rs/core/src/session/turn.rs | 3 +- codex-rs/core/src/turn_timing.rs | 3 +- codex-rs/core/tests/suite/responses_lite.rs | 82 ++++++++++++++++--- codex-rs/ext/image-generation/src/tool.rs | 6 +- codex-rs/otel/src/events/session_telemetry.rs | 1 + codex-rs/protocol/src/models.rs | 6 ++ codex-rs/rollout/src/policy.rs | 6 +- 16 files changed, 139 insertions(+), 46 deletions(-) diff --git a/codex-rs/codex-api/src/common.rs b/codex-rs/codex-api/src/common.rs index aeea85e7fc8b..10eab17c69ef 100644 --- a/codex-rs/codex-api/src/common.rs +++ b/codex-rs/codex-api/src/common.rs @@ -28,7 +28,8 @@ pub struct CompactionInput<'a> { pub input: &'a [ResponseItem], #[serde(skip_serializing_if = "str::is_empty")] pub instructions: &'a str, - pub tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option>, pub parallel_tool_calls: bool, #[serde(skip_serializing_if = "Option::is_none")] pub reasoning: Option, @@ -192,7 +193,8 @@ pub struct ResponsesApiRequest { #[serde(skip_serializing_if = "String::is_empty")] pub instructions: String, pub input: Vec, - pub tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option>, pub tool_choice: String, pub parallel_tool_calls: bool, pub reasoning: Option, @@ -240,7 +242,8 @@ pub struct ResponseCreateWsRequest { #[serde(skip_serializing_if = "Option::is_none")] pub previous_response_id: Option, pub input: Vec, - pub tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option>, pub tool_choice: String, pub parallel_tool_calls: bool, pub reasoning: Option, diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index 2d69f838603d..230b3beeffee 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -401,7 +401,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> { model: "gpt-test".into(), instructions: "Say hi".into(), input: Vec::new(), - tools: Vec::new(), + tools: Some(Vec::new()), tool_choice: "auto".into(), parallel_tool_calls: false, reasoning: None, @@ -520,7 +520,7 @@ async fn azure_store_sends_ids_and_headers() -> Result<()> { phase: None, internal_chat_message_metadata_passthrough: None, }], - tools: Vec::new(), + tools: Some(Vec::new()), tool_choice: "auto".into(), parallel_tool_calls: false, reasoning: None, diff --git a/codex-rs/core/src/agent/control/spawn.rs b/codex-rs/core/src/agent/control/spawn.rs index 05fac6aaf3b3..37a3a8105ea8 100644 --- a/codex-rs/core/src/agent/control/spawn.rs +++ b/codex-rs/core/src/agent/control/spawn.rs @@ -41,7 +41,8 @@ fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item: _ => false, }, RolloutItem::ResponseItem( - ResponseItem::AgentMessage { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::AgentMessage { .. } | ResponseItem::Reasoning { .. } | ResponseItem::LocalShellCall { .. } | ResponseItem::FunctionCall { .. } diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index c6cb92907f8d..774704953279 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -73,6 +73,7 @@ use codex_otel::current_span_w3c_trace_context; use codex_protocol::ThreadId; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::config_types::Verbosity as VerbosityConfig; +use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; @@ -778,7 +779,6 @@ impl ModelClient { service_tier: Option, responses_metadata: &CodexResponsesMetadata, ) -> Result { - let instructions = &prompt.base_instructions.text; let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite); if !self.state.provider.info().is_openai() { input @@ -786,6 +786,26 @@ impl ModelClient { .for_each(ResponseItem::clear_internal_chat_message_metadata_passthrough); } let tools = create_tools_json_for_responses_api(&prompt.tools)?; + let (instructions, tools) = if model_info.use_responses_lite { + let mut prefix = vec![ResponseItem::AdditionalTools { + role: "developer".to_string(), + tools, + }]; + if !prompt.base_instructions.text.is_empty() { + prefix.push(ResponseItem::Message { + id: None, + role: "developer".to_string(), + content: vec![ContentItem::InputText { + text: prompt.base_instructions.text.clone(), + }], + phase: None, + }); + } + input.splice(0..0, prefix); + (String::new(), None) + } else { + (prompt.base_instructions.text.clone(), Some(tools)) + }; let reasoning = Self::build_reasoning(model_info, effort, summary); let include = if reasoning.is_some() { vec!["reasoning.encrypted_content".to_string()] @@ -812,7 +832,7 @@ impl ModelClient { let service_tier = model_info.service_tier_for_request(service_tier); let request = ResponsesApiRequest { model: model_info.slug.clone(), - instructions: instructions.clone(), + instructions, input, tools, tool_choice: "auto".to_string(), diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index e0c2ff03c302..54ad70daaaaf 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -83,7 +83,8 @@ fn strip_image_details(items: &mut [ResponseItem]) { } } } - ResponseItem::Reasoning { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::Reasoning { .. } | ResponseItem::AgentMessage { .. } | ResponseItem::LocalShellCall { .. } | ResponseItem::FunctionCall { .. } diff --git a/codex-rs/core/src/client_common_tests.rs b/codex-rs/core/src/client_common_tests.rs index 8b25c7e30078..9dfef1062287 100644 --- a/codex-rs/core/src/client_common_tests.rs +++ b/codex-rs/core/src/client_common_tests.rs @@ -104,13 +104,11 @@ fn responses_lite_request_copies_strip_image_details() { #[test] fn serializes_text_verbosity_when_set() { - let input: Vec = vec![]; - let tools: Vec = vec![]; let req = ResponsesApiRequest { model: "gpt-5.4".to_string(), instructions: "i".to_string(), - input, - tools, + input: vec![], + tools: Some(vec![]), tool_choice: "auto".to_string(), parallel_tool_calls: true, reasoning: None, @@ -137,8 +135,6 @@ fn serializes_text_verbosity_when_set() { #[test] fn serializes_text_schema_with_strict_format() { - let input: Vec = vec![]; - let tools: Vec = vec![]; let schema = serde_json::json!({ "type": "object", "properties": { @@ -156,8 +152,8 @@ fn serializes_text_schema_with_strict_format() { let req = ResponsesApiRequest { model: "gpt-5.4".to_string(), instructions: "i".to_string(), - input, - tools, + input: vec![], + tools: Some(vec![]), tool_choice: "auto".to_string(), parallel_tool_calls: true, reasoning: None, @@ -212,13 +208,11 @@ fn serializes_text_schema_with_non_strict_format() { #[test] fn omits_text_when_not_set() { - let input: Vec = vec![]; - let tools: Vec = vec![]; let req = ResponsesApiRequest { model: "gpt-5.4".to_string(), instructions: "i".to_string(), - input, - tools, + input: vec![], + tools: Some(vec![]), tool_choice: "auto".to_string(), parallel_tool_calls: true, reasoning: None, @@ -241,7 +235,7 @@ fn serializes_flex_service_tier_when_set() { model: "gpt-5.4".to_string(), instructions: "i".to_string(), input: vec![], - tools: vec![], + tools: Some(vec![]), tool_choice: "auto".to_string(), parallel_tool_calls: true, reasoning: None, diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 3d231406847b..48784141a818 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -352,7 +352,8 @@ pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool { ResponseItem::AgentMessage { .. } => true, ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true, ResponseItem::CompactionTrigger { .. } => false, - ResponseItem::Reasoning { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::Reasoning { .. } | ResponseItem::LocalShellCall { .. } | ResponseItem::FunctionCall { .. } | ResponseItem::ToolSearchCall { .. } diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 0244f7f96f38..b474ed1a9a78 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -386,7 +386,8 @@ impl ContextManager { output: truncate_function_output_payload(output, policy_with_serialization_budget), internal_chat_message_metadata_passthrough: metadata.clone(), }, - ResponseItem::Message { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::Message { .. } | ResponseItem::AgentMessage { .. } | ResponseItem::Reasoning { .. } | ResponseItem::LocalShellCall { .. } @@ -476,7 +477,8 @@ pub(crate) fn truncate_function_output_payload( fn is_api_message(message: &ResponseItem) -> bool { match message { ResponseItem::Message { role, .. } => role.as_str() != "system", - ResponseItem::AgentMessage { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::AgentMessage { .. } | ResponseItem::FunctionCallOutput { .. } | ResponseItem::FunctionCall { .. } | ResponseItem::ToolSearchCall { .. } @@ -722,7 +724,8 @@ fn is_model_generated_item(item: &ResponseItem) -> bool { | ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true, ResponseItem::CompactionTrigger { .. } => false, - ResponseItem::FunctionCallOutput { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::FunctionCallOutput { .. } | ResponseItem::ToolSearchOutput { .. } | ResponseItem::CustomToolCallOutput { .. } | ResponseItem::AgentMessage { .. } diff --git a/codex-rs/core/src/image_preparation.rs b/codex-rs/core/src/image_preparation.rs index 87ac444df595..111ce9b95560 100644 --- a/codex-rs/core/src/image_preparation.rs +++ b/codex-rs/core/src/image_preparation.rs @@ -57,7 +57,8 @@ pub(crate) fn prepare_response_items(items: &mut [ResponseItem]) { prepare_tool_output_content(content); } } - ResponseItem::Reasoning { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::Reasoning { .. } | ResponseItem::AgentMessage { .. } | ResponseItem::LocalShellCall { .. } | ResponseItem::FunctionCall { .. } diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 81f9f5235674..50dac0488901 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -2091,7 +2091,8 @@ async fn try_run_sampling_request( } ResponseItem::Reasoning { .. } => true, ResponseItem::AgentMessage { .. } => false, - ResponseItem::LocalShellCall { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::LocalShellCall { .. } | ResponseItem::FunctionCall { .. } | ResponseItem::ToolSearchCall { .. } | ResponseItem::FunctionCallOutput { .. } diff --git a/codex-rs/core/src/turn_timing.rs b/codex-rs/core/src/turn_timing.rs index e7a90800b0fe..04f7aeb69be9 100644 --- a/codex-rs/core/src/turn_timing.rs +++ b/codex-rs/core/src/turn_timing.rs @@ -380,7 +380,8 @@ fn response_item_records_turn_ttft(item: &ResponseItem) -> bool { | ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true, ResponseItem::CompactionTrigger { .. } => false, - ResponseItem::FunctionCallOutput { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::FunctionCallOutput { .. } | ResponseItem::CustomToolCallOutput { .. } | ResponseItem::ToolSearchOutput { .. } | ResponseItem::Other => false, diff --git a/codex-rs/core/tests/suite/responses_lite.rs b/codex-rs/core/tests/suite/responses_lite.rs index 8e633522f4bb..b51348ed5433 100644 --- a/codex-rs/core/tests/suite/responses_lite.rs +++ b/codex-rs/core/tests/suite/responses_lite.rs @@ -54,6 +54,70 @@ fn has_hosted_tool(tools: &[Value], tool_type: &str) -> bool { .any(|tool| tool.get("type").and_then(Value::as_str) == Some(tool_type)) } +fn additional_tools(body: &Value) -> Result<&[Value]> { + body["input"] + .as_array() + .context("Responses request input should be an array")? + .iter() + .find(|item| item.get("type").and_then(Value::as_str) == Some("additional_tools")) + .context("Responses request should contain additional_tools")?["tools"] + .as_array() + .map(Vec::as_slice) + .context("additional_tools tools should be an array") +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn responses_lite_uses_input_items_for_instructions_and_tools() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let response_mock = responses::mount_sse_once( + &server, + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_completed("resp-1"), + ]), + ) + .await; + + let mut builder = test_codex() + .with_model_info_override("gpt-5.4", |model_info| { + model_info.use_responses_lite = true; + }) + .with_config(|config| { + config.base_instructions = Some("test instructions".to_string()); + }); + let test = builder.build(&server).await?; + + test.submit_turn("hello").await?; + + let body = response_mock.single_request().body_json(); + assert!(body.get("instructions").is_none()); + assert!(body.get("tools").is_none()); + + let input = body["input"] + .as_array() + .context("Responses request input should be an array")?; + assert_eq!(input[0]["type"], "additional_tools"); + assert_eq!(input[0]["role"], "developer"); + assert_eq!( + input[1], + serde_json::json!({ + "type": "message", + "role": "developer", + "content": [{ + "type": "input_text", + "text": "test instructions", + }], + }) + ); + + let tools = additional_tools(&body)?; + assert!(!tools.is_empty()); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn responses_lite_prepares_images() -> Result<()> { skip_if_no_network!(Ok(())); @@ -158,17 +222,10 @@ async fn responses_lite_uses_standalone_web_search_and_image_generation() -> Res request.header(RESPONSES_LITE_HEADER).as_deref(), Some("true") ); - request - .tool_by_name("web", "run") - .context("Responses Lite should expose standalone web search")?; - request - .tool_by_name("image_gen", "imagegen") - .context("Responses Lite should expose standalone image generation")?; - let body = request.body_json(); - let tools = body["tools"] - .as_array() - .context("Responses request tools should be an array")?; + assert!(body.get("tools").is_none()); + let tools = additional_tools(&body)?; + assert!(!tools.is_empty()); assert!(!has_hosted_tool(tools, "web_search")); assert!(!has_hosted_tool(tools, "image_generation")); @@ -256,9 +313,8 @@ async fn responses_lite_omits_hosted_tools_without_standalone_extensions() -> Re test.submit_turn("Do not use hosted tools").await?; let body = response_mock.single_request().body_json(); - let tools = body["tools"] - .as_array() - .context("Responses request tools should be an array")?; + assert!(body.get("tools").is_none()); + let tools = additional_tools(&body)?; assert!(!has_hosted_tool(tools, "web_search")); assert!(!has_hosted_tool(tools, "image_generation")); diff --git a/codex-rs/ext/image-generation/src/tool.rs b/codex-rs/ext/image-generation/src/tool.rs index 3e2cfd1792b7..d4451a89268c 100644 --- a/codex-rs/ext/image-generation/src/tool.rs +++ b/codex-rs/ext/image-generation/src/tool.rs @@ -256,7 +256,8 @@ fn recent_images(history: &[ResponseItem], count: usize) -> Vec { ResponseItem::CustomToolCall { call_id, .. } => { custom_tool_call_ids.insert(call_id.as_str()); } - ResponseItem::Message { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::Message { .. } | ResponseItem::AgentMessage { .. } | ResponseItem::Reasoning { .. } | ResponseItem::LocalShellCall { .. } @@ -296,7 +297,8 @@ fn recent_images(history: &[ResponseItem], count: usize) -> Vec { ResponseItem::ImageGenerationCall { result, .. } if !result.is_empty() => { image_urls.push(format!("data:image/png;base64,{result}")); } - ResponseItem::Reasoning { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::Reasoning { .. } | ResponseItem::AgentMessage { .. } | ResponseItem::LocalShellCall { .. } | ResponseItem::FunctionCall { .. } diff --git a/codex-rs/otel/src/events/session_telemetry.rs b/codex-rs/otel/src/events/session_telemetry.rs index efff5c24e3e6..f457f2e00bc4 100644 --- a/codex-rs/otel/src/events/session_telemetry.rs +++ b/codex-rs/otel/src/events/session_telemetry.rs @@ -1182,6 +1182,7 @@ impl SessionTelemetry { fn responses_item_type(item: &ResponseItem) -> String { match item { + ResponseItem::AdditionalTools { .. } => "additional_tools".into(), ResponseItem::Message { role, .. } => format!("message_from_{role}"), ResponseItem::AgentMessage { .. } => "agent_message".into(), ResponseItem::Reasoning { .. } => "reasoning".into(), diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index e107013a78db..e6703559864b 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -932,6 +932,12 @@ impl InternalChatMessageMetadataPassthrough { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ResponseItem { + #[schemars(skip)] + #[ts(skip)] + AdditionalTools { + role: String, + tools: Vec, + }, Message { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] diff --git a/codex-rs/rollout/src/policy.rs b/codex-rs/rollout/src/policy.rs index b342d699b7b5..be72d622fcd2 100644 --- a/codex-rs/rollout/src/policy.rs +++ b/codex-rs/rollout/src/policy.rs @@ -30,7 +30,8 @@ pub fn persisted_rollout_items(items: &[RolloutItem]) -> Vec { #[inline] pub fn should_persist_response_item(item: &ResponseItem) -> bool { match item { - ResponseItem::Message { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::Message { .. } | ResponseItem::AgentMessage { .. } | ResponseItem::Reasoning { .. } | ResponseItem::LocalShellCall { .. } @@ -62,7 +63,8 @@ pub fn should_persist_response_item_for_memories(item: &ResponseItem) -> bool { | ResponseItem::CustomToolCall { .. } | ResponseItem::CustomToolCallOutput { .. } | ResponseItem::WebSearchCall { .. } => true, - ResponseItem::AgentMessage { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::AgentMessage { .. } | ResponseItem::Reasoning { .. } | ResponseItem::ImageGenerationCall { .. } | ResponseItem::Compaction { .. } From 65342049c6ec17c043c620d8cafe90a9c5b37fee Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Tue, 16 Jun 2026 18:27:06 -0700 Subject: [PATCH 2/7] [codex] Harden Responses Lite input items --- .../src/endpoint/responses_websocket.rs | 4 +- codex-rs/codex-api/tests/clients.rs | 2 +- codex-rs/core/src/client.rs | 15 ++++- codex-rs/core/tests/suite/responses_lite.rs | 60 +++++++++++++++++++ codex-rs/protocol/src/models.rs | 4 +- .../src/reducer/conversation/normalize.rs | 3 + .../src/reducer/conversation_tests.rs | 42 +++++++++++++ codex-rs/rollout/src/policy.rs | 8 +-- 8 files changed, 127 insertions(+), 11 deletions(-) diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs index d44825154a94..0761e47faa3a 100644 --- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs +++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs @@ -827,11 +827,11 @@ mod tests { phase: None, internal_chat_message_metadata_passthrough: None, }], - tools: vec![json!({ + tools: Some(vec![json!({ "type": "function", "name": "lookup", "parameters": {"type": "object"} - })], + })]), tool_choice: "auto".to_string(), parallel_tool_calls: true, reasoning: None, diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index 230b3beeffee..60a9852cfa51 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -315,7 +315,7 @@ async fn responses_client_stream_request_preserves_item_ids() -> Result<()> { phase: None, internal_chat_message_metadata_passthrough: None, }], - tools: Vec::new(), + tools: Some(Vec::new()), tool_choice: "auto".into(), parallel_tool_calls: false, reasoning: None, diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 774704953279..2882ae5504e6 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -84,6 +84,7 @@ use codex_rollout_trace::CompactionTraceContext; use codex_rollout_trace::InferenceTraceAttempt; use codex_rollout_trace::InferenceTraceContext; use codex_tools::create_tools_json_for_responses_api; +use codex_utils_output_truncation::approx_tokens_from_byte_count; use eventsource_stream::Event; use eventsource_stream::EventStreamError; use futures::StreamExt; @@ -150,6 +151,7 @@ const X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE_HEADER: &str = "x-openai-internal-codex-responses-lite"; const RESPONSES_ENDPOINT: &str = "/responses"; const RESPONSES_COMPACT_ENDPOINT: &str = "/responses/compact"; +const RESPONSES_LITE_ADDITIONAL_TOOLS_MAX_TOKENS: u64 = 10_000; // `/responses/compact` is unary, so the timeout covers the full response rather than one idle // period between stream events. const COMPACT_REQUEST_TIMEOUT_IDLE_MULTIPLIER: u32 = 4; @@ -787,10 +789,18 @@ impl ModelClient { } let tools = create_tools_json_for_responses_api(&prompt.tools)?; let (instructions, tools) = if model_info.use_responses_lite { - let mut prefix = vec![ResponseItem::AdditionalTools { + let additional_tools = ResponseItem::AdditionalTools { role: "developer".to_string(), tools, - }]; + }; + let additional_tools_tokens = + approx_tokens_from_byte_count(serde_json::to_vec(&additional_tools)?.len()); + if additional_tools_tokens > RESPONSES_LITE_ADDITIONAL_TOOLS_MAX_TOKENS { + return Err(CodexErr::InvalidRequest(format!( + "Responses Lite additional tools exceed the {RESPONSES_LITE_ADDITIONAL_TOOLS_MAX_TOKENS}-token limit ({additional_tools_tokens} estimated tokens); defer tools behind tool search or reduce their schemas" + ))); + } + let mut prefix = vec![additional_tools]; if !prompt.base_instructions.text.is_empty() { prefix.push(ResponseItem::Message { id: None, @@ -799,6 +809,7 @@ impl ModelClient { text: prompt.base_instructions.text.clone(), }], phase: None, + metadata: None, }); } input.splice(0..0, prefix); diff --git a/codex-rs/core/tests/suite/responses_lite.rs b/codex-rs/core/tests/suite/responses_lite.rs index b51348ed5433..93144827b781 100644 --- a/codex-rs/core/tests/suite/responses_lite.rs +++ b/codex-rs/core/tests/suite/responses_lite.rs @@ -9,6 +9,8 @@ use codex_features::Feature; use codex_image_generation_extension::install as install_image_generation_extension; use codex_login::CodexAuth; use codex_protocol::config_types::WebSearchMode; +use codex_protocol::dynamic_tools::DynamicToolFunctionSpec; +use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::ImageDetail; use codex_protocol::openai_models::InputModality; use codex_protocol::protocol::EventMsg; @@ -118,6 +120,64 @@ async fn responses_lite_uses_input_items_for_instructions_and_tools() -> Result< Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn responses_lite_rejects_oversized_additional_tools_item() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let oversized_tool = DynamicToolSpec::Function(DynamicToolFunctionSpec { + name: "oversized_tool".to_string(), + description: "x".repeat(50_000), + input_schema: serde_json::json!({ + "type": "object", + "properties": {}, + "additionalProperties": false, + }), + defer_loading: false, + }); + let mut builder = test_codex().with_model_info_override("gpt-5.4", |model_info| { + model_info.use_responses_lite = true; + }); + let mut test = builder.build(&server).await?; + let new_thread = test + .thread_manager + .start_thread_with_tools(test.config.clone(), vec![oversized_tool]) + .await?; + test.codex = new_thread.thread; + test.session_configured = new_thread.session_configured; + + test.codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "hello".to_string(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + additional_context: Default::default(), + thread_settings: Default::default(), + }) + .await?; + let error = wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await; + let EventMsg::Error(error) = error else { + unreachable!(); + }; + assert!( + error + .message + .starts_with("Responses Lite additional tools exceed the 10000-token limit") + ); + assert!( + server + .received_requests() + .await + .unwrap_or_default() + .is_empty() + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn responses_lite_prepares_images() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index e6703559864b..08c64470f072 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -1288,7 +1288,7 @@ impl ResponseItem { internal_chat_message_metadata_passthrough: metadata, .. } => metadata.as_ref(), - Self::CompactionTrigger { .. } | Self::Other => None, + Self::CompactionTrigger { .. } | Self::AdditionalTools { .. } | Self::Other => None, } } @@ -1352,7 +1352,7 @@ impl ResponseItem { internal_chat_message_metadata_passthrough: metadata, .. } => Some(metadata), - Self::CompactionTrigger { .. } | Self::Other => None, + Self::CompactionTrigger { .. } | Self::AdditionalTools { .. } | Self::Other => None, } } } diff --git a/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs b/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs index e7a3253f6fec..f97cc65f6731 100644 --- a/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs +++ b/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs @@ -37,6 +37,9 @@ pub(super) fn normalize_model_items( ) -> Result> { let mut normalized_items = Vec::new(); for item in items { + if item.get("type").and_then(Value::as_str) == Some("additional_tools") { + continue; + } normalized_items.push(normalize_model_item(item, raw_payload)?); } Ok(normalized_items) diff --git a/codex-rs/rollout-trace/src/reducer/conversation_tests.rs b/codex-rs/rollout-trace/src/reducer/conversation_tests.rs index 1cf13b1fa8d6..18d38a417002 100644 --- a/codex-rs/rollout-trace/src/reducer/conversation_tests.rs +++ b/codex-rs/rollout-trace/src/reducer/conversation_tests.rs @@ -735,6 +735,48 @@ fn unsupported_model_item_is_reducer_error() -> anyhow::Result<()> { ) } +#[test] +fn additional_tools_are_excluded_from_request_conversation() -> anyhow::Result<()> { + let temp = TempDir::new()?; + let writer = create_started_writer(&temp)?; + start_turn(&writer, "turn-1")?; + + let request = writer.write_json_payload( + RawPayloadKind::InferenceRequest, + &json!({ + "input": [ + { + "type": "additional_tools", + "role": "developer", + "tools": [{ + "type": "function", + "name": "lookup", + "parameters": {"type": "object", "properties": {}} + }] + }, + message("user", "find it") + ] + }), + )?; + append_inference_start(&writer, "inference-1", "turn-1", request)?; + + let rollout = replay_bundle(temp.path())?; + let request_item_ids = &rollout.inference_calls["inference-1"].request_item_ids; + + assert_eq!(request_item_ids.len(), 1); + assert_eq!(rollout.conversation_items.len(), 1); + assert_eq!( + rollout.conversation_items[&request_item_ids[0]].body, + ConversationBody { + parts: vec![ConversationPart::Text { + text: "find it".to_string(), + }], + } + ); + + Ok(()) +} + #[test] fn missing_request_input_is_reducer_error() -> anyhow::Result<()> { let temp = TempDir::new()?; diff --git a/codex-rs/rollout/src/policy.rs b/codex-rs/rollout/src/policy.rs index be72d622fcd2..4db26576b091 100644 --- a/codex-rs/rollout/src/policy.rs +++ b/codex-rs/rollout/src/policy.rs @@ -30,8 +30,7 @@ pub fn persisted_rollout_items(items: &[RolloutItem]) -> Vec { #[inline] pub fn should_persist_response_item(item: &ResponseItem) -> bool { match item { - ResponseItem::AdditionalTools { .. } - | ResponseItem::Message { .. } + ResponseItem::Message { .. } | ResponseItem::AgentMessage { .. } | ResponseItem::Reasoning { .. } | ResponseItem::LocalShellCall { .. } @@ -45,8 +44,9 @@ pub fn should_persist_response_item(item: &ResponseItem) -> bool { | ResponseItem::ImageGenerationCall { .. } | ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true, - ResponseItem::CompactionTrigger { .. } => false, - ResponseItem::Other => false, + ResponseItem::AdditionalTools { .. } + | ResponseItem::CompactionTrigger { .. } + | ResponseItem::Other => false, } } From 825533388652b7fdc3925343a05d29dfc87ba7c5 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Tue, 16 Jun 2026 19:56:16 -0700 Subject: [PATCH 3/7] [codex] Simplify Responses Lite request tests --- codex-rs/core/src/client_common_tests.rs | 18 ++++++++++++------ codex-rs/core/tests/suite/responses_lite.rs | 6 +++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/codex-rs/core/src/client_common_tests.rs b/codex-rs/core/src/client_common_tests.rs index 9dfef1062287..38c4aaecb5c9 100644 --- a/codex-rs/core/src/client_common_tests.rs +++ b/codex-rs/core/src/client_common_tests.rs @@ -104,11 +104,13 @@ fn responses_lite_request_copies_strip_image_details() { #[test] fn serializes_text_verbosity_when_set() { + let input: Vec = vec![]; + let tools: Vec = vec![]; let req = ResponsesApiRequest { model: "gpt-5.4".to_string(), instructions: "i".to_string(), - input: vec![], - tools: Some(vec![]), + input, + tools: Some(tools), tool_choice: "auto".to_string(), parallel_tool_calls: true, reasoning: None, @@ -135,6 +137,8 @@ fn serializes_text_verbosity_when_set() { #[test] fn serializes_text_schema_with_strict_format() { + let input: Vec = vec![]; + let tools: Vec = vec![]; let schema = serde_json::json!({ "type": "object", "properties": { @@ -152,8 +156,8 @@ fn serializes_text_schema_with_strict_format() { let req = ResponsesApiRequest { model: "gpt-5.4".to_string(), instructions: "i".to_string(), - input: vec![], - tools: Some(vec![]), + input, + tools: Some(tools), tool_choice: "auto".to_string(), parallel_tool_calls: true, reasoning: None, @@ -208,11 +212,13 @@ fn serializes_text_schema_with_non_strict_format() { #[test] fn omits_text_when_not_set() { + let input: Vec = vec![]; + let tools: Vec = vec![]; let req = ResponsesApiRequest { model: "gpt-5.4".to_string(), instructions: "i".to_string(), - input: vec![], - tools: Some(vec![]), + input, + tools: Some(tools), tool_choice: "auto".to_string(), parallel_tool_calls: true, reasoning: None, diff --git a/codex-rs/core/tests/suite/responses_lite.rs b/codex-rs/core/tests/suite/responses_lite.rs index 93144827b781..cde5111d6a34 100644 --- a/codex-rs/core/tests/suite/responses_lite.rs +++ b/codex-rs/core/tests/suite/responses_lite.rs @@ -60,9 +60,9 @@ fn additional_tools(body: &Value) -> Result<&[Value]> { body["input"] .as_array() .context("Responses request input should be an array")? - .iter() - .find(|item| item.get("type").and_then(Value::as_str) == Some("additional_tools")) - .context("Responses request should contain additional_tools")?["tools"] + .first() + .filter(|item| item.get("type").and_then(Value::as_str) == Some("additional_tools")) + .context("Responses request should start with additional_tools")?["tools"] .as_array() .map(Vec::as_slice) .context("additional_tools tools should be an array") From f411b1e794742cbefe9ca25a0f3d70d470775cd0 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Mon, 22 Jun 2026 00:03:47 -0700 Subject: [PATCH 4/7] [codex] Preserve additional tools item IDs --- codex-rs/core/src/client.rs | 1 + codex-rs/core/src/session/mod.rs | 1 + codex-rs/core/src/session/tests.rs | 13 +++++++++++++ codex-rs/protocol/src/models.rs | 16 ++++++++++++++-- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 2882ae5504e6..8925cd718591 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -790,6 +790,7 @@ impl ModelClient { let tools = create_tools_json_for_responses_api(&prompt.tools)?; let (instructions, tools) = if model_info.use_responses_lite { let additional_tools = ResponseItem::AdditionalTools { + id: None, role: "developer".to_string(), tools, }; diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 1bd5bae8bf0a..b614c9355e35 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -2714,6 +2714,7 @@ impl Session { continue; } let prefix = match item { + ResponseItem::AdditionalTools { .. } => "at", ResponseItem::Message { .. } => "msg", ResponseItem::Reasoning { .. } => "rs", ResponseItem::LocalShellCall { .. } => "lsh", diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 378dff58c941..52bde2cc4aee 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -222,6 +222,19 @@ fn assign_missing_response_item_ids_skips_agent_messages() { assert!(items[1].id().is_some_and(|id| id.starts_with("msg_"))); } +#[test] +fn assign_missing_response_item_ids_assigns_additional_tools_ids() { + let items = Cow::Owned(vec![ResponseItem::AdditionalTools { + id: None, + role: "developer".to_string(), + tools: Vec::new(), + }]); + + let items = Session::assign_missing_response_item_ids(items); + + assert!(items[0].id().is_some_and(|id| id.starts_with("at_"))); +} + fn assistant_message(text: &str) -> ResponseItem { ResponseItem::Message { id: None, diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 08c64470f072..2faad8734900 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -935,6 +935,8 @@ pub enum ResponseItem { #[schemars(skip)] #[ts(skip)] AdditionalTools { + #[serde(default, skip_serializing_if = "Option::is_none")] + id: Option, role: String, tools: Vec, }, @@ -1166,7 +1168,8 @@ impl ResponseItem { /// Returns the non-empty Responses API item ID, if present. pub fn id(&self) -> Option<&str> { match self { - Self::Message { id, .. } + Self::AdditionalTools { id, .. } + | Self::Message { id, .. } | Self::AgentMessage { id, .. } | Self::LocalShellCall { id, .. } | Self::FunctionCall { id, .. } @@ -1187,7 +1190,8 @@ impl ResponseItem { /// Sets or clears the Responses API item ID for variants that carry one. pub fn set_id(&mut self, new_id: Option) { match self { - Self::Message { id, .. } + Self::AdditionalTools { id, .. } + | Self::Message { id, .. } | Self::AgentMessage { id, .. } | Self::LocalShellCall { id, .. } | Self::FunctionCall { id, .. } @@ -2243,6 +2247,14 @@ mod tests { item.set_id(/*new_id*/ None); assert_eq!(item.id(), None); + + let mut additional_tools = ResponseItem::AdditionalTools { + id: None, + role: "developer".to_string(), + tools: Vec::new(), + }; + additional_tools.set_id(Some("at_test".to_string())); + assert_eq!(additional_tools.id(), Some("at_test")); } fn response_item_with_passthrough_metadata( From 827f28e73675452a559b2ee0d1c9d40531c18f98 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Mon, 22 Jun 2026 09:12:40 -0700 Subject: [PATCH 5/7] [codex] Remove Responses Lite tool size cap --- codex-rs/core/src/client.rs | 14 +---- codex-rs/core/tests/suite/responses_lite.rs | 60 --------------------- 2 files changed, 2 insertions(+), 72 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 8925cd718591..69f66440f8c0 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -84,7 +84,6 @@ use codex_rollout_trace::CompactionTraceContext; use codex_rollout_trace::InferenceTraceAttempt; use codex_rollout_trace::InferenceTraceContext; use codex_tools::create_tools_json_for_responses_api; -use codex_utils_output_truncation::approx_tokens_from_byte_count; use eventsource_stream::Event; use eventsource_stream::EventStreamError; use futures::StreamExt; @@ -151,7 +150,6 @@ const X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE_HEADER: &str = "x-openai-internal-codex-responses-lite"; const RESPONSES_ENDPOINT: &str = "/responses"; const RESPONSES_COMPACT_ENDPOINT: &str = "/responses/compact"; -const RESPONSES_LITE_ADDITIONAL_TOOLS_MAX_TOKENS: u64 = 10_000; // `/responses/compact` is unary, so the timeout covers the full response rather than one idle // period between stream events. const COMPACT_REQUEST_TIMEOUT_IDLE_MULTIPLIER: u32 = 4; @@ -789,19 +787,11 @@ impl ModelClient { } let tools = create_tools_json_for_responses_api(&prompt.tools)?; let (instructions, tools) = if model_info.use_responses_lite { - let additional_tools = ResponseItem::AdditionalTools { + let mut prefix = vec![ResponseItem::AdditionalTools { id: None, role: "developer".to_string(), tools, - }; - let additional_tools_tokens = - approx_tokens_from_byte_count(serde_json::to_vec(&additional_tools)?.len()); - if additional_tools_tokens > RESPONSES_LITE_ADDITIONAL_TOOLS_MAX_TOKENS { - return Err(CodexErr::InvalidRequest(format!( - "Responses Lite additional tools exceed the {RESPONSES_LITE_ADDITIONAL_TOOLS_MAX_TOKENS}-token limit ({additional_tools_tokens} estimated tokens); defer tools behind tool search or reduce their schemas" - ))); - } - let mut prefix = vec![additional_tools]; + }]; if !prompt.base_instructions.text.is_empty() { prefix.push(ResponseItem::Message { id: None, diff --git a/codex-rs/core/tests/suite/responses_lite.rs b/codex-rs/core/tests/suite/responses_lite.rs index cde5111d6a34..4d2a38f6f877 100644 --- a/codex-rs/core/tests/suite/responses_lite.rs +++ b/codex-rs/core/tests/suite/responses_lite.rs @@ -9,8 +9,6 @@ use codex_features::Feature; use codex_image_generation_extension::install as install_image_generation_extension; use codex_login::CodexAuth; use codex_protocol::config_types::WebSearchMode; -use codex_protocol::dynamic_tools::DynamicToolFunctionSpec; -use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::ImageDetail; use codex_protocol::openai_models::InputModality; use codex_protocol::protocol::EventMsg; @@ -120,64 +118,6 @@ async fn responses_lite_uses_input_items_for_instructions_and_tools() -> Result< Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn responses_lite_rejects_oversized_additional_tools_item() -> Result<()> { - skip_if_no_network!(Ok(())); - - let server = responses::start_mock_server().await; - let oversized_tool = DynamicToolSpec::Function(DynamicToolFunctionSpec { - name: "oversized_tool".to_string(), - description: "x".repeat(50_000), - input_schema: serde_json::json!({ - "type": "object", - "properties": {}, - "additionalProperties": false, - }), - defer_loading: false, - }); - let mut builder = test_codex().with_model_info_override("gpt-5.4", |model_info| { - model_info.use_responses_lite = true; - }); - let mut test = builder.build(&server).await?; - let new_thread = test - .thread_manager - .start_thread_with_tools(test.config.clone(), vec![oversized_tool]) - .await?; - test.codex = new_thread.thread; - test.session_configured = new_thread.session_configured; - - test.codex - .submit(Op::UserInput { - items: vec![UserInput::Text { - text: "hello".to_string(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - responsesapi_client_metadata: None, - additional_context: Default::default(), - thread_settings: Default::default(), - }) - .await?; - let error = wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await; - let EventMsg::Error(error) = error else { - unreachable!(); - }; - assert!( - error - .message - .starts_with("Responses Lite additional tools exceed the 10000-token limit") - ); - assert!( - server - .received_requests() - .await - .unwrap_or_default() - .is_empty() - ); - - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn responses_lite_prepares_images() -> Result<()> { skip_if_no_network!(Ok(())); From 280e91e63ef4dc74ef4873d3075cb0ff41cd1e43 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Mon, 22 Jun 2026 16:48:06 -0700 Subject: [PATCH 6/7] Test Responses Lite websocket model switch --- codex-rs/core/tests/suite/agent_websocket.rs | 81 ++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/codex-rs/core/tests/suite/agent_websocket.rs b/codex-rs/core/tests/suite/agent_websocket.rs index f3e1047784f6..9e75e36e99b3 100644 --- a/codex-rs/core/tests/suite/agent_websocket.rs +++ b/codex-rs/core/tests/suite/agent_websocket.rs @@ -1,6 +1,10 @@ use anyhow::Result; use codex_features::Feature; use codex_protocol::config_types::ServiceTier; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::Op; +use codex_protocol::protocol::ThreadSettingsOverrides; +use codex_protocol::user_input::UserInput; use core_test_support::responses::WebSocketConnectionConfig; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -10,12 +14,89 @@ use core_test_support::responses::start_websocket_server; use core_test_support::responses::start_websocket_server_with_headers; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use serde_json::Value; use std::time::Duration; const WS_V2_BETA_HEADER_VALUE: &str = "responses_websockets=2026-02-06"; +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn websocket_model_switch_to_responses_lite_omits_top_level_tools() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_websocket_server(vec![vec![ + vec![ev_response_created("warm-1"), ev_completed("warm-1")], + vec![ev_response_created("resp-1"), ev_completed("resp-1")], + vec![ev_response_created("resp-2"), ev_completed("resp-2")], + ]]) + .await; + + let mut builder = test_codex() + .with_model_info_override("gpt-5.4", |model_info| { + model_info.use_responses_lite = true; + }) + .with_model("gpt-5.3-codex"); + let test = builder.build_with_websocket_server(&server).await?; + + test.submit_turn("non-lite turn").await?; + test.codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "lite turn".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + additional_context: Default::default(), + thread_settings: ThreadSettingsOverrides { + model: Some("gpt-5.4".to_string()), + ..Default::default() + }, + }) + .await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + + assert_eq!(server.handshakes().len(), 1); + let connection = server.single_connection(); + assert_eq!(connection.len(), 3); + let non_lite_turn = connection + .get(1) + .expect("missing non-lite turn request") + .body_json(); + let lite_turn = connection + .get(2) + .expect("missing lite turn request") + .body_json(); + + assert_eq!(non_lite_turn["model"].as_str(), Some("gpt-5.3-codex")); + assert_eq!(lite_turn["model"].as_str(), Some("gpt-5.4")); + assert!( + non_lite_turn + .get("tools") + .and_then(Value::as_array) + .is_some_and(|tools| !tools.is_empty()) + ); + assert_eq!(lite_turn.get("previous_response_id"), None); + assert_eq!(lite_turn.get("tools"), None); + assert_eq!(lite_turn.get("instructions"), None); + let additional_tools = lite_turn + .get("input") + .and_then(Value::as_array) + .and_then(|input| input.first()) + .filter(|item| item.get("type").and_then(Value::as_str) == Some("additional_tools")) + .and_then(|item| item.get("tools")) + .and_then(Value::as_array) + .expect("lite turn should start with an additional_tools item"); + assert!(!additional_tools.is_empty()); + + server.shutdown().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn websocket_test_codex_shell_chain() -> Result<()> { skip_if_no_network!(Ok(())); From a6f31d3e38d7c20086c1c7f67fbe25309d84351c Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Mon, 22 Jun 2026 23:31:11 -0700 Subject: [PATCH 7/7] [codex] Fix Responses Lite message initializers --- codex-rs/core/src/client.rs | 2 +- codex-rs/core/src/context/world_state/environment_tests.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 69f66440f8c0..3ba6fd0b5ec8 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -800,7 +800,7 @@ impl ModelClient { text: prompt.base_instructions.text.clone(), }], phase: None, - metadata: None, + internal_chat_message_metadata_passthrough: None, }); } input.splice(0..0, prefix); diff --git a/codex-rs/core/src/context/world_state/environment_tests.rs b/codex-rs/core/src/context/world_state/environment_tests.rs index 77a3adde17ee..961ee5ed73cd 100644 --- a/codex-rs/core/src/context/world_state/environment_tests.rs +++ b/codex-rs/core/src/context/world_state/environment_tests.rs @@ -251,6 +251,6 @@ fn user_message(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, - metadata: None, + internal_chat_message_metadata_passthrough: None, } }