use super::openai_tool_search::{OpenAiToolSearchCapability, model_supports_native_tool_search};
use super::tool_search::ToolSearchCapability;
use super::{Capability, CapabilityStatus};
pub use super::openai_tool_search::DEFAULT_TOOL_SEARCH_THRESHOLD;
pub const AUTO_TOOL_SEARCH_CAPABILITY_ID: &str = "auto_tool_search";
pub struct AutoToolSearchCapability {
openai: OpenAiToolSearchCapability,
generic: ToolSearchCapability,
}
impl AutoToolSearchCapability {
pub fn new() -> Self {
Self::with_threshold(DEFAULT_TOOL_SEARCH_THRESHOLD)
}
pub fn with_threshold(threshold: usize) -> Self {
Self {
openai: OpenAiToolSearchCapability::with_threshold(threshold),
generic: ToolSearchCapability::with_threshold(threshold),
}
}
}
impl Default for AutoToolSearchCapability {
fn default() -> Self {
Self::new()
}
}
impl Capability for AutoToolSearchCapability {
fn id(&self) -> &str {
AUTO_TOOL_SEARCH_CAPABILITY_ID
}
fn name(&self) -> &str {
"Auto Tool Search"
}
fn description(&self) -> &str {
"Model-adaptive deferred tool loading. Uses OpenAI's hosted tool_search \
on models that support it (GPT-5.4 and newer) and a provider-agnostic \
client-side fallback on every other model. Reduces token usage for \
agents with many tools, regardless of provider."
}
fn status(&self) -> CapabilityStatus {
CapabilityStatus::Available
}
fn category(&self) -> Option<&str> {
Some("Optimization")
}
fn resolve_for_model(&self, model: Option<&str>) -> Option<&dyn Capability> {
if model.is_some_and(model_supports_native_tool_search) {
Some(&self.openai)
} else {
Some(&self.generic)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::capabilities::{
CapabilityRegistry, OPENAI_TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_CAPABILITY_ID,
};
#[test]
fn test_capability_metadata() {
let cap = AutoToolSearchCapability::new();
assert_eq!(cap.id(), AUTO_TOOL_SEARCH_CAPABILITY_ID);
assert_eq!(cap.name(), "Auto Tool Search");
assert_eq!(cap.category(), Some("Optimization"));
}
#[test]
fn test_resolves_to_generic_without_model() {
let cap = AutoToolSearchCapability::new();
let resolved = cap.resolve_for_model(None).expect("dispatches");
assert_eq!(resolved.id(), TOOL_SEARCH_CAPABILITY_ID);
assert_eq!(resolved.tools().len(), 1);
assert_eq!(resolved.tool_definition_hooks().len(), 1);
}
#[test]
fn test_resolves_to_generic_on_non_native_model() {
let cap = AutoToolSearchCapability::new();
let resolved = cap
.resolve_for_model(Some("claude-sonnet-4-5-20250514"))
.expect("dispatches");
assert_eq!(resolved.id(), TOOL_SEARCH_CAPABILITY_ID);
}
#[test]
fn test_resolves_to_hosted_on_native_model() {
let cap = AutoToolSearchCapability::new();
let resolved = cap.resolve_for_model(Some("gpt-5.4")).expect("dispatches");
assert_eq!(resolved.id(), OPENAI_TOOL_SEARCH_CAPABILITY_ID);
assert!(resolved.tools().is_empty());
assert!(resolved.tool_definition_hooks().is_empty());
}
#[test]
fn test_capability_registered_in_builtins() {
let registry = CapabilityRegistry::with_builtins();
let cap = registry.get(AUTO_TOOL_SEARCH_CAPABILITY_ID).unwrap();
assert_eq!(cap.id(), AUTO_TOOL_SEARCH_CAPABILITY_ID);
}
}