omnimancer-cli 0.1.0__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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1889 @@
|
|
|
1
|
+
"""Core data models and interfaces for Omnimancer CLI."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, field_validator
|
|
9
|
+
|
|
10
|
+
from ..utils.errors import MCPError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MessageRole(Enum):
|
|
14
|
+
"""Message roles in conversation."""
|
|
15
|
+
|
|
16
|
+
USER = "user"
|
|
17
|
+
ASSISTANT = "assistant"
|
|
18
|
+
SYSTEM = "system"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ChatMessage:
|
|
23
|
+
"""Represents a single chat message."""
|
|
24
|
+
|
|
25
|
+
role: MessageRole
|
|
26
|
+
content: str
|
|
27
|
+
timestamp: datetime
|
|
28
|
+
model_used: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ChatContext:
|
|
33
|
+
"""Maintains conversation context."""
|
|
34
|
+
|
|
35
|
+
messages: List[ChatMessage]
|
|
36
|
+
current_model: str
|
|
37
|
+
session_id: str
|
|
38
|
+
max_context_length: int = 4000
|
|
39
|
+
|
|
40
|
+
def add_message(self, message: ChatMessage) -> None:
|
|
41
|
+
"""Add a message to the context."""
|
|
42
|
+
self.messages.append(message)
|
|
43
|
+
|
|
44
|
+
# Trim context if it gets too long
|
|
45
|
+
if len(self.messages) > self.max_context_length:
|
|
46
|
+
# Keep system messages and trim older user/assistant messages
|
|
47
|
+
system_messages = [
|
|
48
|
+
msg for msg in self.messages if msg.role == MessageRole.SYSTEM
|
|
49
|
+
]
|
|
50
|
+
other_messages = [
|
|
51
|
+
msg for msg in self.messages if msg.role != MessageRole.SYSTEM
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Keep the most recent messages
|
|
55
|
+
keep_count = self.max_context_length - len(system_messages)
|
|
56
|
+
if keep_count > 0:
|
|
57
|
+
other_messages = other_messages[-keep_count:]
|
|
58
|
+
|
|
59
|
+
self.messages = system_messages + other_messages
|
|
60
|
+
|
|
61
|
+
def clear(self) -> None:
|
|
62
|
+
"""Clear all messages from the context."""
|
|
63
|
+
self.messages.clear()
|
|
64
|
+
|
|
65
|
+
def get_context_for_api(self) -> List[Dict[str, str]]:
|
|
66
|
+
"""Get messages formatted for API calls."""
|
|
67
|
+
return [
|
|
68
|
+
{"role": msg.role.value, "content": msg.content} for msg in self.messages
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class ToolDefinition:
|
|
74
|
+
"""Definition of a tool that can be called by AI providers."""
|
|
75
|
+
|
|
76
|
+
name: str
|
|
77
|
+
description: str
|
|
78
|
+
parameters: Dict[str, Any]
|
|
79
|
+
server_name: Optional[str] = None
|
|
80
|
+
auto_approved: bool = False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class ToolCall:
|
|
85
|
+
"""Represents a tool call made by an AI provider."""
|
|
86
|
+
|
|
87
|
+
name: str
|
|
88
|
+
arguments: Dict[str, Any]
|
|
89
|
+
server_name: Optional[str] = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class ToolResult:
|
|
94
|
+
"""Result of a tool execution."""
|
|
95
|
+
|
|
96
|
+
content: str
|
|
97
|
+
error: Optional[str] = None
|
|
98
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class ChatResponse:
|
|
103
|
+
"""Response from an AI provider."""
|
|
104
|
+
|
|
105
|
+
content: str
|
|
106
|
+
model_used: str
|
|
107
|
+
tokens_used: int
|
|
108
|
+
cost_estimate: Optional[float] = None
|
|
109
|
+
timestamp: Optional[datetime] = None
|
|
110
|
+
error: Optional[str] = None
|
|
111
|
+
tool_calls: Optional[List[ToolCall]] = None
|
|
112
|
+
tool_results: Optional[List[ToolResult]] = None
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def is_success(self) -> bool:
|
|
116
|
+
"""Check if the response was successful."""
|
|
117
|
+
return self.error is None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class ModelInfo:
|
|
122
|
+
"""Information about an AI model."""
|
|
123
|
+
|
|
124
|
+
name: str
|
|
125
|
+
provider: str
|
|
126
|
+
description: str
|
|
127
|
+
max_tokens: int
|
|
128
|
+
cost_per_token: float
|
|
129
|
+
available: bool
|
|
130
|
+
supports_tools: bool = False
|
|
131
|
+
supports_multimodal: bool = False
|
|
132
|
+
latest_version: bool = False
|
|
133
|
+
deprecated: bool = False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class EnhancedModelInfo:
|
|
138
|
+
"""Extended model information with SWE scores and pricing."""
|
|
139
|
+
|
|
140
|
+
name: str
|
|
141
|
+
provider: str
|
|
142
|
+
description: str
|
|
143
|
+
max_tokens: int
|
|
144
|
+
cost_per_million_input: float
|
|
145
|
+
cost_per_million_output: float
|
|
146
|
+
swe_score: Optional[float] = None
|
|
147
|
+
swe_rating: Optional[str] = None # ★★★, ★★☆, ★☆☆
|
|
148
|
+
available: bool = True
|
|
149
|
+
supports_tools: bool = False
|
|
150
|
+
supports_multimodal: bool = False
|
|
151
|
+
latest_version: bool = False
|
|
152
|
+
deprecated: bool = False
|
|
153
|
+
release_date: Optional[datetime] = None
|
|
154
|
+
context_window: int = 4096
|
|
155
|
+
is_free: bool = False
|
|
156
|
+
|
|
157
|
+
def get_cost_display(self) -> str:
|
|
158
|
+
"""Get formatted cost display string."""
|
|
159
|
+
if self.is_free:
|
|
160
|
+
return "Free"
|
|
161
|
+
return f"${self.cost_per_million_input:.2f} in, ${self.cost_per_million_output:.2f} out"
|
|
162
|
+
|
|
163
|
+
def get_swe_display(self) -> str:
|
|
164
|
+
"""Get formatted SWE score display."""
|
|
165
|
+
if self.swe_score is None:
|
|
166
|
+
return "N/A"
|
|
167
|
+
return f"{self.swe_score:.1f}% {self.swe_rating or ''}"
|
|
168
|
+
|
|
169
|
+
def get_swe_rating(self) -> str:
|
|
170
|
+
"""Calculate SWE rating based on score."""
|
|
171
|
+
if self.swe_score is None:
|
|
172
|
+
return ""
|
|
173
|
+
elif self.swe_score >= 60:
|
|
174
|
+
return "★★★"
|
|
175
|
+
elif self.swe_score >= 40:
|
|
176
|
+
return "★★☆"
|
|
177
|
+
else:
|
|
178
|
+
return "★☆☆"
|
|
179
|
+
|
|
180
|
+
def get_cost_tier(self) -> str:
|
|
181
|
+
"""Get cost tier indicator."""
|
|
182
|
+
if self.is_free:
|
|
183
|
+
return "Free"
|
|
184
|
+
|
|
185
|
+
# Calculate average cost per million tokens
|
|
186
|
+
avg_cost = (self.cost_per_million_input + self.cost_per_million_output) / 2
|
|
187
|
+
|
|
188
|
+
if avg_cost <= 1.0:
|
|
189
|
+
return "💰"
|
|
190
|
+
elif avg_cost <= 10.0:
|
|
191
|
+
return "💰💰"
|
|
192
|
+
else:
|
|
193
|
+
return "💰💰💰"
|
|
194
|
+
|
|
195
|
+
def validate_pricing(self) -> bool:
|
|
196
|
+
"""Validate pricing data."""
|
|
197
|
+
if self.is_free:
|
|
198
|
+
return (
|
|
199
|
+
self.cost_per_million_input == 0 and self.cost_per_million_output == 0
|
|
200
|
+
)
|
|
201
|
+
return self.cost_per_million_input >= 0 and self.cost_per_million_output >= 0
|
|
202
|
+
|
|
203
|
+
def validate_swe_score(self) -> bool:
|
|
204
|
+
"""Validate SWE score data."""
|
|
205
|
+
if self.swe_score is None:
|
|
206
|
+
return True
|
|
207
|
+
return 0 <= self.swe_score <= 100
|
|
208
|
+
|
|
209
|
+
def update_swe_rating(self) -> None:
|
|
210
|
+
"""Update SWE rating based on current score."""
|
|
211
|
+
self.swe_rating = self.get_swe_rating()
|
|
212
|
+
|
|
213
|
+
def to_model_info(self) -> ModelInfo:
|
|
214
|
+
"""Convert to legacy ModelInfo for backward compatibility."""
|
|
215
|
+
# Calculate legacy cost_per_token (average of input/output per million / 1M)
|
|
216
|
+
if self.is_free:
|
|
217
|
+
cost_per_token = 0.0
|
|
218
|
+
else:
|
|
219
|
+
cost_per_token = (
|
|
220
|
+
(self.cost_per_million_input + self.cost_per_million_output)
|
|
221
|
+
/ 2
|
|
222
|
+
/ 1_000_000
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return ModelInfo(
|
|
226
|
+
name=self.name,
|
|
227
|
+
provider=self.provider,
|
|
228
|
+
description=self.description,
|
|
229
|
+
max_tokens=self.max_tokens,
|
|
230
|
+
cost_per_token=cost_per_token,
|
|
231
|
+
available=self.available,
|
|
232
|
+
supports_tools=self.supports_tools,
|
|
233
|
+
supports_multimodal=self.supports_multimodal,
|
|
234
|
+
latest_version=self.latest_version,
|
|
235
|
+
deprecated=self.deprecated,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
def from_model_info(cls, model_info: ModelInfo, **kwargs) -> "EnhancedModelInfo":
|
|
240
|
+
"""Create EnhancedModelInfo from legacy ModelInfo."""
|
|
241
|
+
# Convert legacy cost_per_token to per-million costs
|
|
242
|
+
cost_per_million = model_info.cost_per_token * 1_000_000
|
|
243
|
+
|
|
244
|
+
return cls(
|
|
245
|
+
name=model_info.name,
|
|
246
|
+
provider=model_info.provider,
|
|
247
|
+
description=model_info.description,
|
|
248
|
+
max_tokens=model_info.max_tokens,
|
|
249
|
+
cost_per_million_input=cost_per_million,
|
|
250
|
+
cost_per_million_output=cost_per_million,
|
|
251
|
+
available=model_info.available,
|
|
252
|
+
supports_tools=model_info.supports_tools,
|
|
253
|
+
supports_multimodal=model_info.supports_multimodal,
|
|
254
|
+
latest_version=model_info.latest_version,
|
|
255
|
+
deprecated=model_info.deprecated,
|
|
256
|
+
context_window=model_info.max_tokens,
|
|
257
|
+
is_free=model_info.cost_per_token == 0,
|
|
258
|
+
**kwargs,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class ChatSettings(BaseModel):
|
|
263
|
+
"""Chat-specific settings."""
|
|
264
|
+
|
|
265
|
+
max_tokens: Optional[int] = None
|
|
266
|
+
temperature: Optional[float] = None
|
|
267
|
+
context_length: int = 4000
|
|
268
|
+
save_history: bool = True
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class ProviderConfig(BaseModel):
|
|
272
|
+
"""Configuration for a specific AI provider."""
|
|
273
|
+
|
|
274
|
+
api_key: Optional[str] = (
|
|
275
|
+
None # Optional for providers like Ollama that don't need keys
|
|
276
|
+
)
|
|
277
|
+
model: str
|
|
278
|
+
max_tokens: Optional[int] = None
|
|
279
|
+
temperature: Optional[float] = None
|
|
280
|
+
|
|
281
|
+
# Provider-specific settings
|
|
282
|
+
base_url: Optional[str] = None # For custom endpoints (OpenAI-compatible, Ollama)
|
|
283
|
+
organization: Optional[str] = None # For OpenAI organizations
|
|
284
|
+
project_id: Optional[str] = None # For Google Cloud projects
|
|
285
|
+
timeout: Optional[float] = None # Request timeout in seconds
|
|
286
|
+
|
|
287
|
+
# Advanced settings
|
|
288
|
+
top_p: Optional[float] = None # Nucleus sampling parameter
|
|
289
|
+
frequency_penalty: Optional[float] = None # Frequency penalty (OpenAI)
|
|
290
|
+
presence_penalty: Optional[float] = None # Presence penalty (OpenAI)
|
|
291
|
+
|
|
292
|
+
# Gemini-specific settings
|
|
293
|
+
safety_settings: Optional[Dict[str, str]] = None # Gemini safety settings
|
|
294
|
+
generation_config: Optional[Dict[str, Any]] = None # Gemini generation config
|
|
295
|
+
vertex_ai_project: Optional[str] = None # Vertex AI project ID
|
|
296
|
+
vertex_ai_location: Optional[str] = None # Vertex AI location
|
|
297
|
+
service_account_path: Optional[str] = None # Path to service account JSON
|
|
298
|
+
|
|
299
|
+
# Cohere-specific settings
|
|
300
|
+
max_input_tokens: Optional[int] = None # Cohere input token limit
|
|
301
|
+
connectors: Optional[List[Dict[str, Any]]] = None # Cohere connectors for RAG
|
|
302
|
+
chat_history: Optional[List[Dict[str, str]]] = None # Cohere chat history
|
|
303
|
+
documents: Optional[List[Dict[str, Any]]] = None # Cohere documents for RAG
|
|
304
|
+
preamble: Optional[str] = None # Cohere preamble for conversation context
|
|
305
|
+
k: Optional[int] = None # Cohere k parameter for RAG
|
|
306
|
+
|
|
307
|
+
# Ollama-specific settings
|
|
308
|
+
num_predict: Optional[int] = None # Number of tokens to predict
|
|
309
|
+
num_ctx: Optional[int] = None # Context window size
|
|
310
|
+
repeat_penalty: Optional[float] = None # Repetition penalty
|
|
311
|
+
seed: Optional[int] = None # Random seed for reproducibility
|
|
312
|
+
stop: Optional[List[str]] = None # Stop sequences
|
|
313
|
+
mirostat: Optional[int] = None # Mirostat sampling
|
|
314
|
+
mirostat_eta: Optional[float] = None # Mirostat learning rate
|
|
315
|
+
mirostat_tau: Optional[float] = None # Mirostat target entropy
|
|
316
|
+
num_gpu: Optional[int] = None # Number of GPU layers
|
|
317
|
+
num_thread: Optional[int] = None # Number of threads
|
|
318
|
+
|
|
319
|
+
# Claude-specific settings
|
|
320
|
+
anthropic_version: Optional[str] = None # Anthropic API version
|
|
321
|
+
system_prompt: Optional[str] = None # System prompt for Claude
|
|
322
|
+
|
|
323
|
+
# OpenAI-specific settings
|
|
324
|
+
response_format: Optional[Dict[str, str]] = (
|
|
325
|
+
None # Response format (e.g., json_object)
|
|
326
|
+
)
|
|
327
|
+
logit_bias: Optional[Dict[int, float]] = None # Token biasing
|
|
328
|
+
|
|
329
|
+
# Provider capabilities
|
|
330
|
+
supports_tools: bool = False # Whether provider supports function calling
|
|
331
|
+
supports_multimodal: bool = False # Whether provider supports images/files
|
|
332
|
+
supports_streaming: bool = True # Whether provider supports streaming responses
|
|
333
|
+
supports_system_messages: bool = True # Whether provider supports system messages
|
|
334
|
+
supports_function_calling: bool = False # Alias for supports_tools for clarity
|
|
335
|
+
supports_vision: bool = False # Whether provider supports image analysis
|
|
336
|
+
supports_json_mode: bool = False # Whether provider supports JSON mode
|
|
337
|
+
|
|
338
|
+
# Configuration metadata
|
|
339
|
+
provider_type: Optional[str] = None # Provider type identifier
|
|
340
|
+
config_version: str = "2.0" # Configuration schema version
|
|
341
|
+
enabled: bool = True # Whether this provider is enabled
|
|
342
|
+
priority: int = 0 # Provider priority for fallback ordering
|
|
343
|
+
|
|
344
|
+
# Rate limiting and retry settings
|
|
345
|
+
rate_limit_requests_per_minute: Optional[int] = None
|
|
346
|
+
rate_limit_tokens_per_minute: Optional[int] = None
|
|
347
|
+
max_retries: int = 3
|
|
348
|
+
retry_delay: float = 1.0
|
|
349
|
+
exponential_backoff: bool = True
|
|
350
|
+
|
|
351
|
+
# Health check settings
|
|
352
|
+
health_check_enabled: bool = True
|
|
353
|
+
health_check_interval: int = 300 # seconds
|
|
354
|
+
health_check_timeout: float = 10.0 # seconds
|
|
355
|
+
|
|
356
|
+
# Custom headers and authentication
|
|
357
|
+
custom_headers: Optional[Dict[str, str]] = None
|
|
358
|
+
auth_type: str = "api_key" # api_key, bearer, oauth, service_account, none
|
|
359
|
+
oauth_config: Optional[Dict[str, Any]] = None # OAuth configuration
|
|
360
|
+
|
|
361
|
+
# Perplexity-specific settings
|
|
362
|
+
search_enabled: Optional[bool] = None # Enable web search for Perplexity
|
|
363
|
+
search_recency_filter: Optional[str] = None # hour, day, week, month, year
|
|
364
|
+
return_citations: Optional[bool] = None # Return citations in response
|
|
365
|
+
return_images: Optional[bool] = None # Return images in response
|
|
366
|
+
return_related_questions: Optional[bool] = None # Return related questions
|
|
367
|
+
|
|
368
|
+
# xAI-specific settings
|
|
369
|
+
grok_mode: Optional[str] = None # balanced, creative, precise
|
|
370
|
+
enable_web_search: Optional[bool] = None # Enable web search for Grok
|
|
371
|
+
enable_real_time: Optional[bool] = None # Enable real-time information
|
|
372
|
+
|
|
373
|
+
# Mistral-specific settings
|
|
374
|
+
safe_prompt: Optional[bool] = None # Enable safe prompt mode
|
|
375
|
+
random_seed: Optional[int] = None # Random seed for reproducibility
|
|
376
|
+
top_k: Optional[int] = None # Top-k sampling parameter
|
|
377
|
+
|
|
378
|
+
# Azure-specific settings
|
|
379
|
+
azure_endpoint: Optional[str] = None # Azure OpenAI endpoint
|
|
380
|
+
azure_deployment: Optional[str] = None # Azure deployment name
|
|
381
|
+
api_version: Optional[str] = None # Azure API version
|
|
382
|
+
|
|
383
|
+
# Vertex AI-specific settings
|
|
384
|
+
vertex_project: Optional[str] = None # Google Cloud project ID
|
|
385
|
+
vertex_location: Optional[str] = None # Vertex AI location
|
|
386
|
+
vertex_credentials_path: Optional[str] = None # Path to service account JSON
|
|
387
|
+
|
|
388
|
+
# AWS Bedrock-specific settings
|
|
389
|
+
aws_region: Optional[str] = None # AWS region
|
|
390
|
+
aws_access_key_id: Optional[str] = None # AWS access key ID
|
|
391
|
+
aws_secret_access_key: Optional[str] = None # AWS secret access key
|
|
392
|
+
aws_session_token: Optional[str] = None # AWS session token
|
|
393
|
+
|
|
394
|
+
# OpenRouter-specific settings
|
|
395
|
+
openrouter_referrer: Optional[str] = None # HTTP referrer for OpenRouter
|
|
396
|
+
openrouter_title: Optional[str] = None # Application title for OpenRouter
|
|
397
|
+
enable_fallback: Optional[bool] = None # Enable model fallback
|
|
398
|
+
max_cost_per_token: Optional[float] = None # Maximum cost per token
|
|
399
|
+
prefer_cheaper_models: Optional[bool] = None # Prefer cheaper models
|
|
400
|
+
|
|
401
|
+
# Claude-code specific settings
|
|
402
|
+
claude_code_mode: Optional[str] = None # opus, sonnet
|
|
403
|
+
claude_code_path: Optional[str] = None # Path to claude-code executable
|
|
404
|
+
working_directory: Optional[str] = None # Working directory for claude-code
|
|
405
|
+
|
|
406
|
+
# Provider-specific extra settings (for extensibility)
|
|
407
|
+
extra_settings: Optional[Dict[str, Any]] = None
|
|
408
|
+
|
|
409
|
+
@field_validator("temperature")
|
|
410
|
+
@classmethod
|
|
411
|
+
def validate_temperature(cls, v):
|
|
412
|
+
if v is not None and (v < 0 or v > 2):
|
|
413
|
+
raise ValueError("temperature must be between 0 and 2")
|
|
414
|
+
return v
|
|
415
|
+
|
|
416
|
+
@field_validator("top_p")
|
|
417
|
+
@classmethod
|
|
418
|
+
def validate_top_p(cls, v):
|
|
419
|
+
if v is not None and (v < 0 or v > 1):
|
|
420
|
+
raise ValueError("top_p must be between 0 and 1")
|
|
421
|
+
return v
|
|
422
|
+
|
|
423
|
+
@field_validator("frequency_penalty")
|
|
424
|
+
@classmethod
|
|
425
|
+
def validate_frequency_penalty(cls, v):
|
|
426
|
+
if v is not None and (v < -2 or v > 2):
|
|
427
|
+
raise ValueError("frequency_penalty must be between -2 and 2")
|
|
428
|
+
return v
|
|
429
|
+
|
|
430
|
+
@field_validator("presence_penalty")
|
|
431
|
+
@classmethod
|
|
432
|
+
def validate_presence_penalty(cls, v):
|
|
433
|
+
if v is not None and (v < -2 or v > 2):
|
|
434
|
+
raise ValueError("presence_penalty must be between -2 and 2")
|
|
435
|
+
return v
|
|
436
|
+
|
|
437
|
+
@field_validator("timeout")
|
|
438
|
+
@classmethod
|
|
439
|
+
def validate_timeout(cls, v):
|
|
440
|
+
if v is not None and v <= 0:
|
|
441
|
+
raise ValueError("timeout must be positive")
|
|
442
|
+
return v
|
|
443
|
+
|
|
444
|
+
@field_validator("repeat_penalty")
|
|
445
|
+
@classmethod
|
|
446
|
+
def validate_repeat_penalty(cls, v):
|
|
447
|
+
if v is not None and (v < 0.1 or v > 2.0):
|
|
448
|
+
raise ValueError("repeat_penalty must be between 0.1 and 2.0")
|
|
449
|
+
return v
|
|
450
|
+
|
|
451
|
+
@field_validator("max_input_tokens")
|
|
452
|
+
@classmethod
|
|
453
|
+
def validate_max_input_tokens(cls, v):
|
|
454
|
+
if v is not None and v <= 0:
|
|
455
|
+
raise ValueError("max_input_tokens must be positive")
|
|
456
|
+
return v
|
|
457
|
+
|
|
458
|
+
@field_validator("num_predict")
|
|
459
|
+
@classmethod
|
|
460
|
+
def validate_num_predict(cls, v):
|
|
461
|
+
if v is not None and v <= 0:
|
|
462
|
+
raise ValueError("num_predict must be positive")
|
|
463
|
+
return v
|
|
464
|
+
|
|
465
|
+
@field_validator("num_ctx")
|
|
466
|
+
@classmethod
|
|
467
|
+
def validate_num_ctx(cls, v):
|
|
468
|
+
if v is not None and v <= 0:
|
|
469
|
+
raise ValueError("num_ctx must be positive")
|
|
470
|
+
return v
|
|
471
|
+
|
|
472
|
+
@field_validator("seed")
|
|
473
|
+
@classmethod
|
|
474
|
+
def validate_seed(cls, v):
|
|
475
|
+
if v is not None and (v < 0 or v > 2**32 - 1):
|
|
476
|
+
raise ValueError("seed must be between 0 and 2^32-1")
|
|
477
|
+
return v
|
|
478
|
+
|
|
479
|
+
@field_validator("priority")
|
|
480
|
+
@classmethod
|
|
481
|
+
def validate_priority(cls, v):
|
|
482
|
+
if v < 0:
|
|
483
|
+
raise ValueError("priority must be non-negative")
|
|
484
|
+
return v
|
|
485
|
+
|
|
486
|
+
@field_validator("max_retries")
|
|
487
|
+
@classmethod
|
|
488
|
+
def validate_max_retries(cls, v):
|
|
489
|
+
if v < 0:
|
|
490
|
+
raise ValueError("max_retries must be non-negative")
|
|
491
|
+
return v
|
|
492
|
+
|
|
493
|
+
@field_validator("retry_delay")
|
|
494
|
+
@classmethod
|
|
495
|
+
def validate_retry_delay(cls, v):
|
|
496
|
+
if v < 0:
|
|
497
|
+
raise ValueError("retry_delay must be non-negative")
|
|
498
|
+
return v
|
|
499
|
+
|
|
500
|
+
@field_validator("health_check_interval")
|
|
501
|
+
@classmethod
|
|
502
|
+
def validate_health_check_interval(cls, v):
|
|
503
|
+
if v <= 0:
|
|
504
|
+
raise ValueError("health_check_interval must be positive")
|
|
505
|
+
return v
|
|
506
|
+
|
|
507
|
+
@field_validator("health_check_timeout")
|
|
508
|
+
@classmethod
|
|
509
|
+
def validate_health_check_timeout(cls, v):
|
|
510
|
+
if v <= 0:
|
|
511
|
+
raise ValueError("health_check_timeout must be positive")
|
|
512
|
+
return v
|
|
513
|
+
|
|
514
|
+
@field_validator("rate_limit_requests_per_minute")
|
|
515
|
+
@classmethod
|
|
516
|
+
def validate_rate_limit_requests(cls, v):
|
|
517
|
+
if v is not None and v <= 0:
|
|
518
|
+
raise ValueError("rate_limit_requests_per_minute must be positive")
|
|
519
|
+
return v
|
|
520
|
+
|
|
521
|
+
@field_validator("rate_limit_tokens_per_minute")
|
|
522
|
+
@classmethod
|
|
523
|
+
def validate_rate_limit_tokens(cls, v):
|
|
524
|
+
if v is not None and v <= 0:
|
|
525
|
+
raise ValueError("rate_limit_tokens_per_minute must be positive")
|
|
526
|
+
return v
|
|
527
|
+
|
|
528
|
+
@field_validator("auth_type")
|
|
529
|
+
@classmethod
|
|
530
|
+
def validate_auth_type(cls, v):
|
|
531
|
+
valid_types = ["api_key", "bearer", "oauth", "service_account", "none"]
|
|
532
|
+
if v not in valid_types:
|
|
533
|
+
raise ValueError(f'auth_type must be one of: {", ".join(valid_types)}')
|
|
534
|
+
return v
|
|
535
|
+
|
|
536
|
+
@field_validator("mirostat")
|
|
537
|
+
@classmethod
|
|
538
|
+
def validate_mirostat(cls, v):
|
|
539
|
+
if v is not None and v not in [0, 1, 2]:
|
|
540
|
+
raise ValueError("mirostat must be 0, 1, or 2")
|
|
541
|
+
return v
|
|
542
|
+
|
|
543
|
+
@field_validator("mirostat_eta")
|
|
544
|
+
@classmethod
|
|
545
|
+
def validate_mirostat_eta(cls, v):
|
|
546
|
+
if v is not None and (v <= 0 or v > 1):
|
|
547
|
+
raise ValueError("mirostat_eta must be between 0 and 1")
|
|
548
|
+
return v
|
|
549
|
+
|
|
550
|
+
@field_validator("mirostat_tau")
|
|
551
|
+
@classmethod
|
|
552
|
+
def validate_mirostat_tau(cls, v):
|
|
553
|
+
if v is not None and v <= 0:
|
|
554
|
+
raise ValueError("mirostat_tau must be positive")
|
|
555
|
+
return v
|
|
556
|
+
|
|
557
|
+
@field_validator("num_gpu")
|
|
558
|
+
@classmethod
|
|
559
|
+
def validate_num_gpu(cls, v):
|
|
560
|
+
if v is not None and v < 0:
|
|
561
|
+
raise ValueError("num_gpu must be non-negative")
|
|
562
|
+
return v
|
|
563
|
+
|
|
564
|
+
@field_validator("num_thread")
|
|
565
|
+
@classmethod
|
|
566
|
+
def validate_num_thread(cls, v):
|
|
567
|
+
if v is not None and v <= 0:
|
|
568
|
+
raise ValueError("num_thread must be positive")
|
|
569
|
+
return v
|
|
570
|
+
|
|
571
|
+
@field_validator("search_recency_filter")
|
|
572
|
+
@classmethod
|
|
573
|
+
def validate_search_recency_filter(cls, v):
|
|
574
|
+
if v is not None:
|
|
575
|
+
valid_filters = ["hour", "day", "week", "month", "year"]
|
|
576
|
+
if v not in valid_filters:
|
|
577
|
+
raise ValueError(
|
|
578
|
+
f'search_recency_filter must be one of: {", ".join(valid_filters)}'
|
|
579
|
+
)
|
|
580
|
+
return v
|
|
581
|
+
|
|
582
|
+
@field_validator("grok_mode")
|
|
583
|
+
@classmethod
|
|
584
|
+
def validate_grok_mode(cls, v):
|
|
585
|
+
if v is not None:
|
|
586
|
+
valid_modes = ["balanced", "creative", "precise"]
|
|
587
|
+
if v not in valid_modes:
|
|
588
|
+
raise ValueError(f'grok_mode must be one of: {", ".join(valid_modes)}')
|
|
589
|
+
return v
|
|
590
|
+
|
|
591
|
+
@field_validator("random_seed")
|
|
592
|
+
@classmethod
|
|
593
|
+
def validate_random_seed(cls, v):
|
|
594
|
+
if v is not None and (v < 0 or v > 2**32 - 1):
|
|
595
|
+
raise ValueError("random_seed must be between 0 and 2^32-1")
|
|
596
|
+
return v
|
|
597
|
+
|
|
598
|
+
@field_validator("top_k")
|
|
599
|
+
@classmethod
|
|
600
|
+
def validate_top_k(cls, v):
|
|
601
|
+
if v is not None and v <= 0:
|
|
602
|
+
raise ValueError("top_k must be positive")
|
|
603
|
+
return v
|
|
604
|
+
|
|
605
|
+
@field_validator("azure_endpoint")
|
|
606
|
+
@classmethod
|
|
607
|
+
def validate_azure_endpoint(cls, v):
|
|
608
|
+
if v is not None and v.strip():
|
|
609
|
+
# Basic URL validation
|
|
610
|
+
if not (v.startswith("https://") or v.startswith("http://")):
|
|
611
|
+
# Allow endpoints without protocol, we'll add it in the provider
|
|
612
|
+
pass
|
|
613
|
+
return v
|
|
614
|
+
|
|
615
|
+
@field_validator("api_version")
|
|
616
|
+
@classmethod
|
|
617
|
+
def validate_api_version(cls, v):
|
|
618
|
+
if v is not None and not v.strip():
|
|
619
|
+
raise ValueError("api_version cannot be empty if provided")
|
|
620
|
+
return v
|
|
621
|
+
|
|
622
|
+
@field_validator("vertex_location")
|
|
623
|
+
@classmethod
|
|
624
|
+
def validate_vertex_location(cls, v):
|
|
625
|
+
if v is not None and not v.strip():
|
|
626
|
+
raise ValueError("vertex_location cannot be empty if provided")
|
|
627
|
+
return v
|
|
628
|
+
|
|
629
|
+
@field_validator("aws_region")
|
|
630
|
+
@classmethod
|
|
631
|
+
def validate_aws_region(cls, v):
|
|
632
|
+
if v is not None and not v.strip():
|
|
633
|
+
raise ValueError("aws_region cannot be empty if provided")
|
|
634
|
+
return v
|
|
635
|
+
|
|
636
|
+
@field_validator("max_cost_per_token")
|
|
637
|
+
@classmethod
|
|
638
|
+
def validate_max_cost_per_token(cls, v):
|
|
639
|
+
if v is not None and v <= 0:
|
|
640
|
+
raise ValueError("max_cost_per_token must be positive")
|
|
641
|
+
return v
|
|
642
|
+
|
|
643
|
+
@field_validator("claude_code_mode")
|
|
644
|
+
@classmethod
|
|
645
|
+
def validate_claude_code_mode(cls, v):
|
|
646
|
+
if v is not None:
|
|
647
|
+
valid_modes = ["opus", "sonnet"]
|
|
648
|
+
if v not in valid_modes:
|
|
649
|
+
raise ValueError(
|
|
650
|
+
f'claude_code_mode must be one of: {", ".join(valid_modes)}'
|
|
651
|
+
)
|
|
652
|
+
return v
|
|
653
|
+
|
|
654
|
+
def get_provider_specific_config(self) -> Dict[str, Any]:
|
|
655
|
+
"""Get provider-specific configuration as a dictionary."""
|
|
656
|
+
config = {}
|
|
657
|
+
|
|
658
|
+
# Add non-None values to config
|
|
659
|
+
for field_name, field_value in self.model_dump().items():
|
|
660
|
+
if field_value is not None and field_name not in [
|
|
661
|
+
"api_key",
|
|
662
|
+
"model",
|
|
663
|
+
]:
|
|
664
|
+
config[field_name] = field_value
|
|
665
|
+
|
|
666
|
+
return config
|
|
667
|
+
|
|
668
|
+
@classmethod
|
|
669
|
+
def get_provider_config_template(cls, provider_name: str) -> Dict[str, Any]:
|
|
670
|
+
"""
|
|
671
|
+
Get configuration template for a specific provider.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
provider_name: Name of the provider
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
Dictionary with provider-specific configuration template
|
|
678
|
+
"""
|
|
679
|
+
templates = {
|
|
680
|
+
"perplexity": {
|
|
681
|
+
"model": "llama-3.1-sonar-large-128k-online",
|
|
682
|
+
"api_key": "your-perplexity-api-key",
|
|
683
|
+
"search_enabled": True,
|
|
684
|
+
"search_recency_filter": "month",
|
|
685
|
+
"return_citations": True,
|
|
686
|
+
"return_images": False,
|
|
687
|
+
"return_related_questions": False,
|
|
688
|
+
"temperature": 0.2,
|
|
689
|
+
"max_tokens": 4096,
|
|
690
|
+
},
|
|
691
|
+
"xai": {
|
|
692
|
+
"model": "grok-beta",
|
|
693
|
+
"api_key": "your-xai-api-key",
|
|
694
|
+
"grok_mode": "balanced",
|
|
695
|
+
"enable_web_search": True,
|
|
696
|
+
"enable_real_time": True,
|
|
697
|
+
"temperature": 0.7,
|
|
698
|
+
"max_tokens": 4096,
|
|
699
|
+
},
|
|
700
|
+
"mistral": {
|
|
701
|
+
"model": "mistral-large-latest",
|
|
702
|
+
"api_key": "your-mistral-api-key",
|
|
703
|
+
"safe_prompt": False,
|
|
704
|
+
"temperature": 0.7,
|
|
705
|
+
"top_p": 1.0,
|
|
706
|
+
"max_tokens": 4096,
|
|
707
|
+
},
|
|
708
|
+
"azure": {
|
|
709
|
+
"model": "gpt-4",
|
|
710
|
+
"api_key": "your-azure-api-key",
|
|
711
|
+
"azure_endpoint": "https://your-resource.openai.azure.com",
|
|
712
|
+
"azure_deployment": "your-deployment-name",
|
|
713
|
+
"api_version": "2024-02-15-preview",
|
|
714
|
+
"temperature": 0.7,
|
|
715
|
+
"max_tokens": 4096,
|
|
716
|
+
},
|
|
717
|
+
"vertex": {
|
|
718
|
+
"model": "gemini-1.5-pro",
|
|
719
|
+
"vertex_project": "your-gcp-project-id",
|
|
720
|
+
"vertex_location": "us-central1",
|
|
721
|
+
"service_account_path": "/path/to/service-account.json",
|
|
722
|
+
"temperature": 0.7,
|
|
723
|
+
"max_tokens": 8192,
|
|
724
|
+
},
|
|
725
|
+
"bedrock": {
|
|
726
|
+
"model": "anthropic.claude-3-sonnet-20240229-v1:0",
|
|
727
|
+
"aws_region": "us-east-1",
|
|
728
|
+
"aws_access_key_id": "your-aws-access-key",
|
|
729
|
+
"aws_secret_access_key": "your-aws-secret-key",
|
|
730
|
+
"temperature": 0.7,
|
|
731
|
+
"max_tokens": 4096,
|
|
732
|
+
},
|
|
733
|
+
"openrouter": {
|
|
734
|
+
"model": "anthropic/claude-3.5-sonnet",
|
|
735
|
+
"api_key": "your-openrouter-api-key",
|
|
736
|
+
"openrouter_referrer": "https://github.com/your-project",
|
|
737
|
+
"openrouter_title": "Your Application",
|
|
738
|
+
"enable_fallback": True,
|
|
739
|
+
"temperature": 0.7,
|
|
740
|
+
"max_tokens": 4096,
|
|
741
|
+
},
|
|
742
|
+
"claude-code": {
|
|
743
|
+
"model": "claude-code-sonnet",
|
|
744
|
+
"claude_code_mode": "sonnet",
|
|
745
|
+
"claude_code_path": "claude-code",
|
|
746
|
+
"working_directory": "/path/to/working/directory",
|
|
747
|
+
"temperature": 0.7,
|
|
748
|
+
"max_tokens": 4096,
|
|
749
|
+
},
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return templates.get(
|
|
753
|
+
provider_name,
|
|
754
|
+
{
|
|
755
|
+
"model": "default-model",
|
|
756
|
+
"api_key": "your-api-key",
|
|
757
|
+
"temperature": 0.7,
|
|
758
|
+
"max_tokens": 4096,
|
|
759
|
+
},
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
@classmethod
|
|
763
|
+
def get_all_provider_templates(cls) -> Dict[str, Dict[str, Any]]:
|
|
764
|
+
"""
|
|
765
|
+
Get configuration templates for all supported providers.
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
Dictionary mapping provider names to their configuration templates
|
|
769
|
+
"""
|
|
770
|
+
provider_names = [
|
|
771
|
+
"claude",
|
|
772
|
+
"openai",
|
|
773
|
+
"gemini",
|
|
774
|
+
"cohere",
|
|
775
|
+
"ollama",
|
|
776
|
+
"perplexity",
|
|
777
|
+
"xai",
|
|
778
|
+
"mistral",
|
|
779
|
+
"azure",
|
|
780
|
+
"vertex",
|
|
781
|
+
"bedrock",
|
|
782
|
+
"openrouter",
|
|
783
|
+
"claude-code",
|
|
784
|
+
]
|
|
785
|
+
|
|
786
|
+
templates = {}
|
|
787
|
+
for provider_name in provider_names:
|
|
788
|
+
templates[provider_name] = cls.get_provider_config_template(provider_name)
|
|
789
|
+
|
|
790
|
+
return templates
|
|
791
|
+
|
|
792
|
+
def __str__(self) -> str:
|
|
793
|
+
"""String representation with masked sensitive data."""
|
|
794
|
+
data = self.model_dump()
|
|
795
|
+
|
|
796
|
+
# Mask sensitive fields
|
|
797
|
+
sensitive_fields = ["api_key", "service_account_path", "oauth_config"]
|
|
798
|
+
for field in sensitive_fields:
|
|
799
|
+
if field in data and data[field]:
|
|
800
|
+
if isinstance(data[field], str):
|
|
801
|
+
data[field] = self._mask_string(data[field])
|
|
802
|
+
elif isinstance(data[field], dict):
|
|
803
|
+
data[field] = {
|
|
804
|
+
k: (self._mask_string(str(v)) if isinstance(v, str) else v)
|
|
805
|
+
for k, v in data[field].items()
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return f"ProviderConfig(model={data.get('model')}, provider_type={data.get('provider_type')}, api_key={'***masked***' if self.api_key else None})"
|
|
809
|
+
|
|
810
|
+
def __repr__(self) -> str:
|
|
811
|
+
"""Representation with masked sensitive data."""
|
|
812
|
+
return self.__str__()
|
|
813
|
+
|
|
814
|
+
@staticmethod
|
|
815
|
+
def _mask_string(value: str) -> str:
|
|
816
|
+
"""Mask a string value for security."""
|
|
817
|
+
if not value or len(value) < 8:
|
|
818
|
+
return "***masked***"
|
|
819
|
+
return f"{value[:4]}...{value[-4:]}"
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
class MCPServerConfig(BaseModel):
|
|
823
|
+
"""Configuration for an MCP server."""
|
|
824
|
+
|
|
825
|
+
name: str
|
|
826
|
+
command: str
|
|
827
|
+
args: List[str] = []
|
|
828
|
+
env: Dict[str, str] = {}
|
|
829
|
+
enabled: bool = True
|
|
830
|
+
auto_approve: List[str] = []
|
|
831
|
+
timeout: int = 30
|
|
832
|
+
|
|
833
|
+
@field_validator("name")
|
|
834
|
+
@classmethod
|
|
835
|
+
def validate_name(cls, v):
|
|
836
|
+
if not v or not v.strip():
|
|
837
|
+
raise ValueError("MCP server name cannot be empty")
|
|
838
|
+
return v.strip()
|
|
839
|
+
|
|
840
|
+
@field_validator("command")
|
|
841
|
+
@classmethod
|
|
842
|
+
def validate_command(cls, v):
|
|
843
|
+
if not v or not v.strip():
|
|
844
|
+
raise ValueError("MCP server command cannot be empty")
|
|
845
|
+
return v.strip()
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
class MCPConfig(BaseModel):
|
|
849
|
+
"""Configuration for MCP integration."""
|
|
850
|
+
|
|
851
|
+
enabled: bool = True
|
|
852
|
+
servers: Dict[str, MCPServerConfig] = {}
|
|
853
|
+
auto_approve_timeout: int = 30
|
|
854
|
+
max_concurrent_servers: int = 10
|
|
855
|
+
|
|
856
|
+
# Global MCP settings
|
|
857
|
+
default_timeout: int = 30
|
|
858
|
+
retry_attempts: int = 3
|
|
859
|
+
retry_delay: float = 1.0
|
|
860
|
+
|
|
861
|
+
# Security settings
|
|
862
|
+
allow_dangerous_tools: bool = False
|
|
863
|
+
sandbox_mode: bool = True
|
|
864
|
+
max_tool_execution_time: int = 300 # seconds
|
|
865
|
+
|
|
866
|
+
# Logging and monitoring
|
|
867
|
+
log_tool_calls: bool = True
|
|
868
|
+
log_level: str = "INFO"
|
|
869
|
+
metrics_enabled: bool = False
|
|
870
|
+
|
|
871
|
+
# Tool discovery and management
|
|
872
|
+
auto_discover_tools: bool = True
|
|
873
|
+
tool_cache_ttl: int = 3600 # seconds
|
|
874
|
+
refresh_tools_on_startup: bool = True
|
|
875
|
+
|
|
876
|
+
@field_validator("auto_approve_timeout")
|
|
877
|
+
@classmethod
|
|
878
|
+
def validate_auto_approve_timeout(cls, v):
|
|
879
|
+
if v is not None and v <= 0:
|
|
880
|
+
raise ValueError("auto_approve_timeout must be positive")
|
|
881
|
+
if v is not None and v > 120: # 2 minutes max
|
|
882
|
+
raise ValueError("auto_approve_timeout must be 120 seconds or less")
|
|
883
|
+
return v
|
|
884
|
+
|
|
885
|
+
@field_validator("max_concurrent_servers")
|
|
886
|
+
@classmethod
|
|
887
|
+
def validate_max_concurrent_servers(cls, v):
|
|
888
|
+
if v <= 0:
|
|
889
|
+
raise ValueError("max_concurrent_servers must be positive")
|
|
890
|
+
return v
|
|
891
|
+
|
|
892
|
+
@field_validator("default_timeout")
|
|
893
|
+
@classmethod
|
|
894
|
+
def validate_default_timeout(cls, v):
|
|
895
|
+
if v <= 0:
|
|
896
|
+
raise ValueError("default_timeout must be positive")
|
|
897
|
+
return v
|
|
898
|
+
|
|
899
|
+
@field_validator("retry_attempts")
|
|
900
|
+
@classmethod
|
|
901
|
+
def validate_retry_attempts(cls, v):
|
|
902
|
+
if v < 0:
|
|
903
|
+
raise ValueError("retry_attempts must be non-negative")
|
|
904
|
+
return v
|
|
905
|
+
|
|
906
|
+
@field_validator("retry_delay")
|
|
907
|
+
@classmethod
|
|
908
|
+
def validate_retry_delay(cls, v):
|
|
909
|
+
if v < 0:
|
|
910
|
+
raise ValueError("retry_delay must be non-negative")
|
|
911
|
+
return v
|
|
912
|
+
|
|
913
|
+
@field_validator("max_tool_execution_time")
|
|
914
|
+
@classmethod
|
|
915
|
+
def validate_max_tool_execution_time(cls, v):
|
|
916
|
+
if v <= 0:
|
|
917
|
+
raise ValueError("max_tool_execution_time must be positive")
|
|
918
|
+
return v
|
|
919
|
+
|
|
920
|
+
@field_validator("log_level")
|
|
921
|
+
@classmethod
|
|
922
|
+
def validate_log_level(cls, v):
|
|
923
|
+
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
924
|
+
if v.upper() not in valid_levels:
|
|
925
|
+
raise ValueError(f'log_level must be one of: {", ".join(valid_levels)}')
|
|
926
|
+
return v.upper()
|
|
927
|
+
|
|
928
|
+
@field_validator("tool_cache_ttl")
|
|
929
|
+
@classmethod
|
|
930
|
+
def validate_tool_cache_ttl(cls, v):
|
|
931
|
+
if v <= 0:
|
|
932
|
+
raise ValueError("tool_cache_ttl must be positive")
|
|
933
|
+
return v
|
|
934
|
+
|
|
935
|
+
def get_enabled_servers(self) -> Dict[str, MCPServerConfig]:
|
|
936
|
+
"""Get only enabled MCP servers."""
|
|
937
|
+
return {name: config for name, config in self.servers.items() if config.enabled}
|
|
938
|
+
|
|
939
|
+
def get_server_by_name(self, name: str) -> Optional[MCPServerConfig]:
|
|
940
|
+
"""Get a specific MCP server configuration by name."""
|
|
941
|
+
return self.servers.get(name)
|
|
942
|
+
|
|
943
|
+
def add_server(self, name: str, config: MCPServerConfig) -> None:
|
|
944
|
+
"""Add a new MCP server configuration."""
|
|
945
|
+
self.servers[name] = config
|
|
946
|
+
|
|
947
|
+
def remove_server(self, name: str) -> bool:
|
|
948
|
+
"""Remove an MCP server configuration."""
|
|
949
|
+
if name in self.servers:
|
|
950
|
+
del self.servers[name]
|
|
951
|
+
return True
|
|
952
|
+
return False
|
|
953
|
+
|
|
954
|
+
def get_auto_approved_tools(self) -> Dict[str, List[str]]:
|
|
955
|
+
"""Get all auto-approved tools grouped by server."""
|
|
956
|
+
auto_approved = {}
|
|
957
|
+
for server_name, server_config in self.servers.items():
|
|
958
|
+
if server_config.auto_approve:
|
|
959
|
+
auto_approved[server_name] = server_config.auto_approve
|
|
960
|
+
return auto_approved
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
class ConfigProfile(BaseModel):
|
|
964
|
+
"""Configuration profile for different environments/projects."""
|
|
965
|
+
|
|
966
|
+
name: str
|
|
967
|
+
description: Optional[str] = None
|
|
968
|
+
default_provider: str
|
|
969
|
+
providers: Dict[str, ProviderConfig]
|
|
970
|
+
chat_settings: ChatSettings = ChatSettings()
|
|
971
|
+
mcp: MCPConfig = MCPConfig()
|
|
972
|
+
|
|
973
|
+
@field_validator("name")
|
|
974
|
+
@classmethod
|
|
975
|
+
def validate_name(cls, v):
|
|
976
|
+
if not v or not v.strip():
|
|
977
|
+
raise ValueError("profile name cannot be empty")
|
|
978
|
+
return v.strip()
|
|
979
|
+
|
|
980
|
+
@field_validator("default_provider")
|
|
981
|
+
@classmethod
|
|
982
|
+
def validate_default_provider(cls, v):
|
|
983
|
+
if not v or not v.strip():
|
|
984
|
+
raise ValueError("default_provider cannot be empty")
|
|
985
|
+
return v.strip()
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
class Config(BaseModel):
|
|
989
|
+
"""Main configuration model."""
|
|
990
|
+
|
|
991
|
+
default_provider: str
|
|
992
|
+
providers: Dict[str, ProviderConfig]
|
|
993
|
+
chat_settings: ChatSettings = ChatSettings()
|
|
994
|
+
storage_path: str
|
|
995
|
+
mcp: MCPConfig = MCPConfig()
|
|
996
|
+
|
|
997
|
+
# Profile management
|
|
998
|
+
profiles: Dict[str, ConfigProfile] = {}
|
|
999
|
+
active_profile: Optional[str] = None
|
|
1000
|
+
|
|
1001
|
+
# Configuration metadata
|
|
1002
|
+
config_version: str = "2.0"
|
|
1003
|
+
created_at: Optional[datetime] = None
|
|
1004
|
+
updated_at: Optional[datetime] = None
|
|
1005
|
+
|
|
1006
|
+
# Configuration sources tracking
|
|
1007
|
+
config_sources: Dict[str, str] = {} # Maps setting names to their sources
|
|
1008
|
+
|
|
1009
|
+
# Global settings
|
|
1010
|
+
debug_mode: bool = False
|
|
1011
|
+
log_level: str = "INFO"
|
|
1012
|
+
auto_update_check: bool = True
|
|
1013
|
+
telemetry_enabled: bool = False
|
|
1014
|
+
|
|
1015
|
+
# Provider management settings
|
|
1016
|
+
provider_fallback_enabled: bool = True
|
|
1017
|
+
provider_health_check_enabled: bool = True
|
|
1018
|
+
provider_timeout_default: float = 30.0
|
|
1019
|
+
|
|
1020
|
+
# Security settings
|
|
1021
|
+
api_key_encryption_enabled: bool = True
|
|
1022
|
+
secure_storage_enabled: bool = True
|
|
1023
|
+
|
|
1024
|
+
# Performance settings
|
|
1025
|
+
concurrent_requests_limit: int = 5
|
|
1026
|
+
request_cache_enabled: bool = True
|
|
1027
|
+
request_cache_ttl: int = 300 # seconds
|
|
1028
|
+
|
|
1029
|
+
# Custom models - user-defined models that extend available options
|
|
1030
|
+
custom_models: List[EnhancedModelInfo] = []
|
|
1031
|
+
|
|
1032
|
+
@field_validator("default_provider")
|
|
1033
|
+
@classmethod
|
|
1034
|
+
def validate_default_provider(cls, v):
|
|
1035
|
+
if not v or not v.strip():
|
|
1036
|
+
raise ValueError("default_provider cannot be empty")
|
|
1037
|
+
return v.strip()
|
|
1038
|
+
|
|
1039
|
+
@field_validator("storage_path")
|
|
1040
|
+
@classmethod
|
|
1041
|
+
def validate_storage_path(cls, v):
|
|
1042
|
+
if not v or not v.strip():
|
|
1043
|
+
raise ValueError("storage_path cannot be empty")
|
|
1044
|
+
return v.strip()
|
|
1045
|
+
|
|
1046
|
+
@field_validator("active_profile")
|
|
1047
|
+
@classmethod
|
|
1048
|
+
def validate_active_profile(cls, v, info):
|
|
1049
|
+
if v is not None and "profiles" in info.data:
|
|
1050
|
+
profiles = info.data["profiles"]
|
|
1051
|
+
if v not in profiles:
|
|
1052
|
+
raise ValueError(f'active_profile "{v}" not found in profiles')
|
|
1053
|
+
return v
|
|
1054
|
+
|
|
1055
|
+
@field_validator("log_level")
|
|
1056
|
+
@classmethod
|
|
1057
|
+
def validate_log_level(cls, v):
|
|
1058
|
+
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
1059
|
+
if v.upper() not in valid_levels:
|
|
1060
|
+
raise ValueError(f'log_level must be one of: {", ".join(valid_levels)}')
|
|
1061
|
+
return v.upper()
|
|
1062
|
+
|
|
1063
|
+
@field_validator("provider_timeout_default")
|
|
1064
|
+
@classmethod
|
|
1065
|
+
def validate_provider_timeout_default(cls, v):
|
|
1066
|
+
if v <= 0:
|
|
1067
|
+
raise ValueError("provider_timeout_default must be positive")
|
|
1068
|
+
return v
|
|
1069
|
+
|
|
1070
|
+
@field_validator("concurrent_requests_limit")
|
|
1071
|
+
@classmethod
|
|
1072
|
+
def validate_concurrent_requests_limit(cls, v):
|
|
1073
|
+
if v <= 0:
|
|
1074
|
+
raise ValueError("concurrent_requests_limit must be positive")
|
|
1075
|
+
return v
|
|
1076
|
+
|
|
1077
|
+
@field_validator("request_cache_ttl")
|
|
1078
|
+
@classmethod
|
|
1079
|
+
def validate_request_cache_ttl(cls, v):
|
|
1080
|
+
if v <= 0:
|
|
1081
|
+
raise ValueError("request_cache_ttl must be positive")
|
|
1082
|
+
return v
|
|
1083
|
+
|
|
1084
|
+
def get_active_config(self) -> "Config":
|
|
1085
|
+
"""Get the active configuration (profile or main config)."""
|
|
1086
|
+
if self.active_profile and self.active_profile in self.profiles:
|
|
1087
|
+
profile = self.profiles[self.active_profile]
|
|
1088
|
+
# Create a new Config object with profile settings
|
|
1089
|
+
return Config(
|
|
1090
|
+
default_provider=profile.default_provider,
|
|
1091
|
+
providers=profile.providers,
|
|
1092
|
+
chat_settings=profile.chat_settings,
|
|
1093
|
+
storage_path=self.storage_path, # Storage path is global
|
|
1094
|
+
mcp=profile.mcp,
|
|
1095
|
+
profiles=self.profiles,
|
|
1096
|
+
active_profile=self.active_profile,
|
|
1097
|
+
config_version=self.config_version,
|
|
1098
|
+
created_at=self.created_at,
|
|
1099
|
+
updated_at=self.updated_at,
|
|
1100
|
+
config_sources=self.config_sources,
|
|
1101
|
+
)
|
|
1102
|
+
return self
|
|
1103
|
+
|
|
1104
|
+
def create_profile(
|
|
1105
|
+
self, name: str, description: Optional[str] = None
|
|
1106
|
+
) -> ConfigProfile:
|
|
1107
|
+
"""Create a new configuration profile based on current settings."""
|
|
1108
|
+
profile = ConfigProfile(
|
|
1109
|
+
name=name,
|
|
1110
|
+
description=description,
|
|
1111
|
+
default_provider=self.default_provider,
|
|
1112
|
+
providers=self.providers.copy(),
|
|
1113
|
+
chat_settings=self.chat_settings.model_copy(),
|
|
1114
|
+
mcp=self.mcp.model_copy(),
|
|
1115
|
+
)
|
|
1116
|
+
self.profiles[name] = profile
|
|
1117
|
+
return profile
|
|
1118
|
+
|
|
1119
|
+
def switch_profile(self, profile_name: str) -> None:
|
|
1120
|
+
"""Switch to a different configuration profile."""
|
|
1121
|
+
if profile_name not in self.profiles:
|
|
1122
|
+
raise ValueError(f'Profile "{profile_name}" not found')
|
|
1123
|
+
self.active_profile = profile_name
|
|
1124
|
+
|
|
1125
|
+
def delete_profile(self, profile_name: str) -> bool:
|
|
1126
|
+
"""Delete a configuration profile."""
|
|
1127
|
+
if profile_name in self.profiles:
|
|
1128
|
+
del self.profiles[profile_name]
|
|
1129
|
+
if self.active_profile == profile_name:
|
|
1130
|
+
self.active_profile = None
|
|
1131
|
+
return True
|
|
1132
|
+
return False
|
|
1133
|
+
|
|
1134
|
+
def merge_from_env(self, env_vars: Dict[str, str]) -> None:
|
|
1135
|
+
"""Merge configuration from environment variables."""
|
|
1136
|
+
# Map environment variables to configuration fields
|
|
1137
|
+
env_mapping = {
|
|
1138
|
+
"omnimancer_DEFAULT_PROVIDER": "default_provider",
|
|
1139
|
+
"omnimancer_STORAGE_PATH": "storage_path",
|
|
1140
|
+
"omnimancer_MAX_TOKENS": "chat_settings.max_tokens",
|
|
1141
|
+
"omnimancer_TEMPERATURE": "chat_settings.temperature",
|
|
1142
|
+
"omnimancer_CONTEXT_LENGTH": "chat_settings.context_length",
|
|
1143
|
+
"omnimancer_SAVE_HISTORY": "chat_settings.save_history",
|
|
1144
|
+
"omnimancer_MCP_ENABLED": "mcp.enabled",
|
|
1145
|
+
"omnimancer_MCP_AUTO_APPROVE_TIMEOUT": "mcp.auto_approve_timeout",
|
|
1146
|
+
"omnimancer_MCP_MAX_CONCURRENT_SERVERS": "mcp.max_concurrent_servers",
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
for env_var, config_path in env_mapping.items():
|
|
1150
|
+
if env_var in env_vars:
|
|
1151
|
+
value = env_vars[env_var]
|
|
1152
|
+
self._set_nested_value(config_path, value)
|
|
1153
|
+
self.config_sources[config_path] = f"env:{env_var}"
|
|
1154
|
+
|
|
1155
|
+
# Handle provider-specific environment variables
|
|
1156
|
+
for env_var, value in env_vars.items():
|
|
1157
|
+
if env_var.startswith("omnimancer_") and "_API_KEY" in env_var:
|
|
1158
|
+
# Extract provider name from env var (e.g., omnimancer_CLAUDE_API_KEY -> claude)
|
|
1159
|
+
provider_name = (
|
|
1160
|
+
env_var.replace("omnimancer_", "").replace("_API_KEY", "").lower()
|
|
1161
|
+
)
|
|
1162
|
+
if provider_name in self.providers:
|
|
1163
|
+
# Don't store the actual API key in config_sources for security
|
|
1164
|
+
self.config_sources[f"providers.{provider_name}.api_key"] = (
|
|
1165
|
+
f"env:{env_var}"
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
def _set_nested_value(self, path: str, value: str) -> None:
|
|
1169
|
+
"""Set a nested configuration value from a dot-separated path."""
|
|
1170
|
+
parts = path.split(".")
|
|
1171
|
+
obj = self
|
|
1172
|
+
|
|
1173
|
+
# Navigate to the parent object
|
|
1174
|
+
for part in parts[:-1]:
|
|
1175
|
+
if hasattr(obj, part):
|
|
1176
|
+
obj = getattr(obj, part)
|
|
1177
|
+
else:
|
|
1178
|
+
return # Path doesn't exist
|
|
1179
|
+
|
|
1180
|
+
# Set the final value with appropriate type conversion
|
|
1181
|
+
final_key = parts[-1]
|
|
1182
|
+
if hasattr(obj, final_key):
|
|
1183
|
+
current_value = getattr(obj, final_key)
|
|
1184
|
+
try:
|
|
1185
|
+
if isinstance(current_value, bool):
|
|
1186
|
+
converted_value = value.lower() in (
|
|
1187
|
+
"true",
|
|
1188
|
+
"1",
|
|
1189
|
+
"yes",
|
|
1190
|
+
"on",
|
|
1191
|
+
)
|
|
1192
|
+
elif isinstance(current_value, int):
|
|
1193
|
+
converted_value = int(value)
|
|
1194
|
+
elif isinstance(current_value, float):
|
|
1195
|
+
converted_value = float(value)
|
|
1196
|
+
else:
|
|
1197
|
+
converted_value = value
|
|
1198
|
+
setattr(obj, final_key, converted_value)
|
|
1199
|
+
except (ValueError, TypeError):
|
|
1200
|
+
# If conversion fails, keep as string
|
|
1201
|
+
setattr(obj, final_key, value)
|
|
1202
|
+
|
|
1203
|
+
def validate_configuration(self) -> List[str]:
|
|
1204
|
+
"""
|
|
1205
|
+
Validate the entire configuration and return a list of errors.
|
|
1206
|
+
|
|
1207
|
+
Returns:
|
|
1208
|
+
List of validation error messages (empty if valid)
|
|
1209
|
+
"""
|
|
1210
|
+
errors = []
|
|
1211
|
+
|
|
1212
|
+
# Validate basic configuration
|
|
1213
|
+
if not self.default_provider:
|
|
1214
|
+
errors.append("No default provider specified")
|
|
1215
|
+
elif self.default_provider not in self.providers:
|
|
1216
|
+
errors.append(
|
|
1217
|
+
f"Default provider '{self.default_provider}' not found in configured providers"
|
|
1218
|
+
)
|
|
1219
|
+
|
|
1220
|
+
if not self.providers:
|
|
1221
|
+
errors.append("No providers configured")
|
|
1222
|
+
|
|
1223
|
+
# Validate each provider
|
|
1224
|
+
for provider_name, provider_config in self.providers.items():
|
|
1225
|
+
provider_errors = self._validate_provider_config(
|
|
1226
|
+
provider_name, provider_config
|
|
1227
|
+
)
|
|
1228
|
+
errors.extend(provider_errors)
|
|
1229
|
+
|
|
1230
|
+
# Validate MCP configuration
|
|
1231
|
+
mcp_errors = self._validate_mcp_config()
|
|
1232
|
+
errors.extend(mcp_errors)
|
|
1233
|
+
|
|
1234
|
+
# Validate profiles
|
|
1235
|
+
for profile_name, profile in self.profiles.items():
|
|
1236
|
+
profile_errors = self._validate_profile_config(profile_name, profile)
|
|
1237
|
+
errors.extend(profile_errors)
|
|
1238
|
+
|
|
1239
|
+
# Validate active profile
|
|
1240
|
+
if self.active_profile and self.active_profile not in self.profiles:
|
|
1241
|
+
errors.append(f"Active profile '{self.active_profile}' not found")
|
|
1242
|
+
|
|
1243
|
+
return errors
|
|
1244
|
+
|
|
1245
|
+
def __str__(self) -> str:
|
|
1246
|
+
"""String representation with masked sensitive data."""
|
|
1247
|
+
# Create a safe representation without exposing API keys
|
|
1248
|
+
provider_summary = {}
|
|
1249
|
+
for name, config in self.providers.items():
|
|
1250
|
+
provider_summary[name] = {
|
|
1251
|
+
"model": config.model,
|
|
1252
|
+
"provider_type": config.provider_type,
|
|
1253
|
+
"api_key_configured": bool(config.api_key),
|
|
1254
|
+
"enabled": config.enabled,
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return f"Config(default_provider={self.default_provider}, providers={list(self.providers.keys())}, mcp_enabled={self.mcp.enabled})"
|
|
1258
|
+
|
|
1259
|
+
def __repr__(self) -> str:
|
|
1260
|
+
"""Representation with masked sensitive data."""
|
|
1261
|
+
return self.__str__()
|
|
1262
|
+
|
|
1263
|
+
def _validate_provider_config(
|
|
1264
|
+
self, provider_name: str, config: ProviderConfig
|
|
1265
|
+
) -> List[str]:
|
|
1266
|
+
"""Validate a single provider configuration."""
|
|
1267
|
+
errors = []
|
|
1268
|
+
|
|
1269
|
+
# Basic validation
|
|
1270
|
+
if not config.model:
|
|
1271
|
+
errors.append(f"Provider '{provider_name}' has no model specified")
|
|
1272
|
+
|
|
1273
|
+
# Provider-specific validation
|
|
1274
|
+
if config.provider_type == "claude" or provider_name == "claude":
|
|
1275
|
+
errors.extend(self._validate_claude_config(provider_name, config))
|
|
1276
|
+
elif config.provider_type == "openai" or provider_name == "openai":
|
|
1277
|
+
errors.extend(self._validate_openai_config(provider_name, config))
|
|
1278
|
+
elif config.provider_type == "gemini" or provider_name == "gemini":
|
|
1279
|
+
errors.extend(self._validate_gemini_config(provider_name, config))
|
|
1280
|
+
elif config.provider_type == "cohere" or provider_name == "cohere":
|
|
1281
|
+
errors.extend(self._validate_cohere_config(provider_name, config))
|
|
1282
|
+
elif config.provider_type == "ollama" or provider_name == "ollama":
|
|
1283
|
+
errors.extend(self._validate_ollama_config(provider_name, config))
|
|
1284
|
+
|
|
1285
|
+
return errors
|
|
1286
|
+
|
|
1287
|
+
def _validate_claude_config(
|
|
1288
|
+
self, provider_name: str, config: ProviderConfig
|
|
1289
|
+
) -> List[str]:
|
|
1290
|
+
"""Validate Claude-specific configuration."""
|
|
1291
|
+
errors = []
|
|
1292
|
+
if not config.api_key:
|
|
1293
|
+
errors.append(f"Claude provider '{provider_name}' requires an API key")
|
|
1294
|
+
return errors
|
|
1295
|
+
|
|
1296
|
+
def _validate_openai_config(
|
|
1297
|
+
self, provider_name: str, config: ProviderConfig
|
|
1298
|
+
) -> List[str]:
|
|
1299
|
+
"""Validate OpenAI-specific configuration."""
|
|
1300
|
+
errors = []
|
|
1301
|
+
if not config.api_key:
|
|
1302
|
+
errors.append(f"OpenAI provider '{provider_name}' requires an API key")
|
|
1303
|
+
return errors
|
|
1304
|
+
|
|
1305
|
+
def _validate_gemini_config(
|
|
1306
|
+
self, provider_name: str, config: ProviderConfig
|
|
1307
|
+
) -> List[str]:
|
|
1308
|
+
"""Validate Gemini-specific configuration."""
|
|
1309
|
+
errors = []
|
|
1310
|
+
if not config.api_key:
|
|
1311
|
+
errors.append(f"Gemini provider '{provider_name}' requires an API key")
|
|
1312
|
+
return errors
|
|
1313
|
+
|
|
1314
|
+
def _validate_cohere_config(
|
|
1315
|
+
self, provider_name: str, config: ProviderConfig
|
|
1316
|
+
) -> List[str]:
|
|
1317
|
+
"""Validate Cohere-specific configuration."""
|
|
1318
|
+
errors = []
|
|
1319
|
+
if not config.api_key:
|
|
1320
|
+
errors.append(f"Cohere provider '{provider_name}' requires an API key")
|
|
1321
|
+
return errors
|
|
1322
|
+
|
|
1323
|
+
def _validate_ollama_config(
|
|
1324
|
+
self, provider_name: str, config: ProviderConfig
|
|
1325
|
+
) -> List[str]:
|
|
1326
|
+
"""Validate Ollama-specific configuration."""
|
|
1327
|
+
errors = []
|
|
1328
|
+
# Ollama doesn't require API key but needs base_url
|
|
1329
|
+
if not config.base_url:
|
|
1330
|
+
config.base_url = "http://localhost:11434" # Set default
|
|
1331
|
+
return errors
|
|
1332
|
+
|
|
1333
|
+
def _validate_mcp_config(self) -> List[str]:
|
|
1334
|
+
"""Validate MCP configuration."""
|
|
1335
|
+
errors = []
|
|
1336
|
+
|
|
1337
|
+
if self.mcp.enabled:
|
|
1338
|
+
if not self.mcp.servers:
|
|
1339
|
+
errors.append("MCP is enabled but no servers are configured")
|
|
1340
|
+
|
|
1341
|
+
for server_name, server_config in self.mcp.servers.items():
|
|
1342
|
+
if not server_config.command:
|
|
1343
|
+
errors.append(
|
|
1344
|
+
f"MCP server '{server_name}' has no command specified"
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
return errors
|
|
1348
|
+
|
|
1349
|
+
def _validate_profile_config(
|
|
1350
|
+
self, profile_name: str, profile: ConfigProfile
|
|
1351
|
+
) -> List[str]:
|
|
1352
|
+
"""Validate a configuration profile."""
|
|
1353
|
+
errors = []
|
|
1354
|
+
|
|
1355
|
+
if profile.default_provider not in profile.providers:
|
|
1356
|
+
errors.append(
|
|
1357
|
+
f"Profile '{profile_name}' default provider '{profile.default_provider}' not found in profile providers"
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
# Validate each provider in the profile
|
|
1361
|
+
for provider_name, provider_config in profile.providers.items():
|
|
1362
|
+
provider_errors = self._validate_provider_config(
|
|
1363
|
+
provider_name, provider_config
|
|
1364
|
+
)
|
|
1365
|
+
errors.extend(
|
|
1366
|
+
[f"Profile '{profile_name}': {error}" for error in provider_errors]
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
return errors
|
|
1370
|
+
|
|
1371
|
+
def _validate_claude_config(
|
|
1372
|
+
self, provider_name: str, config: ProviderConfig
|
|
1373
|
+
) -> List[str]:
|
|
1374
|
+
"""Validate Claude provider configuration."""
|
|
1375
|
+
errors = []
|
|
1376
|
+
|
|
1377
|
+
if not config.api_key:
|
|
1378
|
+
errors.append(f"Claude provider '{provider_name}' requires an API key")
|
|
1379
|
+
|
|
1380
|
+
# Validate model
|
|
1381
|
+
valid_models = [
|
|
1382
|
+
"claude-3-sonnet-20240229",
|
|
1383
|
+
"claude-3-haiku-20240307",
|
|
1384
|
+
"claude-3-opus-20240229",
|
|
1385
|
+
"claude-3-5-sonnet-20241022",
|
|
1386
|
+
]
|
|
1387
|
+
if config.model not in valid_models:
|
|
1388
|
+
errors.append(
|
|
1389
|
+
f"Unknown Claude model '{config.model}' for provider '{provider_name}'. Valid models: {', '.join(valid_models)}"
|
|
1390
|
+
)
|
|
1391
|
+
|
|
1392
|
+
return errors
|
|
1393
|
+
|
|
1394
|
+
def _validate_openai_config(
|
|
1395
|
+
self, provider_name: str, config: ProviderConfig
|
|
1396
|
+
) -> List[str]:
|
|
1397
|
+
"""Validate OpenAI provider configuration."""
|
|
1398
|
+
errors = []
|
|
1399
|
+
|
|
1400
|
+
if not config.api_key:
|
|
1401
|
+
errors.append(f"OpenAI provider '{provider_name}' requires an API key")
|
|
1402
|
+
|
|
1403
|
+
# Validate model
|
|
1404
|
+
valid_models = [
|
|
1405
|
+
"gpt-4",
|
|
1406
|
+
"gpt-4-turbo",
|
|
1407
|
+
"gpt-4o",
|
|
1408
|
+
"gpt-4o-mini",
|
|
1409
|
+
"gpt-3.5-turbo",
|
|
1410
|
+
"gpt-3.5-turbo-16k",
|
|
1411
|
+
]
|
|
1412
|
+
if config.model not in valid_models:
|
|
1413
|
+
errors.append(
|
|
1414
|
+
f"Unknown OpenAI model '{config.model}' for provider '{provider_name}'. Valid models: {', '.join(valid_models)}"
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
return errors
|
|
1418
|
+
|
|
1419
|
+
def _validate_gemini_config(
|
|
1420
|
+
self, provider_name: str, config: ProviderConfig
|
|
1421
|
+
) -> List[str]:
|
|
1422
|
+
"""Validate Gemini provider configuration."""
|
|
1423
|
+
errors = []
|
|
1424
|
+
|
|
1425
|
+
if not config.api_key and not config.service_account_path:
|
|
1426
|
+
errors.append(
|
|
1427
|
+
f"Gemini provider '{provider_name}' requires either an API key or service account"
|
|
1428
|
+
)
|
|
1429
|
+
|
|
1430
|
+
# Validate model
|
|
1431
|
+
valid_models = ["gemini-1.5-pro", "gemini-1.5-flash", "gemini-1.0-pro"]
|
|
1432
|
+
if config.model not in valid_models:
|
|
1433
|
+
errors.append(
|
|
1434
|
+
f"Unknown Gemini model '{config.model}' for provider '{provider_name}'. Valid models: {', '.join(valid_models)}"
|
|
1435
|
+
)
|
|
1436
|
+
|
|
1437
|
+
return errors
|
|
1438
|
+
|
|
1439
|
+
def _validate_cohere_config(
|
|
1440
|
+
self, provider_name: str, config: ProviderConfig
|
|
1441
|
+
) -> List[str]:
|
|
1442
|
+
"""Validate Cohere provider configuration."""
|
|
1443
|
+
errors = []
|
|
1444
|
+
|
|
1445
|
+
if not config.api_key:
|
|
1446
|
+
errors.append(f"Cohere provider '{provider_name}' requires an API key")
|
|
1447
|
+
|
|
1448
|
+
# Validate model
|
|
1449
|
+
valid_models = [
|
|
1450
|
+
"command-r",
|
|
1451
|
+
"command-r-plus",
|
|
1452
|
+
"command-light",
|
|
1453
|
+
"command",
|
|
1454
|
+
]
|
|
1455
|
+
if config.model not in valid_models:
|
|
1456
|
+
errors.append(
|
|
1457
|
+
f"Unknown Cohere model '{config.model}' for provider '{provider_name}'. Valid models: {', '.join(valid_models)}"
|
|
1458
|
+
)
|
|
1459
|
+
|
|
1460
|
+
return errors
|
|
1461
|
+
|
|
1462
|
+
def _validate_ollama_config(
|
|
1463
|
+
self, provider_name: str, config: ProviderConfig
|
|
1464
|
+
) -> List[str]:
|
|
1465
|
+
"""Validate Ollama provider configuration."""
|
|
1466
|
+
errors = []
|
|
1467
|
+
|
|
1468
|
+
# Ollama doesn't require an API key (local server)
|
|
1469
|
+
# But we should validate the base_url if provided
|
|
1470
|
+
if config.base_url:
|
|
1471
|
+
if not config.base_url.startswith(("http://", "https://")):
|
|
1472
|
+
errors.append(
|
|
1473
|
+
f"Ollama provider '{provider_name}' base_url must start with 'http://' or 'https://'"
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
# Model validation is difficult for Ollama since models are dynamic
|
|
1477
|
+
if not config.model:
|
|
1478
|
+
errors.append(f"Ollama provider '{provider_name}' requires a model name")
|
|
1479
|
+
|
|
1480
|
+
return errors
|
|
1481
|
+
|
|
1482
|
+
def _validate_mcp_config(self) -> List[str]:
|
|
1483
|
+
"""Validate MCP configuration."""
|
|
1484
|
+
errors = []
|
|
1485
|
+
|
|
1486
|
+
# Validate server configurations
|
|
1487
|
+
for server_name, server_config in self.mcp.servers.items():
|
|
1488
|
+
if not server_config.name:
|
|
1489
|
+
errors.append(f"MCP server '{server_name}' has no name")
|
|
1490
|
+
if not server_config.command:
|
|
1491
|
+
errors.append(f"MCP server '{server_name}' has no command")
|
|
1492
|
+
if server_config.timeout <= 0:
|
|
1493
|
+
errors.append(
|
|
1494
|
+
f"MCP server '{server_name}' has invalid timeout: {server_config.timeout}"
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
return errors
|
|
1498
|
+
|
|
1499
|
+
def _validate_profile_config(
|
|
1500
|
+
self, profile_name: str, profile: ConfigProfile
|
|
1501
|
+
) -> List[str]:
|
|
1502
|
+
"""Validate a configuration profile."""
|
|
1503
|
+
errors = []
|
|
1504
|
+
|
|
1505
|
+
if not profile.default_provider:
|
|
1506
|
+
errors.append(f"Profile '{profile_name}' has no default provider")
|
|
1507
|
+
elif profile.default_provider not in profile.providers:
|
|
1508
|
+
errors.append(
|
|
1509
|
+
f"Profile '{profile_name}' default provider '{profile.default_provider}' not found in profile providers"
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1512
|
+
if not profile.providers:
|
|
1513
|
+
errors.append(f"Profile '{profile_name}' has no providers configured")
|
|
1514
|
+
|
|
1515
|
+
# Validate each provider in the profile
|
|
1516
|
+
for provider_name, provider_config in profile.providers.items():
|
|
1517
|
+
provider_errors = self._validate_provider_config(
|
|
1518
|
+
f"{profile_name}.{provider_name}", provider_config
|
|
1519
|
+
)
|
|
1520
|
+
errors.extend(provider_errors)
|
|
1521
|
+
|
|
1522
|
+
return errors
|
|
1523
|
+
|
|
1524
|
+
def get_enabled_providers(self) -> Dict[str, ProviderConfig]:
|
|
1525
|
+
"""Get all enabled provider configurations."""
|
|
1526
|
+
return {
|
|
1527
|
+
name: config for name, config in self.providers.items() if config.enabled
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
def get_providers_by_priority(self) -> List[tuple[str, ProviderConfig]]:
|
|
1531
|
+
"""Get providers sorted by priority (highest first)."""
|
|
1532
|
+
enabled_providers = self.get_enabled_providers()
|
|
1533
|
+
return sorted(
|
|
1534
|
+
enabled_providers.items(),
|
|
1535
|
+
key=lambda x: x[1].priority,
|
|
1536
|
+
reverse=True,
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1539
|
+
def get_providers_with_tools(self) -> Dict[str, ProviderConfig]:
|
|
1540
|
+
"""Get all providers that support tools."""
|
|
1541
|
+
return {
|
|
1542
|
+
name: config
|
|
1543
|
+
for name, config in self.providers.items()
|
|
1544
|
+
if config.enabled
|
|
1545
|
+
and (config.supports_tools or config.supports_function_calling)
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
def get_providers_with_multimodal(self) -> Dict[str, ProviderConfig]:
|
|
1549
|
+
"""Get all providers that support multimodal input."""
|
|
1550
|
+
return {
|
|
1551
|
+
name: config
|
|
1552
|
+
for name, config in self.providers.items()
|
|
1553
|
+
if config.enabled and config.supports_multimodal
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
def update_provider_health_status(
|
|
1557
|
+
self, provider_name: str, is_healthy: bool
|
|
1558
|
+
) -> None:
|
|
1559
|
+
"""Update the health status of a provider (for runtime tracking)."""
|
|
1560
|
+
if provider_name in self.providers:
|
|
1561
|
+
# This would be tracked in runtime state, not persisted config
|
|
1562
|
+
pass
|
|
1563
|
+
|
|
1564
|
+
def get_fallback_providers(self, exclude_provider: str = None) -> List[str]:
|
|
1565
|
+
"""Get list of fallback providers in priority order."""
|
|
1566
|
+
providers_by_priority = self.get_providers_by_priority()
|
|
1567
|
+
fallback_providers = []
|
|
1568
|
+
|
|
1569
|
+
for provider_name, config in providers_by_priority:
|
|
1570
|
+
if provider_name != exclude_provider and config.enabled:
|
|
1571
|
+
fallback_providers.append(provider_name)
|
|
1572
|
+
|
|
1573
|
+
return fallback_providers
|
|
1574
|
+
|
|
1575
|
+
def get_provider_timeout(self, provider_name: str) -> float:
|
|
1576
|
+
"""Get timeout for a specific provider, with fallback to default."""
|
|
1577
|
+
if provider_name in self.providers:
|
|
1578
|
+
provider_config = self.providers[provider_name]
|
|
1579
|
+
return provider_config.timeout or self.provider_timeout_default
|
|
1580
|
+
return self.provider_timeout_default
|
|
1581
|
+
|
|
1582
|
+
def is_provider_enabled(self, provider_name: str) -> bool:
|
|
1583
|
+
"""Check if a provider is enabled."""
|
|
1584
|
+
if provider_name in self.providers:
|
|
1585
|
+
return self.providers[provider_name].enabled
|
|
1586
|
+
return False
|
|
1587
|
+
|
|
1588
|
+
def get_provider_auth_type(self, provider_name: str) -> str:
|
|
1589
|
+
"""Get authentication type for a provider."""
|
|
1590
|
+
if provider_name in self.providers:
|
|
1591
|
+
return self.providers[provider_name].auth_type
|
|
1592
|
+
return "api_key" # Default auth type
|
|
1593
|
+
|
|
1594
|
+
def get_provider_capabilities(self, provider_name: str) -> Dict[str, bool]:
|
|
1595
|
+
"""Get capabilities for a specific provider."""
|
|
1596
|
+
if provider_name not in self.providers:
|
|
1597
|
+
return {}
|
|
1598
|
+
|
|
1599
|
+
config = self.providers[provider_name]
|
|
1600
|
+
return {
|
|
1601
|
+
"supports_tools": config.supports_tools or config.supports_function_calling,
|
|
1602
|
+
"supports_multimodal": config.supports_multimodal,
|
|
1603
|
+
"supports_streaming": config.supports_streaming,
|
|
1604
|
+
"supports_system_messages": config.supports_system_messages,
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
def get_mcp_enabled_servers(self) -> Dict[str, MCPServerConfig]:
|
|
1608
|
+
"""Get all enabled MCP servers."""
|
|
1609
|
+
return self.mcp.get_enabled_servers()
|
|
1610
|
+
|
|
1611
|
+
def has_mcp_servers(self) -> bool:
|
|
1612
|
+
"""Check if any MCP servers are configured and enabled."""
|
|
1613
|
+
return bool(self.get_mcp_enabled_servers())
|
|
1614
|
+
|
|
1615
|
+
def get_config_summary(self) -> Dict[str, Any]:
|
|
1616
|
+
"""Get a summary of the current configuration."""
|
|
1617
|
+
return {
|
|
1618
|
+
"config_version": self.config_version,
|
|
1619
|
+
"default_provider": self.default_provider,
|
|
1620
|
+
"active_profile": self.active_profile,
|
|
1621
|
+
"total_providers": len(self.providers),
|
|
1622
|
+
"enabled_providers": len(self.get_enabled_providers()),
|
|
1623
|
+
"providers_with_tools": len(self.get_providers_with_tools()),
|
|
1624
|
+
"providers_with_multimodal": len(self.get_providers_with_multimodal()),
|
|
1625
|
+
"total_profiles": len(self.profiles),
|
|
1626
|
+
"mcp_enabled": self.mcp.enabled,
|
|
1627
|
+
"mcp_servers": len(self.mcp.servers),
|
|
1628
|
+
"enabled_mcp_servers": len(self.get_mcp_enabled_servers()),
|
|
1629
|
+
"debug_mode": self.debug_mode,
|
|
1630
|
+
"log_level": self.log_level,
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
def get_provider_health_status(self, provider_name: str) -> Optional[bool]:
|
|
1634
|
+
"""Get the health status of a provider."""
|
|
1635
|
+
if provider_name in self.providers:
|
|
1636
|
+
extra_settings = self.providers[provider_name].extra_settings
|
|
1637
|
+
if extra_settings and "is_healthy" in extra_settings:
|
|
1638
|
+
return extra_settings["is_healthy"]
|
|
1639
|
+
return None
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
# BaseProvider is defined in providers/base.py to avoid circular imports
|
|
1643
|
+
|
|
1644
|
+
|
|
1645
|
+
class MCPServerError(MCPError):
|
|
1646
|
+
"""MCP server connection or execution error."""
|
|
1647
|
+
|
|
1648
|
+
pass
|
|
1649
|
+
|
|
1650
|
+
|
|
1651
|
+
class MCPToolError(MCPError):
|
|
1652
|
+
"""MCP tool execution error."""
|
|
1653
|
+
|
|
1654
|
+
pass
|
|
1655
|
+
|
|
1656
|
+
|
|
1657
|
+
# Minimal config template classes to replace removed config_templates.py
|
|
1658
|
+
@dataclass
|
|
1659
|
+
class ConfigTemplate:
|
|
1660
|
+
"""Minimal configuration template definition."""
|
|
1661
|
+
|
|
1662
|
+
name: str
|
|
1663
|
+
description: str
|
|
1664
|
+
use_case: str
|
|
1665
|
+
recommended_providers: List[str]
|
|
1666
|
+
recommended_models: Dict[str, str]
|
|
1667
|
+
settings: Dict[str, Any]
|
|
1668
|
+
mcp_tools: List[str] = None
|
|
1669
|
+
provider_configs: Dict[str, Any] = None
|
|
1670
|
+
mcp_servers: Dict[str, Any] = None
|
|
1671
|
+
|
|
1672
|
+
def __post_init__(self):
|
|
1673
|
+
if self.mcp_tools is None:
|
|
1674
|
+
self.mcp_tools = []
|
|
1675
|
+
if self.provider_configs is None:
|
|
1676
|
+
self.provider_configs = {}
|
|
1677
|
+
if self.mcp_servers is None:
|
|
1678
|
+
self.mcp_servers = {}
|
|
1679
|
+
|
|
1680
|
+
|
|
1681
|
+
class ConfigTemplateManager:
|
|
1682
|
+
"""Minimal configuration template manager."""
|
|
1683
|
+
|
|
1684
|
+
def __init__(self):
|
|
1685
|
+
self.templates = {}
|
|
1686
|
+
self._initialize_templates()
|
|
1687
|
+
|
|
1688
|
+
def _initialize_templates(self):
|
|
1689
|
+
"""Initialize all available templates."""
|
|
1690
|
+
self.templates["coding"] = self._create_coding_template()
|
|
1691
|
+
self.templates["research"] = self._create_research_template()
|
|
1692
|
+
self.templates["creative"] = self._create_creative_template()
|
|
1693
|
+
self.templates["performance"] = self._create_performance_template()
|
|
1694
|
+
self.templates["general"] = self._create_general_template()
|
|
1695
|
+
|
|
1696
|
+
def _create_coding_template(self) -> ConfigTemplate:
|
|
1697
|
+
"""Create coding template."""
|
|
1698
|
+
return ConfigTemplate(
|
|
1699
|
+
name="coding",
|
|
1700
|
+
description="Optimized for software development and programming tasks",
|
|
1701
|
+
use_case="coding",
|
|
1702
|
+
recommended_providers=["claude", "openai", "claude_code"],
|
|
1703
|
+
recommended_models={
|
|
1704
|
+
"claude": "claude-3-5-sonnet-20241022",
|
|
1705
|
+
"openai": "gpt-4o",
|
|
1706
|
+
"claude_code": "claude-code-sonnet",
|
|
1707
|
+
},
|
|
1708
|
+
settings={"temperature": 0.2, "max_tokens": 4096},
|
|
1709
|
+
provider_configs={
|
|
1710
|
+
"claude": {
|
|
1711
|
+
"temperature": 0.2,
|
|
1712
|
+
"max_tokens": 4096,
|
|
1713
|
+
"model": "claude-3-5-sonnet-20241022",
|
|
1714
|
+
},
|
|
1715
|
+
"openai": {
|
|
1716
|
+
"temperature": 0.2,
|
|
1717
|
+
"max_tokens": 4096,
|
|
1718
|
+
"model": "gpt-4o",
|
|
1719
|
+
},
|
|
1720
|
+
"claude_code": {
|
|
1721
|
+
"temperature": 0.2,
|
|
1722
|
+
"max_tokens": 4096,
|
|
1723
|
+
"model": "claude-code-sonnet",
|
|
1724
|
+
},
|
|
1725
|
+
},
|
|
1726
|
+
mcp_servers={
|
|
1727
|
+
"filesystem": {
|
|
1728
|
+
"command": "uvx",
|
|
1729
|
+
"args": ["mcp-server-filesystem"],
|
|
1730
|
+
"enabled": False,
|
|
1731
|
+
},
|
|
1732
|
+
"git": {
|
|
1733
|
+
"command": "uvx",
|
|
1734
|
+
"args": ["mcp-server-git"],
|
|
1735
|
+
"enabled": False,
|
|
1736
|
+
},
|
|
1737
|
+
},
|
|
1738
|
+
)
|
|
1739
|
+
|
|
1740
|
+
def _create_research_template(self) -> ConfigTemplate:
|
|
1741
|
+
"""Create research template."""
|
|
1742
|
+
return ConfigTemplate(
|
|
1743
|
+
name="research",
|
|
1744
|
+
description="Optimized for research and analysis tasks",
|
|
1745
|
+
use_case="research",
|
|
1746
|
+
recommended_providers=["perplexity", "claude", "openai"],
|
|
1747
|
+
recommended_models={
|
|
1748
|
+
"perplexity": "sonar-pro",
|
|
1749
|
+
"claude": "claude-3-5-sonnet-20241022",
|
|
1750
|
+
"openai": "gpt-4o",
|
|
1751
|
+
},
|
|
1752
|
+
settings={"temperature": 0.3, "max_tokens": 8192},
|
|
1753
|
+
provider_configs={
|
|
1754
|
+
"perplexity": {
|
|
1755
|
+
"temperature": 0.3,
|
|
1756
|
+
"max_tokens": 8192,
|
|
1757
|
+
"model": "sonar-pro",
|
|
1758
|
+
},
|
|
1759
|
+
"claude": {
|
|
1760
|
+
"temperature": 0.3,
|
|
1761
|
+
"max_tokens": 8192,
|
|
1762
|
+
"model": "claude-3-5-sonnet-20241022",
|
|
1763
|
+
},
|
|
1764
|
+
"openai": {
|
|
1765
|
+
"temperature": 0.3,
|
|
1766
|
+
"max_tokens": 8192,
|
|
1767
|
+
"model": "gpt-4o",
|
|
1768
|
+
},
|
|
1769
|
+
},
|
|
1770
|
+
mcp_servers={
|
|
1771
|
+
"web-search": {
|
|
1772
|
+
"command": "uvx",
|
|
1773
|
+
"args": ["mcp-server-web-search"],
|
|
1774
|
+
"enabled": False,
|
|
1775
|
+
},
|
|
1776
|
+
"knowledge": {
|
|
1777
|
+
"command": "uvx",
|
|
1778
|
+
"args": ["mcp-server-knowledge"],
|
|
1779
|
+
"enabled": False,
|
|
1780
|
+
},
|
|
1781
|
+
},
|
|
1782
|
+
)
|
|
1783
|
+
|
|
1784
|
+
def _create_creative_template(self) -> ConfigTemplate:
|
|
1785
|
+
"""Create creative template."""
|
|
1786
|
+
return ConfigTemplate(
|
|
1787
|
+
name="creative",
|
|
1788
|
+
description="Optimized for creative writing and content generation",
|
|
1789
|
+
use_case="creative",
|
|
1790
|
+
recommended_providers=["openai", "claude", "gemini"],
|
|
1791
|
+
recommended_models={
|
|
1792
|
+
"openai": "gpt-4o",
|
|
1793
|
+
"claude": "claude-3-5-sonnet-20241022",
|
|
1794
|
+
"gemini": "gemini-1.5-pro",
|
|
1795
|
+
},
|
|
1796
|
+
settings={"temperature": 0.8, "max_tokens": 4096},
|
|
1797
|
+
provider_configs={
|
|
1798
|
+
"openai": {
|
|
1799
|
+
"temperature": 0.8,
|
|
1800
|
+
"max_tokens": 4096,
|
|
1801
|
+
"model": "gpt-4o",
|
|
1802
|
+
},
|
|
1803
|
+
"claude": {
|
|
1804
|
+
"temperature": 0.8,
|
|
1805
|
+
"max_tokens": 4096,
|
|
1806
|
+
"model": "claude-3-5-sonnet-20241022",
|
|
1807
|
+
},
|
|
1808
|
+
"gemini": {
|
|
1809
|
+
"temperature": 0.8,
|
|
1810
|
+
"max_tokens": 4096,
|
|
1811
|
+
"model": "gemini-1.5-pro",
|
|
1812
|
+
},
|
|
1813
|
+
},
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
def _create_performance_template(self) -> ConfigTemplate:
|
|
1817
|
+
"""Create performance template."""
|
|
1818
|
+
return ConfigTemplate(
|
|
1819
|
+
name="performance",
|
|
1820
|
+
description="Optimized for fast responses and efficiency",
|
|
1821
|
+
use_case="performance",
|
|
1822
|
+
recommended_providers=["openai", "claude", "gemini"],
|
|
1823
|
+
recommended_models={
|
|
1824
|
+
"openai": "gpt-4o-mini",
|
|
1825
|
+
"claude": "claude-3-haiku",
|
|
1826
|
+
"gemini": "gemini-1.5-flash",
|
|
1827
|
+
},
|
|
1828
|
+
settings={"temperature": 0.5, "max_tokens": 2048},
|
|
1829
|
+
provider_configs={
|
|
1830
|
+
"openai": {
|
|
1831
|
+
"temperature": 0.5,
|
|
1832
|
+
"max_tokens": 2048,
|
|
1833
|
+
"model": "gpt-4o-mini",
|
|
1834
|
+
},
|
|
1835
|
+
"claude": {
|
|
1836
|
+
"temperature": 0.5,
|
|
1837
|
+
"max_tokens": 2048,
|
|
1838
|
+
"model": "claude-3-haiku",
|
|
1839
|
+
},
|
|
1840
|
+
"gemini": {
|
|
1841
|
+
"temperature": 0.5,
|
|
1842
|
+
"max_tokens": 2048,
|
|
1843
|
+
"model": "gemini-1.5-flash",
|
|
1844
|
+
},
|
|
1845
|
+
},
|
|
1846
|
+
)
|
|
1847
|
+
|
|
1848
|
+
def _create_general_template(self) -> ConfigTemplate:
|
|
1849
|
+
"""Create general template."""
|
|
1850
|
+
return ConfigTemplate(
|
|
1851
|
+
name="general",
|
|
1852
|
+
description="General purpose configuration",
|
|
1853
|
+
use_case="general",
|
|
1854
|
+
recommended_providers=["openai", "anthropic"],
|
|
1855
|
+
recommended_models={
|
|
1856
|
+
"openai": "gpt-4o-mini",
|
|
1857
|
+
"anthropic": "claude-3-haiku",
|
|
1858
|
+
},
|
|
1859
|
+
settings={"temperature": 0.7, "max_tokens": 2048},
|
|
1860
|
+
provider_configs={
|
|
1861
|
+
"openai": {
|
|
1862
|
+
"temperature": 0.7,
|
|
1863
|
+
"max_tokens": 2048,
|
|
1864
|
+
"model": "gpt-4o-mini",
|
|
1865
|
+
},
|
|
1866
|
+
"anthropic": {
|
|
1867
|
+
"temperature": 0.7,
|
|
1868
|
+
"max_tokens": 2048,
|
|
1869
|
+
"model": "claude-3-haiku",
|
|
1870
|
+
},
|
|
1871
|
+
},
|
|
1872
|
+
)
|
|
1873
|
+
|
|
1874
|
+
def get_template(self, name: str) -> ConfigTemplate:
|
|
1875
|
+
"""Get template by name. Raises KeyError if not found."""
|
|
1876
|
+
if name not in self.templates:
|
|
1877
|
+
raise KeyError(f"Template '{name}' not found")
|
|
1878
|
+
return self.templates[name]
|
|
1879
|
+
|
|
1880
|
+
def list_templates(self) -> List[Dict[str, str]]:
|
|
1881
|
+
"""List available templates with their information."""
|
|
1882
|
+
return [
|
|
1883
|
+
{
|
|
1884
|
+
"name": template.name,
|
|
1885
|
+
"description": template.description,
|
|
1886
|
+
"use_case": template.use_case,
|
|
1887
|
+
}
|
|
1888
|
+
for template in self.templates.values()
|
|
1889
|
+
]
|