everruns_core/capabilities/
auto_tool_search.rs1use super::openai_tool_search::{OpenAiToolSearchCapability, model_supports_native_tool_search};
24use super::tool_search::ToolSearchCapability;
25use super::{Capability, CapabilityStatus};
26
27pub use super::openai_tool_search::DEFAULT_TOOL_SEARCH_THRESHOLD;
28
29pub const AUTO_TOOL_SEARCH_CAPABILITY_ID: &str = "auto_tool_search";
31
32pub struct AutoToolSearchCapability {
38 openai: OpenAiToolSearchCapability,
39 generic: ToolSearchCapability,
40}
41
42impl AutoToolSearchCapability {
43 pub fn new() -> Self {
44 Self::with_threshold(DEFAULT_TOOL_SEARCH_THRESHOLD)
45 }
46
47 pub fn with_threshold(threshold: usize) -> Self {
48 Self {
49 openai: OpenAiToolSearchCapability::with_threshold(threshold),
50 generic: ToolSearchCapability::with_threshold(threshold),
51 }
52 }
53}
54
55impl Default for AutoToolSearchCapability {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl Capability for AutoToolSearchCapability {
62 fn id(&self) -> &str {
63 AUTO_TOOL_SEARCH_CAPABILITY_ID
64 }
65
66 fn name(&self) -> &str {
67 "Auto Tool Search"
68 }
69
70 fn description(&self) -> &str {
71 "Model-adaptive deferred tool loading. Uses OpenAI's hosted tool_search \
72 on models that support it (GPT-5.4 and newer) and a provider-agnostic \
73 client-side fallback on every other model. Reduces token usage for \
74 agents with many tools, regardless of provider."
75 }
76
77 fn status(&self) -> CapabilityStatus {
78 CapabilityStatus::Available
79 }
80
81 fn category(&self) -> Option<&str> {
82 Some("Optimization")
83 }
84
85 fn resolve_for_model(&self, model: Option<&str>) -> Option<&dyn Capability> {
91 if model.is_some_and(model_supports_native_tool_search) {
92 Some(&self.openai)
93 } else {
94 Some(&self.generic)
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::capabilities::{
103 CapabilityRegistry, OPENAI_TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_CAPABILITY_ID,
104 };
105
106 #[test]
107 fn test_capability_metadata() {
108 let cap = AutoToolSearchCapability::new();
109 assert_eq!(cap.id(), AUTO_TOOL_SEARCH_CAPABILITY_ID);
110 assert_eq!(cap.name(), "Auto Tool Search");
111 assert_eq!(cap.category(), Some("Optimization"));
112 }
113
114 #[test]
115 fn test_resolves_to_generic_without_model() {
116 let cap = AutoToolSearchCapability::new();
118 let resolved = cap.resolve_for_model(None).expect("dispatches");
119 assert_eq!(resolved.id(), TOOL_SEARCH_CAPABILITY_ID);
120 assert_eq!(resolved.tools().len(), 1);
122 assert_eq!(resolved.tool_definition_hooks().len(), 1);
123 }
124
125 #[test]
126 fn test_resolves_to_generic_on_non_native_model() {
127 let cap = AutoToolSearchCapability::new();
128 let resolved = cap
129 .resolve_for_model(Some("claude-sonnet-4-5-20250514"))
130 .expect("dispatches");
131 assert_eq!(resolved.id(), TOOL_SEARCH_CAPABILITY_ID);
132 }
133
134 #[test]
135 fn test_resolves_to_hosted_on_native_model() {
136 let cap = AutoToolSearchCapability::new();
137 let resolved = cap.resolve_for_model(Some("gpt-5.4")).expect("dispatches");
138 assert_eq!(resolved.id(), OPENAI_TOOL_SEARCH_CAPABILITY_ID);
139 assert!(resolved.tools().is_empty());
141 assert!(resolved.tool_definition_hooks().is_empty());
142 }
143
144 #[test]
145 fn test_capability_registered_in_builtins() {
146 let registry = CapabilityRegistry::with_builtins();
147 let cap = registry.get(AUTO_TOOL_SEARCH_CAPABILITY_ID).unwrap();
148 assert_eq!(cap.id(), AUTO_TOOL_SEARCH_CAPABILITY_ID);
149 }
150}