abstractcore 2.4.0__py3-none-any.whl → 2.4.2__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.
@@ -64,59 +64,10 @@ def create_llm(provider: str, model: Optional[str] = None, **kwargs) -> Abstract
64
64
  elif (".gguf" in model.lower() or "-gguf" in model.lower()) and provider.lower() == "mlx":
65
65
  provider = "huggingface"
66
66
 
67
- # Mock provider for testing
68
- if provider.lower() == "mock":
69
- from ..providers.mock_provider import MockProvider
70
- return MockProvider(model=model or "mock-model", **kwargs)
71
-
72
- # Import providers dynamically to avoid hard dependencies
73
- elif provider.lower() == "openai":
74
- try:
75
- from ..providers.openai_provider import OpenAIProvider
76
- return OpenAIProvider(model=model or "gpt-5-nano-2025-08-07", **kwargs)
77
- except ImportError:
78
- raise ImportError("OpenAI dependencies not installed. Install with: pip install abstractcore[openai]")
79
- except (ModelNotFoundError, AuthenticationError, ProviderAPIError) as e:
80
- # Re-raise provider exceptions cleanly
81
- raise e
82
-
83
- elif provider.lower() == "anthropic":
84
- try:
85
- from ..providers.anthropic_provider import AnthropicProvider
86
- return AnthropicProvider(model=model or "claude-3-5-haiku-latest", **kwargs)
87
- except ImportError:
88
- raise ImportError("Anthropic dependencies not installed. Install with: pip install abstractcore[anthropic]")
89
- except (ModelNotFoundError, AuthenticationError, ProviderAPIError) as e:
90
- # Re-raise provider exceptions cleanly
91
- raise e
92
-
93
- elif provider.lower() == "ollama":
94
- try:
95
- from ..providers.ollama_provider import OllamaProvider
96
- return OllamaProvider(model=model or "qwen3-coder:30b", **kwargs)
97
- except ImportError:
98
- raise ImportError("Ollama dependencies not installed. Install with: pip install abstractcore[ollama]")
99
-
100
- elif provider.lower() == "huggingface":
101
- try:
102
- from ..providers.huggingface_provider import HuggingFaceProvider
103
- return HuggingFaceProvider(model=model or "Qwen/Qwen3-4B/", **kwargs)
104
- except ImportError:
105
- raise ImportError("HuggingFace dependencies not installed. Install with: pip install abstractcore[huggingface]")
106
-
107
- elif provider.lower() == "mlx":
108
- try:
109
- from ..providers.mlx_provider import MLXProvider
110
- return MLXProvider(model=model or "mlx-community/Qwen3-4B", **kwargs)
111
- except ImportError:
112
- raise ImportError("MLX dependencies not installed. Install with: pip install abstractcore[mlx]")
113
-
114
- elif provider.lower() == "lmstudio":
115
- try:
116
- from ..providers.lmstudio_provider import LMStudioProvider
117
- return LMStudioProvider(model=model or "qwen/qwen3-4b-2507", **kwargs)
118
- except ImportError:
119
- raise ImportError("LM Studio provider not available")
120
-
121
- else:
122
- raise ValueError(f"Unknown provider: {provider}. Available providers: openai, anthropic, ollama, huggingface, mlx, lmstudio, mock")
67
+ # Use centralized provider registry for all provider creation
68
+ try:
69
+ from ..providers.registry import create_provider
70
+ return create_provider(provider, model, **kwargs)
71
+ except (ModelNotFoundError, AuthenticationError, ProviderAPIError) as e:
72
+ # Re-raise provider exceptions cleanly
73
+ raise e
@@ -0,0 +1,125 @@
1
+ """
2
+ Custom exceptions for AbstractCore.
3
+ """
4
+
5
+
6
+ class AbstractCoreError(Exception):
7
+ """Base exception for AbstractCore"""
8
+ pass
9
+
10
+
11
+ class ProviderError(AbstractCoreError):
12
+ """Base exception for provider-related errors"""
13
+ pass
14
+
15
+
16
+ class ProviderAPIError(ProviderError):
17
+ """API call to provider failed"""
18
+ pass
19
+
20
+
21
+ class AuthenticationError(ProviderError):
22
+ """Authentication with provider failed"""
23
+ pass
24
+
25
+
26
+ # Alias for backward compatibility with old AbstractCore
27
+ Authentication = AuthenticationError
28
+
29
+
30
+ class RateLimitError(ProviderError):
31
+ """Rate limit exceeded"""
32
+ pass
33
+
34
+
35
+ class InvalidRequestError(ProviderError):
36
+ """Invalid request to provider"""
37
+ pass
38
+
39
+
40
+ class UnsupportedFeatureError(AbstractCoreError):
41
+ """Feature not supported by provider"""
42
+ pass
43
+
44
+
45
+ class FileProcessingError(AbstractCoreError):
46
+ """Error processing file or media"""
47
+ pass
48
+
49
+
50
+ class ToolExecutionError(AbstractCoreError):
51
+ """Error executing tool"""
52
+ pass
53
+
54
+
55
+ class SessionError(AbstractCoreError):
56
+ """Error with session management"""
57
+ pass
58
+
59
+
60
+ class ConfigurationError(AbstractCoreError):
61
+ """Invalid configuration"""
62
+ pass
63
+
64
+
65
+ class ModelNotFoundError(ProviderError):
66
+ """Model not found or invalid model name"""
67
+ pass
68
+
69
+
70
+ def format_model_error(provider: str, invalid_model: str, available_models: list) -> str:
71
+ """
72
+ Format a helpful error message for model not found errors.
73
+
74
+ Args:
75
+ provider: Provider name (e.g., "OpenAI", "Anthropic")
76
+ invalid_model: The model name that was not found
77
+ available_models: List of available model names
78
+
79
+ Returns:
80
+ Formatted error message string
81
+ """
82
+ message = f"❌ Model '{invalid_model}' not found for {provider} provider.\n"
83
+
84
+ if available_models:
85
+ message += f"\n✅ Available models ({len(available_models)}):\n"
86
+ for model in available_models[:30]: # Show max 30
87
+ message += f" • {model}\n"
88
+ if len(available_models) > 30:
89
+ message += f" ... and {len(available_models) - 30} more\n"
90
+ else:
91
+ # Show provider documentation when we can't fetch models
92
+ doc_links = {
93
+ "anthropic": "https://docs.anthropic.com/en/docs/about-claude/models",
94
+ "openai": "https://platform.openai.com/docs/models",
95
+ "ollama": "https://ollama.com/library",
96
+ "huggingface": "https://huggingface.co/models",
97
+ "mlx": "https://huggingface.co/mlx-community"
98
+ }
99
+
100
+ provider_lower = provider.lower()
101
+ if provider_lower in doc_links:
102
+ message += f"\n📚 See available models: {doc_links[provider_lower]}\n"
103
+ else:
104
+ message += f"\n⚠️ Could not fetch available models for {provider}.\n"
105
+
106
+ return message.rstrip()
107
+
108
+
109
+ # Export all exceptions for easy importing
110
+ __all__ = [
111
+ 'AbstractCoreError',
112
+ 'ProviderError',
113
+ 'ProviderAPIError',
114
+ 'AuthenticationError',
115
+ 'Authentication', # Backward compatibility alias
116
+ 'RateLimitError',
117
+ 'InvalidRequestError',
118
+ 'UnsupportedFeatureError',
119
+ 'FileProcessingError',
120
+ 'ToolExecutionError',
121
+ 'SessionError',
122
+ 'ConfigurationError',
123
+ 'ModelNotFoundError',
124
+ 'format_model_error'
125
+ ]
@@ -0,0 +1,151 @@
1
+ """
2
+ Media handling for different providers.
3
+ """
4
+
5
+ import base64
6
+ from pathlib import Path
7
+ from typing import Union, Dict, Any, Optional
8
+ from enum import Enum
9
+
10
+
11
+ class MediaType(Enum):
12
+ """Supported media types"""
13
+ IMAGE = "image"
14
+ AUDIO = "audio"
15
+ VIDEO = "video"
16
+ DOCUMENT = "document"
17
+
18
+
19
+ class MediaHandler:
20
+ """Base class for media handling"""
21
+
22
+ @staticmethod
23
+ def encode_image(image_path: Union[str, Path]) -> str:
24
+ """
25
+ Encode an image file to base64.
26
+
27
+ Args:
28
+ image_path: Path to the image file
29
+
30
+ Returns:
31
+ Base64 encoded string
32
+ """
33
+ with open(image_path, "rb") as image_file:
34
+ return base64.b64encode(image_file.read()).decode('utf-8')
35
+
36
+ @staticmethod
37
+ def format_for_openai(image_path: Union[str, Path]) -> Dict[str, Any]:
38
+ """
39
+ Format image for OpenAI API.
40
+
41
+ Args:
42
+ image_path: Path to the image
43
+
44
+ Returns:
45
+ Formatted content for OpenAI
46
+ """
47
+ base64_image = MediaHandler.encode_image(image_path)
48
+ return {
49
+ "type": "image_url",
50
+ "image_url": {
51
+ "url": f"data:image/jpeg;base64,{base64_image}"
52
+ }
53
+ }
54
+
55
+ @staticmethod
56
+ def format_for_anthropic(image_path: Union[str, Path]) -> Dict[str, Any]:
57
+ """
58
+ Format image for Anthropic API.
59
+
60
+ Args:
61
+ image_path: Path to the image
62
+
63
+ Returns:
64
+ Formatted content for Anthropic
65
+ """
66
+ base64_image = MediaHandler.encode_image(image_path)
67
+
68
+ # Detect image type
69
+ path = Path(image_path)
70
+ media_type = "image/jpeg"
71
+ if path.suffix.lower() == ".png":
72
+ media_type = "image/png"
73
+ elif path.suffix.lower() == ".gif":
74
+ media_type = "image/gif"
75
+ elif path.suffix.lower() == ".webp":
76
+ media_type = "image/webp"
77
+
78
+ return {
79
+ "type": "image",
80
+ "source": {
81
+ "type": "base64",
82
+ "media_type": media_type,
83
+ "data": base64_image
84
+ }
85
+ }
86
+
87
+ @staticmethod
88
+ def format_for_provider(image_path: Union[str, Path], provider: str) -> Optional[Dict[str, Any]]:
89
+ """
90
+ Format media for a specific provider.
91
+
92
+ Args:
93
+ image_path: Path to the media file
94
+ provider: Provider name
95
+
96
+ Returns:
97
+ Formatted content or None if not supported
98
+ """
99
+ provider_lower = provider.lower()
100
+
101
+ if provider_lower == "openai":
102
+ return MediaHandler.format_for_openai(image_path)
103
+ elif provider_lower == "anthropic":
104
+ return MediaHandler.format_for_anthropic(image_path)
105
+ else:
106
+ # Local providers typically don't support images directly
107
+ return None
108
+
109
+ @staticmethod
110
+ def is_image_file(path: Union[str, Path]) -> bool:
111
+ """
112
+ Check if a file is an image.
113
+
114
+ Args:
115
+ path: Path to check
116
+
117
+ Returns:
118
+ True if the file is an image
119
+ """
120
+ image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.ico', '.tiff'}
121
+ return Path(path).suffix.lower() in image_extensions
122
+
123
+ @staticmethod
124
+ def get_media_type(path: Union[str, Path]) -> MediaType:
125
+ """
126
+ Determine the media type of a file.
127
+
128
+ Args:
129
+ path: Path to the file
130
+
131
+ Returns:
132
+ MediaType enum value
133
+ """
134
+ path = Path(path)
135
+ extension = path.suffix.lower()
136
+
137
+ image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
138
+ audio_extensions = {'.mp3', '.wav', '.m4a', '.ogg', '.flac'}
139
+ video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.webm'}
140
+ document_extensions = {'.pdf', '.doc', '.docx', '.txt', '.md'}
141
+
142
+ if extension in image_extensions:
143
+ return MediaType.IMAGE
144
+ elif extension in audio_extensions:
145
+ return MediaType.AUDIO
146
+ elif extension in video_extensions:
147
+ return MediaType.VIDEO
148
+ elif extension in document_extensions:
149
+ return MediaType.DOCUMENT
150
+ else:
151
+ return MediaType.DOCUMENT # Default to document
@@ -9,7 +9,22 @@ from .huggingface_provider import HuggingFaceProvider
9
9
  from .mlx_provider import MLXProvider
