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.

Files changed (153) hide show
  1. foundry_mcp/__init__.py +13 -0
  2. foundry_mcp/cli/__init__.py +67 -0
  3. foundry_mcp/cli/__main__.py +9 -0
  4. foundry_mcp/cli/agent.py +96 -0
  5. foundry_mcp/cli/commands/__init__.py +37 -0
  6. foundry_mcp/cli/commands/cache.py +137 -0
  7. foundry_mcp/cli/commands/dashboard.py +148 -0
  8. foundry_mcp/cli/commands/dev.py +446 -0
  9. foundry_mcp/cli/commands/journal.py +377 -0
  10. foundry_mcp/cli/commands/lifecycle.py +274 -0
  11. foundry_mcp/cli/commands/modify.py +824 -0
  12. foundry_mcp/cli/commands/plan.py +640 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +667 -0
  15. foundry_mcp/cli/commands/session.py +472 -0
  16. foundry_mcp/cli/commands/specs.py +686 -0
  17. foundry_mcp/cli/commands/tasks.py +807 -0
  18. foundry_mcp/cli/commands/testing.py +676 -0
  19. foundry_mcp/cli/commands/validate.py +982 -0
  20. foundry_mcp/cli/config.py +98 -0
  21. foundry_mcp/cli/context.py +298 -0
  22. foundry_mcp/cli/logging.py +212 -0
  23. foundry_mcp/cli/main.py +44 -0
  24. foundry_mcp/cli/output.py +122 -0
  25. foundry_mcp/cli/registry.py +110 -0
  26. foundry_mcp/cli/resilience.py +178 -0
  27. foundry_mcp/cli/transcript.py +217 -0
  28. foundry_mcp/config.py +1454 -0
  29. foundry_mcp/core/__init__.py +144 -0
  30. foundry_mcp/core/ai_consultation.py +1773 -0
  31. foundry_mcp/core/batch_operations.py +1202 -0
  32. foundry_mcp/core/cache.py +195 -0
  33. foundry_mcp/core/capabilities.py +446 -0
  34. foundry_mcp/core/concurrency.py +898 -0
  35. foundry_mcp/core/context.py +540 -0
  36. foundry_mcp/core/discovery.py +1603 -0
  37. foundry_mcp/core/error_collection.py +728 -0
  38. foundry_mcp/core/error_store.py +592 -0
  39. foundry_mcp/core/health.py +749 -0
  40. foundry_mcp/core/intake.py +933 -0
  41. foundry_mcp/core/journal.py +700 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1376 -0
  44. foundry_mcp/core/llm_patterns.py +510 -0
  45. foundry_mcp/core/llm_provider.py +1569 -0
  46. foundry_mcp/core/logging_config.py +374 -0
  47. foundry_mcp/core/metrics_persistence.py +584 -0
  48. foundry_mcp/core/metrics_registry.py +327 -0
  49. foundry_mcp/core/metrics_store.py +641 -0
  50. foundry_mcp/core/modifications.py +224 -0
  51. foundry_mcp/core/naming.py +146 -0
  52. foundry_mcp/core/observability.py +1216 -0
  53. foundry_mcp/core/otel.py +452 -0
  54. foundry_mcp/core/otel_stubs.py +264 -0
  55. foundry_mcp/core/pagination.py +255 -0
  56. foundry_mcp/core/progress.py +387 -0
  57. foundry_mcp/core/prometheus.py +564 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +691 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
  61. foundry_mcp/core/prompts/plan_review.py +627 -0
  62. foundry_mcp/core/providers/__init__.py +237 -0
  63. foundry_mcp/core/providers/base.py +515 -0
  64. foundry_mcp/core/providers/claude.py +472 -0
  65. foundry_mcp/core/providers/codex.py +637 -0
  66. foundry_mcp/core/providers/cursor_agent.py +630 -0
  67. foundry_mcp/core/providers/detectors.py +515 -0
  68. foundry_mcp/core/providers/gemini.py +426 -0
  69. foundry_mcp/core/providers/opencode.py +718 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +308 -0
  71. foundry_mcp/core/providers/package-lock.json +24 -0
  72. foundry_mcp/core/providers/package.json +25 -0
  73. foundry_mcp/core/providers/registry.py +607 -0
  74. foundry_mcp/core/providers/test_provider.py +171 -0
  75. foundry_mcp/core/providers/validation.py +857 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/research/__init__.py +68 -0
  78. foundry_mcp/core/research/memory.py +528 -0
  79. foundry_mcp/core/research/models.py +1234 -0
  80. foundry_mcp/core/research/providers/__init__.py +40 -0
  81. foundry_mcp/core/research/providers/base.py +242 -0
  82. foundry_mcp/core/research/providers/google.py +507 -0
  83. foundry_mcp/core/research/providers/perplexity.py +442 -0
  84. foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
  85. foundry_mcp/core/research/providers/tavily.py +383 -0
  86. foundry_mcp/core/research/workflows/__init__.py +25 -0
  87. foundry_mcp/core/research/workflows/base.py +298 -0
  88. foundry_mcp/core/research/workflows/chat.py +271 -0
  89. foundry_mcp/core/research/workflows/consensus.py +539 -0
  90. foundry_mcp/core/research/workflows/deep_research.py +4142 -0
  91. foundry_mcp/core/research/workflows/ideate.py +682 -0
  92. foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
  93. foundry_mcp/core/resilience.py +600 -0
  94. foundry_mcp/core/responses.py +1624 -0
  95. foundry_mcp/core/review.py +366 -0
  96. foundry_mcp/core/security.py +438 -0
  97. foundry_mcp/core/spec.py +4119 -0
  98. foundry_mcp/core/task.py +2463 -0
  99. foundry_mcp/core/testing.py +839 -0
  100. foundry_mcp/core/validation.py +2357 -0
  101. foundry_mcp/dashboard/__init__.py +32 -0
  102. foundry_mcp/dashboard/app.py +119 -0
  103. foundry_mcp/dashboard/components/__init__.py +17 -0
  104. foundry_mcp/dashboard/components/cards.py +88 -0
  105. foundry_mcp/dashboard/components/charts.py +177 -0
  106. foundry_mcp/dashboard/components/filters.py +136 -0
  107. foundry_mcp/dashboard/components/tables.py +195 -0
  108. foundry_mcp/dashboard/data/__init__.py +11 -0
  109. foundry_mcp/dashboard/data/stores.py +433 -0
  110. foundry_mcp/dashboard/launcher.py +300 -0
  111. foundry_mcp/dashboard/views/__init__.py +12 -0
  112. foundry_mcp/dashboard/views/errors.py +217 -0
  113. foundry_mcp/dashboard/views/metrics.py +164 -0
  114. foundry_mcp/dashboard/views/overview.py +96 -0
  115. foundry_mcp/dashboard/views/providers.py +83 -0
  116. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  117. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  118. foundry_mcp/prompts/__init__.py +9 -0
  119. foundry_mcp/prompts/workflows.py +525 -0
  120. foundry_mcp/resources/__init__.py +9 -0
  121. foundry_mcp/resources/specs.py +591 -0
  122. foundry_mcp/schemas/__init__.py +38 -0
  123. foundry_mcp/schemas/intake-schema.json +89 -0
  124. foundry_mcp/schemas/sdd-spec-schema.json +414 -0
  125. foundry_mcp/server.py +150 -0
  126. foundry_mcp/tools/__init__.py +10 -0
  127. foundry_mcp/tools/unified/__init__.py +92 -0
  128. foundry_mcp/tools/unified/authoring.py +3620 -0
  129. foundry_mcp/tools/unified/context_helpers.py +98 -0
  130. foundry_mcp/tools/unified/documentation_helpers.py +268 -0
  131. foundry_mcp/tools/unified/environment.py +1341 -0
  132. foundry_mcp/tools/unified/error.py +479 -0
  133. foundry_mcp/tools/unified/health.py +225 -0
  134. foundry_mcp/tools/unified/journal.py +841 -0
  135. foundry_mcp/tools/unified/lifecycle.py +640 -0
  136. foundry_mcp/tools/unified/metrics.py +777 -0
  137. foundry_mcp/tools/unified/plan.py +876 -0
  138. foundry_mcp/tools/unified/pr.py +294 -0
  139. foundry_mcp/tools/unified/provider.py +589 -0
  140. foundry_mcp/tools/unified/research.py +1283 -0
  141. foundry_mcp/tools/unified/review.py +1042 -0
  142. foundry_mcp/tools/unified/review_helpers.py +314 -0
  143. foundry_mcp/tools/unified/router.py +102 -0
  144. foundry_mcp/tools/unified/server.py +565 -0
  145. foundry_mcp/tools/unified/spec.py +1283 -0
  146. foundry_mcp/tools/unified/task.py +3846 -0
  147. foundry_mcp/tools/unified/test.py +431 -0
  148. foundry_mcp/tools/unified/verification.py +520 -0
  149. foundry_mcp-0.8.22.dist-info/METADATA +344 -0
  150. foundry_mcp-0.8.22.dist-info/RECORD +153 -0
  151. foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
  152. foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
  153. foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,515 @@
