Skip to main content

everruns_core/capabilities/
openai_tool_search.rs

1// OpenAI Tool Search Capability
2//
3// When added to an agent, enables tool_search (deferred tool loading) for
4// models with tool_search=true in their profile. Tools are grouped into
5// namespaces based on capability categories, and their full parameter schemas
6// are loaded on-demand by the model instead of sent upfront.
7//
8// This capability does not provide any tools itself — it configures the
9// LLM driver to use tool_search when constructing the API request.
10//
11// If the model does not support tool_search (tool_search=false in profile),
12// this capability is silently ignored — no error, no crash.
13
14use super::{Capability, CapabilityLocalization, CapabilityStatus, SystemPromptContext};
15use crate::llm_driver_registry::ToolSearchConfig;
16use async_trait::async_trait;
17
18/// Default minimum tool count to activate tool_search.
19/// Below this threshold, full schemas are sent even when capability is enabled.
20pub const DEFAULT_TOOL_SEARCH_THRESHOLD: usize = 15;
21
22/// Capability ID for OpenAI tool search
23pub const OPENAI_TOOL_SEARCH_CAPABILITY_ID: &str = "openai_tool_search";
24
25/// OpenAI Tool Search capability.
26///
27/// Adding this capability to an agent/harness enables deferred tool loading
28/// for models that support it. The `threshold` controls the minimum number
29/// of tools before tool_search activates (default: 15).
30pub struct OpenAiToolSearchCapability {
31    threshold: usize,
32}
33
34impl OpenAiToolSearchCapability {
35    pub fn new() -> Self {
36        Self {
37            threshold: DEFAULT_TOOL_SEARCH_THRESHOLD,
38        }
39    }
40
41    pub fn with_threshold(threshold: usize) -> Self {
42        Self { threshold }
43    }
44
45    /// Returns the ToolSearchConfig for this capability
46    pub fn tool_search_config(&self) -> ToolSearchConfig {
47        ToolSearchConfig {
48            enabled: true,
49            threshold: self.threshold,
50        }
51    }
52}
53
54/// Whether `model` natively supports hosted tool_search (OpenAI GPT-5.4+).
55///
56/// This is the single source of truth for the native-vs-client-side decision,
57/// consulted by `auto_tool_search`'s runtime dispatch (at capability-collection
58/// time) and by `RuntimeAgentBuilder::build` (when disabling a hosted config the
59/// model can't honor). Native tool_search is an OpenAI hosted feature, so the
60/// lookup is against the OpenAI provider profile regardless of how the model is
61/// otherwise routed.
62pub fn model_supports_native_tool_search(model: &str) -> bool {
63    crate::llm_model_profiles::get_model_profile(&crate::llm_models::LlmProviderType::Openai, model)
64        .is_some_and(|profile| profile.tool_search)
65}
66
67impl Default for OpenAiToolSearchCapability {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73#[async_trait]
74impl Capability for OpenAiToolSearchCapability {
75    fn id(&self) -> &str {
76        OPENAI_TOOL_SEARCH_CAPABILITY_ID
77    }
78
79    fn name(&self) -> &str {
80        "OpenAI Tool Search"
81    }
82
83    fn description(&self) -> &str {
84        "Enables deferred tool loading for models that support it (GPT-5.4 and newer). \
85         Reduces token usage by loading tool schemas on-demand instead of upfront."
86    }
87
88    fn localizations(&self) -> Vec<CapabilityLocalization> {
89        vec![CapabilityLocalization::text(
90            "uk",
91            "Пошук інструментів OpenAI",
92            "Вмикає відкладене завантаження інструментів для моделей, які його підтримують (GPT-5.4 і новіші). Зменшує використання токенів, завантажуючи схеми інструментів на вимогу, а не заздалегідь.",
93        )]
94    }
95
96    fn status(&self) -> CapabilityStatus {
97        CapabilityStatus::Available
98    }
99
100    fn category(&self) -> Option<&str> {
101        Some("Optimization")
102    }
103
104    async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
105        None // No system prompt needed
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_capability_metadata() {
115        let cap = OpenAiToolSearchCapability::new();
116        assert_eq!(cap.id(), OPENAI_TOOL_SEARCH_CAPABILITY_ID);
117        assert_eq!(cap.name(), "OpenAI Tool Search");
118        assert_eq!(cap.status(), CapabilityStatus::Available);
119        assert!(cap.tools().is_empty());
120    }
121
122    #[test]
123    fn test_default_threshold() {
124        let cap = OpenAiToolSearchCapability::new();
125        let config = cap.tool_search_config();
126        assert!(config.enabled);
127        assert_eq!(config.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
128    }
129
130    #[test]
131    fn test_custom_threshold() {
132        let cap = OpenAiToolSearchCapability::with_threshold(5);
133        let config = cap.tool_search_config();
134        assert_eq!(config.threshold, 5);
135    }
136}