everruns_core/capabilities/
auto_tool_search.rs1use super::claude_tool_search::{
29 ClaudeToolSearchCapability,
30 model_supports_native_tool_search as model_supports_native_claude_tool_search,
31};
32use super::openai_tool_search::{
33 OpenAiToolSearchCapability,
34 model_supports_native_tool_search as model_supports_native_openai_tool_search,
35};
36use super::tool_search::ToolSearchCapability;
37use super::{Capability, CapabilityLocalization, CapabilityStatus};
38
39pub use super::openai_tool_search::DEFAULT_TOOL_SEARCH_THRESHOLD;
40
41pub const AUTO_TOOL_SEARCH_CAPABILITY_ID: &str = "auto_tool_search";
43
44pub struct AutoToolSearchCapability {
50 openai: OpenAiToolSearchCapability,
51 claude: ClaudeToolSearchCapability,
52 generic: ToolSearchCapability,
53}
54
55impl AutoToolSearchCapability {
56 pub fn new() -> Self {
57 Self::with_threshold(DEFAULT_TOOL_SEARCH_THRESHOLD)
58 }
59
60 pub fn with_threshold(threshold: usize) -> Self {
61 Self {
62 openai: OpenAiToolSearchCapability::with_threshold(threshold),
63 claude: ClaudeToolSearchCapability::with_threshold(threshold),
64 generic: ToolSearchCapability::with_threshold(threshold),
65 }
66 }
67
68 pub fn with_never_defer<I, S>(mut self, names: I) -> Self
73 where
74 I: IntoIterator<Item = S>,
75 S: Into<String>,
76 {
77 self.generic = self.generic.with_never_defer(names);
78 self
79 }
80}
81
82impl Default for AutoToolSearchCapability {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88impl Capability for AutoToolSearchCapability {
89 fn id(&self) -> &str {
90 AUTO_TOOL_SEARCH_CAPABILITY_ID
91 }
92
93 fn name(&self) -> &str {
94 "Auto Tool Search"
95 }
96
97 fn description(&self) -> &str {
98 "Model-adaptive deferred tool loading. Uses the provider's hosted \
99 tool_search on models that support it (OpenAI GPT-5.4+ and Claude \
100 Sonnet 4 / Opus 4 / Haiku 4.5 / Fable 5 and newer) and a \
101 provider-agnostic client-side fallback on every other model. Reduces \
102 token usage for agents with many tools, regardless of provider."
103 }
104
105 fn localizations(&self) -> Vec<CapabilityLocalization> {
106 vec![CapabilityLocalization::text(
107 "uk",
108 "Автоматичний пошук інструментів",
109 "Відкладене завантаження інструментів, що адаптується до моделі. Використовує хостований tool_search провайдера на моделях, які його підтримують (OpenAI GPT-5.4+ та Claude Sonnet 4 / Opus 4 / Haiku 4.5 / Fable 5 і новіші), та незалежний від провайдера клієнтський резервний механізм на всіх інших моделях. Зменшує використання токенів для агентів із багатьма інструментами незалежно від провайдера.",
110 )]
111 }
112
113 fn status(&self) -> CapabilityStatus {
114 CapabilityStatus::Available
115 }
116
117 fn category(&self) -> Option<&str> {
118 Some("Optimization")
119 }
120
121 fn resolve_for_model(&self, model: Option<&str>) -> Option<&dyn Capability> {
129 match model {
130 Some(m) if model_supports_native_openai_tool_search(m) => Some(&self.openai),
131 Some(m) if model_supports_native_claude_tool_search(m) => Some(&self.claude),
132 _ => Some(&self.generic),
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::capabilities::{
141 CLAUDE_TOOL_SEARCH_CAPABILITY_ID, OPENAI_TOOL_SEARCH_CAPABILITY_ID,
142 TOOL_SEARCH_CAPABILITY_ID,
143 };
144
145 #[test]
148 fn test_resolves_to_generic_without_model() {
149 let cap = AutoToolSearchCapability::new();
151 let resolved = cap.resolve_for_model(None).expect("dispatches");
152 assert_eq!(resolved.id(), TOOL_SEARCH_CAPABILITY_ID);
153 assert_eq!(resolved.tools().len(), 1);
155 assert_eq!(resolved.tool_definition_hooks().len(), 1);
156 }
157
158 #[test]
159 fn test_resolves_to_generic_on_non_native_model() {
160 let cap = AutoToolSearchCapability::new();
163 let resolved = cap
164 .resolve_for_model(Some("claude-3-5-haiku"))
165 .expect("dispatches");
166 assert_eq!(resolved.id(), TOOL_SEARCH_CAPABILITY_ID);
167 }
168
169 #[test]
170 fn test_resolves_to_hosted_on_native_openai_model() {
171 let cap = AutoToolSearchCapability::new();
172 let resolved = cap.resolve_for_model(Some("gpt-5.4")).expect("dispatches");
173 assert_eq!(resolved.id(), OPENAI_TOOL_SEARCH_CAPABILITY_ID);
174 assert!(resolved.tools().is_empty());
176 assert!(resolved.tool_definition_hooks().is_empty());
177 }
178
179 #[test]
180 fn test_resolves_to_hosted_on_native_claude_model() {
181 let cap = AutoToolSearchCapability::new();
182 let resolved = cap
183 .resolve_for_model(Some("claude-opus-4-8"))
184 .expect("dispatches");
185 assert_eq!(resolved.id(), CLAUDE_TOOL_SEARCH_CAPABILITY_ID);
186 assert!(resolved.tools().is_empty());
188 assert!(resolved.tool_definition_hooks().is_empty());
189 }
190}