meshapi 0.1.4__tar.gz → 0.1.6__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.4 → meshapi-0.1.6}/CLAUDE.md +64 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/PKG-INFO +95 -1
- {meshapi-0.1.4 → meshapi-0.1.6}/README.md +94 -0
- meshapi-0.1.6/livetests/test_audio.py +40 -0
- meshapi-0.1.6/livetests/test_compare.py +38 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/test_inference_resources.py +0 -1
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/test_rag.py +1 -1
- meshapi-0.1.6/livetests/test_structured_output.py +86 -0
- meshapi-0.1.6/livetests/test_video.py +30 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/__init__.py +48 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/_http.py +34 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/_types.py +235 -0
- meshapi-0.1.6/meshapi/resources/audio.py +140 -0
- meshapi-0.1.6/meshapi/resources/videos.py +57 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/pyproject.toml +1 -1
- {meshapi-0.1.4 → meshapi-0.1.6}/.gitignore +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/CHANGELOG.md +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/TESTING.md +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/compare.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/config.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/conftest.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/pytest.ini +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/requirements.txt +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/responses.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/test_chat.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/test_errors.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/test_feature_matrix.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/test_models.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/test_realtime.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/test_stream.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/test_templates.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/livetests/tool_call.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/_errors.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/resources/__init__.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/resources/batches.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/resources/chat.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/resources/compare.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/resources/embeddings.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/resources/images.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/resources/models.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/resources/rag.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/resources/realtime.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/resources/responses.py +0 -0
- {meshapi-0.1.4 → meshapi-0.1.6}/meshapi/resources/templates.py +0 -0
|
@@ -147,6 +147,70 @@ 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
|
+
| `test_compare.py` | Non-streaming compare, streaming compare |
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Contribution checklist
|
|
170
|
+
|
|
171
|
+
Every SDK change — however small — must include all of the following before merging:
|
|
172
|
+
|
|
173
|
+
1. **Live tests** — add or update `livetests/test_<resource>.py` to cover the new/changed behaviour.
|
|
174
|
+
2. **Unit / contract tests** — if the change affects request/response serialisation or error handling, add a test in `tests/unit/` or `tests/contract/`.
|
|
175
|
+
3. **README** — update `README.md` with a usage example for any new or changed public surface.
|
|
176
|
+
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.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Release
|
|
183
|
+
|
|
184
|
+
Releases are triggered by pushing a `v*` git tag. The `publish.yml` workflow builds the package with hatch and publishes to PyPI via OIDC (no API token needed).
|
|
185
|
+
|
|
186
|
+
### Release checklist
|
|
187
|
+
|
|
188
|
+
1. **Bump the version** in `pyproject.toml`:
|
|
189
|
+
```toml
|
|
190
|
+
version = "0.1.6"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
2. **Commit the version bump**:
|
|
194
|
+
```bash
|
|
195
|
+
git add pyproject.toml
|
|
196
|
+
git commit -m "chore: bump version to 0.1.6"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
3. **Tag and push**:
|
|
200
|
+
```bash
|
|
201
|
+
git tag v0.1.6
|
|
202
|
+
git push origin main
|
|
203
|
+
git push origin v0.1.6
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
4. **Monitor the workflow** at `Actions → Publish to PyPI`.
|
|
207
|
+
|
|
208
|
+
5. **Verify** the new version is live:
|
|
209
|
+
```bash
|
|
210
|
+
pip install meshapi==0.1.6
|
|
211
|
+
python -c "import meshapi; print(meshapi.__version__)"
|
|
212
|
+
```
|
|
213
|
+
|
|
150
214
|
### RAG live test notes
|
|
151
215
|
|
|
152
216
|
`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.
|
|
3
|
+
Version: 0.1.6
|
|
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,40 @@
|
|
|
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, TranscriptionParams
|
|
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_stt_from_tts(client: MeshAPI) -> None:
|
|
22
|
+
audio_bytes = client.audio.synthesize(
|
|
23
|
+
SpeechParams(input="Hello from MeshAPI audio test.", model=TTS_MODEL)
|
|
24
|
+
)
|
|
25
|
+
assert isinstance(audio_bytes, bytes) and len(audio_bytes) > 0, "TTS step failed"
|
|
26
|
+
|
|
27
|
+
result = client.audio.transcribe(
|
|
28
|
+
audio_bytes,
|
|
29
|
+
TranscriptionParams(model=STT_MODEL),
|
|
30
|
+
filename="tts_output.wav",
|
|
31
|
+
)
|
|
32
|
+
assert result is not None
|
|
33
|
+
assert isinstance(result.text, str) and len(result.text) > 0
|
|
34
|
+
print(f"[PASS] audio.transcribe (via TTS audio) -> {result.text!r}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_audio_list_voices(client: MeshAPI) -> None:
|
|
38
|
+
voices = client.audio.list_voices(ListVoicesParams(page_size=5))
|
|
39
|
+
assert voices is not None
|
|
40
|
+
print(f"[PASS] audio.list_voices -> {type(voices)}")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Live tests: Model Compare API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from meshapi import MeshAPI, CompareParams, ChatMessage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_compare_nonstreaming(client: MeshAPI, model: str, second_model: str) -> None:
|
|
10
|
+
result = client.compare.create(
|
|
11
|
+
CompareParams(
|
|
12
|
+
models=[model, second_model],
|
|
13
|
+
messages=[ChatMessage(role="user", content="What is 2+2? Reply in one word.")],
|
|
14
|
+
skip_comparison=True,
|
|
15
|
+
max_tokens=20,
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
assert result.comparison_id, "expected comparison_id"
|
|
19
|
+
assert len(result.results) == 2, f"expected 2 results, got {len(result.results)}"
|
|
20
|
+
found_models = {r.model for r in result.results}
|
|
21
|
+
assert model in found_models, f"expected {model} in results"
|
|
22
|
+
assert second_model in found_models, f"expected {second_model} in results"
|
|
23
|
+
for r in result.results:
|
|
24
|
+
assert r.content or r.error, f"result for {r.model} has neither content nor error"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_compare_streaming(client: MeshAPI, model: str, second_model: str) -> None:
|
|
28
|
+
events = list(
|
|
29
|
+
client.compare.stream(
|
|
30
|
+
CompareParams(
|
|
31
|
+
models=[model, second_model],
|
|
32
|
+
messages=[ChatMessage(role="user", content="Tell me a joke.")],
|
|
33
|
+
skip_comparison=True,
|
|
34
|
+
max_tokens=50,
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
assert len(events) > 0, "expected at least one streaming event"
|
|
@@ -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(
|
|
@@ -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,15 @@ 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
|
+
Voice,
|
|
20
|
+
VoicesResponse,
|
|
21
|
+
VoiceSettings,
|
|
13
22
|
BulkEmbedResponse,
|
|
14
23
|
BulkEmbedResult,
|
|
15
24
|
ChatCompletionChunk,
|
|
@@ -42,6 +51,15 @@ from ._types import (
|
|
|
42
51
|
ImageItem,
|
|
43
52
|
ImageOptions,
|
|
44
53
|
ImageUsage,
|
|
54
|
+
CreateVideoGenerationResponse,
|
|
55
|
+
ListVideoGenerationsParams,
|
|
56
|
+
VideoContentItem,
|
|
57
|
+
VideoGenerationParams,
|
|
58
|
+
VideoTaskContent,
|
|
59
|
+
VideoTaskError,
|
|
60
|
+
VideoTaskListResponse,
|
|
61
|
+
VideoTaskResponse,
|
|
62
|
+
VideoTaskUsage,
|
|
45
63
|
InputAudio,
|
|
46
64
|
ListModelsParams,
|
|
47
65
|
ModelOverride,
|
|
@@ -75,6 +93,8 @@ from ._types import (
|
|
|
75
93
|
UpdateTemplateParams,
|
|
76
94
|
UsageInfo,
|
|
77
95
|
)
|
|
96
|
+
from .resources.audio import AsyncAudioResource, AudioResource
|
|
97
|
+
from .resources.videos import AsyncVideosResource, VideosResource
|
|
78
98
|
from .resources.batches import AsyncBatchesResource, BatchesResource
|
|
79
99
|
from .resources.chat import AsyncChatResource, ChatResource
|
|
80
100
|
from .resources.compare import AsyncCompareResource, CompareResource
|
|
@@ -178,6 +198,30 @@ __all__ = [
|
|
|
178
198
|
"SearchRequest",
|
|
179
199
|
"SearchResult",
|
|
180
200
|
"SearchResponse",
|
|
201
|
+
# Video
|
|
202
|
+
"VideosResource",
|
|
203
|
+
"AsyncVideosResource",
|
|
204
|
+
"CreateVideoGenerationResponse",
|
|
205
|
+
"ListVideoGenerationsParams",
|
|
206
|
+
"VideoContentItem",
|
|
207
|
+
"VideoGenerationParams",
|
|
208
|
+
"VideoTaskContent",
|
|
209
|
+
"VideoTaskError",
|
|
210
|
+
"VideoTaskListResponse",
|
|
211
|
+
"VideoTaskResponse",
|
|
212
|
+
"VideoTaskUsage",
|
|
213
|
+
# Audio
|
|
214
|
+
"AudioResource",
|
|
215
|
+
"AsyncAudioResource",
|
|
216
|
+
"SpeechParams",
|
|
217
|
+
"VoiceSettings",
|
|
218
|
+
"PronunciationDictionaryLocator",
|
|
219
|
+
"TranscriptionParams",
|
|
220
|
+
"TranscriptionTranslateParams",
|
|
221
|
+
"TranscriptionResponse",
|
|
222
|
+
"ListVoicesParams",
|
|
223
|
+
"Voice",
|
|
224
|
+
"VoicesResponse",
|
|
181
225
|
]
|
|
182
226
|
|
|
183
227
|
|
|
@@ -226,6 +270,8 @@ class MeshAPI:
|
|
|
226
270
|
self.models = ModelsResource(http)
|
|
227
271
|
self.templates = TemplatesResource(http)
|
|
228
272
|
self.images = ImagesResource(http)
|
|
273
|
+
self.videos = VideosResource(http)
|
|
274
|
+
self.audio = AudioResource(http)
|
|
229
275
|
self.rag = RagResource(http)
|
|
230
276
|
self.realtime = RealtimeResource(config)
|
|
231
277
|
self._http = http
|
|
@@ -284,6 +330,8 @@ class AsyncMeshAPI:
|
|
|
284
330
|
self.models = AsyncModelsResource(http)
|
|
285
331
|
self.templates = AsyncTemplatesResource(http)
|
|
286
332
|
self.images = AsyncImagesResource(http)
|
|
333
|
+
self.videos = AsyncVideosResource(http)
|
|
334
|
+
self.audio = AsyncAudioResource(http)
|
|
287
335
|
self.rag = AsyncRagResource(http)
|
|
288
336
|
self.realtime = AsyncRealtimeResource(config)
|
|
289
337
|
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,126 @@ 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
|
+
|
|
938
|
+
class Voice(BaseModel):
|
|
939
|
+
model_config = ConfigDict(extra="ignore")
|
|
940
|
+
voice_id: str
|
|
941
|
+
name: str
|
|
942
|
+
category: str
|
|
943
|
+
description: str
|
|
944
|
+
preview_url: str
|
|
945
|
+
labels: Dict[str, str] = {}
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
class VoicesResponse(BaseModel):
|
|
949
|
+
model_config = ConfigDict(extra="ignore")
|
|
950
|
+
voices: List[Voice]
|
|
951
|
+
has_more: bool
|
|
952
|
+
total_count: int
|
|
953
|
+
next_page_token: Optional[str] = None
|
|
954
|
+
|
|
955
|
+
|
|
721
956
|
# ---------------------------------------------------------------------------
|
|
722
957
|
|
|
723
958
|
class ApiErrorBody(BaseModel):
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
Voice,
|
|
15
|
+
VoicesResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AudioResource:
|
|
20
|
+
def __init__(self, http: SyncHttpClient) -> None:
|
|
21
|
+
self._http = http
|
|
22
|
+
|
|
23
|
+
def synthesize(self, params: SpeechParams) -> bytes:
|
|
24
|
+
"""POST /v1/audio/speech — returns raw audio bytes."""
|
|
25
|
+
return self._http.post_bytes("/v1/audio/speech", params.model_dump(exclude_none=True))
|
|
26
|
+
|
|
27
|
+
def transcribe(
|
|
28
|
+
self,
|
|
29
|
+
file: bytes,
|
|
30
|
+
params: TranscriptionParams,
|
|
31
|
+
*,
|
|
32
|
+
filename: str = "audio.mp3",
|
|
33
|
+
) -> TranscriptionResponse:
|
|
34
|
+
"""POST /v1/audio/transcriptions — multipart upload."""
|
|
35
|
+
fields = params.model_dump(exclude_none=True)
|
|
36
|
+
data = self._http.post_multipart(
|
|
37
|
+
"/v1/audio/transcriptions",
|
|
38
|
+
fields,
|
|
39
|
+
file_data=(filename, file, "application/octet-stream"),
|
|
40
|
+
file_field="file",
|
|
41
|
+
)
|
|
42
|
+
return TranscriptionResponse.model_validate(data)
|
|
43
|
+
|
|
44
|
+
def get_transcription(self, transcription_id: str) -> Any:
|
|
45
|
+
"""GET /v1/audio/transcriptions/{transcription_id}."""
|
|
46
|
+
return self._http.get(f"/v1/audio/transcriptions/{transcription_id}")
|
|
47
|
+
|
|
48
|
+
def translate(
|
|
49
|
+
self,
|
|
50
|
+
file: bytes,
|
|
51
|
+
params: Optional[TranscriptionTranslateParams] = None,
|
|
52
|
+
*,
|
|
53
|
+
filename: str = "audio.mp3",
|
|
54
|
+
) -> TranscriptionResponse:
|
|
55
|
+
"""POST /v1/audio/transcriptions/translate — multipart upload, translates to English."""
|
|
56
|
+
fields: Dict[str, Any] = {}
|
|
57
|
+
if params is not None:
|
|
58
|
+
fields = params.model_dump(exclude_none=True)
|
|
59
|
+
data = self._http.post_multipart(
|
|
60
|
+
"/v1/audio/transcriptions/translate",
|
|
61
|
+
fields,
|
|
62
|
+
file_data=(filename, file, "application/octet-stream"),
|
|
63
|
+
file_field="file",
|
|
64
|
+
)
|
|
65
|
+
return TranscriptionResponse.model_validate(data)
|
|
66
|
+
|
|
67
|
+
def list_voices(self, params: Optional[ListVoicesParams] = None) -> VoicesResponse:
|
|
68
|
+
"""GET /v1/audio/voices — list/search voices."""
|
|
69
|
+
query: Dict[str, Any] = {}
|
|
70
|
+
if params is not None:
|
|
71
|
+
query = {k: v for k, v in params.model_dump(exclude_none=True).items()}
|
|
72
|
+
data = self._http.get("/v1/audio/voices", params=query or None)
|
|
73
|
+
return VoicesResponse.model_validate(data)
|
|
74
|
+
|
|
75
|
+
def get_voice(self, voice_id: str) -> Voice:
|
|
76
|
+
"""GET /v1/audio/voices/{voice_id}."""
|
|
77
|
+
data = self._http.get(f"/v1/audio/voices/{voice_id}")
|
|
78
|
+
return Voice.model_validate(data)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AsyncAudioResource:
|
|
82
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
83
|
+
self._http = http
|
|
84
|
+
|
|
85
|
+
async def synthesize(self, params: SpeechParams) -> bytes:
|
|
86
|
+
"""POST /v1/audio/speech — returns raw audio bytes."""
|
|
87
|
+
return await self._http.post_bytes("/v1/audio/speech", params.model_dump(exclude_none=True))
|
|
88
|
+
|
|
89
|
+
async def transcribe(
|
|
90
|
+
self,
|
|
91
|
+
file: bytes,
|
|
92
|
+
params: TranscriptionParams,
|
|
93
|
+
*,
|
|
94
|
+
filename: str = "audio.mp3",
|
|
95
|
+
) -> TranscriptionResponse:
|
|
96
|
+
"""POST /v1/audio/transcriptions — multipart upload."""
|
|
97
|
+
fields = params.model_dump(exclude_none=True)
|
|
98
|
+
data = await self._http.post_multipart(
|
|
99
|
+
"/v1/audio/transcriptions",
|
|
100
|
+
fields,
|
|
101
|
+
file_data=(filename, file, "application/octet-stream"),
|
|
102
|
+
file_field="file",
|
|
103
|
+
)
|
|
104
|
+
return TranscriptionResponse.model_validate(data)
|
|
105
|
+
|
|
106
|
+
async def get_transcription(self, transcription_id: str) -> Any:
|
|
107
|
+
"""GET /v1/audio/transcriptions/{transcription_id}."""
|
|
108
|
+
return await self._http.get(f"/v1/audio/transcriptions/{transcription_id}")
|
|
109
|
+
|
|
110
|
+
async def translate(
|
|
111
|
+
self,
|
|
112
|
+
file: bytes,
|
|
113
|
+
params: Optional[TranscriptionTranslateParams] = None,
|
|
114
|
+
*,
|
|
115
|
+
filename: str = "audio.mp3",
|
|
116
|
+
) -> TranscriptionResponse:
|
|
117
|
+
"""POST /v1/audio/transcriptions/translate — multipart upload, translates to English."""
|
|
118
|
+
fields: Dict[str, Any] = {}
|
|
119
|
+
if params is not None:
|
|
120
|
+
fields = params.model_dump(exclude_none=True)
|
|
121
|
+
data = await self._http.post_multipart(
|
|
122
|
+
"/v1/audio/transcriptions/translate",
|
|
123
|
+
fields,
|
|
124
|
+
file_data=(filename, file, "application/octet-stream"),
|
|
125
|
+
file_field="file",
|
|
126
|
+
)
|
|
127
|
+
return TranscriptionResponse.model_validate(data)
|
|
128
|
+
|
|
129
|
+
async def list_voices(self, params: Optional[ListVoicesParams] = None) -> VoicesResponse:
|
|
130
|
+
"""GET /v1/audio/voices — list/search voices."""
|
|
131
|
+
query: Dict[str, Any] = {}
|
|
132
|
+
if params is not None:
|
|
133
|
+
query = {k: v for k, v in params.model_dump(exclude_none=True).items()}
|
|
134
|
+
data = await self._http.get("/v1/audio/voices", params=query or None)
|
|
135
|
+
return VoicesResponse.model_validate(data)
|
|
136
|
+
|
|
137
|
+
async def get_voice(self, voice_id: str) -> Voice:
|
|
138
|
+
"""GET /v1/audio/voices/{voice_id}."""
|
|
139
|
+
data = await self._http.get(f"/v1/audio/voices/{voice_id}")
|
|
140
|
+
return Voice.model_validate(data)
|
|
@@ -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)
|
|
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
|
|
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
|