meshapi 0.1.0__tar.gz → 0.1.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshapi
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Official Python SDK for the MeshAPI AI model gateway
5
5
  Project-URL: Homepage, https://meshapi.ai
6
6
  Project-URL: Documentation, https://developers.meshapi.ai
@@ -8,7 +8,17 @@ import math
8
8
  import random
9
9
  import time
10
10
  from dataclasses import dataclass, field
11
- from typing import Any, AsyncIterator, Dict, Iterator, Optional, Set, Type, TypeVar, Union
11
+ from typing import (
12
+ Any,
13
+ AsyncIterator,
14
+ Dict,
15
+ Iterator,
16
+ Optional,
17
+ Set,
18
+ Type,
19
+ TypeVar,
20
+ Union,
21
+ )
12
22
 
13
23
  import httpx
14
24
 
@@ -52,12 +62,19 @@ def _extract_sse_data(frame: str) -> Optional[str]:
52
62
  data_lines = []
53
63
  for line in frame.splitlines():
54
64
  if line.startswith("data: "):
55
- data_lines.append(line[len("data: "):])
65
+ data_lines.append(line[len("data: ") :])
56
66
  if not data_lines:
57
67
  return None
58
68
  return "\n".join(data_lines)
59
69
 
60
70
 
71
+ def _extract_sse_event(frame: str) -> Optional[str]:
72
+ for line in frame.splitlines():
73
+ if line.startswith("event: "):
74
+ return line[len("event: ") :]
75
+ return None
76
+
77
+
61
78
  def _try_parse_sse_frame(frame: str) -> "Optional[Union[ChatCompletionChunk, object]]":
