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.
- {meshapi-0.1.0 → meshapi-0.1.2}/PKG-INFO +1 -1
- {meshapi-0.1.0 → meshapi-0.1.2}/meshapi/_http.py +85 -25
- {meshapi-0.1.0 → meshapi-0.1.2}/meshapi/_types.py +31 -1
- {meshapi-0.1.0 → meshapi-0.1.2}/pyproject.toml +5 -18
- {meshapi-0.1.0 → meshapi-0.1.2}/.gitignore +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/CHANGELOG.md +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/README.md +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/TESTING.md +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/meshapi/__init__.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/meshapi/_errors.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/meshapi/resources/__init__.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/meshapi/resources/batches.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/meshapi/resources/chat.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/meshapi/resources/compare.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/meshapi/resources/embeddings.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/meshapi/resources/files.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/meshapi/resources/models.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/meshapi/resources/responses.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.2}/meshapi/resources/templates.py +0 -0
|
@@ -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
|
|
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"
|
|
98
|
+
if isinstance(parsed, dict) and parsed.get("error") is not None:
|
|
82
99
|
err = parsed["error"]
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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(
|
|
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"
|
|
134
|
+
if isinstance(parsed, dict) and parsed.get("error") is not None:
|
|
104
135
|
err = parsed["error"]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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[
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|