Files
codex/codex-rs/tools/src/tool_search_tests.rs
T
Charlie Marsh e16b631d08 Reduce memory overhead in tool JSON schemas (#54754)
## Why

Schema strings and sequences do not grow after construction, and most schema nodes do not contain maps. Retaining collection capacity and inline optional maps adds unnecessary memory overhead.

## What changed

Store `JsonSchema` strings and sequences as boxed strings and slices, and box optional property and definition maps. Adapt constructors, schema consumers, and existing test expectations to the new representation while preserving the serialized JSON Schema format.

GitOrigin-RevId: 0a249b1ae32932cbbfe3c575c32bfd20b060c303
2026-10-17 01:08:30 +00:00

281 lines
9.6 KiB
Rust

use super::*;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
#[test]
fn shared_search_specs_preserve_results_and_release_the_source() {
let function = ResponsesApiTool {
name: "lookup".to_string(),
description: "Look up an item".to_string(),
strict: true,
defer_loading: None,
parameters: JsonSchema::object(
BTreeMap::new(),
/*required*/ None,
/*additional_properties*/ None,
),
output_schema: Some(serde_json::json!({"type": "object"}).into()),
};
let custom = crate::FreeformTool {
name: "patch".to_string(),
description: "Apply a patch".to_string(),
defer_loading: None,
format: crate::FreeformToolFormat {
r#type: "grammar".to_string(),
syntax: "lark".to_string(),
definition: "start: \"patch\"".to_string(),
},
};
for (spec, expected_names) in [
(
ToolSpec::Function(function.clone()),
vec![ToolName::namespaced("functions", "lookup")],
),
(
ToolSpec::Freeform(custom.clone()),
vec![ToolName::namespaced("functions", "patch")],
),
(
ToolSpec::Namespace(ResponsesApiNamespace {
name: "example".to_string(),
description: String::new(),
tools: vec![
ResponsesApiNamespaceTool::Function(function),
ResponsesApiNamespaceTool::Custom(custom),
],
}),
vec![
ToolName::namespaced("example", "lookup"),
ToolName::namespaced("example", "patch"),
],
),
(
ToolSpec::Namespace(ResponsesApiNamespace {
name: "empty".to_string(),
description: String::new(),
tools: Vec::new(),
}),
Vec::new(),
),
] {
let normalized =
ToolSearchInfo::from_spec("query".to_string(), spec.clone(), /*source_info*/ None)
.unwrap();
assert_eq!(
normalized.entry.tool_names().collect::<Vec<_>>(),
expected_names
);
let expected = normalized.entry.to_loadable_spec();
let spec = Arc::new(spec);
let weak = Arc::downgrade(&spec);
let info = ToolSearchInfo::from_shared_spec(
"query".to_string(),
Arc::clone(&spec),
/*source_info*/ None,
)
.unwrap();
drop(spec);
assert!(
weak.upgrade().is_some(),
"search retains the original spec rather than a deep copy"
);
assert_eq!(info.entry.to_loadable_spec(), expected);
assert_eq!(info.entry.tool_names().collect::<Vec<_>>(), expected_names);
drop(info);
assert!(
weak.upgrade().is_none(),
"search does not keep discarded catalogs alive"
);
}
}
#[test]
fn top_level_function_search_results_use_the_default_namespace() {
let function_tool = 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: Some(serde_json::json!({ "type": "object" }).into()),
};
let mut configured_function_tool = function_tool.clone();
configured_function_tool
.parameters
.mcp_input_schema_max_bytes = Some(20_000);
let search_info = ToolSearchInfo::from_tool_spec(
ToolSpec::Function(configured_function_tool),
/*source_info*/ None,
)
.expect("top-level function should be searchable");
assert_eq!(
(
search_info.entry.search_text.clone(),
search_info.entry.to_loadable_spec()
),
(
"lookup_order lookup order Look up an order".to_string(),
LoadableToolSpec::Namespace(ResponsesApiNamespace {
name: "functions".to_string(),
description: String::new(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
defer_loading: Some(true),
output_schema: None,
..function_tool
})],
}),
)
);
}
#[test]
fn top_level_custom_tools_are_searchable() {
let custom_tool = crate::FreeformTool {
name: "apply_patch".to_string(),
description: "Apply a patch".to_string(),
defer_loading: None,
format: crate::FreeformToolFormat {
r#type: "grammar".to_string(),
syntax: "lark".to_string(),
definition: "start: \"patch\"".to_string(),
},
};
let search_info = ToolSearchInfo::from_tool_spec(
ToolSpec::Freeform(custom_tool.clone()),
/*source_info*/ None,
)
.expect("top-level custom tool should be searchable");
assert_eq!(
(
search_info.entry.search_text.clone(),
search_info.entry.to_loadable_spec()
),
(
"apply_patch Apply a patch lark".to_string(),
LoadableToolSpec::Namespace(ResponsesApiNamespace {
name: "functions".to_string(),
description: String::new(),
tools: vec![ResponsesApiNamespaceTool::Custom(crate::FreeformTool {
defer_loading: Some(true),
..custom_tool
})],
}),
)
);
}
#[test]
fn default_search_text_uses_model_visible_namespace_metadata_once() {
let mut schedule_schema = JsonSchema::object(
BTreeMap::from([(
"timezone".to_string(),
JsonSchema::string(Some("IANA timezone.".to_string())),
)]),
/*required*/ None,
/*additional_properties*/ None,
);
schedule_schema.description = Some("Schedule settings.".into());
let mut parameters = JsonSchema::object(
BTreeMap::from([
(
"mode".to_string(),
JsonSchema::string(Some("Update mode.".to_string())),
),
("schedule".to_string(), schedule_schema),
]),
/*required*/ None,
/*additional_properties*/ None,
);
parameters.description = Some("Automation options.".into());
let spec = ToolSpec::Namespace(crate::ResponsesApiNamespace {
name: "codex_app".to_string(),
description: "Manage Codex automations.".to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "automation_update".to_string(),
description: "Create or update automations.".to_string(),
strict: false,
defer_loading: None,
parameters,
output_schema: None,
})],
});
let search_info = ToolSearchInfo::from_tool_spec(spec, /*source_info*/ None)
.expect("namespace should be searchable");
assert_eq!(
search_info.entry.search_text,
"codex_app Manage Codex automations. automation_update automation update Create or update automations. Automation options. mode Update mode. schedule Schedule settings. timezone IANA timezone."
);
}
#[test]
fn mixed_namespaced_function_and_custom_tools_are_searchable() {
let function_tool = 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: Some(serde_json::json!({"type": "object"}).into()),
};
let custom_tool = crate::FreeformTool {
name: "apply_patch".to_string(),
description: "Apply a patch".to_string(),
defer_loading: None,
format: crate::FreeformToolFormat {
r#type: "grammar".to_string(),
syntax: "lark".to_string(),
definition: "start: \"patch\"".to_string(),
},
};
let mut configured_function_tool = function_tool.clone();
configured_function_tool
.parameters
.mcp_input_schema_max_bytes = Some(20_000);
let spec = ToolSpec::Namespace(crate::ResponsesApiNamespace {
name: "editor".to_string(),
description: "Editing tools".to_string(),
tools: vec![
ResponsesApiNamespaceTool::Function(configured_function_tool),
ResponsesApiNamespaceTool::Custom(custom_tool.clone()),
],
});
let search_info = ToolSearchInfo::from_tool_spec(spec, /*source_info*/ None)
.expect("mixed namespace should be searchable");
assert_eq!(
search_info.entry.search_text,
"editor Editing tools lookup_order lookup order Look up an order apply_patch Apply a patch lark"
);
assert_eq!(
search_info.entry.to_loadable_spec(),
LoadableToolSpec::Namespace(crate::ResponsesApiNamespace {
name: "editor".to_string(),
description: "Editing tools".to_string(),
tools: vec![
ResponsesApiNamespaceTool::Function(ResponsesApiTool {
defer_loading: Some(true),
output_schema: None,
..function_tool
}),
ResponsesApiNamespaceTool::Custom(crate::FreeformTool {
defer_loading: Some(true),
..custom_tool
}),
],
})
);
}