1
+ """
2
+ Base provider abstractions for foundry-mcp.
3
+
4
+ This module provides the core provider contracts adapted from sdd-toolkit,
5
+ enabling pluggable LLM backends for CLI operations. The abstractions support
6
+ capability negotiation, request/response normalization, and lifecycle hooks.
7
+
8
+ Design principles:
9
+ - Frozen dataclasses for immutability
10
+ - Enum-based capabilities for type-safe routing
11
+ - Status codes aligned with existing ProviderStatus patterns
12
+ - Error hierarchy for granular exception handling
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from abc import ABC, abstractmethod
18
+ from dataclasses import dataclass, field
19
+ from enum import Enum
20
+ from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence, Set
21
+
22
+ if TYPE_CHECKING:
23
+ from foundry_mcp.core.responses import ErrorType
24
+
25
+
26
+ class ProviderCapability(Enum):
27
+ """
28
+ Feature flags a provider can expose to routing heuristics.
29
+
30
+ These capabilities enable callers to select providers based on
31
+ required features (vision, streaming, etc.) and allow registries
32
+ to route requests to appropriate backends.
33
+
34
+ Values:
35
+ TEXT: Basic text generation capability
36
+ VISION: Image/vision input processing
37
+ FUNCTION_CALLING: Tool/function invocation support
38
+ STREAMING: Incremental response streaming
39
+ THINKING: Extended reasoning/chain-of-thought support
40
+ """
41
+
42
+ TEXT = "text_generation"
43
+ VISION = "vision"
44
+ FUNCTION_CALLING = "function_calling"
45
+ STREAMING = "streaming"
46
+ THINKING = "thinking"
47
+
48
+
49
+ class ProviderStatus(Enum):
50
+ """
51
+ Normalized execution outcomes emitted by providers.
52
+
53
+ These status codes provide a consistent interface for handling provider
54
+ responses across different backends, enabling uniform error handling
55
+ and retry logic.
56
+
57
+ Values:
58
+ SUCCESS: Operation completed successfully
59
+ TIMEOUT: Operation exceeded time limit (retryable)
60
+ NOT_FOUND: Provider/resource not available (not retryable)
61
+ INVALID_OUTPUT: Provider returned malformed response (not retryable)
62
+ ERROR: Generic error during execution (retryable)
63
+ CANCELED: Operation was explicitly canceled (not retryable)
64
+ """
65
+
66
+ SUCCESS = "success"
67
+ TIMEOUT = "timeout"
68
+ NOT_FOUND = "not_found"
69
+ INVALID_OUTPUT = "invalid_output"
70
+ ERROR = "error"
71
+ CANCELED = "canceled"
72
+
73
+ def is_retryable(self) -> bool:
74
+ """
75
+ Check if this status represents a transient failure that may succeed on retry.
76
+
77
+ Retryable statuses:
78
+ - TIMEOUT: May succeed with more time or if temporary resource contention resolves
79
+ - ERROR: Generic errors may be transient (network issues, rate limits, etc.)
80
+
81
+ Non-retryable statuses:
82
+ - SUCCESS: Already succeeded
83
+ - NOT_FOUND: Provider/tool not available (won't change on retry)
84
+ - INVALID_OUTPUT: Provider responded but output was malformed
85
+ - CANCELED: Explicitly canceled (shouldn't retry)
86
+
87
+ Returns:
88
+ True if the status is retryable, False otherwise
89
+ """
90
+ return self in (ProviderStatus.TIMEOUT, ProviderStatus.ERROR)
91
+
92
+ def to_error_type(self) -> "ErrorType":
93
+ """
94
+ Map provider status to foundry-mcp ErrorType for response envelopes.
95
+
96
+ This enables consistent error categorization across MCP and CLI surfaces,
97
+ allowing callers to handle provider errors using the standard error taxonomy.
98
+
99
+ Mapping:
100
+ - SUCCESS: raises ValueError (not an error state)
101
+ - TIMEOUT: UNAVAILABLE (503 analog, retryable)
102
+ - NOT_FOUND: NOT_FOUND (404 analog, not retryable)
103
+ - INVALID_OUTPUT: VALIDATION (400 analog, not retryable)
104
+ - ERROR: INTERNAL (500 analog, retryable)
105
+ - CANCELED: INTERNAL (operation aborted)
106
+
107
+ Returns:
108
+ ErrorType enum value corresponding to this status
109
+
110
+ Raises:
111
+ ValueError: If called on SUCCESS status (not an error)
112
+ """
113
+ from foundry_mcp.core.responses import ErrorType
114
+
115
+ if self == ProviderStatus.SUCCESS:
116
+ raise ValueError("SUCCESS status cannot be mapped to an error type")
117
+
118
+ mapping = {
119
+ ProviderStatus.TIMEOUT: ErrorType.UNAVAILABLE,
120
+ ProviderStatus.NOT_FOUND: ErrorType.NOT_FOUND,
121
+ ProviderStatus.INVALID_OUTPUT: ErrorType.VALIDATION,
122
+ ProviderStatus.ERROR: ErrorType.INTERNAL,
123
+ ProviderStatus.CANCELED: ErrorType.INTERNAL,
124
+ }
125
+ return mapping[self]
126
+
127
+
128
+ @dataclass(frozen=True)
129
+ class ProviderRequest:
130
+ """
131
+ Normalized request payload for provider execution.
132
+
133
+ This dataclass encapsulates all parameters needed to make a generation
134
+ request to any provider backend. Fields follow common LLM API conventions
135
+ to ensure portability across different providers.
136
+
137
+ Attributes:
138
+ prompt: The user's input prompt/message
139
+ system_prompt: Optional system/instruction prompt
140
+ model: Model identifier (provider-specific, e.g., "pro", "flash")
141
+ timeout: Request timeout in seconds (None = provider default)
142
+ temperature: Sampling temperature (0.0-2.0, None = provider default)
143
+ max_tokens: Maximum output tokens (None = provider default)
144
+ metadata: Arbitrary request metadata (tracing IDs, feature flags, etc.)
145
+ stream: Whether to request streaming response
146
+ attachments: File paths or URIs for multimodal inputs
147
+ """
148
+
149
+ prompt: str
150
+ system_prompt: Optional[str] = None
151
+ model: Optional[str] = None
152
+ timeout: Optional[float] = None
153
+ temperature: Optional[float] = None
154
+ max_tokens: Optional[int] = None
155
+ metadata: Dict[str, Any] = field(default_factory=dict)
156
+ stream: bool = False
157
+ attachments: Sequence[str] = field(default_factory=list)
158
+
159
+
160
+ @dataclass(frozen=True)
161
+ class TokenUsage:
162
+ """
163
+ Token accounting information reported by providers.
164
+
165
+ Tracks input, output, and cached tokens for cost estimation
166
+ and usage monitoring.
167
+
168
+ Attributes:
169
+ input_tokens: Tokens consumed by the prompt
170
+ output_tokens: Tokens generated in the response
171
+ cached_input_tokens: Tokens served from cache (if supported)
172
+ total_tokens: Sum of all token counts
173
+ """
174
+
175
+ input_tokens: int = 0
176
+ output_tokens: int = 0
177
+ cached_input_tokens: int = 0
178
+ total_tokens: int = 0
179
+
180
+
181
+ @dataclass(frozen=True)
182
+ class ProviderResult:
183
+ """
184
+ Normalized provider response.
185
+
186
+ This dataclass encapsulates all data returned from a provider execution,
187
+ providing a consistent interface regardless of the backend used.
188
+
189
+ Attributes:
190
+ content: Final text output (aggregated if streaming was used)
191
+ provider_id: Canonical provider identifier (e.g., "gemini", "codex")
192
+ model_used: Fully-qualified model identifier (e.g., "gemini:pro")
193
+ status: ProviderStatus describing execution outcome
194
+ tokens: Token usage data (if reported by provider)
195
+ duration_ms: Execution duration in milliseconds
196
+ stderr: Captured stderr/log output for debugging
197
+ raw_payload: Provider-specific metadata (traces, debug info, etc.)
198
+ """
199
+
200
+ content: str
201
+ provider_id: str
202
+ model_used: str
203
+ status: ProviderStatus
204
+ tokens: TokenUsage = field(default_factory=TokenUsage)
205
+ duration_ms: Optional[float] = None
206
+ stderr: Optional[str] = None
207
+ raw_payload: Dict[str, Any] = field(default_factory=dict)
208
+
209
+
210
+ @dataclass(frozen=True)
211
+ class ModelDescriptor:
212
+ """
213
+ Describes a model supported by a provider.
214
+
215
+ Attributes:
216
+ id: Provider-specific model identifier (e.g., "pro", "flash")
217
+ display_name: Human-friendly name for UIs/logs
218
+ capabilities: Feature flags supported by this model
219
+ routing_hints: Optional metadata for routing (cost, latency, etc.)
220
+ """
221
+
222
+ id: str
223
+ display_name: Optional[str] = None
224
+ capabilities: Set[ProviderCapability] = field(default_factory=set)
225
+ routing_hints: Dict[str, Any] = field(default_factory=dict)
226
+
227
+
228
+ @dataclass(frozen=True)
229
+ class ProviderMetadata:
230
+ """
231
+ Provider-level metadata shared with registries and consumers.
232
+
233
+ This dataclass describes a provider's capabilities, supported models,
234
+ and configuration, enabling informed routing decisions.
235
+
236
+ Attributes:
237
+ provider_id: Canonical provider identifier (e.g., "gemini", "codex")
238
+ display_name: Human-friendly provider name
239
+ models: Supported model descriptors
240
+ default_model: Model ID used when no override supplied
241
+ capabilities: Aggregate capabilities across all models
242
+ security_flags: Provider-specific sandbox/safety configuration
243
+ extra: Arbitrary metadata (version info, auth requirements, etc.)
244
+ """
245
+
246
+ provider_id: str
247
+ display_name: Optional[str] = None
248
+ models: Sequence[ModelDescriptor] = field(default_factory=list)
249
+ default_model: Optional[str] = None
250
+ capabilities: Set[ProviderCapability] = field(default_factory=set)
251
+ security_flags: Dict[str, Any] = field(default_factory=dict)
252
+ extra: Dict[str, Any] = field(default_factory=dict)
253
+
254
+
255
+ # =============================================================================
256
+ # Error Hierarchy
257
+ # =============================================================================
258
+
259
+
260
+ class ProviderError(RuntimeError):
261
+ """Base exception for provider orchestration errors."""
262
+
263
+ def __init__(self, message: str, *, provider: Optional[str] = None):
264
+ self.provider = provider
265
+ super().__init__(message)
266
+
267
+
268
+ class ProviderUnavailableError(ProviderError):
269
+ """Raised when a provider cannot be instantiated (binary missing, auth issues)."""
270
+
271
+
272
+ class ProviderExecutionError(ProviderError):
273
+ """Raised when a provider command returns a non-retryable error."""
274
+
275
+
276
+ class ProviderTimeoutError(ProviderError):
277
+ """Raised when a provider exceeds its allotted execution time."""
278
+
279
+
280
+ class ContextWindowError(ProviderExecutionError):
281
+ """Raised when prompt exceeds the model's context window limit.
282
+
283
+ This error indicates the prompt/context size exceeded what the model
284
+ can process. It includes token counts to help with debugging and
285
+ provides actionable guidance for resolution.
286
+
287
+ Attributes:
288
+ prompt_tokens: Estimated tokens in the prompt (if known)
289
+ max_tokens: Maximum context window size (if known)
290
+ provider: Provider that raised the error
291
+ truncation_needed: How many tokens need to be removed
292
+ """
293
+
294
+ def __init__(
295
+ self,
296
+ message: str,
297
+ *,
298
+ provider: Optional[str] = None,
299
+ prompt_tokens: Optional[int] = None,
300
+ max_tokens: Optional[int] = None,
301
+ ):
302
+ """Initialize context window error.
303
+
304
+ Args:
305
+ message: Error message describing the issue
306
+ provider: Provider ID that raised the error
307
+ prompt_tokens: Number of tokens in the prompt (if known)
308
+ max_tokens: Maximum tokens allowed (if known)
309
+ """
310
+ super().__init__(message, provider=provider)
311
+ self.prompt_tokens = prompt_tokens
312
+ self.max_tokens = max_tokens
313
+ self.truncation_needed = (
314
+ (prompt_tokens - max_tokens) if prompt_tokens and max_tokens else None
315
+ )
316
+
317
+
318
+ # =============================================================================
319
+ # Lifecycle Hooks
320
+ # =============================================================================
321
+
322
+
323
+ # Type aliases for hook callables
324
+ StreamChunkCallback = Callable[["StreamChunk", ProviderMetadata], None]
325
+ BeforeExecuteHook = Callable[[ProviderRequest, ProviderMetadata], None]
326
+ AfterResultHook = Callable[[ProviderResult, ProviderMetadata], None]
327
+
328
+
329
+ @dataclass(frozen=True)
330
+ class StreamChunk:
331
+ """Represents a streamed fragment emitted by the provider."""
332
+
333
+ content: str
334
+ index: int
335
+ metadata: Dict[str, Any] = field(default_factory=dict)
336
+
337
+
338
+ @dataclass
339
+ class ProviderHooks:
340
+ """
341
+ Optional lifecycle hooks wired by the registry.
342
+
343
+ Hooks default to None (no-ops) so providers can invoke them unconditionally.
344
+ Registries can wire hooks for observability, logging, or streaming.
345
+ """
346
+
347
+ before_execute: Optional[BeforeExecuteHook] = None
348
+ on_stream_chunk: Optional[StreamChunkCallback] = None
349
+ after_result: Optional[AfterResultHook] = None
350
+
351
+ def emit_before(self, request: ProviderRequest, metadata: ProviderMetadata) -> None:
352
+ """Emit before-execution hook if registered."""
353
+ if self.before_execute:
354
+ self.before_execute(request, metadata)
355
+
356
+ def emit_stream(self, chunk: StreamChunk, metadata: ProviderMetadata) -> None:
357
+ """Emit stream chunk hook if registered."""
358
+ if self.on_stream_chunk:
359
+ self.on_stream_chunk(chunk, metadata)
360
+
361
+ def emit_after(self, result: ProviderResult, metadata: ProviderMetadata) -> None:
362
+ """Emit after-result hook if registered."""
363
+ if self.after_result:
364
+ self.after_result(result, metadata)
365
+
366
+
367
+ # =============================================================================
368
+ # Abstract Base Class
369
+ # =============================================================================
370
+
371
+
372
+ class ProviderContext(ABC):
373
+ """
374
+ Base class for provider implementations.
375
+
376
+ Subclasses should:
377
+ * Resolve CLI/environment dependencies during initialization
378
+ * Implement `_execute()` to run the underlying provider
379
+ * Return a populated `ProviderResult` from `_execute()`
380
+ * Emit streaming chunks via `self._emit_stream_chunk()` when
381
+ `request.stream` is True and the provider supports streaming
382
+
383
+ The `generate()` method is a template method that:
384
+ 1. Calls `_prepare_request()` for any request modifications
385
+ 2. Emits the `before_execute` hook
386
+ 3. Calls the abstract `_execute()` method
387
+ 4. Normalizes exceptions into typed ProviderErrors
388
+ 5. Emits the `after_result` hook
389
+ 6. Returns the result
390
+ """
391
+
392
+ def __init__(
393
+ self,
394
+ metadata: ProviderMetadata,
395
+ hooks: Optional[ProviderHooks] = None,
396
+ ):
397
+ self._metadata = metadata
398
+ self._hooks = hooks or ProviderHooks()
399
+
400
+ @property
401
+ def metadata(self) -> ProviderMetadata:
402
+ """Return provider metadata."""
403
+ return self._metadata
404
+
405
+ def supports(self, capability: ProviderCapability) -> bool:
406
+ """Return True if any registered model advertises the capability."""
407
+ # Check provider-level capabilities first
408
+ if capability in self._metadata.capabilities:
409
+ return True
410
+ # Then check model-level capabilities
411
+ return any(capability in model.capabilities for model in self._metadata.models)
412
+
413
+ def generate(self, request: ProviderRequest) -> ProviderResult:
414
+ """
415
+ Execute the provider with the supplied request (template method).
416
+
417
+ Applies lifecycle hooks, normalizes errors, and ensures ProviderStatus
418
+ is consistent across implementations.
419
+
420
+ Args:
421
+ request: The generation request
422
+
423
+ Returns:
424
+ ProviderResult with the generation output
425
+
426
+ Raises:
427
+ ProviderUnavailableError: If provider binary/auth unavailable
428
+ ProviderTimeoutError: If request exceeds timeout
429
+ ProviderExecutionError: For other execution errors
430
+ """
431
+ normalized_request = self._prepare_request(request)
432
+ self._hooks.emit_before(normalized_request, self._metadata)
433
+
434
+ try:
435
+ result = self._execute(normalized_request)
436
+ except ProviderTimeoutError:
437
+ raise
438
+ except ProviderUnavailableError:
439
+ raise
440
+ except ProviderError:
441
+ raise
442
+ except FileNotFoundError as exc:
443
+ raise ProviderUnavailableError(
444
+ str(exc), provider=self._metadata.provider_id
445
+ ) from exc
446
+ except TimeoutError as exc:
447
+ raise ProviderTimeoutError(
448
+ str(exc), provider=self._metadata.provider_id
449
+ ) from exc
450
+ except Exception as exc: # noqa: BLE001 - intentionally wrap all provider exceptions
451
+ raise ProviderExecutionError(
452
+ str(exc), provider=self._metadata.provider_id
453
+ ) from exc
454
+
455
+ self._hooks.emit_after(result, self._metadata)
456
+ return result
457
+
458
+ def _prepare_request(self, request: ProviderRequest) -> ProviderRequest:
459
+ """
460
+ Allow subclasses to adjust request metadata before execution.
461
+
462
+ The default implementation simply returns the request unchanged.
463
+ Subclasses can override to inject defaults, normalize parameters, etc.
464
+ """
465
+ return request
466
+
467
+ def _emit_stream_chunk(self, chunk: StreamChunk) -> None:
468
+ """Helper for subclasses to publish streaming output through hooks."""
469
+ self._hooks.emit_stream(chunk, self._metadata)
470
+
471
+ @abstractmethod
472
+ def _execute(self, request: ProviderRequest) -> ProviderResult:
473
+ """
474
+ Subclasses must implement the actual provider invocation.
475
+
476
+ Args:
477
+ request: The (possibly modified) generation request
478
+
479
+ Returns:
480
+ ProviderResult with generated content and metadata
481
+ """
482
+ raise NotImplementedError
483
+
484
+
485
+ # =============================================================================
486
+ # Module Exports
487
+ # =============================================================================
488
+
489
+
490
+ __all__ = [
491
+ # Enums
492
+ "ProviderCapability",
493
+ "ProviderStatus",
494
+ # Request/Response dataclasses
495
+ "ProviderRequest",
496
+ "ProviderResult",
497
+ "TokenUsage",
498
+ "StreamChunk",
499
+ # Metadata dataclasses
500
+ "ModelDescriptor",
501
+ "ProviderMetadata",
502
+ # Hooks
503
+ "ProviderHooks",
504
+ "StreamChunkCallback",
505
+ "BeforeExecuteHook",
506
+ "AfterResultHook",
507
+ # Errors
508
+ "ProviderError",
509
+ "ProviderUnavailableError",
510
+ "ProviderExecutionError",
511
+ "ProviderTimeoutError",
512
+ "ContextWindowError",
513
+ # ABC
514
+ "ProviderContext",
515
+ ]