meshapi 0.1.0__tar.gz → 0.1.3__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.3}/PKG-INFO +28 -25
- {meshapi-0.1.0 → meshapi-0.1.3}/README.md +27 -24
- {meshapi-0.1.0 → meshapi-0.1.3}/TESTING.md +2 -2
- meshapi-0.1.3/livetests/batch_file.py +61 -0
- meshapi-0.1.3/livetests/compare.py +34 -0
- meshapi-0.1.3/livetests/config.py +43 -0
- meshapi-0.1.3/livetests/conftest.py +93 -0
- meshapi-0.1.3/livetests/pytest.ini +16 -0
- meshapi-0.1.3/livetests/requirements.txt +2 -0
- meshapi-0.1.3/livetests/responses.py +23 -0
- meshapi-0.1.3/livetests/test_chat.py +83 -0
- meshapi-0.1.3/livetests/test_errors.py +45 -0
- meshapi-0.1.3/livetests/test_feature_matrix.py +158 -0
- meshapi-0.1.3/livetests/test_inference_resources.py +174 -0
- meshapi-0.1.3/livetests/test_models.py +38 -0
- meshapi-0.1.3/livetests/test_stream.py +73 -0
- meshapi-0.1.3/livetests/test_templates.py +61 -0
- meshapi-0.1.3/livetests/tool_call.py +101 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/__init__.py +11 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/_http.py +93 -25
- {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/_types.py +89 -6
- meshapi-0.1.3/meshapi/resources/images.py +38 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/pyproject.toml +5 -18
- {meshapi-0.1.0 → meshapi-0.1.3}/.gitignore +0 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/CHANGELOG.md +0 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/_errors.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/__init__.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/batches.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/chat.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/compare.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/embeddings.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/files.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/models.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/responses.py +0 -0
- {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/templates.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshapi
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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
|
|
@@ -85,28 +85,15 @@ Get a key at [meshapi.ai](https://meshapi.ai). Data-plane keys are prefixed `rsk
|
|
|
85
85
|
| **Structured errors** | `MeshAPIError` with `error_code`, `status`, `request_id`, `retry_after_seconds`, and provider error details. |
|
|
86
86
|
| **Type-safe** | Every request and response is a Pydantic v2 model. Autocomplete in your editor, validation at the boundary. |
|
|
87
87
|
|
|
88
|
-
##
|
|
88
|
+
## Authentication
|
|
89
|
+
|
|
90
|
+
The SDK requires a Mesh API key (prefixed with `rsk_`) for all requests. You can obtain a key at [meshapi.ai](https://meshapi.ai).
|
|
89
91
|
|
|
90
92
|
```python
|
|
91
|
-
client = MeshAPI(
|
|
92
|
-
base_url="https://api.meshapi.ai", # required
|
|
93
|
-
token="rsk_...", # required: data-plane key or Supabase JWT
|
|
94
|
-
timeout=60.0, # default 60s
|
|
95
|
-
max_retries=3, # default 3, set 0 to disable
|
|
96
|
-
httpx_client=None, # optional: inject a custom httpx.Client
|
|
97
|
-
)
|
|
93
|
+
client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...")
|
|
98
94
|
```
|
|
99
95
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
Two auth realms. Use one client per realm.
|
|
103
|
-
|
|
104
|
-
| Realm | Token | Resources |
|
|
105
|
-
|---|---|---|
|
|
106
|
-
| **Data-plane** | `rsk_<ULID>` | `chat`, `responses`, `embeddings`, `compare`, `files`, `batches` |
|
|
107
|
-
| **Control-plane** | Supabase JWT | `templates`, `models` |
|
|
108
|
-
|
|
109
|
-
`models` accepts either token type.
|
|
96
|
+
This key provides access to all resources: `chat`, `responses`, `embeddings`, `compare`, `files`, `batches`, `models`, and `templates`.
|
|
110
97
|
|
|
111
98
|
## Chat completions
|
|
112
99
|
|
|
@@ -240,6 +227,25 @@ result = client.embeddings.create(
|
|
|
240
227
|
print(len(result.data[0].embedding))
|
|
241
228
|
```
|
|
242
229
|
|
|
230
|
+
## Image Generation
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from meshapi import ImageGenerationParams
|
|
234
|
+
|
|
235
|
+
result = client.images.generate(
|
|
236
|
+
ImageGenerationParams(
|
|
237
|
+
model="openai/gpt-image-1",
|
|
238
|
+
prompt="A watercolor of a fox in a snowy forest",
|
|
239
|
+
n=1,
|
|
240
|
+
size="1024x1024",
|
|
241
|
+
quality="high",
|
|
242
|
+
output_format="webp",
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
print(result.data[0].url)
|
|
247
|
+
```
|
|
248
|
+
|
|
243
249
|
## Compare (multi-model fanout)
|
|
244
250
|
|
|
245
251
|
Fire one prompt at several models and stream their replies in parallel.
|
|
@@ -339,10 +345,10 @@ Server-stored prompts with `{{variable}}` interpolation. Reference them by name
|
|
|
339
345
|
```python
|
|
340
346
|
from meshapi import MeshAPI, CreateTemplateParams, ChatCompletionParams, ChatMessage
|
|
341
347
|
|
|
342
|
-
# Manage templates with a control-plane JWT
|
|
343
|
-
|
|
348
|
+
# Manage templates with either a data-plane key or control-plane JWT
|
|
349
|
+
client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...")
|
|
344
350
|
|
|
345
|
-
|
|
351
|
+
client.templates.create(
|
|
346
352
|
CreateTemplateParams(
|
|
347
353
|
name="support-agent",
|
|
348
354
|
system="You are a support agent for {{company}}. Be concise and friendly.",
|
|
@@ -351,9 +357,6 @@ ctrl.templates.create(
|
|
|
351
357
|
)
|
|
352
358
|
)
|
|
353
359
|
|
|
354
|
-
# Use the template with a data-plane rsk_ key
|
|
355
|
-
client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...")
|
|
356
|
-
|
|
357
360
|
reply = client.chat.completions.create(
|
|
358
361
|
ChatCompletionParams(
|
|
359
362
|
messages=[ChatMessage(role="user", content="How do I reset my password?")],
|
|
@@ -51,28 +51,15 @@ Get a key at [meshapi.ai](https://meshapi.ai). Data-plane keys are prefixed `rsk
|
|
|
51
51
|
| **Structured errors** | `MeshAPIError` with `error_code`, `status`, `request_id`, `retry_after_seconds`, and provider error details. |
|
|
52
52
|
| **Type-safe** | Every request and response is a Pydantic v2 model. Autocomplete in your editor, validation at the boundary. |
|
|
53
53
|
|
|
54
|
-
##
|
|
54
|
+
## Authentication
|
|
55
|
+
|
|
56
|
+
The SDK requires a Mesh API key (prefixed with `rsk_`) for all requests. You can obtain a key at [meshapi.ai](https://meshapi.ai).
|
|
55
57
|
|
|
56
58
|
```python
|
|
57
|
-
client = MeshAPI(
|
|
58
|
-
base_url="https://api.meshapi.ai", # required
|
|
59
|
-
token="rsk_...", # required: data-plane key or Supabase JWT
|
|
60
|
-
timeout=60.0, # default 60s
|
|
61
|
-
max_retries=3, # default 3, set 0 to disable
|
|
62
|
-
httpx_client=None, # optional: inject a custom httpx.Client
|
|
63
|
-
)
|
|
59
|
+
client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...")
|
|
64
60
|
```
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
Two auth realms. Use one client per realm.
|
|
69
|
-
|
|
70
|
-
| Realm | Token | Resources |
|
|
71
|
-
|---|---|---|
|
|
72
|
-
| **Data-plane** | `rsk_<ULID>` | `chat`, `responses`, `embeddings`, `compare`, `files`, `batches` |
|
|
73
|
-
| **Control-plane** | Supabase JWT | `templates`, `models` |
|
|
74
|
-
|
|
75
|
-
`models` accepts either token type.
|
|
62
|
+
This key provides access to all resources: `chat`, `responses`, `embeddings`, `compare`, `files`, `batches`, `models`, and `templates`.
|
|
76
63
|
|
|
77
64
|
## Chat completions
|
|
78
65
|
|
|
@@ -206,6 +193,25 @@ result = client.embeddings.create(
|
|
|
206
193
|
print(len(result.data[0].embedding))
|
|
207
194
|
```
|
|
208
195
|
|
|
196
|
+
## Image Generation
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from meshapi import ImageGenerationParams
|
|
200
|
+
|
|
201
|
+
result = client.images.generate(
|
|
202
|
+
ImageGenerationParams(
|
|
203
|
+
model="openai/gpt-image-1",
|
|
204
|
+
prompt="A watercolor of a fox in a snowy forest",
|
|
205
|
+
n=1,
|
|
206
|
+
size="1024x1024",
|
|
207
|
+
quality="high",
|
|
208
|
+
output_format="webp",
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
print(result.data[0].url)
|
|
213
|
+
```
|
|
214
|
+
|
|
209
215
|
## Compare (multi-model fanout)
|
|
210
216
|
|
|
211
217
|
Fire one prompt at several models and stream their replies in parallel.
|
|
@@ -305,10 +311,10 @@ Server-stored prompts with `{{variable}}` interpolation. Reference them by name
|
|
|
305
311
|
```python
|
|
306
312
|
from meshapi import MeshAPI, CreateTemplateParams, ChatCompletionParams, ChatMessage
|
|
307
313
|
|
|
308
|
-
# Manage templates with a control-plane JWT
|
|
309
|
-
|
|
314
|
+
# Manage templates with either a data-plane key or control-plane JWT
|
|
315
|
+
client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...")
|
|
310
316
|
|
|
311
|
-
|
|
317
|
+
client.templates.create(
|
|
312
318
|
CreateTemplateParams(
|
|
313
319
|
name="support-agent",
|
|
314
320
|
system="You are a support agent for {{company}}. Be concise and friendly.",
|
|
@@ -317,9 +323,6 @@ ctrl.templates.create(
|
|
|
317
323
|
)
|
|
318
324
|
)
|
|
319
325
|
|
|
320
|
-
# Use the template with a data-plane rsk_ key
|
|
321
|
-
client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...")
|
|
322
|
-
|
|
323
326
|
reply = client.chat.completions.create(
|
|
324
327
|
ChatCompletionParams(
|
|
325
328
|
messages=[ChatMessage(role="user", content="How do I reset my password?")],
|
|
@@ -55,10 +55,10 @@ MESHAPI_BASE_URL=http://localhost:8000 MESHAPI_TOKEN=your_rsk_key pytest tests/i
|
|
|
55
55
|
|
|
56
56
|
## 3. Live Tests (Standalone)
|
|
57
57
|
|
|
58
|
-
Located in the separate `meshapi-
|
|
58
|
+
Located in the separate `meshapi-sdk-livetest/` directory. These are standalone scripts designed for quick, manual verification of the SDK against a live server.
|
|
59
59
|
|
|
60
60
|
**To run live tests:**
|
|
61
|
-
1. Navigate to the directory: `cd ../meshapi-
|
|
61
|
+
1. Navigate to the directory: `cd ../meshapi-sdk-livetest`
|
|
62
62
|
2. Configure the server and token in `config.py`.
|
|
63
63
|
3. Run individual scripts:
|
|
64
64
|
```bash
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from meshapi import (
|
|
2
|
+
UploadBatchFileParams,
|
|
3
|
+
BatchRequestItem,
|
|
4
|
+
CreateBatchParams,
|
|
5
|
+
)
|
|
6
|
+
from meshapi import CompareParams, ChatMessage
|
|
7
|
+
from meshapi import (
|
|
8
|
+
ChatCompletionParams,
|
|
9
|
+
ChatMessage,
|
|
10
|
+
Tool,
|
|
11
|
+
ToolFunction,
|
|
12
|
+
MeshAPI,
|
|
13
|
+
MeshAPIError,
|
|
14
|
+
)
|
|
15
|
+
from config import BASE_URL, TOKEN
|
|
16
|
+
|
|
17
|
+
client = MeshAPI(base_url=BASE_URL, token=TOKEN)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# 1. Upload the batch input
|
|
21
|
+
# file = client.files.upload(
|
|
22
|
+
# UploadBatchFileParams(
|
|
23
|
+
# purpose="batch",
|
|
24
|
+
# requests=[
|
|
25
|
+
# BatchRequestItem(
|
|
26
|
+
# custom_id="req-1",
|
|
27
|
+
# body={
|
|
28
|
+
# "model": "openai/gpt-4.1-mini",
|
|
29
|
+
# "messages": [{"role": "user", "content": "Say hi."}],
|
|
30
|
+
# },
|
|
31
|
+
# ),
|
|
32
|
+
# BatchRequestItem(
|
|
33
|
+
# custom_id="req-2",
|
|
34
|
+
# body={
|
|
35
|
+
# "model": "openai/gpt-4.1-mini",
|
|
36
|
+
# "messages": [{"role": "user", "content": "Say bye."}],
|
|
37
|
+
# },
|
|
38
|
+
# ),
|
|
39
|
+
# ],
|
|
40
|
+
# )
|
|
41
|
+
# )
|
|
42
|
+
|
|
43
|
+
# 2. Create the batch
|
|
44
|
+
# batch = client.batches.create(
|
|
45
|
+
# CreateBatchParams(
|
|
46
|
+
# input_file_id=file.id,
|
|
47
|
+
# endpoint="/v1/chat/completions",
|
|
48
|
+
# completion_window="24h",
|
|
49
|
+
# )
|
|
50
|
+
# )
|
|
51
|
+
|
|
52
|
+
# print("batch id is: ", batch.id)
|
|
53
|
+
|
|
54
|
+
# 3. Poll later
|
|
55
|
+
status = client.batches.get("batch_6a038d038ad481908c22ced67b7001c1")
|
|
56
|
+
print("batch status is: ", status)
|
|
57
|
+
|
|
58
|
+
if status.status == "completed" and status.output_file_id:
|
|
59
|
+
output_bytes = client.files.content(status.output_file_id)
|
|
60
|
+
print(output_bytes)
|
|
61
|
+
# output_bytes is JSONL
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from meshapi import CompareParams, ChatMessage
|
|
2
|
+
from meshapi import (
|
|
3
|
+
ChatCompletionParams,
|
|
4
|
+
ChatMessage,
|
|
5
|
+
Tool,
|
|
6
|
+
ToolFunction,
|
|
7
|
+
MeshAPI,
|
|
8
|
+
MeshAPIError,
|
|
9
|
+
)
|
|
10
|
+
from config import BASE_URL, TOKEN
|
|
11
|
+
|
|
12
|
+
client = MeshAPI(base_url=BASE_URL, token=TOKEN)
|
|
13
|
+
|
|
14
|
+
stream = client.compare.stream(
|
|
15
|
+
CompareParams(
|
|
16
|
+
models=[
|
|
17
|
+
# "openai/gpt-4o-mini",
|
|
18
|
+
# "anthropic/claude-sonnet-4.5",
|
|
19
|
+
"google/gemini-2.5-flash",
|
|
20
|
+
"openai/gpt-4o",
|
|
21
|
+
],
|
|
22
|
+
messages=[
|
|
23
|
+
ChatMessage(
|
|
24
|
+
role="user", content="Summarize this paragraph in one sentence: ..."
|
|
25
|
+
)
|
|
26
|
+
],
|
|
27
|
+
stream=True,
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
for event in stream:
|
|
32
|
+
print(event)
|
|
33
|
+
if event.delta:
|
|
34
|
+
print(event.delta, end="", flush=True)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
SDK_ROOT = Path(__file__).resolve().parents[1]
|
|
9
|
+
if str(SDK_ROOT) not in sys.path:
|
|
10
|
+
sys.path.insert(0, str(SDK_ROOT))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load_shared_env() -> dict[str, str]:
|
|
14
|
+
env_path = Path(__file__).resolve().parents[1] / ".env.livetest"
|
|
15
|
+
if not env_path.exists():
|
|
16
|
+
env_path = Path(__file__).resolve().parents[2] / ".env.livetest"
|
|
17
|
+
|
|
18
|
+
values: dict[str, str] = {}
|
|
19
|
+
if not env_path.exists():
|
|
20
|
+
return values
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
for raw_line in env_path.read_text().splitlines():
|
|
24
|
+
line = raw_line.strip()
|
|
25
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
26
|
+
continue
|
|
27
|
+
key, value = line.split("=", 1)
|
|
28
|
+
key = key.strip()
|
|
29
|
+
value = value.strip().strip('"').strip("'")
|
|
30
|
+
if key:
|
|
31
|
+
values[key] = value
|
|
32
|
+
return values
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_SHARED_ENV = _load_shared_env()
|
|
36
|
+
|
|
37
|
+
BASE_URL = os.getenv("MESHAPI_BASE_URL") or _SHARED_ENV.get("MESHAPI_BASE_URL", "http://localhost:8000")
|
|
38
|
+
TOKEN = os.getenv("MESHAPI_TOKEN") or _SHARED_ENV.get("MESHAPI_TOKEN", "rsk_01KN96KQWDPF2X1E9CP8567JY4")
|
|
39
|
+
MODEL = os.getenv("MESHAPI_MODEL") or _SHARED_ENV.get("MESHAPI_MODEL", "openai/gpt-4o-mini")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_env(name: str, default: str | None = None) -> str | None:
|
|
43
|
+
return os.getenv(name) or _SHARED_ENV.get(name, default)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pytest conftest for the MeshAPI Python live-test suite.
|
|
3
|
+
|
|
4
|
+
Registers shared fixtures so every test_*.py file can import
|
|
5
|
+
`client`, `model`, and helpers directly without touching config.py.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Make sure the local SDK is importable even when running via pytest from CI
|
|
19
|
+
# (the requirements.txt already does `pip install -e ../meshapi-python-sdk`,
|
|
20
|
+
# but this is a safety net for non-venv runs).
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
SDK_ROOT = Path(__file__).resolve().parents[1]
|
|
23
|
+
if str(SDK_ROOT) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(SDK_ROOT))
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Import after sys.path is set up
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
from config import BASE_URL, TOKEN, MODEL, get_env # noqa: E402
|
|
30
|
+
from meshapi import MeshAPI # noqa: E402
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Session-scoped shared client fixture
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
@pytest.fixture(scope="session")
|
|
38
|
+
def client() -> MeshAPI:
|
|
39
|
+
"""Return a single MeshAPI client reused for all tests in the session."""
|
|
40
|
+
return MeshAPI(base_url=BASE_URL, token=TOKEN)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture(scope="session")
|
|
44
|
+
def model() -> str:
|
|
45
|
+
return MODEL
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.fixture(scope="session")
|
|
49
|
+
def embeddings_model() -> str:
|
|
50
|
+
return get_env("MESHAPI_EMBEDDINGS_MODEL", "openai/text-embedding-3-small")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture(scope="session")
|
|
55
|
+
def second_model() -> str:
|
|
56
|
+
"""A second distinct model for compare tests. Defaults to a different model from MODEL."""
|
|
57
|
+
default = "anthropic/claude-haiku-4-5" if MODEL == "openai/gpt-4o-mini" else "openai/gpt-4o-mini"
|
|
58
|
+
return get_env("MESHAPI_SECOND_MODEL", default)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.fixture(scope="session")
|
|
62
|
+
def image_url() -> str | None:
|
|
63
|
+
return get_env("MESHAPI_IMAGE_URL")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.fixture(scope="session")
|
|
67
|
+
def audio_b64() -> str | None:
|
|
68
|
+
return get_env("MESHAPI_INPUT_AUDIO_B64")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.fixture(scope="session")
|
|
72
|
+
def audio_format() -> str:
|
|
73
|
+
return get_env("MESHAPI_INPUT_AUDIO_FORMAT", "wav")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.fixture(scope="session")
|
|
77
|
+
def audio_out_model() -> str | None:
|
|
78
|
+
return get_env("MESHAPI_AUDIO_OUT_MODEL")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.fixture(scope="session")
|
|
82
|
+
def image_gen_model() -> str | None:
|
|
83
|
+
return get_env("MESHAPI_IMAGE_GEN_MODEL")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Helpers available as fixtures
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
@pytest.fixture(scope="session")
|
|
91
|
+
def unique_tag() -> str:
|
|
92
|
+
"""A stable unique tag for the entire test session (e.g. for file uploads)."""
|
|
93
|
+
return f"ci-{int(time.time())}"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[pytest]
|
|
2
|
+
# Discover all live-test files in this directory
|
|
3
|
+
testpaths = .
|
|
4
|
+
python_files = test_*.py
|
|
5
|
+
python_classes = Test*
|
|
6
|
+
python_functions = test_*
|
|
7
|
+
|
|
8
|
+
# Never import from parent packages accidentally
|
|
9
|
+
addopts =
|
|
10
|
+
--tb=short
|
|
11
|
+
--no-header
|
|
12
|
+
-v
|
|
13
|
+
|
|
14
|
+
# Treat warnings as informational only during live tests
|
|
15
|
+
filterwarnings =
|
|
16
|
+
ignore::DeprecationWarning
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from meshapi import ResponsesParams
|
|
2
|
+
from meshapi import (
|
|
3
|
+
ChatCompletionParams,
|
|
4
|
+
ChatMessage,
|
|
5
|
+
Tool,
|
|
6
|
+
ToolFunction,
|
|
7
|
+
MeshAPI,
|
|
8
|
+
MeshAPIError,
|
|
9
|
+
)
|
|
10
|
+
from config import BASE_URL, TOKEN
|
|
11
|
+
|
|
12
|
+
client = MeshAPI(base_url=BASE_URL, token=TOKEN)
|
|
13
|
+
|
|
14
|
+
reply = client.responses.create(
|
|
15
|
+
ResponsesParams(
|
|
16
|
+
model="openai/o4-mini",
|
|
17
|
+
input="Explain the halting problem in two sentences.",
|
|
18
|
+
reasoning={"effort": "medium"},
|
|
19
|
+
max_output_tokens=512,
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
print(reply.output)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Live tests: Chat completions (non-streaming)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from meshapi import MeshAPI, ChatCompletionParams, ChatMessage
|
|
7
|
+
from meshapi._types import CreateTemplateParams
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_chat_basic(client: MeshAPI, model: str) -> None:
|
|
11
|
+
resp = client.chat.completions.create(
|
|
12
|
+
ChatCompletionParams(
|
|
13
|
+
model=model,
|
|
14
|
+
messages=[ChatMessage(role="user", content="What is the capital of France? Reply in one word.")],
|
|
15
|
+
max_tokens=10,
|
|
16
|
+
temperature=0,
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
content = resp.choices[0].message.content
|
|
20
|
+
role = resp.choices[0].message.role
|
|
21
|
+
assert role == "assistant", f"expected role 'assistant', got {role!r}"
|
|
22
|
+
assert content, "expected non-empty content"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_chat_multi_turn(client: MeshAPI, model: str) -> None:
|
|
26
|
+
resp = client.chat.completions.create(
|
|
27
|
+
ChatCompletionParams(
|
|
28
|
+
model=model,
|
|
29
|
+
messages=[
|
|
30
|
+
ChatMessage(role="user", content="My favourite color is blue. Remember this."),
|
|
31
|
+
ChatMessage(role="assistant", content="Got it! Your favourite color is blue."),
|
|
32
|
+
ChatMessage(role="user", content="What is my favourite color? Reply in 3 words max."),
|
|
33
|
+
],
|
|
34
|
+
max_tokens=20,
|
|
35
|
+
temperature=0,
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
content = resp.choices[0].message.content
|
|
39
|
+
assert content, "expected non-empty content in multi-turn response"
|
|
40
|
+
assert resp.choices[0].finish_reason in ("stop", "length")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_chat_with_template(client: MeshAPI, model: str) -> None:
|
|
44
|
+
import uuid
|
|
45
|
+
|
|
46
|
+
name = f"py-livetest-chat-{uuid.uuid4().hex[:8]}"
|
|
47
|
+
tmpl = client.templates.create(
|
|
48
|
+
CreateTemplateParams(
|
|
49
|
+
name=name,
|
|
50
|
+
system="You are a {{role}}. Always reply in exactly one sentence.",
|
|
51
|
+
variables=["role"],
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
try:
|
|
55
|
+
resp = client.chat.completions.create(
|
|
56
|
+
ChatCompletionParams(
|
|
57
|
+
model=model,
|
|
58
|
+
messages=[ChatMessage(role="user", content="Introduce yourself.")],
|
|
59
|
+
template=tmpl.name,
|
|
60
|
+
variables={"role": "friendly pirate"},
|
|
61
|
+
max_tokens=80,
|
|
62
|
+
temperature=0,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
content = resp.choices[0].message.content
|
|
66
|
+
assert content, "expected non-empty templated chat response"
|
|
67
|
+
finally:
|
|
68
|
+
client.templates.delete(tmpl.id)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_chat_response_fields(client: MeshAPI, model: str) -> None:
|
|
72
|
+
resp = client.chat.completions.create(
|
|
73
|
+
ChatCompletionParams(
|
|
74
|
+
model=model,
|
|
75
|
+
messages=[ChatMessage(role="user", content="Say hello.")],
|
|
76
|
+
max_tokens=10,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
assert resp.id, "response should have an id"
|
|
80
|
+
assert resp.model, "response should have a model field"
|
|
81
|
+
assert resp.usage is not None, "response should include usage"
|
|
82
|
+
assert resp.choices, "response should have choices"
|
|
83
|
+
assert resp.choices[0].message.role == "assistant"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Live tests: error handling and exception types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from meshapi import MeshAPI, MeshAPIError, ChatCompletionParams, ChatMessage
|
|
7
|
+
from config import BASE_URL
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_error_unauthorized_chat(model: str) -> None:
|
|
11
|
+
bad = MeshAPI(base_url=BASE_URL, token="rsk_INVALID_TOKEN")
|
|
12
|
+
with pytest.raises(MeshAPIError) as exc_info:
|
|
13
|
+
bad.chat.completions.create(
|
|
14
|
+
ChatCompletionParams(
|
|
15
|
+
model=model,
|
|
16
|
+
messages=[ChatMessage(role="user", content="hello")],
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
err = exc_info.value
|
|
20
|
+
assert err.status == 401
|
|
21
|
+
assert err.error_code, "expected an error_code in the response"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_error_unauthorized_models() -> None:
|
|
25
|
+
bad = MeshAPI(base_url=BASE_URL, token="rsk_INVALID_TOKEN")
|
|
26
|
+
with pytest.raises(MeshAPIError) as exc_info:
|
|
27
|
+
bad.models.list()
|
|
28
|
+
assert exc_info.value.status == 401
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_error_not_found_template(client: MeshAPI) -> None:
|
|
32
|
+
with pytest.raises(MeshAPIError) as exc_info:
|
|
33
|
+
client.templates.get("tmpl_nonexistent_id_000000")
|
|
34
|
+
assert exc_info.value.status == 404
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_error_is_exception(model: str) -> None:
|
|
38
|
+
bad = MeshAPI(base_url=BASE_URL, token="rsk_INVALID_TOKEN")
|
|
39
|
+
with pytest.raises(Exception):
|
|
40
|
+
bad.chat.completions.create(
|
|
41
|
+
ChatCompletionParams(
|
|
42
|
+
model=model,
|
|
43
|
+
messages=[ChatMessage(role="user", content="hello")],
|
|
44
|
+
)
|
|
45
|
+
)
|