foundry-mcp 0.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. foundry_mcp/__init__.py +7 -0
  2. foundry_mcp/cli/__init__.py +80 -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 +633 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +652 -0
  15. foundry_mcp/cli/commands/session.py +479 -0
  16. foundry_mcp/cli/commands/specs.py +856 -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 +259 -0
  22. foundry_mcp/cli/flags.py +266 -0
  23. foundry_mcp/cli/logging.py +212 -0
  24. foundry_mcp/cli/main.py +44 -0
  25. foundry_mcp/cli/output.py +122 -0
  26. foundry_mcp/cli/registry.py +110 -0
  27. foundry_mcp/cli/resilience.py +178 -0
  28. foundry_mcp/cli/transcript.py +217 -0
  29. foundry_mcp/config.py +850 -0
  30. foundry_mcp/core/__init__.py +144 -0
  31. foundry_mcp/core/ai_consultation.py +1636 -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/feature_flags.py +592 -0
  40. foundry_mcp/core/health.py +749 -0
  41. foundry_mcp/core/journal.py +694 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1350 -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 +123 -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 +317 -0
  57. foundry_mcp/core/prometheus.py +577 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +546 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
  61. foundry_mcp/core/prompts/plan_review.py +623 -0
  62. foundry_mcp/core/providers/__init__.py +225 -0
  63. foundry_mcp/core/providers/base.py +476 -0
  64. foundry_mcp/core/providers/claude.py +460 -0
  65. foundry_mcp/core/providers/codex.py +619 -0
  66. foundry_mcp/core/providers/cursor_agent.py +642 -0
  67. foundry_mcp/core/providers/detectors.py +488 -0
  68. foundry_mcp/core/providers/gemini.py +405 -0
  69. foundry_mcp/core/providers/opencode.py +616 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +302 -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 +729 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/resilience.py +600 -0
  78. foundry_mcp/core/responses.py +934 -0
  79. foundry_mcp/core/review.py +366 -0
  80. foundry_mcp/core/security.py +438 -0
  81. foundry_mcp/core/spec.py +1650 -0
  82. foundry_mcp/core/task.py +1289 -0
  83. foundry_mcp/core/testing.py +450 -0
  84. foundry_mcp/core/validation.py +2081 -0
  85. foundry_mcp/dashboard/__init__.py +32 -0
  86. foundry_mcp/dashboard/app.py +119 -0
  87. foundry_mcp/dashboard/components/__init__.py +17 -0
  88. foundry_mcp/dashboard/components/cards.py +88 -0
  89. foundry_mcp/dashboard/components/charts.py +234 -0
  90. foundry_mcp/dashboard/components/filters.py +136 -0
  91. foundry_mcp/dashboard/components/tables.py +195 -0
  92. foundry_mcp/dashboard/data/__init__.py +11 -0
  93. foundry_mcp/dashboard/data/stores.py +433 -0
  94. foundry_mcp/dashboard/launcher.py +289 -0
  95. foundry_mcp/dashboard/views/__init__.py +12 -0
  96. foundry_mcp/dashboard/views/errors.py +217 -0
  97. foundry_mcp/dashboard/views/metrics.py +174 -0
  98. foundry_mcp/dashboard/views/overview.py +160 -0
  99. foundry_mcp/dashboard/views/providers.py +83 -0
  100. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  101. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  102. foundry_mcp/prompts/__init__.py +9 -0
  103. foundry_mcp/prompts/workflows.py +525 -0
  104. foundry_mcp/resources/__init__.py +9 -0
  105. foundry_mcp/resources/specs.py +591 -0
  106. foundry_mcp/schemas/__init__.py +38 -0
  107. foundry_mcp/schemas/sdd-spec-schema.json +386 -0
  108. foundry_mcp/server.py +164 -0
  109. foundry_mcp/tools/__init__.py +10 -0
  110. foundry_mcp/tools/unified/__init__.py +71 -0
  111. foundry_mcp/tools/unified/authoring.py +1487 -0
  112. foundry_mcp/tools/unified/context_helpers.py +98 -0
  113. foundry_mcp/tools/unified/documentation_helpers.py +198 -0
  114. foundry_mcp/tools/unified/environment.py +939 -0
  115. foundry_mcp/tools/unified/error.py +462 -0
  116. foundry_mcp/tools/unified/health.py +225 -0
  117. foundry_mcp/tools/unified/journal.py +841 -0
  118. foundry_mcp/tools/unified/lifecycle.py +632 -0
  119. foundry_mcp/tools/unified/metrics.py +777 -0
  120. foundry_mcp/tools/unified/plan.py +745 -0
  121. foundry_mcp/tools/unified/pr.py +294 -0
  122. foundry_mcp/tools/unified/provider.py +629 -0
  123. foundry_mcp/tools/unified/review.py +685 -0
  124. foundry_mcp/tools/unified/review_helpers.py +299 -0
  125. foundry_mcp/tools/unified/router.py +102 -0
  126. foundry_mcp/tools/unified/server.py +580 -0
  127. foundry_mcp/tools/unified/spec.py +808 -0
  128. foundry_mcp/tools/unified/task.py +2202 -0
  129. foundry_mcp/tools/unified/test.py +370 -0
  130. foundry_mcp/tools/unified/verification.py +520 -0
  131. foundry_mcp-0.3.3.dist-info/METADATA +337 -0
  132. foundry_mcp-0.3.3.dist-info/RECORD +135 -0
  133. foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
  134. foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
  135. foundry_mcp-0.3.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,225 @@