62
79
  """Parse one SSE frame string.
63
80
 
@@ -78,19 +95,33 @@ def _try_parse_sse_frame(frame: str) -> "Optional[Union[ChatCompletionChunk, obj
78
95
  except json.JSONDecodeError:
79
96
  return None
80
97
 
81
- if isinstance(parsed, dict) and "error" in parsed:
98
+ if isinstance(parsed, dict) and parsed.get("error") is not None:
82
99
  err = parsed["error"]
83
- raise MeshAPIError(
84
- err.get("message", "upstream error"),
85
- status=0,
86
- error_code=err.get("code", "upstream_error"),
87
- request_id="",
88
- )
100
+ if isinstance(err, dict):
101
+ raise MeshAPIError(
102
+ err.get("message", "upstream error"),
103
+ status=0,
104
+ error_code=err.get("code", "upstream_error"),
105
+ request_id="",
106
+ )
107
+ else:
108
+ raise MeshAPIError(
109
+ str(err),
110
+ status=0,
111
+ error_code="upstream_error",
112
+ request_id="",
113
+ )
114
+
115
+ sse_event = _extract_sse_event(frame)
116
+ if isinstance(parsed, dict) and sse_event is not None:
117
+ parsed["event"] = sse_event
89
118
 
90
119
  return ChatCompletionChunk.model_validate(parsed)
91
120
 
92
121
 
93
- def _try_parse_json_sse_frame(frame: str, model_cls: Type[T]) -> Optional[Union[T, object]]:
122
+ def _try_parse_json_sse_frame(
123
+ frame: str, model_cls: Type[T]
124
+ ) -> Optional[Union[T, object]]:
94
125
  data_line = _extract_sse_data(frame)
95
126
  if data_line is None or data_line.strip() == "":
96
127
  return None
@@ -100,14 +131,27 @@ def _try_parse_json_sse_frame(frame: str, model_cls: Type[T]) -> Optional[Union[
100
131
  parsed = json.loads(data_line)
101
132
  except json.JSONDecodeError:
102
133
  return None
103
- if isinstance(parsed, dict) and "error" in parsed:
134
+ if isinstance(parsed, dict) and parsed.get("error") is not None:
104
135
  err = parsed["error"]
105
- raise MeshAPIError(
106
- err.get("message", "upstream error"),
107
- status=0,
108
- error_code=err.get("code", "upstream_error"),
109
- request_id="",
110
- )
136
+ if isinstance(err, dict):
137
+ raise MeshAPIError(
138
+ err.get("message", "upstream error"),
139
+ status=0,
140
+ error_code=err.get("code", "upstream_error"),
141
+ request_id="",
142
+ )
143
+ else:
144
+ raise MeshAPIError(
145
+ str(err),
146
+ status=0,
147
+ error_code="upstream_error",
148
+ request_id="",
149
+ )
150
+
151
+ sse_event = _extract_sse_event(frame)
152
+ if isinstance(parsed, dict) and sse_event is not None:
153
+ parsed["event"] = sse_event
154
+
111
155
  return model_cls.model_validate(parsed)
112
156
 
113
157
 
@@ -185,7 +229,9 @@ async def _aiter_sse(response: httpx.Response) -> AsyncIterator[ChatCompletionCh
185
229
  raise MeshAPIError.stream_interrupted(str(exc)) from exc
186
230
 
187
231
 
188
- async def _aiter_json_sse(response: httpx.Response, model_cls: Type[T]) -> AsyncIterator[T]:
232
+ async def _aiter_json_sse(
233
+ response: httpx.Response, model_cls: Type[T]
234
+ ) -> AsyncIterator[T]:
189
235
  remainder = ""
190
236
  try:
191
237
  async for raw_bytes in response.aiter_bytes():
@@ -219,7 +265,7 @@ def _compute_delay_s(attempt: int, retry_after: Optional[int]) -> float:
219
265
  if retry_after is not None:
220
266
  base = retry_after * 1000
221
267
  else:
222
- base = _BACKOFF_BASE_MS * (2 ** attempt)
268
+ base = _BACKOFF_BASE_MS * (2**attempt)
223
269
  capped = min(base, _BACKOFF_MAX_MS)
224
270
  jittered = capped * (0.8 + random.random() * 0.4) # ±20%
225
271
  return jittered / 1000.0
@@ -286,7 +332,10 @@ class SyncHttpClient:
286
332
  return req
287
333
 
288
334
  response = self._client.request(method, path, **kwargs)
289
- if response.status_code in _RETRY_STATUS_CODES and attempt < self._config.max_retries:
335
+ if (
336
+ response.status_code in _RETRY_STATUS_CODES
337
+ and attempt < self._config.max_retries
338
+ ):
290
339
  delay = _compute_delay_s(attempt, _retry_after_from_response(response))
291
340
  time.sleep(delay)
292
341
  continue
@@ -325,12 +374,16 @@ class SyncHttpClient:
325
374
  return response.content
326
375
 
327
376
  def stream(self, path: str, body: Any) -> Iterator[ChatCompletionChunk]:
328
- with self._client.stream("POST", path, json=body, headers=self._headers()) as response:
377
+ with self._client.stream(
378
+ "POST", path, json=body, headers=self._headers()
379
+ ) as response:
329
380
  _raise_for_status(response)
330
381
  yield from _iter_sse(response)
331
382
 
332
383
  def stream_json(self, path: str, body: Any, model_cls: Type[T]) -> Iterator[T]:
333
- with self._client.stream("POST", path, json=body, headers=self._headers()) as response:
384
+ with self._client.stream(
385
+ "POST", path, json=body, headers=self._headers()
386
+ ) as response:
334
387
  _raise_for_status(response)
335
388
  yield from _iter_json_sse(response, model_cls)
336
389
 
@@ -382,7 +435,10 @@ class AsyncHttpClient:
382
435
 
383
436
  for attempt in range(self._config.max_retries + 1):
384
437
  response = await self._client.request(method, path, **kwargs)
385
- if response.status_code in _RETRY_STATUS_CODES and attempt < self._config.max_retries:
438
+ if (
439
+ response.status_code in _RETRY_STATUS_CODES
440
+ and attempt < self._config.max_retries
441
+ ):
386
442
  delay = _compute_delay_s(attempt, _retry_after_from_response(response))
387
443
  await asyncio.sleep(delay)
388
444
  continue
@@ -414,7 +470,9 @@ class AsyncHttpClient:
414
470
  if response.status_code == 204:
415
471
  return
416
472
 
417
- async def get_bytes(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> bytes:
473
+ async def get_bytes(
474
+ self, path: str, *, params: Optional[Dict[str, Any]] = None
475
+ ) -> bytes:
418
476
  response = await self._request("GET", path, params=params)
419
477
  return response.content
420
478
 
@@ -426,7 +484,9 @@ class AsyncHttpClient:
426
484
  async for chunk in _aiter_sse(response):
427
485
  yield chunk
428
486
 
429
- async def stream_json(self, path: str, body: Any, model_cls: Type[T]) -> AsyncIterator[T]:
487
+ async def stream_json(
488
+ self, path: str, body: Any, model_cls: Type[T]
489
+ ) -> AsyncIterator[T]:
430
490
  async with self._client.stream(
431
491
  "POST", path, json=body, headers=self._headers()
432
492
  ) as response:
@@ -59,6 +59,20 @@ class ToolCall(BaseModel):
59
59
  function: ToolCallFunction
60
60
 
61
61
 
62
+ class ToolCallFunctionChunk(BaseModel):
63
+ model_config = ConfigDict(extra="ignore")
64
+ name: Optional[str] = None
65
+ arguments: Optional[str] = None
66
+
67
+
68
+ class ToolCallChunk(BaseModel):
69
+ model_config = ConfigDict(extra="ignore")
70
+ index: int
71
+ id: Optional[str] = None
72
+ type: Optional[Literal["function"]] = None
73
+ function: Optional[ToolCallFunctionChunk] = None
74
+
75
+
62
76
  class ChatMessage(BaseModel):
63
77
  model_config = ConfigDict(extra="ignore")
64
78
  role: ChatRole
@@ -194,7 +208,7 @@ class ChatCompletionChunkDelta(BaseModel):
194
208
  model_config = ConfigDict(extra="ignore")
195
209
  role: Optional[str] = None
196
210
  content: Optional[str] = None
197
- tool_calls: Optional[List[ToolCall]] = None
211
+ tool_calls: Optional[List[ToolCallChunk]] = None
198
212
  audio: Optional[Dict[str, Any]] = None
199
213
 
200
214
 
@@ -489,6 +503,22 @@ class CompareStreamEvent(BaseModel):
489
503
  event: Optional[str] = None
490
504
  data: Optional[Dict[str, Any]] = None
491
505
 
506
+ # Common fields flattened from various compare event types
507
+ delta: Optional[str] = None
508
+ model: Optional[str] = None
509
+ comparison_id: Optional[str] = None
510
+ comparison_model: Optional[str] = None
511
+ models: Optional[List[str]] = None
512
+ latency_ms: Optional[int] = None
513
+ total_latency_ms: Optional[int] = None
514
+ finish_reason: Optional[str] = None
515
+ error: Optional[Any] = None
516
+ error_code: Optional[str] = None
517
+ usage: Optional[Dict[str, Any]] = None
518
+ skip_comparison: Optional[bool] = None
519
+ partial: Optional[bool] = None
520
+ comparison_fallback_used: Optional[bool] = None
521
+
492
522
 
493
523
  # ---------------------------------------------------------------------------
494
524
  # Files / Batches
@@ -4,14 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meshapi"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Official Python SDK for the MeshAPI AI model gateway"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = { text = "MIT" }
12
- authors = [
13
- { name = "MeshAPI", email = "contact@meshapi.ai" },
14
- ]
12
+ authors = [{ name = "MeshAPI", email = "contact@meshapi.ai" }]
15
13
  keywords = [
16
14
  "meshapi",
17
15
  "llm",
@@ -39,17 +37,10 @@ classifiers = [
39
37
  "Topic :: Software Development :: Libraries :: Python Modules",
40
38
  "Typing :: Typed",
41
39
  ]
42
- dependencies = [
43
- "httpx>=0.27.0",
44
- "pydantic>=2.0.0",
45
- ]
40
+ dependencies = ["httpx>=0.27.0", "pydantic>=2.0.0"]
46
41
 
47
42
  [project.optional-dependencies]
48
- dev = [
49
- "pytest>=8.0.0",
50
- "pytest-asyncio>=0.23.0",
51
- "respx>=0.21.0",
52
- ]
43
+ dev = ["pytest>=8.0.0", "pytest-asyncio>=0.23.0", "respx>=0.21.0"]
53
44
 
54
45
  [project.urls]
55
46
  Homepage = "https://meshapi.ai"
@@ -62,11 +53,7 @@ Changelog = "https://github.com/aifiesta/meshapi-python-sdk/blob/main/CHANGELOG.
62
53
  packages = ["meshapi"]
63
54
 
64
55
  [tool.hatch.build.targets.sdist]
65
- exclude = [
66
- "tests/",
67
- ".github/",
68
- "*.egg-info",
69
- ]
56
+ exclude = ["tests/", ".github/", "*.egg-info"]
70
57
 
71
58
  [tool.pytest.ini_options]
72
59
  asyncio_mode = "auto"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes