mirror of
https://github.com/openai/codex.git
synced 2026-05-25 05:24:37 +00:00
Add model and reasoning effort to MCP turn metadata (#21219)
## Why - Similar change as https://github.com/openai/codex/pull/19473. - Without change: MCP tool calls receive `_meta["x-codex-turn-metadata"]` with `session_id`, `turn_id`, and `turn_started_at_unix_ms`. - Issue: MCP servers may want the model and reasoning effort to better understand tool-call behavior and latency relative to turn start. ## What Changed - With change: MCP turn metadata now includes `model` and `reasoning_effort`, propagated in `_meta["x-codex-turn-metadata"]`. - Normal `/responses` turn metadata headers are unchanged. ## Verification - `codex-rs/core/src/mcp_tool_call_tests.rs` - `codex-rs/core/src/turn_metadata_tests.rs` - `codex-rs/core/tests/suite/search_tool.rs`
This commit is contained in:
committed by
Channing Conger
parent
b852d9627c
commit
384a34ec6e
@@ -33,6 +33,7 @@ use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::sandboxing::PermissionRequestPayload;
|
||||
use crate::turn_metadata::McpTurnMetadataContext;
|
||||
use codex_analytics::AppInvocation;
|
||||
use codex_analytics::InvocationType;
|
||||
use codex_analytics::build_track_events_context;
|
||||
@@ -895,7 +896,13 @@ fn build_mcp_tool_call_request_meta(
|
||||
) -> Option<serde_json::Value> {
|
||||
let mut request_meta = serde_json::Map::new();
|
||||
|
||||
if let Some(turn_metadata) = turn_context.turn_metadata_state.current_meta_value() {
|
||||
if let Some(turn_metadata) = turn_context
|
||||
.turn_metadata_state
|
||||
.current_meta_value_for_mcp_request(McpTurnMetadataContext {
|
||||
model: turn_context.model_info.slug.as_str(),
|
||||
reasoning_effort: turn_context.effective_reasoning_effort(),
|
||||
})
|
||||
{
|
||||
request_meta.insert(
|
||||
crate::X_CODEX_TURN_METADATA_HEADER.to_string(),
|
||||
turn_metadata,
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::session::tests::make_session_and_context;
|
||||
use crate::session::tests::make_session_and_context_with_rx;
|
||||
use crate::state::ActiveTurn;
|
||||
use crate::test_support::models_manager_with_provider;
|
||||
use crate::turn_metadata::McpTurnMetadataContext;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::types::AppConfig;
|
||||
@@ -71,6 +72,13 @@ fn approval_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_turn_metadata_context(turn_context: &TurnContext) -> McpTurnMetadataContext<'_> {
|
||||
McpTurnMetadataContext {
|
||||
model: turn_context.model_info.slug.as_str(),
|
||||
reasoning_effort: turn_context.effective_reasoning_effort(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_sample_plugin_mcp(codex_home: &std::path::Path) {
|
||||
let plugin_root = codex_home.join("plugins/cache/test/sample/local");
|
||||
std::fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("create plugin manifest dir");
|
||||
@@ -920,13 +928,10 @@ fn truncate_mcp_tool_result_for_event_bounds_large_error() {
|
||||
#[tokio::test]
|
||||
async fn mcp_tool_call_request_meta_includes_turn_metadata_for_custom_server() {
|
||||
let (_, turn_context) = make_session_and_context().await;
|
||||
let expected_turn_metadata = serde_json::from_str::<serde_json::Value>(
|
||||
&turn_context
|
||||
.turn_metadata_state
|
||||
.current_header_value()
|
||||
.expect("turn metadata header"),
|
||||
)
|
||||
.expect("turn metadata json");
|
||||
let expected_turn_metadata = turn_context
|
||||
.turn_metadata_state
|
||||
.current_meta_value_for_mcp_request(mcp_turn_metadata_context(&turn_context))
|
||||
.expect("turn metadata");
|
||||
|
||||
let meta = build_mcp_tool_call_request_meta(
|
||||
&turn_context,
|
||||
@@ -935,6 +940,25 @@ async fn mcp_tool_call_request_meta_includes_turn_metadata_for_custom_server() {
|
||||
/*metadata*/ None,
|
||||
)
|
||||
.expect("custom servers should receive turn metadata");
|
||||
let turn_metadata = meta
|
||||
.get(crate::X_CODEX_TURN_METADATA_HEADER)
|
||||
.expect("turn metadata should be present");
|
||||
|
||||
assert_eq!(
|
||||
turn_metadata
|
||||
.get("model")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some(turn_context.model_info.slug.as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
turn_metadata
|
||||
.get("reasoning_effort")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
turn_context
|
||||
.effective_reasoning_effort()
|
||||
.map(|effort| effort.to_string())
|
||||
.as_deref()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
meta,
|
||||
@@ -973,13 +997,10 @@ async fn mcp_tool_call_request_meta_includes_turn_started_at_unix_ms() {
|
||||
#[tokio::test]
|
||||
async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps_meta() {
|
||||
let (_, turn_context) = make_session_and_context().await;
|
||||
let expected_turn_metadata = serde_json::from_str::<serde_json::Value>(
|
||||
&turn_context
|
||||
.turn_metadata_state
|
||||
.current_header_value()
|
||||
.expect("turn metadata header"),
|
||||
)
|
||||
.expect("turn metadata json");
|
||||
let expected_turn_metadata = turn_context
|
||||
.turn_metadata_state
|
||||
.current_meta_value_for_mcp_request(mcp_turn_metadata_context(&turn_context))
|
||||
.expect("turn metadata");
|
||||
let metadata = McpToolApprovalMetadata {
|
||||
annotations: None,
|
||||
connector_id: Some("calendar".to_string()),
|
||||
@@ -1023,13 +1044,10 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps
|
||||
#[tokio::test]
|
||||
async fn codex_apps_tool_call_request_meta_includes_call_id_without_existing_codex_apps_meta() {
|
||||
let (_, turn_context) = make_session_and_context().await;
|
||||
let expected_turn_metadata = serde_json::from_str::<serde_json::Value>(
|
||||
&turn_context
|
||||
.turn_metadata_state
|
||||
.current_header_value()
|
||||
.expect("turn metadata header"),
|
||||
)
|
||||
.expect("turn metadata json");
|
||||
let expected_turn_metadata = turn_context
|
||||
.turn_metadata_state
|
||||
.current_meta_value_for_mcp_request(mcp_turn_metadata_context(&turn_context))
|
||||
.expect("turn metadata");
|
||||
|
||||
assert_eq!(
|
||||
build_mcp_tool_call_request_meta(
|
||||
|
||||
@@ -119,20 +119,21 @@ impl TurnContext {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn effective_reasoning_effort_for_tracing(&self) -> String {
|
||||
pub(crate) fn effective_reasoning_effort(&self) -> Option<ReasoningEffortConfig> {
|
||||
if self.model_info.supports_reasoning_summaries {
|
||||
match self
|
||||
.reasoning_effort
|
||||
self.reasoning_effort
|
||||
.or(self.model_info.default_reasoning_level)
|
||||
{
|
||||
Some(effort) => effort.to_string(),
|
||||
None => "default".to_string(),
|
||||
}
|
||||
} else {
|
||||
"default".to_string()
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn effective_reasoning_effort_for_tracing(&self) -> String {
|
||||
self.effective_reasoning_effort()
|
||||
.map(|effort| effort.to_string())
|
||||
.unwrap_or_else(|| "default".to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn model_context_window(&self) -> Option<i64> {
|
||||
let effective_context_window_percent = self.model_info.effective_context_window_percent;
|
||||
self.model_info
|
||||
|
||||
@@ -16,11 +16,19 @@ use codex_git_utils::get_has_changes;
|
||||
use codex_git_utils::get_head_commit_hash;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
const MODEL_KEY: &str = "model";
|
||||
const REASONING_EFFORT_KEY: &str = "reasoning_effort";
|
||||
const TURN_STARTED_AT_UNIX_MS_KEY: &str = "turn_started_at_unix_ms";
|
||||
|
||||
pub(crate) struct McpTurnMetadataContext<'a> {
|
||||
pub(crate) model: &'a str,
|
||||
pub(crate) reasoning_effort: Option<ReasoningEffortConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct WorkspaceGitMetadata {
|
||||
associated_remote_urls: Option<BTreeMap<String, String>>,
|
||||
@@ -248,9 +256,28 @@ impl TurnMetadataState {
|
||||
.or(Some(header))
|
||||
}
|
||||
|
||||
pub(crate) fn current_meta_value(&self) -> Option<serde_json::Value> {
|
||||
self.current_header_value()
|
||||
.and_then(|header| serde_json::from_str(&header).ok())
|
||||
pub(crate) fn current_meta_value_for_mcp_request(
|
||||
&self,
|
||||
context: McpTurnMetadataContext<'_>,
|
||||
) -> Option<serde_json::Value> {
|
||||
let header = self.current_header_value()?;
|
||||
let mut metadata = serde_json::from_str::<serde_json::Map<String, Value>>(&header).ok()?;
|
||||
metadata.insert(
|
||||
MODEL_KEY.to_string(),
|
||||
Value::String(context.model.to_string()),
|
||||
);
|
||||
match context.reasoning_effort {
|
||||
Some(reasoning_effort) => {
|
||||
metadata.insert(
|
||||
REASONING_EFFORT_KEY.to_string(),
|
||||
Value::String(reasoning_effort.to_string()),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
metadata.remove(REASONING_EFFORT_KEY);
|
||||
}
|
||||
}
|
||||
Some(Value::Object(metadata))
|
||||
}
|
||||
|
||||
pub(crate) fn set_responsesapi_client_metadata(
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
|
||||
use crate::sandbox_tags::sandbox_tag;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
@@ -13,6 +14,13 @@ use std::collections::HashMap;
|
||||
use tempfile::TempDir;
|
||||
use tokio::process::Command;
|
||||
|
||||
fn test_mcp_turn_metadata_context() -> McpTurnMetadataContext<'static> {
|
||||
McpTurnMetadataContext {
|
||||
model: "gpt-5.4",
|
||||
reasoning_effort: Some(ReasoningEffortConfig::High),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_turn_metadata_header_includes_has_changes_for_clean_repo() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
@@ -158,6 +166,50 @@ fn turn_metadata_state_includes_turn_started_at_unix_ms_after_start() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_metadata_state_includes_model_and_reasoning_effort_only_in_request_meta() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = temp_dir.path().abs();
|
||||
let permission_profile = PermissionProfile::read_only();
|
||||
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
&SessionSource::Exec,
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*enforce_managed_network*/ false,
|
||||
);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let header_json: Value = serde_json::from_str(&header).expect("json");
|
||||
assert!(header_json.get("model").is_none());
|
||||
assert!(header_json.get("reasoning_effort").is_none());
|
||||
|
||||
let meta = state
|
||||
.current_meta_value_for_mcp_request(test_mcp_turn_metadata_context())
|
||||
.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"));
|
||||
|
||||
let meta_without_reasoning_effort = state
|
||||
.current_meta_value_for_mcp_request(McpTurnMetadataContext {
|
||||
model: "gpt-5.4",
|
||||
reasoning_effort: None,
|
||||
})
|
||||
.expect("turn metadata should be present");
|
||||
assert_eq!(
|
||||
meta_without_reasoning_effort["model"].as_str(),
|
||||
Some("gpt-5.4")
|
||||
);
|
||||
assert!(
|
||||
meta_without_reasoning_effort
|
||||
.get("reasoning_effort")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_metadata_state_ignores_client_turn_started_at_unix_ms_before_start() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
@@ -202,6 +254,11 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
state.set_responsesapi_client_metadata(HashMap::from([
|
||||
("fiber_run_id".to_string(), "fiber-123".to_string()),
|
||||
("origin".to_string(), "東京".to_string()),
|
||||
("model".to_string(), "client-supplied".to_string()),
|
||||
(
|
||||
"reasoning_effort".to_string(),
|
||||
"client-supplied".to_string(),
|
||||
),
|
||||
("session_id".to_string(), "client-supplied".to_string()),
|
||||
("thread_source".to_string(), "client-supplied".to_string()),
|
||||
(
|
||||
@@ -218,6 +275,8 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
|
||||
assert_eq!(json["fiber_run_id"].as_str(), Some("fiber-123"));
|
||||
assert_eq!(json["origin"].as_str(), Some("東京"));
|
||||
assert_eq!(json["model"].as_str(), Some("client-supplied"));
|
||||
assert_eq!(json["reasoning_effort"].as_str(), Some("client-supplied"));
|
||||
assert_eq!(json["session_id"].as_str(), Some("session-a"));
|
||||
assert_eq!(json["thread_source"].as_str(), Some("user"));
|
||||
assert_eq!(json["turn_id"].as_str(), Some("turn-a"));
|
||||
@@ -225,4 +284,10 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
json["turn_started_at_unix_ms"].as_i64(),
|
||||
Some(1_700_000_000_123)
|
||||
);
|
||||
|
||||
let meta = state
|
||||
.current_meta_value_for_mcp_request(test_mcp_turn_metadata_context())
|
||||
.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"));
|
||||
}
|
||||
|
||||
@@ -570,6 +570,7 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
|
||||
|
||||
let requests = mock.requests();
|
||||
assert_eq!(requests.len(), 3);
|
||||
let first_request_body = requests[0].body_json();
|
||||
|
||||
let apps_tool_call = server
|
||||
.received_requests()
|
||||
@@ -604,6 +605,22 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
|
||||
.is_some_and(|turn_id| !turn_id.is_empty()),
|
||||
"apps tools/call should include turn metadata turn_id: {apps_tool_call:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
apps_tool_call
|
||||
.pointer("/params/_meta/x-codex-turn-metadata/model")
|
||||
.and_then(Value::as_str),
|
||||
Some("gpt-5.4")
|
||||
);
|
||||
let first_request_reasoning_effort = first_request_body
|
||||
.pointer("/reasoning/effort")
|
||||
.and_then(Value::as_str)
|
||||
.expect("first response request should include reasoning effort");
|
||||
assert_eq!(
|
||||
apps_tool_call
|
||||
.pointer("/params/_meta/x-codex-turn-metadata/reasoning_effort")
|
||||
.and_then(Value::as_str),
|
||||
Some(first_request_reasoning_effort)
|
||||
);
|
||||
let mcp_turn_started_at_unix_ms = apps_tool_call
|
||||
.pointer("/params/_meta/x-codex-turn-metadata/turn_started_at_unix_ms")
|
||||
.and_then(Value::as_i64)
|
||||
@@ -626,7 +643,6 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
|
||||
Some(mcp_turn_started_at_unix_ms)
|
||||
);
|
||||
|
||||
let first_request_body = requests[0].body_json();
|
||||
let first_request_tools = tool_names(&first_request_body);
|
||||
assert!(
|
||||
first_request_tools
|
||||
|
||||
Reference in New Issue
Block a user