1
+ """
2
+ Provider abstractions for foundry-mcp.
3
+
4
+ This package provides pluggable LLM provider backends for CLI operations,
5
+ with support for capability negotiation, request/response normalization,
6
+ lifecycle hooks, availability detection, and registry management.
7
+
8
+ Example usage:
9
+ from foundry_mcp.core.providers import (
10
+ # Core types
11
+ ProviderCapability,
12
+ ProviderRequest,
13
+ ProviderResult,
14
+ ProviderContext,
15
+ ProviderHooks,
16
+ # Detection
17
+ detect_provider_availability,
18
+ get_provider_statuses,
19
+ # Registry
20
+ register_provider,
21
+ resolve_provider,
22
+ available_providers,
23
+ )
24
+
25
+ # Check provider availability
26
+ if detect_provider_availability("gemini"):
27
+ # Register and resolve a provider
28
+ hooks = ProviderHooks()
29
+ provider = resolve_provider("gemini", hooks=hooks)
30
+
31
+ # Check if provider supports streaming
32
+ if provider.supports(ProviderCapability.STREAMING):
33
+ request = ProviderRequest(prompt="Hello", stream=True)
34
+ result = provider.generate(request)
35
+ """
36
+
37
+ from foundry_mcp.core.providers.base import (
38
+ # Enums
39
+ ProviderCapability,
40
+ ProviderStatus,
41
+ # Request/Response dataclasses
42
+ ProviderRequest,
43
+ ProviderResult,
44
+ TokenUsage,
45
+ StreamChunk,
46
+ # Metadata dataclasses
47
+ ModelDescriptor,
48
+ ProviderMetadata,
49
+ # Hooks
50
+ ProviderHooks,
51
+ StreamChunkCallback,
52
+ BeforeExecuteHook,
53
+ AfterResultHook,
54
+ # Errors
55
+ ProviderError,
56
+ ProviderUnavailableError,
57
+ ProviderExecutionError,
58
+ ProviderTimeoutError,
59
+ # ABC
60
+ ProviderContext,
61
+ )
62
+
63
+ from foundry_mcp.core.providers.detectors import (
64
+ ProviderDetector,
65
+ register_detector,
66
+ get_detector,
67
+ detect_provider_availability,
68
+ get_provider_statuses,
69
+ list_detectors,
70
+ reset_detectors,
71
+ )
72
+
73
+ from foundry_mcp.core.providers.registry import (
74
+ # Types
75
+ ProviderFactory,
76
+ ProviderRegistration,
77
+ AvailabilityCheck,
78
+ MetadataResolver,
79
+ LazyFactoryLoader,
80
+ DependencyResolver,
81
+ # Registration
82
+ register_provider,
83
+ register_lazy_provider,
84
+ # Resolution
85
+ available_providers,
86
+ check_provider_available,
87
+ resolve_provider,
88
+ get_provider_metadata,
89
+ describe_providers,
90
+ # Dependency Injection
91
+ set_dependency_resolver,
92
+ # Testing
93
+ reset_registry,
94
+ get_registration,
95
+ )
96
+
97
+ from foundry_mcp.core.providers.validation import (
98
+ # Validation
99
+ ValidationError,
100
+ strip_ansi,
101
+ ensure_utf8,
102
+ sanitize_prompt,
103
+ validate_request,
104
+ # Command allowlists
105
+ COMMON_SAFE_COMMANDS,
106
+ BLOCKED_COMMANDS,
107
+ is_command_allowed,
108
+ # Observability
109
+ ExecutionSpan,
110
+ create_execution_span,
111
+ log_span,
112
+ # Retry
113
+ RETRYABLE_STATUSES,
114
+ is_retryable,
115
+ is_retryable_error,
116
+ # Circuit breaker
117
+ CircuitState,
118
+ CircuitBreaker,
119
+ get_circuit_breaker,
120
+ reset_circuit_breakers,
121
+ # Rate limiting
122
+ RateLimiter,
123
+ get_rate_limiter,
124
+ reset_rate_limiters,
125
+ # Execution wrapper
126
+ with_validation_and_resilience,
127
+ )
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # Import provider modules to trigger auto-registration with the registry.
131
+ # Each provider module calls register_provider() at import time.
132
+ # ---------------------------------------------------------------------------
133
+ from foundry_mcp.core.providers import gemini as _gemini_provider # noqa: F401
134
+ from foundry_mcp.core.providers import codex as _codex_provider # noqa: F401
135
+ from foundry_mcp.core.providers import cursor_agent as _cursor_agent_provider # noqa: F401
136
+ from foundry_mcp.core.providers import claude as _claude_provider # noqa: F401
137
+ from foundry_mcp.core.providers import opencode as _opencode_provider # noqa: F401
138
+ from foundry_mcp.core.providers import test_provider as _test_provider # noqa: F401
139
+
140
+ __all__ = [
141
+ # === Base Types (base.py) ===
142
+ # Enums
143
+ "ProviderCapability",
144
+ "ProviderStatus",
145
+ # Request/Response dataclasses
146
+ "ProviderRequest",
147
+ "ProviderResult",
148
+ "TokenUsage",
149
+ "StreamChunk",
150
+ # Metadata dataclasses
151
+ "ModelDescriptor",
152
+ "ProviderMetadata",
153
+ # Hooks
154
+ "ProviderHooks",
155
+ "StreamChunkCallback",
156
+ "BeforeExecuteHook",
157
+ "AfterResultHook",
158
+ # Errors
159
+ "ProviderError",
160
+ "ProviderUnavailableError",
161
+ "ProviderExecutionError",
162
+ "ProviderTimeoutError",
163
+ # ABC
164
+ "ProviderContext",
165
+ # === Detection (detectors.py) ===
166
+ "ProviderDetector",
167
+ "register_detector",
168
+ "get_detector",
169
+ "detect_provider_availability",
170
+ "get_provider_statuses",
171
+ "list_detectors",
172
+ "reset_detectors",
173
+ # === Registry (registry.py) ===
174
+ # Types
175
+ "ProviderFactory",
176
+ "ProviderRegistration",
177
+ "AvailabilityCheck",
178
+ "MetadataResolver",
179
+ "LazyFactoryLoader",
180
+ "DependencyResolver",
181
+ # Registration
182
+ "register_provider",
183
+ "register_lazy_provider",
184
+ # Resolution
185
+ "available_providers",
186
+ "check_provider_available",
187
+ "resolve_provider",
188
+ "get_provider_metadata",
189
+ "describe_providers",
190
+ # Dependency Injection
191
+ "set_dependency_resolver",
192
+ # Testing
193
+ "reset_registry",
194
+ "get_registration",
195
+ # === Validation & Resilience (validation.py) ===
196
+ # Validation
197
+ "ValidationError",
198
+ "strip_ansi",
199
+ "ensure_utf8",
200
+ "sanitize_prompt",
201
+ "validate_request",
202
+ # Command allowlists
203
+ "COMMON_SAFE_COMMANDS",
204
+ "BLOCKED_COMMANDS",
205
+ "is_command_allowed",
206
+ # Observability
207
+ "ExecutionSpan",
208
+ "create_execution_span",
209
+ "log_span",
210
+ # Retry
211
+ "RETRYABLE_STATUSES",
212
+ "is_retryable",
213
+ "is_retryable_error",
214
+ # Circuit breaker
215
+ "CircuitState",
216
+ "CircuitBreaker",
217
+ "get_circuit_breaker",
218
+ "reset_circuit_breakers",
219
+ # Rate limiting
220
+ "RateLimiter",
221
+ "get_rate_limiter",
222
+ "reset_rate_limiters",
223
+ # Execution wrapper
224
+ "with_validation_and_resilience",
225
+ ]
@@ -0,0 +1,476 @@
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
+ # =============================================================================
281
+ # Lifecycle Hooks
282
+ # =============================================================================
283
+
284
+
285
+ # Type aliases for hook callables
286
+ StreamChunkCallback = Callable[["StreamChunk", ProviderMetadata], None]
287
+ BeforeExecuteHook = Callable[[ProviderRequest, ProviderMetadata], None]
288
+ AfterResultHook = Callable[[ProviderResult, ProviderMetadata], None]
289
+
290
+
291
+ @dataclass(frozen=True)
292
+ class StreamChunk:
293
+ """Represents a streamed fragment emitted by the provider."""
294
+
295
+ content: str
296
+ index: int
297
+ metadata: Dict[str, Any] = field(default_factory=dict)
298
+
299
+
300
+ @dataclass
301
+ class ProviderHooks:
302
+ """
303
+ Optional lifecycle hooks wired by the registry.
304
+
305
+ Hooks default to None (no-ops) so providers can invoke them unconditionally.
306
+ Registries can wire hooks for observability, logging, or streaming.
307
+ """
308
+
309
+ before_execute: Optional[BeforeExecuteHook] = None
310
+ on_stream_chunk: Optional[StreamChunkCallback] = None
311
+ after_result: Optional[AfterResultHook] = None
312
+
313
+ def emit_before(self, request: ProviderRequest, metadata: ProviderMetadata) -> None:
314
+ """Emit before-execution hook if registered."""
315
+ if self.before_execute:
316
+ self.before_execute(request, metadata)
317
+
318
+ def emit_stream(self, chunk: StreamChunk, metadata: ProviderMetadata) -> None:
319
+ """Emit stream chunk hook if registered."""
320
+ if self.on_stream_chunk:
321
+ self.on_stream_chunk(chunk, metadata)
322
+
323
+ def emit_after(self, result: ProviderResult, metadata: ProviderMetadata) -> None:
324
+ """Emit after-result hook if registered."""
325
+ if self.after_result:
326
+ self.after_result(result, metadata)
327
+
328
+
329
+ # =============================================================================
330
+ # Abstract Base Class
331
+ # =============================================================================
332
+
333
+
334
+ class ProviderContext(ABC):
335
+ """
336
+ Base class for provider implementations.
337
+
338
+ Subclasses should:
339
+ * Resolve CLI/environment dependencies during initialization
340
+ * Implement `_execute()` to run the underlying provider
341
+ * Return a populated `ProviderResult` from `_execute()`
342
+ * Emit streaming chunks via `self._emit_stream_chunk()` when
343
+ `request.stream` is True and the provider supports streaming
344
+
345
+ The `generate()` method is a template method that:
346
+ 1. Calls `_prepare_request()` for any request modifications
347
+ 2. Emits the `before_execute` hook
348
+ 3. Calls the abstract `_execute()` method
349
+ 4. Normalizes exceptions into typed ProviderErrors
350
+ 5. Emits the `after_result` hook
351
+ 6. Returns the result
352
+ """
353
+
354
+ def __init__(
355
+ self,
356
+ metadata: ProviderMetadata,
357
+ hooks: Optional[ProviderHooks] = None,
358
+ ):
359
+ self._metadata = metadata
360
+ self._hooks = hooks or ProviderHooks()
361
+
362
+ @property
363
+ def metadata(self) -> ProviderMetadata:
364
+ """Return provider metadata."""
365
+ return self._metadata
366
+
367
+ def supports(self, capability: ProviderCapability) -> bool:
368
+ """Return True if any registered model advertises the capability."""
369
+ # Check provider-level capabilities first
370
+ if capability in self._metadata.capabilities:
371
+ return True
372
+ # Then check model-level capabilities
373
+ return any(capability in model.capabilities for model in self._metadata.models)
374
+
375
+ def generate(self, request: ProviderRequest) -> ProviderResult:
376
+ """
377
+ Execute the provider with the supplied request (template method).
378
+
379
+ Applies lifecycle hooks, normalizes errors, and ensures ProviderStatus
380
+ is consistent across implementations.
381
+
382
+ Args:
383
+ request: The generation request
384
+
385
+ Returns:
386
+ ProviderResult with the generation output
387
+
388
+ Raises:
389
+ ProviderUnavailableError: If provider binary/auth unavailable
390
+ ProviderTimeoutError: If request exceeds timeout
391
+ ProviderExecutionError: For other execution errors
392
+ """
393
+ normalized_request = self._prepare_request(request)
394
+ self._hooks.emit_before(normalized_request, self._metadata)
395
+
396
+ try:
397
+ result = self._execute(normalized_request)
398
+ except ProviderTimeoutError:
399
+ raise
400
+ except ProviderUnavailableError:
401
+ raise
402
+ except ProviderError:
403
+ raise
404
+ except FileNotFoundError as exc:
405
+ raise ProviderUnavailableError(
406
+ str(exc), provider=self._metadata.provider_id
407
+ ) from exc
408
+ except TimeoutError as exc:
409
+ raise ProviderTimeoutError(
410
+ str(exc), provider=self._metadata.provider_id
411
+ ) from exc
412
+ except Exception as exc: # noqa: BLE001 - intentionally wrap all provider exceptions
413
+ raise ProviderExecutionError(
414
+ str(exc), provider=self._metadata.provider_id
415
+ ) from exc
416
+
417
+ self._hooks.emit_after(result, self._metadata)
418
+ return result
419
+
420
+ def _prepare_request(self, request: ProviderRequest) -> ProviderRequest:
421
+ """
422
+ Allow subclasses to adjust request metadata before execution.
423
+
424
+ The default implementation simply returns the request unchanged.
425
+ Subclasses can override to inject defaults, normalize parameters, etc.
426
+ """
427
+ return request
428
+
429
+ def _emit_stream_chunk(self, chunk: StreamChunk) -> None:
430
+ """Helper for subclasses to publish streaming output through hooks."""
431
+ self._hooks.emit_stream(chunk, self._metadata)
432
+
433
+ @abstractmethod
434
+ def _execute(self, request: ProviderRequest) -> ProviderResult:
435
+ """
436
+ Subclasses must implement the actual provider invocation.
437
+
438
+ Args:
439
+ request: The (possibly modified) generation request
440
+
441
+ Returns:
442
+ ProviderResult with generated content and metadata
443
+ """
444
+ raise NotImplementedError
445
+
446
+
447
+ # =============================================================================
448
+ # Module Exports
449
+ # =============================================================================
450
+
451
+
452
+ __all__ = [
453
+ # Enums
454
+ "ProviderCapability",
455
+ "ProviderStatus",
456
+ # Request/Response dataclasses
457
+ "ProviderRequest",
458
+ "ProviderResult",
459
+ "TokenUsage",
460
+ "StreamChunk",
461
+ # Metadata dataclasses
462
+ "ModelDescriptor",
463
+ "ProviderMetadata",
464
+ # Hooks
465
+ "ProviderHooks",
466
+ "StreamChunkCallback",
467
+ "BeforeExecuteHook",
468
+ "AfterResultHook",
469
+ # Errors
470
+ "ProviderError",
471
+ "ProviderUnavailableError",
472
+ "ProviderExecutionError",
473
+ "ProviderTimeoutError",
474
+ # ABC
475
+ "ProviderContext",
476
+ ]