10
10
  from .mock_provider import MockProvider
11
11
 
12
+ # Provider registry for centralized provider discovery and management
13
+ from .registry import (
14
+ ProviderRegistry,
15
+ ProviderInfo,
16
+ get_provider_registry,
17
+ list_available_providers,
18
+ get_provider_info,
19
+ is_provider_available,
20
+ get_all_providers_with_models,
21
+ get_all_providers_status,
22
+ create_provider,
23
+ get_available_models_for_provider
24
+ )
25
+
12
26
  __all__ = [
27
+ # Provider classes
13
28
  'BaseProvider',
14
29
  'OpenAIProvider',
15
30
  'AnthropicProvider',
@@ -18,4 +33,16 @@ __all__ = [
18
33
  'HuggingFaceProvider',
19
34
  'MLXProvider',
20
35
  'MockProvider',
36
+
37
+ # Provider registry
38
+ 'ProviderRegistry',
39
+ 'ProviderInfo',
40
+ 'get_provider_registry',
41
+ 'list_available_providers',
42
+ 'get_provider_info',
43
+ 'is_provider_available',
44
+ 'get_all_providers_with_models',
45
+ 'get_all_providers_status',
46
+ 'create_provider',
47
+ 'get_available_models_for_provider',
21
48
  ]
@@ -0,0 +1,406 @@
1
+ """
2
+ Provider Registry - Centralized provider discovery and metadata management.
3
+
4
+ This module provides a single source of truth for all AbstractCore providers,
5
+ eliminating the need for manual synchronization across factory.py, server/app.py,
6
+ and __init__.py files.
7
+ """
8
+
9
+ from typing import List, Dict, Any, Optional, Type, Callable
10
+ from dataclasses import dataclass, field
11
+ from abc import ABC
12
+ import logging
13
+ from ..utils.structured_logging import get_logger
14
+
15
+ logger = get_logger("provider_registry")
16
+
17
+
18
+ @dataclass
19
+ class ProviderInfo:
20
+ """Information about a registered provider."""
21
+ name: str
22
+ display_name: str
23
+ provider_class: Type
24
+ description: str
25
+ provider_type: str = "llm"
26
+ default_model: Optional[str] = None
27
+ supported_features: List[str] = field(default_factory=list)
28
+ authentication_required: bool = True
29
+ local_provider: bool = False
30
+ installation_extras: Optional[str] = None
31
+ import_path: str = ""
32
+
33
+ def __post_init__(self):
34
+ """Set default values after initialization."""
35
+ if not self.import_path:
36
+ self.import_path = f"..providers.{self.name}_provider"
37
+
38
+
39
+ class ProviderRegistry:
40
+ """
41
+ Centralized registry for all AbstractCore providers.
42
+
43
+ This registry serves as the single source of truth for provider discovery,
44
+ metadata, and instantiation across the entire AbstractCore system.
45
+ """
46
+
47
+ def __init__(self):
48
+ self._providers: Dict[str, ProviderInfo] = {}
49
+ self._logger = get_logger("ProviderRegistry")
50
+ self._register_all_providers()
51
+
52
+ def _register_all_providers(self):
53
+ """Register all available providers with their metadata."""
54
+
55
+ # OpenAI Provider
56
+ self.register_provider(ProviderInfo(
57
+ name="openai",
58
+ display_name="OpenAI",
59
+ provider_class=None, # Will be set during lazy loading
60
+ description="Commercial API with GPT-4, GPT-3.5, and embedding models",
61
+ default_model="gpt-5-nano-2025-08-07",
62
+ supported_features=["chat", "completion", "embeddings", "native_tools", "streaming", "structured_output"],
63
+ authentication_required=True,
64
+ local_provider=False,
65
+ installation_extras="openai",
66
+ import_path="..providers.openai_provider"
67
+ ))
68
+
69
+ # Anthropic Provider
70
+ self.register_provider(ProviderInfo(
71
+ name="anthropic",
72
+ display_name="Anthropic",
73
+ provider_class=None,
74
+ description="Commercial API with Claude 3 family models",
75
+ default_model="claude-3-5-haiku-latest",
76
+ supported_features=["chat", "completion", "native_tools", "streaming", "structured_output"],
77
+ authentication_required=True,
78
+ local_provider=False,
79
+ installation_extras="anthropic",
80
+ import_path="..providers.anthropic_provider"
81
+ ))
82
+
83
+ # Ollama Provider
84
+ self.register_provider(ProviderInfo(
85
+ name="ollama",
86
+ display_name="Ollama",
87
+ provider_class=None,
88
+ description="Local LLM server for running open-source models",
89
+ default_model="qwen3-coder:30b",
90
+ supported_features=["chat", "completion", "embeddings", "prompted_tools", "streaming"],
91
+ authentication_required=False,
92
+ local_provider=True,
93
+ installation_extras="ollama",
94
+ import_path="..providers.ollama_provider"
95
+ ))
96
+
97
+ # LMStudio Provider
98
+ self.register_provider(ProviderInfo(
99
+ name="lmstudio",
100
+ display_name="LMStudio",
101
+ provider_class=None,
102
+ description="Local model development and testing platform",
103
+ default_model="qwen/qwen3-4b-2507",
104
+ supported_features=["chat", "completion", "embeddings", "prompted_tools", "streaming"],
105
+ authentication_required=False,
106
+ local_provider=True,
107
+ installation_extras=None,
108
+ import_path="..providers.lmstudio_provider"
109
+ ))
110
+
111
+ # MLX Provider
112
+ self.register_provider(ProviderInfo(
113
+ name="mlx",
114
+ display_name="MLX",
115
+ provider_class=None,
116
+ description="Apple Silicon optimized local inference",
117
+ default_model="mlx-community/Qwen3-4B",
118
+ supported_features=["chat", "completion", "prompted_tools", "streaming", "apple_silicon"],
119
+ authentication_required=False,
120
+ local_provider=True,
121
+ installation_extras="mlx",
122
+ import_path="..providers.mlx_provider"
123
+ ))
124
+
125
+ # HuggingFace Provider
126
+ self.register_provider(ProviderInfo(
127
+ name="huggingface",
128
+ display_name="HuggingFace",
129
+ provider_class=None,
130
+ description="Access to HuggingFace models (transformers and embeddings)",
131
+ default_model="Qwen/Qwen3-4B/",
132
+ supported_features=["chat", "completion", "embeddings", "prompted_tools", "local_models"],
133
+ authentication_required=False, # Optional for public models
134
+ local_provider=True,
135
+ installation_extras="huggingface",
136
+ import_path="..providers.huggingface_provider"
137
+ ))
138
+
139
+ # Mock Provider
140
+ self.register_provider(ProviderInfo(
141
+ name="mock",
142
+ display_name="Mock",
143
+ provider_class=None,
144
+ description="Testing provider for development and unit tests",
145
+ default_model="mock-model",
146
+ supported_features=["chat", "completion", "embeddings", "prompted_tools", "streaming", "testing"],
147
+ authentication_required=False,
148
+ local_provider=True,
149
+ installation_extras=None,
150
+ import_path="..providers.mock_provider"
151
+ ))
152
+
153
+ def register_provider(self, provider_info: ProviderInfo):
154
+ """Register a provider in the registry."""
155
+ self._providers[provider_info.name] = provider_info
156
+ self._logger.debug(f"Registered provider: {provider_info.name}")
157
+
158
+ def get_provider_info(self, provider_name: str) -> Optional[ProviderInfo]:
159
+ """Get information about a specific provider."""
160
+ return self._providers.get(provider_name.lower())
161
+
162
+ def list_provider_names(self) -> List[str]:
163
+ """Get list of all registered provider names."""
164
+ return list(self._providers.keys())
165
+
166
+ def is_provider_available(self, provider_name: str) -> bool:
167
+ """Check if a provider is registered."""
168
+ return provider_name.lower() in self._providers
169
+
170
+ def get_provider_class(self, provider_name: str):
171
+ """Get the provider class, loading it lazily if needed."""
172
+ provider_info = self.get_provider_info(provider_name)
173
+ if not provider_info:
174
+ raise ValueError(f"Unknown provider: {provider_name}")
175
+
176
+ # Lazy loading of provider class
177
+ if provider_info.provider_class is None:
178
+ provider_info.provider_class = self._load_provider_class(provider_info)
179
+
180
+ return provider_info.provider_class
181
+
182
+ def _load_provider_class(self, provider_info: ProviderInfo):
183
+ """Dynamically load a provider class."""
184
+ try:
185
+ if provider_info.name == "mock":
186
+ from ..providers.mock_provider import MockProvider
187
+ return MockProvider
188
+ elif provider_info.name == "openai":
189
+ from ..providers.openai_provider import OpenAIProvider
190
+ return OpenAIProvider
191
+ elif provider_info.name == "anthropic":
192
+ from ..providers.anthropic_provider import AnthropicProvider
193
+ return AnthropicProvider
194
+ elif provider_info.name == "ollama":
195
+ from ..providers.ollama_provider import OllamaProvider
196
+ return OllamaProvider
197
+ elif provider_info.name == "lmstudio":
198
+ from ..providers.lmstudio_provider import LMStudioProvider
199
+ return LMStudioProvider
200
+ elif provider_info.name == "mlx":
201
+ from ..providers.mlx_provider import MLXProvider
202
+ return MLXProvider
203
+ elif provider_info.name == "huggingface":
204
+ from ..providers.huggingface_provider import HuggingFaceProvider
205
+ return HuggingFaceProvider
206
+ else:
207
+ raise ImportError(f"No import logic for provider: {provider_info.name}")
208
+ except ImportError as e:
209
+ self._logger.warning(f"Failed to load provider {provider_info.name}: {e}")
210
+ raise ImportError(
211
+ f"{provider_info.display_name} dependencies not installed. "
212
+ f"Install with: pip install abstractcore[{provider_info.installation_extras}]"
213
+ ) from e
214
+
215
+ def get_available_models(self, provider_name: str, **kwargs) -> List[str]:
216
+ """
217
+ Get available models for a specific provider.
218
+
219
+ Args:
220
+ provider_name: Name of the provider
221
+ **kwargs: Provider-specific parameters (e.g., api_key, base_url)
222
+
223
+ Returns:
224
+ List of available model names
225
+ """
226
+ try:
227
+ provider_class = self.get_provider_class(provider_name)
228
+
229
+ # Handle providers that need instance for model listing
230
+ if provider_name in ["anthropic", "ollama", "lmstudio"]:
231
+ provider_info = self.get_provider_info(provider_name)
232
+ # Create minimal instance for API access
233
+ instance = provider_class(model=provider_info.default_model, **kwargs)
234
+ return instance.list_available_models(**kwargs)
235
+ else:
236
+ # Handle providers with static method or class method
237
+ try:
238
+ # First try as static/class method
239
+ return provider_class.list_available_models(**kwargs)
240
+ except TypeError:
241
+ # If that fails (method needs 'self'), create temporary instance
242
+ provider_info = self.get_provider_info(provider_name)
243
+ instance = provider_class(model=provider_info.default_model, **kwargs)
244
+ return instance.list_available_models(**kwargs)
245
+
246
+ except Exception as e:
247
+ self._logger.debug(f"Failed to get models from provider {provider_name}: {e}")
248
+ return []
249
+
250
+ def get_provider_status(self, provider_name: str) -> Dict[str, Any]:
251
+ """
252
+ Get detailed status information for a provider.
253
+
254
+ Returns provider information including availability, model count, etc.
255
+ This is used by the server /providers endpoint.
256
+ """
257
+ provider_info = self.get_provider_info(provider_name)
258
+ if not provider_info:
259
+ return {
260
+ "name": provider_name,
261
+ "status": "unknown",
262
+ "error": "Provider not registered"
263
+ }
264
+
265
+ try:
266
+ # Try to get models to test availability
267
+ models = self.get_available_models(provider_name)
268
+
269
+ return {
270
+ "name": provider_info.name,
271
+ "display_name": provider_info.display_name,
272
+ "type": provider_info.provider_type,
273
+ "model_count": len(models),
274
+ "status": "available" if models else "no_models",
275
+ "description": provider_info.description,
276
+ "local_provider": provider_info.local_provider,
277
+ "authentication_required": provider_info.authentication_required,
278
+ "supported_features": provider_info.supported_features,
279
+ "installation_extras": provider_info.installation_extras,
280
+ "models": models
281
+ }
282
+ except Exception as e:
283
+ return {
284
+ "name": provider_info.name,
285
+ "display_name": provider_info.display_name,
286
+ "type": provider_info.provider_type,
287
+ "model_count": 0,
288
+ "status": "error",
289
+ "description": provider_info.description,
290
+ "error": str(e),
291
+ "local_provider": provider_info.local_provider,
292
+ "authentication_required": provider_info.authentication_required,
293
+ "supported_features": provider_info.supported_features,
294
+ "installation_extras": provider_info.installation_extras
295
+ }
296
+
297
+ def get_all_providers_status(self) -> List[Dict[str, Any]]:
298
+ """Get status information for all registered providers."""
299
+ return [
300
+ self.get_provider_status(provider_name)
301
+ for provider_name in self.list_provider_names()
302
+ ]
303
+
304
+ def get_providers_with_models(self) -> List[Dict[str, Any]]:
305
+ """Get only providers that have available models."""
306
+ all_providers = self.get_all_providers_status()
307
+ return [
308
+ provider for provider in all_providers
309
+ if provider.get("status") == "available" and provider.get("model_count", 0) > 0
310
+ ]
311
+
312
+ def create_provider_instance(self, provider_name: str, model: Optional[str] = None, **kwargs):
313
+ """
314
+ Create a provider instance using the registry.
315
+
316
+ This is used by the factory to create provider instances.
317
+ """
318
+ provider_info = self.get_provider_info(provider_name)
319
+ if not provider_info:
320
+ available_providers = ", ".join(self.list_provider_names())
321
+ raise ValueError(f"Unknown provider: {provider_name}. Available providers: {available_providers}")
322
+
323
+ provider_class = self.get_provider_class(provider_name)
324
+ model = model or provider_info.default_model
325
+
326
+ try:
327
+ return provider_class(model=model, **kwargs)
328
+ except ImportError as e:
329
+ # Re-raise import errors with helpful message
330
+ if provider_info.installation_extras:
331
+ raise ImportError(
332
+ f"{provider_info.display_name} dependencies not installed. "
333
+ f"Install with: pip install abstractcore[{provider_info.installation_extras}]"
334
+ ) from e
335
+ else:
336
+ raise ImportError(f"{provider_info.display_name} provider not available") from e
337
+
338
+
339
+ # Global registry instance
340
+ _registry = None
341
+
342
+
343
+ def get_provider_registry() -> ProviderRegistry:
344
+ """Get the global provider registry instance."""
345
+ global _registry
346
+ if _registry is None:
347
+ _registry = ProviderRegistry()
348
+ return _registry
349
+
350
+
351
+ # Convenience functions for external use
352
+ def list_available_providers() -> List[str]:
353
+ """Get list of all available provider names."""
354
+ return get_provider_registry().list_provider_names()
355
+
356
+
357
+ def get_provider_info(provider_name: str) -> Optional[ProviderInfo]:
358
+ """Get information about a specific provider."""
359
+ return get_provider_registry().get_provider_info(provider_name)
360
+
361
+
362
+ def is_provider_available(provider_name: str) -> bool:
363
+ """Check if a provider is available."""
364
+ return get_provider_registry().is_provider_available(provider_name)
365
+
366
+
367
+ def get_all_providers_with_models() -> List[Dict[str, Any]]:
368
+ """
369
+ Get comprehensive information about all providers with available models.
370
+
371
+ This is the main function that should be used throughout AbstractCore
372
+ for provider discovery and information. It replaces the manual provider
373
+ lists in factory.py and server/app.py.
374
+
375
+ Returns:
376
+ List of provider dictionaries with comprehensive metadata including:
377
+ - name, display_name, type, description
378
+ - model_count, status, supported_features
379
+ - local_provider, authentication_required
380
+ - installation_extras, sample models
381
+ """
382
+ return get_provider_registry().get_providers_with_models()
383
+
384
+
385
+ def get_all_providers_status() -> List[Dict[str, Any]]:
386
+ """
387
+ Get status information for all registered providers.
388
+
389
+ This includes providers that may not have models available,
390
+ useful for debugging and comprehensive provider listing.
391
+ """
392
+ return get_provider_registry().get_all_providers_status()
393
+
394
+
395
+ def create_provider(provider_name: str, model: Optional[str] = None, **kwargs):
396
+ """
397
+ Create a provider instance using the centralized registry.
398
+
399
+ This replaces the factory logic and provides better error messages.
400
+ """
401
+ return get_provider_registry().create_provider_instance(provider_name, model, **kwargs)
402
+
403
+
404
+ def get_available_models_for_provider(provider_name: str, **kwargs) -> List[str]:
405
+ """Get available models for a specific provider."""
406
+ return get_provider_registry().get_available_models(provider_name, **kwargs)
@@ -109,50 +109,14 @@ def is_embedding_model(model_name: str) -> bool:
109
109
  return any(pattern in model_lower for pattern in embedding_patterns)
110
110
 
111
111
  # ============================================================================
112
- # Provider Model Discovery
112
+ # Provider Model Discovery (Using Centralized Registry)
113
113
  # ============================================================================
114
114
 
115
115
  def get_models_from_provider(provider_name: str) -> List[str]:
116
- """Get available models from a specific provider using their list_available_models() method."""
116
+ """Get available models from a specific provider using the centralized provider registry."""
117
117
  try:
118
- if provider_name == "openai":
119
- from ..providers.openai_provider import OpenAIProvider
120
- return OpenAIProvider.list_available_models()
121
- elif provider_name == "anthropic":
122
- from ..providers.anthropic_provider import AnthropicProvider
123
- # Need minimal instance for API key access
124
- try:
125
- provider = AnthropicProvider(model="claude-3-haiku-20240307")
126
- return provider.list_available_models()
127
- except Exception:
128
- return []
129
- elif provider_name == "ollama":
130
- from ..providers.ollama_provider import OllamaProvider
131
- # Need minimal instance for HTTP client
132
- try:
133
- provider = OllamaProvider(model="llama2")
134
- return provider.list_available_models()
135
- except Exception:
136
- return []
137
- elif provider_name == "lmstudio":
138
- from ..providers.lmstudio_provider import LMStudioProvider
139
- # Need minimal instance for HTTP client
140
- try:
141
- provider = LMStudioProvider(model="local-model")
142
- return provider.list_available_models()
143
- except Exception:
144
- return []
145
- elif provider_name == "mlx":
146
- from ..providers.mlx_provider import MLXProvider
147
- return MLXProvider.list_available_models()
148
- elif provider_name == "huggingface":
149
- from ..providers.huggingface_provider import HuggingFaceProvider
150
- return HuggingFaceProvider.list_available_models()
151
- elif provider_name == "mock":
152
- # Mock provider for testing
153
- return ["mock-model-1", "mock-model-2", "mock-embedding-1"]
154
- else:
155
- return []
118
+ from ..providers.registry import get_available_models_for_provider
119
+ return get_available_models_for_provider(provider_name)
156
120
  except Exception as e:
157
121
  logger.debug(f"Failed to get models from provider {provider_name}: {e}")
158
122
  return []
@@ -508,7 +472,7 @@ async def list_models(
508
472
  continue # Skip non-embedding models
509
473
  if type == ModelType.TEXT_GENERATION and is_embedding:
510
474
  continue # Skip embedding models
511
-
475
+
512
476
  model_id = f"{provider.lower()}/{model}"
513
477
  models_data.append({
514
478
  "id": model_id,
@@ -517,12 +481,13 @@ async def list_models(
517
481
  "created": int(time.time()),
518
482
  "permission": [{"allow_create_engine": False, "allow_sampling": True}]
519
483
  })
520
-
484
+
521
485
  filter_msg = f" (type={type.value})" if type else ""
522
486
  logger.info(f"Listed {len(models_data)} models for provider {provider}{filter_msg}")
523
487
  else:
524
- # Get models from all providers
525
- providers = ["openai", "anthropic", "ollama", "lmstudio", "mlx", "huggingface", "mock"]
488
+ # Get models from all providers using centralized registry
489
+ from ..providers.registry import list_available_providers
490
+ providers = list_available_providers()
526
491
  for prov in providers:
527
492
  models = get_models_from_provider(prov)
528
493
  for model in models:
@@ -533,7 +498,7 @@ async def list_models(
533
498
  continue # Skip non-embedding models
534
499
  if type == ModelType.TEXT_GENERATION and is_embedding:
535
500
  continue # Skip embedding models
536
-
501
+
537
502
  model_id = f"{prov}/{model}"
538
503
  models_data.append({
539
504
  "id": model_id,
@@ -542,7 +507,7 @@ async def list_models(
542
507
  "created": int(time.time()),
543
508
  "permission": [{"allow_create_engine": False, "allow_sampling": True}]
544
509
  })
545
-
510
+
546
511
  filter_msg = f" (type={type.value})" if type else ""
547
512
  logger.info(f"Listed {len(models_data)} models from all providers{filter_msg}")
548
513
 
@@ -562,13 +527,15 @@ async def list_models(
562
527
  async def list_providers():
563
528
  """
564
529
  List all available AbstractCore providers and their capabilities.
565
-
566
- Returns information about all registered LLM providers, including:
567
- - Provider name and type
568
- - Number of available models
569
- - Current availability status
570
- - Provider description
571
-
530
+
531
+ Returns comprehensive information about all registered LLM providers, including:
532
+ - Provider name, display name, and type
533
+ - Number of available models and sample models
534
+ - Current availability status and detailed error information
535
+ - Provider description and supported features
536
+ - Authentication requirements and installation instructions
537
+ - Local vs. cloud provider designation
538
+
572
539
  **Supported Providers:**
573
540
  - **OpenAI**: Commercial API with GPT-4, GPT-3.5, and embedding models
574
541
  - **Anthropic**: Commercial API with Claude 3 family models
@@ -577,42 +544,47 @@ async def list_providers():
577
544
  - **MLX**: Apple Silicon optimized local inference
578
545
  - **HuggingFace**: Access to HuggingFace models (transformers and embeddings)
579
546
  - **Mock**: Testing provider for development
580
-
547
+
581
548
  **Use Cases:**
582
549
  - Discover available providers before making requests
583
550
  - Check provider availability and model counts
584
551
  - Build dynamic provider selection UIs
585
- - Monitor provider status
586
-
587
- **Note:** Only providers with available models are included in the response.
588
-
589
- **Returns:** A list of provider objects with name, type, model count, status, and description.
552
+ - Monitor provider status and troubleshoot issues
553
+ - Get installation instructions for missing dependencies
554
+
555
+ **Enhanced Information:**
556
+ This endpoint now uses the centralized provider registry to provide
557
+ comprehensive information including supported features, authentication
558
+ requirements, and detailed status information.
559
+
560
+ **Returns:** A list of provider objects with comprehensive metadata.
590
561
  """
591
562
  try:
592
- providers_info = []
593
- providers = ["openai", "anthropic", "ollama", "lmstudio", "mlx", "huggingface", "mock"]
594
-
595
- for provider_name in providers:
596
- models = get_models_from_provider(provider_name)
597
- if models: # Only include providers that have models
598
- providers_info.append({
599
- "name": provider_name,
600
- "type": "llm", # Could be extended to include "embedding" type
601
- "model_count": len(models),
602
- "status": "available",
603
- "description": f"{provider_name.title()} provider with {len(models)} available models"
604
- })
563
+ from ..providers.registry import get_all_providers_with_models, get_all_providers_status
564
+
565
+ # Get providers with models (available providers)
566
+ available_providers = get_all_providers_with_models()
567
+
568
+ # Optionally include all providers (even those with issues) for debugging
569
+ # Uncomment the next line if you want to see providers with errors too:
570
+ # all_providers = get_all_providers_status()
605
571
 
606
- logger.info(f"Listed {len(providers_info)} available providers")
572
+ logger.info(f"Listed {len(available_providers)} available providers with models")
607
573
 
608
574
  return {
609
- "providers": sorted(providers_info, key=lambda x: x["name"])
575
+ "providers": available_providers,
576
+ "total_providers": len(available_providers),
577
+ "registry_version": "2.0", # Indicate this is using the new registry system
578
+ "note": "Provider information from centralized AbstractCore registry"
610
579
  }
611
580
 
612
581
  except Exception as e:
613
582
  logger.error(f"Failed to list providers: {e}")
614
583
  return {
615
- "providers": []
584
+ "providers": [],
585
+ "total_providers": 0,
586
+ "error": str(e),
587
+ "registry_version": "2.0"
616
588
  }
617
589
 
618
590
  @app.post("/v1/responses")
@@ -11,4 +11,4 @@ including when the package is installed from PyPI where pyproject.toml is not av
11
11
 
12
12
  # Package version - update this when releasing new versions
13
13
  # This must be manually synchronized with the version in pyproject.toml
14
- __version__ = "2.4.0"
14
+ __version__ = "2.4.2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractcore
3
- Version: 2.4.0
3
+ Version: 2.4.2
4
4
  Summary: Unified interface to all LLM providers with essential infrastructure for tool calling, streaming, and model management
5
5
  Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
6
6
  Maintainer-email: Laurent-Philippe Albou <contact@abstractcore.ai>
@@ -12,7 +12,7 @@ abstractcore/assets/model_capabilities.json,sha256=mSfP5C68I1c4GNLgL0sCDjpSZA3ks
12
12
  abstractcore/assets/session_schema.json,sha256=b6HTAWxRVlVhAzA7FqaKpunK1yO6jilBOsD5sQkqJTo,10580
13
13
  abstractcore/core/__init__.py,sha256=2h-86U4QkCQ4gzZ4iRusSTMlkODiUS6tKjZHiEXz6rM,684
14
14
  abstractcore/core/enums.py,sha256=BhkVnHC-X1_377JDmqd-2mnem9GdBLqixWlYzlP_FJU,695
15
- abstractcore/core/factory.py,sha256=WqEfGae0ikyjpTsjlErgkmm58fnkXgaCKnp8pkWHIno,5242
15
+ abstractcore/core/factory.py,sha256=UdrNwQAvifvFS3LMjF5KO87m-2n1bJBryTs9pvesYcI,2804
16
16
  abstractcore/core/interface.py,sha256=DIA46gR7JN33kLyKbtbAkoME7l7rixtKbFGAQgt7ilk,13693
17
17
  abstractcore/core/retry.py,sha256=wNlUAxfmvdO_uVWb4iqkhTqd7O1oRwXxqvVQaLXQOw0,14538
18
18
  abstractcore/core/session.py,sha256=Zjy_oo_Ha_Kufrf4ffE9Sxl2gVXcdpBaYl-IssqoBe4,35200
@@ -21,11 +21,13 @@ abstractcore/embeddings/__init__.py,sha256=hR3xZyqcRm4c2pq1dIa5lxj_-Bk70Zad802JQ
21
21
  abstractcore/embeddings/manager.py,sha256=QzDtSna4FDCPg1il7GGe_7p1VknuUHjXAFQa98PgU9A,50048
22
22
  abstractcore/embeddings/models.py,sha256=bsPAzL6gv57AVii8O15PT0kxfwRkOml3f3njJN4UDi4,4874
23
23
  abstractcore/events/__init__.py,sha256=UtbdTOeL05kvi7YP91yo4OEqs5UAbKylBvOOEkrUL5E,11065
24
+ abstractcore/exceptions/__init__.py,sha256=h6y3sdZR6uFMh0iq7z85DfJi01zGQvjVOm1W7l86iVQ,3224
25
+ abstractcore/media/__init__.py,sha256=BjWR8OIN2uxoOJBAlbM83VkyUtyDtiXM4AerYzhI9fU,4241
24
26
  abstractcore/processing/__init__.py,sha256=t6hiakQjcZROT4pw9ZFt2q6fF3vf5VpdMKG2EWlsFd8,540
25
27
  abstractcore/processing/basic_extractor.py,sha256=3x-3BdIHgLvqLnLF6K1-P4qVaLIpAnNIIutaJi7lDQM,49832
26
28
  abstractcore/processing/basic_judge.py,sha256=tKWJrg_tY4vCHzWgXxz0ZjgLXBYYfpMcpG7vl03hJcM,32218
27
29
  abstractcore/processing/basic_summarizer.py,sha256=XHNxMQ_8aLStTeUo6_2JaThlct12Htpz7ORmm0iuJsg,25495
28
- abstractcore/providers/__init__.py,sha256=UTpR2Bf_ICFG7M--1kxUmNXs4gl026Tp-KI9zJlvMKU,574
30
+ abstractcore/providers/__init__.py,sha256=t8Kp4flH5GvZEC6dx-iYJSPeSxMODa2spXb8MqtlPy4,1282
29
31
  abstractcore/providers/anthropic_provider.py,sha256=BM8Vu89c974yicvFwlsJ5C3N0wR9Kkt1pOszViWCwAQ,19694
30
32
  abstractcore/providers/base.py,sha256=AJa9KFJGLJvrNrhI-EHaVSzaF1ocOOYc98GnnCjLuag,42824
31
33
  abstractcore/providers/huggingface_provider.py,sha256=aucyfGFHi67NRU7_5vWp2TEXdBBjb710zz0lQjC_rWo,42283
@@ -34,9 +36,10 @@ abstractcore/providers/mlx_provider.py,sha256=eANGeexmJIS4KWn77fRBOJRkXvwgh7irAt
34
36
  abstractcore/providers/mock_provider.py,sha256=tIjA57Hlwu3vNODOZShNn0tY9HWvz1p4z-HyD_bsvbo,5741
35
37
  abstractcore/providers/ollama_provider.py,sha256=SkXD5gjuzeu9Thqnt4pRPSi-cjWxwuZGV2x5YMm26jo,19340
36
38
  abstractcore/providers/openai_provider.py,sha256=xGFrSkbCrsBnWnho1U2aMCBdzfCqf121wU1EFGmU3YQ,21678
39
+ abstractcore/providers/registry.py,sha256=c0hxp9RRa-uipGotaAu48fHXc_HGlLcOxC1k763mzhU,16596
37
40
  abstractcore/providers/streaming.py,sha256=VnffBV_CU9SAKzghL154OoFyEdDsiLwUNXPahyU41Bw,31342
38
41
  abstractcore/server/__init__.py,sha256=1DSAz_YhQtnKv7sNi5TMQV8GFujctDOabgvAdilQE0o,249
39
- abstractcore/server/app.py,sha256=lz6ys1rd2AmsQarwkcOxpLsYQ5xKsp0iskTiYKIC92s,44495
42
+ abstractcore/server/app.py,sha256=DCzKEe2hzvIfdq3N410boU0kRocFipRoiFDnABEGkYs,43350
40
43
  abstractcore/structured/__init__.py,sha256=VXRQHGcm-iaYnLOBPin2kyhvhhQA0kaGt_pcNDGsE_8,339
41
44
  abstractcore/structured/handler.py,sha256=Vb15smiR81JGDXX2RLkY2Exuj67J7a6C-xwVrZoXp0I,17134
42
45
  abstractcore/structured/retry.py,sha256=BN_PvrWybyU1clMy2cult1-TVxFSMaVqiCPmmXvA5aI,3805
@@ -53,10 +56,10 @@ abstractcore/utils/cli.py,sha256=8ua5Lu4bKSs9y1JB5W3kJ0OA-_dFpUHk6prH1U684xc,568
53
56
  abstractcore/utils/self_fixes.py,sha256=QEDwNTW80iQM4ftfEY3Ghz69F018oKwLM9yeRCYZOvw,5886
54
57
  abstractcore/utils/structured_logging.py,sha256=Y7TVkf1tP9BCOPNbBY1rQubBxcAxhhUOYMbrV2k50ZM,15830
55
58
  abstractcore/utils/token_utils.py,sha256=eLwFmJ68p9WMFD_MHLMmeJRW6Oqx_4hKELB8FNQ2Mnk,21097
56
- abstractcore/utils/version.py,sha256=IM2ECuDR2OHXGSdAqLdQib35efGRt7ZUzbD9SUnyky4,605
57
- abstractcore-2.4.0.dist-info/licenses/LICENSE,sha256=PI2v_4HMvd6050uDD_4AY_8PzBnu2asa3RKbdDjowTA,1078
58
- abstractcore-2.4.0.dist-info/METADATA,sha256=vqJTIn2uX7-YACF6DbTWCgZhDVCjMRR3VDuKf-HxdV4,19684
59
- abstractcore-2.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
60
- abstractcore-2.4.0.dist-info/entry_points.txt,sha256=Ocy403YwzaOBT7D_vf7w6YFiIQ4nTbp0htjXfeI5IOo,315
61
- abstractcore-2.4.0.dist-info/top_level.txt,sha256=DiNHBI35SIawW3N9Z-z0y6cQYNbXd32pvBkW0RLfScs,13
62
- abstractcore-2.4.0.dist-info/RECORD,,
59
+ abstractcore/utils/version.py,sha256=5z25ZawOKXC7_TsMUhliL7I_XUADAb5ums58hgtaXEc,605
60
+ abstractcore-2.4.2.dist-info/licenses/LICENSE,sha256=PI2v_4HMvd6050uDD_4AY_8PzBnu2asa3RKbdDjowTA,1078
61
+ abstractcore-2.4.2.dist-info/METADATA,sha256=AMCwg1STsdh8FmhmHw05GNK6ANM8nN1VDvusq7lFEFM,19684
62
+ abstractcore-2.4.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
63
+ abstractcore-2.4.2.dist-info/entry_points.txt,sha256=Ocy403YwzaOBT7D_vf7w6YFiIQ4nTbp0htjXfeI5IOo,315
64
+ abstractcore-2.4.2.dist-info/top_level.txt,sha256=DiNHBI35SIawW3N9Z-z0y6cQYNbXd32pvBkW0RLfScs,13
65
+ abstractcore-2.4.2.dist-info/RECORD,,