|
36 | 36 | from otari._client.api.batches_api import BatchesApi |
37 | 37 | from otari._client.api.chat_api import ChatApi |
38 | 38 | from otari._client.api.embeddings_api import EmbeddingsApi |
| 39 | +from otari._client.api.images_api import ImagesApi |
39 | 40 | from otari._client.api.messages_api import MessagesApi |
40 | 41 | from otari._client.api.models_api import ModelsApi |
41 | 42 | from otari._client.api.moderations_api import ModerationsApi |
|
46 | 47 | from otari._client.models.count_tokens_request import CountTokensRequest |
47 | 48 | from otari._client.models.create_batch_request import CreateBatchRequest |
48 | 49 | from otari._client.models.embedding_request import EmbeddingRequest |
| 50 | +from otari._client.models.image_generation_request import ImageGenerationRequest |
49 | 51 | from otari._client.models.messages_request import MessagesRequest |
50 | 52 | from otari._client.models.moderation_request import ModerationRequest |
51 | 53 | from otari._client.models.rerank_request import RerankRequest |
@@ -126,6 +128,7 @@ def __init__( |
126 | 128 | self._rerank = RerankApi(self._api) |
127 | 129 | self._messages = MessagesApi(self._api) |
128 | 130 | self._models = ModelsApi(self._api) |
| 131 | + self._images = ImagesApi(self._api) |
129 | 132 | self._batches = BatchesApi(self._api) |
130 | 133 |
|
131 | 134 | @cached_property |
@@ -301,6 +304,91 @@ async def rerank( |
301 | 304 | result = await self._call(lambda: self._rerank.create_rerank_v1_rerank_post(request)) |
302 | 305 | return cast("RerankResponse", result) |
303 | 306 |
|
| 307 | + # -- Images ------------------------------------------------------------- |
| 308 | + |
| 309 | + async def image_generation( |
| 310 | + self, |
| 311 | + *, |
| 312 | + model: str, |
| 313 | + prompt: str, |
| 314 | + **kwargs: Any, |
| 315 | + ) -> dict[str, Any]: |
| 316 | + """Generate images from a text prompt. |
| 317 | +
|
| 318 | + Returns the gateway's OpenAI-compatible image payload as a dict |
| 319 | + (``{"created": ..., "data": [...]}``). The generated core models this |
| 320 | + response as an opaque object, so the parsed JSON is returned unchanged. |
| 321 | +
|
| 322 | + Args: |
| 323 | + model: Model identifier (e.g. ``"openai:dall-e-3"``). |
| 324 | + prompt: Text prompt describing the desired image(s). |
| 325 | + **kwargs: Additional parameters (``n``, ``size``, ``quality``, |
| 326 | + ``response_format``, ``style``, ``user``). |
| 327 | + """ |
| 328 | + request = build_request( |
| 329 | + ImageGenerationRequest, {"model": model, "prompt": prompt, **kwargs} |
| 330 | + ) |
| 331 | + result = await self._call( |
| 332 | + lambda: self._images.create_image_v1_images_generations_post(request) |
| 333 | + ) |
| 334 | + return cast("dict[str, Any]", result) |
| 335 | + |
| 336 | + # -- Audio -------------------------------------------------------------- |
| 337 | + |
| 338 | + async def speech( |
| 339 | + self, |
| 340 | + *, |
| 341 | + model: str, |
| 342 | + input: str, # noqa: A002 |
| 343 | + voice: str, |
| 344 | + **kwargs: Any, |
| 345 | + ) -> bytes: |
| 346 | + """Synthesize speech (text-to-speech), returning raw audio bytes. |
| 347 | +
|
| 348 | + The gateway returns binary audio (``audio/mpeg`` by default) with no |
| 349 | + JSON response model, so this posts over httpx and returns the raw |
| 350 | + ``bytes``. |
| 351 | +
|
| 352 | + Args: |
| 353 | + model: Model identifier (e.g. ``"openai:tts-1"``). |
| 354 | + input: Text to synthesize. |
| 355 | + voice: Voice to use (e.g. ``"alloy"``). |
| 356 | + **kwargs: Additional parameters (``response_format``, ``speed``, |
| 357 | + ``instructions``, ``user``). |
| 358 | + """ |
| 359 | + body = {"model": model, "input": input, "voice": voice, **kwargs} |
| 360 | + response = await self._post("/audio/speech", json=body) |
| 361 | + return response.content |
| 362 | + |
| 363 | + async def transcription( |
| 364 | + self, |
| 365 | + *, |
| 366 | + model: str, |
| 367 | + file: bytes, |
| 368 | + filename: str = "audio", |
| 369 | + **kwargs: Any, |
| 370 | + ) -> Any: |
| 371 | + """Transcribe audio to text. |
| 372 | +
|
| 373 | + ``file`` is the raw audio bytes uploaded as multipart form data. Returns |
| 374 | + the parsed JSON (a dict) for JSON response formats, or the raw text for |
| 375 | + ``text`` / ``srt`` / ``vtt`` formats. |
| 376 | +
|
| 377 | + Args: |
| 378 | + model: Model identifier (e.g. ``"openai:whisper-1"``). |
| 379 | + file: Raw audio bytes to transcribe. |
| 380 | + filename: Filename for the multipart upload (some providers infer |
| 381 | + the audio format from its extension). |
| 382 | + **kwargs: Additional parameters (``language``, ``prompt``, |
| 383 | + ``response_format``, ``temperature``, ``user``). |
| 384 | + """ |
| 385 | + data = {"model": model, **{key: str(value) for key, value in kwargs.items()}} |
| 386 | + files = {"file": (filename, file)} |
| 387 | + response = await self._post("/audio/transcriptions", data=data, files=files) |
| 388 | + if "application/json" in response.headers.get("content-type", ""): |
| 389 | + return response.json() |
| 390 | + return response.text |
| 391 | + |
304 | 392 | # -- Models ------------------------------------------------------------- |
305 | 393 |
|
306 | 394 | async def list_models(self) -> list[ModelObject]: |
@@ -378,6 +466,28 @@ async def _call(self, fn: Callable[[], Any]) -> Any: |
378 | 466 | except ApiException as exc: |
379 | 467 | raise self._map_api_exception(exc) from exc |
380 | 468 |
|
| 469 | + async def _post( |
| 470 | + self, |
| 471 | + path: str, |
| 472 | + *, |
| 473 | + json: dict[str, Any] | None = None, |
| 474 | + data: dict[str, Any] | None = None, |
| 475 | + files: dict[str, Any] | None = None, |
| 476 | + ) -> httpx.Response: |
| 477 | + """Issue a non-streaming raw httpx POST, mapping error responses. |
| 478 | +
|
| 479 | + Audio endpoints (binary speech, multipart transcription) do not fit the |
| 480 | + generated JSON core, so they post directly over httpx and reuse the same |
| 481 | + error mapping as the streaming shim. |
| 482 | + """ |
| 483 | + url = f"{self._base_url}{path}" |
| 484 | + response = await self._http.post( |
| 485 | + url, headers=self._default_headers, json=json, data=data, files=files |
| 486 | + ) |
| 487 | + if response.status_code >= 400: |
| 488 | + raise self._map_streaming_response(response, response.content) |
| 489 | + return response |
| 490 | + |
381 | 491 | async def _stream(self, path: str, body: dict[str, Any], kind: Any) -> AsyncIterator[Any]: |
382 | 492 | """Open a raw async streaming POST and yield parsed SSE chunks.""" |
383 | 493 | url = f"{self._base_url}{path}" |
|
0 commit comments