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.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. 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
+ ]