meshapi 0.1.4__tar.gz → 0.1.5__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 (43) hide show
  1. {meshapi-0.1.4 → meshapi-0.1.5}/CLAUDE.md +29 -0
  2. {meshapi-0.1.4 → meshapi-0.1.5}/PKG-INFO +95 -1
  3. {meshapi-0.1.4 → meshapi-0.1.5}/README.md +94 -0
  4. meshapi-0.1.5/livetests/test_audio.py +24 -0
  5. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/test_inference_resources.py +0 -1
  6. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/test_rag.py +1 -1
  7. meshapi-0.1.5/livetests/test_structured_output.py +86 -0
  8. meshapi-0.1.5/livetests/test_video.py +30 -0
  9. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/__init__.py +44 -0
  10. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/_http.py +34 -0
  11. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/_types.py +217 -0
  12. meshapi-0.1.5/meshapi/resources/audio.py +134 -0
  13. meshapi-0.1.5/meshapi/resources/videos.py +57 -0
  14. {meshapi-0.1.4 → meshapi-0.1.5}/pyproject.toml +1 -1
  15. {meshapi-0.1.4 → meshapi-0.1.5}/.gitignore +0 -0
  16. {meshapi-0.1.4 → meshapi-0.1.5}/CHANGELOG.md +0 -0
  17. {meshapi-0.1.4 → meshapi-0.1.5}/TESTING.md +0 -0
  18. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/compare.py +0 -0
  19. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/config.py +0 -0
  20. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/conftest.py +0 -0
  21. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/pytest.ini +0 -0
  22. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/requirements.txt +0 -0
  23. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/responses.py +0 -0
  24. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/test_chat.py +0 -0
  25. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/test_errors.py +0 -0
  26. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/test_feature_matrix.py +0 -0
  27. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/test_models.py +0 -0
  28. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/test_realtime.py +0 -0
  29. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/test_stream.py +0 -0
  30. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/test_templates.py +0 -0
  31. {meshapi-0.1.4 → meshapi-0.1.5}/livetests/tool_call.py +0 -0
  32. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/_errors.py +0 -0
  33. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/resources/__init__.py +0 -0
  34. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/resources/batches.py +0 -0
  35. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/resources/chat.py +0 -0
  36. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/resources/compare.py +0 -0
  37. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/resources/embeddings.py +0 -0
  38. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/resources/images.py +0 -0
  39. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/resources/models.py +0 -0
  40. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/resources/rag.py +0 -0
  41. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/resources/realtime.py +0 -0
  42. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/resources/responses.py +0 -0
  43. {meshapi-0.1.4 → meshapi-0.1.5}/meshapi/resources/templates.py +0 -0
@@ -147,6 +147,35 @@ pytest test_rag.py::test_rag_upload_embed_search -v
147
147
  | `test_rag.py` | RAG upload → embed → list → search |
148
148
  | `test_realtime.py` | WebSocket connect/close, session.created, session.update, error envelopes, iterator API, async variants |
149
149
 
