mirror of
https://github.com/openai/codex.git
synced 2026-06-01 19:02:59 +00:00
Add MCP tool wall time to model output (#17406)
Include MCP wall time in the output so the model is aware of how long it's calls are taking.
This commit is contained in:
@@ -118,6 +118,63 @@ impl ToolOutput for CallToolResult {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct McpToolOutput {
|
||||
pub result: CallToolResult,
|
||||
pub wall_time: Duration,
|
||||
}
|
||||
|
||||
impl ToolOutput for McpToolOutput {
|
||||
fn log_preview(&self) -> String {
|
||||
let payload = self.response_payload();
|
||||
let preview = payload.body.to_text().unwrap_or_else(|| {
|
||||
serde_json::to_string(&self.result.content)
|
||||
.unwrap_or_else(|err| format!("failed to serialize mcp result: {err}"))
|
||||
});
|
||||
telemetry_preview(&preview)
|
||||
}
|
||||
|
||||
fn success_for_logging(&self) -> bool {
|
||||
self.result.success()
|
||||
}
|
||||
|
||||
fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem {
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id: call_id.to_string(),
|
||||
output: self.response_payload(),
|
||||
}
|
||||
}
|
||||
|
||||
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue {
|
||||
serde_json::to_value(&self.result).unwrap_or_else(|err| {
|
||||
JsonValue::String(format!("failed to serialize mcp result: {err}"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl McpToolOutput {
|
||||
fn response_payload(&self) -> FunctionCallOutputPayload {
|
||||
let mut payload = self.result.as_function_call_output_payload();
|
||||
let wall_time_seconds = self.wall_time.as_secs_f64();
|
||||
let header = format!("Wall time: {wall_time_seconds:.4} seconds\nOutput:");
|
||||
|
||||
match &mut payload.body {
|
||||
FunctionCallOutputBody::Text(text) => {
|
||||
if text.is_empty() {
|
||||
*text = header;
|
||||
} else {
|
||||
*text = format!("{header}\n{text}");
|
||||
}
|
||||
}
|
||||
FunctionCallOutputBody::ContentItems(items) => {
|
||||
items.insert(0, FunctionCallOutputContentItem::InputText { text: header });
|
||||
}
|
||||
}
|
||||
|
||||
payload
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToolSearchOutput {
|
||||
pub tools: Vec<ToolSearchOutputTool>,
|
||||
|
||||
@@ -85,6 +85,145 @@ fn mcp_code_mode_result_serializes_full_call_tool_result() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_output_response_item_includes_wall_time() {
|
||||
let output = McpToolOutput {
|
||||
result: CallToolResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
"text": "done",
|
||||
})],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
},
|
||||
wall_time: std::time::Duration::from_millis(1250),
|
||||
};
|
||||
|
||||
let response = output.to_response_item(
|
||||
"mcp-call-1",
|
||||
&ToolPayload::Mcp {
|
||||
server: "server".to_string(),
|
||||
tool: "tool".to_string(),
|
||||
raw_arguments: "{}".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
match response {
|
||||
ResponseInputItem::FunctionCallOutput { call_id, output } => {
|
||||
assert_eq!(call_id, "mcp-call-1");
|
||||
assert_eq!(output.success, Some(true));
|
||||
let Some(text) = output.body.to_text() else {
|
||||
panic!("MCP output should serialize as text");
|
||||
};
|
||||
let Some(payload) = text.strip_prefix("Wall time: 1.2500 seconds\nOutput:\n") else {
|
||||
panic!("MCP output should include wall-time header: {text}");
|
||||
};
|
||||
let parsed: serde_json::Value = serde_json::from_str(payload).unwrap_or_else(|err| {
|
||||
panic!("MCP output should serialize JSON content: {err}");
|
||||
});
|
||||
assert_eq!(
|
||||
parsed,
|
||||
json!([{
|
||||
"type": "text",
|
||||
"text": "done",
|
||||
}])
|
||||
);
|
||||
}
|
||||
other => panic!("expected FunctionCallOutput, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_output_response_item_preserves_content_items() {
|
||||
let image_url = "data:image/png;base64,AAA";
|
||||
let output = McpToolOutput {
|
||||
result: CallToolResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "image",
|
||||
"mimeType": "image/png",
|
||||
"data": "AAA",
|
||||
})],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
},
|
||||
wall_time: std::time::Duration::from_millis(500),
|
||||
};
|
||||
|
||||
let response = output.to_response_item(
|
||||
"mcp-call-2",
|
||||
&ToolPayload::Mcp {
|
||||
server: "server".to_string(),
|
||||
tool: "tool".to_string(),
|
||||
raw_arguments: "{}".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
match response {
|
||||
ResponseInputItem::FunctionCallOutput { output, .. } => {
|
||||
assert_eq!(
|
||||
output.content_items(),
|
||||
Some(
|
||||
vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
text: "Wall time: 0.5000 seconds\nOutput:".to_string(),
|
||||
},
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url: image_url.to_string(),
|
||||
detail: None,
|
||||
},
|
||||
]
|
||||
.as_slice()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
output.body.to_text().as_deref(),
|
||||
Some("Wall time: 0.5000 seconds\nOutput:")
|
||||
);
|
||||
}
|
||||
other => panic!("expected FunctionCallOutput, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_output_code_mode_result_stays_raw_call_tool_result() {
|
||||
let output = McpToolOutput {
|
||||
result: CallToolResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
"text": "ignored",
|
||||
})],
|
||||
structured_content: Some(serde_json::json!({
|
||||
"content": "done",
|
||||
})),
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
},
|
||||
wall_time: std::time::Duration::from_millis(1250),
|
||||
};
|
||||
|
||||
let result = output.code_mode_result(&ToolPayload::Mcp {
|
||||
server: "server".to_string(),
|
||||
tool: "tool".to_string(),
|
||||
raw_arguments: "{}".to_string(),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!({
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": "ignored",
|
||||
}],
|
||||
"structuredContent": {
|
||||
"content": "done",
|
||||
},
|
||||
"isError": false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_tool_calls_can_derive_text_from_content_items() {
|
||||
let payload = ToolPayload::Custom {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::mcp_tool_call::handle_mcp_tool_call;
|
||||
use crate::tools::context::McpToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
|
||||
pub struct McpHandler;
|
||||
impl ToolHandler for McpHandler {
|
||||
type Output = CallToolResult;
|
||||
type Output = McpToolOutput;
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Mcp
|
||||
@@ -41,7 +42,8 @@ impl ToolHandler for McpHandler {
|
||||
let (server, tool, raw_arguments) = payload;
|
||||
let arguments_str = raw_arguments;
|
||||
|
||||
let output = handle_mcp_tool_call(
|
||||
let started = Instant::now();
|
||||
let result = handle_mcp_tool_call(
|
||||
Arc::clone(&session),
|
||||
&turn,
|
||||
call_id.clone(),
|
||||
@@ -51,6 +53,9 @@ impl ToolHandler for McpHandler {
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(output)
|
||||
Ok(McpToolOutput {
|
||||
result,
|
||||
wall_time: started.elapsed(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user