mirror of
https://github.com/openai/codex.git
synced 2026-05-04 21:32:21 +03:00
stacked on #17402. MCP tools returned by `tool_search` (deferred tools) get registered in our `ToolRegistry` with a different format than directly available tools. this leads to two different ways of accessing MCP tools from our tool catalog, only one of which works for each. fix this by registering all MCP tools with the namespace format, since this info is already available. also, direct MCP tools are registered to responsesapi without a namespace, while deferred MCP tools have a namespace. this means we can receive MCP `FunctionCall`s in both formats from namespaces. fix this by always registering MCP tools with namespace, regardless of deferral status. make code mode track `ToolName` provenance of tools so it can map the literal JS function name string to the correct `ToolName` for invocation, rather than supporting both in core. this lets us unify to a single canonical `ToolName` representation for each MCP tool and force everywhere to use that one, without supporting fallbacks.
294 lines
9.6 KiB
Rust
294 lines
9.6 KiB
Rust
use super::ConfiguredToolSpec;
|
|
use super::ResponsesApiNamespace;
|
|
use super::ResponsesApiWebSearchFilters;
|
|
use super::ResponsesApiWebSearchUserLocation;
|
|
use super::ToolSpec;
|
|
use crate::AdditionalProperties;
|
|
use crate::FreeformTool;
|
|
use crate::FreeformToolFormat;
|
|
use crate::JsonSchema;
|
|
use crate::ResponsesApiNamespaceTool;
|
|
use crate::ResponsesApiTool;
|
|
use crate::create_tools_json_for_responses_api;
|
|
use codex_protocol::config_types::WebSearchContextSize;
|
|
use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
|
|
use codex_protocol::config_types::WebSearchUserLocation as ConfigWebSearchUserLocation;
|
|
use codex_protocol::config_types::WebSearchUserLocationType;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use std::collections::BTreeMap;
|
|
|
|
#[test]
|
|
fn tool_spec_name_covers_all_variants() {
|
|
assert_eq!(
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "lookup_order".to_string(),
|
|
description: "Look up an order".to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::object(
|
|
BTreeMap::new(),
|
|
/*required*/ None,
|
|
/*additional_properties*/ None
|
|
),
|
|
output_schema: None,
|
|
})
|
|
.name(),
|
|
"lookup_order"
|
|
);
|
|
assert_eq!(
|
|
ToolSpec::Namespace(ResponsesApiNamespace {
|
|
name: "mcp__demo__".to_string(),
|
|
description: "Demo tools".to_string(),
|
|
tools: Vec::new(),
|
|
})
|
|
.name(),
|
|
"mcp__demo__"
|
|
);
|
|
assert_eq!(
|
|
ToolSpec::ToolSearch {
|
|
execution: "sync".to_string(),
|
|
description: "Search for tools".to_string(),
|
|
parameters: JsonSchema::object(
|
|
BTreeMap::new(),
|
|
/*required*/ None,
|
|
/*additional_properties*/ None
|
|
),
|
|
}
|
|
.name(),
|
|
"tool_search"
|
|
);
|
|
assert_eq!(ToolSpec::LocalShell {}.name(), "local_shell");
|
|
assert_eq!(
|
|
ToolSpec::ImageGeneration {
|
|
output_format: "png".to_string(),
|
|
}
|
|
.name(),
|
|
"image_generation"
|
|
);
|
|
assert_eq!(
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: None,
|
|
user_location: None,
|
|
search_context_size: None,
|
|
search_content_types: None,
|
|
}
|
|
.name(),
|
|
"web_search"
|
|
);
|
|
assert_eq!(
|
|
ToolSpec::Freeform(FreeformTool {
|
|
name: "exec".to_string(),
|
|
description: "Run a command".to_string(),
|
|
format: FreeformToolFormat {
|
|
r#type: "grammar".to_string(),
|
|
syntax: "lark".to_string(),
|
|
definition: "start: \"exec\"".to_string(),
|
|
},
|
|
})
|
|
.name(),
|
|
"exec"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn configured_tool_spec_name_delegates_to_tool_spec() {
|
|
assert_eq!(
|
|
ConfiguredToolSpec::new(
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "lookup_order".to_string(),
|
|
description: "Look up an order".to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::object(
|
|
BTreeMap::new(),
|
|
/*required*/ None,
|
|
/*additional_properties*/ None
|
|
),
|
|
output_schema: None,
|
|
}),
|
|
/*supports_parallel_tool_calls*/ true,
|
|
)
|
|
.name(),
|
|
"lookup_order"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_config_converts_to_responses_api_types() {
|
|
assert_eq!(
|
|
ResponsesApiWebSearchFilters::from(ConfigWebSearchFilters {
|
|
allowed_domains: Some(vec!["example.com".to_string()]),
|
|
}),
|
|
ResponsesApiWebSearchFilters {
|
|
allowed_domains: Some(vec!["example.com".to_string()]),
|
|
}
|
|
);
|
|
assert_eq!(
|
|
ResponsesApiWebSearchUserLocation::from(ConfigWebSearchUserLocation {
|
|
r#type: WebSearchUserLocationType::Approximate,
|
|
country: Some("US".to_string()),
|
|
region: Some("California".to_string()),
|
|
city: Some("San Francisco".to_string()),
|
|
timezone: Some("America/Los_Angeles".to_string()),
|
|
}),
|
|
ResponsesApiWebSearchUserLocation {
|
|
r#type: WebSearchUserLocationType::Approximate,
|
|
country: Some("US".to_string()),
|
|
region: Some("California".to_string()),
|
|
city: Some("San Francisco".to_string()),
|
|
timezone: Some("America/Los_Angeles".to_string()),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn create_tools_json_for_responses_api_includes_top_level_name() {
|
|
assert_eq!(
|
|
create_tools_json_for_responses_api(&[ToolSpec::Function(ResponsesApiTool {
|
|
name: "demo".to_string(),
|
|
description: "A demo tool".to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::object(
|
|
BTreeMap::from([("foo".to_string(), JsonSchema::string(/*description*/ None),)]),
|
|
/*required*/ None,
|
|
/*additional_properties*/ None
|
|
),
|
|
output_schema: None,
|
|
})])
|
|
.expect("serialize tools"),
|
|
vec![json!({
|
|
"type": "function",
|
|
"name": "demo",
|
|
"description": "A demo tool",
|
|
"strict": false,
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"foo": { "type": "string" }
|
|
},
|
|
},
|
|
})]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn namespace_tool_spec_serializes_expected_wire_shape() {
|
|
assert_eq!(
|
|
serde_json::to_value(ToolSpec::Namespace(ResponsesApiNamespace {
|
|
name: "mcp__demo__".to_string(),
|
|
description: "Demo tools".to_string(),
|
|
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
|
name: "lookup_order".to_string(),
|
|
description: "Look up an order".to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::object(
|
|
BTreeMap::from([(
|
|
"order_id".to_string(),
|
|
JsonSchema::string(/*description*/ None),
|
|
)]),
|
|
/*required*/ None,
|
|
/*additional_properties*/ None,
|
|
),
|
|
output_schema: None,
|
|
})],
|
|
}))
|
|
.expect("serialize namespace tool"),
|
|
json!({
|
|
"type": "namespace",
|
|
"name": "mcp__demo__",
|
|
"description": "Demo tools",
|
|
"tools": [
|
|
{
|
|
"type": "function",
|
|
"name": "lookup_order",
|
|
"description": "Look up an order",
|
|
"strict": false,
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"order_id": { "type": "string" },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_tool_spec_serializes_expected_wire_shape() {
|
|
assert_eq!(
|
|
serde_json::to_value(ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: Some(ResponsesApiWebSearchFilters {
|
|
allowed_domains: Some(vec!["example.com".to_string()]),
|
|
}),
|
|
user_location: Some(ResponsesApiWebSearchUserLocation {
|
|
r#type: WebSearchUserLocationType::Approximate,
|
|
country: Some("US".to_string()),
|
|
region: Some("California".to_string()),
|
|
city: Some("San Francisco".to_string()),
|
|
timezone: Some("America/Los_Angeles".to_string()),
|
|
}),
|
|
search_context_size: Some(WebSearchContextSize::High),
|
|
search_content_types: Some(vec!["text".to_string(), "image".to_string()]),
|
|
})
|
|
.expect("serialize web_search"),
|
|
json!({
|
|
"type": "web_search",
|
|
"external_web_access": true,
|
|
"filters": {
|
|
"allowed_domains": ["example.com"],
|
|
},
|
|
"user_location": {
|
|
"type": "approximate",
|
|
"country": "US",
|
|
"region": "California",
|
|
"city": "San Francisco",
|
|
"timezone": "America/Los_Angeles",
|
|
},
|
|
"search_context_size": "high",
|
|
"search_content_types": ["text", "image"],
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tool_search_tool_spec_serializes_expected_wire_shape() {
|
|
assert_eq!(
|
|
serde_json::to_value(ToolSpec::ToolSearch {
|
|
execution: "sync".to_string(),
|
|
description: "Search app tools".to_string(),
|
|
parameters: JsonSchema::object(
|
|
BTreeMap::from([(
|
|
"query".to_string(),
|
|
JsonSchema::string(Some("Tool search query".to_string()),),
|
|
)]),
|
|
Some(vec!["query".to_string()]),
|
|
Some(AdditionalProperties::Boolean(false))
|
|
),
|
|
})
|
|
.expect("serialize tool_search"),
|
|
json!({
|
|
"type": "tool_search",
|
|
"execution": "sync",
|
|
"description": "Search app tools",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {
|
|
"type": "string",
|
|
"description": "Tool search query",
|
|
}
|
|
},
|
|
"required": ["query"],
|
|
"additionalProperties": false,
|
|
},
|
|
})
|
|
);
|
|
}
|