150
+ ### Available live test files (updated)
151
+
152
+ | File | What it tests |
153
+ |------|---------------|
154
+ | `test_chat.py` | Chat completions (basic, tools, multi-turn) |
155
+ | `test_stream.py` | Streaming chat and responses |
156
+ | `test_models.py` | Model listing |
157
+ | `test_templates.py` | Template CRUD lifecycle |
158
+ | `test_inference_resources.py` | Embeddings, responses |
159
+ | `test_errors.py` | 401/404 error handling |
160
+ | `test_feature_matrix.py` | Cross-model feature matrix |
161
+ | `test_rag.py` | RAG upload → embed → list → search |
162
+ | `test_realtime.py` | WebSocket connect/close, session lifecycle |
163
+ | `test_audio.py` | TTS synthesize, voice listing |
164
+ | `test_video.py` | Video list, generate → retrieve |
165
+
166
+ ---
167
+
168
+ ## Contribution checklist
169
+
170
+ Every SDK change — however small — must include all of the following before merging:
171
+
172
+ 1. **Live tests** — add or update `livetests/test_<resource>.py` to cover the new/changed behaviour.
173
+ 2. **Unit / contract tests** — if the change affects request/response serialisation or error handling, add a test in `tests/unit/` or `tests/contract/`.
174
+ 3. **README** — update `README.md` with a usage example for any new or changed public surface.
175
+ 4. **meshapi-docs** — open a follow-up PR (or note in the PR description) to update the [meshapi-docs](https://github.com/aifiesta/meshapi-docs) repository so the developer documentation stays in sync.
176
+
177
+ ---
178
+
150
179
  ### RAG live test notes
151
180
 
152
181
  `test_rag_upload_embed_search` does the following:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshapi
3
- Version: 0.1.4
3
+ Version: 0.1.5
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
@@ -80,6 +80,8 @@ Get a key at [meshapi.ai](https://meshapi.ai). Data-plane keys are prefixed `rsk
80
80
  | **Reasoning models** | First-class `responses` API with `reasoning.effort` and `max_output_tokens`. |
81
81
  | **Embeddings** | Drop-in OpenAI-compatible embeddings endpoint. |
82
82
  | **Multi-model compare** | Fire one prompt at N models in parallel and stream their replies side by side. |
83
+ | **Audio** | Text-to-speech, speech-to-text, transcription translation, and voice listing. |
84
+ | **Video** | Submit and poll async video generation tasks. |
83
85
  | **RAG** | Upload files, embed them, and run vector search — all through the same client. |
84
86
  | **Batches** | Async bulk inference jobs at discounted rates with inline request submission. |
85
87
  | **Prompt templates** | Server-stored prompts with `{{variable}}` slots. Update prompts without redeploying. |
@@ -199,6 +201,91 @@ result = client.embeddings.create(
199
201
  print(len(result.data[0].embedding))
200
202
  ```
201
203
 
204
+ ## Audio (TTS, STT, voices)
205
+
206
+ ```python
207
+ from meshapi import SpeechParams, TranscriptionParams, ListVoicesParams
208
+
209
+ # Text-to-speech — returns raw audio bytes
210
+ audio_bytes = client.audio.synthesize(
211
+ SpeechParams(
212
+ input="Hello from MeshAPI.",
213
+ model="sarvam/bulbul:v2",
214
+ voice="meera",
215
+ )
216
+ )
217
+ with open("output.wav", "wb") as f:
218
+ f.write(audio_bytes)
219
+
220
+ # Speech-to-text — submit transcription job
221
+ result = client.audio.transcribe(
222
+ TranscriptionParams(
223
+ model="sarvam/saaras:v3",
224
+ file=open("audio.wav", "rb").read(),
225
+ file_name="audio.wav",
226
+ language="en",
227
+ )
228
+ )
229
+ print(result.text)
230
+
231
+ # Translate audio to English
232
+ translated = client.audio.translate(
233
+ TranscriptionParams(
234
+ model="sarvam/saaras:v3",
235
+ file=open("audio.wav", "rb").read(),
236
+ file_name="audio.wav",
237
+ )
238
+ )
239
+ print(translated.text)
240
+
241
+ # List available voices
242
+ voices = client.audio.list_voices(ListVoicesParams(page_size=10))
243
+
244
+ # Get a specific voice
245
+ voice = client.audio.get_voice("voice-id")
246
+ ```
247
+
248
+ ### Async audio
249
+
250
+ ```python
251
+ audio_bytes = await client.audio.synthesize(SpeechParams(input="Hello!", model="sarvam/bulbul:v2"))
252
+ voices = await client.audio.list_voices(ListVoicesParams())
253
+ ```
254
+
255
+ ## Video generation
256
+
257
+ ```python
258
+ from meshapi import VideoGenerationParams, VideoContentItem, ListVideoGenerationsParams
259
+ import time
260
+
261
+ # Submit a video generation task
262
+ task = client.videos.generate(
263
+ VideoGenerationParams(
264
+ model="byteplus/dreamina-seedance-2-0",
265
+ content=[VideoContentItem(type="text", text="A serene mountain lake at sunrise")],
266
+ )
267
+ )
268
+ print(f"Task ID: {task.id}")
269
+
270
+ # Poll until complete
271
+ while True:
272
+ status = client.videos.retrieve(task.id)
273
+ if status.status in ("succeeded", "failed"):
274
+ break
275
+ time.sleep(5)
276
+
277
+ # List past generation tasks
278
+ listing = client.videos.list(ListVideoGenerationsParams(limit=20))
279
+ print(f"{listing.total} total tasks")
280
+ ```
281
+
282
+ ### Async video
283
+
284
+ ```python
285
+ task = await client.videos.generate(VideoGenerationParams(...))
286
+ status = await client.videos.retrieve(task.id)
287
+ ```
288
+
202
289
  ## Image generation
203
290
 
204
291
  ```python
@@ -433,6 +520,13 @@ from meshapi import (
433
520
  RagFileStatus, RagFileListResponse,
434
521
  BulkEmbedRequest, BulkEmbedResponse,
435
522
  SearchRequest, SearchResponse, SearchResult,
523
+ # audio
524
+ SpeechParams, TranscriptionParams, TranscriptionTranslateParams,
525
+ TranscriptionResponse, ListVoicesParams,
526
+ # video
527
+ VideoGenerationParams, VideoContentItem,
528
+ CreateVideoGenerationResponse, VideoTaskResponse, VideoTaskListResponse,
529
+ ListVideoGenerationsParams,
436
530
  # models
437
531
  ModelInfo, ModelPricing,
438
532
  # templates
@@ -43,6 +43,8 @@ Get a key at [meshapi.ai](https://meshapi.ai). Data-plane keys are prefixed `rsk
43
43
  | **Reasoning models** | First-class `responses` API with `reasoning.effort` and `max_output_tokens`. |
44
44
  | **Embeddings** | Drop-in OpenAI-compatible embeddings endpoint. |
45
45
  | **Multi-model compare** | Fire one prompt at N models in parallel and stream their replies side by side. |
46
+ | **Audio** | Text-to-speech, speech-to-text, transcription translation, and voice listing. |
47
+ | **Video** | Submit and poll async video generation tasks. |
46
48
  | **RAG** | Upload files, embed them, and run vector search — all through the same client. |
47
49
  | **Batches** | Async bulk inference jobs at discounted rates with inline request submission. |
48
50
  | **Prompt templates** | Server-stored prompts with `{{variable}}` slots. Update prompts without redeploying. |
@@ -162,6 +164,91 @@ result = client.embeddings.create(
162
164
  print(len(result.data[0].embedding))
163
165
  ```
164
166
 
167
+ ## Audio (TTS, STT, voices)
168
+
169
+ ```python
170
+ from meshapi import SpeechParams, TranscriptionParams, ListVoicesParams
171
+
172
+ # Text-to-speech — returns raw audio bytes
173
+ audio_bytes = client.audio.synthesize(
174
+ SpeechParams(
175
+ input="Hello from MeshAPI.",
176
+ model="sarvam/bulbul:v2",
177
+ voice="meera",
178
+ )
179
+ )
180
+ with open("output.wav", "wb") as f:
181
+ f.write(audio_bytes)
182
+
183
+ # Speech-to-text — submit transcription job
184
+ result = client.audio.transcribe(
185
+ TranscriptionParams(
186
+ model="sarvam/saaras:v3",
187
+ file=open("audio.wav", "rb").read(),
188
+ file_name="audio.wav",
189
+ language="en",
190
+ )
191
+ )
192
+ print(result.text)
193
+
194
+ # Translate audio to English
195
+ translated = client.audio.translate(
196
+ TranscriptionParams(
197
+ model="sarvam/saaras:v3",
198
+ file=open("audio.wav", "rb").read(),
199
+ file_name="audio.wav",
200
+ )
201
+ )
202
+ print(translated.text)
203
+
204
+ # List available voices
205
+ voices = client.audio.list_voices(ListVoicesParams(page_size=10))
206
+
207
+ # Get a specific voice
208
+ voice = client.audio.get_voice("voice-id")
209
+ ```
210
+
211
+ ### Async audio
212
+
213
+ ```python
214
+ audio_bytes = await client.audio.synthesize(SpeechParams(input="Hello!", model="sarvam/bulbul:v2"))
215
+ voices = await client.audio.list_voices(ListVoicesParams())
216
+ ```
217
+
218
+ ## Video generation
219
+
220
+ ```python
221
+ from meshapi import VideoGenerationParams, VideoContentItem, ListVideoGenerationsParams
222
+ import time
223
+
224
+ # Submit a video generation task
225
+ task = client.videos.generate(
226
+ VideoGenerationParams(
227
+ model="byteplus/dreamina-seedance-2-0",
228
+ content=[VideoContentItem(type="text", text="A serene mountain lake at sunrise")],
229
+ )
230
+ )
231
+ print(f"Task ID: {task.id}")
232
+
233
+ # Poll until complete
234
+ while True:
235
+ status = client.videos.retrieve(task.id)
236
+ if status.status in ("succeeded", "failed"):
237
+ break
238
+ time.sleep(5)
239
+
240
+ # List past generation tasks
241
+ listing = client.videos.list(ListVideoGenerationsParams(limit=20))
242
+ print(f"{listing.total} total tasks")
243
+ ```
244
+
245
+ ### Async video
246
+
247
+ ```python
248
+ task = await client.videos.generate(VideoGenerationParams(...))
249
+ status = await client.videos.retrieve(task.id)
250
+ ```
251
+
165
252
  ## Image generation
166
253
 
167
254
  ```python
@@ -396,6 +483,13 @@ from meshapi import (
396
483
  RagFileStatus, RagFileListResponse,
397
484
  BulkEmbedRequest, BulkEmbedResponse,
398
485
  SearchRequest, SearchResponse, SearchResult,
486
+ # audio
487
+ SpeechParams, TranscriptionParams, TranscriptionTranslateParams,
488
+ TranscriptionResponse, ListVoicesParams,
489
+ # video
490
+ VideoGenerationParams, VideoContentItem,
491
+ CreateVideoGenerationResponse, VideoTaskResponse, VideoTaskListResponse,
492
+ ListVideoGenerationsParams,
399
493
  # models
400
494
  ModelInfo, ModelPricing,
401
495
  # templates
@@ -0,0 +1,24 @@
1
+ """Live tests for /v1/audio/* endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from meshapi import MeshAPI, SpeechParams, ListVoicesParams
8
+
9
+ TTS_MODEL = os.environ.get("MESHAPI_TTS_MODEL", "sarvam/bulbul:v2")
10
+ STT_MODEL = os.environ.get("MESHAPI_STT_MODEL", "sarvam/saaras:v3")
11
+
12
+
13
+ def test_audio_synthesize(client: MeshAPI) -> None:
14
+ params = SpeechParams(input="Hello from MeshAPI audio test.", model=TTS_MODEL)
15
+ audio_bytes = client.audio.synthesize(params)
16
+ assert isinstance(audio_bytes, bytes)
17
+ assert len(audio_bytes) > 0
18
+ print(f"[PASS] audio.synthesize -> {len(audio_bytes)} bytes")
19
+
20
+
21
+ def test_audio_list_voices(client: MeshAPI) -> None:
22
+ voices = client.audio.list_voices(ListVoicesParams(page_size=5))
23
+ assert voices is not None
24
+ print(f"[PASS] audio.list_voices -> {type(voices)}")
@@ -98,7 +98,6 @@ def test_compare_create(client: MeshAPI, model: str, second_model: str) -> None:
98
98
  assert len(result.results) == 2, f"expected 2 results, got {len(result.results)}"
99
99
 
100
100
 
101
- @pytest.mark.xfail(reason="server-side SQLAlchemy session concurrency issue when compare tests run back-to-back")
102
101
  def test_compare_stream(client: MeshAPI, model: str, second_model: str) -> None:
103
102
  events = list(
104
103
  client.compare.stream(
@@ -102,7 +102,7 @@ def test_rag_upload_embed_search(client: MeshAPI) -> None:
102
102
  # ── Step 7: Search ──
103
103
  search_resp = client.rag.search(
104
104
  SearchRequest(
105
- query="meshapi rag livetest py",
105
+ query="summary",
106
106
  top_k=5,
107
107
  file_ids=[upload.file_id],
108
108
  )
@@ -0,0 +1,86 @@
1
+ """Live tests: Structured output (response_format with JSON schema)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import pytest
8
+ from meshapi import MeshAPI, ChatCompletionParams, ChatMessage
9
+
10
+ MODELS = [
11
+ "openai/gpt-4o-mini",
12
+ "google/gemini-3-flash-preview",
13
+ ]
14
+
15
+ _SCHEMA = {
16
+ "type": "json_schema",
17
+ "json_schema": {
18
+ "name": "country_info",
19
+ "schema": {
20
+ "type": "object",
21
+ "properties": {
22
+ "capital": {"type": "string"},
23
+ "country": {"type": "string"},
24
+ },
25
+ "required": ["capital", "country"],
26
+ "additionalProperties": False,
27
+ },
28
+ },
29
+ }
30
+
31
+
32
+ @pytest.mark.parametrize("so_model", MODELS)
33
+ def test_structured_output_fields(client: MeshAPI, so_model: str) -> None:
34
+ resp = client.chat.completions.create(
35
+ ChatCompletionParams(
36
+ model=so_model,
37
+ messages=[ChatMessage(role="user", content="What is the capital of France? Use the provided schema.")],
38
+ response_format=_SCHEMA,
39
+ max_tokens=1000,
40
+ temperature=0,
41
+ )
42
+ )
43
+ assert resp.choices, "expected choices"
44
+ content = resp.choices[0].message.content
45
+ assert content, "expected non-empty content"
46
+
47
+ data = json.loads(content)
48
+ assert "capital" in data, f"missing 'capital' field: {data}"
49
+ assert "country" in data, f"missing 'country' field: {data}"
50
+ assert isinstance(data["capital"], str), f"'capital' must be a string: {data}"
51
+ assert isinstance(data["country"], str), f"'country' must be a string: {data}"
52
+ assert "paris" in data["capital"].lower(), f"expected Paris as capital, got: {data}"
53
+
54
+
55
+ @pytest.mark.parametrize("so_model", MODELS)
56
+ def test_structured_output_finish_reason(client: MeshAPI, so_model: str) -> None:
57
+ resp = client.chat.completions.create(
58
+ ChatCompletionParams(
59
+ model=so_model,
60
+ messages=[ChatMessage(role="user", content="Name any planet in our solar system. Use the provided schema.")],
61
+ response_format={
62
+ "type": "json_schema",
63
+ "json_schema": {
64
+ "name": "planet_info",
65
+ "schema": {
66
+ "type": "object",
67
+ "properties": {
68
+ "name": {"type": "string"},
69
+ "position_from_sun": {"type": "integer"},
70
+ },
71
+ "required": ["name", "position_from_sun"],
72
+ "additionalProperties": False,
73
+ },
74
+ },
75
+ },
76
+ max_tokens=1000,
77
+ temperature=0,
78
+ )
79
+ )
80
+ assert resp.choices[0].finish_reason == "stop", (
81
+ f"expected finish_reason 'stop', got {resp.choices[0].finish_reason!r}"
82
+ )
83
+ data = json.loads(resp.choices[0].message.content)
84
+ assert "name" in data
85
+ assert "position_from_sun" in data
86
+ assert isinstance(data["position_from_sun"], int), f"expected integer position: {data}"
@@ -0,0 +1,30 @@
1
+ """Live tests for /v1/video/generations endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from meshapi import MeshAPI, VideoGenerationParams, VideoContentItem, ListVideoGenerationsParams
8
+
9
+ VIDEO_MODEL = os.environ.get("MESHAPI_VIDEO_GEN_MODEL", "byteplus/dreamina-seedance-2-0")
10
+
11
+
12
+ def test_video_list(client: MeshAPI) -> None:
13
+ listing = client.videos.list(ListVideoGenerationsParams(limit=5))
14
+ assert listing.data is not None
15
+ print(f"[PASS] videos.list -> total={listing.total}, items={len(listing.data)}")
16
+
17
+
18
+ def test_video_generate_and_retrieve(client: MeshAPI) -> None:
19
+ params = VideoGenerationParams(
20
+ model=VIDEO_MODEL,
21
+ content=[VideoContentItem(type="text", text="A serene mountain lake at sunrise")],
22
+ )
23
+ resp = client.videos.generate(params)
24
+ assert resp.id
25
+ print(f"[PASS] videos.generate -> task_id={resp.id}")
26
+
27
+ task = client.videos.retrieve(resp.id)
28
+ assert task.id == resp.id
29
+ assert task.status
30
+ print(f"[PASS] videos.retrieve -> status={task.status}")
@@ -10,6 +10,13 @@ from ._types import (
10
10
  ApiErrorBody,
11
11
  ApiErrorEnvelope,
12
12
  BulkEmbedRequest,
13
+ ListVoicesParams,
14
+ PronunciationDictionaryLocator,
15
+ SpeechParams,
16
+ TranscriptionParams,
17
+ TranscriptionResponse,
18
+ TranscriptionTranslateParams,
19
+ VoiceSettings,
13
20
  BulkEmbedResponse,
14
21
  BulkEmbedResult,
15
22
  ChatCompletionChunk,
@@ -42,6 +49,15 @@ from ._types import (
42
49
  ImageItem,
43
50
  ImageOptions,
44
51
  ImageUsage,
52
+ CreateVideoGenerationResponse,
53
+ ListVideoGenerationsParams,
54
+ VideoContentItem,
55
+ VideoGenerationParams,
56
+ VideoTaskContent,
57
+ VideoTaskError,
58
+ VideoTaskListResponse,
59
+ VideoTaskResponse,
60
+ VideoTaskUsage,
45
61
  InputAudio,
46
62
  ListModelsParams,
47
63
  ModelOverride,
@@ -75,6 +91,8 @@ from ._types import (
75
91
  UpdateTemplateParams,
76
92
  UsageInfo,
77
93
  )
94
+ from .resources.audio import AsyncAudioResource, AudioResource
95
+ from .resources.videos import AsyncVideosResource, VideosResource
78
96
  from .resources.batches import AsyncBatchesResource, BatchesResource
79
97
  from .resources.chat import AsyncChatResource, ChatResource
80
98
  from .resources.compare import AsyncCompareResource, CompareResource
@@ -178,6 +196,28 @@ __all__ = [
178
196
  "SearchRequest",
179
197
  "SearchResult",
180
198
  "SearchResponse",
199
+ # Video
200
+ "VideosResource",
201
+ "AsyncVideosResource",
202
+ "CreateVideoGenerationResponse",
203
+ "ListVideoGenerationsParams",
204
+ "VideoContentItem",
205
+ "VideoGenerationParams",
206
+ "VideoTaskContent",
207
+ "VideoTaskError",
208
+ "VideoTaskListResponse",
209
+ "VideoTaskResponse",
210
+ "VideoTaskUsage",
211
+ # Audio
212
+ "AudioResource",
213
+ "AsyncAudioResource",
214
+ "SpeechParams",
215
+ "VoiceSettings",
216
+ "PronunciationDictionaryLocator",
217
+ "TranscriptionParams",
218
+ "TranscriptionTranslateParams",
219
+ "TranscriptionResponse",
220
+ "ListVoicesParams",
181
221
  ]
182
222
 
183
223
 
@@ -226,6 +266,8 @@ class MeshAPI:
226
266
  self.models = ModelsResource(http)
227
267
  self.templates = TemplatesResource(http)
228
268
  self.images = ImagesResource(http)
269
+ self.videos = VideosResource(http)
270
+ self.audio = AudioResource(http)
229
271
  self.rag = RagResource(http)
230
272
  self.realtime = RealtimeResource(config)
231
273
  self._http = http
@@ -284,6 +326,8 @@ class AsyncMeshAPI:
284
326
  self.models = AsyncModelsResource(http)
285
327
  self.templates = AsyncTemplatesResource(http)
286
328
  self.images = AsyncImagesResource(http)
329
+ self.videos = AsyncVideosResource(http)
330
+ self.audio = AsyncAudioResource(http)
287
331
  self.rag = AsyncRagResource(http)
288
332
  self.realtime = AsyncRealtimeResource(config)
289
333
  self._http = http
@@ -373,6 +373,23 @@ class SyncHttpClient:
373
373
  response = self._request("GET", path, params=params)
374
374
  return response.content
375
375
 
376
+ def post_bytes(self, path: str, body: Any) -> bytes:
377
+ response = self._request("POST", path, json_body=body)
378
+ return response.content
379
+
380
+ def post_multipart(self, path: str, fields: Dict[str, Any], file_data: Optional[tuple] = None, file_field: str = "file") -> Any:
381
+ headers = {k: v for k, v in self._headers().items() if k != "Content-Type"}
382
+ files = None
383
+ data = None
384
+ if file_data is not None:
385
+ files = {file_field: file_data}
386
+ data = {k: str(v) for k, v in fields.items() if v is not None}
387
+ else:
388
+ data = {k: str(v) for k, v in fields.items() if v is not None}
389
+ response = self._client.post(path, headers=headers, data=data, files=files)
390
+ _raise_for_status(response)
391
+ return response.json()
392
+
376
393
  def stream(self, path: str, body: Any) -> Iterator[ChatCompletionChunk]:
377
394
  with self._client.stream(
378
395
  "POST", path, json=body, headers=self._headers()
@@ -480,6 +497,23 @@ class AsyncHttpClient:
480
497
  response = await self._request("GET", path, params=params)
481
498
  return response.content
482
499
 
500
+ async def post_bytes(self, path: str, body: Any) -> bytes:
501
+ response = await self._request("POST", path, json_body=body)
502
+ return response.content
503
+
504
+ async def post_multipart(self, path: str, fields: Dict[str, Any], file_data: Optional[tuple] = None, file_field: str = "file") -> Any:
505
+ headers = {k: v for k, v in self._headers().items() if k != "Content-Type"}
506
+ files = None
507
+ data = None
508
+ if file_data is not None:
509
+ files = {file_field: file_data}
510
+ data = {k: str(v) for k, v in fields.items() if v is not None}
511
+ else:
512
+ data = {k: str(v) for k, v in fields.items() if v is not None}
513
+ response = await self._client.post(path, headers=headers, data=data, files=files)
514
+ _raise_for_status(response)
515
+ return response.json()
516
+
483
517
  async def stream(self, path: str, body: Any) -> AsyncIterator[ChatCompletionChunk]:
484
518
  async with self._client.stream(
485
519
  "POST", path, json=body, headers=self._headers()
@@ -152,6 +152,7 @@ class ChatCompletionParams(BaseModel):
152
152
  seed: Optional[int] = None
153
153
  tools: Optional[List[Tool]] = None
154
154
  tool_choice: Optional[ToolChoice] = None
155
+ response_format: Optional[Dict[str, Any]] = None
155
156
 
156
157
  user: Optional[str] = None
157
158
  modality: Optional[Literal["text", "image"]] = None
@@ -160,6 +161,12 @@ class ChatCompletionParams(BaseModel):
160
161
  modalities: Optional[List[Literal["text", "audio"]]] = None
161
162
  audio: Optional[AudioOutputOptions] = None
162
163
 
164
+ # MeshAPI extension — overrides the server's 300 s upstream-provider timeout.
165
+ # Set this when your request may take longer than 5 minutes (e.g. long reasoning
166
+ # chains). The SDK-level timeout (MeshAPI(timeout=…)) is a separate HTTP-client
167
+ # timeout and does not affect this value.
168
+ timeout: Optional[float] = Field(default=None, gt=0)
169
+
163
170
 
164
171
  class UsageInfo(BaseModel):
165
172
  model_config = ConfigDict(extra="ignore")
@@ -399,6 +406,7 @@ class ResponsesParams(BaseModel):
399
406
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None
400
407
  response_format: Optional[Dict[str, Any]] = None
401
408
  user: Optional[str] = Field(default=None, max_length=256)
409
+ timeout: Optional[float] = Field(default=None, gt=0)
402
410
 
403
411
 
404
412
  class ResponsesUsage(BaseModel):
@@ -616,6 +624,113 @@ class ImageGenerationChunk(BaseModel):
616
624
  model: Optional[str] = None
617
625
 
618
626
 
627
+ # ---------------------------------------------------------------------------
628
+ # Video
629
+ # ---------------------------------------------------------------------------
630
+
631
+
632
+ class VideoContentItem(BaseModel):
633
+ """A single item in the content array (text, image_url, video_url, audio_url)."""
634
+ model_config = ConfigDict(extra="ignore")
635
+ type: str
636
+ text: Optional[str] = None
637
+ image_url: Optional[Dict[str, Any]] = None
638
+ video_url: Optional[Dict[str, Any]] = None
639
+ audio_url: Optional[Dict[str, Any]] = None
640
+ draft_task: Optional[Dict[str, Any]] = None
641
+ role: Optional[str] = None
642
+
643
+
644
+ class VideoGenerationParams(BaseModel):
645
+ """Request body for POST /v1/video/generations."""
646
+ model_config = ConfigDict(extra="ignore")
647
+ model: str
648
+ content: List[VideoContentItem]
649
+ callback_url: Optional[str] = None
650
+ return_last_frame: Optional[bool] = None
651
+ service_tier: Optional[str] = None
652
+ execution_expires_after: Optional[int] = None
653
+ generate_audio: Optional[bool] = None
654
+ draft: Optional[bool] = None
655
+ resolution: Optional[str] = None
656
+ ratio: Optional[str] = None
657
+ duration: Optional[int] = None
658
+ frames: Optional[int] = None
659
+ seed: Optional[int] = None
660
+ camera_fixed: Optional[bool] = None
661
+ watermark: Optional[bool] = None
662
+ safety_identifier: Optional[str] = None
663
+ priority: Optional[int] = None
664
+
665
+
666
+ class CreateVideoGenerationResponse(BaseModel):
667
+ model_config = ConfigDict(extra="ignore")
668
+ id: str
669
+
670
+
671
+ class VideoTaskError(BaseModel):
672
+ model_config = ConfigDict(extra="ignore")
673
+ code: str
674
+ message: str
675
+
676
+
677
+ class VideoTaskContent(BaseModel):
678
+ model_config = ConfigDict(extra="ignore")
679
+ video_url: Optional[str] = None
680
+ last_frame_url: Optional[str] = None
681
+
682
+
683
+ class VideoTaskUsage(BaseModel):
684
+ model_config = ConfigDict(extra="ignore")
685
+ completion_tokens: int
686
+ total_tokens: int
687
+
688
+
689
+ class VideoTaskResponse(BaseModel):
690
+ model_config = ConfigDict(extra="ignore")
691
+ id: str
692
+ status: str
693
+ model: Optional[str] = None
694
+ error: Optional[VideoTaskError] = None
695
+ created_at: Optional[int] = None
696
+ updated_at: Optional[int] = None
697
+ content: Optional[VideoTaskContent] = None
698
+ seed: Optional[int] = None
699
+ resolution: Optional[str] = None
700
+ ratio: Optional[str] = None
701
+ duration: Optional[int] = None
702
+ frames: Optional[int] = None
703
+ framespersecond: Optional[int] = None
704
+ generate_audio: Optional[bool] = None
705
+ safety_identifier: Optional[str] = None
706
+ priority: Optional[int] = None
707
+ draft: Optional[bool] = None
708
+ draft_task_id: Optional[str] = None
709
+ service_tier: Optional[str] = None
710
+ execution_expires_after: Optional[int] = None
711
+ usage: Optional[VideoTaskUsage] = None
712
+
713
+
714
+ class ListVideoGenerationsParams(BaseModel):
715
+ model_config = ConfigDict(extra="ignore")
716
+ status: Optional[str] = None
717
+ model: Optional[str] = None
718
+ created_after: Optional[str] = None
719
+ created_before: Optional[str] = None
720
+ limit: Optional[int] = None
721
+ offset: Optional[int] = None
722
+
723
+
724
+ class VideoTaskListResponse(BaseModel):
725
+ model_config = ConfigDict(extra="ignore")
726
+ object: Optional[str] = None
727
+ data: List[VideoTaskResponse]
728
+ has_more: bool
729
+ total: int
730
+ limit: int
731
+ offset: int
732
+
733
+
619
734
  # ---------------------------------------------------------------------------
620
735
 
621
736
  # Error wire format
@@ -718,6 +833,108 @@ class SearchResponse(BaseModel):
718
833
  results: List[SearchResult]
719
834
 
720
835
 
836
+ # ---------------------------------------------------------------------------
837
+ # Audio
838
+ # ---------------------------------------------------------------------------
839
+
840
+
841
+ class VoiceSettings(BaseModel):
842
+ model_config = ConfigDict(extra="ignore")
843
+ stability: Optional[float] = None
844
+ similarity_boost: Optional[float] = None
845
+ style: Optional[float] = None
846
+ use_speaker_boost: Optional[bool] = None
847
+ speed: Optional[float] = None
848
+
849
+
850
+ class PronunciationDictionaryLocator(BaseModel):
851
+ model_config = ConfigDict(extra="ignore")
852
+ pronunciation_dictionary_id: str
853
+ version_id: str
854
+
855
+
856
+ class SpeechParams(BaseModel):
857
+ model_config = ConfigDict(extra="ignore")
858
+ input: str
859
+ model: Optional[str] = None
860
+ voice: Optional[str] = None
861
+ stream: Optional[bool] = None
862
+ response_format: Optional[str] = None
863
+ language_code: Optional[str] = None
864
+ voice_settings: Optional[VoiceSettings] = None
865
+ pronunciation_dictionary_locators: Optional[List[PronunciationDictionaryLocator]] = None
866
+ seed: Optional[int] = None
867
+ previous_text: Optional[str] = None
868
+ next_text: Optional[str] = None
869
+ previous_request_ids: Optional[List[str]] = None
870
+ next_request_ids: Optional[List[str]] = None
871
+ apply_text_normalization: Optional[str] = None
872
+ apply_language_text_normalization: Optional[bool] = None
873
+ use_pvc_as_ivc: Optional[bool] = None
874
+ enable_logging: Optional[bool] = None
875
+ optimize_streaming_latency: Optional[int] = None
876
+ speaker: Optional[str] = None
877
+ target_language_code: Optional[str] = None
878
+ pitch: Optional[float] = None
879
+ pace: Optional[float] = None
880
+ loudness: Optional[float] = None
881
+ speech_sample_rate: Optional[int] = None
882
+ enable_preprocessing: Optional[bool] = None
883
+
884
+
885
+ class TranscriptionParams(BaseModel):
886
+ model_config = ConfigDict(extra="ignore")
887
+ model: str
888
+ language_code: Optional[str] = None
889
+ tag_audio_events: Optional[bool] = None
890
+ num_speakers: Optional[int] = None
891
+ timestamps_granularity: Optional[str] = None
892
+ diarize: Optional[bool] = None
893
+ diarization_threshold: Optional[float] = None
894
+ additional_formats: Optional[str] = None
895
+ file_format: Optional[str] = None
896
+ cloud_storage_url: Optional[str] = None
897
+ source_url: Optional[str] = None
898
+ webhook: Optional[bool] = None
899
+ webhook_id: Optional[str] = None
900
+ temperature: Optional[float] = None
901
+ seed: Optional[int] = None
902
+ use_multi_channel: Optional[bool] = None
903
+ webhook_metadata: Optional[str] = None
904
+ entity_detection: Optional[str] = None
905
+ no_verbatim: Optional[bool] = None
906
+ detect_speaker_roles: Optional[bool] = None
907
+ entity_redaction: Optional[str] = None
908
+ entity_redaction_mode: Optional[str] = None
909
+ keyterms: Optional[List[str]] = None
910
+ with_timestamps: Optional[bool] = None
911
+ debug_mode: Optional[bool] = None
912
+
913
+
914
+ class TranscriptionTranslateParams(BaseModel):
915
+ model_config = ConfigDict(extra="ignore")
916
+ model: Optional[str] = None
917
+ prompt: Optional[str] = None
918
+
919
+
920
+ class TranscriptionResponse(BaseModel):
921
+ model_config = ConfigDict(extra="ignore")
922
+ text: str
923
+
924
+
925
+ class ListVoicesParams(BaseModel):
926
+ model_config = ConfigDict(extra="ignore")
927
+ next_page_token: Optional[str] = None
928
+ page_size: Optional[int] = None
929
+ search: Optional[str] = None
930
+ sort: Optional[str] = None
931
+ sort_direction: Optional[str] = None
932
+ voice_type: Optional[str] = None
933
+ category: Optional[str] = None
934
+ include_total_count: Optional[bool] = None
935
+ voice_ids: Optional[List[str]] = None
936
+
937
+
721
938
  # ---------------------------------------------------------------------------
722
939
 
723
940
  class ApiErrorBody(BaseModel):
@@ -0,0 +1,134 @@
1
+ """Audio resource — /v1/audio/* endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Optional
6
+
7
+ from .._http import AsyncHttpClient, SyncHttpClient
8
+ from .._types import (
9
+ ListVoicesParams,
10
+ SpeechParams,
11
+ TranscriptionParams,
12
+ TranscriptionResponse,
13
+ TranscriptionTranslateParams,
14
+ )
15
+
16
+
17
+ class AudioResource:
18
+ def __init__(self, http: SyncHttpClient) -> None:
19
+ self._http = http
20
+
21
+ def synthesize(self, params: SpeechParams) -> bytes:
22
+ """POST /v1/audio/speech — returns raw audio bytes."""
23
+ return self._http.post_bytes("/v1/audio/speech", params.model_dump(exclude_none=True))
24
+
25
+ def transcribe(
26
+ self,
27
+ file: bytes,
28
+ params: TranscriptionParams,
29
+ *,
30
+ filename: str = "audio.mp3",
31
+ ) -> TranscriptionResponse:
32
+ """POST /v1/audio/transcriptions — multipart upload."""
33
+ fields = params.model_dump(exclude_none=True)
34
+ data = self._http.post_multipart(
35
+ "/v1/audio/transcriptions",
36
+ fields,
37
+ file_data=(filename, file, "application/octet-stream"),
38
+ file_field="file",
39
+ )
40
+ return TranscriptionResponse.model_validate(data)
41
+
42
+ def get_transcription(self, transcription_id: str) -> Any:
43
+ """GET /v1/audio/transcriptions/{transcription_id}."""
44
+ return self._http.get(f"/v1/audio/transcriptions/{transcription_id}")
45
+
46
+ def translate(
47
+ self,
48
+ file: bytes,
49
+ params: Optional[TranscriptionTranslateParams] = None,
50
+ *,
51
+ filename: str = "audio.mp3",
52
+ ) -> TranscriptionResponse:
53
+ """POST /v1/audio/transcriptions/translate — multipart upload, translates to English."""
54
+ fields: Dict[str, Any] = {}
55
+ if params is not None:
56
+ fields = params.model_dump(exclude_none=True)
57
+ data = self._http.post_multipart(
58
+ "/v1/audio/transcriptions/translate",
59
+ fields,
60
+ file_data=(filename, file, "application/octet-stream"),
61
+ file_field="file",
62
+ )
63
+ return TranscriptionResponse.model_validate(data)
64
+
65
+ def list_voices(self, params: Optional[ListVoicesParams] = None) -> Any:
66
+ """GET /v1/audio/voices — list/search voices."""
67
+ query: Dict[str, Any] = {}
68
+ if params is not None:
69
+ query = {k: v for k, v in params.model_dump(exclude_none=True).items()}
70
+ return self._http.get("/v1/audio/voices", params=query or None)
71
+
72
+ def get_voice(self, voice_id: str) -> Any:
73
+ """GET /v1/audio/voices/{voice_id}."""
74
+ return self._http.get(f"/v1/audio/voices/{voice_id}")
75
+
76
+
77
+ class AsyncAudioResource:
78
+ def __init__(self, http: AsyncHttpClient) -> None:
79
+ self._http = http
80
+
81
+ async def synthesize(self, params: SpeechParams) -> bytes:
82
+ """POST /v1/audio/speech — returns raw audio bytes."""
83
+ return await self._http.post_bytes("/v1/audio/speech", params.model_dump(exclude_none=True))
84
+
85
+ async def transcribe(
86
+ self,
87
+ file: bytes,
88
+ params: TranscriptionParams,
89
+ *,
90
+ filename: str = "audio.mp3",
91
+ ) -> TranscriptionResponse:
92
+ """POST /v1/audio/transcriptions — multipart upload."""
93
+ fields = params.model_dump(exclude_none=True)
94
+ data = await self._http.post_multipart(
95
+ "/v1/audio/transcriptions",
96
+ fields,
97
+ file_data=(filename, file, "application/octet-stream"),
98
+ file_field="file",
99
+ )
100
+ return TranscriptionResponse.model_validate(data)
101
+
102
+ async def get_transcription(self, transcription_id: str) -> Any:
103
+ """GET /v1/audio/transcriptions/{transcription_id}."""
104
+ return await self._http.get(f"/v1/audio/transcriptions/{transcription_id}")
105
+
106
+ async def translate(
107
+ self,
108
+ file: bytes,
109
+ params: Optional[TranscriptionTranslateParams] = None,
110
+ *,
111
+ filename: str = "audio.mp3",
112
+ ) -> TranscriptionResponse:
113
+ """POST /v1/audio/transcriptions/translate — multipart upload, translates to English."""
114
+ fields: Dict[str, Any] = {}
115
+ if params is not None:
116
+ fields = params.model_dump(exclude_none=True)
117
+ data = await self._http.post_multipart(
118
+ "/v1/audio/transcriptions/translate",
119
+ fields,
120
+ file_data=(filename, file, "application/octet-stream"),
121
+ file_field="file",
122
+ )
123
+ return TranscriptionResponse.model_validate(data)
124
+
125
+ async def list_voices(self, params: Optional[ListVoicesParams] = None) -> Any:
126
+ """GET /v1/audio/voices — list/search voices."""
127
+ query: Dict[str, Any] = {}
128
+ if params is not None:
129
+ query = {k: v for k, v in params.model_dump(exclude_none=True).items()}
130
+ return await self._http.get("/v1/audio/voices", params=query or None)
131
+
132
+ async def get_voice(self, voice_id: str) -> Any:
133
+ """GET /v1/audio/voices/{voice_id}."""
134
+ return await self._http.get(f"/v1/audio/voices/{voice_id}")
@@ -0,0 +1,57 @@
1
+ """Videos resource — /v1/video/generations endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from .._http import AsyncHttpClient, SyncHttpClient
8
+ from .._types import (
9
+ CreateVideoGenerationResponse,
10
+ ListVideoGenerationsParams,
11
+ VideoGenerationParams,
12
+ VideoTaskListResponse,
13
+ VideoTaskResponse,
14
+ )
15
+
16
+
17
+ class VideosResource:
18
+ def __init__(self, http: SyncHttpClient) -> None:
19
+ self._http = http
20
+
21
+ def generate(self, params: VideoGenerationParams) -> CreateVideoGenerationResponse:
22
+ """POST /v1/video/generations — submit a video generation task."""
23
+ data = self._http.post("/v1/video/generations", params.model_dump(exclude_none=True))
24
+ return CreateVideoGenerationResponse.model_validate(data)
25
+
26
+ def list(self, params: Optional[ListVideoGenerationsParams] = None) -> VideoTaskListResponse:
27
+ """GET /v1/video/generations — list video generation tasks."""
28
+ query = None
29
+ if params is not None:
30
+ query = {k: str(v) for k, v in params.model_dump(exclude_none=True).items()} or None
31
+ data = self._http.get("/v1/video/generations", params=query)
32
+ return VideoTaskListResponse.model_validate(data)
33
+
34
+ def retrieve(self, task_id: str) -> VideoTaskResponse:
35
+ """GET /v1/video/generations/{task_id} — get a video generation task."""
36
+ data = self._http.get(f"/v1/video/generations/{task_id}")
37
+ return VideoTaskResponse.model_validate(data)
38
+
39
+
40
+ class AsyncVideosResource:
41
+ def __init__(self, http: AsyncHttpClient) -> None:
42
+ self._http = http
43
+
44
+ async def generate(self, params: VideoGenerationParams) -> CreateVideoGenerationResponse:
45
+ data = await self._http.post("/v1/video/generations", params.model_dump(exclude_none=True))
46
+ return CreateVideoGenerationResponse.model_validate(data)
47
+
48
+ async def list(self, params: Optional[ListVideoGenerationsParams] = None) -> VideoTaskListResponse:
49
+ query = None
50
+ if params is not None:
51
+ query = {k: str(v) for k, v in params.model_dump(exclude_none=True).items()} or None
52
+ data = await self._http.get("/v1/video/generations", params=query)
53
+ return VideoTaskListResponse.model_validate(data)
54
+
55
+ async def retrieve(self, task_id: str) -> VideoTaskResponse:
56
+ data = await self._http.get(f"/v1/video/generations/{task_id}")
57
+ return VideoTaskResponse.model_validate(data)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meshapi"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "Official Python SDK for the MeshAPI AI model gateway"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
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