omnimancer-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|