From 25b6fc9bbc49bbec12e8d38ceee550fc07cbc60d Mon Sep 17 00:00:00 2001 From: rka-oai Date: Sat, 25 Jul 2026 01:00:45 +0000 Subject: [PATCH] Include code-mode tool names in Responses Lite metadata (#35271) ## What changed - Add `code_mode_tool_names` to Responses Lite turn metadata, mapping each normalized code-mode identifier to its structured `ToolName`. - Reserve the metadata key against client overrides and omit it from metadata exposed to external MCP servers. - Keep non-Lite requests and the legacy top-level client metadata unchanged. ## Testing - Cover HTTP and WebSocket Responses Lite requests, including switching from a non-Lite model. - Verify reserved-key handling and external MCP metadata filtering. GitOrigin-RevId: b678afba13d57a91e26f5895aa37238fbb4cb276 --- codex-rs/core/src/responses_metadata.rs | 8 +++++ codex-rs/core/src/tools/spec_plan.rs | 16 ++++++++++ codex-rs/core/src/turn_metadata.rs | 21 +++++++++++++ codex-rs/core/src/turn_metadata_tests.rs | 32 ++++++++++++++++++++ codex-rs/core/tests/suite/agent_websocket.rs | 28 +++++++++++++++++ codex-rs/core/tests/suite/responses_lite.rs | 19 ++++++++++++ 6 files changed, 124 insertions(+) diff --git a/codex-rs/core/src/responses_metadata.rs b/codex-rs/core/src/responses_metadata.rs index c76d3078cd..6724516a15 100644 --- a/codex-rs/core/src/responses_metadata.rs +++ b/codex-rs/core/src/responses_metadata.rs @@ -7,6 +7,7 @@ use codex_analytics::CompactionReason; use codex_analytics::CompactionStrategy; use codex_analytics::CompactionTrigger; use codex_protocol::ThreadId; +use codex_protocol::ToolName; use codex_protocol::protocol::InternalSessionSource; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; @@ -30,6 +31,7 @@ pub(crate) const TURN_ID_KEY: &str = "turn_id"; pub(crate) const WINDOW_ID_KEY: &str = "window_id"; pub(crate) const REQUEST_KIND_KEY: &str = "request_kind"; pub(crate) const COMPACTION_KEY: &str = "compaction"; +pub(crate) const CODE_MODE_TOOL_NAMES_KEY: &str = "code_mode_tool_names"; pub(crate) const TURN_STARTED_AT_UNIX_MS_KEY: &str = "turn_started_at_unix_ms"; pub(crate) const FORKED_FROM_THREAD_ID_KEY: &str = "forked_from_thread_id"; @@ -54,6 +56,7 @@ const RESERVED_METADATA_KEYS: &[&str] = &[ X_OPENAI_SUBAGENT_HEADER, REQUEST_KIND_KEY, COMPACTION_KEY, + CODE_MODE_TOOL_NAMES_KEY, TURN_STARTED_AT_UNIX_MS_KEY, FORKED_FROM_THREAD_ID_KEY, PARENT_THREAD_ID_KEY, @@ -165,6 +168,7 @@ pub struct CodexResponsesMetadata { pub(crate) thread_source: Option, pub(crate) sandbox: Option, pub(crate) workspaces: BTreeMap, + pub(crate) code_mode_tool_names: Option>, pub(crate) turn_started_at_unix_ms: Option, pub(crate) extra: BTreeMap, } @@ -190,6 +194,7 @@ impl CodexResponsesMetadata { thread_source: None, sandbox: None, workspaces: BTreeMap::new(), + code_mode_tool_names: None, turn_started_at_unix_ms: None, extra: BTreeMap::new(), } @@ -292,6 +297,7 @@ impl CodexResponsesMetadata { thread_source: self.thread_source.as_ref(), sandbox: self.sandbox.as_deref(), workspaces: non_empty_workspaces(&self.workspaces), + code_mode_tool_names: self.code_mode_tool_names.as_ref(), turn_started_at_unix_ms: self.turn_started_at_unix_ms, compaction, // responsesapi_client_metadata enriches the Codex turn metadata blob, not literal @@ -382,6 +388,8 @@ struct CodexTurnMetadataPayload<'a> { #[serde(default, skip_serializing_if = "Option::is_none")] workspaces: Option<&'a BTreeMap>, #[serde(default, skip_serializing_if = "Option::is_none")] + code_mode_tool_names: Option<&'a BTreeMap>, + #[serde(default, skip_serializing_if = "Option::is_none")] turn_started_at_unix_ms: Option, #[serde(default, skip_serializing_if = "Option::is_none")] compaction: Option, diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index c9dbba0d33..5ff9e392f3 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -493,6 +493,22 @@ fn build_code_mode_executors( code_mode_nested_tool_specs.push(spec); } + if turn_context.model_info.use_responses_lite { + let code_mode_tool_names = + collect_code_mode_exec_prompt_tool_definitions(code_mode_nested_tool_specs.iter()) + .into_iter() + .map(|tool| { + ( + codex_code_mode::normalize_code_mode_identifier(&tool.name), + tool.tool_name, + ) + }) + .collect(); + turn_context + .turn_metadata_state + .set_code_mode_tool_names(code_mode_tool_names); + } + let namespace_descriptions = code_mode_namespace_descriptions(&exec_prompt_tool_specs); let mut enabled_tools = collect_code_mode_exec_prompt_tool_definitions(exec_prompt_tool_specs.iter()); diff --git a/codex-rs/core/src/turn_metadata.rs b/codex-rs/core/src/turn_metadata.rs index 0a1f781b38..7f7abd07b7 100644 --- a/codex-rs/core/src/turn_metadata.rs +++ b/codex-rs/core/src/turn_metadata.rs @@ -9,6 +9,7 @@ use std::sync::atomic::Ordering; use serde_json::Value; use tokio::task::JoinHandle; +use crate::responses_metadata::CODE_MODE_TOOL_NAMES_KEY; use crate::responses_metadata::CodexResponsesMetadata; use crate::responses_metadata::CodexResponsesRequestKind; use crate::responses_metadata::TurnMetadataWorkspace; @@ -21,6 +22,7 @@ use codex_git_utils::get_git_repo_root; use codex_git_utils::get_has_changes; use codex_git_utils::get_head_commit_hash; use codex_protocol::ThreadId; +use codex_protocol::ToolName; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; @@ -96,6 +98,7 @@ pub(crate) struct TurnMetadataState { turn_id: String, sandbox: Option, enriched_workspaces: Arc>>>, + code_mode_tool_names: Arc>>>, turn_started_at_unix_ms: Arc>>, responsesapi_client_metadata: Arc>>, user_input_requested_during_turn: Arc, @@ -139,6 +142,7 @@ impl TurnMetadataState { turn_id, sandbox, enriched_workspaces: Arc::new(RwLock::new(None)), + code_mode_tool_names: Arc::new(RwLock::new(None)), turn_started_at_unix_ms: Arc::new(RwLock::new(None)), responsesapi_client_metadata: Arc::new(RwLock::new(BTreeMap::new())), user_input_requested_during_turn: Arc::new(AtomicBool::new(false)), @@ -155,6 +159,7 @@ impl TurnMetadataState { else { return None; }; + metadata.remove(CODE_MODE_TOOL_NAMES_KEY); // Precaution: avoid exposing tool data to external MCPs. metadata.insert( MODEL_KEY.to_string(), Value::String(context.model.to_string()), @@ -203,6 +208,17 @@ impl TurnMetadataState { .store(true, Ordering::Relaxed); } + pub(crate) fn set_code_mode_tool_names( + &self, + code_mode_tool_names: BTreeMap, + ) { + *self + .code_mode_tool_names + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner) = + (!code_mode_tool_names.is_empty()).then_some(code_mode_tool_names); + } + pub(crate) fn set_responsesapi_client_metadata( &self, responsesapi_client_metadata: HashMap, @@ -232,6 +248,11 @@ impl TurnMetadataState { thread_source: self.thread_source.clone(), sandbox: self.sandbox.clone(), workspaces: self.current_workspaces(), + code_mode_tool_names: self + .code_mode_tool_names + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone(), turn_started_at_unix_ms: self.current_turn_started_at_unix_ms(), extra: self .responsesapi_client_metadata diff --git a/codex-rs/core/src/turn_metadata_tests.rs b/codex-rs/core/src/turn_metadata_tests.rs index 99eb4d3753..8f56d7db72 100644 --- a/codex-rs/core/src/turn_metadata_tests.rs +++ b/codex-rs/core/src/turn_metadata_tests.rs @@ -1,5 +1,6 @@ use super::*; +use crate::responses_metadata::CODE_MODE_TOOL_NAMES_KEY; use crate::responses_metadata::CodexResponsesRequestKind; use crate::responses_metadata::CompactionTurnMetadata; use crate::responses_metadata::INSTALLATION_ID_KEY; @@ -9,6 +10,7 @@ use codex_analytics::CompactionImplementation; use codex_analytics::CompactionPhase; use codex_analytics::CompactionReason; use codex_analytics::CompactionTrigger; +use codex_protocol::ToolName; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::protocol::SessionSource; @@ -542,6 +544,10 @@ fn turn_metadata_state_ignores_client_reserved_metadata_before_start() { /*enforce_managed_network*/ false, ); state.set_responsesapi_client_metadata(HashMap::from([ + ( + CODE_MODE_TOOL_NAMES_KEY.to_string(), + "client-supplied".to_string(), + ), ( "turn_started_at_unix_ms".to_string(), "client-supplied".to_string(), @@ -560,6 +566,7 @@ fn turn_metadata_state_ignores_client_reserved_metadata_before_start() { let header = test_turn_metadata_header(&state); let json: Value = serde_json::from_str(&header).expect("json"); + assert!(json.get(CODE_MODE_TOOL_NAMES_KEY).is_none()); assert!(json.get("turn_started_at_unix_ms").is_none()); assert!(json.get("forked_from_thread_id").is_none()); assert!(json.get("parent_thread_id").is_none()); @@ -628,6 +635,10 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields( "client-supplied".to_string(), ), ("subagent_kind".to_string(), "client-supplied".to_string()), + ( + CODE_MODE_TOOL_NAMES_KEY.to_string(), + "client-supplied".to_string(), + ), ("turn_id".to_string(), "client-supplied".to_string()), (WINDOW_ID_KEY.to_string(), "client-supplied".to_string()), ("thread_source".to_string(), "client-supplied".to_string()), @@ -638,6 +649,13 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields( ), ])); state.set_turn_started_at_unix_ms(/*turn_started_at_unix_ms*/ 1_700_000_000_123); + state.set_code_mode_tool_names(BTreeMap::from([ + ("exec_command".to_string(), ToolName::plain("exec_command")), + ( + "mcp__calendar__lookup".to_string(), + ToolName::namespaced("mcp__calendar", "lookup"), + ), + ])); let header = test_turn_metadata_header(&state); assert!(header.is_ascii()); @@ -651,6 +669,19 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields( assert_eq!(json["reasoning_effort"].as_str(), Some("client-supplied")); assert_eq!(json["session_id"].as_str(), Some("session-a")); assert_eq!(json["thread_id"].as_str(), Some("thread-a")); + assert_eq!( + json[CODE_MODE_TOOL_NAMES_KEY], + serde_json::json!({ + "exec_command": { + "name": "exec_command", + "namespace": null, + }, + "mcp__calendar__lookup": { + "name": "lookup", + "namespace": "mcp__calendar", + }, + }) + ); assert!(json.get(INSTALLATION_ID_KEY).is_none()); assert!(json.get("x-codex-installation-id").is_none()); assert!(json.get("x-codex-parent-thread-id").is_none()); @@ -695,6 +726,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields( .expect("turn metadata should be present"); assert_eq!(meta["model"].as_str(), Some("gpt-5.4")); assert_eq!(meta["reasoning_effort"].as_str(), Some("high")); + assert!(meta.get(CODE_MODE_TOOL_NAMES_KEY).is_none()); assert!(meta.get(WINDOW_ID_KEY).is_none()); assert_eq!(state.workspace_kind().as_deref(), Some("projectless")); } diff --git a/codex-rs/core/tests/suite/agent_websocket.rs b/codex-rs/core/tests/suite/agent_websocket.rs index 00e2f84bbd..9faff49c23 100644 --- a/codex-rs/core/tests/suite/agent_websocket.rs +++ b/codex-rs/core/tests/suite/agent_websocket.rs @@ -1,6 +1,7 @@ use anyhow::Result; use codex_features::Feature; use codex_protocol::config_types::ServiceTier; +use codex_protocol::openai_models::ToolMode; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides; @@ -33,8 +34,12 @@ async fn websocket_model_switch_to_responses_lite_omits_top_level_tools() -> Res .await; let mut builder = test_codex() + .with_model_info_override("gpt-5.2", |model_info| { + model_info.tool_mode = Some(ToolMode::CodeMode); + }) .with_model_info_override("gpt-5.4", |model_info| { model_info.use_responses_lite = true; + model_info.tool_mode = Some(ToolMode::CodeMode); }) .with_model("gpt-5.2"); let test = builder.build_with_websocket_server(&server).await?; @@ -74,6 +79,29 @@ async fn websocket_model_switch_to_responses_lite_omits_top_level_tools() -> Res assert_eq!(non_lite_turn["model"].as_str(), Some("gpt-5.2")); assert_eq!(lite_turn["model"].as_str(), Some("gpt-5.4")); + let non_lite_turn_metadata: Value = serde_json::from_str( + non_lite_turn["client_metadata"]["x-codex-turn-metadata"] + .as_str() + .expect("non-lite turn metadata"), + )?; + assert!(non_lite_turn_metadata.get("code_mode_tool_names").is_none()); + let lite_turn_metadata: Value = serde_json::from_str( + lite_turn["client_metadata"]["x-codex-turn-metadata"] + .as_str() + .expect("Responses Lite turn metadata"), + )?; + assert_eq!( + lite_turn_metadata["code_mode_tool_names"]["view_image"], + serde_json::json!({ + "name": "view_image", + "namespace": null, + }) + ); + assert!( + lite_turn["client_metadata"] + .get("x-codex-code-mode-tool-names") + .is_none() + ); assert!( non_lite_turn .get("tools") diff --git a/codex-rs/core/tests/suite/responses_lite.rs b/codex-rs/core/tests/suite/responses_lite.rs index 40de16c31e..3ba46439c6 100644 --- a/codex-rs/core/tests/suite/responses_lite.rs +++ b/codex-rs/core/tests/suite/responses_lite.rs @@ -13,6 +13,7 @@ use codex_login::auth::BedrockApiKeyAuth; use codex_protocol::config_types::WebSearchMode; use codex_protocol::models::ImageDetail; use codex_protocol::openai_models::InputModality; +use codex_protocol::openai_models::ToolMode; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::user_input::UserInput; @@ -97,6 +98,7 @@ async fn responses_lite_uses_input_items_for_instructions_and_tools() -> Result< let mut builder = test_codex() .with_model_info_override("gpt-5.4", |model_info| { model_info.use_responses_lite = true; + model_info.tool_mode = Some(ToolMode::CodeMode); }) .with_config(|config| { config.base_instructions = Some("test instructions".to_string()); @@ -128,6 +130,23 @@ async fn responses_lite_uses_input_items_for_instructions_and_tools() -> Result< let tools = additional_tools(&body)?; assert!(!tools.is_empty()); + let client_metadata = body["client_metadata"] + .as_object() + .context("Responses request should include client metadata")?; + let turn_metadata: Value = serde_json::from_str( + client_metadata["x-codex-turn-metadata"] + .as_str() + .context("Responses request should include turn metadata")?, + )?; + + assert_eq!( + turn_metadata["code_mode_tool_names"]["view_image"], + serde_json::json!({ + "name": "view_image", + "namespace": null, + }) + ); + assert!(!client_metadata.contains_key("x-codex-code-mode-tool-names")); Ok(()) }