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.
Files changed (35) hide show
  1. {meshapi-0.1.0 → meshapi-0.1.3}/PKG-INFO +28 -25
  2. {meshapi-0.1.0 → meshapi-0.1.3}/README.md +27 -24
  3. {meshapi-0.1.0 → meshapi-0.1.3}/TESTING.md +2 -2
  4. meshapi-0.1.3/livetests/batch_file.py +61 -0
  5. meshapi-0.1.3/livetests/compare.py +34 -0
  6. meshapi-0.1.3/livetests/config.py +43 -0
  7. meshapi-0.1.3/livetests/conftest.py +93 -0
  8. meshapi-0.1.3/livetests/pytest.ini +16 -0
  9. meshapi-0.1.3/livetests/requirements.txt +2 -0
  10. meshapi-0.1.3/livetests/responses.py +23 -0
  11. meshapi-0.1.3/livetests/test_chat.py +83 -0
  12. meshapi-0.1.3/livetests/test_errors.py +45 -0
  13. meshapi-0.1.3/livetests/test_feature_matrix.py +158 -0
  14. meshapi-0.1.3/livetests/test_inference_resources.py +174 -0
  15. meshapi-0.1.3/livetests/test_models.py +38 -0
  16. meshapi-0.1.3/livetests/test_stream.py +73 -0
  17. meshapi-0.1.3/livetests/test_templates.py +61 -0
  18. meshapi-0.1.3/livetests/tool_call.py +101 -0
  19. {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/__init__.py +11 -0
  20. {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/_http.py +93 -25
  21. {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/_types.py +89 -6
  22. meshapi-0.1.3/meshapi/resources/images.py +38 -0
  23. {meshapi-0.1.0 → meshapi-0.1.3}/pyproject.toml +5 -18
  24. {meshapi-0.1.0 → meshapi-0.1.3}/.gitignore +0 -0
  25. {meshapi-0.1.0 → meshapi-0.1.3}/CHANGELOG.md +0 -0
  26. {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/_errors.py +0 -0
  27. {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/__init__.py +0 -0
  28. {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/batches.py +0 -0
  29. {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/chat.py +0 -0
  30. {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/compare.py +0 -0
  31. {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/embeddings.py +0 -0
  32. {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/files.py +0 -0
  33. {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/models.py +0 -0
  34. {meshapi-0.1.0 → meshapi-0.1.3}/meshapi/resources/responses.py +0 -0
  35. {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.0
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
- ## Configuration
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
- `AsyncMeshAPI` takes the same arguments (with `async_httpx_client` instead of `httpx_client`) and supports `async with` for clean shutdown.
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
- ctrl = MeshAPI(base_url="https://api.meshapi.ai", token=supabase_session_jwt)
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
- ctrl.templates.create(
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
- ## Configuration
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
- `AsyncMeshAPI` takes the same arguments (with `async_httpx_client` instead of `httpx_client`) and supports `async with` for clean shutdown.
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
- ctrl = MeshAPI(base_url="https://api.meshapi.ai", token=supabase_session_jwt)
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
- ctrl.templates.create(
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-python-livetest/` directory. These are standalone scripts designed for quick, manual verification of the SDK against a live server.
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-python-livetest`
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,2 @@
1
+ -e ..[dev]
2
+ pytest>=8.0.0
@@ -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
+ )