mirror of
https://github.com/openai/codex.git
synced 2026-09-13 11:47:17 +00:00
## What changed - Group top-level function and custom tool definitions into a single `functions` namespace for Responses Lite providers that support namespaced tools. - Normalize missing, empty, and explicit `functions` namespaces to the same tool identity across registration, routing, lifecycle hooks, configuration, and tool search. - Keep default tool names unprefixed in code mode, display output, and dispatch traces while preserving explicit non-default namespaces. ## Testing - Add coverage for Responses Lite serialization, tool search results, namespace normalization and collision handling, routing, lifecycle events, and code-mode namespace policies. GitOrigin-RevId: d48414005b5d22d39b11a198e19c47814d3a19f2
414 lines
14 KiB
Rust
414 lines
14 KiB
Rust
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 crate::create_tools_json_for_responses_lite;
|
|
use crate::create_tools_raw_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::WebSearch {
|
|
external_web_access: Some(true),
|
|
indexed_web_access: None,
|
|
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(),
|
|
defer_loading: None,
|
|
format: FreeformToolFormat {
|
|
r#type: "grammar".to_string(),
|
|
syntax: "lark".to_string(),
|
|
definition: "start: \"exec\"".to_string(),
|
|
},
|
|
})
|
|
.name(),
|
|
"exec"
|
|
);
|
|
}
|
|
|
|
#[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" }
|
|
},
|
|
},
|
|
})]
|
|
);
|
|
}
|
|
|
|
fn responses_lite_function(name: &str) -> ResponsesApiTool {
|
|
ResponsesApiTool {
|
|
name: name.to_string(),
|
|
description: format!("{name} tool"),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::object(
|
|
BTreeMap::new(),
|
|
/*required*/ None,
|
|
/*additional_properties*/ None,
|
|
),
|
|
output_schema: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn responses_lite_groups_default_function_and_custom_tools() {
|
|
let tools = create_tools_json_for_responses_lite(&[
|
|
ToolSpec::ToolSearch {
|
|
execution: "client".to_string(),
|
|
description: "Search tools".to_string(),
|
|
parameters: JsonSchema::object(
|
|
BTreeMap::new(),
|
|
/*required*/ None,
|
|
/*additional_properties*/ None,
|
|
),
|
|
},
|
|
ToolSpec::Function(responses_lite_function("lookup_order")),
|
|
ToolSpec::Namespace(ResponsesApiNamespace {
|
|
name: "editor".to_string(),
|
|
description: "Editing tools".to_string(),
|
|
tools: vec![ResponsesApiNamespaceTool::Function(
|
|
responses_lite_function("edit"),
|
|
)],
|
|
}),
|
|
ToolSpec::Freeform(FreeformTool {
|
|
name: "exec".to_string(),
|
|
description: "Run code".to_string(),
|
|
defer_loading: None,
|
|
format: FreeformToolFormat {
|
|
r#type: "grammar".to_string(),
|
|
syntax: "lark".to_string(),
|
|
definition: "start: /.+/".to_string(),
|
|
},
|
|
}),
|
|
ToolSpec::Namespace(ResponsesApiNamespace {
|
|
name: "functions".to_string(),
|
|
description: "Existing default tools".to_string(),
|
|
tools: vec![ResponsesApiNamespaceTool::Function(
|
|
responses_lite_function("existing"),
|
|
)],
|
|
}),
|
|
ToolSpec::Function(responses_lite_function("last")),
|
|
])
|
|
.expect("serialize Responses Lite tools");
|
|
|
|
assert_eq!(tools.len(), 3);
|
|
assert_eq!(tools[0]["type"], "tool_search");
|
|
assert_eq!(tools[2]["type"], "namespace");
|
|
assert_eq!(tools[2]["name"], "editor");
|
|
assert_eq!(tools[2]["tools"][0]["name"], "edit");
|
|
assert_eq!(tools[1]["type"], "namespace");
|
|
assert_eq!(tools[1]["name"], "functions");
|
|
assert_eq!(tools[1]["description"], "Existing default tools");
|
|
assert_eq!(
|
|
tools[1]["tools"]
|
|
.as_array()
|
|
.expect("functions namespace should contain tools")
|
|
.iter()
|
|
.map(|tool| (tool["type"].as_str(), tool["name"].as_str()))
|
|
.collect::<Vec<_>>(),
|
|
[
|
|
(Some("function"), Some("lookup_order")),
|
|
(Some("custom"), Some("exec")),
|
|
(Some("function"), Some("existing")),
|
|
(Some("function"), Some("last")),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn responses_lite_preserves_empty_functions_namespace_description() {
|
|
let tools = create_tools_json_for_responses_lite(&[ToolSpec::Function(
|
|
responses_lite_function("lookup_order"),
|
|
)])
|
|
.expect("serialize Responses Lite tools");
|
|
assert_eq!(tools[0]["description"], "");
|
|
}
|
|
|
|
#[test]
|
|
fn responses_lite_does_not_add_an_empty_functions_namespace() {
|
|
let tools =
|
|
create_tools_json_for_responses_lite(&[ToolSpec::Namespace(ResponsesApiNamespace {
|
|
name: "functions".to_string(),
|
|
description: "Empty default tools".to_string(),
|
|
tools: Vec::new(),
|
|
})])
|
|
.expect("serialize Responses Lite tools");
|
|
assert!(tools.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn raw_tool_json_matches_value_encoding() {
|
|
let specs = vec![ToolSpec::Function(ResponsesApiTool {
|
|
name: "demo".to_string(),
|
|
description: "A demo tool".to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::object(
|
|
BTreeMap::new(),
|
|
/*required*/ None,
|
|
/*additional_properties*/ None,
|
|
),
|
|
output_schema: None,
|
|
})];
|
|
let expected = create_tools_json_for_responses_api(&specs).expect("serialize tools");
|
|
let raw = create_tools_raw_json_for_responses_api(&specs).expect("serialize raw tools");
|
|
|
|
assert_eq!(
|
|
serde_json::from_str::<Vec<serde_json::Value>>(raw.get()).expect("parse raw tools"),
|
|
expected,
|
|
);
|
|
}
|
|
|
|
#[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,
|
|
}),
|
|
ResponsesApiNamespaceTool::Custom(FreeformTool {
|
|
name: "apply_patch".to_string(),
|
|
description: "Apply a patch".to_string(),
|
|
defer_loading: None,
|
|
format: FreeformToolFormat {
|
|
r#type: "grammar".to_string(),
|
|
syntax: "lark".to_string(),
|
|
definition: "start: \"patch\"".to_string(),
|
|
},
|
|
}),
|
|
],
|
|
}))
|
|
.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" },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"type": "custom",
|
|
"name": "apply_patch",
|
|
"description": "Apply a patch",
|
|
"format": {
|
|
"type": "grammar",
|
|
"syntax": "lark",
|
|
"definition": "start: \"patch\"",
|
|
},
|
|
},
|
|
],
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_tool_spec_serializes_expected_wire_shape() {
|
|
assert_eq!(
|
|
serde_json::to_value(ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
indexed_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,
|
|
"indexed_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,
|
|
},
|
|
})
|
|
);
|
|
}
|