mbxai 0.5.22__py3-none-any.whl → 0.5.24__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.
- mbxai/__init__.py +1 -1
- mbxai/mcp/server.py +1 -1
- mbxai/openrouter/client.py +107 -53
- mbxai/openrouter/config.py +13 -1
- {mbxai-0.5.22.dist-info → mbxai-0.5.24.dist-info}/METADATA +1 -1
- {mbxai-0.5.22.dist-info → mbxai-0.5.24.dist-info}/RECORD +8 -8
- {mbxai-0.5.22.dist-info → mbxai-0.5.24.dist-info}/WHEEL +0 -0
- {mbxai-0.5.22.dist-info → mbxai-0.5.24.dist-info}/licenses/LICENSE +0 -0
mbxai/__init__.py
CHANGED
mbxai/mcp/server.py
CHANGED
mbxai/openrouter/client.py
CHANGED
@@ -4,10 +4,12 @@ OpenRouter client implementation.
|
|
4
4
|
|
5
5
|
from typing import Any, Optional, Union
|
6
6
|
from openai import OpenAI, OpenAIError
|
7
|
-
from pydantic import BaseModel, TypeAdapter
|
7
|
+
from pydantic import BaseModel, TypeAdapter, Field
|
8
8
|
from .models import OpenRouterModel, OpenRouterModelRegistry
|
9
9
|
from .config import OpenRouterConfig
|
10
10
|
import logging
|
11
|
+
import time
|
12
|
+
from functools import wraps
|
11
13
|
|
12
14
|
logger = logging.getLogger(__name__)
|
13
15
|
|
@@ -27,6 +29,65 @@ class OpenRouterAPIError(OpenRouterError):
|
|
27
29
|
pass
|
28
30
|
|
29
31
|
|
32
|
+
def with_retry(max_retries: int = 3, initial_delay: float = 1.0, max_delay: float = 10.0):
|
33
|
+
"""Decorator to add retry logic to a function.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
max_retries: Maximum number of retry attempts
|
37
|
+
initial_delay: Initial delay between retries in seconds
|
38
|
+
max_delay: Maximum delay between retries in seconds
|
39
|
+
"""
|
40
|
+
def decorator(func):
|
41
|
+
@wraps(func)
|
42
|
+
async def async_wrapper(*args, **kwargs):
|
43
|
+
last_error = None
|
44
|
+
delay = initial_delay
|
45
|
+
|
46
|
+
for attempt in range(max_retries + 1):
|
47
|
+
try:
|
48
|
+
return await func(*args, **kwargs)
|
49
|
+
except (OpenRouterConnectionError, OpenRouterAPIError) as e:
|
50
|
+
last_error = e
|
51
|
+
if attempt < max_retries:
|
52
|
+
logger.warning(f"Attempt {attempt + 1} failed: {str(e)}. Retrying in {delay:.1f}s...")
|
53
|
+
await asyncio.sleep(delay)
|
54
|
+
delay = min(delay * 2, max_delay)
|
55
|
+
else:
|
56
|
+
logger.error(f"All {max_retries + 1} attempts failed")
|
57
|
+
raise last_error
|
58
|
+
except Exception as e:
|
59
|
+
# Don't retry other types of errors
|
60
|
+
raise e
|
61
|
+
|
62
|
+
raise last_error
|
63
|
+
|
64
|
+
@wraps(func)
|
65
|
+
def sync_wrapper(*args, **kwargs):
|
66
|
+
last_error = None
|
67
|
+
delay = initial_delay
|
68
|
+
|
69
|
+
for attempt in range(max_retries + 1):
|
70
|
+
try:
|
71
|
+
return func(*args, **kwargs)
|
72
|
+
except (OpenRouterConnectionError, OpenRouterAPIError) as e:
|
73
|
+
last_error = e
|
74
|
+
if attempt < max_retries:
|
75
|
+
logger.warning(f"Attempt {attempt + 1} failed: {str(e)}. Retrying in {delay:.1f}s...")
|
76
|
+
time.sleep(delay)
|
77
|
+
delay = min(delay * 2, max_delay)
|
78
|
+
else:
|
79
|
+
logger.error(f"All {max_retries + 1} attempts failed")
|
80
|
+
raise last_error
|
81
|
+
except Exception as e:
|
82
|
+
# Don't retry other types of errors
|
83
|
+
raise e
|
84
|
+
|
85
|
+
raise last_error
|
86
|
+
|
87
|
+
return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
|
88
|
+
return decorator
|
89
|
+
|
90
|
+
|
30
91
|
class OpenRouterClient:
|
31
92
|
"""Client for interacting with the OpenRouter API."""
|
32
93
|
|
@@ -36,6 +97,9 @@ class OpenRouterClient:
|
|
36
97
|
model: Union[str, OpenRouterModel] = OpenRouterModel.GPT4_TURBO,
|
37
98
|
base_url: Optional[str] = None,
|
38
99
|
default_headers: Optional[dict[str, str]] = None,
|
100
|
+
max_retries: int = 3,
|
101
|
+
retry_initial_delay: float = 1.0,
|
102
|
+
retry_max_delay: float = 10.0,
|
39
103
|
) -> None:
|
40
104
|
"""Initialize the OpenRouter client.
|
41
105
|
|
@@ -44,6 +108,9 @@ class OpenRouterClient:
|
|
44
108
|
model: The model to use (default: GPT4_TURBO)
|
45
109
|
base_url: Optional custom base URL for the API
|
46
110
|
default_headers: Optional default headers for API requests
|
111
|
+
max_retries: Maximum number of retry attempts (default: 3)
|
112
|
+
retry_initial_delay: Initial delay between retries in seconds (default: 1.0)
|
113
|
+
retry_max_delay: Maximum delay between retries in seconds (default: 10.0)
|
47
114
|
|
48
115
|
Raises:
|
49
116
|
OpenRouterError: If initialization fails
|
@@ -56,7 +123,10 @@ class OpenRouterClient:
|
|
56
123
|
default_headers=default_headers or {
|
57
124
|
"HTTP-Referer": "https://github.com/mibexx/mbxai",
|
58
125
|
"X-Title": "MBX AI",
|
59
|
-
}
|
126
|
+
},
|
127
|
+
max_retries=max_retries,
|
128
|
+
retry_initial_delay=retry_initial_delay,
|
129
|
+
retry_max_delay=retry_max_delay,
|
60
130
|
)
|
61
131
|
|
62
132
|
self._client = OpenAI(
|
@@ -86,6 +156,10 @@ class OpenRouterClient:
|
|
86
156
|
raise OpenRouterAPIError(f"API error during {operation}: {error_msg}")
|
87
157
|
elif "Connection" in error_msg:
|
88
158
|
raise OpenRouterConnectionError(f"Connection error during {operation}: {error_msg}")
|
159
|
+
elif "Expecting value" in error_msg and "line" in error_msg:
|
160
|
+
# This is a JSON parsing error, likely due to a truncated or malformed response
|
161
|
+
logger.error("JSON parsing error detected. This might be due to a large response or network issues.")
|
162
|
+
raise OpenRouterAPIError(f"Response parsing error during {operation}. The response might be too large or malformed.")
|
89
163
|
else:
|
90
164
|
raise OpenRouterError(f"Error during {operation}: {error_msg}")
|
91
165
|
|
@@ -111,80 +185,59 @@ class OpenRouterClient:
|
|
111
185
|
"""
|
112
186
|
self.model = value
|
113
187
|
|
188
|
+
@with_retry()
|
114
189
|
def chat_completion(
|
115
190
|
self,
|
116
191
|
messages: list[dict[str, Any]],
|
117
192
|
*,
|
118
|
-
model:
|
193
|
+
model: str | None = None,
|
119
194
|
stream: bool = False,
|
120
195
|
**kwargs: Any,
|
121
196
|
) -> Any:
|
122
|
-
"""
|
123
|
-
|
124
|
-
Args:
|
125
|
-
messages: list of messages
|
126
|
-
model: Optional model override
|
127
|
-
stream: Whether to stream the response
|
128
|
-
**kwargs: Additional parameters
|
129
|
-
|
130
|
-
Returns:
|
131
|
-
Completion response
|
132
|
-
|
133
|
-
Raises:
|
134
|
-
OpenRouterConnectionError: For connection issues
|
135
|
-
OpenRouterAPIError: For API errors
|
136
|
-
OpenRouterError: For other errors
|
137
|
-
"""
|
197
|
+
"""Get a chat completion from OpenRouter."""
|
138
198
|
try:
|
139
|
-
#
|
140
|
-
|
199
|
+
# Log the request details
|
200
|
+
logger.info(f"Sending chat completion request to OpenRouter with model: {model or self.model}")
|
201
|
+
logger.info(f"Message count: {len(messages)}")
|
141
202
|
|
142
|
-
|
143
|
-
|
144
|
-
logger.
|
203
|
+
# Calculate total message size for logging
|
204
|
+
total_size = sum(len(str(msg)) for msg in messages)
|
205
|
+
logger.info(f"Total message size: {total_size} bytes")
|
145
206
|
|
146
207
|
response = self._client.chat.completions.create(
|
147
|
-
model=str(model or self.model),
|
148
208
|
messages=messages,
|
209
|
+
model=model or self.model,
|
149
210
|
stream=stream,
|
150
211
|
**kwargs,
|
151
212
|
)
|
152
213
|
|
153
|
-
# Log response
|
154
|
-
logger.
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
if response is None:
|
164
|
-
logger.error("Received None response from API")
|
165
|
-
raise OpenRouterError("Received None response from API")
|
166
|
-
|
167
|
-
if not hasattr(response, 'choices'):
|
168
|
-
logger.error(f"Invalid response: missing choices attribute. Response: {response}")
|
169
|
-
raise OpenRouterError("Invalid response: missing choices attribute")
|
170
|
-
|
171
|
-
if not response.choices:
|
172
|
-
logger.error(f"Invalid response: empty choices list. Response: {response}")
|
173
|
-
raise OpenRouterError("Invalid response: empty choices list")
|
174
|
-
|
175
|
-
if not hasattr(response.choices[0], 'message'):
|
176
|
-
logger.error(f"Invalid response: missing message in first choice. Response: {response}")
|
177
|
-
raise OpenRouterError("Invalid response: missing message in first choice")
|
214
|
+
# Log response details
|
215
|
+
logger.info("Received response from OpenRouter")
|
216
|
+
if hasattr(response, 'choices') and response.choices:
|
217
|
+
logger.info(f"Response has {len(response.choices)} choices")
|
218
|
+
if hasattr(response.choices[0], 'message'):
|
219
|
+
content = response.choices[0].message.content
|
220
|
+
content_length = len(content) if content else 0
|
221
|
+
logger.info(f"First choice has message with content length: {content_length}")
|
222
|
+
if content_length > 1000000: # Log warning for very large responses
|
223
|
+
logger.warning(f"Response content is very large ({content_length} bytes)")
|
178
224
|
|
179
|
-
logger.debug(f"Response message: {response.choices[0].message}")
|
180
225
|
return response
|
181
226
|
|
182
227
|
except Exception as e:
|
183
228
|
logger.error(f"Error in chat completion: {str(e)}")
|
184
|
-
if
|
185
|
-
|
229
|
+
if hasattr(e, 'response') and e.response is not None:
|
230
|
+
logger.error(f"Response status: {e.response.status_code}")
|
231
|
+
logger.error(f"Response headers: {e.response.headers}")
|
232
|
+
try:
|
233
|
+
content = e.response.text
|
234
|
+
logger.error(f"Response content length: {len(content)} bytes")
|
235
|
+
logger.error(f"Response content preview: {content[:1000]}...")
|
236
|
+
except:
|
237
|
+
logger.error("Could not read response content")
|
186
238
|
self._handle_api_error("chat completion", e)
|
187
239
|
|
240
|
+
@with_retry()
|
188
241
|
def chat_completion_parse(
|
189
242
|
self,
|
190
243
|
messages: list[dict[str, Any]],
|
@@ -251,6 +304,7 @@ class OpenRouterClient:
|
|
251
304
|
except Exception as e:
|
252
305
|
self._handle_api_error("chat completion parse", e)
|
253
306
|
|
307
|
+
@with_retry()
|
254
308
|
def embeddings(
|
255
309
|
self,
|
256
310
|
input: Union[str, list[str]],
|
mbxai/openrouter/config.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from pydantic import BaseModel, Field, field_validator
|
2
|
-
from typing import Union
|
2
|
+
from typing import Union, Optional
|
3
3
|
from .models import OpenRouterModel, OpenRouterModelRegistry
|
4
4
|
|
5
5
|
class OpenRouterConfig(BaseModel):
|
@@ -21,6 +21,18 @@ class OpenRouterConfig(BaseModel):
|
|
21
21
|
},
|
22
22
|
description="Default headers to include in all requests"
|
23
23
|
)
|
24
|
+
max_retries: int = Field(
|
25
|
+
default=3,
|
26
|
+
description="Maximum number of retry attempts"
|
27
|
+
)
|
28
|
+
retry_initial_delay: float = Field(
|
29
|
+
default=1.0,
|
30
|
+
description="Initial delay between retries in seconds"
|
31
|
+
)
|
32
|
+
retry_max_delay: float = Field(
|
33
|
+
default=10.0,
|
34
|
+
description="Maximum delay between retries in seconds"
|
35
|
+
)
|
24
36
|
|
25
37
|
@field_validator("token")
|
26
38
|
def validate_token(cls, v: str) -> str:
|
@@ -1,18 +1,18 @@
|
|
1
|
-
mbxai/__init__.py,sha256=
|
1
|
+
mbxai/__init__.py,sha256=uOU108fCrriuZbpIuLG4acnQ0ZDvDwD6AnuvvE9mA4A,48
|
2
2
|
mbxai/core.py,sha256=WMvmU9TTa7M_m-qWsUew4xH8Ul6xseCZ2iBCXJTW-Bs,196
|
3
3
|
mbxai/mcp/__init__.py,sha256=_ek9iYdYqW5saKetj4qDci11jxesQDiHPJRpHMKkxgU,175
|
4
4
|
mbxai/mcp/client.py,sha256=B8ZpH-uecmTCgoDw65LwwVxsFWVoX-08t5ff0hOEPXk,6011
|
5
5
|
mbxai/mcp/example.py,sha256=oaol7AvvZnX86JWNz64KvPjab5gg1VjVN3G8eFSzuaE,2350
|
6
|
-
mbxai/mcp/server.py,sha256
|
6
|
+
mbxai/mcp/server.py,sha256=-oTWHpKJBF2p6Y9s8FhSFY4mytwNzCCx4S2lwR5Xr_E,3463
|
7
7
|
mbxai/openrouter/__init__.py,sha256=Ito9Qp_B6q-RLGAQcYyTJVWwR2YAZvNqE-HIYXxhtD8,298
|
8
|
-
mbxai/openrouter/client.py,sha256=
|
9
|
-
mbxai/openrouter/config.py,sha256=
|
8
|
+
mbxai/openrouter/client.py,sha256=YOjcYkD8VLMVzBd6z_rB7kLGEzNwIVSUEOyMZlcIhkw,13647
|
9
|
+
mbxai/openrouter/config.py,sha256=Ia93s-auim9Sq71eunVDbn9ET5xX2zusXpV4JBdHAzs,3251
|
10
10
|
mbxai/openrouter/models.py,sha256=b3IjjtZAjeGOf2rLsdnCD1HacjTnS8jmv_ZXorc-KJQ,2604
|
11
11
|
mbxai/tools/__init__.py,sha256=QUFaXhDm-UKcuAtT1rbKzhBkvyRBVokcQIOf9cxIuwc,160
|
12
12
|
mbxai/tools/client.py,sha256=t7rdITqgCbDXQPFOZhGj6VDDPAwqdilJMKPfCOcJaFo,17279
|
13
13
|
mbxai/tools/example.py,sha256=1HgKK39zzUuwFbnp3f0ThyWVfA_8P28PZcTwaUw5K78,2232
|
14
14
|
mbxai/tools/types.py,sha256=fo5t9UbsHGynhA88vD_ecgDqL8iLvt2E1h1ym43Rrgk,745
|
15
|
-
mbxai-0.5.
|
16
|
-
mbxai-0.5.
|
17
|
-
mbxai-0.5.
|
18
|
-
mbxai-0.5.
|
15
|
+
mbxai-0.5.24.dist-info/METADATA,sha256=yA2MFVTn6yxrLmoC_XgS8frW9MalTT2XEv2Re5S7Org,4108
|
16
|
+
mbxai-0.5.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
17
|
+
mbxai-0.5.24.dist-info/licenses/LICENSE,sha256=hEyhc4FxwYo3NQ40yNgZ7STqwVk-1_XcTXOnAPbGJAw,1069
|
18
|
+
mbxai-0.5.24.dist-info/RECORD,,
|
File without changes
|
File without changes
|