foundry-mcp 0.8.22__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.
Potentially problematic release.
This version of foundry-mcp might be problematic. Click here for more details.
- foundry_mcp/__init__.py +13 -0
- foundry_mcp/cli/__init__.py +67 -0
- foundry_mcp/cli/__main__.py +9 -0
- foundry_mcp/cli/agent.py +96 -0
- foundry_mcp/cli/commands/__init__.py +37 -0
- foundry_mcp/cli/commands/cache.py +137 -0
- foundry_mcp/cli/commands/dashboard.py +148 -0
- foundry_mcp/cli/commands/dev.py +446 -0
- foundry_mcp/cli/commands/journal.py +377 -0
- foundry_mcp/cli/commands/lifecycle.py +274 -0
- foundry_mcp/cli/commands/modify.py +824 -0
- foundry_mcp/cli/commands/plan.py +640 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +667 -0
- foundry_mcp/cli/commands/session.py +472 -0
- foundry_mcp/cli/commands/specs.py +686 -0
- foundry_mcp/cli/commands/tasks.py +807 -0
- foundry_mcp/cli/commands/testing.py +676 -0
- foundry_mcp/cli/commands/validate.py +982 -0
- foundry_mcp/cli/config.py +98 -0
- foundry_mcp/cli/context.py +298 -0
- foundry_mcp/cli/logging.py +212 -0
- foundry_mcp/cli/main.py +44 -0
- foundry_mcp/cli/output.py +122 -0
- foundry_mcp/cli/registry.py +110 -0
- foundry_mcp/cli/resilience.py +178 -0
- foundry_mcp/cli/transcript.py +217 -0
- foundry_mcp/config.py +1454 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1773 -0
- foundry_mcp/core/batch_operations.py +1202 -0
- foundry_mcp/core/cache.py +195 -0
- foundry_mcp/core/capabilities.py +446 -0
- foundry_mcp/core/concurrency.py +898 -0
- foundry_mcp/core/context.py +540 -0
- foundry_mcp/core/discovery.py +1603 -0
- foundry_mcp/core/error_collection.py +728 -0
- foundry_mcp/core/error_store.py +592 -0
- foundry_mcp/core/health.py +749 -0
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/journal.py +700 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1376 -0
- foundry_mcp/core/llm_patterns.py +510 -0
- foundry_mcp/core/llm_provider.py +1569 -0
- foundry_mcp/core/logging_config.py +374 -0
- foundry_mcp/core/metrics_persistence.py +584 -0
- foundry_mcp/core/metrics_registry.py +327 -0
- foundry_mcp/core/metrics_store.py +641 -0
- foundry_mcp/core/modifications.py +224 -0
- foundry_mcp/core/naming.py +146 -0
- foundry_mcp/core/observability.py +1216 -0
- foundry_mcp/core/otel.py +452 -0
- foundry_mcp/core/otel_stubs.py +264 -0
- foundry_mcp/core/pagination.py +255 -0
- foundry_mcp/core/progress.py +387 -0
- foundry_mcp/core/prometheus.py +564 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +691 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
- foundry_mcp/core/prompts/plan_review.py +627 -0
- foundry_mcp/core/providers/__init__.py +237 -0
- foundry_mcp/core/providers/base.py +515 -0
- foundry_mcp/core/providers/claude.py +472 -0
- foundry_mcp/core/providers/codex.py +637 -0
- foundry_mcp/core/providers/cursor_agent.py +630 -0
- foundry_mcp/core/providers/detectors.py +515 -0
- foundry_mcp/core/providers/gemini.py +426 -0
- foundry_mcp/core/providers/opencode.py +718 -0
- foundry_mcp/core/providers/opencode_wrapper.js +308 -0
- foundry_mcp/core/providers/package-lock.json +24 -0
- foundry_mcp/core/providers/package.json +25 -0
- foundry_mcp/core/providers/registry.py +607 -0
- foundry_mcp/core/providers/test_provider.py +171 -0
- foundry_mcp/core/providers/validation.py +857 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +528 -0
- foundry_mcp/core/research/models.py +1234 -0
- foundry_mcp/core/research/providers/__init__.py +40 -0
- foundry_mcp/core/research/providers/base.py +242 -0
- foundry_mcp/core/research/providers/google.py +507 -0
- foundry_mcp/core/research/providers/perplexity.py +442 -0
- foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
- foundry_mcp/core/research/providers/tavily.py +383 -0
- foundry_mcp/core/research/workflows/__init__.py +25 -0
- foundry_mcp/core/research/workflows/base.py +298 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +539 -0
- foundry_mcp/core/research/workflows/deep_research.py +4142 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +1624 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +4119 -0
- foundry_mcp/core/task.py +2463 -0
- foundry_mcp/core/testing.py +839 -0
- foundry_mcp/core/validation.py +2357 -0
- foundry_mcp/dashboard/__init__.py +32 -0
- foundry_mcp/dashboard/app.py +119 -0
- foundry_mcp/dashboard/components/__init__.py +17 -0
- foundry_mcp/dashboard/components/cards.py +88 -0
- foundry_mcp/dashboard/components/charts.py +177 -0
- foundry_mcp/dashboard/components/filters.py +136 -0
- foundry_mcp/dashboard/components/tables.py +195 -0
- foundry_mcp/dashboard/data/__init__.py +11 -0
- foundry_mcp/dashboard/data/stores.py +433 -0
- foundry_mcp/dashboard/launcher.py +300 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +164 -0
- foundry_mcp/dashboard/views/overview.py +96 -0
- foundry_mcp/dashboard/views/providers.py +83 -0
- foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
- foundry_mcp/dashboard/views/tool_usage.py +139 -0
- foundry_mcp/prompts/__init__.py +9 -0
- foundry_mcp/prompts/workflows.py +525 -0
- foundry_mcp/resources/__init__.py +9 -0
- foundry_mcp/resources/specs.py +591 -0
- foundry_mcp/schemas/__init__.py +38 -0
- foundry_mcp/schemas/intake-schema.json +89 -0
- foundry_mcp/schemas/sdd-spec-schema.json +414 -0
- foundry_mcp/server.py +150 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +92 -0
- foundry_mcp/tools/unified/authoring.py +3620 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +268 -0
- foundry_mcp/tools/unified/environment.py +1341 -0
- foundry_mcp/tools/unified/error.py +479 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +640 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +876 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +589 -0
- foundry_mcp/tools/unified/research.py +1283 -0
- foundry_mcp/tools/unified/review.py +1042 -0
- foundry_mcp/tools/unified/review_helpers.py +314 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +565 -0
- foundry_mcp/tools/unified/spec.py +1283 -0
- foundry_mcp/tools/unified/task.py +3846 -0
- foundry_mcp/tools/unified/test.py +431 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.8.22.dist-info/METADATA +344 -0
- foundry_mcp-0.8.22.dist-info/RECORD +153 -0
- foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
- foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
- foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider registry utilities.
|
|
3
|
+
|
|
4
|
+
Encapsulates registration, lazy loading, availability checks, and dependency
|
|
5
|
+
injection hooks for ProviderContext implementations. This module backs the
|
|
6
|
+
CLI provider runner plus future skill integrations, providing a single source
|
|
7
|
+
of truth for discovering available providers.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from foundry_mcp.core.providers.registry import (
|
|
11
|
+
... register_provider,
|
|
12
|
+
... resolve_provider,
|
|
13
|
+
... available_providers,
|
|
14
|
+
... )
|
|
15
|
+
>>> from foundry_mcp.core.providers import ProviderHooks
|
|
16
|
+
>>>
|
|
17
|
+
>>> # Register a provider factory
|
|
18
|
+
>>> register_provider(
|
|
19
|
+
... "my-provider",
|
|
20
|
+
... factory=my_provider_factory,
|
|
21
|
+
... description="My custom provider",
|
|
22
|
+
... )
|
|
23
|
+
>>>
|
|
24
|
+
>>> # List available providers
|
|
25
|
+
>>> available_providers()
|
|
26
|
+
['my-provider']
|
|
27
|
+
>>>
|
|
28
|
+
>>> # Resolve and instantiate a provider
|
|
29
|
+
>>> hooks = ProviderHooks()
|
|
30
|
+
>>> provider = resolve_provider("my-provider", hooks=hooks)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import importlib
|
|
36
|
+
import logging
|
|
37
|
+
from dataclasses import dataclass, field
|
|
38
|
+
from typing import Any, Callable, Dict, List, Optional, Protocol, Sequence
|
|
39
|
+
|
|
40
|
+
from foundry_mcp.core.providers.base import (
|
|
41
|
+
ProviderContext,
|
|
42
|
+
ProviderHooks,
|
|
43
|
+
ProviderMetadata,
|
|
44
|
+
ProviderUnavailableError,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# =============================================================================
|
|
51
|
+
# Type Definitions
|
|
52
|
+
# =============================================================================
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ProviderFactory(Protocol):
|
|
56
|
+
"""
|
|
57
|
+
Callable that instantiates a ProviderContext.
|
|
58
|
+
|
|
59
|
+
Implementations should accept keyword-only arguments for hooks, model,
|
|
60
|
+
dependencies, and overrides so the registry can pass future options
|
|
61
|
+
without breaking signatures.
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
def create_my_provider(
|
|
65
|
+
*,
|
|
66
|
+
hooks: ProviderHooks,
|
|
67
|
+
model: Optional[str] = None,
|
|
68
|
+
dependencies: Optional[Dict[str, object]] = None,
|
|
69
|
+
overrides: Optional[Dict[str, object]] = None,
|
|
70
|
+
) -> ProviderContext:
|
|
71
|
+
return MyProvider(hooks=hooks, model=model)
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __call__(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
hooks: ProviderHooks,
|
|
78
|
+
model: Optional[str] = None,
|
|
79
|
+
dependencies: Optional[Dict[str, object]] = None,
|
|
80
|
+
overrides: Optional[Dict[str, object]] = None,
|
|
81
|
+
) -> ProviderContext:
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Type aliases for registry callables
|
|
86
|
+
AvailabilityCheck = Callable[[], bool]
|
|
87
|
+
MetadataResolver = Callable[[], ProviderMetadata]
|
|
88
|
+
LazyFactoryLoader = Callable[[], ProviderFactory]
|
|
89
|
+
DependencyResolver = Callable[[str], Dict[str, object]]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# =============================================================================
|
|
93
|
+
# Provider Registration
|
|
94
|
+
# =============================================================================
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class ProviderRegistration:
|
|
99
|
+
"""
|
|
100
|
+
Internal record for a registered provider.
|
|
101
|
+
|
|
102
|
+
Supports both eager and lazy factory loading, with optional metadata
|
|
103
|
+
resolvers and availability checks.
|
|
104
|
+
|
|
105
|
+
Attributes:
|
|
106
|
+
provider_id: Canonical provider identifier (e.g., "gemini", "codex")
|
|
107
|
+
factory: Callable that instantiates ProviderContext (eager)
|
|
108
|
+
lazy_loader: Callable that returns a factory when invoked (lazy import)
|
|
109
|
+
metadata: Cached ProviderMetadata object
|
|
110
|
+
metadata_resolver: Callable that returns ProviderMetadata on demand
|
|
111
|
+
availability_check: Callable returning bool to gate resolution
|
|
112
|
+
priority: Sorting priority for available_providers (higher first)
|
|
113
|
+
description: Human-readable description for diagnostics
|
|
114
|
+
tags: Optional labels describing the provider (e.g., ["cli", "external"])
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
provider_id: str
|
|
118
|
+
factory: Optional[ProviderFactory] = None
|
|
119
|
+
lazy_loader: Optional[LazyFactoryLoader] = None
|
|
120
|
+
metadata: Optional[ProviderMetadata] = None
|
|
121
|
+
metadata_resolver: Optional[MetadataResolver] = None
|
|
122
|
+
availability_check: Optional[AvailabilityCheck] = None
|
|
123
|
+
priority: int = 0
|
|
124
|
+
description: Optional[str] = None
|
|
125
|
+
tags: Sequence[str] = field(default_factory=tuple)
|
|
126
|
+
|
|
127
|
+
def load_factory(self) -> ProviderFactory:
|
|
128
|
+
"""
|
|
129
|
+
Return the provider factory, performing lazy import if needed.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The provider factory callable
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ProviderUnavailableError: If no factory is available
|
|
136
|
+
"""
|
|
137
|
+
if self.factory is not None:
|
|
138
|
+
return self.factory
|
|
139
|
+
|
|
140
|
+
if self.lazy_loader is None:
|
|
141
|
+
raise ProviderUnavailableError(
|
|
142
|
+
f"Provider '{self.provider_id}' is missing a factory.",
|
|
143
|
+
provider=self.provider_id,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
factory = self.lazy_loader()
|
|
147
|
+
if factory is None:
|
|
148
|
+
raise ProviderUnavailableError(
|
|
149
|
+
f"Lazy loader for '{self.provider_id}' returned None.",
|
|
150
|
+
provider=self.provider_id,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Cache the factory for future calls
|
|
154
|
+
self.factory = factory
|
|
155
|
+
self.lazy_loader = None
|
|
156
|
+
return factory
|
|
157
|
+
|
|
158
|
+
def is_available(self) -> bool:
|
|
159
|
+
"""
|
|
160
|
+
Return True if the provider passes its availability check.
|
|
161
|
+
|
|
162
|
+
If no availability_check is registered, returns True.
|
|
163
|
+
"""
|
|
164
|
+
if self.availability_check is None:
|
|
165
|
+
return True
|
|
166
|
+
try:
|
|
167
|
+
return bool(self.availability_check())
|
|
168
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
169
|
+
logger.warning(
|
|
170
|
+
"Availability check for provider '%s' failed: %s",
|
|
171
|
+
self.provider_id,
|
|
172
|
+
exc,
|
|
173
|
+
)
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
def resolve_metadata(self) -> Optional[ProviderMetadata]:
|
|
177
|
+
"""
|
|
178
|
+
Return cached metadata, resolving lazily when necessary.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
ProviderMetadata if available, None otherwise
|
|
182
|
+
"""
|
|
183
|
+
if self.metadata is not None:
|
|
184
|
+
return self.metadata
|
|
185
|
+
|
|
186
|
+
if self.metadata_resolver is None:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
self.metadata = self.metadata_resolver()
|
|
191
|
+
return self.metadata
|
|
192
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
193
|
+
logger.warning(
|
|
194
|
+
"Metadata resolver for provider '%s' failed: %s",
|
|
195
|
+
self.provider_id,
|
|
196
|
+
exc,
|
|
197
|
+
)
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Global registry state
|
|
202
|
+
_REGISTRY: Dict[str, ProviderRegistration] = {}
|
|
203
|
+
_dependency_resolver: Optional[DependencyResolver] = None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# =============================================================================
|
|
207
|
+
# Public API - Registration
|
|
208
|
+
# =============================================================================
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def register_provider(
|
|
212
|
+
provider_id: str,
|
|
213
|
+
*,
|
|
214
|
+
factory: Optional[ProviderFactory] = None,
|
|
215
|
+
lazy_loader: Optional[LazyFactoryLoader] = None,
|
|
216
|
+
metadata: Optional[ProviderMetadata] = None,
|
|
217
|
+
metadata_resolver: Optional[MetadataResolver] = None,
|
|
218
|
+
availability_check: Optional[AvailabilityCheck] = None,
|
|
219
|
+
priority: int = 0,
|
|
220
|
+
description: Optional[str] = None,
|
|
221
|
+
tags: Optional[Sequence[str]] = None,
|
|
222
|
+
replace: bool = False,
|
|
223
|
+
) -> None:
|
|
224
|
+
"""
|
|
225
|
+
Register a provider factory with the global registry.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
provider_id: Canonical provider identifier (e.g., "gemini")
|
|
229
|
+
factory: Callable that instantiates ProviderContext instances
|
|
230
|
+
lazy_loader: Callable that returns a factory when invoked (lazy import)
|
|
231
|
+
metadata: Optional ProviderMetadata object (cached)
|
|
232
|
+
metadata_resolver: Callable that returns ProviderMetadata on demand
|
|
233
|
+
availability_check: Callable returning bool to gate resolution
|
|
234
|
+
priority: Sorting priority for available_providers (higher first)
|
|
235
|
+
description: Human-readable description for diagnostics
|
|
236
|
+
tags: Optional labels describing the provider
|
|
237
|
+
replace: Overwrite existing registration if True
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
ValueError: If provider_id already registered and replace=False
|
|
241
|
+
ValueError: If neither factory nor lazy_loader provided
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
>>> register_provider(
|
|
245
|
+
... "my-provider",
|
|
246
|
+
... factory=my_factory,
|
|
247
|
+
... availability_check=lambda: True,
|
|
248
|
+
... priority=10,
|
|
249
|
+
... description="My custom provider",
|
|
250
|
+
... tags=["external", "experimental"],
|
|
251
|
+
... )
|
|
252
|
+
"""
|
|
253
|
+
if provider_id in _REGISTRY and not replace:
|
|
254
|
+
raise ValueError(f"Provider '{provider_id}' is already registered")
|
|
255
|
+
|
|
256
|
+
if factory is None and lazy_loader is None:
|
|
257
|
+
raise ValueError("Either 'factory' or 'lazy_loader' must be provided")
|
|
258
|
+
|
|
259
|
+
registration = ProviderRegistration(
|
|
260
|
+
provider_id=provider_id,
|
|
261
|
+
factory=factory,
|
|
262
|
+
lazy_loader=lazy_loader,
|
|
263
|
+
metadata=metadata,
|
|
264
|
+
metadata_resolver=metadata_resolver,
|
|
265
|
+
availability_check=availability_check,
|
|
266
|
+
priority=priority,
|
|
267
|
+
description=description,
|
|
268
|
+
tags=tuple(tags or ()),
|
|
269
|
+
)
|
|
270
|
+
_REGISTRY[provider_id] = registration
|
|
271
|
+
logger.debug("Provider '%s' registered (priority=%s)", provider_id, priority)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def register_lazy_provider(
|
|
275
|
+
provider_id: str,
|
|
276
|
+
module_path: str,
|
|
277
|
+
*,
|
|
278
|
+
factory_attr: str = "create_provider",
|
|
279
|
+
metadata_attr: Optional[str] = None,
|
|
280
|
+
availability_attr: Optional[str] = None,
|
|
281
|
+
priority: int = 0,
|
|
282
|
+
description: Optional[str] = None,
|
|
283
|
+
tags: Optional[Sequence[str]] = None,
|
|
284
|
+
replace: bool = False,
|
|
285
|
+
) -> None:
|
|
286
|
+
"""
|
|
287
|
+
Register a provider by module path without importing upfront.
|
|
288
|
+
|
|
289
|
+
This is useful for providers with heavy dependencies that should only
|
|
290
|
+
be loaded when actually needed.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
provider_id: Canonical provider identifier
|
|
294
|
+
module_path: Full module path (e.g., "mypackage.providers.gemini")
|
|
295
|
+
factory_attr: Attribute name for factory function (default: "create_provider")
|
|
296
|
+
metadata_attr: Attribute name for metadata (callable or static)
|
|
297
|
+
availability_attr: Attribute name for availability check (callable)
|
|
298
|
+
priority: Sorting priority for available_providers (higher first)
|
|
299
|
+
description: Human-readable description
|
|
300
|
+
tags: Optional labels
|
|
301
|
+
replace: Overwrite existing registration if True
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
>>> register_lazy_provider(
|
|
305
|
+
... "gemini",
|
|
306
|
+
... "foundry_mcp.providers.gemini",
|
|
307
|
+
... factory_attr="create_gemini_provider",
|
|
308
|
+
... availability_attr="is_gemini_available",
|
|
309
|
+
... )
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
def _lazy_loader() -> ProviderFactory:
|
|
313
|
+
module = importlib.import_module(module_path)
|
|
314
|
+
factory_obj = getattr(module, factory_attr, None)
|
|
315
|
+
if factory_obj is None:
|
|
316
|
+
raise ProviderUnavailableError(
|
|
317
|
+
f"Module '{module_path}' is missing '{factory_attr}'.",
|
|
318
|
+
provider=provider_id,
|
|
319
|
+
)
|
|
320
|
+
return factory_obj
|
|
321
|
+
|
|
322
|
+
metadata_resolver: Optional[MetadataResolver] = None
|
|
323
|
+
if metadata_attr:
|
|
324
|
+
metadata_resolver = _build_attr_resolver(
|
|
325
|
+
module_path, metadata_attr, provider_id
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
availability_check: Optional[AvailabilityCheck] = None
|
|
329
|
+
if availability_attr:
|
|
330
|
+
availability_check = _build_attr_resolver(
|
|
331
|
+
module_path, availability_attr, provider_id
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
register_provider(
|
|
335
|
+
provider_id,
|
|
336
|
+
lazy_loader=_lazy_loader,
|
|
337
|
+
metadata_resolver=metadata_resolver,
|
|
338
|
+
availability_check=availability_check,
|
|
339
|
+
priority=priority,
|
|
340
|
+
description=description,
|
|
341
|
+
tags=tags,
|
|
342
|
+
replace=replace,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _build_attr_resolver(
|
|
347
|
+
module_path: str, attr: str, provider_id: str
|
|
348
|
+
) -> Callable[[], Any]:
|
|
349
|
+
"""Build a lazy attribute resolver for a module."""
|
|
350
|
+
|
|
351
|
+
def _resolver() -> Any:
|
|
352
|
+
module = importlib.import_module(module_path)
|
|
353
|
+
target = getattr(module, attr, None)
|
|
354
|
+
if callable(target):
|
|
355
|
+
return target()
|
|
356
|
+
return target
|
|
357
|
+
|
|
358
|
+
return _resolver
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# =============================================================================
|
|
362
|
+
# Public API - Resolution
|
|
363
|
+
# =============================================================================
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def available_providers(*, include_unavailable: bool = False) -> List[str]:
|
|
367
|
+
"""
|
|
368
|
+
Return provider identifiers sorted by priority (desc) then name.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
include_unavailable: If True, include providers that fail availability check
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
List of provider IDs sorted by priority (descending), then alphabetically
|
|
375
|
+
|
|
376
|
+
Example:
|
|
377
|
+
>>> available_providers()
|
|
378
|
+
['gemini', 'codex', 'cursor-agent']
|
|
379
|
+
>>> available_providers(include_unavailable=True)
|
|
380
|
+
['gemini', 'codex', 'cursor-agent', 'opencode']
|
|
381
|
+
"""
|
|
382
|
+
providers: List[ProviderRegistration] = list(_REGISTRY.values())
|
|
383
|
+
providers.sort(key=lambda reg: (-reg.priority, reg.provider_id))
|
|
384
|
+
|
|
385
|
+
if include_unavailable:
|
|
386
|
+
return [reg.provider_id for reg in providers]
|
|
387
|
+
|
|
388
|
+
return [reg.provider_id for reg in providers if reg.is_available()]
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def check_provider_available(provider_id: str) -> bool:
|
|
392
|
+
"""
|
|
393
|
+
Check if a provider is available using its registered availability check.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
provider_id: Provider identifier (e.g., "gemini", "codex")
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
True if provider is registered and passes its availability check,
|
|
400
|
+
False otherwise
|
|
401
|
+
|
|
402
|
+
Example:
|
|
403
|
+
>>> check_provider_available("gemini")
|
|
404
|
+
True
|
|
405
|
+
>>> check_provider_available("nonexistent")
|
|
406
|
+
False
|
|
407
|
+
"""
|
|
408
|
+
registration = _REGISTRY.get(provider_id)
|
|
409
|
+
if registration is None:
|
|
410
|
+
return False
|
|
411
|
+
return registration.is_available()
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def resolve_provider(
|
|
415
|
+
provider_id: str,
|
|
416
|
+
*,
|
|
417
|
+
hooks: ProviderHooks,
|
|
418
|
+
model: Optional[str] = None,
|
|
419
|
+
overrides: Optional[Dict[str, object]] = None,
|
|
420
|
+
) -> ProviderContext:
|
|
421
|
+
"""
|
|
422
|
+
Instantiate a provider by ID using the registered factory.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
provider_id: Provider identifier (e.g., "gemini", "codex")
|
|
426
|
+
hooks: Lifecycle hooks to wire into the provider
|
|
427
|
+
model: Optional model override
|
|
428
|
+
overrides: Optional provider-specific configuration overrides
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Instantiated ProviderContext
|
|
432
|
+
|
|
433
|
+
Raises:
|
|
434
|
+
ProviderUnavailableError: If provider not registered or unavailable
|
|
435
|
+
|
|
436
|
+
Example:
|
|
437
|
+
>>> hooks = ProviderHooks()
|
|
438
|
+
>>> provider = resolve_provider("gemini", hooks=hooks, model="pro")
|
|
439
|
+
>>> result = provider.generate(request)
|
|
440
|
+
"""
|
|
441
|
+
registration = _REGISTRY.get(provider_id)
|
|
442
|
+
if registration is None:
|
|
443
|
+
raise ProviderUnavailableError(
|
|
444
|
+
f"Provider '{provider_id}' is not registered.", provider=provider_id
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
if not registration.is_available():
|
|
448
|
+
raise ProviderUnavailableError(
|
|
449
|
+
f"Provider '{provider_id}' is currently unavailable.",
|
|
450
|
+
provider=provider_id,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
factory = registration.load_factory()
|
|
454
|
+
dependencies = _resolve_dependencies(provider_id)
|
|
455
|
+
return factory(
|
|
456
|
+
hooks=hooks,
|
|
457
|
+
model=model,
|
|
458
|
+
dependencies=dependencies,
|
|
459
|
+
overrides=overrides,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def get_provider_metadata(provider_id: str) -> Optional[ProviderMetadata]:
|
|
464
|
+
"""
|
|
465
|
+
Return ProviderMetadata for a registered provider.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
provider_id: Provider identifier
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
ProviderMetadata if available, None otherwise
|
|
472
|
+
"""
|
|
473
|
+
registration = _REGISTRY.get(provider_id)
|
|
474
|
+
if registration is None:
|
|
475
|
+
return None
|
|
476
|
+
return registration.resolve_metadata()
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def describe_providers() -> List[Dict[str, object]]:
|
|
480
|
+
"""
|
|
481
|
+
Return descriptive information for all registered providers.
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
List of dicts with provider info (id, description, priority, tags, available)
|
|
485
|
+
|
|
486
|
+
Example:
|
|
487
|
+
>>> describe_providers()
|
|
488
|
+
[
|
|
489
|
+
{
|
|
490
|
+
"id": "gemini",
|
|
491
|
+
"description": "Google Gemini CLI",
|
|
492
|
+
"priority": 10,
|
|
493
|
+
"tags": ["external", "cli"],
|
|
494
|
+
"available": True,
|
|
495
|
+
},
|
|
496
|
+
...
|
|
497
|
+
]
|
|
498
|
+
"""
|
|
499
|
+
summary: List[Dict[str, object]] = []
|
|
500
|
+
for reg in _REGISTRY.values():
|
|
501
|
+
summary.append(
|
|
502
|
+
{
|
|
503
|
+
"id": reg.provider_id,
|
|
504
|
+
"description": reg.description,
|
|
505
|
+
"priority": reg.priority,
|
|
506
|
+
"tags": list(reg.tags),
|
|
507
|
+
"available": reg.is_available(),
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
return summary
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
# =============================================================================
|
|
514
|
+
# Public API - Dependency Injection
|
|
515
|
+
# =============================================================================
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def set_dependency_resolver(resolver: Optional[DependencyResolver]) -> None:
|
|
519
|
+
"""
|
|
520
|
+
Register a callable that supplies dependency dictionaries per provider ID.
|
|
521
|
+
|
|
522
|
+
The resolver is invoked during `resolve_provider` and should return a dict
|
|
523
|
+
that will be passed to the provider factory via the `dependencies` keyword.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
resolver: Callable taking provider_id, returning dependency dict.
|
|
527
|
+
Pass None to clear the resolver.
|
|
528
|
+
|
|
529
|
+
Example:
|
|
530
|
+
>>> def my_resolver(provider_id: str) -> Dict[str, object]:
|
|
531
|
+
... if provider_id == "gemini":
|
|
532
|
+
... return {"api_client": my_api_client}
|
|
533
|
+
... return {}
|
|
534
|
+
>>> set_dependency_resolver(my_resolver)
|
|
535
|
+
"""
|
|
536
|
+
global _dependency_resolver
|
|
537
|
+
_dependency_resolver = resolver
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _resolve_dependencies(provider_id: str) -> Dict[str, object]:
|
|
541
|
+
"""Resolve dependencies for a provider using the configured resolver."""
|
|
542
|
+
if _dependency_resolver is None:
|
|
543
|
+
return {}
|
|
544
|
+
try:
|
|
545
|
+
dependencies = _dependency_resolver(provider_id)
|
|
546
|
+
return dependencies or {}
|
|
547
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
548
|
+
logger.warning(
|
|
549
|
+
"Dependency resolver failed for provider '%s': %s",
|
|
550
|
+
provider_id,
|
|
551
|
+
exc,
|
|
552
|
+
)
|
|
553
|
+
return {}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
# =============================================================================
|
|
557
|
+
# Public API - Testing Support
|
|
558
|
+
# =============================================================================
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def reset_registry() -> None:
|
|
562
|
+
"""
|
|
563
|
+
Clear the registry and dependency resolver.
|
|
564
|
+
|
|
565
|
+
Primarily used by tests to restore a clean state.
|
|
566
|
+
"""
|
|
567
|
+
_REGISTRY.clear()
|
|
568
|
+
set_dependency_resolver(None)
|
|
569
|
+
logger.debug("Registry cleared")
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def get_registration(provider_id: str) -> Optional[ProviderRegistration]:
|
|
573
|
+
"""
|
|
574
|
+
Get the registration record for a provider (for testing/introspection).
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
provider_id: Provider identifier
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
ProviderRegistration if found, None otherwise
|
|
581
|
+
"""
|
|
582
|
+
return _REGISTRY.get(provider_id)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
__all__ = [
|
|
586
|
+
# Types
|
|
587
|
+
"ProviderFactory",
|
|
588
|
+
"ProviderRegistration",
|
|
589
|
+
"AvailabilityCheck",
|
|
590
|
+
"MetadataResolver",
|
|
591
|
+
"LazyFactoryLoader",
|
|
592
|
+
"DependencyResolver",
|
|
593
|
+
# Registration
|
|
594
|
+
"register_provider",
|
|
595
|
+
"register_lazy_provider",
|
|
596
|
+
# Resolution
|
|
597
|
+
"available_providers",
|
|
598
|
+
"check_provider_available",
|
|
599
|
+
"resolve_provider",
|
|
600
|
+
"get_provider_metadata",
|
|
601
|
+
"describe_providers",
|
|
602
|
+
# Dependency Injection
|
|
603
|
+
"set_dependency_resolver",
|
|
604
|
+
# Testing
|
|
605
|
+
"reset_registry",
|
|
606
|
+
"get_registration",
|
|
607
|
+
]
|