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,709 @@
1
+ """
2
+ OpenRouter provider implementation for Omnimancer.
3
+
4
+ This module provides the OpenRouter provider implementation using OpenRouter's API
5
+ with support for model aggregation, cost optimization, and access to multiple models.
6
+ """
7
+
8
+ from datetime import datetime
9
+ from typing import Dict, List
10
+
11
+ import certifi
12
+ import httpx
13
+
14
+ from ..core.models import (
15
+ ChatContext,
16
+ ChatResponse,
17
+ EnhancedModelInfo,
18
+ ToolCall,
19
+ ToolDefinition,
20
+ )
21
+ from ..utils.errors import (
22
+ AuthenticationError,
23
+ ModelNotFoundError,
24
+ NetworkError,
25
+ ProviderError,
26
+ RateLimitError,
27
+ )
28
+ from .base import BaseProvider
29
+
30
+
31
+ class OpenRouterProvider(BaseProvider):
32
+ """
33
+ OpenRouter provider implementation using OpenRouter's API.
34
+
35
+ Provides access to multiple models through OpenRouter's aggregation service
36
+ with cost optimization features and model selection logic.
37
+ """
38
+
39
+ BASE_URL = "https://openrouter.ai/api/v1"
40
+
41
+ def __init__(self, api_key: str, model: str = "", **kwargs):
42
+ """
43
+ Initialize OpenRouter provider.
44
+
45
+ Args:
46
+ api_key: OpenRouter API key
47
+ model: OpenRouter model ID to use (e.g., 'anthropic/claude-3.5-sonnet', 'openai/gpt-4', 'meta-llama/llama-3.1-70b-instruct')
48
+ **kwargs: Additional configuration including OpenRouter-specific settings
49
+ """
50
+ super().__init__(api_key, model or "anthropic/claude-3.5-sonnet", **kwargs)
51
+
52
+ # OpenRouter-specific configuration
53
+ self.openrouter_referrer = kwargs.get(
54
+ "openrouter_referrer", "https://github.com/omnimancer-cli"
55
+ )
56
+ self.openrouter_title = kwargs.get("openrouter_title", "Omnimancer CLI")
57
+
58
+ # Standard parameters
59
+ self.max_tokens = kwargs.get("max_tokens", 4096)
60
+ self.temperature = kwargs.get("temperature", 0.7)
61
+ self.top_p = kwargs.get("top_p", 1.0)
62
+ self.frequency_penalty = kwargs.get("frequency_penalty", 0.0)
63
+ self.presence_penalty = kwargs.get("presence_penalty", 0.0)
64
+
65
+ # Cost optimization settings
66
+ self.enable_fallback = kwargs.get("enable_fallback", True)
67
+ self.max_cost_per_token = kwargs.get("max_cost_per_token", None)
68
+ self.prefer_cheaper_models = kwargs.get("prefer_cheaper_models", False)
69
+
70
+ # Fallback notification settings
71
+ self.show_fallback_warnings = kwargs.get("show_fallback_warnings", True)
72
+
73
+ async def send_message(self, message: str, context: ChatContext) -> ChatResponse:
74
+ """
75
+ Send a message to OpenRouter API.
76
+
77
+ Args:
78
+ message: User message
79
+ context: Conversation context
80
+
81
+ Returns:
82
+ ChatResponse with OpenRouter's reply
83
+ """
84
+ # Prepare messages for OpenRouter API
85
+ messages = self._prepare_messages(message, context)
86
+
87
+ # Build request payload
88
+ payload = {
89
+ "model": self.model,
90
+ "messages": messages,
91
+ "max_tokens": self.max_tokens,
92
+ "temperature": self.temperature,
93
+ "top_p": self.top_p,
94
+ "frequency_penalty": self.frequency_penalty,
95
+ "presence_penalty": self.presence_penalty,
96
+ "stream": False,
97
+ }
98
+
99
+ # Add cost optimization parameters
100
+ if self.enable_fallback:
101
+ payload["route"] = "fallback"
102
+
103
+ # Try with SSL verification first, then fall back if needed
104
+ for ssl_verify in [True, certifi.where(), False]:
105
+ try:
106
+ async with httpx.AsyncClient(verify=ssl_verify) as client:
107
+ response = await client.post(
108
+ f"{self.BASE_URL}/chat/completions",
109
+ headers=self._get_headers(),
110
+ json=payload,
111
+ timeout=30.0,
112
+ )
113
+
114
+ # If we get here, the request succeeded
115
+ return self._handle_response(response)
116
+
117
+ except httpx.ConnectError as e:
118
+ if "SSL" in str(e) or "certificate" in str(e):
119
+ # SSL error, try next verification method
120
+ continue
121
+ else:
122
+ # Non-SSL connection error, don't retry
123
+ raise NetworkError(f"Connection error: {e}")
124
+ except httpx.TimeoutException:
125
+ raise NetworkError("Request to OpenRouter API timed out")
126
+ except httpx.RequestError as e:
127
+ if "SSL" not in str(e) and "certificate" not in str(e):
128
+ # Non-SSL request error, don't retry
129
+ raise NetworkError(f"Network error: {e}")
130
+ # SSL-related error, try next verification method
131
+ continue
132
+ except (
133
+ AuthenticationError,
134
+ RateLimitError,
135
+ ModelNotFoundError,
136
+ ProviderError,
137
+ ):
138
+ # Known provider errors, don't retry
139
+ raise
140
+ except Exception as e:
141
+ # Unknown error, don't retry
142
+ raise ProviderError(f"Unexpected error: {e}")
143
+
144
+ # If we get here, all SSL methods failed
145
+ raise NetworkError("Failed to establish SSL connection to OpenRouter API")
146
+
147
+ async def send_message_with_tools(
148
+ self,
149
+ message: str,
150
+ context: ChatContext,
151
+ available_tools: List[ToolDefinition],
152
+ ) -> ChatResponse:
153
+ """
154
+ Send a message with available tools for OpenRouter to use.
155
+
156
+ Args:
157
+ message: User message
158
+ context: Conversation context
159
+ available_tools: List of tools available for the model to use
160
+
161
+ Returns:
162
+ ChatResponse with OpenRouter's reply and any tool calls
163
+ """
164
+ # Prepare messages for OpenRouter API
165
+ messages = self._prepare_messages(message, context)
166
+
167
+ # Convert tools to OpenRouter format
168
+ tools = self._convert_tools_to_openrouter_format(available_tools)
169
+
170
+ # Build request payload
171
+ payload = {
172
+ "model": self.model,
173
+ "messages": messages,
174
+ "max_tokens": self.max_tokens,
175
+ "temperature": self.temperature,
176
+ "top_p": self.top_p,
177
+ "frequency_penalty": self.frequency_penalty,
178
+ "presence_penalty": self.presence_penalty,
179
+ "tools": tools,
180
+ "tool_choice": "auto",
181
+ "stream": False,
182
+ }
183
+
184
+ # Add cost optimization parameters
185
+ if self.enable_fallback:
186
+ payload["route"] = "fallback"
187
+
188
+ # Try with SSL verification first, then fall back if needed
189
+ for ssl_verify in [True, certifi.where(), False]:
190
+ try:
191
+ async with httpx.AsyncClient(verify=ssl_verify) as client:
192
+ response = await client.post(
193
+ f"{self.BASE_URL}/chat/completions",
194
+ headers=self._get_headers(),
195
+ json=payload,
196
+ timeout=30.0,
197
+ )
198
+
199
+ # If we get here, the request succeeded
200
+ return self._handle_response_with_tools(response)
201
+
202
+ except httpx.ConnectError as e:
203
+ if "SSL" in str(e) or "certificate" in str(e):
204
+ # SSL error, try next verification method
205
+ continue
206
+ else:
207
+ # Non-SSL connection error, don't retry
208
+ raise NetworkError(f"Connection error: {e}")
209
+ except httpx.TimeoutException:
210
+ raise NetworkError("Request to OpenRouter API timed out")
211
+ except httpx.RequestError as e:
212
+ if "SSL" not in str(e) and "certificate" not in str(e):
213
+ # Non-SSL request error, don't retry
214
+ raise NetworkError(f"Network error: {e}")
215
+ # SSL-related error, try next verification method
216
+ continue
217
+ except (
218
+ AuthenticationError,
219
+ RateLimitError,
220
+ ModelNotFoundError,
221
+ ProviderError,
222
+ ):
223
+ # Known provider errors, don't retry
224
+ raise
225
+ except Exception as e:
226
+ # Unknown error, don't retry
227
+ raise ProviderError(f"Unexpected error: {e}")
228
+
229
+ # If we get here, all SSL methods failed
230
+ raise NetworkError("Failed to establish SSL connection to OpenRouter API")
231
+
232
+ async def validate_credentials(self) -> bool:
233
+ """
234
+ Validate OpenRouter API credentials by making a test request.
235
+
236
+ Returns:
237
+ True if credentials are valid
238
+ """
239
+ # Try different SSL verification methods
240
+ for ssl_verify in [True, certifi.where(), False]:
241
+ try:
242
+ async with httpx.AsyncClient(verify=ssl_verify) as client:
243
+ response = await client.post(
244
+ f"{self.BASE_URL}/chat/completions",
245
+ headers=self._get_headers(),
246
+ json={
247
+ "model": self.model,
248
+ "messages": [{"role": "user", "content": "Hi"}],
249
+ "max_tokens": 10,
250
+ },
251
+ timeout=10.0,
252
+ )
253
+
254
+ # If we get here, connection worked
255
+ return response.status_code == 200
256
+
257
+ except httpx.ConnectError as e:
258
+ if "SSL" in str(e) or "certificate" in str(e):
259
+ # SSL error, try next verification method
260
+ continue
261
+ else:
262
+ # Non-SSL connection error
263
+ return False
264
+ except httpx.RequestError as e:
265
+ if "SSL" not in str(e) and "certificate" not in str(e):
266
+ # Non-SSL request error
267
+ return False
268
+ # SSL-related error, try next verification method
269
+ continue
270
+ except Exception as e:
271
+ # Other error (timeout, etc.) - if it's not SSL related, stop trying
272
+ if "SSL" not in str(e) and "certificate" not in str(e):
273
+ return False
274
+ # SSL-related error, continue to next method
275
+ continue
276
+
277
+ # If we get here, all SSL methods failed
278
+ return False
279
+
280
+ def _get_headers(self) -> Dict[str, str]:
281
+ """
282
+ Get headers for OpenRouter API requests.
283
+
284
+ Returns:
285
+ Dictionary of headers
286
+ """
287
+ headers = {
288
+ "Content-Type": "application/json",
289
+ "Authorization": f"Bearer {self.api_key}",
290
+ "HTTP-Referer": self.openrouter_referrer,
291
+ "X-Title": self.openrouter_title,
292
+ }
293
+
294
+ return headers
295
+
296
+ def _prepare_messages(
297
+ self, message: str, context: ChatContext
298
+ ) -> List[Dict[str, str]]:
299
+ """
300
+ Prepare messages for OpenRouter API format.
301
+
302
+ Args:
303
+ message: Current user message
304
+ context: Conversation context
305
+
306
+ Returns:
307
+ List of messages formatted for OpenRouter API
308
+ """
309
+ messages = []
310
+
311
+ # Add context messages
312
+ for msg in context.messages:
313
+ messages.append({"role": msg.role.value, "content": msg.content})
314
+
315
+ # Add current message
316
+ messages.append({"role": "user", "content": message})
317
+
318
+ return messages
319
+
320
+ def _convert_tools_to_openrouter_format(
321
+ self, tools: List[ToolDefinition]
322
+ ) -> List[Dict]:
323
+ """
324
+ Convert tool definitions to OpenRouter API format.
325
+
326
+ Args:
327
+ tools: List of tool definitions
328
+
329
+ Returns:
330
+ List of tools formatted for OpenRouter API
331
+ """
332
+ openrouter_tools = []
333
+
334
+ for tool in tools:
335
+ openrouter_tool = {
336
+ "type": "function",
337
+ "function": {
338
+ "name": tool.name,
339
+ "description": tool.description,
340
+ "parameters": tool.parameters,
341
+ },
342
+ }
343
+ openrouter_tools.append(openrouter_tool)
344
+
345
+ return openrouter_tools
346
+
347
+ def _handle_response(self, response: httpx.Response) -> ChatResponse:
348
+ """
349
+ Handle OpenRouter API response.
350
+
351
+ Args:
352
+ response: HTTP response from OpenRouter API
353
+
354
+ Returns:
355
+ ChatResponse object
356
+
357
+ Raises:
358
+ Various provider errors based on response status
359
+ """
360
+ if response.status_code == 200:
361
+ data = response.json()
362
+ choices = data.get("choices", [])
363
+
364
+ if choices and len(choices) > 0:
365
+ message = choices[0].get("message", {})
366
+ content = message.get("content", "")
367
+ usage = data.get("usage", {})
368
+
369
+ # Extract model used (OpenRouter may route to different models)
370
+ model_used = data.get("model", self.model)
371
+
372
+ # Check for model fallback and add warning if needed
373
+ final_content = content
374
+ if model_used != self.model:
375
+ # Always log the fallback for debugging
376
+ logger = __import__("logging").getLogger(__name__)
377
+ logger.warning(
378
+ f"OpenRouter model fallback: requested '{self.model}' but got '{model_used}'"
379
+ )
380
+
381
+ # Add user warning if enabled
382
+ if self.show_fallback_warnings:
383
+ fallback_warning = f"⚠️ **Model Fallback Notice**: Requested '{self.model}' but OpenRouter routed to '{model_used}' (model may be unavailable or overloaded)\n\n"
384
+ final_content = fallback_warning + content
385
+
386
+ return ChatResponse(
387
+ content=final_content,
388
+ model_used=model_used,
389
+ tokens_used=usage.get("total_tokens", 0),
390
+ timestamp=datetime.now(),
391
+ )
392
+ else:
393
+ raise ProviderError("Empty response from OpenRouter API")
394
+
395
+ elif response.status_code == 401:
396
+ raise AuthenticationError("Invalid OpenRouter API key")
397
+ elif response.status_code == 429:
398
+ raise RateLimitError("OpenRouter API rate limit exceeded")
399
+ elif response.status_code == 404:
400
+ raise ModelNotFoundError(f"OpenRouter model '{self.model}' not found")
401
+ else:
402
+ try:
403
+ error_data = response.json()
404
+ error_msg = error_data.get("error", {}).get("message", "Unknown error")
405
+ except:
406
+ error_msg = f"HTTP {response.status_code}"
407
+
408
+ raise ProviderError(f"OpenRouter API error: {error_msg}")
409
+
410
+ def _handle_response_with_tools(self, response: httpx.Response) -> ChatResponse:
411
+ """
412
+ Handle OpenRouter API response with tool calls.
413
+
414
+ Args:
415
+ response: HTTP response from OpenRouter API
416
+
417
+ Returns:
418
+ ChatResponse object with tool calls
419
+ """
420
+ if response.status_code == 200:
421
+ data = response.json()
422
+ choices = data.get("choices", [])
423
+
424
+ if choices and len(choices) > 0:
425
+ message = choices[0].get("message", {})
426
+ content = message.get("content", "")
427
+ usage = data.get("usage", {})
428
+
429
+ # Extract tool calls if present
430
+ tool_calls = []
431
+ if "tool_calls" in message:
432
+ for tool_call in message["tool_calls"]:
433
+ if tool_call.get("type") == "function":
434
+ function = tool_call.get("function", {})
435
+ tool_calls.append(
436
+ ToolCall(
437
+ name=function.get("name", ""),
438
+ arguments=function.get("arguments", {}),
439
+ )
440
+ )
441
+
442
+ # Extract model used
443
+ model_used = data.get("model", self.model)
444
+
445
+ # Check for model fallback and add warning if needed
446
+ final_content = content
447
+ if model_used != self.model:
448
+ # Always log the fallback for debugging
449
+ logger = __import__("logging").getLogger(__name__)
450
+ logger.warning(
451
+ f"OpenRouter model fallback: requested '{self.model}' but got '{model_used}'"
452
+ )
453
+
454
+ # Add user warning if enabled
455
+ if self.show_fallback_warnings:
456
+ fallback_warning = f"⚠️ **Model Fallback Notice**: Requested '{self.model}' but OpenRouter routed to '{model_used}' (model may be unavailable or overloaded)\n\n"
457
+ final_content = fallback_warning + content
458
+
459
+ return ChatResponse(
460
+ content=final_content,
461
+ model_used=model_used,
462
+ tokens_used=usage.get("total_tokens", 0),
463
+ tool_calls=tool_calls if tool_calls else None,
464
+ timestamp=datetime.now(),
465
+ )
466
+ else:
467
+ raise ProviderError("Empty response from OpenRouter API")
468
+ else:
469
+ return self._handle_response(response)
470
+
471
+ def get_model_info(self) -> EnhancedModelInfo:
472
+ """
473
+ Get information about the current OpenRouter model.
474
+ """
475
+ # Common OpenRouter model configurations
476
+ model_configs = {
477
+ "anthropic/claude-3.5-sonnet": {
478
+ "description": "Claude 3.5 Sonnet via OpenRouter - Most capable",
479
+ "max_tokens": 200000,
480
+ "cost_per_million_input": 3.0,
481
+ "cost_per_million_output": 15.0,
482
+ "swe_score": 88.7,
483
+ "supports_tools": True,
484
+ "supports_multimodal": True,
485
+ },
486
+ "anthropic/claude-3-opus": {
487
+ "description": "Claude 3 Opus via OpenRouter - Highest quality",
488
+ "max_tokens": 200000,
489
+ "cost_per_million_input": 15.0,
490
+ "cost_per_million_output": 75.0,
491
+ "swe_score": 84.9,
492
+ "supports_tools": True,
493
+ "supports_multimodal": True,
494
+ },
495
+ "openai/gpt-4o": {
496
+ "description": "GPT-4o via OpenRouter - Fast and capable",
497
+ "max_tokens": 128000,
498
+ "cost_per_million_input": 5.0,
499
+ "cost_per_million_output": 15.0,
500
+ "swe_score": 71.2,
501
+ "supports_tools": True,
502
+ "supports_multimodal": True,
503
+ },
504
+ "google/gemini-pro-1.5": {
505
+ "description": "Gemini Pro 1.5 via OpenRouter - Long context",
506
+ "max_tokens": 2097152,
507
+ "cost_per_million_input": 1.25,
508
+ "cost_per_million_output": 5.0,
509
+ "swe_score": 71.9,
510
+ "supports_tools": True,
511
+ "supports_multimodal": True,
512
+ },
513
+ "meta-llama/llama-3.1-405b-instruct": {
514
+ "description": "Llama 3.1 405B via OpenRouter - Open source flagship",
515
+ "max_tokens": 131072,
516
+ "cost_per_million_input": 5.0,
517
+ "cost_per_million_output": 15.0,
518
+ "swe_score": 69.4,
519
+ "supports_tools": True,
520
+ "supports_multimodal": False,
521
+ },
522
+ "qwen/qwen-2.5-72b-instruct": {
523
+ "description": "Qwen 2.5 72B via OpenRouter - Efficient and capable",
524
+ "max_tokens": 131072,
525
+ "cost_per_million_input": 0.4,
526
+ "cost_per_million_output": 1.2,
527
+ "swe_score": 65.9,
528
+ "supports_tools": True,
529
+ "supports_multimodal": False,
530
+ },
531
+ }
532
+
533
+ config = model_configs.get(
534
+ self.model,
535
+ {
536
+ "description": f"OpenRouter model {self.model}",
537
+ "max_tokens": 32768,
538
+ "cost_per_million_input": 1.0,
539
+ "cost_per_million_output": 3.0,
540
+ "swe_score": 50.0,
541
+ "supports_tools": True,
542
+ "supports_multimodal": False,
543
+ },
544
+ )
545
+
546
+ enhanced_info = EnhancedModelInfo(
547
+ name=self.model,
548
+ provider="openrouter",
549
+ description=config["description"],
550
+ max_tokens=config["max_tokens"],
551
+ cost_per_million_input=config["cost_per_million_input"],
552
+ cost_per_million_output=config["cost_per_million_output"],
553
+ swe_score=config["swe_score"],
554
+ available=True,
555
+ supports_tools=config["supports_tools"],
556
+ supports_multimodal=config["supports_multimodal"],
557
+ latest_version="claude-3.5-sonnet" in self.model,
558
+ context_window=config["max_tokens"],
559
+ is_free=False,
560
+ release_date=datetime(2024, 6, 1), # Approximate OpenRouter availability
561
+ )
562
+
563
+ # Update SWE rating based on score
564
+ enhanced_info.update_swe_rating()
565
+
566
+ return enhanced_info
567
+
568
+ def get_available_models(self) -> List[EnhancedModelInfo]:
569
+ """
570
+ Get list of popular OpenRouter models.
571
+
572
+ Note: OpenRouter has hundreds of models. This returns a curated list
573
+ of the most popular and capable models.
574
+ """
575
+ models = [
576
+ EnhancedModelInfo(
577
+ name="anthropic/claude-3.5-sonnet",
578
+ provider="openrouter",
579
+ description="Claude 3.5 Sonnet via OpenRouter - Most capable",
580
+ max_tokens=200000,
581
+ cost_per_million_input=3.0,
582
+ cost_per_million_output=15.0,
583
+ swe_score=88.7,
584
+ available=True,
585
+ supports_tools=True,
586
+ supports_multimodal=True,
587
+ latest_version=True,
588
+ context_window=200000,
589
+ is_free=False,
590
+ release_date=datetime(2024, 6, 1),
591
+ ),
592
+ EnhancedModelInfo(
593
+ name="anthropic/claude-3-opus",
594
+ provider="openrouter",
595
+ description="Claude 3 Opus via OpenRouter - Highest quality",
596
+ max_tokens=200000,
597
+ cost_per_million_input=15.0,
598
+ cost_per_million_output=75.0,
599
+ swe_score=84.9,
600
+ available=True,
601
+ supports_tools=True,
602
+ supports_multimodal=True,
603
+ context_window=200000,
604
+ is_free=False,
605
+ release_date=datetime(2024, 6, 1),
606
+ ),
607
+ EnhancedModelInfo(
608
+ name="openai/gpt-4o",
609
+ provider="openrouter",
610
+ description="GPT-4o via OpenRouter - Fast and capable",
611
+ max_tokens=128000,
612
+ cost_per_million_input=5.0,
613
+ cost_per_million_output=15.0,
614
+ swe_score=71.2,
615
+ available=True,
616
+ supports_tools=True,
617
+ supports_multimodal=True,
618
+ context_window=128000,
619
+ is_free=False,
620
+ release_date=datetime(2024, 6, 1),
621
+ ),
622
+ EnhancedModelInfo(
623
+ name="google/gemini-pro-1.5",
624
+ provider="openrouter",
625
+ description="Gemini Pro 1.5 via OpenRouter - Long context",
626
+ max_tokens=2097152,
627
+ cost_per_million_input=1.25,
628
+ cost_per_million_output=5.0,
629
+ swe_score=71.9,
630
+ available=True,
631
+ supports_tools=True,
632
+ supports_multimodal=True,
633
+ context_window=2097152,
634
+ is_free=False,
635
+ release_date=datetime(2024, 6, 1),
636
+ ),
637
+ EnhancedModelInfo(
638
+ name="meta-llama/llama-3.1-405b-instruct",
639
+ provider="openrouter",
640
+ description="Llama 3.1 405B via OpenRouter - Open source flagship",
641
+ max_tokens=131072,
642
+ cost_per_million_input=5.0,
643
+ cost_per_million_output=15.0,
644
+ swe_score=69.4,
645
+ available=True,
646
+ supports_tools=True,
647
+ supports_multimodal=False,
648
+ context_window=131072,
649
+ is_free=False,
650
+ release_date=datetime(2024, 6, 1),
651
+ ),
652
+ EnhancedModelInfo(
653
+ name="qwen/qwen-2.5-72b-instruct",
654
+ provider="openrouter",
655
+ description="Qwen 2.5 72B via OpenRouter - Efficient and capable",
656
+ max_tokens=131072,
657
+ cost_per_million_input=0.4,
658
+ cost_per_million_output=1.2,
659
+ swe_score=65.9,
660
+ available=True,
661
+ supports_tools=True,
662
+ supports_multimodal=False,
663
+ context_window=131072,
664
+ is_free=False,
665
+ release_date=datetime(2024, 6, 1),
666
+ ),
667
+ ]
668
+
669
+ # Update SWE ratings for all models
670
+ for model in models:
671
+ model.update_swe_rating()
672
+
673
+ return models
674
+
675
+ def supports_tools(self) -> bool:
676
+ """
677
+ Check if OpenRouter provider supports tool calling.
678
+
679
+ Returns:
680
+ True - Most models on OpenRouter support tool calling
681
+ """
682
+ # Most modern models on OpenRouter support tools
683
+ return True
684
+
685
+ def supports_multimodal(self) -> bool:
686
+ """
687
+ Check if OpenRouter provider supports multimodal inputs.
688
+
689
+ Returns:
690
+ True for models that support vision/multimodal
691
+ """
692
+ multimodal_models = [
693
+ "claude-3",
694
+ "claude-3.5",
695
+ "gpt-4o",
696
+ "gpt-4-vision",
697
+ "gemini-pro",
698
+ "gemini-1.5",
699
+ ]
700
+ return any(model in self.model.lower() for model in multimodal_models)
701
+
702
+ def supports_streaming(self) -> bool:
703
+ """
704
+ Check if OpenRouter provider supports streaming responses.
705
+
706
+ Returns:
707
+ True - OpenRouter supports streaming
708
+ """
709
+ return True