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 CHANGED
@@ -2,4 +2,4 @@
2
2
  MBX AI package.
3
3
  """
4
4
 
5
- __version__ = "0.5.22"
5
+ __version__ = "0.5.24"
mbxai/mcp/server.py CHANGED
@@ -31,7 +31,7 @@ class MCPServer:
31
31
  self.app = FastAPI(
32
32
  title=self.name,
33
33
  description=self.description,
34
- version="0.5.22",
34
+ version="0.5.24",
35
35
  )
36
36
 
37
37
  # Initialize MCP server
@@ -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: Optional[Union[str, OpenRouterModel]] = None,
193
+ model: str | None = None,
119
194
  stream: bool = False,
120
195
  **kwargs: Any,
121
196
  ) -> Any:
122
- """Create a chat completion.
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
- # Remove any incompatible parameters
140
- kwargs.pop("parse", None) # Remove parse parameter if present
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
- logger.debug(f"Making chat completion request with model {model or self.model}")
143
- logger.debug(f"Request messages: {messages}")
144
- logger.debug(f"Request kwargs: {kwargs}")
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 structure for debugging
154
- logger.debug(f"Response type: {type(response)}")
155
- logger.debug(f"Response attributes: {dir(response)}")
156
- logger.debug(f"Raw response: {response}")
157
-
158
- # Handle streaming response differently
159
- if stream:
160
- return response
161
-
162
- # Validate response structure
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 isinstance(e, OpenRouterError):
185
- raise
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]],
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mbxai
3
- Version: 0.5.22
3
+ Version: 0.5.24
4
4
  Summary: MBX AI SDK
5
5
  Project-URL: Homepage, https://www.mibexx.de
6
6
  Project-URL: Documentation, https://www.mibexx.de
@@ -1,18 +1,18 @@
1
- mbxai/__init__.py,sha256=Y0TOlMhMNWJBWpYsqp-fuKo6LuQ2nlSRQ9Q4CfNQ1mM,48
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=xtialyKJMkHnggu26AseV0BfCzY36xQgQ7DxcOYeLXM,3463
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=XLRMRNRJH96Jl6_af0KkzRDdLJnixh8I3RvEEcFuXyg,10840
9
- mbxai/openrouter/config.py,sha256=MTX_YHsFrM7JYqovJSkEF6JzVyIdajeI5Dja2CALH58,2874
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.22.dist-info/METADATA,sha256=aIlB2mJ22tPjTPwU5CsUYXkr9_R4pe0xCKXfe54KotY,4108
16
- mbxai-0.5.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
- mbxai-0.5.22.dist-info/licenses/LICENSE,sha256=hEyhc4FxwYo3NQ40yNgZ7STqwVk-1_XcTXOnAPbGJAw,1069
18
- mbxai-0.5.22.dist-info/RECORD,,
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