Coverage for src / local_deep_research / llm / providers / auto_discovery.py: 88%
92 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
1"""Auto-discovery system for OpenAI-compatible providers."""
3import importlib
4import inspect
5from pathlib import Path
6from typing import Dict, List, Optional
8from loguru import logger
10from .openai_base import OpenAICompatibleProvider
13class ProviderInfo:
14 """Information about a discovered provider."""
16 def __init__(self, provider_class):
17 self.provider_class = provider_class
18 self.provider_key = getattr(
19 provider_class,
20 "provider_key",
21 provider_class.__name__.replace("Provider", "").upper(),
22 )
23 self.provider_name = provider_class.provider_name
24 self.company_name = getattr(
25 provider_class, "company_name", provider_class.provider_name
26 )
27 self.is_cloud = getattr(provider_class, "is_cloud", True)
28 # Handle providers that may not have requires_auth_for_models method
29 if hasattr(provider_class, "requires_auth_for_models"):
30 self.requires_auth_for_models = (
31 provider_class.requires_auth_for_models()
32 )
33 else:
34 # Default to True for providers without the method
35 self.requires_auth_for_models = True
37 # Generate display name from attributes
38 self.display_name = self._generate_display_name()
40 def _generate_display_name(self):
41 """Generate a descriptive display name from provider attributes."""
42 # Start with the provider name
43 name_parts = [self.provider_name]
45 # Add cloud/local indicator
46 if self.is_cloud is True:
47 name_parts.append("☁️ Cloud")
48 elif self.is_cloud is False:
49 name_parts.append("💻 Local")
51 return " ".join(name_parts)
53 def to_dict(self):
54 """Convert to dictionary for API responses."""
55 return {
56 "value": self.provider_key,
57 "label": self.display_name,
58 "is_cloud": self.is_cloud,
59 }
62class ProviderDiscovery:
63 """Discovers and manages OpenAI-compatible providers."""
65 _instance = None
66 _providers: Dict[str, ProviderInfo] = {}
68 def __new__(cls):
69 if cls._instance is None:
70 cls._instance = super().__new__(cls)
71 cls._instance._discovered = False
72 return cls._instance
74 def discover_providers(
75 self, force_refresh: bool = False
76 ) -> Dict[str, ProviderInfo]:
77 """Discover all providers in the providers directory.
79 Args:
80 force_refresh: Force re-discovery even if already done
82 Returns:
83 Dictionary mapping provider keys to ProviderInfo objects
84 """
85 if self._discovered and not force_refresh:
86 return self._providers
88 self._providers.clear()
89 # Scan the implementations subdirectory for providers
90 implementations_dir = Path(__file__).parent / "implementations"
92 if not implementations_dir.exists(): 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true
93 logger.warning(
94 f"Implementations directory not found: {implementations_dir}"
95 )
96 return self._providers
98 # Scan all Python files in the implementations directory
99 logger.info(f"Scanning directory: {implementations_dir}")
100 for file_path in implementations_dir.glob("*.py"):
101 # Skip special files (like __init__.py)
102 if file_path.name.startswith("_"): 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true
103 continue
105 module_name = file_path.stem
106 logger.debug(f"Processing module: {module_name} from {file_path}")
107 try:
108 # Import the module from implementations subdirectory
109 module = importlib.import_module(
110 f".implementations.{module_name}",
111 package="local_deep_research.llm.providers",
112 )
114 # Find all Provider classes (both OpenAICompatibleProvider and standalone)
115 logger.debug(
116 f"Inspecting module {module_name} for Provider classes"
117 )
118 for name, obj in inspect.getmembers(module, inspect.isclass):
119 if inspect.isclass(obj): 119 ↛ 124line 119 didn't jump to line 124 because the condition on line 119 was always true
120 logger.debug(
121 f" Found class: {name}, bases: {obj.__bases__}"
122 )
123 # Check if it's a Provider class (ends with "Provider" and has provider_name)
124 if (
125 name.endswith("Provider")
126 and hasattr(obj, "provider_name")
127 and obj is not OpenAICompatibleProvider
128 ):
129 # Found a provider class
130 provider_info = ProviderInfo(obj)
131 self._providers[provider_info.provider_key] = (
132 provider_info
133 )
135 # Auto-register the provider
136 register_func_name = f"register_{module_name}_provider"
137 try:
138 register_func = getattr(module, register_func_name)
139 register_func()
140 logger.info(
141 f"Auto-registered provider: {provider_info.provider_key}"
142 )
143 except AttributeError:
144 logger.warning(
145 f"Provider {provider_info.provider_key} from {module_name}.py "
146 f"does not have a {register_func_name} function"
147 )
149 logger.info(
150 f"Discovered provider: {provider_info.provider_key} from {module_name}.py"
151 )
153 except Exception:
154 logger.exception(f"Error loading provider from {module_name}")
156 self._discovered = True
157 logger.info(f"Discovered {len(self._providers)} providers")
158 return self._providers
160 def get_provider_info(self, provider_key: str) -> Optional[ProviderInfo]:
161 """Get information about a specific provider.
163 Args:
164 provider_key: The provider key (e.g., 'IONOS', 'GOOGLE')
166 Returns:
167 ProviderInfo object or None if not found
168 """
169 if not self._discovered: 169 ↛ 170line 169 didn't jump to line 170 because the condition on line 169 was never true
170 self.discover_providers()
171 return self._providers.get(provider_key.upper())
173 def get_provider_options(self) -> List[Dict]:
174 """Get list of provider options for UI dropdowns.
176 Returns:
177 List of dictionaries with 'value' and 'label' keys
178 """
179 if not self._discovered: 179 ↛ 180line 179 didn't jump to line 180 because the condition on line 179 was never true
180 self.discover_providers()
182 options = []
183 for provider_info in self._providers.values():
184 options.append(provider_info.to_dict())
186 # Sort by label
187 options.sort(key=lambda x: x["label"])
188 return options
190 def get_provider_class(self, provider_key: str):
191 """Get the provider class for a given key.
193 Args:
194 provider_key: The provider key (e.g., 'IONOS', 'GOOGLE')
196 Returns:
197 Provider class or None if not found
198 """
199 provider_info = self.get_provider_info(provider_key)
200 return provider_info.provider_class if provider_info else None
203# Global instance
204provider_discovery = ProviderDiscovery()
207def discover_providers(force_refresh: bool = False) -> Dict[str, ProviderInfo]:
208 """Discover all available providers.
210 Args:
211 force_refresh: Force re-discovery even if already done
213 Returns:
214 Dictionary mapping provider keys to ProviderInfo objects
215 """
216 return provider_discovery.discover_providers(force_refresh)
219def get_discovered_provider_options() -> List[Dict]:
220 """Get list of discovered provider options for UI dropdowns.
222 Returns:
223 List of dictionaries with 'value' and 'label' keys
224 """
225 return provider_discovery.get_provider_options()
228def get_provider_class(provider_key: str):
229 """Get the provider class for a given key.
231 Args:
232 provider_key: The provider key (e.g., 'IONOS', 'GOOGLE')
234 Returns:
235 Provider class or None if not found
236 """
237 return provider_discovery.get_provider_class(provider_key)