kubrick-cli 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kubrick_cli/__init__.py +47 -0
- kubrick_cli/agent_loop.py +307 -0
- kubrick_cli/classifier.py +198 -0
- kubrick_cli/config.py +248 -0
- kubrick_cli/display.py +165 -0
- kubrick_cli/execution_strategy.py +197 -0
- kubrick_cli/main.py +797 -0
- kubrick_cli/planning.py +328 -0
- kubrick_cli/progress.py +172 -0
- kubrick_cli/providers/__init__.py +6 -0
- kubrick_cli/providers/anthropic_provider.py +224 -0
- kubrick_cli/providers/base.py +137 -0
- kubrick_cli/providers/factory.py +183 -0
- kubrick_cli/providers/openai_provider.py +190 -0
- kubrick_cli/providers/triton_provider.py +96 -0
- kubrick_cli/safety.py +216 -0
- kubrick_cli/scheduler.py +197 -0
- kubrick_cli/setup_wizard.py +172 -0
- kubrick_cli/tools.py +407 -0
- kubrick_cli/triton_client.py +192 -0
- kubrick_cli-0.1.2.dist-info/METADATA +86 -0
- kubrick_cli-0.1.2.dist-info/RECORD +26 -0
- kubrick_cli-0.1.2.dist-info/WHEEL +5 -0
- kubrick_cli-0.1.2.dist-info/entry_points.txt +2 -0
- kubrick_cli-0.1.2.dist-info/licenses/LICENSE +21 -0
- kubrick_cli-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Anthropic provider adapter."""
|
|
2
|
+
|
|
3
|
+
import http.client
|
|
4
|
+
import json
|
|
5
|
+
import ssl
|
|
6
|
+
from typing import Dict, Iterator, List
|
|
7
|
+
|
|
8
|
+
from .base import ProviderAdapter, ProviderMetadata
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AnthropicProvider(ProviderAdapter):
|
|
12
|
+
"""Provider adapter for Anthropic API."""
|
|
13
|
+
|
|
14
|
+
METADATA = ProviderMetadata(
|
|
15
|
+
name="anthropic",
|
|
16
|
+
display_name="Anthropic",
|
|
17
|
+
description="Anthropic API (Claude Sonnet 4.5, etc.)",
|
|
18
|
+
config_fields=[
|
|
19
|
+
{
|
|
20
|
+
"key": "anthropic_api_key",
|
|
21
|
+
"label": "Anthropic API key",
|
|
22
|
+
"type": "password",
|
|
23
|
+
"help_text": "Get your API key from: https://console.anthropic.com/settings/keys",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"key": "anthropic_model",
|
|
27
|
+
"label": "Model name",
|
|
28
|
+
"type": "text",
|
|
29
|
+
"default": "claude-sonnet-4-5-20250929",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
anthropic_api_key: str,
|
|
37
|
+
anthropic_model: str = "claude-sonnet-4-5-20250929",
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Initialize Anthropic provider.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
anthropic_api_key: Anthropic API key
|
|
44
|
+
anthropic_model: Model name (default: claude-3-5-sonnet-20241022)
|
|
45
|
+
"""
|
|
46
|
+
self.api_key = anthropic_api_key
|
|
47
|
+
self._model_name = anthropic_model
|
|
48
|
+
self.base_url = "api.anthropic.com"
|
|
49
|
+
self.timeout = 600
|
|
50
|
+
self.api_version = "2023-06-01"
|
|
51
|
+
|
|
52
|
+
def generate_streaming(
|
|
53
|
+
self, messages: List[Dict[str, str]], stream_options: Dict = None
|
|
54
|
+
) -> Iterator[str]:
|
|
55
|
+
"""
|
|
56
|
+
Generate streaming response from Anthropic.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
messages: List of message dicts with 'role' and 'content'
|
|
60
|
+
stream_options: Optional streaming parameters
|
|
61
|
+
|
|
62
|
+
Yields:
|
|
63
|
+
Text chunks as they arrive
|
|
64
|
+
"""
|
|
65
|
+
# Anthropic requires system message to be separate
|
|
66
|
+
system_message = ""
|
|
67
|
+
conversation_messages = []
|
|
68
|
+
|
|
69
|
+
for msg in messages:
|
|
70
|
+
if msg["role"] == "system":
|
|
71
|
+
system_message = msg["content"]
|
|
72
|
+
else:
|
|
73
|
+
conversation_messages.append(msg)
|
|
74
|
+
|
|
75
|
+
# Build request payload
|
|
76
|
+
payload = {
|
|
77
|
+
"model": self._model_name,
|
|
78
|
+
"messages": conversation_messages,
|
|
79
|
+
"max_tokens": 4096,
|
|
80
|
+
"stream": True,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if system_message:
|
|
84
|
+
payload["system"] = system_message
|
|
85
|
+
|
|
86
|
+
# Add optional parameters
|
|
87
|
+
if stream_options:
|
|
88
|
+
if "temperature" in stream_options:
|
|
89
|
+
payload["temperature"] = stream_options["temperature"]
|
|
90
|
+
if "max_tokens" in stream_options:
|
|
91
|
+
payload["max_tokens"] = stream_options["max_tokens"]
|
|
92
|
+
|
|
93
|
+
headers = {
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
"x-api-key": self.api_key,
|
|
96
|
+
"anthropic-version": self.api_version,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
body = json.dumps(payload).encode("utf-8")
|
|
100
|
+
|
|
101
|
+
# Create HTTPS connection
|
|
102
|
+
context = ssl.create_default_context()
|
|
103
|
+
conn = http.client.HTTPSConnection(
|
|
104
|
+
self.base_url, 443, timeout=self.timeout, context=context
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
conn.request("POST", "/v1/messages", body=body, headers=headers)
|
|
109
|
+
response = conn.getresponse()
|
|
110
|
+
|
|
111
|
+
if response.status != 200:
|
|
112
|
+
error_body = response.read().decode("utf-8")
|
|
113
|
+
raise Exception(f"Anthropic API error {response.status}: {error_body}")
|
|
114
|
+
|
|
115
|
+
# Read streaming response
|
|
116
|
+
buffer = ""
|
|
117
|
+
while True:
|
|
118
|
+
chunk = response.read(1024)
|
|
119
|
+
if not chunk:
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
if isinstance(chunk, bytes):
|
|
123
|
+
chunk = chunk.decode("utf-8")
|
|
124
|
+
|
|
125
|
+
buffer += chunk
|
|
126
|
+
|
|
127
|
+
# Process complete lines
|
|
128
|
+
while "\n" in buffer:
|
|
129
|
+
line, buffer = buffer.split("\n", 1)
|
|
130
|
+
line = line.strip()
|
|
131
|
+
|
|
132
|
+
if not line:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
# Anthropic SSE format
|
|
136
|
+
if line.startswith("data: "):
|
|
137
|
+
line = line[6:]
|
|
138
|
+
|
|
139
|
+
if line.startswith("event: "):
|
|
140
|
+
# Skip event lines
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
data = json.loads(line)
|
|
145
|
+
|
|
146
|
+
# Handle different event types
|
|
147
|
+
event_type = data.get("type")
|
|
148
|
+
|
|
149
|
+
if event_type == "content_block_delta":
|
|
150
|
+
# Extract text delta
|
|
151
|
+
delta = data.get("delta", {})
|
|
152
|
+
if delta.get("type") == "text_delta":
|
|
153
|
+
text = delta.get("text", "")
|
|
154
|
+
if text:
|
|
155
|
+
yield text
|
|
156
|
+
|
|
157
|
+
elif event_type == "message_stop":
|
|
158
|
+
# End of stream
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
except json.JSONDecodeError:
|
|
162
|
+
# Skip malformed JSON
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
finally:
|
|
166
|
+
conn.close()
|
|
167
|
+
|
|
168
|
+
def generate(
|
|
169
|
+
self, messages: List[Dict[str, str]], stream_options: Dict = None
|
|
170
|
+
) -> str:
|
|
171
|
+
"""
|
|
172
|
+
Generate non-streaming response from Anthropic.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
messages: List of message dicts with 'role' and 'content'
|
|
176
|
+
stream_options: Optional parameters
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Complete response text
|
|
180
|
+
"""
|
|
181
|
+
chunks = []
|
|
182
|
+
for chunk in self.generate_streaming(messages, stream_options):
|
|
183
|
+
chunks.append(chunk)
|
|
184
|
+
return "".join(chunks)
|
|
185
|
+
|
|
186
|
+
def is_healthy(self) -> bool:
|
|
187
|
+
"""
|
|
188
|
+
Check if Anthropic API is accessible.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
True if healthy, False otherwise
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
# Simple health check - just verify we can connect
|
|
195
|
+
context = ssl.create_default_context()
|
|
196
|
+
conn = http.client.HTTPSConnection(
|
|
197
|
+
self.base_url, 443, timeout=10, context=context
|
|
198
|
+
)
|
|
199
|
+
headers = {
|
|
200
|
+
"x-api-key": self.api_key,
|
|
201
|
+
"anthropic-version": self.api_version,
|
|
202
|
+
}
|
|
203
|
+
# Try to make a minimal request
|
|
204
|
+
conn.request("GET", "/v1/models", headers=headers)
|
|
205
|
+
response = conn.getresponse()
|
|
206
|
+
conn.close()
|
|
207
|
+
# Anthropic might not have a /v1/models endpoint, so we check for 404 as well
|
|
208
|
+
return response.status in (200, 404)
|
|
209
|
+
except Exception:
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def provider_name(self) -> str:
|
|
214
|
+
"""Get provider name."""
|
|
215
|
+
return "anthropic"
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def model_name(self) -> str:
|
|
219
|
+
"""Get model name."""
|
|
220
|
+
return self._model_name
|
|
221
|
+
|
|
222
|
+
def set_model(self, model_name: str):
|
|
223
|
+
"""Set model name dynamically."""
|
|
224
|
+
self._model_name = model_name
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Base provider adapter interface."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Dict, Iterator, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ProviderMetadata:
|
|
8
|
+
"""
|
|
9
|
+
Metadata for a provider that describes its configuration needs.
|
|
10
|
+
|
|
11
|
+
This allows providers to be self-describing and enables automatic
|
|
12
|
+
setup wizard generation and provider discovery.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
name: str,
|
|
18
|
+
display_name: str,
|
|
19
|
+
description: str,
|
|
20
|
+
config_fields: List[Dict[str, str]],
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
Initialize provider metadata.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
name: Internal provider name (e.g., "triton", "openai")
|
|
27
|
+
display_name: Human-readable name (e.g., "Triton", "OpenAI")
|
|
28
|
+
description: Brief description shown in setup wizard
|
|
29
|
+
config_fields: List of configuration field definitions
|
|
30
|
+
Each field should be a dict with:
|
|
31
|
+
- key: Config key name (e.g., "openai_api_key")
|
|
32
|
+
- label: Display label (e.g., "OpenAI API key")
|
|
33
|
+
- type: Field type ("text", "password", "url")
|
|
34
|
+
- default: Default value (optional)
|
|
35
|
+
- help_text: Help text or URL (optional)
|
|
36
|
+
"""
|
|
37
|
+
self.name = name
|
|
38
|
+
self.display_name = display_name
|
|
39
|
+
self.description = description
|
|
40
|
+
self.config_fields = config_fields
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ProviderAdapter(ABC):
|
|
44
|
+
"""
|
|
45
|
+
Abstract base class for LLM provider adapters.
|
|
46
|
+
|
|
47
|
+
All provider implementations must inherit from this class and implement
|
|
48
|
+
the required methods for streaming and non-streaming generation.
|
|
49
|
+
|
|
50
|
+
To create a truly plug-and-play provider:
|
|
51
|
+
1. Inherit from ProviderAdapter
|
|
52
|
+
2. Define METADATA as a class attribute with ProviderMetadata
|
|
53
|
+
3. Implement all abstract methods
|
|
54
|
+
4. Place the file in kubrick_cli/providers/ directory
|
|
55
|
+
|
|
56
|
+
The provider will be automatically discovered and available in the setup wizard.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# Providers should override this with their metadata
|
|
60
|
+
METADATA: Optional[ProviderMetadata] = None
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def generate_streaming(
|
|
64
|
+
self, messages: List[Dict[str, str]], stream_options: Dict = None
|
|
65
|
+
) -> Iterator[str]:
|
|
66
|
+
"""
|
|
67
|
+
Generate streaming response from LLM.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
messages: List of message dicts with 'role' and 'content'
|
|
71
|
+
stream_options: Optional streaming parameters
|
|
72
|
+
|
|
73
|
+
Yields:
|
|
74
|
+
Text chunks as they arrive
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def generate(
|
|
80
|
+
self, messages: List[Dict[str, str]], stream_options: Dict = None
|
|
81
|
+
) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Generate non-streaming response from LLM.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
messages: List of message dicts with 'role' and 'content'
|
|
87
|
+
stream_options: Optional parameters
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Complete response text
|
|
91
|
+
"""
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
@abstractmethod
|
|
95
|
+
def is_healthy(self) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
Check if the provider is healthy and accessible.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if healthy, False otherwise
|
|
101
|
+
"""
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def provider_name(self) -> str:
|
|
107
|
+
"""
|
|
108
|
+
Get the name of this provider.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Provider name (e.g., "triton", "openai", "anthropic")
|
|
112
|
+
"""
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def model_name(self) -> str:
|
|
118
|
+
"""
|
|
119
|
+
Get the model name being used.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Model name/identifier
|
|
123
|
+
"""
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
def set_model(self, model_name: str):
|
|
127
|
+
"""
|
|
128
|
+
Set the model name dynamically (optional, for model switching).
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
model_name: New model name
|
|
132
|
+
|
|
133
|
+
Note:
|
|
134
|
+
Default implementation does nothing. Override in subclasses
|
|
135
|
+
that support model switching.
|
|
136
|
+
"""
|
|
137
|
+
pass
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Provider factory with automatic provider discovery."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
import pkgutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Type
|
|
8
|
+
|
|
9
|
+
from .base import ProviderAdapter, ProviderMetadata
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProviderFactory:
|
|
13
|
+
"""
|
|
14
|
+
Factory for creating provider adapter instances with automatic discovery.
|
|
15
|
+
|
|
16
|
+
This factory automatically discovers all provider classes in the providers
|
|
17
|
+
directory that inherit from ProviderAdapter and have METADATA defined.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_provider_registry: Dict[str, Type[ProviderAdapter]] = {}
|
|
21
|
+
_discovered = False
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def _discover_providers(cls):
|
|
25
|
+
"""
|
|
26
|
+
Discover all provider classes in the providers directory.
|
|
27
|
+
|
|
28
|
+
This method scans the providers package for classes that:
|
|
29
|
+
1. Inherit from ProviderAdapter
|
|
30
|
+
2. Are not the base ProviderAdapter class itself
|
|
31
|
+
3. Have a METADATA attribute defined
|
|
32
|
+
"""
|
|
33
|
+
if cls._discovered:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
# Get the directory containing provider modules
|
|
37
|
+
providers_dir = Path(__file__).parent
|
|
38
|
+
|
|
39
|
+
# Scan all Python files in the providers directory
|
|
40
|
+
for module_info in pkgutil.iter_modules([str(providers_dir)]):
|
|
41
|
+
module_name = module_info.name
|
|
42
|
+
|
|
43
|
+
# Skip special files
|
|
44
|
+
if module_name in ("base", "factory", "__init__"):
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
# Import the module
|
|
49
|
+
module = importlib.import_module(
|
|
50
|
+
f"kubrick_cli.providers.{module_name}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Find all classes in the module
|
|
54
|
+
for name, obj in inspect.getmembers(module, inspect.isclass):
|
|
55
|
+
# Check if it's a ProviderAdapter subclass (but not the base class)
|
|
56
|
+
if (
|
|
57
|
+
issubclass(obj, ProviderAdapter)
|
|
58
|
+
and obj is not ProviderAdapter
|
|
59
|
+
and hasattr(obj, "METADATA")
|
|
60
|
+
and obj.METADATA is not None
|
|
61
|
+
):
|
|
62
|
+
# Register the provider by its metadata name
|
|
63
|
+
provider_name = obj.METADATA.name.lower()
|
|
64
|
+
cls._provider_registry[provider_name] = obj
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
# Log the error but continue discovering other providers
|
|
68
|
+
print(
|
|
69
|
+
f"Warning: Failed to load provider from {module_name}: {e}"
|
|
70
|
+
)
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
cls._discovered = True
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def create_provider(cls, config: Dict) -> ProviderAdapter:
|
|
77
|
+
"""
|
|
78
|
+
Create a provider instance based on configuration.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
config: Configuration dictionary containing provider settings
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
ProviderAdapter instance
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ValueError: If provider is invalid or required credentials are missing
|
|
88
|
+
"""
|
|
89
|
+
# Ensure providers are discovered
|
|
90
|
+
cls._discover_providers()
|
|
91
|
+
|
|
92
|
+
provider_name = config.get("provider", "triton").lower()
|
|
93
|
+
|
|
94
|
+
if provider_name not in cls._provider_registry:
|
|
95
|
+
available = ", ".join(cls._provider_registry.keys())
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Unknown provider: {provider_name}. "
|
|
98
|
+
f"Available providers: {available}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Get the provider class
|
|
102
|
+
provider_class = cls._provider_registry[provider_name]
|
|
103
|
+
metadata = provider_class.METADATA
|
|
104
|
+
|
|
105
|
+
# Extract configuration values needed for this provider
|
|
106
|
+
provider_config = {}
|
|
107
|
+
for field in metadata.config_fields:
|
|
108
|
+
key = field["key"]
|
|
109
|
+
value = config.get(key)
|
|
110
|
+
|
|
111
|
+
# Check for required fields (no default value)
|
|
112
|
+
if value is None and "default" not in field:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"{metadata.display_name} configuration missing required field: '{key}'. "
|
|
115
|
+
f"Please run setup wizard or add '{key}' to config."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Use provided value or default
|
|
119
|
+
provider_config[key] = value if value is not None else field.get("default")
|
|
120
|
+
|
|
121
|
+
# Instantiate the provider
|
|
122
|
+
# We need to map config keys to constructor parameters
|
|
123
|
+
# This is done by inspecting the __init__ signature
|
|
124
|
+
init_signature = inspect.signature(provider_class.__init__)
|
|
125
|
+
init_params = {}
|
|
126
|
+
|
|
127
|
+
for param_name, param in init_signature.parameters.items():
|
|
128
|
+
if param_name == "self":
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
# Try to find a matching config field
|
|
132
|
+
for field in metadata.config_fields:
|
|
133
|
+
# Match based on key name or parameter name
|
|
134
|
+
if field["key"] == param_name or field["key"].endswith(f"_{param_name}"):
|
|
135
|
+
init_params[param_name] = provider_config[field["key"]]
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
return provider_class(**init_params)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def list_available_providers(cls) -> List[ProviderMetadata]:
|
|
142
|
+
"""
|
|
143
|
+
Get list of available providers with their metadata.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of ProviderMetadata objects
|
|
147
|
+
"""
|
|
148
|
+
cls._discover_providers()
|
|
149
|
+
|
|
150
|
+
providers = []
|
|
151
|
+
for provider_class in cls._provider_registry.values():
|
|
152
|
+
if provider_class.METADATA:
|
|
153
|
+
providers.append(provider_class.METADATA)
|
|
154
|
+
|
|
155
|
+
# Sort with Triton first (default), then others alphabetically
|
|
156
|
+
def sort_key(p):
|
|
157
|
+
# Triton comes first (returns 0), others sorted alphabetically (returns name)
|
|
158
|
+
return (0, "") if p.name == "triton" else (1, p.name)
|
|
159
|
+
|
|
160
|
+
providers.sort(key=sort_key)
|
|
161
|
+
return providers
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def get_provider_metadata(cls, provider_name: str) -> ProviderMetadata:
|
|
165
|
+
"""
|
|
166
|
+
Get metadata for a specific provider.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
provider_name: Name of the provider
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
ProviderMetadata object
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
ValueError: If provider not found
|
|
176
|
+
"""
|
|
177
|
+
cls._discover_providers()
|
|
178
|
+
|
|
179
|
+
provider_name = provider_name.lower()
|
|
180
|
+
if provider_name not in cls._provider_registry:
|
|
181
|
+
raise ValueError(f"Provider not found: {provider_name}")
|
|
182
|
+
|
|
183
|
+
return cls._provider_registry[provider_name].METADATA
|