mirror of
https://github.com/openai/codex.git
synced 2026-09-03 14:59:03 +00:00
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
This commit is contained in:
@@ -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<ThreadSource>,
|
||||
pub(crate) sandbox: Option<String>,
|
||||
pub(crate) workspaces: BTreeMap<String, TurnMetadataWorkspace>,
|
||||
pub(crate) code_mode_tool_names: Option<BTreeMap<String, ToolName>>,
|
||||
pub(crate) turn_started_at_unix_ms: Option<i64>,
|
||||
pub(crate) extra: BTreeMap<String, String>,
|
||||
}
|
||||
@@ -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<String, TurnMetadataWorkspace>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
code_mode_tool_names: Option<&'a BTreeMap<String, ToolName>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
turn_started_at_unix_ms: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
compaction: Option<CompactionTurnMetadata>,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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<String>,
|
||||
enriched_workspaces: Arc<RwLock<Option<BTreeMap<String, TurnMetadataWorkspace>>>>,
|
||||
code_mode_tool_names: Arc<RwLock<Option<BTreeMap<String, ToolName>>>>,
|
||||
turn_started_at_unix_ms: Arc<RwLock<Option<i64>>>,
|
||||
responsesapi_client_metadata: Arc<RwLock<BTreeMap<String, String>>>,
|
||||
user_input_requested_during_turn: Arc<AtomicBool>,
|
||||
@@ -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<String, ToolName>,
|
||||
) {
|
||||
*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<String, String>,
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user