omnimancer-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,988 @@
1
+ """
2
+ Provider factory for Omnimancer.
3
+
4
+ This module provides a factory for creating AI provider instances
5
+ based on configuration and provider type.
6
+ """
7
+
8
+ from typing import Dict, List, Type
9
+
10
+ from ..core.models import EnhancedModelInfo, ModelInfo, ProviderConfig
11
+ from ..utils.errors import ConfigurationError
12
+ from .azure import AzureProvider
13
+ from .base import BaseProvider
14
+ from .bedrock import BedrockProvider
15
+ from .claude import ClaudeProvider
16
+ from .claude_code import ClaudeCodeProvider
17
+ from .cohere import CohereProvider
18
+ from .gemini import GeminiProvider
19
+ from .mistral import MistralProvider
20
+ from .ollama import OllamaProvider
21
+ from .openai import OpenAIProvider
22
+ from .openrouter import OpenRouterProvider
23
+ from .perplexity import PerplexityProvider
24
+ from .vertex import VertexAIProvider
25
+ from .xai import XAIProvider
26
+
27
+
28
+ class ProviderFactory:
29
+ """
30
+ Factory for creating AI provider instances.
31
+
32
+ This class manages the registration and creation of different
33
+ AI provider implementations with lazy loading and caching.
34
+ """
35
+
36
+ _providers: Dict[str, Type[BaseProvider]] = {}
37
+ _provider_instances: Dict[str, BaseProvider] = {}
38
+ _model_cache: Dict[str, List[ModelInfo]] = {}
39
+ _enhanced_model_cache: Dict[str, List[EnhancedModelInfo]] = {}
40
+ _cache_timestamps: Dict[str, float] = {}
41
+ _cache_ttl: float = 3600.0 # 1 hour cache TTL
42
+ _lazy_loading_enabled: bool = True
43
+
44
+ @classmethod
45
+ def register_provider(cls, name: str, provider_class: Type[BaseProvider]) -> None:
46
+ """
47
+ Register a provider class.
48
+
49
+ Args:
50
+ name: Provider name (e.g., "claude", "openai")
51
+ provider_class: Provider class to register
52
+ """
53
+ cls._providers[name] = provider_class
54
+ # Clear cached data for this provider when re-registering
55
+ cls._clear_provider_cache(name)
56
+
57
+ @classmethod
58
+ def _clear_provider_cache(cls, provider_name: str) -> None:
59
+ """Clear cached data for a specific provider."""
60
+
61
+ if provider_name in cls._model_cache:
62
+ del cls._model_cache[provider_name]
63
+ if provider_name in cls._enhanced_model_cache:
64
+ del cls._enhanced_model_cache[provider_name]
65
+ if provider_name in cls._cache_timestamps:
66
+ del cls._cache_timestamps[provider_name]
67
+ if provider_name in cls._provider_instances:
68
+ del cls._provider_instances[provider_name]
69
+
70
+ @classmethod
71
+ def _is_cache_valid(cls, provider_name: str) -> bool:
72
+ """Check if cached data for a provider is still valid."""
73
+ import time
74
+
75
+ if provider_name not in cls._cache_timestamps:
76
+ return False
77
+ return (time.time() - cls._cache_timestamps[provider_name]) < cls._cache_ttl
78
+
79
+ @classmethod
80
+ def _get_cached_provider_instance(
81
+ cls, provider_name: str, config: ProviderConfig
82
+ ) -> BaseProvider:
83
+ """Get or create a cached provider instance."""
84
+ cache_key = f"{provider_name}:{config.model}:{hash(str(config.dict()))}"
85
+
86
+ if cache_key in cls._provider_instances and cls._is_cache_valid(cache_key):
87
+ return cls._provider_instances[cache_key]
88
+
89
+ # Create new instance
90
+ if provider_name not in cls._providers:
91
+ raise ConfigurationError(f"Unknown provider: {provider_name}")
92
+
93
+ provider_class = cls._providers[provider_name]
94
+ instance = provider_class(
95
+ api_key=config.api_key,
96
+ model=config.model,
97
+ **config.dict(exclude={"api_key", "model"}),
98
+ )
99
+
100
+ # Cache the instance
101
+ import time
102
+
103
+ cls._provider_instances[cache_key] = instance
104
+ cls._cache_timestamps[cache_key] = time.time()
105
+
106
+ return instance
107
+
108
+ @classmethod
109
+ def _get_cached_models(cls, provider_name: str, enhanced: bool = False) -> List:
110
+ """Get cached model information for a provider."""
111
+ import time
112
+
113
+ cache_dict = cls._enhanced_model_cache if enhanced else cls._model_cache
114
+
115
+ if provider_name in cache_dict and cls._is_cache_valid(provider_name):
116
+ return cache_dict[provider_name]
117
+
118
+ # Cache miss - need to fetch models
119
+ if provider_name not in cls._providers:
120
+ return []
121
+
122
+ try:
123
+ provider_class = cls._providers[provider_name]
124
+ temp_provider = provider_class(api_key="dummy", model="dummy")
125
+ models = temp_provider.get_available_models()
126
+
127
+ if enhanced:
128
+ # Convert to EnhancedModelInfo if needed
129
+ if models and isinstance(models[0], ModelInfo):
130
+ models = [
131
+ EnhancedModelInfo.from_model_info(model) for model in models
132
+ ]
133
+ cls._enhanced_model_cache[provider_name] = models
134
+ else:
135
+ # Convert to ModelInfo if needed
136
+ if models and isinstance(models[0], EnhancedModelInfo):
137
+ models = [model.to_model_info() for model in models]
138
+ cls._model_cache[provider_name] = models
139
+
140
+ cls._cache_timestamps[provider_name] = time.time()
141
+ return models
142
+
143
+ except Exception:
144
+ # Return empty list on error
145
+ cache_dict[provider_name] = []
146
+ cls._cache_timestamps[provider_name] = time.time()
147
+ return []
148
+
149
+ @classmethod
150
+ def clear_all_caches(cls) -> None:
151
+ """Clear all cached data."""
152
+ cls._model_cache.clear()
153
+ cls._enhanced_model_cache.clear()
154
+ cls._cache_timestamps.clear()
155
+ cls._provider_instances.clear()
156
+
157
+ @classmethod
158
+ def set_cache_ttl(cls, ttl_seconds: float) -> None:
159
+ """Set the cache TTL (time to live) in seconds."""
160
+ cls._cache_ttl = ttl_seconds
161
+
162
+ @classmethod
163
+ def enable_lazy_loading(cls, enabled: bool = True) -> None:
164
+ """Enable or disable lazy loading."""
165
+ cls._lazy_loading_enabled = enabled
166
+ if not enabled:
167
+ # If disabling lazy loading, clear caches to force immediate loading
168
+ cls.clear_all_caches()
169
+
170
+ @classmethod
171
+ def create_provider(
172
+ cls,
173
+ name: str,
174
+ config: ProviderConfig,
175
+ config_manager: "ConfigManager" = None,
176
+ ) -> BaseProvider:
177
+ """
178
+ Create a provider instance.
179
+
180
+ Args:
181
+ name: Provider name
182
+ config: Provider configuration
183
+ config_manager: Optional config manager for API key decryption
184
+
185
+ Returns:
186
+ Provider instance
187
+
188
+ Raises:
189
+ ConfigurationError: If provider is not registered
190
+ """
191
+ if name not in cls._providers:
192
+ raise ConfigurationError(f"Unknown provider: {name}")
193
+
194
+ # Use the optimized provider initializer for lazy loading and caching
195
+ from ..core.provider_initializer import ProviderInitializer
196
+
197
+ return ProviderInitializer.get_provider_instance(name, config, config_manager)
198
+
199
+ @classmethod
200
+ def get_available_providers(cls) -> list[str]:
201
+ """
202
+ Get list of available provider names.
203
+
204
+ Returns:
205
+ List of registered provider names
206
+ """
207
+ return list(cls._providers.keys())
208
+
209
+ @classmethod
210
+ def is_provider_available(cls, name: str) -> bool:
211
+ """
212
+ Check if a provider is available.
213
+
214
+ Args:
215
+ name: Provider name
216
+
217
+ Returns:
218
+ True if provider is registered, False otherwise
219
+ """
220
+ return name in cls._providers
221
+
222
+ @classmethod
223
+ def get_all_models(cls) -> Dict[str, List[ModelInfo]]:
224
+ """
225
+ Get all available models from all registered providers.
226
+
227
+ Returns:
228
+ Dictionary mapping provider names to their available models (legacy format)
229
+ """
230
+ all_models = {}
231
+
232
+ for provider_name, provider_class in cls._providers.items():
233
+ try:
234
+ # Create a temporary instance to get model info
235
+ # We'll use a dummy config since we only need model info
236
+ temp_provider = provider_class(api_key="dummy", model="dummy")
237
+ models = temp_provider.get_available_models()
238
+
239
+ # Convert EnhancedModelInfo to ModelInfo for backward compatibility
240
+ if models and isinstance(models[0], EnhancedModelInfo):
241
+ all_models[provider_name] = [
242
+ model.to_model_info() for model in models
243
+ ]
244
+ else:
245
+ all_models[provider_name] = models
246
+ except Exception:
247
+ # If we can't create the provider, skip it
248
+ all_models[provider_name] = []
249
+
250
+ return all_models
251
+
252
+ @classmethod
253
+ def get_all_enhanced_models(cls) -> Dict[str, List[EnhancedModelInfo]]:
254
+ """
255
+ Get all available models with enhanced information from all registered providers.
256
+
257
+ Returns:
258
+ Dictionary mapping provider names to their enhanced model information
259
+ """
260
+ all_models = {}
261
+
262
+ for provider_name, provider_class in cls._providers.items():
263
+ try:
264
+ # Create a temporary instance to get model info
265
+ temp_provider = provider_class(api_key="dummy", model="dummy")
266
+ models = temp_provider.get_available_models()
267
+
268
+ # Convert ModelInfo to EnhancedModelInfo if needed
269
+ if models and isinstance(models[0], ModelInfo):
270
+ all_models[provider_name] = [
271
+ EnhancedModelInfo.from_model_info(model) for model in models
272
+ ]
273
+ else:
274
+ all_models[provider_name] = models
275
+ except Exception:
276
+ # If we can't create the provider, skip it
277
+ all_models[provider_name] = []
278
+
279
+ return all_models
280
+
281
+ @classmethod
282
+ def get_all_models_with_capabilities(cls) -> Dict[str, Dict[str, any]]:
283
+ """
284
+ Get all available models with their capabilities and provider info.
285
+
286
+ Returns:
287
+ Dictionary with provider info and models with capabilities
288
+ """
289
+ result = {}
290
+
291
+ for provider_name, provider_class in cls._providers.items():
292
+ try:
293
+ # Create a temporary instance to get model info
294
+ temp_provider = provider_class(api_key="dummy", model="dummy")
295
+ models = temp_provider.get_available_models()
296
+
297
+ # Get provider capabilities
298
+ provider_supports_tools = temp_provider.supports_tools()
299
+ provider_supports_multimodal = temp_provider.supports_multimodal()
300
+
301
+ result[provider_name] = {
302
+ "models": models,
303
+ "provider_capabilities": {
304
+ "supports_tools": provider_supports_tools,
305
+ "supports_multimodal": provider_supports_multimodal,
306
+ "supports_streaming": temp_provider.supports_streaming(),
307
+ },
308
+ "latest_models": [
309
+ m for m in models if getattr(m, "latest_version", False)
310
+ ],
311
+ "tool_capable_models": [
312
+ m for m in models if getattr(m, "supports_tools", False)
313
+ ],
314
+ "multimodal_models": [
315
+ m for m in models if getattr(m, "supports_multimodal", False)
316
+ ],
317
+ }
318
+ except Exception:
319
+ # If we can't create the provider, provide minimal info
320
+ result[provider_name] = {
321
+ "models": [],
322
+ "provider_capabilities": {
323
+ "supports_tools": False,
324
+ "supports_multimodal": False,
325
+ "supports_streaming": False,
326
+ },
327
+ "latest_models": [],
328
+ "tool_capable_models": [],
329
+ "multimodal_models": [],
330
+ }
331
+
332
+ return result
333
+
334
+ @classmethod
335
+ def get_models_for_provider(cls, provider_name: str) -> List[ModelInfo]:
336
+ """
337
+ Get available models for a specific provider.
338
+
339
+ Args:
340
+ provider_name: Name of the provider
341
+
342
+ Returns:
343
+ List of ModelInfo objects for the provider
344
+
345
+ Raises:
346
+ ConfigurationError: If provider is not registered
347
+ """
348
+ if provider_name not in cls._providers:
349
+ raise ConfigurationError(f"Unknown provider: {provider_name}")
350
+
351
+ provider_class = cls._providers[provider_name]
352
+ try:
353
+ # Create a temporary instance to get model info
354
+ temp_provider = provider_class(api_key="dummy", model="dummy")
355
+ return temp_provider.get_available_models()
356
+ except Exception as e:
357
+ raise ConfigurationError(
358
+ f"Failed to get models for provider {provider_name}: {e}"
359
+ )
360
+
361
+ @classmethod
362
+ def validate_model_for_provider(cls, provider_name: str, model_name: str) -> bool:
363
+ """
364
+ Validate that a model is available for a specific provider.
365
+
366
+ Args:
367
+ provider_name: Name of the provider
368
+ model_name: Name of the model
369
+
370
+ Returns:
371
+ True if model is available for the provider
372
+ """
373
+ try:
374
+ models = cls.get_models_for_provider(provider_name)
375
+ return any(model.name == model_name for model in models)
376
+ except ConfigurationError:
377
+ return False
378
+
379
+ @classmethod
380
+ def get_latest_models(cls) -> Dict[str, List[ModelInfo]]:
381
+ """
382
+ Get the latest models from all providers.
383
+
384
+ Returns:
385
+ Dictionary mapping provider names to their latest models
386
+ """
387
+ latest_models = {}
388
+
389
+ for provider_name, provider_class in cls._providers.items():
390
+ try:
391
+ temp_provider = provider_class(api_key="dummy", model="dummy")
392
+ all_models = temp_provider.get_available_models()
393
+ latest_models[provider_name] = [
394
+ model
395
+ for model in all_models
396
+ if getattr(model, "latest_version", False)
397
+ ]
398
+ except Exception:
399
+ latest_models[provider_name] = []
400
+
401
+ return latest_models
402
+
403
+ @classmethod
404
+ def get_provider_capabilities(cls, provider_name: str) -> Dict[str, bool]:
405
+ """
406
+ Get capabilities for a specific provider.
407
+
408
+ Args:
409
+ provider_name: Name of the provider
410
+
411
+ Returns:
412
+ Dictionary with provider capabilities
413
+
414
+ Raises:
415
+ ConfigurationError: If provider is not registered
416
+ """
417
+ if provider_name not in cls._providers:
418
+ raise ConfigurationError(f"Unknown provider: {provider_name}")
419
+
420
+ try:
421
+ provider_class = cls._providers[provider_name]
422
+ temp_provider = provider_class(api_key="dummy", model="dummy")
423
+
424
+ return {
425
+ "supports_tools": temp_provider.supports_tools(),
426
+ "supports_multimodal": temp_provider.supports_multimodal(),
427
+ "supports_streaming": temp_provider.supports_streaming(),
428
+ }
429
+ except Exception as e:
430
+ raise ConfigurationError(
431
+ f"Failed to get capabilities for provider {provider_name}: {e}"
432
+ )
433
+
434
+ @classmethod
435
+ async def check_provider_health(
436
+ cls, provider_name: str, config: ProviderConfig
437
+ ) -> Dict[str, any]:
438
+ """
439
+ Check the health status of a provider.
440
+
441
+ Args:
442
+ provider_name: Name of the provider
443
+ config: Provider configuration
444
+
445
+ Returns:
446
+ Dictionary with health status information
447
+ """
448
+ if provider_name not in cls._providers:
449
+ return {
450
+ "status": "error",
451
+ "message": f"Unknown provider: {provider_name}",
452
+ "available": False,
453
+ "credentials_valid": False,
454
+ }
455
+
456
+ try:
457
+ # Create provider instance
458
+ provider = cls.create_provider(provider_name, config)
459
+
460
+ # Check credentials
461
+ credentials_valid = await provider.validate_credentials()
462
+
463
+ # Get model info to check if model is available
464
+ try:
465
+ model_info = provider.get_model_info()
466
+ model_available = model_info.available
467
+ except Exception:
468
+ model_available = False
469
+
470
+ # Determine overall status
471
+ if credentials_valid and model_available:
472
+ status = "healthy"
473
+ message = "Provider is working correctly"
474
+ elif credentials_valid:
475
+ status = "warning"
476
+ message = f'Provider accessible but model "{config.model}" may not be available'
477
+ else:
478
+ status = "error"
479
+ message = "Invalid credentials or provider not accessible"
480
+
481
+ return {
482
+ "status": status,
483
+ "message": message,
484
+ "available": True,
485
+ "credentials_valid": credentials_valid,
486
+ "model_available": model_available,
487
+ "provider_capabilities": cls.get_provider_capabilities(provider_name),
488
+ }
489
+
490
+ except Exception as e:
491
+ return {
492
+ "status": "error",
493
+ "message": f"Health check failed: {str(e)}",
494
+ "available": False,
495
+ "credentials_valid": False,
496
+ "model_available": False,
497
+ }
498
+
499
+ @classmethod
500
+ async def get_all_provider_health(
501
+ cls, configs: Dict[str, ProviderConfig]
502
+ ) -> Dict[str, Dict[str, any]]:
503
+ """
504
+ Check health status for all configured providers.
505
+
506
+ Args:
507
+ configs: Dictionary mapping provider names to their configurations
508
+
509
+ Returns:
510
+ Dictionary mapping provider names to their health status
511
+ """
512
+ health_status = {}
513
+
514
+ for provider_name in cls._providers.keys():
515
+ if provider_name in configs:
516
+ health_status[provider_name] = await cls.check_provider_health(
517
+ provider_name, configs[provider_name]
518
+ )
519
+ else:
520
+ health_status[provider_name] = {
521
+ "status": "not_configured",
522
+ "message": "Provider not configured",
523
+ "available": True, # Provider class is available
524
+ "credentials_valid": False,
525
+ "configured": False,
526
+ }
527
+
528
+ return health_status
529
+
530
+ @classmethod
531
+ def get_models_by_capability(cls, capability: str) -> Dict[str, List[ModelInfo]]:
532
+ """
533
+ Get models that support a specific capability.
534
+
535
+ Args:
536
+ capability: Capability to filter by ('tools', 'multimodal', 'streaming')
537
+
538
+ Returns:
539
+ Dictionary mapping provider names to models with the capability
540
+ """
541
+ capability_models = {}
542
+
543
+ for provider_name, provider_class in cls._providers.items():
544
+ try:
545
+ temp_provider = provider_class(api_key="dummy", model="dummy")
546
+ all_models = temp_provider.get_available_models()
547
+
548
+ if capability == "tools":
549
+ filtered_models = [
550
+ m for m in all_models if getattr(m, "supports_tools", False)
551
+ ]
552
+ elif capability == "multimodal":
553
+ filtered_models = [
554
+ m
555
+ for m in all_models
556
+ if getattr(m, "supports_multimodal", False)
557
+ ]
558
+ elif capability == "streaming":
559
+ # For streaming, check provider capability since it's not model-specific
560
+ if temp_provider.supports_streaming():
561
+ filtered_models = all_models
562
+ else:
563
+ filtered_models = []
564
+ else:
565
+ filtered_models = []
566
+
567
+ # Convert to legacy format if needed
568
+ if filtered_models and isinstance(
569
+ filtered_models[0], EnhancedModelInfo
570
+ ):
571
+ capability_models[provider_name] = [
572
+ model.to_model_info() for model in filtered_models
573
+ ]
574
+ else:
575
+ capability_models[provider_name] = filtered_models
576
+ except Exception:
577
+ capability_models[provider_name] = []
578
+
579
+ return capability_models
580
+
581
+ @classmethod
582
+ def get_enhanced_models_by_capability(
583
+ cls, capability: str
584
+ ) -> Dict[str, List[EnhancedModelInfo]]:
585
+ """
586
+ Get enhanced models that support a specific capability.
587
+
588
+ Args:
589
+ capability: Capability to filter by ('tools', 'multimodal', 'streaming', 'latest', 'free')
590
+
591
+ Returns:
592
+ Dictionary mapping provider names to enhanced models with the capability
593
+ """
594
+ capability_models = {}
595
+
596
+ for provider_name, provider_class in cls._providers.items():
597
+ try:
598
+ temp_provider = provider_class(api_key="dummy", model="dummy")
599
+ all_models = temp_provider.get_available_models()
600
+
601
+ # Convert to EnhancedModelInfo if needed
602
+ if all_models and isinstance(all_models[0], ModelInfo):
603
+ all_models = [
604
+ EnhancedModelInfo.from_model_info(model) for model in all_models
605
+ ]
606
+
607
+ if capability == "tools":
608
+ filtered_models = [m for m in all_models if m.supports_tools]
609
+ elif capability == "multimodal":
610
+ filtered_models = [m for m in all_models if m.supports_multimodal]
611
+ elif capability == "streaming":
612
+ # For streaming, check provider capability since it's not model-specific
613
+ if temp_provider.supports_streaming():
614
+ filtered_models = all_models
615
+ else:
616
+ filtered_models = []
617
+ elif capability == "latest":
618
+ filtered_models = [m for m in all_models if m.latest_version]
619
+ elif capability == "free":
620
+ filtered_models = [m for m in all_models if m.is_free]
621
+ elif capability == "available":
622
+ filtered_models = [
623
+ m for m in all_models if m.available and not m.deprecated
624
+ ]
625
+ else:
626
+ filtered_models = []
627
+
628
+ if filtered_models:
629
+ capability_models[provider_name] = filtered_models
630
+ except Exception:
631
+ capability_models[provider_name] = []
632
+
633
+ return capability_models
634
+
635
+ @classmethod
636
+ def get_latest_models_enhanced(cls) -> Dict[str, List[EnhancedModelInfo]]:
637
+ """
638
+ Get the latest models from all providers with enhanced information.
639
+
640
+ Returns:
641
+ Dictionary mapping provider names to their latest enhanced models
642
+ """
643
+ return cls.get_enhanced_models_by_capability("latest")
644
+
645
+ @classmethod
646
+ def get_models_by_performance(
647
+ cls, min_swe_score: float
648
+ ) -> Dict[str, List[EnhancedModelInfo]]:
649
+ """
650
+ Get models filtered by minimum SWE score.
651
+
652
+ Args:
653
+ min_swe_score: Minimum SWE score (0-100)
654
+
655
+ Returns:
656
+ Dictionary mapping provider names to high-performance models
657
+ """
658
+ performance_models = {}
659
+
660
+ for provider_name, provider_class in cls._providers.items():
661
+ try:
662
+ temp_provider = provider_class(api_key="dummy", model="dummy")
663
+ all_models = temp_provider.get_available_models()
664
+
665
+ # Convert to EnhancedModelInfo if needed
666
+ if all_models and isinstance(all_models[0], ModelInfo):
667
+ all_models = [
668
+ EnhancedModelInfo.from_model_info(model) for model in all_models
669
+ ]
670
+
671
+ high_perf_models = [
672
+ m
673
+ for m in all_models
674
+ if m.swe_score is not None and m.swe_score >= min_swe_score
675
+ ]
676
+
677
+ if high_perf_models:
678
+ performance_models[provider_name] = high_perf_models
679
+ except Exception:
680
+ performance_models[provider_name] = []
681
+
682
+ return performance_models
683
+
684
+ @classmethod
685
+ def get_models_by_pricing(
686
+ cls, max_cost_per_million: float
687
+ ) -> Dict[str, List[EnhancedModelInfo]]:
688
+ """
689
+ Get models filtered by maximum cost per million tokens.
690
+
691
+ Args:
692
+ max_cost_per_million: Maximum average cost per million tokens
693
+
694
+ Returns:
695
+ Dictionary mapping provider names to models within budget
696
+ """
697
+ budget_models = {}
698
+
699
+ for provider_name, provider_class in cls._providers.items():
700
+ try:
701
+ temp_provider = provider_class(api_key="dummy", model="dummy")
702
+ all_models = temp_provider.get_available_models()
703
+
704
+ # Convert to EnhancedModelInfo if needed
705
+ if all_models and isinstance(all_models[0], ModelInfo):
706
+ all_models = [
707
+ EnhancedModelInfo.from_model_info(model) for model in all_models
708
+ ]
709
+
710
+ affordable_models = []
711
+ for model in all_models:
712
+ if model.is_free:
713
+ affordable_models.append(model)
714
+ else:
715
+ avg_cost = (
716
+ model.cost_per_million_input + model.cost_per_million_output
717
+ ) / 2
718
+ if avg_cost <= max_cost_per_million:
719
+ affordable_models.append(model)
720
+
721
+ if affordable_models:
722
+ budget_models[provider_name] = affordable_models
723
+ except Exception:
724
+ budget_models[provider_name] = []
725
+
726
+ return budget_models
727
+
728
+ @classmethod
729
+ def get_provider_model_summary(cls, provider_name: str) -> Dict[str, any]:
730
+ """
731
+ Get comprehensive model summary for a specific provider.
732
+
733
+ Args:
734
+ provider_name: Name of the provider
735
+
736
+ Returns:
737
+ Dictionary with provider model summary and capabilities
738
+ """
739
+ if provider_name not in cls._providers:
740
+ return {"error": f"Provider {provider_name} not registered"}
741
+
742
+ try:
743
+ provider_class = cls._providers[provider_name]
744
+ temp_provider = provider_class(api_key="dummy", model="dummy")
745
+ models = temp_provider.get_available_models()
746
+
747
+ # Convert to EnhancedModelInfo if needed
748
+ if models and isinstance(models[0], ModelInfo):
749
+ models = [EnhancedModelInfo.from_model_info(model) for model in models]
750
+
751
+ # Calculate statistics
752
+ total_models = len(models)
753
+ available_models = len(
754
+ [m for m in models if m.available and not m.deprecated]
755
+ )
756
+ tool_capable = len([m for m in models if m.supports_tools])
757
+ multimodal = len([m for m in models if m.supports_multimodal])
758
+ free_models = len([m for m in models if m.is_free])
759
+ latest_models = len([m for m in models if m.latest_version])
760
+ deprecated_models = len([m for m in models if m.deprecated])
761
+
762
+ # Get provider capabilities
763
+ provider_caps = cls.get_provider_capabilities(provider_name)
764
+
765
+ # Find best performing model
766
+ best_model = None
767
+ if models:
768
+ scored_models = [m for m in models if m.swe_score is not None]
769
+ if scored_models:
770
+ best_model = max(scored_models, key=lambda x: x.swe_score)
771
+
772
+ # Find cheapest model
773
+ cheapest_model = None
774
+ if models:
775
+ paid_models = [m for m in models if not m.is_free]
776
+ if paid_models:
777
+ cheapest_model = min(
778
+ paid_models,
779
+ key=lambda x: (
780
+ x.cost_per_million_input + x.cost_per_million_output
781
+ )
782
+ / 2,
783
+ )
784
+
785
+ return {
786
+ "provider_name": provider_name,
787
+ "total_models": total_models,
788
+ "available_models": available_models,
789
+ "tool_capable_models": tool_capable,
790
+ "multimodal_models": multimodal,
791
+ "free_models": free_models,
792
+ "latest_models": latest_models,
793
+ "deprecated_models": deprecated_models,
794
+ "provider_capabilities": provider_caps,
795
+ "best_performing_model": (
796
+ {
797
+ "name": best_model.name,
798
+ "swe_score": best_model.swe_score,
799
+ "swe_rating": best_model.swe_rating,
800
+ }
801
+ if best_model
802
+ else None
803
+ ),
804
+ "cheapest_model": (
805
+ {
806
+ "name": cheapest_model.name,
807
+ "cost_display": cheapest_model.get_cost_display(),
808
+ }
809
+ if cheapest_model
810
+ else None
811
+ ),
812
+ "model_names": [
813
+ m.name for m in models if m.available and not m.deprecated
814
+ ],
815
+ }
816
+
817
+ except Exception as e:
818
+ return {
819
+ "error": f"Failed to get summary for provider {provider_name}: {str(e)}"
820
+ }
821
+
822
+ @classmethod
823
+ def get_all_provider_summaries(cls) -> Dict[str, Dict[str, any]]:
824
+ """
825
+ Get comprehensive summaries for all registered providers.
826
+
827
+ Returns:
828
+ Dictionary mapping provider names to their summaries
829
+ """
830
+ summaries = {}
831
+
832
+ for provider_name in cls._providers.keys():
833
+ summaries[provider_name] = cls.get_provider_model_summary(provider_name)
834
+
835
+ return summaries
836
+
837
+ @classmethod
838
+ def detect_latest_models(cls) -> Dict[str, List[str]]:
839
+ """
840
+ Detect and flag latest models across all providers.
841
+
842
+ Returns:
843
+ Dictionary mapping provider names to their latest model names
844
+ """
845
+ latest_models = {}
846
+
847
+ for provider_name, provider_class in cls._providers.items():
848
+ try:
849
+ temp_provider = provider_class(api_key="dummy", model="dummy")
850
+ models = temp_provider.get_available_models()
851
+
852
+ # Convert to EnhancedModelInfo if needed
853
+ if models and isinstance(models[0], ModelInfo):
854
+ models = [
855
+ EnhancedModelInfo.from_model_info(model) for model in models
856
+ ]
857
+
858
+ # Find latest models
859
+ latest = [m.name for m in models if m.latest_version]
860
+ if latest:
861
+ latest_models[provider_name] = latest
862
+
863
+ except Exception:
864
+ latest_models[provider_name] = []
865
+
866
+ return latest_models
867
+
868
+ @classmethod
869
+ def get_capability_matrix(cls) -> Dict[str, Dict[str, bool]]:
870
+ """
871
+ Get capability matrix showing which providers support which features.
872
+
873
+ Returns:
874
+ Dictionary with provider capabilities matrix
875
+ """
876
+ matrix = {}
877
+
878
+ for provider_name in cls._providers.keys():
879
+ try:
880
+ caps = cls.get_provider_capabilities(provider_name)
881
+ matrix[provider_name] = caps
882
+ except Exception:
883
+ matrix[provider_name] = {
884
+ "supports_tools": False,
885
+ "supports_multimodal": False,
886
+ "supports_streaming": False,
887
+ }
888
+
889
+ return matrix
890
+
891
+ @classmethod
892
+ async def batch_health_check(
893
+ cls, configs: Dict[str, ProviderConfig], timeout: float = 30.0
894
+ ) -> Dict[str, Dict[str, any]]:
895
+ """
896
+ Perform batch health checks on multiple providers with timeout.
897
+
898
+ Args:
899
+ configs: Dictionary mapping provider names to their configurations
900
+ timeout: Timeout for each health check in seconds
901
+
902
+ Returns:
903
+ Dictionary mapping provider names to their health status
904
+ """
905
+ import asyncio
906
+
907
+ async def check_single_provider(
908
+ provider_name: str, config: ProviderConfig
909
+ ) -> tuple[str, Dict[str, any]]:
910
+ try:
911
+ # Add timeout to individual health check
912
+ health_result = await asyncio.wait_for(
913
+ cls.check_provider_health(provider_name, config),
914
+ timeout=timeout,
915
+ )
916
+ return provider_name, health_result
917
+ except asyncio.TimeoutError:
918
+ return provider_name, {
919
+ "status": "timeout",
920
+ "message": f"Health check timed out after {timeout} seconds",
921
+ "available": True,
922
+ "credentials_valid": False,
923
+ "model_available": False,
924
+ }
925
+ except Exception as e:
926
+ return provider_name, {
927
+ "status": "error",
928
+ "message": f"Health check failed: {str(e)}",
929
+ "available": False,
930
+ "credentials_valid": False,
931
+ "model_available": False,
932
+ }
933
+
934
+ # Create tasks for all configured providers
935
+ tasks = []
936
+ for provider_name, config in configs.items():
937
+ if provider_name in cls._providers:
938
+ tasks.append(check_single_provider(provider_name, config))
939
+
940
+ # Execute all health checks concurrently
941
+ results = await asyncio.gather(*tasks, return_exceptions=True)
942
+
943
+ # Process results
944
+ health_status = {}
945
+ for result in results:
946
+ if isinstance(result, tuple):
947
+ provider_name, status = result
948
+ health_status[provider_name] = status
949
+ else:
950
+ # Handle exceptions
951
+ health_status["unknown"] = {
952
+ "status": "error",
953
+ "message": f"Unexpected error: {str(result)}",
954
+ "available": False,
955
+ "credentials_valid": False,
956
+ "model_available": False,
957
+ }
958
+
959
+ # Add unconfigured providers
960
+ for provider_name in cls._providers.keys():
961
+ if provider_name not in health_status:
962
+ health_status[provider_name] = {
963
+ "status": "not_configured",
964
+ "message": "Provider not configured",
965
+ "available": True,
966
+ "credentials_valid": False,
967
+ "configured": False,
968
+ }
969
+
970
+ return health_status
971
+
972
+
973
+ # Register built-in providers
974
+ ProviderFactory.register_provider("claude", ClaudeProvider)
975
+ ProviderFactory.register_provider("openai", OpenAIProvider)
976
+ ProviderFactory.register_provider("gemini", GeminiProvider)
977
+ ProviderFactory.register_provider("cohere", CohereProvider)
978
+ ProviderFactory.register_provider("ollama", OllamaProvider)
979
+
980
+ # Register new providers
981
+ ProviderFactory.register_provider("perplexity", PerplexityProvider)
982
+ ProviderFactory.register_provider("xai", XAIProvider)
983
+ ProviderFactory.register_provider("mistral", MistralProvider)
984
+ ProviderFactory.register_provider("azure", AzureProvider)
985
+ ProviderFactory.register_provider("vertex", VertexAIProvider)
986
+ ProviderFactory.register_provider("bedrock", BedrockProvider)
987
+ ProviderFactory.register_provider("openrouter", OpenRouterProvider)
988
+ ProviderFactory.register_provider("claude-code", ClaudeCodeProvider)