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.
- foundry_mcp/__init__.py +7 -0
- foundry_mcp/cli/__init__.py +80 -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 +633 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +652 -0
- foundry_mcp/cli/commands/session.py +479 -0
- foundry_mcp/cli/commands/specs.py +856 -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 +259 -0
- foundry_mcp/cli/flags.py +266 -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 +850 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1636 -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/feature_flags.py +592 -0
- foundry_mcp/core/health.py +749 -0
- foundry_mcp/core/journal.py +694 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1350 -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 +123 -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 +317 -0
- foundry_mcp/core/prometheus.py +577 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +546 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
- foundry_mcp/core/prompts/plan_review.py +623 -0
- foundry_mcp/core/providers/__init__.py +225 -0
- foundry_mcp/core/providers/base.py +476 -0
- foundry_mcp/core/providers/claude.py +460 -0
- foundry_mcp/core/providers/codex.py +619 -0
- foundry_mcp/core/providers/cursor_agent.py +642 -0
- foundry_mcp/core/providers/detectors.py +488 -0
- foundry_mcp/core/providers/gemini.py +405 -0
- foundry_mcp/core/providers/opencode.py +616 -0
- foundry_mcp/core/providers/opencode_wrapper.js +302 -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 +729 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +934 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +1650 -0
- foundry_mcp/core/task.py +1289 -0
- foundry_mcp/core/testing.py +450 -0
- foundry_mcp/core/validation.py +2081 -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 +234 -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 +289 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +174 -0
- foundry_mcp/dashboard/views/overview.py +160 -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/sdd-spec-schema.json +386 -0
- foundry_mcp/server.py +164 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +71 -0
- foundry_mcp/tools/unified/authoring.py +1487 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +198 -0
- foundry_mcp/tools/unified/environment.py +939 -0
- foundry_mcp/tools/unified/error.py +462 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +632 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +745 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +629 -0
- foundry_mcp/tools/unified/review.py +685 -0
- foundry_mcp/tools/unified/review_helpers.py +299 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +580 -0
- foundry_mcp/tools/unified/spec.py +808 -0
- foundry_mcp/tools/unified/task.py +2202 -0
- foundry_mcp/tools/unified/test.py +370 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.3.3.dist-info/METADATA +337 -0
- foundry_mcp-0.3.3.dist-info/RECORD +135 -0
- foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
- foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
- 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
|
+
]
|