From d1fd6a4bae21fafacc174a676eb6919d10805cc6 Mon Sep 17 00:00:00 2001 From: Omar-V2 Date: Mon, 8 Jun 2026 15:33:26 +0100 Subject: [PATCH] Add Imagine file storage & file-ID inputs to image/video generation - Add storage_options (filename, expires_after, public_url) to image/video generation to persist outputs to the Files API, with file_output/storage_error/public_url/public_url_error response properties - Accept Files API file_id references as generation inputs (image_file_id/image_file_ids, video_file_id, reference_image_file_ids), including mixing URLs and file IDs in multi-image requests - Regenerate v5/v6 proto stubs for files/image/video - Add unit tests across sync/aio for storage options, file-ID inputs, and response properties --- CHANGELOG.md | 3 + src/xai_sdk/aio/image.py | 95 +++++- src/xai_sdk/aio/video.py | 150 ++++++++- src/xai_sdk/files.py | 70 +++- src/xai_sdk/image.py | 123 ++++++- src/xai_sdk/proto/v5/files_pb2.py | 56 ++-- src/xai_sdk/proto/v5/files_pb2.pyi | 46 ++- src/xai_sdk/proto/v5/files_pb2_grpc.py | 121 +++++++ src/xai_sdk/proto/v5/image_pb2.py | 49 +-- src/xai_sdk/proto/v5/image_pb2.pyi | 53 ++- src/xai_sdk/proto/v5/video_pb2.py | 44 +-- src/xai_sdk/proto/v5/video_pb2.pyi | 26 +- src/xai_sdk/proto/v6/files_pb2.py | 56 ++-- src/xai_sdk/proto/v6/files_pb2.pyi | 46 ++- src/xai_sdk/proto/v6/files_pb2_grpc.py | 121 +++++++ src/xai_sdk/proto/v6/image_pb2.py | 49 +-- src/xai_sdk/proto/v6/image_pb2.pyi | 53 ++- src/xai_sdk/proto/v6/video_pb2.py | 44 +-- src/xai_sdk/proto/v6/video_pb2.pyi | 26 +- src/xai_sdk/sync/image.py | 95 +++++- src/xai_sdk/sync/video.py | 150 ++++++++- src/xai_sdk/video.py | 150 ++++++++- tests/aio/image_test.py | 351 +++++++++++++++++++- tests/aio/video_test.py | 433 +++++++++++++++++++++++++ tests/sync/files_test.py | 62 +++- tests/sync/image_test.py | 335 ++++++++++++++++++- tests/sync/video_test.py | 408 +++++++++++++++++++++++ 27 files changed, 2946 insertions(+), 269 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57ddc4a..6c8bad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Added +- **Imagine File Storage**: Image and video generation (sync and async) now accept a `storage_options` parameter to persist generated assets to the Files API. It takes a dict with a required `filename` and optional `expires_after` (an `int` in seconds or a `datetime.timedelta`) and `public_url` (`True` to create a public URL with default expiry, or `{"expires_after": }` for an independent URL expiry). Image and video responses expose new `file_output`, `storage_error`, `public_url`, and `public_url_error` properties. +- **File-ID Inputs for Generation**: Image and video generation now accept Files API `file_id` references as inputs alongside URLs/base64 — `image_file_id` / `image_file_ids` for `image.sample()` / `image.sample_batch()`, and `image_file_id` / `video_file_id` / `reference_image_file_ids` for `video.generate()` / `video.extend()` (and the batch `prepare` helpers). URL and file-ID lists may be mixed in the same multi-image request (file IDs are sent first). ## [v1.14.0] ### Added diff --git a/src/xai_sdk/aio/image.py b/src/xai_sdk/aio/image.py index 3192e11..6beb03a 100644 --- a/src/xai_sdk/aio/image.py +++ b/src/xai_sdk/aio/image.py @@ -4,6 +4,7 @@ from opentelemetry.trace import SpanKind from ..__about__ import __version__ +from ..files import StorageOptions from ..image import ( BaseClient, BaseImageResponse, @@ -14,7 +15,7 @@ _make_span_request_attributes, _make_span_response_attributes, ) -from ..proto import batch_pb2 +from ..proto import batch_pb2, image_pb2 from ..telemetry import get_tracer from ..types import ImageGenerationModel @@ -31,11 +32,14 @@ def prepare( *, batch_request_id: Optional[str] = None, image_url: Optional[str] = None, + image_file_id: Optional[str] = None, image_urls: Optional[Sequence[str]] = None, + image_file_ids: Optional[Sequence[str]] = None, user: Optional[str] = None, image_format: Optional[ImageFormat] = None, aspect_ratio: Optional[ImageAspectRatio] = None, resolution: Optional[ImageResolution] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> batch_pb2.BatchRequest: """Prepares an image generation request for batch processing. @@ -49,13 +53,35 @@ def prepare( **If provided, it must be unique within the batch.** Used to identify the corresponding result when the response is returned. image_url: The URL or base64-encoded string of an input image to use as a starting point. - Cannot be set together with `image_urls`. Only supported for grok-imagine models. + Cannot be set together with `image_file_id`, `image_urls`, or `image_file_ids`. + Only supported for grok-imagine models. + image_file_id: The file ID of an input image to use as a starting point. + Cannot be set together with `image_url`, `image_urls`, or `image_file_ids`. + Only supported for grok-imagine models. image_urls: Optional list of input images for multi-reference image editing. - Cannot be set together with `image_url`. Only supported for grok-imagine models. + Cannot be set together with single-image params (`image_url` / + `image_file_id`). May be combined with `image_file_ids` to mix + URL/base64 and file-ID references in the same request; file IDs + are appended first in that case. + Only supported for grok-imagine models. + image_file_ids: Optional list of input image file IDs for multi-reference image editing. + Cannot be set together with single-image params (`image_url` / + `image_file_id`). May be combined with `image_urls` to mix + URL/base64 and file-ID references in the same request. + Only supported for grok-imagine models. user: A unique identifier representing your end-user. image_format: The format of the image to return ("url" or "base64"). Defaults to "url". aspect_ratio: The aspect ratio of the image to generate. resolution: The image resolution to generate ("1k" or "2k"). + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.png"} # store privately, no expiry + storage_options={"filename": "output.png", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.png", "public_url": True} # + shareable URL + storage_options={"filename": "output.png", "public_url": {"expires_after": 86400}} # + expiring URL Returns: A `BatchRequest` proto ready to be added to a batch. @@ -91,11 +117,14 @@ def prepare( prompt, model, image_url=image_url, + image_file_id=image_file_id, image_urls=image_urls, + image_file_ids=image_file_ids, user=user, image_format=image_format, aspect_ratio=aspect_ratio, resolution=resolution, + storage_options=storage_options, ) return batch_pb2.BatchRequest( image_request=request, @@ -108,11 +137,14 @@ async def sample( model: Union[ImageGenerationModel, str], *, image_url: Optional[str] = None, + image_file_id: Optional[str] = None, image_urls: Optional[Sequence[str]] = None, + image_file_ids: Optional[Sequence[str]] = None, user: Optional[str] = None, image_format: Optional[ImageFormat] = None, aspect_ratio: Optional[ImageAspectRatio] = None, resolution: Optional[ImageResolution] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> "ImageResponse": """Samples a single image asynchronously based on the provided prompt. @@ -120,11 +152,22 @@ async def sample( prompt: The prompt to generate an image from. model: The model to use for image generation. image_url: The URL or base64-encoded string of an input image to use as a starting point for generation. - This field cannot be set together with `image_urls`. + This field cannot be set together with `image_file_id`, `image_urls`, or `image_file_ids`. + Only supported for grok-imagine models. + image_file_id: The file ID of an input image to use as a starting point for generation. + Cannot be set together with `image_url`, `image_urls`, or `image_file_ids`. Only supported for grok-imagine models. image_urls: Optional list of input images for multi-reference image editing. Each image is a URL or base64-encoded string, matching the `image_url` format. - This field cannot be set together with `image_url`. + Cannot be set together with single-image params (`image_url` / + `image_file_id`). May be combined with `image_file_ids` to mix + URL/base64 and file-ID references in the same request; file IDs + are appended first in that case. + Only supported for grok-imagine models. + image_file_ids: Optional list of input image file IDs for multi-reference image editing. + Cannot be set together with single-image params (`image_url` / + `image_file_id`). May be combined with `image_urls` to mix + URL/base64 and file-ID references in the same request. Only supported for grok-imagine models. user: A unique identifier representing your end-user, which can help xAI to monitor and detect abuse. image_format: The format of the image to return. One of: @@ -150,6 +193,15 @@ async def sample( - `"1k"`: ~1 megapixel total. Dimensions vary by aspect ratio. - `"2k"`: ~4 megapixels total. Dimensions vary by aspect ratio. Only supported for grok-imagine models. + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.png"} # store privately, no expiry + storage_options={"filename": "output.png", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.png", "public_url": True} # + shareable URL + storage_options={"filename": "output.png", "public_url": {"expires_after": 86400}} # + expiring URL Returns: An `ImageResponse` object allowing access to the generated image. @@ -158,11 +210,14 @@ async def sample( prompt, model, image_url=image_url, + image_file_id=image_file_id, image_urls=image_urls, + image_file_ids=image_file_ids, user=user, image_format=image_format, aspect_ratio=aspect_ratio, resolution=resolution, + storage_options=storage_options, ) with tracer.start_as_current_span( name=f"image.sample {model}", @@ -181,11 +236,14 @@ async def sample_batch( n: int, *, image_url: Optional[str] = None, + image_file_id: Optional[str] = None, image_urls: Optional[Sequence[str]] = None, + image_file_ids: Optional[Sequence[str]] = None, user: Optional[str] = None, image_format: Optional[ImageFormat] = None, aspect_ratio: Optional[ImageAspectRatio] = None, resolution: Optional[ImageResolution] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> Sequence["ImageResponse"]: """Samples a batch of images asynchronously based on the provided prompt. @@ -194,11 +252,22 @@ async def sample_batch( model: The model to use for image generation. n: The number of images to generate. image_url: The URL or base64-encoded string of an input image to use as a starting point for generation. - This field cannot be set together with `image_urls`. + This field cannot be set together with `image_file_id`, `image_urls`, or `image_file_ids`. + Only supported for grok-imagine models. + image_file_id: The file ID of an input image to use as a starting point for generation. + Cannot be set together with `image_url`, `image_urls`, or `image_file_ids`. Only supported for grok-imagine models. image_urls: Optional list of input images for multi-reference image editing. Each image is a URL or base64-encoded string, matching the `image_url` format. - This field cannot be set together with `image_url`. + Cannot be set together with single-image params (`image_url` / + `image_file_id`). May be combined with `image_file_ids` to mix + URL/base64 and file-ID references in the same request; file IDs + are appended first in that case. + Only supported for grok-imagine models. + image_file_ids: Optional list of input image file IDs for multi-reference image editing. + Cannot be set together with single-image params (`image_url` / + `image_file_id`). May be combined with `image_urls` to mix + URL/base64 and file-ID references in the same request. Only supported for grok-imagine models. user: A unique identifier representing your end-user, which can help xAI to monitor and detect abuse. image_format: The format of the image to return. One of: @@ -224,6 +293,15 @@ async def sample_batch( - `"1k"`: ~1 megapixel total. Dimensions vary by aspect ratio. - `"2k"`: ~4 megapixels total. Dimensions vary by aspect ratio. Only supported for grok-imagine models. + storage_options: Persist the results to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.png"} # store privately, no expiry + storage_options={"filename": "output.png", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.png", "public_url": True} # + shareable URL + storage_options={"filename": "output.png", "public_url": {"expires_after": 86400}} # + expiring URL Returns: A sequence of `ImageResponse` objects, one for each image generated. @@ -233,11 +311,14 @@ async def sample_batch( model, n=n, image_url=image_url, + image_file_id=image_file_id, image_urls=image_urls, + image_file_ids=image_file_ids, user=user, image_format=image_format, aspect_ratio=aspect_ratio, resolution=resolution, + storage_options=storage_options, ) with tracer.start_as_current_span( name=f"image.sample_batch {model}", diff --git a/src/xai_sdk/aio/video.py b/src/xai_sdk/aio/video.py index f181570..a1c0ed3 100644 --- a/src/xai_sdk/aio/video.py +++ b/src/xai_sdk/aio/video.py @@ -5,8 +5,9 @@ from opentelemetry.trace import SpanKind +from ..files import StorageOptions from ..poll_timer import PollTimer -from ..proto import batch_pb2, deferred_pb2, video_pb2 +from ..proto import batch_pb2, deferred_pb2, image_pb2, video_pb2 from ..telemetry import get_tracer from ..types import VideoGenerationModel from ..video import ( @@ -38,11 +39,15 @@ def prepare( *, batch_request_id: Optional[str] = None, image_url: Optional[str] = None, + image_file_id: Optional[str] = None, video_url: Optional[str] = None, + video_file_id: Optional[str] = None, duration: Optional[int] = None, aspect_ratio: Optional[VideoAspectRatio] = None, resolution: Optional[VideoResolution] = None, reference_image_urls: Optional[Sequence[str]] = None, + reference_image_file_ids: Optional[Sequence[str]] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> batch_pb2.BatchRequest: """Prepares a video generation request for batch processing. @@ -56,13 +61,35 @@ def prepare( **If provided, it must be unique within the batch.** Used to identify the corresponding result when the response is returned. image_url: The URL of an input image to use as a starting frame. + Cannot be set together with ``image_file_id``. + image_file_id: The file ID of an input image to use as a starting frame. + Cannot be set together with ``image_url``. video_url: The URL of an input video to use as a starting point. + Cannot be set together with ``video_file_id``. + video_file_id: The file ID of an input video to use as a starting point. + Cannot be set together with ``video_url``. duration: The duration of the video to generate in seconds. aspect_ratio: The aspect ratio of the video to generate. resolution: The video resolution to generate. reference_image_urls: Optional list of reference image URLs for reference-to-video (R2V) generation. When provided (and `image_url` is not set), generates video using these images as style/content references. + May be combined with ``reference_image_file_ids`` to mix URL/base64 + and file-ID references in the same request; file IDs are appended + first in that case. + reference_image_file_ids: Optional list of reference image file IDs for + reference-to-video (R2V) generation. May be combined with + ``reference_image_urls`` to mix URL/base64 and file-ID references + in the same request. + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.mp4"} # store privately, no expiry + storage_options={"filename": "output.mp4", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.mp4", "public_url": True} # + shareable URL + storage_options={"filename": "output.mp4", "public_url": {"expires_after": 86400}} # + expiring URL Returns: A `BatchRequest` proto ready to be added to a batch. @@ -98,11 +125,15 @@ def prepare( prompt, model, image_url=image_url, + image_file_id=image_file_id, video_url=video_url, + video_file_id=video_file_id, duration=duration, aspect_ratio=aspect_ratio, resolution=resolution, reference_image_urls=reference_image_urls, + reference_image_file_ids=reference_image_file_ids, + storage_options=storage_options, ) return batch_pb2.BatchRequest( @@ -114,10 +145,12 @@ def prepare_extension( self, prompt: str, model: Union[VideoGenerationModel, str], - video_url: str, + video_url: Optional[str] = None, *, + video_file_id: Optional[str] = None, batch_request_id: Optional[str] = None, duration: Optional[int] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> batch_pb2.BatchRequest: """Prepares a video extension request for batch processing. @@ -128,10 +161,22 @@ def prepare_extension( prompt: Prompt describing what should happen next in the video. model: The model to use for video extension. video_url: The URL of the input video to extend. + Cannot be set together with ``video_file_id``. + video_file_id: The file ID of the input video to extend. + Cannot be set together with ``video_url``. batch_request_id: An optional user-provided identifier for the batch request. **If provided, it must be unique within the batch.** duration: Duration of the extension segment in seconds (1-10). Defaults to 6 seconds if not specified. + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.mp4"} # store privately, no expiry + storage_options={"filename": "output.mp4", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.mp4", "public_url": True} # + shareable URL + storage_options={"filename": "output.mp4", "public_url": {"expires_after": 86400}} # + expiring URL Returns: A `BatchRequest` proto ready to be added to a batch. @@ -139,8 +184,10 @@ def prepare_extension( request = _make_extend_request( prompt, model, - video_url, + video_url=video_url, + video_file_id=video_file_id, duration=duration, + storage_options=storage_options, ) return batch_pb2.BatchRequest( @@ -154,22 +201,33 @@ async def start( model: Union[VideoGenerationModel, str], *, image_url: Optional[str] = None, + image_file_id: Optional[str] = None, video_url: Optional[str] = None, + video_file_id: Optional[str] = None, duration: Optional[int] = None, aspect_ratio: Optional[VideoAspectRatio] = None, resolution: Optional[VideoResolution] = None, reference_image_urls: Optional[Sequence[str]] = None, + reference_image_file_ids: Optional[Sequence[str]] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> deferred_pb2.StartDeferredResponse: - """Starts a video generation request and returns a request_id for polling.""" + """Starts a video generation request and returns a request_id for polling. + + See `generate()` for full parameter documentation. + """ request = _make_generate_request( prompt, model, image_url=image_url, + image_file_id=image_file_id, video_url=video_url, + video_file_id=video_file_id, duration=duration, aspect_ratio=aspect_ratio, resolution=resolution, reference_image_urls=reference_image_urls, + reference_image_file_ids=reference_image_file_ids, + storage_options=storage_options, ) with tracer.start_as_current_span( @@ -190,11 +248,15 @@ async def generate( model: Union[VideoGenerationModel, str], *, image_url: Optional[str] = None, + image_file_id: Optional[str] = None, video_url: Optional[str] = None, + video_file_id: Optional[str] = None, duration: Optional[int] = None, aspect_ratio: Optional[VideoAspectRatio] = None, resolution: Optional[VideoResolution] = None, reference_image_urls: Optional[Sequence[str]] = None, + reference_image_file_ids: Optional[Sequence[str]] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, timeout: Optional[datetime.timedelta] = None, interval: Optional[datetime.timedelta] = None, ) -> VideoResponse: @@ -206,18 +268,26 @@ async def generate( Supports four generation modes depending on which optional inputs are provided: - **Text-to-video**: Only a `prompt` is provided. - - **Image-to-video**: An `image_url` is provided; the image is used as the first frame. - - **Reference-to-video**: `reference_image_urls` are provided; generates video using the - images as style/content references. - - **Video editing**: A `video_url` is provided; the video is edited based on the prompt. + - **Image-to-video**: An `image_url` or `image_file_id` is provided; the image is + used as the first frame. + - **Reference-to-video**: `reference_image_urls` or `reference_image_file_ids` are + provided; generates video using the images as style/content references. + - **Video editing**: A `video_url` or `video_file_id` is provided; the video is edited + based on the prompt. Args: prompt: The text prompt to generate a video from. model: The model to use for video generation. image_url: The URL or base64-encoded data URL of an input image to use as - the first frame (image-to-video). Cannot be combined with `video_url`. + the first frame (image-to-video). Cannot be combined with `image_file_id` + or `video_url`. + image_file_id: The file ID of an input image to use as the first frame + (image-to-video). Cannot be combined with `image_url`. video_url: The URL or base64-encoded data URL of an input video to edit - based on the prompt (video-to-video). Cannot be combined with `image_url`. + based on the prompt (video-to-video). Cannot be combined with `video_file_id` + or `image_url`. + video_file_id: The file ID of an input video to edit based on the prompt + (video-to-video). Cannot be combined with `video_url`. duration: Duration of the video to generate in seconds (1-15). Defaults to 8 seconds if not specified. aspect_ratio: The aspect ratio of the video to generate. @@ -227,6 +297,22 @@ async def generate( reference_image_urls: Optional list of reference image URLs for reference-to-video (R2V) generation. When provided (and `image_url` is not set), generates video using these images as style/content references. + May be combined with `reference_image_file_ids` to mix URL/base64 + and file-ID references in the same request; file IDs are appended + first in that case. + reference_image_file_ids: Optional list of reference image file IDs for + reference-to-video (R2V) generation. May be combined with + `reference_image_urls` to mix URL/base64 and file-ID references + in the same request. + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.mp4"} # store privately, no expiry + storage_options={"filename": "output.mp4", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.mp4", "public_url": True} # + shareable URL + storage_options={"filename": "output.mp4", "public_url": {"expires_after": 86400}} # + expiring URL timeout: Maximum time to wait for video generation to complete. Defaults to 10 minutes. interval: Polling interval between status checks. @@ -290,11 +376,15 @@ async def generate( prompt, model, image_url=image_url, + image_file_id=image_file_id, video_url=video_url, + video_file_id=video_file_id, duration=duration, aspect_ratio=aspect_ratio, resolution=resolution, reference_image_urls=reference_image_urls, + reference_image_file_ids=reference_image_file_ids, + storage_options=storage_options, ) with tracer.start_as_current_span( @@ -336,9 +426,11 @@ async def extend_start( self, prompt: str, model: Union[VideoGenerationModel, str], - video_url: str, + video_url: Optional[str] = None, *, + video_file_id: Optional[str] = None, duration: Optional[int] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> deferred_pb2.StartDeferredResponse: """Starts a video extension request and returns a request_id for polling. @@ -347,8 +439,20 @@ async def extend_start( model: The model to use for video extension. video_url: The URL of the input video to extend. The extension continues from the end of this video. Input video must be between 2 and 30 seconds long. + Cannot be set together with ``video_file_id``. + video_file_id: The file ID of the input video to extend. The extension continues + from the end of this video. Cannot be set together with ``video_url``. duration: Duration of the extension segment to generate in seconds (1-10). Defaults to 6 seconds if not specified. + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.mp4"} # store privately, no expiry + storage_options={"filename": "output.mp4", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.mp4", "public_url": True} # + shareable URL + storage_options={"filename": "output.mp4", "public_url": {"expires_after": 86400}} # + expiring URL Returns: A `StartDeferredResponse` containing the `request_id` for polling. @@ -356,8 +460,10 @@ async def extend_start( request = _make_extend_request( prompt, model, - video_url, + video_url=video_url, + video_file_id=video_file_id, duration=duration, + storage_options=storage_options, ) with tracer.start_as_current_span( @@ -371,9 +477,11 @@ async def extend( self, prompt: str, model: Union[VideoGenerationModel, str], - video_url: str, + video_url: Optional[str] = None, *, + video_file_id: Optional[str] = None, duration: Optional[int] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, timeout: Optional[datetime.timedelta] = None, interval: Optional[datetime.timedelta] = None, ) -> VideoResponse: @@ -392,8 +500,20 @@ async def extend( video_url: The URL or base64-encoded data URL of the input video to extend. The extension continues from the end of this video. Input video must be between 2 and 30 seconds long. + Cannot be set together with ``video_file_id``. + video_file_id: The file ID of the input video to extend. The extension continues + from the end of this video. Cannot be set together with ``video_url``. duration: Duration of the extension segment to generate in seconds (1-10). Defaults to 6 seconds if not specified. + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.mp4"} # store privately, no expiry + storage_options={"filename": "output.mp4", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.mp4", "public_url": True} # + shareable URL + storage_options={"filename": "output.mp4", "public_url": {"expires_after": 86400}} # + expiring URL timeout: Maximum time to wait for video extension to complete. Defaults to 10 minutes. interval: Polling interval between status checks. @@ -439,8 +559,10 @@ async def extend( request_pb = _make_extend_request( prompt, model, - video_url, + video_url=video_url, + video_file_id=video_file_id, duration=duration, + storage_options=storage_options, ) with tracer.start_as_current_span( diff --git a/src/xai_sdk/files.py b/src/xai_sdk/files.py index c6d9ca1..cd51806 100644 --- a/src/xai_sdk/files.py +++ b/src/xai_sdk/files.py @@ -4,8 +4,10 @@ from typing import Any, AsyncIterator, BinaryIO, Callable, Iterator, Literal, Optional, Protocol, Union import grpc +from pydantic import TypeAdapter +from typing_extensions import NotRequired, TypedDict -from .proto import files_pb2, files_pb2_grpc +from .proto import files_pb2, files_pb2_grpc, image_pb2 # Chunk size for file uploads (3 MiB) _CHUNK_SIZE = 3 << 20 @@ -23,6 +25,72 @@ def _expires_after_to_seconds(expires_after: Optional[Union[datetime.timedelta, return expires_after +class PublicUrlOptions(TypedDict, total=False): + """Options for creating a public URL alongside file storage. + + Attributes: + expires_after: Seconds (or ``timedelta``) until the public URL expires. + Independent of the file's ``expires_after`` -- the file remains + accessible via authenticated endpoints after the public URL expires. + If omitted, the public URL inherits the file's TTL, or remains + valid indefinitely if the file has no TTL. + """ + + expires_after: Union[datetime.timedelta, int] + + +# Accept either True (create with defaults) or a PublicUrlOptions dict. +PublicUrlInput = Union[bool, PublicUrlOptions] + + +class StorageOptions(TypedDict): + """Options for storing generated assets in the Files API. + + Attributes: + filename: Filename for the stored file (required). + expires_after: Time until the stored file auto-deletes. Accepts an + ``int`` (seconds) or a ``datetime.timedelta``. + public_url: ``True`` to create a public URL with default TTL behavior + (inherits the file's TTL, or no expiry if the file has none). + Pass ``{"expires_after": }`` to set an independent TTL + on the public URL. Omit entirely to store the file privately. + """ + + filename: str + expires_after: NotRequired[Union[datetime.timedelta, int]] + public_url: NotRequired[PublicUrlInput] + + +# Validates StorageOptions dicts at runtime (required keys, types, and the +# bool-or-PublicUrlOptions `public_url` union). Raises pydantic.ValidationError. +StorageOptionsValidator = TypeAdapter(StorageOptions) + + +def _resolve_storage_options_pb( + storage_options: Union[StorageOptions, image_pb2.StorageOptions], +) -> image_pb2.StorageOptions: + """Converts a StorageOptions TypedDict or proto into a StorageOptions protobuf message.""" + if isinstance(storage_options, image_pb2.StorageOptions): + return storage_options + + validated = StorageOptionsValidator.validate_python(storage_options) + pb = image_pb2.StorageOptions( + filename=validated["filename"], + expires_after=_expires_after_to_seconds(validated.get("expires_after")), + ) + public_url = validated.get("public_url") + if public_url is True: + # True = create public URL with defaults (no independent expiry). + pb.public_url.CopyFrom(image_pb2.PublicUrlOptions()) + elif isinstance(public_url, dict): + pb.public_url.CopyFrom( + image_pb2.PublicUrlOptions( + expires_after=_expires_after_to_seconds(public_url.get("expires_after")), + ) + ) + return pb + + class ProgressBarLike(Protocol): """Protocol for tqdm-like progress bars.""" diff --git a/src/xai_sdk/image.py b/src/xai_sdk/image.py index 49db100..54ab127 100644 --- a/src/xai_sdk/image.py +++ b/src/xai_sdk/image.py @@ -5,6 +5,7 @@ import grpc from .cost import cost_usd_from_usage +from .files import StorageOptions, _resolve_storage_options_pb from .meta import ProtoDecorator from .proto import image_pb2, image_pb2_grpc, usage_pb2 from .telemetry import should_disable_sensitive_attributes @@ -111,6 +112,36 @@ def base64(self) -> str: raise ValueError("Image was not returned via base64.") return value + @property + def file_output(self) -> Optional[image_pb2.FileOutput]: + """The full FileOutput proto if the asset was stored, or None otherwise.""" + if self._image.HasField("file_output"): + return self._image.file_output + return None + + @property + def storage_error(self) -> Optional[str]: + """Error message if storage was requested but failed, or None on success.""" + if self._image.storage_error: + return self._image.storage_error + return None + + @property + def public_url(self) -> Optional[str]: + """Public URL for the stored file, or None if not requested or creation failed.""" + file_output = self.file_output + if file_output is not None and file_output.public_url: + return file_output.public_url + return None + + @property + def public_url_error(self) -> Optional[str]: + """Error message if public URL creation failed, or None on success.""" + file_output = self.file_output + if file_output is not None and file_output.public_url_error: + return file_output.public_url_error + return None + def _decode_base64(self) -> bytes: """Returns the raw image buffer from a base64-encoded response.""" encoded = self.base64 @@ -119,20 +150,42 @@ def _decode_base64(self) -> bytes: return base64.b64decode(encoded_buffer) +def _validate_image_inputs( + image_url: str | None, + image_file_id: str | None, + image_urls: Sequence[str] | None, + image_file_ids: Sequence[str] | None, +) -> None: + """Validates mutual exclusion constraints on image input parameters. + + `image_urls` and `image_file_ids` may be supplied together so callers can + mix URL/base64 and file-ID references in the same multi-image request. + When both are provided, file IDs are appended first, then URLs. + """ + if image_url is not None and image_file_id is not None: + raise ValueError("Only one of image_url or image_file_id can be set for a request.") + has_single = image_url is not None or image_file_id is not None + has_multi = image_urls is not None or image_file_ids is not None + if has_single and has_multi: + raise ValueError("Only one of image_url/image_file_id or image_urls/image_file_ids can be set for a request.") + + def _make_generate_request( prompt: str, model: Union[ImageGenerationModel, str], *, n: int = 1, image_url: str | None = None, + image_file_id: str | None = None, image_urls: Sequence[str] | None = None, + image_file_ids: Sequence[str] | None = None, user: str | None = None, image_format: ImageFormat | None = None, aspect_ratio: ImageAspectRatio | None = None, resolution: ImageResolution | None = None, + storage_options: Union[StorageOptions, image_pb2.StorageOptions, None] = None, ) -> image_pb2.GenerateImageRequest: - if image_url is not None and image_urls is not None: - raise ValueError("Only one of image_url or image_urls can be set for a request.") + _validate_image_inputs(image_url, image_file_id, image_urls, image_file_ids) image_format = image_format or "url" request = image_pb2.GenerateImageRequest( @@ -149,6 +202,27 @@ def _make_generate_request( detail=image_pb2.ImageDetail.DETAIL_AUTO, ) ) + elif image_file_id is not None: + request.image.CopyFrom( + image_pb2.ImageUrlContent( + file_id=image_file_id, + detail=image_pb2.ImageDetail.DETAIL_AUTO, + ) + ) + # When both `image_file_ids` and `image_urls` are provided we send both + # lists in the same `images` field. File IDs are appended first so callers + # can predict the resulting `` positional indices used by the + # model prompt. + if image_file_ids is not None: + request.images.extend( + [ + image_pb2.ImageUrlContent( + file_id=fid, + detail=image_pb2.ImageDetail.DETAIL_AUTO, + ) + for fid in image_file_ids + ] + ) if image_urls is not None: request.images.extend( [ @@ -163,6 +237,8 @@ def _make_generate_request( request.aspect_ratio = convert_image_aspect_ratio_to_pb(aspect_ratio) if resolution is not None: request.resolution = convert_image_resolution_to_pb(resolution) + if storage_options is not None: + request.storage_options.CopyFrom(_resolve_storage_options_pb(storage_options)) return request @@ -183,6 +259,15 @@ def _make_span_request_attributes(request: image_pb2.GenerateImageRequest) -> di ) attributes["gen_ai.prompt"] = request.prompt + if request.HasField("storage_options"): + attributes["gen_ai.request.storage"] = True + if request.storage_options.filename: + attributes["gen_ai.request.storage.filename"] = request.storage_options.filename + if request.storage_options.expires_after: + attributes["gen_ai.request.storage.expires_after"] = request.storage_options.expires_after + if request.storage_options.HasField("public_url"): + attributes["gen_ai.request.storage.public_url"] = True + if request.HasField("n"): attributes["gen_ai.request.image.count"] = request.n if request.HasField("aspect_ratio"): @@ -197,6 +282,30 @@ def _make_span_request_attributes(request: image_pb2.GenerateImageRequest) -> di return attributes +def _collect_per_image_attributes( + prefix: str, + response: BaseImageResponse, + image_format: int, +) -> dict[str, Any]: + """Collects span attributes for a single generated image.""" + attributes: dict[str, Any] = { + f"{prefix}.respect_moderation": response.respect_moderation, + } + if image_format == image_pb2.ImageFormat.IMG_FORMAT_URL and response._image.url: + attributes[f"{prefix}.url"] = response._image.url + elif image_format == image_pb2.ImageFormat.IMG_FORMAT_BASE64 and response._image.base64: + attributes[f"{prefix}.base64"] = response._image.base64 + if response.file_output and response.file_output.file_id: + attributes[f"{prefix}.file_id"] = response.file_output.file_id + if response.public_url: + attributes[f"{prefix}.public_url"] = response.public_url + if response.public_url_error: + attributes[f"{prefix}.public_url_error"] = response.public_url_error + if response.storage_error: + attributes[f"{prefix}.storage_error"] = response.storage_error + return attributes + + def _make_span_response_attributes( request: image_pb2.GenerateImageRequest, responses: Sequence[BaseImageResponse] ) -> dict[str, Any]: @@ -208,7 +317,6 @@ def _make_span_response_attributes( if should_disable_sensitive_attributes(): return attributes - # All of these attributes are the same for all images in this response. if responses: usage = responses[0].usage attributes["gen_ai.usage.input_tokens"] = usage.prompt_tokens @@ -225,14 +333,7 @@ def _make_span_response_attributes( image_pb2.ImageFormat.Name(request.format).removeprefix("IMG_FORMAT_").lower() ) for index, response in enumerate(responses): - attributes[f"gen_ai.response.{index}.image.up_sampled_prompt"] = "" - attributes[f"gen_ai.response.{index}.image.respect_moderation"] = response.respect_moderation - if request.format == image_pb2.ImageFormat.IMG_FORMAT_URL: - if response._image.url: - attributes[f"gen_ai.response.{index}.image.url"] = response._image.url - elif request.format == image_pb2.ImageFormat.IMG_FORMAT_BASE64: - if response._image.base64: - attributes[f"gen_ai.response.{index}.image.base64"] = response._image.base64 + attributes.update(_collect_per_image_attributes(f"gen_ai.response.{index}.image", response, request.format)) return attributes diff --git a/src/xai_sdk/proto/v5/files_pb2.py b/src/xai_sdk/proto/v5/files_pb2.py index c92fd44..1b79849 100644 --- a/src/xai_sdk/proto/v5/files_pb2.py +++ b/src/xai_sdk/proto/v5/files_pb2.py @@ -25,7 +25,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/files.proto\x12\x07xai_api\x1a\x1fgoogle/protobuf/timestamp.proto\"`\n\x0eUploadFileInit\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12(\n\rexpires_after\x18\x02 \x01(\x03H\x00R\x0c\x65xpiresAfter\x88\x01\x01\x42\x10\n\x0e_expires_after\"_\n\x0fUploadFileChunk\x12-\n\x04init\x18\x01 \x01(\x0b\x32\x17.xai_api.UploadFileInitH\x00R\x04init\x12\x14\n\x04\x64\x61ta\x18\x02 \x01(\x0cH\x00R\x04\x64\x61taB\x07\n\x05\x63hunk\"\xd6\x01\n\x04\x46ile\x12\x12\n\x04size\x18\x01 \x01(\x03R\x04size\x12\x39\n\ncreated_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12>\n\nexpires_at\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\texpiresAt\x88\x01\x01\x12\x1a\n\x08\x66ilename\x18\x04 \x01(\tR\x08\x66ilename\x12\x0e\n\x02id\x18\x05 \x01(\tR\x02idB\r\n\x0b_expires_atJ\x04\x08\x06\x10\x07\"\xd6\x01\n\x10ListFilesRequest\x12\x14\n\x05limit\x18\x01 \x01(\x05R\x05limit\x12\'\n\x05order\x18\x02 \x01(\x0e\x32\x11.xai_api.OrderingR\x05order\x12.\n\x10pagination_token\x18\x03 \x01(\tH\x00R\x0fpaginationToken\x88\x01\x01\x12\x32\n\x07sort_by\x18\x04 \x01(\x0e\x32\x14.xai_api.FilesSortByH\x01R\x06sortBy\x88\x01\x01\x42\x13\n\x11_pagination_tokenB\n\n\x08_sort_by\"{\n\x11ListFilesResponse\x12!\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\r.xai_api.FileR\x04\x64\x61ta\x12.\n\x10pagination_token\x18\x02 \x01(\tH\x00R\x0fpaginationToken\x88\x01\x01\x42\x13\n\x11_pagination_token\".\n\x13RetrieveFileRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\",\n\x11\x44\x65leteFileRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\">\n\x12\x44\x65leteFileResponse\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n\x07\x64\x65leted\x18\x02 \x01(\x08R\x07\x64\x65leted\"v\n\x1aRetrieveFileContentRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\x12\x34\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x17.xai_api.DownloadFormatH\x00R\x06\x66ormat\x88\x01\x01\x42\t\n\x07_format\"&\n\x10\x46ileContentChunk\x12\x12\n\x04\x64\x61ta\x18\x01 \x01(\x0cR\x04\x64\x61ta*)\n\x08Ordering\x12\r\n\tASCENDING\x10\x00\x12\x0e\n\nDESCENDING\x10\x01*_\n\x0b\x46ilesSortBy\x12\x1c\n\x18\x46ILES_SORT_BY_CREATED_AT\x10\x00\x12\x1a\n\x16\x46ILES_SORT_BY_FILENAME\x10\x01\x12\x16\n\x12\x46ILES_SORT_BY_SIZE\x10\x02*e\n\x0e\x44ownloadFormat\x12\x1b\n\x17\x44OWNLOAD_FORMAT_UNKNOWN\x10\x00\x12\x1c\n\x18\x44OWNLOAD_FORMAT_ORIGINAL\x10\x01\x12\x18\n\x14\x44OWNLOAD_FORMAT_TEXT\x10\x02\x32\xeb\x02\n\x05\x46iles\x12\x39\n\nUploadFile\x12\x18.xai_api.UploadFileChunk\x1a\r.xai_api.File\"\x00(\x01\x12\x44\n\tListFiles\x12\x19.xai_api.ListFilesRequest\x1a\x1a.xai_api.ListFilesResponse\"\x00\x12=\n\x0cRetrieveFile\x12\x1c.xai_api.RetrieveFileRequest\x1a\r.xai_api.File\"\x00\x12G\n\nDeleteFile\x12\x1a.xai_api.DeleteFileRequest\x1a\x1b.xai_api.DeleteFileResponse\"\x00\x12Y\n\x13RetrieveFileContent\x12#.xai_api.RetrieveFileContentRequest\x1a\x19.xai_api.FileContentChunk\"\x00\x30\x01\x42Q\n\x0b\x63om.xai_apiB\nFilesProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/files.proto\x12\x07xai_api\x1a\x1fgoogle/protobuf/timestamp.proto\"`\n\x0eUploadFileInit\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12(\n\rexpires_after\x18\x02 \x01(\x03H\x00R\x0c\x65xpiresAfter\x88\x01\x01\x42\x10\n\x0e_expires_after\"_\n\x0fUploadFileChunk\x12-\n\x04init\x18\x01 \x01(\x0b\x32\x17.xai_api.UploadFileInitH\x00R\x04init\x12\x14\n\x04\x64\x61ta\x18\x02 \x01(\x0cH\x00R\x04\x64\x61taB\x07\n\x05\x63hunk\"\xf7\x02\n\x04\x46ile\x12\x12\n\x04size\x18\x01 \x01(\x03R\x04size\x12\x39\n\ncreated_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12>\n\nexpires_at\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\texpiresAt\x88\x01\x01\x12\x1a\n\x08\x66ilename\x18\x04 \x01(\tR\x08\x66ilename\x12\x0e\n\x02id\x18\x05 \x01(\tR\x02id\x12\"\n\npublic_url\x18\x07 \x01(\tH\x01R\tpublicUrl\x88\x01\x01\x12R\n\x15public_url_expires_at\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x02R\x12publicUrlExpiresAt\x88\x01\x01\x42\r\n\x0b_expires_atB\r\n\x0b_public_urlB\x18\n\x16_public_url_expires_atJ\x04\x08\x06\x10\x07\"\xfe\x01\n\x10ListFilesRequest\x12\x14\n\x05limit\x18\x01 \x01(\x05R\x05limit\x12\'\n\x05order\x18\x02 \x01(\x0e\x32\x11.xai_api.OrderingR\x05order\x12.\n\x10pagination_token\x18\x03 \x01(\tH\x00R\x0fpaginationToken\x88\x01\x01\x12\x32\n\x07sort_by\x18\x04 \x01(\x0e\x32\x14.xai_api.FilesSortByH\x01R\x06sortBy\x88\x01\x01\x12\x1b\n\x06\x66ilter\x18\x05 \x01(\tH\x02R\x06\x66ilter\x88\x01\x01\x42\x13\n\x11_pagination_tokenB\n\n\x08_sort_byB\t\n\x07_filter\"{\n\x11ListFilesResponse\x12!\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\r.xai_api.FileR\x04\x64\x61ta\x12.\n\x10pagination_token\x18\x02 \x01(\tH\x00R\x0fpaginationToken\x88\x01\x01\x42\x13\n\x11_pagination_token\".\n\x13RetrieveFileRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\",\n\x11\x44\x65leteFileRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\">\n\x12\x44\x65leteFileResponse\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n\x07\x64\x65leted\x18\x02 \x01(\x08R\x07\x64\x65leted\"v\n\x1aRetrieveFileContentRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\x12\x34\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x17.xai_api.DownloadFormatH\x00R\x06\x66ormat\x88\x01\x01\x42\t\n\x07_format\"&\n\x10\x46ileContentChunk\x12\x12\n\x04\x64\x61ta\x18\x01 \x01(\x0cR\x04\x64\x61ta\"m\n\x16\x43reatePublicUrlRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\x12(\n\rexpires_after\x18\x02 \x01(\x03H\x00R\x0c\x65xpiresAfter\x88\x01\x01\x42\x10\n\x0e_expires_after\"\x87\x01\n\x17\x43reatePublicUrlResponse\x12\x1d\n\npublic_url\x18\x01 \x01(\tR\tpublicUrl\x12>\n\nexpires_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\texpiresAt\x88\x01\x01\x42\r\n\x0b_expires_at\"1\n\x16RevokePublicUrlRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\"\x7f\n\x17RevokePublicUrlResponse\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\x12\x18\n\x07revoked\x18\x02 \x01(\x08R\x07revoked\x12\"\n\npublic_url\x18\x03 \x01(\tH\x00R\tpublicUrl\x88\x01\x01\x42\r\n\x0b_public_url*)\n\x08Ordering\x12\r\n\tASCENDING\x10\x00\x12\x0e\n\nDESCENDING\x10\x01*_\n\x0b\x46ilesSortBy\x12\x1c\n\x18\x46ILES_SORT_BY_CREATED_AT\x10\x00\x12\x1a\n\x16\x46ILES_SORT_BY_FILENAME\x10\x01\x12\x16\n\x12\x46ILES_SORT_BY_SIZE\x10\x02*e\n\x0e\x44ownloadFormat\x12\x1b\n\x17\x44OWNLOAD_FORMAT_UNKNOWN\x10\x00\x12\x1c\n\x18\x44OWNLOAD_FORMAT_ORIGINAL\x10\x01\x12\x18\n\x14\x44OWNLOAD_FORMAT_TEXT\x10\x02\x32\x9b\x04\n\x05\x46iles\x12\x39\n\nUploadFile\x12\x18.xai_api.UploadFileChunk\x1a\r.xai_api.File\"\x00(\x01\x12\x44\n\tListFiles\x12\x19.xai_api.ListFilesRequest\x1a\x1a.xai_api.ListFilesResponse\"\x00\x12=\n\x0cRetrieveFile\x12\x1c.xai_api.RetrieveFileRequest\x1a\r.xai_api.File\"\x00\x12G\n\nDeleteFile\x12\x1a.xai_api.DeleteFileRequest\x1a\x1b.xai_api.DeleteFileResponse\"\x00\x12Y\n\x13RetrieveFileContent\x12#.xai_api.RetrieveFileContentRequest\x1a\x19.xai_api.FileContentChunk\"\x00\x30\x01\x12V\n\x0f\x43reatePublicUrl\x12\x1f.xai_api.CreatePublicUrlRequest\x1a .xai_api.CreatePublicUrlResponse\"\x00\x12V\n\x0fRevokePublicUrl\x12\x1f.xai_api.RevokePublicUrlRequest\x1a .xai_api.RevokePublicUrlResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nFilesProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -33,32 +33,40 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\013com.xai_apiB\nFilesProtoP\001\242\002\003XXX\252\002\006XaiApi\312\002\006XaiApi\342\002\022XaiApi\\GPBMetadata\352\002\006XaiApi' - _globals['_ORDERING']._serialized_start=1140 - _globals['_ORDERING']._serialized_end=1181 - _globals['_FILESSORTBY']._serialized_start=1183 - _globals['_FILESSORTBY']._serialized_end=1278 - _globals['_DOWNLOADFORMAT']._serialized_start=1280 - _globals['_DOWNLOADFORMAT']._serialized_end=1381 + _globals['_ORDERING']._serialized_start=1770 + _globals['_ORDERING']._serialized_end=1811 + _globals['_FILESSORTBY']._serialized_start=1813 + _globals['_FILESSORTBY']._serialized_end=1908 + _globals['_DOWNLOADFORMAT']._serialized_start=1910 + _globals['_DOWNLOADFORMAT']._serialized_end=2011 _globals['_UPLOADFILEINIT']._serialized_start=68 _globals['_UPLOADFILEINIT']._serialized_end=164 _globals['_UPLOADFILECHUNK']._serialized_start=166 _globals['_UPLOADFILECHUNK']._serialized_end=261 _globals['_FILE']._serialized_start=264 - _globals['_FILE']._serialized_end=478 - _globals['_LISTFILESREQUEST']._serialized_start=481 - _globals['_LISTFILESREQUEST']._serialized_end=695 - _globals['_LISTFILESRESPONSE']._serialized_start=697 - _globals['_LISTFILESRESPONSE']._serialized_end=820 - _globals['_RETRIEVEFILEREQUEST']._serialized_start=822 - _globals['_RETRIEVEFILEREQUEST']._serialized_end=868 - _globals['_DELETEFILEREQUEST']._serialized_start=870 - _globals['_DELETEFILEREQUEST']._serialized_end=914 - _globals['_DELETEFILERESPONSE']._serialized_start=916 - _globals['_DELETEFILERESPONSE']._serialized_end=978 - _globals['_RETRIEVEFILECONTENTREQUEST']._serialized_start=980 - _globals['_RETRIEVEFILECONTENTREQUEST']._serialized_end=1098 - _globals['_FILECONTENTCHUNK']._serialized_start=1100 - _globals['_FILECONTENTCHUNK']._serialized_end=1138 - _globals['_FILES']._serialized_start=1384 - _globals['_FILES']._serialized_end=1747 + _globals['_FILE']._serialized_end=639 + _globals['_LISTFILESREQUEST']._serialized_start=642 + _globals['_LISTFILESREQUEST']._serialized_end=896 + _globals['_LISTFILESRESPONSE']._serialized_start=898 + _globals['_LISTFILESRESPONSE']._serialized_end=1021 + _globals['_RETRIEVEFILEREQUEST']._serialized_start=1023 + _globals['_RETRIEVEFILEREQUEST']._serialized_end=1069 + _globals['_DELETEFILEREQUEST']._serialized_start=1071 + _globals['_DELETEFILEREQUEST']._serialized_end=1115 + _globals['_DELETEFILERESPONSE']._serialized_start=1117 + _globals['_DELETEFILERESPONSE']._serialized_end=1179 + _globals['_RETRIEVEFILECONTENTREQUEST']._serialized_start=1181 + _globals['_RETRIEVEFILECONTENTREQUEST']._serialized_end=1299 + _globals['_FILECONTENTCHUNK']._serialized_start=1301 + _globals['_FILECONTENTCHUNK']._serialized_end=1339 + _globals['_CREATEPUBLICURLREQUEST']._serialized_start=1341 + _globals['_CREATEPUBLICURLREQUEST']._serialized_end=1450 + _globals['_CREATEPUBLICURLRESPONSE']._serialized_start=1453 + _globals['_CREATEPUBLICURLRESPONSE']._serialized_end=1588 + _globals['_REVOKEPUBLICURLREQUEST']._serialized_start=1590 + _globals['_REVOKEPUBLICURLREQUEST']._serialized_end=1639 + _globals['_REVOKEPUBLICURLRESPONSE']._serialized_start=1641 + _globals['_REVOKEPUBLICURLRESPONSE']._serialized_end=1768 + _globals['_FILES']._serialized_start=2014 + _globals['_FILES']._serialized_end=2553 # @@protoc_insertion_point(module_scope) diff --git a/src/xai_sdk/proto/v5/files_pb2.pyi b/src/xai_sdk/proto/v5/files_pb2.pyi index 15ee2f6..060bb3a 100644 --- a/src/xai_sdk/proto/v5/files_pb2.pyi +++ b/src/xai_sdk/proto/v5/files_pb2.pyi @@ -49,30 +49,36 @@ class UploadFileChunk(_message.Message): def __init__(self, init: _Optional[_Union[UploadFileInit, _Mapping]] = ..., data: _Optional[bytes] = ...) -> None: ... class File(_message.Message): - __slots__ = ("size", "created_at", "expires_at", "filename", "id") + __slots__ = ("size", "created_at", "expires_at", "filename", "id", "public_url", "public_url_expires_at") SIZE_FIELD_NUMBER: _ClassVar[int] CREATED_AT_FIELD_NUMBER: _ClassVar[int] EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] FILENAME_FIELD_NUMBER: _ClassVar[int] ID_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] size: int created_at: _timestamp_pb2.Timestamp expires_at: _timestamp_pb2.Timestamp filename: str id: str - def __init__(self, size: _Optional[int] = ..., created_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., expires_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., filename: _Optional[str] = ..., id: _Optional[str] = ...) -> None: ... + public_url: str + public_url_expires_at: _timestamp_pb2.Timestamp + def __init__(self, size: _Optional[int] = ..., created_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., expires_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., filename: _Optional[str] = ..., id: _Optional[str] = ..., public_url: _Optional[str] = ..., public_url_expires_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... class ListFilesRequest(_message.Message): - __slots__ = ("limit", "order", "pagination_token", "sort_by") + __slots__ = ("limit", "order", "pagination_token", "sort_by", "filter") LIMIT_FIELD_NUMBER: _ClassVar[int] ORDER_FIELD_NUMBER: _ClassVar[int] PAGINATION_TOKEN_FIELD_NUMBER: _ClassVar[int] SORT_BY_FIELD_NUMBER: _ClassVar[int] + FILTER_FIELD_NUMBER: _ClassVar[int] limit: int order: Ordering pagination_token: str sort_by: FilesSortBy - def __init__(self, limit: _Optional[int] = ..., order: _Optional[_Union[Ordering, str]] = ..., pagination_token: _Optional[str] = ..., sort_by: _Optional[_Union[FilesSortBy, str]] = ...) -> None: ... + filter: str + def __init__(self, limit: _Optional[int] = ..., order: _Optional[_Union[Ordering, str]] = ..., pagination_token: _Optional[str] = ..., sort_by: _Optional[_Union[FilesSortBy, str]] = ..., filter: _Optional[str] = ...) -> None: ... class ListFilesResponse(_message.Message): __slots__ = ("data", "pagination_token") @@ -115,3 +121,35 @@ class FileContentChunk(_message.Message): DATA_FIELD_NUMBER: _ClassVar[int] data: bytes def __init__(self, data: _Optional[bytes] = ...) -> None: ... + +class CreatePublicUrlRequest(_message.Message): + __slots__ = ("file_id", "expires_after") + FILE_ID_FIELD_NUMBER: _ClassVar[int] + EXPIRES_AFTER_FIELD_NUMBER: _ClassVar[int] + file_id: str + expires_after: int + def __init__(self, file_id: _Optional[str] = ..., expires_after: _Optional[int] = ...) -> None: ... + +class CreatePublicUrlResponse(_message.Message): + __slots__ = ("public_url", "expires_at") + PUBLIC_URL_FIELD_NUMBER: _ClassVar[int] + EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] + public_url: str + expires_at: _timestamp_pb2.Timestamp + def __init__(self, public_url: _Optional[str] = ..., expires_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + +class RevokePublicUrlRequest(_message.Message): + __slots__ = ("file_id",) + FILE_ID_FIELD_NUMBER: _ClassVar[int] + file_id: str + def __init__(self, file_id: _Optional[str] = ...) -> None: ... + +class RevokePublicUrlResponse(_message.Message): + __slots__ = ("file_id", "revoked", "public_url") + FILE_ID_FIELD_NUMBER: _ClassVar[int] + REVOKED_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_FIELD_NUMBER: _ClassVar[int] + file_id: str + revoked: bool + public_url: str + def __init__(self, file_id: _Optional[str] = ..., revoked: bool = ..., public_url: _Optional[str] = ...) -> None: ... diff --git a/src/xai_sdk/proto/v5/files_pb2_grpc.py b/src/xai_sdk/proto/v5/files_pb2_grpc.py index ebc74f2..1eaf429 100644 --- a/src/xai_sdk/proto/v5/files_pb2_grpc.py +++ b/src/xai_sdk/proto/v5/files_pb2_grpc.py @@ -43,6 +43,16 @@ def __init__(self, channel): request_serializer=xai_dot_api_dot_v1_dot_files__pb2.RetrieveFileContentRequest.SerializeToString, response_deserializer=xai_dot_api_dot_v1_dot_files__pb2.FileContentChunk.FromString, _registered_method=True) + self.CreatePublicUrl = channel.unary_unary( + '/xai_api.Files/CreatePublicUrl', + request_serializer=xai_dot_api_dot_v1_dot_files__pb2.CreatePublicUrlRequest.SerializeToString, + response_deserializer=xai_dot_api_dot_v1_dot_files__pb2.CreatePublicUrlResponse.FromString, + _registered_method=True) + self.RevokePublicUrl = channel.unary_unary( + '/xai_api.Files/RevokePublicUrl', + request_serializer=xai_dot_api_dot_v1_dot_files__pb2.RevokePublicUrlRequest.SerializeToString, + response_deserializer=xai_dot_api_dot_v1_dot_files__pb2.RevokePublicUrlResponse.FromString, + _registered_method=True) class FilesServicer(object): @@ -99,6 +109,53 @@ def RetrieveFileContent(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def CreatePublicUrl(self, request, context): + """Create a public, unauthenticated URL for a file, accessible without an + API key. Only images, videos, and PDFs can be made public. + + A file can have at most one public URL at a time. Calling this on a + file that already has a public URL returns the existing URL. To update + the expiry, call again with a new `expires_after` value. + + **Public URL expiry behavior:** + + - If `expires_after` is set, the public URL expires that many seconds + from now, independently of the file's own TTL. + - If `expires_after` is omitted and the file has a TTL + (`UploadFileInit.expires_after` was set at upload), the public URL + automatically inherits the file's expiry — it will expire at the + same time as the file. + - If `expires_after` is omitted and the file has no TTL, the public + URL remains valid indefinitely (until explicitly revoked or the file + is deleted). + + **Automatic revocation:** + + A public URL is automatically revoked when the file is deleted (by the + user or by TTL expiry). Revocation may not take effect immediately, so + the URL can remain accessible for a short period after the file or the + public URL expires. Call `RevokePublicUrl` to revoke immediately. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def RevokePublicUrl(self, request, context): + """Revoke the public URL for a file. After revocation the public URL is + no longer accessible, while the original file remains accessible via + authenticated endpoints. + + Public URLs are also automatically revoked when the file is deleted, + when the file's TTL elapses, or when the public URL's own expiry + elapses. Automatic revocation may not take effect immediately; use this + RPC to revoke a public URL right away. + + Returns success if the file has no public URL (nothing to revoke). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_FilesServicer_to_server(servicer, server): rpc_method_handlers = { @@ -127,6 +184,16 @@ def add_FilesServicer_to_server(servicer, server): request_deserializer=xai_dot_api_dot_v1_dot_files__pb2.RetrieveFileContentRequest.FromString, response_serializer=xai_dot_api_dot_v1_dot_files__pb2.FileContentChunk.SerializeToString, ), + 'CreatePublicUrl': grpc.unary_unary_rpc_method_handler( + servicer.CreatePublicUrl, + request_deserializer=xai_dot_api_dot_v1_dot_files__pb2.CreatePublicUrlRequest.FromString, + response_serializer=xai_dot_api_dot_v1_dot_files__pb2.CreatePublicUrlResponse.SerializeToString, + ), + 'RevokePublicUrl': grpc.unary_unary_rpc_method_handler( + servicer.RevokePublicUrl, + request_deserializer=xai_dot_api_dot_v1_dot_files__pb2.RevokePublicUrlRequest.FromString, + response_serializer=xai_dot_api_dot_v1_dot_files__pb2.RevokePublicUrlResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'xai_api.Files', rpc_method_handlers) @@ -276,3 +343,57 @@ def RetrieveFileContent(request, timeout, metadata, _registered_method=True) + + @staticmethod + def CreatePublicUrl(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/xai_api.Files/CreatePublicUrl', + xai_dot_api_dot_v1_dot_files__pb2.CreatePublicUrlRequest.SerializeToString, + xai_dot_api_dot_v1_dot_files__pb2.CreatePublicUrlResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def RevokePublicUrl(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/xai_api.Files/RevokePublicUrl', + xai_dot_api_dot_v1_dot_files__pb2.RevokePublicUrlRequest.SerializeToString, + xai_dot_api_dot_v1_dot_files__pb2.RevokePublicUrlResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/src/xai_sdk/proto/v5/image_pb2.py b/src/xai_sdk/proto/v5/image_pb2.py index 7a7c599..1a7c646 100644 --- a/src/xai_sdk/proto/v5/image_pb2.py +++ b/src/xai_sdk/proto/v5/image_pb2.py @@ -22,10 +22,11 @@ _sym_db = _symbol_database.Default() +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 from . import usage_pb2 as xai_dot_api_dot_v1_dot_usage__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/image.proto\x12\x07xai_api\x1a\x16xai/api/v1/usage.proto\"\xa9\x03\n\x14GenerateImageRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05image\x18\x05 \x01(\x0b\x32\x18.xai_api.ImageUrlContentR\x05image\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12\x11\n\x01n\x18\x03 \x01(\x05H\x00R\x01n\x88\x01\x01\x12\x12\n\x04user\x18\x04 \x01(\tR\x04user\x12,\n\x06\x66ormat\x18\x0b \x01(\x0e\x32\x14.xai_api.ImageFormatR\x06\x66ormat\x12\x41\n\x0c\x61spect_ratio\x18\x0e \x01(\x0e\x32\x19.xai_api.ImageAspectRatioH\x01R\x0b\x61spectRatio\x88\x01\x01\x12=\n\nresolution\x18\x0f \x01(\x0e\x32\x18.xai_api.ImageResolutionH\x02R\nresolution\x88\x01\x01\x12\x30\n\x06images\x18\x11 \x03(\x0b\x32\x18.xai_api.ImageUrlContentR\x06imagesB\x04\n\x02_nB\x0f\n\r_aspect_ratioB\r\n\x0b_resolutionJ\x04\x08\r\x10\x0e\"\x84\x01\n\rImageResponse\x12/\n\x06images\x18\x01 \x03(\x0b\x32\x17.xai_api.GeneratedImageR\x06images\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12,\n\x05usage\x18\x03 \x01(\x0b\x32\x16.xai_api.SamplingUsageR\x05usage\"|\n\x0eGeneratedImage\x12\x18\n\x06\x62\x61se64\x18\x01 \x01(\tH\x00R\x06\x62\x61se64\x12\x12\n\x03url\x18\x03 \x01(\tH\x00R\x03url\x12-\n\x12respect_moderation\x18\x04 \x01(\x08R\x11respectModerationB\x07\n\x05imageJ\x04\x08\x02\x10\x03\"\\\n\x0fImageUrlContent\x12\x1b\n\timage_url\x18\x01 \x01(\tR\x08imageUrl\x12,\n\x06\x64\x65tail\x18\x02 \x01(\x0e\x32\x14.xai_api.ImageDetailR\x06\x64\x65tail*S\n\x0bImageDetail\x12\x12\n\x0e\x44\x45TAIL_INVALID\x10\x00\x12\x0f\n\x0b\x44\x45TAIL_AUTO\x10\x01\x12\x0e\n\nDETAIL_LOW\x10\x02\x12\x0f\n\x0b\x44\x45TAIL_HIGH\x10\x03*P\n\x0bImageFormat\x12\x16\n\x12IMG_FORMAT_INVALID\x10\x00\x12\x15\n\x11IMG_FORMAT_BASE64\x10\x01\x12\x12\n\x0eIMG_FORMAT_URL\x10\x02*j\n\x0cImageQuality\x12\x17\n\x13IMG_QUALITY_INVALID\x10\x00\x12\x13\n\x0fIMG_QUALITY_LOW\x10\x01\x12\x16\n\x12IMG_QUALITY_MEDIUM\x10\x02\x12\x14\n\x10IMG_QUALITY_HIGH\x10\x03*\xa7\x03\n\x10ImageAspectRatio\x12\x1c\n\x18IMG_ASPECT_RATIO_INVALID\x10\x00\x12\x18\n\x14IMG_ASPECT_RATIO_1_1\x10\x01\x12\x18\n\x14IMG_ASPECT_RATIO_3_4\x10\x02\x12\x18\n\x14IMG_ASPECT_RATIO_4_3\x10\x03\x12\x19\n\x15IMG_ASPECT_RATIO_9_16\x10\x04\x12\x19\n\x15IMG_ASPECT_RATIO_16_9\x10\x05\x12\x18\n\x14IMG_ASPECT_RATIO_2_3\x10\x06\x12\x18\n\x14IMG_ASPECT_RATIO_3_2\x10\x07\x12\x19\n\x15IMG_ASPECT_RATIO_AUTO\x10\x08\x12\x1b\n\x17IMG_ASPECT_RATIO_9_19_5\x10\t\x12\x1b\n\x17IMG_ASPECT_RATIO_19_5_9\x10\n\x12\x19\n\x15IMG_ASPECT_RATIO_9_20\x10\x0b\x12\x19\n\x15IMG_ASPECT_RATIO_20_9\x10\x0c\x12\x18\n\x14IMG_ASPECT_RATIO_1_2\x10\r\x12\x18\n\x14IMG_ASPECT_RATIO_2_1\x10\x0e*[\n\x0fImageResolution\x12\x1a\n\x16IMG_RESOLUTION_INVALID\x10\x00\x12\x15\n\x11IMG_RESOLUTION_1K\x10\x01\x12\x15\n\x11IMG_RESOLUTION_2K\x10\x02\x32Q\n\x05Image\x12H\n\rGenerateImage\x12\x1d.xai_api.GenerateImageRequest\x1a\x16.xai_api.ImageResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nImageProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/image.proto\x12\x07xai_api\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x16xai/api/v1/usage.proto\"\x84\x04\n\x14GenerateImageRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05image\x18\x05 \x01(\x0b\x32\x18.xai_api.ImageUrlContentR\x05image\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12\x11\n\x01n\x18\x03 \x01(\x05H\x00R\x01n\x88\x01\x01\x12\x12\n\x04user\x18\x04 \x01(\tR\x04user\x12,\n\x06\x66ormat\x18\x0b \x01(\x0e\x32\x14.xai_api.ImageFormatR\x06\x66ormat\x12\x41\n\x0c\x61spect_ratio\x18\x0e \x01(\x0e\x32\x19.xai_api.ImageAspectRatioH\x01R\x0b\x61spectRatio\x88\x01\x01\x12=\n\nresolution\x18\x0f \x01(\x0e\x32\x18.xai_api.ImageResolutionH\x02R\nresolution\x88\x01\x01\x12\x30\n\x06images\x18\x11 \x03(\x0b\x32\x18.xai_api.ImageUrlContentR\x06images\x12\x45\n\x0fstorage_options\x18\x13 \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x03R\x0estorageOptions\x88\x01\x01\x42\x04\n\x02_nB\x0f\n\r_aspect_ratioB\r\n\x0b_resolutionB\x12\n\x10_storage_optionsJ\x04\x08\r\x10\x0e\"\xb6\x01\n\x0eStorageOptions\x12\x1a\n\x08\x66ilename\x18\x01 \x01(\tR\x08\x66ilename\x12(\n\rexpires_after\x18\x02 \x01(\x03H\x00R\x0c\x65xpiresAfter\x88\x01\x01\x12=\n\npublic_url\x18\x03 \x01(\x0b\x32\x19.xai_api.PublicUrlOptionsH\x01R\tpublicUrl\x88\x01\x01\x42\x10\n\x0e_expires_afterB\r\n\x0b_public_url\"N\n\x10PublicUrlOptions\x12(\n\rexpires_after\x18\x01 \x01(\x03H\x00R\x0c\x65xpiresAfter\x88\x01\x01\x42\x10\n\x0e_expires_after\"\xfb\x02\n\nFileOutput\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\x12\x1a\n\x08\x66ilename\x18\x02 \x01(\tR\x08\x66ilename\x12\"\n\npublic_url\x18\x04 \x01(\tH\x00R\tpublicUrl\x88\x01\x01\x12R\n\x15public_url_expires_at\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x01R\x12publicUrlExpiresAt\x88\x01\x01\x12>\n\nexpires_at\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x02R\texpiresAt\x88\x01\x01\x12-\n\x10public_url_error\x18\x07 \x01(\tH\x03R\x0epublicUrlError\x88\x01\x01\x42\r\n\x0b_public_urlB\x18\n\x16_public_url_expires_atB\r\n\x0b_expires_atB\x13\n\x11_public_url_errorJ\x04\x08\x03\x10\x04\"\x84\x01\n\rImageResponse\x12/\n\x06images\x18\x01 \x03(\x0b\x32\x17.xai_api.GeneratedImageR\x06images\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12,\n\x05usage\x18\x03 \x01(\x0b\x32\x16.xai_api.SamplingUsageR\x05usage\"\x83\x02\n\x0eGeneratedImage\x12\x18\n\x06\x62\x61se64\x18\x01 \x01(\tH\x00R\x06\x62\x61se64\x12\x12\n\x03url\x18\x03 \x01(\tH\x00R\x03url\x12-\n\x12respect_moderation\x18\x04 \x01(\x08R\x11respectModeration\x12\x39\n\x0b\x66ile_output\x18\x08 \x01(\x0b\x32\x13.xai_api.FileOutputH\x01R\nfileOutput\x88\x01\x01\x12(\n\rstorage_error\x18\t \x01(\tH\x02R\x0cstorageError\x88\x01\x01\x42\x07\n\x05imageB\x0e\n\x0c_file_outputB\x10\n\x0e_storage_errorJ\x04\x08\x02\x10\x03\"\x83\x01\n\x0fImageUrlContent\x12\x1d\n\timage_url\x18\x01 \x01(\tH\x00R\x08imageUrl\x12\x19\n\x07\x66ile_id\x18\x03 \x01(\tH\x00R\x06\x66ileId\x12,\n\x06\x64\x65tail\x18\x02 \x01(\x0e\x32\x14.xai_api.ImageDetailR\x06\x64\x65tailB\x08\n\x06source*S\n\x0bImageDetail\x12\x12\n\x0e\x44\x45TAIL_INVALID\x10\x00\x12\x0f\n\x0b\x44\x45TAIL_AUTO\x10\x01\x12\x0e\n\nDETAIL_LOW\x10\x02\x12\x0f\n\x0b\x44\x45TAIL_HIGH\x10\x03*P\n\x0bImageFormat\x12\x16\n\x12IMG_FORMAT_INVALID\x10\x00\x12\x15\n\x11IMG_FORMAT_BASE64\x10\x01\x12\x12\n\x0eIMG_FORMAT_URL\x10\x02*j\n\x0cImageQuality\x12\x17\n\x13IMG_QUALITY_INVALID\x10\x00\x12\x13\n\x0fIMG_QUALITY_LOW\x10\x01\x12\x16\n\x12IMG_QUALITY_MEDIUM\x10\x02\x12\x14\n\x10IMG_QUALITY_HIGH\x10\x03*\xa7\x03\n\x10ImageAspectRatio\x12\x1c\n\x18IMG_ASPECT_RATIO_INVALID\x10\x00\x12\x18\n\x14IMG_ASPECT_RATIO_1_1\x10\x01\x12\x18\n\x14IMG_ASPECT_RATIO_3_4\x10\x02\x12\x18\n\x14IMG_ASPECT_RATIO_4_3\x10\x03\x12\x19\n\x15IMG_ASPECT_RATIO_9_16\x10\x04\x12\x19\n\x15IMG_ASPECT_RATIO_16_9\x10\x05\x12\x18\n\x14IMG_ASPECT_RATIO_2_3\x10\x06\x12\x18\n\x14IMG_ASPECT_RATIO_3_2\x10\x07\x12\x19\n\x15IMG_ASPECT_RATIO_AUTO\x10\x08\x12\x1b\n\x17IMG_ASPECT_RATIO_9_19_5\x10\t\x12\x1b\n\x17IMG_ASPECT_RATIO_19_5_9\x10\n\x12\x19\n\x15IMG_ASPECT_RATIO_9_20\x10\x0b\x12\x19\n\x15IMG_ASPECT_RATIO_20_9\x10\x0c\x12\x18\n\x14IMG_ASPECT_RATIO_1_2\x10\r\x12\x18\n\x14IMG_ASPECT_RATIO_2_1\x10\x0e*[\n\x0fImageResolution\x12\x1a\n\x16IMG_RESOLUTION_INVALID\x10\x00\x12\x15\n\x11IMG_RESOLUTION_1K\x10\x01\x12\x15\n\x11IMG_RESOLUTION_2K\x10\x02\x32Q\n\x05Image\x12H\n\rGenerateImage\x12\x1d.xai_api.GenerateImageRequest\x1a\x16.xai_api.ImageResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nImageProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -33,24 +34,30 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\013com.xai_apiB\nImageProtoP\001\242\002\003XXX\252\002\006XaiApi\312\002\006XaiApi\342\002\022XaiApi\\GPBMetadata\352\002\006XaiApi' - _globals['_IMAGEDETAIL']._serialized_start=842 - _globals['_IMAGEDETAIL']._serialized_end=925 - _globals['_IMAGEFORMAT']._serialized_start=927 - _globals['_IMAGEFORMAT']._serialized_end=1007 - _globals['_IMAGEQUALITY']._serialized_start=1009 - _globals['_IMAGEQUALITY']._serialized_end=1115 - _globals['_IMAGEASPECTRATIO']._serialized_start=1118 - _globals['_IMAGEASPECTRATIO']._serialized_end=1541 - _globals['_IMAGERESOLUTION']._serialized_start=1543 - _globals['_IMAGERESOLUTION']._serialized_end=1634 - _globals['_GENERATEIMAGEREQUEST']._serialized_start=60 - _globals['_GENERATEIMAGEREQUEST']._serialized_end=485 - _globals['_IMAGERESPONSE']._serialized_start=488 - _globals['_IMAGERESPONSE']._serialized_end=620 - _globals['_GENERATEDIMAGE']._serialized_start=622 - _globals['_GENERATEDIMAGE']._serialized_end=746 - _globals['_IMAGEURLCONTENT']._serialized_start=748 - _globals['_IMAGEURLCONTENT']._serialized_end=840 - _globals['_IMAGE']._serialized_start=1636 - _globals['_IMAGE']._serialized_end=1717 + _globals['_IMAGEDETAIL']._serialized_start=1789 + _globals['_IMAGEDETAIL']._serialized_end=1872 + _globals['_IMAGEFORMAT']._serialized_start=1874 + _globals['_IMAGEFORMAT']._serialized_end=1954 + _globals['_IMAGEQUALITY']._serialized_start=1956 + _globals['_IMAGEQUALITY']._serialized_end=2062 + _globals['_IMAGEASPECTRATIO']._serialized_start=2065 + _globals['_IMAGEASPECTRATIO']._serialized_end=2488 + _globals['_IMAGERESOLUTION']._serialized_start=2490 + _globals['_IMAGERESOLUTION']._serialized_end=2581 + _globals['_GENERATEIMAGEREQUEST']._serialized_start=93 + _globals['_GENERATEIMAGEREQUEST']._serialized_end=609 + _globals['_STORAGEOPTIONS']._serialized_start=612 + _globals['_STORAGEOPTIONS']._serialized_end=794 + _globals['_PUBLICURLOPTIONS']._serialized_start=796 + _globals['_PUBLICURLOPTIONS']._serialized_end=874 + _globals['_FILEOUTPUT']._serialized_start=877 + _globals['_FILEOUTPUT']._serialized_end=1256 + _globals['_IMAGERESPONSE']._serialized_start=1259 + _globals['_IMAGERESPONSE']._serialized_end=1391 + _globals['_GENERATEDIMAGE']._serialized_start=1394 + _globals['_GENERATEDIMAGE']._serialized_end=1653 + _globals['_IMAGEURLCONTENT']._serialized_start=1656 + _globals['_IMAGEURLCONTENT']._serialized_end=1787 + _globals['_IMAGE']._serialized_start=2583 + _globals['_IMAGE']._serialized_end=2664 # @@protoc_insertion_point(module_scope) diff --git a/src/xai_sdk/proto/v5/image_pb2.pyi b/src/xai_sdk/proto/v5/image_pb2.pyi index 2bd7ac4..371dffa 100644 --- a/src/xai_sdk/proto/v5/image_pb2.pyi +++ b/src/xai_sdk/proto/v5/image_pb2.pyi @@ -1,3 +1,4 @@ +from google.protobuf import timestamp_pb2 as _timestamp_pb2 from . import usage_pb2 as _usage_pb2 from google.protobuf.internal import containers as _containers from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper @@ -81,7 +82,7 @@ IMG_RESOLUTION_1K: ImageResolution IMG_RESOLUTION_2K: ImageResolution class GenerateImageRequest(_message.Message): - __slots__ = ("prompt", "image", "model", "n", "user", "format", "aspect_ratio", "resolution", "images") + __slots__ = ("prompt", "image", "model", "n", "user", "format", "aspect_ratio", "resolution", "images", "storage_options") PROMPT_FIELD_NUMBER: _ClassVar[int] IMAGE_FIELD_NUMBER: _ClassVar[int] MODEL_FIELD_NUMBER: _ClassVar[int] @@ -91,6 +92,7 @@ class GenerateImageRequest(_message.Message): ASPECT_RATIO_FIELD_NUMBER: _ClassVar[int] RESOLUTION_FIELD_NUMBER: _ClassVar[int] IMAGES_FIELD_NUMBER: _ClassVar[int] + STORAGE_OPTIONS_FIELD_NUMBER: _ClassVar[int] prompt: str image: ImageUrlContent model: str @@ -100,7 +102,40 @@ class GenerateImageRequest(_message.Message): aspect_ratio: ImageAspectRatio resolution: ImageResolution images: _containers.RepeatedCompositeFieldContainer[ImageUrlContent] - def __init__(self, prompt: _Optional[str] = ..., image: _Optional[_Union[ImageUrlContent, _Mapping]] = ..., model: _Optional[str] = ..., n: _Optional[int] = ..., user: _Optional[str] = ..., format: _Optional[_Union[ImageFormat, str]] = ..., aspect_ratio: _Optional[_Union[ImageAspectRatio, str]] = ..., resolution: _Optional[_Union[ImageResolution, str]] = ..., images: _Optional[_Iterable[_Union[ImageUrlContent, _Mapping]]] = ...) -> None: ... + storage_options: StorageOptions + def __init__(self, prompt: _Optional[str] = ..., image: _Optional[_Union[ImageUrlContent, _Mapping]] = ..., model: _Optional[str] = ..., n: _Optional[int] = ..., user: _Optional[str] = ..., format: _Optional[_Union[ImageFormat, str]] = ..., aspect_ratio: _Optional[_Union[ImageAspectRatio, str]] = ..., resolution: _Optional[_Union[ImageResolution, str]] = ..., images: _Optional[_Iterable[_Union[ImageUrlContent, _Mapping]]] = ..., storage_options: _Optional[_Union[StorageOptions, _Mapping]] = ...) -> None: ... + +class StorageOptions(_message.Message): + __slots__ = ("filename", "expires_after", "public_url") + FILENAME_FIELD_NUMBER: _ClassVar[int] + EXPIRES_AFTER_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_FIELD_NUMBER: _ClassVar[int] + filename: str + expires_after: int + public_url: PublicUrlOptions + def __init__(self, filename: _Optional[str] = ..., expires_after: _Optional[int] = ..., public_url: _Optional[_Union[PublicUrlOptions, _Mapping]] = ...) -> None: ... + +class PublicUrlOptions(_message.Message): + __slots__ = ("expires_after",) + EXPIRES_AFTER_FIELD_NUMBER: _ClassVar[int] + expires_after: int + def __init__(self, expires_after: _Optional[int] = ...) -> None: ... + +class FileOutput(_message.Message): + __slots__ = ("file_id", "filename", "public_url", "public_url_expires_at", "expires_at", "public_url_error") + FILE_ID_FIELD_NUMBER: _ClassVar[int] + FILENAME_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] + EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_ERROR_FIELD_NUMBER: _ClassVar[int] + file_id: str + filename: str + public_url: str + public_url_expires_at: _timestamp_pb2.Timestamp + expires_at: _timestamp_pb2.Timestamp + public_url_error: str + def __init__(self, file_id: _Optional[str] = ..., filename: _Optional[str] = ..., public_url: _Optional[str] = ..., public_url_expires_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., expires_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., public_url_error: _Optional[str] = ...) -> None: ... class ImageResponse(_message.Message): __slots__ = ("images", "model", "usage") @@ -113,19 +148,25 @@ class ImageResponse(_message.Message): def __init__(self, images: _Optional[_Iterable[_Union[GeneratedImage, _Mapping]]] = ..., model: _Optional[str] = ..., usage: _Optional[_Union[_usage_pb2.SamplingUsage, _Mapping]] = ...) -> None: ... class GeneratedImage(_message.Message): - __slots__ = ("base64", "url", "respect_moderation") + __slots__ = ("base64", "url", "respect_moderation", "file_output", "storage_error") BASE64_FIELD_NUMBER: _ClassVar[int] URL_FIELD_NUMBER: _ClassVar[int] RESPECT_MODERATION_FIELD_NUMBER: _ClassVar[int] + FILE_OUTPUT_FIELD_NUMBER: _ClassVar[int] + STORAGE_ERROR_FIELD_NUMBER: _ClassVar[int] base64: str url: str respect_moderation: bool - def __init__(self, base64: _Optional[str] = ..., url: _Optional[str] = ..., respect_moderation: bool = ...) -> None: ... + file_output: FileOutput + storage_error: str + def __init__(self, base64: _Optional[str] = ..., url: _Optional[str] = ..., respect_moderation: bool = ..., file_output: _Optional[_Union[FileOutput, _Mapping]] = ..., storage_error: _Optional[str] = ...) -> None: ... class ImageUrlContent(_message.Message): - __slots__ = ("image_url", "detail") + __slots__ = ("image_url", "file_id", "detail") IMAGE_URL_FIELD_NUMBER: _ClassVar[int] + FILE_ID_FIELD_NUMBER: _ClassVar[int] DETAIL_FIELD_NUMBER: _ClassVar[int] image_url: str + file_id: str detail: ImageDetail - def __init__(self, image_url: _Optional[str] = ..., detail: _Optional[_Union[ImageDetail, str]] = ...) -> None: ... + def __init__(self, image_url: _Optional[str] = ..., file_id: _Optional[str] = ..., detail: _Optional[_Union[ImageDetail, str]] = ...) -> None: ... diff --git a/src/xai_sdk/proto/v5/video_pb2.py b/src/xai_sdk/proto/v5/video_pb2.py index 8176716..569b98e 100644 --- a/src/xai_sdk/proto/v5/video_pb2.py +++ b/src/xai_sdk/proto/v5/video_pb2.py @@ -27,7 +27,7 @@ from . import usage_pb2 as xai_dot_api_dot_v1_dot_usage__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/video.proto\x12\x07xai_api\x1a\x19xai/api/v1/deferred.proto\x1a\x16xai/api/v1/image.proto\x1a\x16xai/api/v1/usage.proto\"#\n\x0fVideoUrlContent\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\"\xb9\x03\n\x14GenerateVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05image\x18\x02 \x01(\x0b\x32\x18.xai_api.ImageUrlContentR\x05image\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12.\n\x05video\x18\x06 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x41\n\x0c\x61spect_ratio\x18\x07 \x01(\x0e\x32\x19.xai_api.VideoAspectRatioH\x01R\x0b\x61spectRatio\x88\x01\x01\x12=\n\nresolution\x18\x08 \x01(\x0e\x32\x18.xai_api.VideoResolutionH\x02R\nresolution\x88\x01\x01\x12\x43\n\x10reference_images\x18\r \x03(\x0b\x32\x18.xai_api.ImageUrlContentR\x0freferenceImagesB\x0b\n\t_durationB\x0f\n\r_aspect_ratioB\r\n\x0b_resolution\"8\n\x17GetDeferredVideoRequest\x12\x1d\n\nrequest_id\x18\x01 \x01(\tR\trequestId\"\xd8\x01\n\rVideoResponse\x12-\n\x05video\x18\x01 \x01(\x0b\x32\x17.xai_api.GeneratedVideoR\x05video\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12,\n\x05usage\x18\x03 \x01(\x0b\x32\x16.xai_api.SamplingUsageR\x05usage\x12.\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x13.xai_api.VideoErrorH\x00R\x05\x65rror\x88\x01\x01\x12\x1a\n\x08progress\x18\x07 \x01(\x05R\x08progressB\x08\n\x06_error\"m\n\x0eGeneratedVideo\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x1a\n\x08\x64uration\x18\x04 \x01(\x05R\x08\x64uration\x12-\n\x12respect_moderation\x18\x05 \x01(\x08R\x11respectModeration\"\x91\x01\n\x18GetDeferredVideoResponse\x12/\n\x06status\x18\x01 \x01(\x0e\x32\x17.xai_api.DeferredStatusR\x06status\x12\x37\n\x08response\x18\x02 \x01(\x0b\x32\x16.xai_api.VideoResponseH\x00R\x08response\x88\x01\x01\x42\x0b\n\t_response\":\n\nVideoError\x12\x12\n\x04\x63ode\x18\x01 \x01(\tR\x04\x63ode\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\"\xa0\x01\n\x12\x45xtendVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05video\x18\x02 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x42\x0b\n\t_duration*\xfc\x01\n\x10VideoAspectRatio\x12\"\n\x1eVIDEO_ASPECT_RATIO_UNSPECIFIED\x10\x00\x12\x1a\n\x16VIDEO_ASPECT_RATIO_1_1\x10\x01\x12\x1b\n\x17VIDEO_ASPECT_RATIO_16_9\x10\x02\x12\x1b\n\x17VIDEO_ASPECT_RATIO_9_16\x10\x03\x12\x1a\n\x16VIDEO_ASPECT_RATIO_4_3\x10\x04\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_4\x10\x05\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_2\x10\x06\x12\x1a\n\x16VIDEO_ASPECT_RATIO_2_3\x10\x07*i\n\x0fVideoResolution\x12 \n\x1cVIDEO_RESOLUTION_UNSPECIFIED\x10\x00\x12\x19\n\x15VIDEO_RESOLUTION_480P\x10\x01\x12\x19\n\x15VIDEO_RESOLUTION_720P\x10\x02\x32\x82\x02\n\x05Video\x12P\n\rGenerateVideo\x12\x1d.xai_api.GenerateVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12L\n\x0b\x45xtendVideo\x12\x1b.xai_api.ExtendVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12Y\n\x10GetDeferredVideo\x12 .xai_api.GetDeferredVideoRequest\x1a!.xai_api.GetDeferredVideoResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nVideoProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/video.proto\x12\x07xai_api\x1a\x19xai/api/v1/deferred.proto\x1a\x16xai/api/v1/image.proto\x1a\x16xai/api/v1/usage.proto\"J\n\x0fVideoUrlContent\x12\x12\n\x03url\x18\x01 \x01(\tH\x00R\x03url\x12\x19\n\x07\x66ile_id\x18\x02 \x01(\tH\x00R\x06\x66ileIdB\x08\n\x06source\"\x94\x04\n\x14GenerateVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05image\x18\x02 \x01(\x0b\x32\x18.xai_api.ImageUrlContentR\x05image\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12.\n\x05video\x18\x06 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x41\n\x0c\x61spect_ratio\x18\x07 \x01(\x0e\x32\x19.xai_api.VideoAspectRatioH\x01R\x0b\x61spectRatio\x88\x01\x01\x12=\n\nresolution\x18\x08 \x01(\x0e\x32\x18.xai_api.VideoResolutionH\x02R\nresolution\x88\x01\x01\x12\x43\n\x10reference_images\x18\r \x03(\x0b\x32\x18.xai_api.ImageUrlContentR\x0freferenceImages\x12\x45\n\x0fstorage_options\x18\x0e \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x03R\x0estorageOptions\x88\x01\x01\x42\x0b\n\t_durationB\x0f\n\r_aspect_ratioB\r\n\x0b_resolutionB\x12\n\x10_storage_options\"8\n\x17GetDeferredVideoRequest\x12\x1d\n\nrequest_id\x18\x01 \x01(\tR\trequestId\"\xd8\x01\n\rVideoResponse\x12-\n\x05video\x18\x01 \x01(\x0b\x32\x17.xai_api.GeneratedVideoR\x05video\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12,\n\x05usage\x18\x03 \x01(\x0b\x32\x16.xai_api.SamplingUsageR\x05usage\x12.\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x13.xai_api.VideoErrorH\x00R\x05\x65rror\x88\x01\x01\x12\x1a\n\x08progress\x18\x07 \x01(\x05R\x08progressB\x08\n\x06_error\"\xf4\x01\n\x0eGeneratedVideo\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x1a\n\x08\x64uration\x18\x04 \x01(\x05R\x08\x64uration\x12-\n\x12respect_moderation\x18\x05 \x01(\x08R\x11respectModeration\x12\x39\n\x0b\x66ile_output\x18\x06 \x01(\x0b\x32\x13.xai_api.FileOutputH\x00R\nfileOutput\x88\x01\x01\x12(\n\rstorage_error\x18\x07 \x01(\tH\x01R\x0cstorageError\x88\x01\x01\x42\x0e\n\x0c_file_outputB\x10\n\x0e_storage_error\"\x91\x01\n\x18GetDeferredVideoResponse\x12/\n\x06status\x18\x01 \x01(\x0e\x32\x17.xai_api.DeferredStatusR\x06status\x12\x37\n\x08response\x18\x02 \x01(\x0b\x32\x16.xai_api.VideoResponseH\x00R\x08response\x88\x01\x01\x42\x0b\n\t_response\":\n\nVideoError\x12\x12\n\x04\x63ode\x18\x01 \x01(\tR\x04\x63ode\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\"\xfb\x01\n\x12\x45xtendVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05video\x18\x02 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12\x45\n\x0fstorage_options\x18\x06 \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x01R\x0estorageOptions\x88\x01\x01\x42\x0b\n\t_durationB\x12\n\x10_storage_options*\xfc\x01\n\x10VideoAspectRatio\x12\"\n\x1eVIDEO_ASPECT_RATIO_UNSPECIFIED\x10\x00\x12\x1a\n\x16VIDEO_ASPECT_RATIO_1_1\x10\x01\x12\x1b\n\x17VIDEO_ASPECT_RATIO_16_9\x10\x02\x12\x1b\n\x17VIDEO_ASPECT_RATIO_9_16\x10\x03\x12\x1a\n\x16VIDEO_ASPECT_RATIO_4_3\x10\x04\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_4\x10\x05\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_2\x10\x06\x12\x1a\n\x16VIDEO_ASPECT_RATIO_2_3\x10\x07*i\n\x0fVideoResolution\x12 \n\x1cVIDEO_RESOLUTION_UNSPECIFIED\x10\x00\x12\x19\n\x15VIDEO_RESOLUTION_480P\x10\x01\x12\x19\n\x15VIDEO_RESOLUTION_720P\x10\x02\x32\x82\x02\n\x05Video\x12P\n\rGenerateVideo\x12\x1d.xai_api.GenerateVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12L\n\x0b\x45xtendVideo\x12\x1b.xai_api.ExtendVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12Y\n\x10GetDeferredVideo\x12 .xai_api.GetDeferredVideoRequest\x1a!.xai_api.GetDeferredVideoResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nVideoProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -35,26 +35,26 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\013com.xai_apiB\nVideoProtoP\001\242\002\003XXX\252\002\006XaiApi\312\002\006XaiApi\342\002\022XaiApi\\GPBMetadata\352\002\006XaiApi' - _globals['_VIDEOASPECTRATIO']._serialized_start=1351 - _globals['_VIDEOASPECTRATIO']._serialized_end=1603 - _globals['_VIDEORESOLUTION']._serialized_start=1605 - _globals['_VIDEORESOLUTION']._serialized_end=1710 + _globals['_VIDEOASPECTRATIO']._serialized_start=1708 + _globals['_VIDEOASPECTRATIO']._serialized_end=1960 + _globals['_VIDEORESOLUTION']._serialized_start=1962 + _globals['_VIDEORESOLUTION']._serialized_end=2067 _globals['_VIDEOURLCONTENT']._serialized_start=110 - _globals['_VIDEOURLCONTENT']._serialized_end=145 - _globals['_GENERATEVIDEOREQUEST']._serialized_start=148 - _globals['_GENERATEVIDEOREQUEST']._serialized_end=589 - _globals['_GETDEFERREDVIDEOREQUEST']._serialized_start=591 - _globals['_GETDEFERREDVIDEOREQUEST']._serialized_end=647 - _globals['_VIDEORESPONSE']._serialized_start=650 - _globals['_VIDEORESPONSE']._serialized_end=866 - _globals['_GENERATEDVIDEO']._serialized_start=868 - _globals['_GENERATEDVIDEO']._serialized_end=977 - _globals['_GETDEFERREDVIDEORESPONSE']._serialized_start=980 - _globals['_GETDEFERREDVIDEORESPONSE']._serialized_end=1125 - _globals['_VIDEOERROR']._serialized_start=1127 - _globals['_VIDEOERROR']._serialized_end=1185 - _globals['_EXTENDVIDEOREQUEST']._serialized_start=1188 - _globals['_EXTENDVIDEOREQUEST']._serialized_end=1348 - _globals['_VIDEO']._serialized_start=1713 - _globals['_VIDEO']._serialized_end=1971 + _globals['_VIDEOURLCONTENT']._serialized_end=184 + _globals['_GENERATEVIDEOREQUEST']._serialized_start=187 + _globals['_GENERATEVIDEOREQUEST']._serialized_end=719 + _globals['_GETDEFERREDVIDEOREQUEST']._serialized_start=721 + _globals['_GETDEFERREDVIDEOREQUEST']._serialized_end=777 + _globals['_VIDEORESPONSE']._serialized_start=780 + _globals['_VIDEORESPONSE']._serialized_end=996 + _globals['_GENERATEDVIDEO']._serialized_start=999 + _globals['_GENERATEDVIDEO']._serialized_end=1243 + _globals['_GETDEFERREDVIDEORESPONSE']._serialized_start=1246 + _globals['_GETDEFERREDVIDEORESPONSE']._serialized_end=1391 + _globals['_VIDEOERROR']._serialized_start=1393 + _globals['_VIDEOERROR']._serialized_end=1451 + _globals['_EXTENDVIDEOREQUEST']._serialized_start=1454 + _globals['_EXTENDVIDEOREQUEST']._serialized_end=1705 + _globals['_VIDEO']._serialized_start=2070 + _globals['_VIDEO']._serialized_end=2328 # @@protoc_insertion_point(module_scope) diff --git a/src/xai_sdk/proto/v5/video_pb2.pyi b/src/xai_sdk/proto/v5/video_pb2.pyi index 363acb9..48ff438 100644 --- a/src/xai_sdk/proto/v5/video_pb2.pyi +++ b/src/xai_sdk/proto/v5/video_pb2.pyi @@ -38,13 +38,15 @@ VIDEO_RESOLUTION_480P: VideoResolution VIDEO_RESOLUTION_720P: VideoResolution class VideoUrlContent(_message.Message): - __slots__ = ("url",) + __slots__ = ("url", "file_id") URL_FIELD_NUMBER: _ClassVar[int] + FILE_ID_FIELD_NUMBER: _ClassVar[int] url: str - def __init__(self, url: _Optional[str] = ...) -> None: ... + file_id: str + def __init__(self, url: _Optional[str] = ..., file_id: _Optional[str] = ...) -> None: ... class GenerateVideoRequest(_message.Message): - __slots__ = ("prompt", "image", "model", "duration", "video", "aspect_ratio", "resolution", "reference_images") + __slots__ = ("prompt", "image", "model", "duration", "video", "aspect_ratio", "resolution", "reference_images", "storage_options") PROMPT_FIELD_NUMBER: _ClassVar[int] IMAGE_FIELD_NUMBER: _ClassVar[int] MODEL_FIELD_NUMBER: _ClassVar[int] @@ -53,6 +55,7 @@ class GenerateVideoRequest(_message.Message): ASPECT_RATIO_FIELD_NUMBER: _ClassVar[int] RESOLUTION_FIELD_NUMBER: _ClassVar[int] REFERENCE_IMAGES_FIELD_NUMBER: _ClassVar[int] + STORAGE_OPTIONS_FIELD_NUMBER: _ClassVar[int] prompt: str image: _image_pb2.ImageUrlContent model: str @@ -61,7 +64,8 @@ class GenerateVideoRequest(_message.Message): aspect_ratio: VideoAspectRatio resolution: VideoResolution reference_images: _containers.RepeatedCompositeFieldContainer[_image_pb2.ImageUrlContent] - def __init__(self, prompt: _Optional[str] = ..., image: _Optional[_Union[_image_pb2.ImageUrlContent, _Mapping]] = ..., model: _Optional[str] = ..., duration: _Optional[int] = ..., video: _Optional[_Union[VideoUrlContent, _Mapping]] = ..., aspect_ratio: _Optional[_Union[VideoAspectRatio, str]] = ..., resolution: _Optional[_Union[VideoResolution, str]] = ..., reference_images: _Optional[_Iterable[_Union[_image_pb2.ImageUrlContent, _Mapping]]] = ...) -> None: ... + storage_options: _image_pb2.StorageOptions + def __init__(self, prompt: _Optional[str] = ..., image: _Optional[_Union[_image_pb2.ImageUrlContent, _Mapping]] = ..., model: _Optional[str] = ..., duration: _Optional[int] = ..., video: _Optional[_Union[VideoUrlContent, _Mapping]] = ..., aspect_ratio: _Optional[_Union[VideoAspectRatio, str]] = ..., resolution: _Optional[_Union[VideoResolution, str]] = ..., reference_images: _Optional[_Iterable[_Union[_image_pb2.ImageUrlContent, _Mapping]]] = ..., storage_options: _Optional[_Union[_image_pb2.StorageOptions, _Mapping]] = ...) -> None: ... class GetDeferredVideoRequest(_message.Message): __slots__ = ("request_id",) @@ -84,14 +88,18 @@ class VideoResponse(_message.Message): def __init__(self, video: _Optional[_Union[GeneratedVideo, _Mapping]] = ..., model: _Optional[str] = ..., usage: _Optional[_Union[_usage_pb2.SamplingUsage, _Mapping]] = ..., error: _Optional[_Union[VideoError, _Mapping]] = ..., progress: _Optional[int] = ...) -> None: ... class GeneratedVideo(_message.Message): - __slots__ = ("url", "duration", "respect_moderation") + __slots__ = ("url", "duration", "respect_moderation", "file_output", "storage_error") URL_FIELD_NUMBER: _ClassVar[int] DURATION_FIELD_NUMBER: _ClassVar[int] RESPECT_MODERATION_FIELD_NUMBER: _ClassVar[int] + FILE_OUTPUT_FIELD_NUMBER: _ClassVar[int] + STORAGE_ERROR_FIELD_NUMBER: _ClassVar[int] url: str duration: int respect_moderation: bool - def __init__(self, url: _Optional[str] = ..., duration: _Optional[int] = ..., respect_moderation: bool = ...) -> None: ... + file_output: _image_pb2.FileOutput + storage_error: str + def __init__(self, url: _Optional[str] = ..., duration: _Optional[int] = ..., respect_moderation: bool = ..., file_output: _Optional[_Union[_image_pb2.FileOutput, _Mapping]] = ..., storage_error: _Optional[str] = ...) -> None: ... class GetDeferredVideoResponse(_message.Message): __slots__ = ("status", "response") @@ -110,13 +118,15 @@ class VideoError(_message.Message): def __init__(self, code: _Optional[str] = ..., message: _Optional[str] = ...) -> None: ... class ExtendVideoRequest(_message.Message): - __slots__ = ("prompt", "video", "model", "duration") + __slots__ = ("prompt", "video", "model", "duration", "storage_options") PROMPT_FIELD_NUMBER: _ClassVar[int] VIDEO_FIELD_NUMBER: _ClassVar[int] MODEL_FIELD_NUMBER: _ClassVar[int] DURATION_FIELD_NUMBER: _ClassVar[int] + STORAGE_OPTIONS_FIELD_NUMBER: _ClassVar[int] prompt: str video: VideoUrlContent model: str duration: int - def __init__(self, prompt: _Optional[str] = ..., video: _Optional[_Union[VideoUrlContent, _Mapping]] = ..., model: _Optional[str] = ..., duration: _Optional[int] = ...) -> None: ... + storage_options: _image_pb2.StorageOptions + def __init__(self, prompt: _Optional[str] = ..., video: _Optional[_Union[VideoUrlContent, _Mapping]] = ..., model: _Optional[str] = ..., duration: _Optional[int] = ..., storage_options: _Optional[_Union[_image_pb2.StorageOptions, _Mapping]] = ...) -> None: ... diff --git a/src/xai_sdk/proto/v6/files_pb2.py b/src/xai_sdk/proto/v6/files_pb2.py index 39b32fc..e576836 100644 --- a/src/xai_sdk/proto/v6/files_pb2.py +++ b/src/xai_sdk/proto/v6/files_pb2.py @@ -25,7 +25,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/files.proto\x12\x07xai_api\x1a\x1fgoogle/protobuf/timestamp.proto\"`\n\x0eUploadFileInit\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12(\n\rexpires_after\x18\x02 \x01(\x03H\x00R\x0c\x65xpiresAfter\x88\x01\x01\x42\x10\n\x0e_expires_after\"_\n\x0fUploadFileChunk\x12-\n\x04init\x18\x01 \x01(\x0b\x32\x17.xai_api.UploadFileInitH\x00R\x04init\x12\x14\n\x04\x64\x61ta\x18\x02 \x01(\x0cH\x00R\x04\x64\x61taB\x07\n\x05\x63hunk\"\xd6\x01\n\x04\x46ile\x12\x12\n\x04size\x18\x01 \x01(\x03R\x04size\x12\x39\n\ncreated_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12>\n\nexpires_at\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\texpiresAt\x88\x01\x01\x12\x1a\n\x08\x66ilename\x18\x04 \x01(\tR\x08\x66ilename\x12\x0e\n\x02id\x18\x05 \x01(\tR\x02idB\r\n\x0b_expires_atJ\x04\x08\x06\x10\x07\"\xd6\x01\n\x10ListFilesRequest\x12\x14\n\x05limit\x18\x01 \x01(\x05R\x05limit\x12\'\n\x05order\x18\x02 \x01(\x0e\x32\x11.xai_api.OrderingR\x05order\x12.\n\x10pagination_token\x18\x03 \x01(\tH\x00R\x0fpaginationToken\x88\x01\x01\x12\x32\n\x07sort_by\x18\x04 \x01(\x0e\x32\x14.xai_api.FilesSortByH\x01R\x06sortBy\x88\x01\x01\x42\x13\n\x11_pagination_tokenB\n\n\x08_sort_by\"{\n\x11ListFilesResponse\x12!\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\r.xai_api.FileR\x04\x64\x61ta\x12.\n\x10pagination_token\x18\x02 \x01(\tH\x00R\x0fpaginationToken\x88\x01\x01\x42\x13\n\x11_pagination_token\".\n\x13RetrieveFileRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\",\n\x11\x44\x65leteFileRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\">\n\x12\x44\x65leteFileResponse\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n\x07\x64\x65leted\x18\x02 \x01(\x08R\x07\x64\x65leted\"v\n\x1aRetrieveFileContentRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\x12\x34\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x17.xai_api.DownloadFormatH\x00R\x06\x66ormat\x88\x01\x01\x42\t\n\x07_format\"&\n\x10\x46ileContentChunk\x12\x12\n\x04\x64\x61ta\x18\x01 \x01(\x0cR\x04\x64\x61ta*)\n\x08Ordering\x12\r\n\tASCENDING\x10\x00\x12\x0e\n\nDESCENDING\x10\x01*_\n\x0b\x46ilesSortBy\x12\x1c\n\x18\x46ILES_SORT_BY_CREATED_AT\x10\x00\x12\x1a\n\x16\x46ILES_SORT_BY_FILENAME\x10\x01\x12\x16\n\x12\x46ILES_SORT_BY_SIZE\x10\x02*e\n\x0e\x44ownloadFormat\x12\x1b\n\x17\x44OWNLOAD_FORMAT_UNKNOWN\x10\x00\x12\x1c\n\x18\x44OWNLOAD_FORMAT_ORIGINAL\x10\x01\x12\x18\n\x14\x44OWNLOAD_FORMAT_TEXT\x10\x02\x32\xeb\x02\n\x05\x46iles\x12\x39\n\nUploadFile\x12\x18.xai_api.UploadFileChunk\x1a\r.xai_api.File\"\x00(\x01\x12\x44\n\tListFiles\x12\x19.xai_api.ListFilesRequest\x1a\x1a.xai_api.ListFilesResponse\"\x00\x12=\n\x0cRetrieveFile\x12\x1c.xai_api.RetrieveFileRequest\x1a\r.xai_api.File\"\x00\x12G\n\nDeleteFile\x12\x1a.xai_api.DeleteFileRequest\x1a\x1b.xai_api.DeleteFileResponse\"\x00\x12Y\n\x13RetrieveFileContent\x12#.xai_api.RetrieveFileContentRequest\x1a\x19.xai_api.FileContentChunk\"\x00\x30\x01\x42Q\n\x0b\x63om.xai_apiB\nFilesProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/files.proto\x12\x07xai_api\x1a\x1fgoogle/protobuf/timestamp.proto\"`\n\x0eUploadFileInit\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12(\n\rexpires_after\x18\x02 \x01(\x03H\x00R\x0c\x65xpiresAfter\x88\x01\x01\x42\x10\n\x0e_expires_after\"_\n\x0fUploadFileChunk\x12-\n\x04init\x18\x01 \x01(\x0b\x32\x17.xai_api.UploadFileInitH\x00R\x04init\x12\x14\n\x04\x64\x61ta\x18\x02 \x01(\x0cH\x00R\x04\x64\x61taB\x07\n\x05\x63hunk\"\xf7\x02\n\x04\x46ile\x12\x12\n\x04size\x18\x01 \x01(\x03R\x04size\x12\x39\n\ncreated_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12>\n\nexpires_at\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\texpiresAt\x88\x01\x01\x12\x1a\n\x08\x66ilename\x18\x04 \x01(\tR\x08\x66ilename\x12\x0e\n\x02id\x18\x05 \x01(\tR\x02id\x12\"\n\npublic_url\x18\x07 \x01(\tH\x01R\tpublicUrl\x88\x01\x01\x12R\n\x15public_url_expires_at\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x02R\x12publicUrlExpiresAt\x88\x01\x01\x42\r\n\x0b_expires_atB\r\n\x0b_public_urlB\x18\n\x16_public_url_expires_atJ\x04\x08\x06\x10\x07\"\xfe\x01\n\x10ListFilesRequest\x12\x14\n\x05limit\x18\x01 \x01(\x05R\x05limit\x12\'\n\x05order\x18\x02 \x01(\x0e\x32\x11.xai_api.OrderingR\x05order\x12.\n\x10pagination_token\x18\x03 \x01(\tH\x00R\x0fpaginationToken\x88\x01\x01\x12\x32\n\x07sort_by\x18\x04 \x01(\x0e\x32\x14.xai_api.FilesSortByH\x01R\x06sortBy\x88\x01\x01\x12\x1b\n\x06\x66ilter\x18\x05 \x01(\tH\x02R\x06\x66ilter\x88\x01\x01\x42\x13\n\x11_pagination_tokenB\n\n\x08_sort_byB\t\n\x07_filter\"{\n\x11ListFilesResponse\x12!\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\r.xai_api.FileR\x04\x64\x61ta\x12.\n\x10pagination_token\x18\x02 \x01(\tH\x00R\x0fpaginationToken\x88\x01\x01\x42\x13\n\x11_pagination_token\".\n\x13RetrieveFileRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\",\n\x11\x44\x65leteFileRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\">\n\x12\x44\x65leteFileResponse\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n\x07\x64\x65leted\x18\x02 \x01(\x08R\x07\x64\x65leted\"v\n\x1aRetrieveFileContentRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\x12\x34\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x17.xai_api.DownloadFormatH\x00R\x06\x66ormat\x88\x01\x01\x42\t\n\x07_format\"&\n\x10\x46ileContentChunk\x12\x12\n\x04\x64\x61ta\x18\x01 \x01(\x0cR\x04\x64\x61ta\"m\n\x16\x43reatePublicUrlRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\x12(\n\rexpires_after\x18\x02 \x01(\x03H\x00R\x0c\x65xpiresAfter\x88\x01\x01\x42\x10\n\x0e_expires_after\"\x87\x01\n\x17\x43reatePublicUrlResponse\x12\x1d\n\npublic_url\x18\x01 \x01(\tR\tpublicUrl\x12>\n\nexpires_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\texpiresAt\x88\x01\x01\x42\r\n\x0b_expires_at\"1\n\x16RevokePublicUrlRequest\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\"\x7f\n\x17RevokePublicUrlResponse\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\x12\x18\n\x07revoked\x18\x02 \x01(\x08R\x07revoked\x12\"\n\npublic_url\x18\x03 \x01(\tH\x00R\tpublicUrl\x88\x01\x01\x42\r\n\x0b_public_url*)\n\x08Ordering\x12\r\n\tASCENDING\x10\x00\x12\x0e\n\nDESCENDING\x10\x01*_\n\x0b\x46ilesSortBy\x12\x1c\n\x18\x46ILES_SORT_BY_CREATED_AT\x10\x00\x12\x1a\n\x16\x46ILES_SORT_BY_FILENAME\x10\x01\x12\x16\n\x12\x46ILES_SORT_BY_SIZE\x10\x02*e\n\x0e\x44ownloadFormat\x12\x1b\n\x17\x44OWNLOAD_FORMAT_UNKNOWN\x10\x00\x12\x1c\n\x18\x44OWNLOAD_FORMAT_ORIGINAL\x10\x01\x12\x18\n\x14\x44OWNLOAD_FORMAT_TEXT\x10\x02\x32\x9b\x04\n\x05\x46iles\x12\x39\n\nUploadFile\x12\x18.xai_api.UploadFileChunk\x1a\r.xai_api.File\"\x00(\x01\x12\x44\n\tListFiles\x12\x19.xai_api.ListFilesRequest\x1a\x1a.xai_api.ListFilesResponse\"\x00\x12=\n\x0cRetrieveFile\x12\x1c.xai_api.RetrieveFileRequest\x1a\r.xai_api.File\"\x00\x12G\n\nDeleteFile\x12\x1a.xai_api.DeleteFileRequest\x1a\x1b.xai_api.DeleteFileResponse\"\x00\x12Y\n\x13RetrieveFileContent\x12#.xai_api.RetrieveFileContentRequest\x1a\x19.xai_api.FileContentChunk\"\x00\x30\x01\x12V\n\x0f\x43reatePublicUrl\x12\x1f.xai_api.CreatePublicUrlRequest\x1a .xai_api.CreatePublicUrlResponse\"\x00\x12V\n\x0fRevokePublicUrl\x12\x1f.xai_api.RevokePublicUrlRequest\x1a .xai_api.RevokePublicUrlResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nFilesProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -33,32 +33,40 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\013com.xai_apiB\nFilesProtoP\001\242\002\003XXX\252\002\006XaiApi\312\002\006XaiApi\342\002\022XaiApi\\GPBMetadata\352\002\006XaiApi' - _globals['_ORDERING']._serialized_start=1140 - _globals['_ORDERING']._serialized_end=1181 - _globals['_FILESSORTBY']._serialized_start=1183 - _globals['_FILESSORTBY']._serialized_end=1278 - _globals['_DOWNLOADFORMAT']._serialized_start=1280 - _globals['_DOWNLOADFORMAT']._serialized_end=1381 + _globals['_ORDERING']._serialized_start=1770 + _globals['_ORDERING']._serialized_end=1811 + _globals['_FILESSORTBY']._serialized_start=1813 + _globals['_FILESSORTBY']._serialized_end=1908 + _globals['_DOWNLOADFORMAT']._serialized_start=1910 + _globals['_DOWNLOADFORMAT']._serialized_end=2011 _globals['_UPLOADFILEINIT']._serialized_start=68 _globals['_UPLOADFILEINIT']._serialized_end=164 _globals['_UPLOADFILECHUNK']._serialized_start=166 _globals['_UPLOADFILECHUNK']._serialized_end=261 _globals['_FILE']._serialized_start=264 - _globals['_FILE']._serialized_end=478 - _globals['_LISTFILESREQUEST']._serialized_start=481 - _globals['_LISTFILESREQUEST']._serialized_end=695 - _globals['_LISTFILESRESPONSE']._serialized_start=697 - _globals['_LISTFILESRESPONSE']._serialized_end=820 - _globals['_RETRIEVEFILEREQUEST']._serialized_start=822 - _globals['_RETRIEVEFILEREQUEST']._serialized_end=868 - _globals['_DELETEFILEREQUEST']._serialized_start=870 - _globals['_DELETEFILEREQUEST']._serialized_end=914 - _globals['_DELETEFILERESPONSE']._serialized_start=916 - _globals['_DELETEFILERESPONSE']._serialized_end=978 - _globals['_RETRIEVEFILECONTENTREQUEST']._serialized_start=980 - _globals['_RETRIEVEFILECONTENTREQUEST']._serialized_end=1098 - _globals['_FILECONTENTCHUNK']._serialized_start=1100 - _globals['_FILECONTENTCHUNK']._serialized_end=1138 - _globals['_FILES']._serialized_start=1384 - _globals['_FILES']._serialized_end=1747 + _globals['_FILE']._serialized_end=639 + _globals['_LISTFILESREQUEST']._serialized_start=642 + _globals['_LISTFILESREQUEST']._serialized_end=896 + _globals['_LISTFILESRESPONSE']._serialized_start=898 + _globals['_LISTFILESRESPONSE']._serialized_end=1021 + _globals['_RETRIEVEFILEREQUEST']._serialized_start=1023 + _globals['_RETRIEVEFILEREQUEST']._serialized_end=1069 + _globals['_DELETEFILEREQUEST']._serialized_start=1071 + _globals['_DELETEFILEREQUEST']._serialized_end=1115 + _globals['_DELETEFILERESPONSE']._serialized_start=1117 + _globals['_DELETEFILERESPONSE']._serialized_end=1179 + _globals['_RETRIEVEFILECONTENTREQUEST']._serialized_start=1181 + _globals['_RETRIEVEFILECONTENTREQUEST']._serialized_end=1299 + _globals['_FILECONTENTCHUNK']._serialized_start=1301 + _globals['_FILECONTENTCHUNK']._serialized_end=1339 + _globals['_CREATEPUBLICURLREQUEST']._serialized_start=1341 + _globals['_CREATEPUBLICURLREQUEST']._serialized_end=1450 + _globals['_CREATEPUBLICURLRESPONSE']._serialized_start=1453 + _globals['_CREATEPUBLICURLRESPONSE']._serialized_end=1588 + _globals['_REVOKEPUBLICURLREQUEST']._serialized_start=1590 + _globals['_REVOKEPUBLICURLREQUEST']._serialized_end=1639 + _globals['_REVOKEPUBLICURLRESPONSE']._serialized_start=1641 + _globals['_REVOKEPUBLICURLRESPONSE']._serialized_end=1768 + _globals['_FILES']._serialized_start=2014 + _globals['_FILES']._serialized_end=2553 # @@protoc_insertion_point(module_scope) diff --git a/src/xai_sdk/proto/v6/files_pb2.pyi b/src/xai_sdk/proto/v6/files_pb2.pyi index 254c65a..2e85d9b 100644 --- a/src/xai_sdk/proto/v6/files_pb2.pyi +++ b/src/xai_sdk/proto/v6/files_pb2.pyi @@ -50,30 +50,36 @@ class UploadFileChunk(_message.Message): def __init__(self, init: _Optional[_Union[UploadFileInit, _Mapping]] = ..., data: _Optional[bytes] = ...) -> None: ... class File(_message.Message): - __slots__ = ("size", "created_at", "expires_at", "filename", "id") + __slots__ = ("size", "created_at", "expires_at", "filename", "id", "public_url", "public_url_expires_at") SIZE_FIELD_NUMBER: _ClassVar[int] CREATED_AT_FIELD_NUMBER: _ClassVar[int] EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] FILENAME_FIELD_NUMBER: _ClassVar[int] ID_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] size: int created_at: _timestamp_pb2.Timestamp expires_at: _timestamp_pb2.Timestamp filename: str id: str - def __init__(self, size: _Optional[int] = ..., created_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., expires_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., filename: _Optional[str] = ..., id: _Optional[str] = ...) -> None: ... + public_url: str + public_url_expires_at: _timestamp_pb2.Timestamp + def __init__(self, size: _Optional[int] = ..., created_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., expires_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., filename: _Optional[str] = ..., id: _Optional[str] = ..., public_url: _Optional[str] = ..., public_url_expires_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... class ListFilesRequest(_message.Message): - __slots__ = ("limit", "order", "pagination_token", "sort_by") + __slots__ = ("limit", "order", "pagination_token", "sort_by", "filter") LIMIT_FIELD_NUMBER: _ClassVar[int] ORDER_FIELD_NUMBER: _ClassVar[int] PAGINATION_TOKEN_FIELD_NUMBER: _ClassVar[int] SORT_BY_FIELD_NUMBER: _ClassVar[int] + FILTER_FIELD_NUMBER: _ClassVar[int] limit: int order: Ordering pagination_token: str sort_by: FilesSortBy - def __init__(self, limit: _Optional[int] = ..., order: _Optional[_Union[Ordering, str]] = ..., pagination_token: _Optional[str] = ..., sort_by: _Optional[_Union[FilesSortBy, str]] = ...) -> None: ... + filter: str + def __init__(self, limit: _Optional[int] = ..., order: _Optional[_Union[Ordering, str]] = ..., pagination_token: _Optional[str] = ..., sort_by: _Optional[_Union[FilesSortBy, str]] = ..., filter: _Optional[str] = ...) -> None: ... class ListFilesResponse(_message.Message): __slots__ = ("data", "pagination_token") @@ -116,3 +122,35 @@ class FileContentChunk(_message.Message): DATA_FIELD_NUMBER: _ClassVar[int] data: bytes def __init__(self, data: _Optional[bytes] = ...) -> None: ... + +class CreatePublicUrlRequest(_message.Message): + __slots__ = ("file_id", "expires_after") + FILE_ID_FIELD_NUMBER: _ClassVar[int] + EXPIRES_AFTER_FIELD_NUMBER: _ClassVar[int] + file_id: str + expires_after: int + def __init__(self, file_id: _Optional[str] = ..., expires_after: _Optional[int] = ...) -> None: ... + +class CreatePublicUrlResponse(_message.Message): + __slots__ = ("public_url", "expires_at") + PUBLIC_URL_FIELD_NUMBER: _ClassVar[int] + EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] + public_url: str + expires_at: _timestamp_pb2.Timestamp + def __init__(self, public_url: _Optional[str] = ..., expires_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + +class RevokePublicUrlRequest(_message.Message): + __slots__ = ("file_id",) + FILE_ID_FIELD_NUMBER: _ClassVar[int] + file_id: str + def __init__(self, file_id: _Optional[str] = ...) -> None: ... + +class RevokePublicUrlResponse(_message.Message): + __slots__ = ("file_id", "revoked", "public_url") + FILE_ID_FIELD_NUMBER: _ClassVar[int] + REVOKED_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_FIELD_NUMBER: _ClassVar[int] + file_id: str + revoked: bool + public_url: str + def __init__(self, file_id: _Optional[str] = ..., revoked: bool = ..., public_url: _Optional[str] = ...) -> None: ... diff --git a/src/xai_sdk/proto/v6/files_pb2_grpc.py b/src/xai_sdk/proto/v6/files_pb2_grpc.py index ebc74f2..1eaf429 100644 --- a/src/xai_sdk/proto/v6/files_pb2_grpc.py +++ b/src/xai_sdk/proto/v6/files_pb2_grpc.py @@ -43,6 +43,16 @@ def __init__(self, channel): request_serializer=xai_dot_api_dot_v1_dot_files__pb2.RetrieveFileContentRequest.SerializeToString, response_deserializer=xai_dot_api_dot_v1_dot_files__pb2.FileContentChunk.FromString, _registered_method=True) + self.CreatePublicUrl = channel.unary_unary( + '/xai_api.Files/CreatePublicUrl', + request_serializer=xai_dot_api_dot_v1_dot_files__pb2.CreatePublicUrlRequest.SerializeToString, + response_deserializer=xai_dot_api_dot_v1_dot_files__pb2.CreatePublicUrlResponse.FromString, + _registered_method=True) + self.RevokePublicUrl = channel.unary_unary( + '/xai_api.Files/RevokePublicUrl', + request_serializer=xai_dot_api_dot_v1_dot_files__pb2.RevokePublicUrlRequest.SerializeToString, + response_deserializer=xai_dot_api_dot_v1_dot_files__pb2.RevokePublicUrlResponse.FromString, + _registered_method=True) class FilesServicer(object): @@ -99,6 +109,53 @@ def RetrieveFileContent(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def CreatePublicUrl(self, request, context): + """Create a public, unauthenticated URL for a file, accessible without an + API key. Only images, videos, and PDFs can be made public. + + A file can have at most one public URL at a time. Calling this on a + file that already has a public URL returns the existing URL. To update + the expiry, call again with a new `expires_after` value. + + **Public URL expiry behavior:** + + - If `expires_after` is set, the public URL expires that many seconds + from now, independently of the file's own TTL. + - If `expires_after` is omitted and the file has a TTL + (`UploadFileInit.expires_after` was set at upload), the public URL + automatically inherits the file's expiry — it will expire at the + same time as the file. + - If `expires_after` is omitted and the file has no TTL, the public + URL remains valid indefinitely (until explicitly revoked or the file + is deleted). + + **Automatic revocation:** + + A public URL is automatically revoked when the file is deleted (by the + user or by TTL expiry). Revocation may not take effect immediately, so + the URL can remain accessible for a short period after the file or the + public URL expires. Call `RevokePublicUrl` to revoke immediately. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def RevokePublicUrl(self, request, context): + """Revoke the public URL for a file. After revocation the public URL is + no longer accessible, while the original file remains accessible via + authenticated endpoints. + + Public URLs are also automatically revoked when the file is deleted, + when the file's TTL elapses, or when the public URL's own expiry + elapses. Automatic revocation may not take effect immediately; use this + RPC to revoke a public URL right away. + + Returns success if the file has no public URL (nothing to revoke). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_FilesServicer_to_server(servicer, server): rpc_method_handlers = { @@ -127,6 +184,16 @@ def add_FilesServicer_to_server(servicer, server): request_deserializer=xai_dot_api_dot_v1_dot_files__pb2.RetrieveFileContentRequest.FromString, response_serializer=xai_dot_api_dot_v1_dot_files__pb2.FileContentChunk.SerializeToString, ), + 'CreatePublicUrl': grpc.unary_unary_rpc_method_handler( + servicer.CreatePublicUrl, + request_deserializer=xai_dot_api_dot_v1_dot_files__pb2.CreatePublicUrlRequest.FromString, + response_serializer=xai_dot_api_dot_v1_dot_files__pb2.CreatePublicUrlResponse.SerializeToString, + ), + 'RevokePublicUrl': grpc.unary_unary_rpc_method_handler( + servicer.RevokePublicUrl, + request_deserializer=xai_dot_api_dot_v1_dot_files__pb2.RevokePublicUrlRequest.FromString, + response_serializer=xai_dot_api_dot_v1_dot_files__pb2.RevokePublicUrlResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'xai_api.Files', rpc_method_handlers) @@ -276,3 +343,57 @@ def RetrieveFileContent(request, timeout, metadata, _registered_method=True) + + @staticmethod + def CreatePublicUrl(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/xai_api.Files/CreatePublicUrl', + xai_dot_api_dot_v1_dot_files__pb2.CreatePublicUrlRequest.SerializeToString, + xai_dot_api_dot_v1_dot_files__pb2.CreatePublicUrlResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def RevokePublicUrl(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/xai_api.Files/RevokePublicUrl', + xai_dot_api_dot_v1_dot_files__pb2.RevokePublicUrlRequest.SerializeToString, + xai_dot_api_dot_v1_dot_files__pb2.RevokePublicUrlResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/src/xai_sdk/proto/v6/image_pb2.py b/src/xai_sdk/proto/v6/image_pb2.py index 3853afc..f52ee6c 100644 --- a/src/xai_sdk/proto/v6/image_pb2.py +++ b/src/xai_sdk/proto/v6/image_pb2.py @@ -22,10 +22,11 @@ _sym_db = _symbol_database.Default() +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 from . import usage_pb2 as xai_dot_api_dot_v1_dot_usage__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/image.proto\x12\x07xai_api\x1a\x16xai/api/v1/usage.proto\"\xa9\x03\n\x14GenerateImageRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05image\x18\x05 \x01(\x0b\x32\x18.xai_api.ImageUrlContentR\x05image\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12\x11\n\x01n\x18\x03 \x01(\x05H\x00R\x01n\x88\x01\x01\x12\x12\n\x04user\x18\x04 \x01(\tR\x04user\x12,\n\x06\x66ormat\x18\x0b \x01(\x0e\x32\x14.xai_api.ImageFormatR\x06\x66ormat\x12\x41\n\x0c\x61spect_ratio\x18\x0e \x01(\x0e\x32\x19.xai_api.ImageAspectRatioH\x01R\x0b\x61spectRatio\x88\x01\x01\x12=\n\nresolution\x18\x0f \x01(\x0e\x32\x18.xai_api.ImageResolutionH\x02R\nresolution\x88\x01\x01\x12\x30\n\x06images\x18\x11 \x03(\x0b\x32\x18.xai_api.ImageUrlContentR\x06imagesB\x04\n\x02_nB\x0f\n\r_aspect_ratioB\r\n\x0b_resolutionJ\x04\x08\r\x10\x0e\"\x84\x01\n\rImageResponse\x12/\n\x06images\x18\x01 \x03(\x0b\x32\x17.xai_api.GeneratedImageR\x06images\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12,\n\x05usage\x18\x03 \x01(\x0b\x32\x16.xai_api.SamplingUsageR\x05usage\"|\n\x0eGeneratedImage\x12\x18\n\x06\x62\x61se64\x18\x01 \x01(\tH\x00R\x06\x62\x61se64\x12\x12\n\x03url\x18\x03 \x01(\tH\x00R\x03url\x12-\n\x12respect_moderation\x18\x04 \x01(\x08R\x11respectModerationB\x07\n\x05imageJ\x04\x08\x02\x10\x03\"\\\n\x0fImageUrlContent\x12\x1b\n\timage_url\x18\x01 \x01(\tR\x08imageUrl\x12,\n\x06\x64\x65tail\x18\x02 \x01(\x0e\x32\x14.xai_api.ImageDetailR\x06\x64\x65tail*S\n\x0bImageDetail\x12\x12\n\x0e\x44\x45TAIL_INVALID\x10\x00\x12\x0f\n\x0b\x44\x45TAIL_AUTO\x10\x01\x12\x0e\n\nDETAIL_LOW\x10\x02\x12\x0f\n\x0b\x44\x45TAIL_HIGH\x10\x03*P\n\x0bImageFormat\x12\x16\n\x12IMG_FORMAT_INVALID\x10\x00\x12\x15\n\x11IMG_FORMAT_BASE64\x10\x01\x12\x12\n\x0eIMG_FORMAT_URL\x10\x02*j\n\x0cImageQuality\x12\x17\n\x13IMG_QUALITY_INVALID\x10\x00\x12\x13\n\x0fIMG_QUALITY_LOW\x10\x01\x12\x16\n\x12IMG_QUALITY_MEDIUM\x10\x02\x12\x14\n\x10IMG_QUALITY_HIGH\x10\x03*\xa7\x03\n\x10ImageAspectRatio\x12\x1c\n\x18IMG_ASPECT_RATIO_INVALID\x10\x00\x12\x18\n\x14IMG_ASPECT_RATIO_1_1\x10\x01\x12\x18\n\x14IMG_ASPECT_RATIO_3_4\x10\x02\x12\x18\n\x14IMG_ASPECT_RATIO_4_3\x10\x03\x12\x19\n\x15IMG_ASPECT_RATIO_9_16\x10\x04\x12\x19\n\x15IMG_ASPECT_RATIO_16_9\x10\x05\x12\x18\n\x14IMG_ASPECT_RATIO_2_3\x10\x06\x12\x18\n\x14IMG_ASPECT_RATIO_3_2\x10\x07\x12\x19\n\x15IMG_ASPECT_RATIO_AUTO\x10\x08\x12\x1b\n\x17IMG_ASPECT_RATIO_9_19_5\x10\t\x12\x1b\n\x17IMG_ASPECT_RATIO_19_5_9\x10\n\x12\x19\n\x15IMG_ASPECT_RATIO_9_20\x10\x0b\x12\x19\n\x15IMG_ASPECT_RATIO_20_9\x10\x0c\x12\x18\n\x14IMG_ASPECT_RATIO_1_2\x10\r\x12\x18\n\x14IMG_ASPECT_RATIO_2_1\x10\x0e*[\n\x0fImageResolution\x12\x1a\n\x16IMG_RESOLUTION_INVALID\x10\x00\x12\x15\n\x11IMG_RESOLUTION_1K\x10\x01\x12\x15\n\x11IMG_RESOLUTION_2K\x10\x02\x32Q\n\x05Image\x12H\n\rGenerateImage\x12\x1d.xai_api.GenerateImageRequest\x1a\x16.xai_api.ImageResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nImageProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/image.proto\x12\x07xai_api\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x16xai/api/v1/usage.proto\"\x84\x04\n\x14GenerateImageRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05image\x18\x05 \x01(\x0b\x32\x18.xai_api.ImageUrlContentR\x05image\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12\x11\n\x01n\x18\x03 \x01(\x05H\x00R\x01n\x88\x01\x01\x12\x12\n\x04user\x18\x04 \x01(\tR\x04user\x12,\n\x06\x66ormat\x18\x0b \x01(\x0e\x32\x14.xai_api.ImageFormatR\x06\x66ormat\x12\x41\n\x0c\x61spect_ratio\x18\x0e \x01(\x0e\x32\x19.xai_api.ImageAspectRatioH\x01R\x0b\x61spectRatio\x88\x01\x01\x12=\n\nresolution\x18\x0f \x01(\x0e\x32\x18.xai_api.ImageResolutionH\x02R\nresolution\x88\x01\x01\x12\x30\n\x06images\x18\x11 \x03(\x0b\x32\x18.xai_api.ImageUrlContentR\x06images\x12\x45\n\x0fstorage_options\x18\x13 \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x03R\x0estorageOptions\x88\x01\x01\x42\x04\n\x02_nB\x0f\n\r_aspect_ratioB\r\n\x0b_resolutionB\x12\n\x10_storage_optionsJ\x04\x08\r\x10\x0e\"\xb6\x01\n\x0eStorageOptions\x12\x1a\n\x08\x66ilename\x18\x01 \x01(\tR\x08\x66ilename\x12(\n\rexpires_after\x18\x02 \x01(\x03H\x00R\x0c\x65xpiresAfter\x88\x01\x01\x12=\n\npublic_url\x18\x03 \x01(\x0b\x32\x19.xai_api.PublicUrlOptionsH\x01R\tpublicUrl\x88\x01\x01\x42\x10\n\x0e_expires_afterB\r\n\x0b_public_url\"N\n\x10PublicUrlOptions\x12(\n\rexpires_after\x18\x01 \x01(\x03H\x00R\x0c\x65xpiresAfter\x88\x01\x01\x42\x10\n\x0e_expires_after\"\xfb\x02\n\nFileOutput\x12\x17\n\x07\x66ile_id\x18\x01 \x01(\tR\x06\x66ileId\x12\x1a\n\x08\x66ilename\x18\x02 \x01(\tR\x08\x66ilename\x12\"\n\npublic_url\x18\x04 \x01(\tH\x00R\tpublicUrl\x88\x01\x01\x12R\n\x15public_url_expires_at\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x01R\x12publicUrlExpiresAt\x88\x01\x01\x12>\n\nexpires_at\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x02R\texpiresAt\x88\x01\x01\x12-\n\x10public_url_error\x18\x07 \x01(\tH\x03R\x0epublicUrlError\x88\x01\x01\x42\r\n\x0b_public_urlB\x18\n\x16_public_url_expires_atB\r\n\x0b_expires_atB\x13\n\x11_public_url_errorJ\x04\x08\x03\x10\x04\"\x84\x01\n\rImageResponse\x12/\n\x06images\x18\x01 \x03(\x0b\x32\x17.xai_api.GeneratedImageR\x06images\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12,\n\x05usage\x18\x03 \x01(\x0b\x32\x16.xai_api.SamplingUsageR\x05usage\"\x83\x02\n\x0eGeneratedImage\x12\x18\n\x06\x62\x61se64\x18\x01 \x01(\tH\x00R\x06\x62\x61se64\x12\x12\n\x03url\x18\x03 \x01(\tH\x00R\x03url\x12-\n\x12respect_moderation\x18\x04 \x01(\x08R\x11respectModeration\x12\x39\n\x0b\x66ile_output\x18\x08 \x01(\x0b\x32\x13.xai_api.FileOutputH\x01R\nfileOutput\x88\x01\x01\x12(\n\rstorage_error\x18\t \x01(\tH\x02R\x0cstorageError\x88\x01\x01\x42\x07\n\x05imageB\x0e\n\x0c_file_outputB\x10\n\x0e_storage_errorJ\x04\x08\x02\x10\x03\"\x83\x01\n\x0fImageUrlContent\x12\x1d\n\timage_url\x18\x01 \x01(\tH\x00R\x08imageUrl\x12\x19\n\x07\x66ile_id\x18\x03 \x01(\tH\x00R\x06\x66ileId\x12,\n\x06\x64\x65tail\x18\x02 \x01(\x0e\x32\x14.xai_api.ImageDetailR\x06\x64\x65tailB\x08\n\x06source*S\n\x0bImageDetail\x12\x12\n\x0e\x44\x45TAIL_INVALID\x10\x00\x12\x0f\n\x0b\x44\x45TAIL_AUTO\x10\x01\x12\x0e\n\nDETAIL_LOW\x10\x02\x12\x0f\n\x0b\x44\x45TAIL_HIGH\x10\x03*P\n\x0bImageFormat\x12\x16\n\x12IMG_FORMAT_INVALID\x10\x00\x12\x15\n\x11IMG_FORMAT_BASE64\x10\x01\x12\x12\n\x0eIMG_FORMAT_URL\x10\x02*j\n\x0cImageQuality\x12\x17\n\x13IMG_QUALITY_INVALID\x10\x00\x12\x13\n\x0fIMG_QUALITY_LOW\x10\x01\x12\x16\n\x12IMG_QUALITY_MEDIUM\x10\x02\x12\x14\n\x10IMG_QUALITY_HIGH\x10\x03*\xa7\x03\n\x10ImageAspectRatio\x12\x1c\n\x18IMG_ASPECT_RATIO_INVALID\x10\x00\x12\x18\n\x14IMG_ASPECT_RATIO_1_1\x10\x01\x12\x18\n\x14IMG_ASPECT_RATIO_3_4\x10\x02\x12\x18\n\x14IMG_ASPECT_RATIO_4_3\x10\x03\x12\x19\n\x15IMG_ASPECT_RATIO_9_16\x10\x04\x12\x19\n\x15IMG_ASPECT_RATIO_16_9\x10\x05\x12\x18\n\x14IMG_ASPECT_RATIO_2_3\x10\x06\x12\x18\n\x14IMG_ASPECT_RATIO_3_2\x10\x07\x12\x19\n\x15IMG_ASPECT_RATIO_AUTO\x10\x08\x12\x1b\n\x17IMG_ASPECT_RATIO_9_19_5\x10\t\x12\x1b\n\x17IMG_ASPECT_RATIO_19_5_9\x10\n\x12\x19\n\x15IMG_ASPECT_RATIO_9_20\x10\x0b\x12\x19\n\x15IMG_ASPECT_RATIO_20_9\x10\x0c\x12\x18\n\x14IMG_ASPECT_RATIO_1_2\x10\r\x12\x18\n\x14IMG_ASPECT_RATIO_2_1\x10\x0e*[\n\x0fImageResolution\x12\x1a\n\x16IMG_RESOLUTION_INVALID\x10\x00\x12\x15\n\x11IMG_RESOLUTION_1K\x10\x01\x12\x15\n\x11IMG_RESOLUTION_2K\x10\x02\x32Q\n\x05Image\x12H\n\rGenerateImage\x12\x1d.xai_api.GenerateImageRequest\x1a\x16.xai_api.ImageResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nImageProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -33,24 +34,30 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\013com.xai_apiB\nImageProtoP\001\242\002\003XXX\252\002\006XaiApi\312\002\006XaiApi\342\002\022XaiApi\\GPBMetadata\352\002\006XaiApi' - _globals['_IMAGEDETAIL']._serialized_start=842 - _globals['_IMAGEDETAIL']._serialized_end=925 - _globals['_IMAGEFORMAT']._serialized_start=927 - _globals['_IMAGEFORMAT']._serialized_end=1007 - _globals['_IMAGEQUALITY']._serialized_start=1009 - _globals['_IMAGEQUALITY']._serialized_end=1115 - _globals['_IMAGEASPECTRATIO']._serialized_start=1118 - _globals['_IMAGEASPECTRATIO']._serialized_end=1541 - _globals['_IMAGERESOLUTION']._serialized_start=1543 - _globals['_IMAGERESOLUTION']._serialized_end=1634 - _globals['_GENERATEIMAGEREQUEST']._serialized_start=60 - _globals['_GENERATEIMAGEREQUEST']._serialized_end=485 - _globals['_IMAGERESPONSE']._serialized_start=488 - _globals['_IMAGERESPONSE']._serialized_end=620 - _globals['_GENERATEDIMAGE']._serialized_start=622 - _globals['_GENERATEDIMAGE']._serialized_end=746 - _globals['_IMAGEURLCONTENT']._serialized_start=748 - _globals['_IMAGEURLCONTENT']._serialized_end=840 - _globals['_IMAGE']._serialized_start=1636 - _globals['_IMAGE']._serialized_end=1717 + _globals['_IMAGEDETAIL']._serialized_start=1789 + _globals['_IMAGEDETAIL']._serialized_end=1872 + _globals['_IMAGEFORMAT']._serialized_start=1874 + _globals['_IMAGEFORMAT']._serialized_end=1954 + _globals['_IMAGEQUALITY']._serialized_start=1956 + _globals['_IMAGEQUALITY']._serialized_end=2062 + _globals['_IMAGEASPECTRATIO']._serialized_start=2065 + _globals['_IMAGEASPECTRATIO']._serialized_end=2488 + _globals['_IMAGERESOLUTION']._serialized_start=2490 + _globals['_IMAGERESOLUTION']._serialized_end=2581 + _globals['_GENERATEIMAGEREQUEST']._serialized_start=93 + _globals['_GENERATEIMAGEREQUEST']._serialized_end=609 + _globals['_STORAGEOPTIONS']._serialized_start=612 + _globals['_STORAGEOPTIONS']._serialized_end=794 + _globals['_PUBLICURLOPTIONS']._serialized_start=796 + _globals['_PUBLICURLOPTIONS']._serialized_end=874 + _globals['_FILEOUTPUT']._serialized_start=877 + _globals['_FILEOUTPUT']._serialized_end=1256 + _globals['_IMAGERESPONSE']._serialized_start=1259 + _globals['_IMAGERESPONSE']._serialized_end=1391 + _globals['_GENERATEDIMAGE']._serialized_start=1394 + _globals['_GENERATEDIMAGE']._serialized_end=1653 + _globals['_IMAGEURLCONTENT']._serialized_start=1656 + _globals['_IMAGEURLCONTENT']._serialized_end=1787 + _globals['_IMAGE']._serialized_start=2583 + _globals['_IMAGE']._serialized_end=2664 # @@protoc_insertion_point(module_scope) diff --git a/src/xai_sdk/proto/v6/image_pb2.pyi b/src/xai_sdk/proto/v6/image_pb2.pyi index 3a37c55..97a504f 100644 --- a/src/xai_sdk/proto/v6/image_pb2.pyi +++ b/src/xai_sdk/proto/v6/image_pb2.pyi @@ -1,3 +1,4 @@ +from google.protobuf import timestamp_pb2 as _timestamp_pb2 from . import usage_pb2 as _usage_pb2 from google.protobuf.internal import containers as _containers from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper @@ -82,7 +83,7 @@ IMG_RESOLUTION_1K: ImageResolution IMG_RESOLUTION_2K: ImageResolution class GenerateImageRequest(_message.Message): - __slots__ = ("prompt", "image", "model", "n", "user", "format", "aspect_ratio", "resolution", "images") + __slots__ = ("prompt", "image", "model", "n", "user", "format", "aspect_ratio", "resolution", "images", "storage_options") PROMPT_FIELD_NUMBER: _ClassVar[int] IMAGE_FIELD_NUMBER: _ClassVar[int] MODEL_FIELD_NUMBER: _ClassVar[int] @@ -92,6 +93,7 @@ class GenerateImageRequest(_message.Message): ASPECT_RATIO_FIELD_NUMBER: _ClassVar[int] RESOLUTION_FIELD_NUMBER: _ClassVar[int] IMAGES_FIELD_NUMBER: _ClassVar[int] + STORAGE_OPTIONS_FIELD_NUMBER: _ClassVar[int] prompt: str image: ImageUrlContent model: str @@ -101,7 +103,40 @@ class GenerateImageRequest(_message.Message): aspect_ratio: ImageAspectRatio resolution: ImageResolution images: _containers.RepeatedCompositeFieldContainer[ImageUrlContent] - def __init__(self, prompt: _Optional[str] = ..., image: _Optional[_Union[ImageUrlContent, _Mapping]] = ..., model: _Optional[str] = ..., n: _Optional[int] = ..., user: _Optional[str] = ..., format: _Optional[_Union[ImageFormat, str]] = ..., aspect_ratio: _Optional[_Union[ImageAspectRatio, str]] = ..., resolution: _Optional[_Union[ImageResolution, str]] = ..., images: _Optional[_Iterable[_Union[ImageUrlContent, _Mapping]]] = ...) -> None: ... + storage_options: StorageOptions + def __init__(self, prompt: _Optional[str] = ..., image: _Optional[_Union[ImageUrlContent, _Mapping]] = ..., model: _Optional[str] = ..., n: _Optional[int] = ..., user: _Optional[str] = ..., format: _Optional[_Union[ImageFormat, str]] = ..., aspect_ratio: _Optional[_Union[ImageAspectRatio, str]] = ..., resolution: _Optional[_Union[ImageResolution, str]] = ..., images: _Optional[_Iterable[_Union[ImageUrlContent, _Mapping]]] = ..., storage_options: _Optional[_Union[StorageOptions, _Mapping]] = ...) -> None: ... + +class StorageOptions(_message.Message): + __slots__ = ("filename", "expires_after", "public_url") + FILENAME_FIELD_NUMBER: _ClassVar[int] + EXPIRES_AFTER_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_FIELD_NUMBER: _ClassVar[int] + filename: str + expires_after: int + public_url: PublicUrlOptions + def __init__(self, filename: _Optional[str] = ..., expires_after: _Optional[int] = ..., public_url: _Optional[_Union[PublicUrlOptions, _Mapping]] = ...) -> None: ... + +class PublicUrlOptions(_message.Message): + __slots__ = ("expires_after",) + EXPIRES_AFTER_FIELD_NUMBER: _ClassVar[int] + expires_after: int + def __init__(self, expires_after: _Optional[int] = ...) -> None: ... + +class FileOutput(_message.Message): + __slots__ = ("file_id", "filename", "public_url", "public_url_expires_at", "expires_at", "public_url_error") + FILE_ID_FIELD_NUMBER: _ClassVar[int] + FILENAME_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] + EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] + PUBLIC_URL_ERROR_FIELD_NUMBER: _ClassVar[int] + file_id: str + filename: str + public_url: str + public_url_expires_at: _timestamp_pb2.Timestamp + expires_at: _timestamp_pb2.Timestamp + public_url_error: str + def __init__(self, file_id: _Optional[str] = ..., filename: _Optional[str] = ..., public_url: _Optional[str] = ..., public_url_expires_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., expires_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., public_url_error: _Optional[str] = ...) -> None: ... class ImageResponse(_message.Message): __slots__ = ("images", "model", "usage") @@ -114,19 +149,25 @@ class ImageResponse(_message.Message): def __init__(self, images: _Optional[_Iterable[_Union[GeneratedImage, _Mapping]]] = ..., model: _Optional[str] = ..., usage: _Optional[_Union[_usage_pb2.SamplingUsage, _Mapping]] = ...) -> None: ... class GeneratedImage(_message.Message): - __slots__ = ("base64", "url", "respect_moderation") + __slots__ = ("base64", "url", "respect_moderation", "file_output", "storage_error") BASE64_FIELD_NUMBER: _ClassVar[int] URL_FIELD_NUMBER: _ClassVar[int] RESPECT_MODERATION_FIELD_NUMBER: _ClassVar[int] + FILE_OUTPUT_FIELD_NUMBER: _ClassVar[int] + STORAGE_ERROR_FIELD_NUMBER: _ClassVar[int] base64: str url: str respect_moderation: bool - def __init__(self, base64: _Optional[str] = ..., url: _Optional[str] = ..., respect_moderation: bool = ...) -> None: ... + file_output: FileOutput + storage_error: str + def __init__(self, base64: _Optional[str] = ..., url: _Optional[str] = ..., respect_moderation: bool = ..., file_output: _Optional[_Union[FileOutput, _Mapping]] = ..., storage_error: _Optional[str] = ...) -> None: ... class ImageUrlContent(_message.Message): - __slots__ = ("image_url", "detail") + __slots__ = ("image_url", "file_id", "detail") IMAGE_URL_FIELD_NUMBER: _ClassVar[int] + FILE_ID_FIELD_NUMBER: _ClassVar[int] DETAIL_FIELD_NUMBER: _ClassVar[int] image_url: str + file_id: str detail: ImageDetail - def __init__(self, image_url: _Optional[str] = ..., detail: _Optional[_Union[ImageDetail, str]] = ...) -> None: ... + def __init__(self, image_url: _Optional[str] = ..., file_id: _Optional[str] = ..., detail: _Optional[_Union[ImageDetail, str]] = ...) -> None: ... diff --git a/src/xai_sdk/proto/v6/video_pb2.py b/src/xai_sdk/proto/v6/video_pb2.py index 1c1a355..7f9f335 100644 --- a/src/xai_sdk/proto/v6/video_pb2.py +++ b/src/xai_sdk/proto/v6/video_pb2.py @@ -27,7 +27,7 @@ from . import usage_pb2 as xai_dot_api_dot_v1_dot_usage__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/video.proto\x12\x07xai_api\x1a\x19xai/api/v1/deferred.proto\x1a\x16xai/api/v1/image.proto\x1a\x16xai/api/v1/usage.proto\"#\n\x0fVideoUrlContent\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\"\xb9\x03\n\x14GenerateVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05image\x18\x02 \x01(\x0b\x32\x18.xai_api.ImageUrlContentR\x05image\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12.\n\x05video\x18\x06 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x41\n\x0c\x61spect_ratio\x18\x07 \x01(\x0e\x32\x19.xai_api.VideoAspectRatioH\x01R\x0b\x61spectRatio\x88\x01\x01\x12=\n\nresolution\x18\x08 \x01(\x0e\x32\x18.xai_api.VideoResolutionH\x02R\nresolution\x88\x01\x01\x12\x43\n\x10reference_images\x18\r \x03(\x0b\x32\x18.xai_api.ImageUrlContentR\x0freferenceImagesB\x0b\n\t_durationB\x0f\n\r_aspect_ratioB\r\n\x0b_resolution\"8\n\x17GetDeferredVideoRequest\x12\x1d\n\nrequest_id\x18\x01 \x01(\tR\trequestId\"\xd8\x01\n\rVideoResponse\x12-\n\x05video\x18\x01 \x01(\x0b\x32\x17.xai_api.GeneratedVideoR\x05video\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12,\n\x05usage\x18\x03 \x01(\x0b\x32\x16.xai_api.SamplingUsageR\x05usage\x12.\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x13.xai_api.VideoErrorH\x00R\x05\x65rror\x88\x01\x01\x12\x1a\n\x08progress\x18\x07 \x01(\x05R\x08progressB\x08\n\x06_error\"m\n\x0eGeneratedVideo\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x1a\n\x08\x64uration\x18\x04 \x01(\x05R\x08\x64uration\x12-\n\x12respect_moderation\x18\x05 \x01(\x08R\x11respectModeration\"\x91\x01\n\x18GetDeferredVideoResponse\x12/\n\x06status\x18\x01 \x01(\x0e\x32\x17.xai_api.DeferredStatusR\x06status\x12\x37\n\x08response\x18\x02 \x01(\x0b\x32\x16.xai_api.VideoResponseH\x00R\x08response\x88\x01\x01\x42\x0b\n\t_response\":\n\nVideoError\x12\x12\n\x04\x63ode\x18\x01 \x01(\tR\x04\x63ode\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\"\xa0\x01\n\x12\x45xtendVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05video\x18\x02 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x42\x0b\n\t_duration*\xfc\x01\n\x10VideoAspectRatio\x12\"\n\x1eVIDEO_ASPECT_RATIO_UNSPECIFIED\x10\x00\x12\x1a\n\x16VIDEO_ASPECT_RATIO_1_1\x10\x01\x12\x1b\n\x17VIDEO_ASPECT_RATIO_16_9\x10\x02\x12\x1b\n\x17VIDEO_ASPECT_RATIO_9_16\x10\x03\x12\x1a\n\x16VIDEO_ASPECT_RATIO_4_3\x10\x04\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_4\x10\x05\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_2\x10\x06\x12\x1a\n\x16VIDEO_ASPECT_RATIO_2_3\x10\x07*i\n\x0fVideoResolution\x12 \n\x1cVIDEO_RESOLUTION_UNSPECIFIED\x10\x00\x12\x19\n\x15VIDEO_RESOLUTION_480P\x10\x01\x12\x19\n\x15VIDEO_RESOLUTION_720P\x10\x02\x32\x82\x02\n\x05Video\x12P\n\rGenerateVideo\x12\x1d.xai_api.GenerateVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12L\n\x0b\x45xtendVideo\x12\x1b.xai_api.ExtendVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12Y\n\x10GetDeferredVideo\x12 .xai_api.GetDeferredVideoRequest\x1a!.xai_api.GetDeferredVideoResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nVideoProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/video.proto\x12\x07xai_api\x1a\x19xai/api/v1/deferred.proto\x1a\x16xai/api/v1/image.proto\x1a\x16xai/api/v1/usage.proto\"J\n\x0fVideoUrlContent\x12\x12\n\x03url\x18\x01 \x01(\tH\x00R\x03url\x12\x19\n\x07\x66ile_id\x18\x02 \x01(\tH\x00R\x06\x66ileIdB\x08\n\x06source\"\x94\x04\n\x14GenerateVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05image\x18\x02 \x01(\x0b\x32\x18.xai_api.ImageUrlContentR\x05image\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12.\n\x05video\x18\x06 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x41\n\x0c\x61spect_ratio\x18\x07 \x01(\x0e\x32\x19.xai_api.VideoAspectRatioH\x01R\x0b\x61spectRatio\x88\x01\x01\x12=\n\nresolution\x18\x08 \x01(\x0e\x32\x18.xai_api.VideoResolutionH\x02R\nresolution\x88\x01\x01\x12\x43\n\x10reference_images\x18\r \x03(\x0b\x32\x18.xai_api.ImageUrlContentR\x0freferenceImages\x12\x45\n\x0fstorage_options\x18\x0e \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x03R\x0estorageOptions\x88\x01\x01\x42\x0b\n\t_durationB\x0f\n\r_aspect_ratioB\r\n\x0b_resolutionB\x12\n\x10_storage_options\"8\n\x17GetDeferredVideoRequest\x12\x1d\n\nrequest_id\x18\x01 \x01(\tR\trequestId\"\xd8\x01\n\rVideoResponse\x12-\n\x05video\x18\x01 \x01(\x0b\x32\x17.xai_api.GeneratedVideoR\x05video\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12,\n\x05usage\x18\x03 \x01(\x0b\x32\x16.xai_api.SamplingUsageR\x05usage\x12.\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x13.xai_api.VideoErrorH\x00R\x05\x65rror\x88\x01\x01\x12\x1a\n\x08progress\x18\x07 \x01(\x05R\x08progressB\x08\n\x06_error\"\xf4\x01\n\x0eGeneratedVideo\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x1a\n\x08\x64uration\x18\x04 \x01(\x05R\x08\x64uration\x12-\n\x12respect_moderation\x18\x05 \x01(\x08R\x11respectModeration\x12\x39\n\x0b\x66ile_output\x18\x06 \x01(\x0b\x32\x13.xai_api.FileOutputH\x00R\nfileOutput\x88\x01\x01\x12(\n\rstorage_error\x18\x07 \x01(\tH\x01R\x0cstorageError\x88\x01\x01\x42\x0e\n\x0c_file_outputB\x10\n\x0e_storage_error\"\x91\x01\n\x18GetDeferredVideoResponse\x12/\n\x06status\x18\x01 \x01(\x0e\x32\x17.xai_api.DeferredStatusR\x06status\x12\x37\n\x08response\x18\x02 \x01(\x0b\x32\x16.xai_api.VideoResponseH\x00R\x08response\x88\x01\x01\x42\x0b\n\t_response\":\n\nVideoError\x12\x12\n\x04\x63ode\x18\x01 \x01(\tR\x04\x63ode\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\"\xfb\x01\n\x12\x45xtendVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05video\x18\x02 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12\x45\n\x0fstorage_options\x18\x06 \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x01R\x0estorageOptions\x88\x01\x01\x42\x0b\n\t_durationB\x12\n\x10_storage_options*\xfc\x01\n\x10VideoAspectRatio\x12\"\n\x1eVIDEO_ASPECT_RATIO_UNSPECIFIED\x10\x00\x12\x1a\n\x16VIDEO_ASPECT_RATIO_1_1\x10\x01\x12\x1b\n\x17VIDEO_ASPECT_RATIO_16_9\x10\x02\x12\x1b\n\x17VIDEO_ASPECT_RATIO_9_16\x10\x03\x12\x1a\n\x16VIDEO_ASPECT_RATIO_4_3\x10\x04\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_4\x10\x05\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_2\x10\x06\x12\x1a\n\x16VIDEO_ASPECT_RATIO_2_3\x10\x07*i\n\x0fVideoResolution\x12 \n\x1cVIDEO_RESOLUTION_UNSPECIFIED\x10\x00\x12\x19\n\x15VIDEO_RESOLUTION_480P\x10\x01\x12\x19\n\x15VIDEO_RESOLUTION_720P\x10\x02\x32\x82\x02\n\x05Video\x12P\n\rGenerateVideo\x12\x1d.xai_api.GenerateVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12L\n\x0b\x45xtendVideo\x12\x1b.xai_api.ExtendVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12Y\n\x10GetDeferredVideo\x12 .xai_api.GetDeferredVideoRequest\x1a!.xai_api.GetDeferredVideoResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nVideoProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -35,26 +35,26 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\013com.xai_apiB\nVideoProtoP\001\242\002\003XXX\252\002\006XaiApi\312\002\006XaiApi\342\002\022XaiApi\\GPBMetadata\352\002\006XaiApi' - _globals['_VIDEOASPECTRATIO']._serialized_start=1351 - _globals['_VIDEOASPECTRATIO']._serialized_end=1603 - _globals['_VIDEORESOLUTION']._serialized_start=1605 - _globals['_VIDEORESOLUTION']._serialized_end=1710 + _globals['_VIDEOASPECTRATIO']._serialized_start=1708 + _globals['_VIDEOASPECTRATIO']._serialized_end=1960 + _globals['_VIDEORESOLUTION']._serialized_start=1962 + _globals['_VIDEORESOLUTION']._serialized_end=2067 _globals['_VIDEOURLCONTENT']._serialized_start=110 - _globals['_VIDEOURLCONTENT']._serialized_end=145 - _globals['_GENERATEVIDEOREQUEST']._serialized_start=148 - _globals['_GENERATEVIDEOREQUEST']._serialized_end=589 - _globals['_GETDEFERREDVIDEOREQUEST']._serialized_start=591 - _globals['_GETDEFERREDVIDEOREQUEST']._serialized_end=647 - _globals['_VIDEORESPONSE']._serialized_start=650 - _globals['_VIDEORESPONSE']._serialized_end=866 - _globals['_GENERATEDVIDEO']._serialized_start=868 - _globals['_GENERATEDVIDEO']._serialized_end=977 - _globals['_GETDEFERREDVIDEORESPONSE']._serialized_start=980 - _globals['_GETDEFERREDVIDEORESPONSE']._serialized_end=1125 - _globals['_VIDEOERROR']._serialized_start=1127 - _globals['_VIDEOERROR']._serialized_end=1185 - _globals['_EXTENDVIDEOREQUEST']._serialized_start=1188 - _globals['_EXTENDVIDEOREQUEST']._serialized_end=1348 - _globals['_VIDEO']._serialized_start=1713 - _globals['_VIDEO']._serialized_end=1971 + _globals['_VIDEOURLCONTENT']._serialized_end=184 + _globals['_GENERATEVIDEOREQUEST']._serialized_start=187 + _globals['_GENERATEVIDEOREQUEST']._serialized_end=719 + _globals['_GETDEFERREDVIDEOREQUEST']._serialized_start=721 + _globals['_GETDEFERREDVIDEOREQUEST']._serialized_end=777 + _globals['_VIDEORESPONSE']._serialized_start=780 + _globals['_VIDEORESPONSE']._serialized_end=996 + _globals['_GENERATEDVIDEO']._serialized_start=999 + _globals['_GENERATEDVIDEO']._serialized_end=1243 + _globals['_GETDEFERREDVIDEORESPONSE']._serialized_start=1246 + _globals['_GETDEFERREDVIDEORESPONSE']._serialized_end=1391 + _globals['_VIDEOERROR']._serialized_start=1393 + _globals['_VIDEOERROR']._serialized_end=1451 + _globals['_EXTENDVIDEOREQUEST']._serialized_start=1454 + _globals['_EXTENDVIDEOREQUEST']._serialized_end=1705 + _globals['_VIDEO']._serialized_start=2070 + _globals['_VIDEO']._serialized_end=2328 # @@protoc_insertion_point(module_scope) diff --git a/src/xai_sdk/proto/v6/video_pb2.pyi b/src/xai_sdk/proto/v6/video_pb2.pyi index 6e2cb73..b8bd5a0 100644 --- a/src/xai_sdk/proto/v6/video_pb2.pyi +++ b/src/xai_sdk/proto/v6/video_pb2.pyi @@ -39,13 +39,15 @@ VIDEO_RESOLUTION_480P: VideoResolution VIDEO_RESOLUTION_720P: VideoResolution class VideoUrlContent(_message.Message): - __slots__ = ("url",) + __slots__ = ("url", "file_id") URL_FIELD_NUMBER: _ClassVar[int] + FILE_ID_FIELD_NUMBER: _ClassVar[int] url: str - def __init__(self, url: _Optional[str] = ...) -> None: ... + file_id: str + def __init__(self, url: _Optional[str] = ..., file_id: _Optional[str] = ...) -> None: ... class GenerateVideoRequest(_message.Message): - __slots__ = ("prompt", "image", "model", "duration", "video", "aspect_ratio", "resolution", "reference_images") + __slots__ = ("prompt", "image", "model", "duration", "video", "aspect_ratio", "resolution", "reference_images", "storage_options") PROMPT_FIELD_NUMBER: _ClassVar[int] IMAGE_FIELD_NUMBER: _ClassVar[int] MODEL_FIELD_NUMBER: _ClassVar[int] @@ -54,6 +56,7 @@ class GenerateVideoRequest(_message.Message): ASPECT_RATIO_FIELD_NUMBER: _ClassVar[int] RESOLUTION_FIELD_NUMBER: _ClassVar[int] REFERENCE_IMAGES_FIELD_NUMBER: _ClassVar[int] + STORAGE_OPTIONS_FIELD_NUMBER: _ClassVar[int] prompt: str image: _image_pb2.ImageUrlContent model: str @@ -62,7 +65,8 @@ class GenerateVideoRequest(_message.Message): aspect_ratio: VideoAspectRatio resolution: VideoResolution reference_images: _containers.RepeatedCompositeFieldContainer[_image_pb2.ImageUrlContent] - def __init__(self, prompt: _Optional[str] = ..., image: _Optional[_Union[_image_pb2.ImageUrlContent, _Mapping]] = ..., model: _Optional[str] = ..., duration: _Optional[int] = ..., video: _Optional[_Union[VideoUrlContent, _Mapping]] = ..., aspect_ratio: _Optional[_Union[VideoAspectRatio, str]] = ..., resolution: _Optional[_Union[VideoResolution, str]] = ..., reference_images: _Optional[_Iterable[_Union[_image_pb2.ImageUrlContent, _Mapping]]] = ...) -> None: ... + storage_options: _image_pb2.StorageOptions + def __init__(self, prompt: _Optional[str] = ..., image: _Optional[_Union[_image_pb2.ImageUrlContent, _Mapping]] = ..., model: _Optional[str] = ..., duration: _Optional[int] = ..., video: _Optional[_Union[VideoUrlContent, _Mapping]] = ..., aspect_ratio: _Optional[_Union[VideoAspectRatio, str]] = ..., resolution: _Optional[_Union[VideoResolution, str]] = ..., reference_images: _Optional[_Iterable[_Union[_image_pb2.ImageUrlContent, _Mapping]]] = ..., storage_options: _Optional[_Union[_image_pb2.StorageOptions, _Mapping]] = ...) -> None: ... class GetDeferredVideoRequest(_message.Message): __slots__ = ("request_id",) @@ -85,14 +89,18 @@ class VideoResponse(_message.Message): def __init__(self, video: _Optional[_Union[GeneratedVideo, _Mapping]] = ..., model: _Optional[str] = ..., usage: _Optional[_Union[_usage_pb2.SamplingUsage, _Mapping]] = ..., error: _Optional[_Union[VideoError, _Mapping]] = ..., progress: _Optional[int] = ...) -> None: ... class GeneratedVideo(_message.Message): - __slots__ = ("url", "duration", "respect_moderation") + __slots__ = ("url", "duration", "respect_moderation", "file_output", "storage_error") URL_FIELD_NUMBER: _ClassVar[int] DURATION_FIELD_NUMBER: _ClassVar[int] RESPECT_MODERATION_FIELD_NUMBER: _ClassVar[int] + FILE_OUTPUT_FIELD_NUMBER: _ClassVar[int] + STORAGE_ERROR_FIELD_NUMBER: _ClassVar[int] url: str duration: int respect_moderation: bool - def __init__(self, url: _Optional[str] = ..., duration: _Optional[int] = ..., respect_moderation: bool = ...) -> None: ... + file_output: _image_pb2.FileOutput + storage_error: str + def __init__(self, url: _Optional[str] = ..., duration: _Optional[int] = ..., respect_moderation: bool = ..., file_output: _Optional[_Union[_image_pb2.FileOutput, _Mapping]] = ..., storage_error: _Optional[str] = ...) -> None: ... class GetDeferredVideoResponse(_message.Message): __slots__ = ("status", "response") @@ -111,13 +119,15 @@ class VideoError(_message.Message): def __init__(self, code: _Optional[str] = ..., message: _Optional[str] = ...) -> None: ... class ExtendVideoRequest(_message.Message): - __slots__ = ("prompt", "video", "model", "duration") + __slots__ = ("prompt", "video", "model", "duration", "storage_options") PROMPT_FIELD_NUMBER: _ClassVar[int] VIDEO_FIELD_NUMBER: _ClassVar[int] MODEL_FIELD_NUMBER: _ClassVar[int] DURATION_FIELD_NUMBER: _ClassVar[int] + STORAGE_OPTIONS_FIELD_NUMBER: _ClassVar[int] prompt: str video: VideoUrlContent model: str duration: int - def __init__(self, prompt: _Optional[str] = ..., video: _Optional[_Union[VideoUrlContent, _Mapping]] = ..., model: _Optional[str] = ..., duration: _Optional[int] = ...) -> None: ... + storage_options: _image_pb2.StorageOptions + def __init__(self, prompt: _Optional[str] = ..., video: _Optional[_Union[VideoUrlContent, _Mapping]] = ..., model: _Optional[str] = ..., duration: _Optional[int] = ..., storage_options: _Optional[_Union[_image_pb2.StorageOptions, _Mapping]] = ...) -> None: ... diff --git a/src/xai_sdk/sync/image.py b/src/xai_sdk/sync/image.py index b6b402f..f25d333 100644 --- a/src/xai_sdk/sync/image.py +++ b/src/xai_sdk/sync/image.py @@ -4,6 +4,7 @@ from opentelemetry.trace import SpanKind from ..__about__ import __version__ +from ..files import StorageOptions from ..image import ( BaseClient, BaseImageResponse, @@ -14,7 +15,7 @@ _make_span_request_attributes, _make_span_response_attributes, ) -from ..proto import batch_pb2 +from ..proto import batch_pb2, image_pb2 from ..telemetry import get_tracer from ..types import ImageGenerationModel @@ -31,11 +32,14 @@ def prepare( *, batch_request_id: Optional[str] = None, image_url: Optional[str] = None, + image_file_id: Optional[str] = None, image_urls: Optional[Sequence[str]] = None, + image_file_ids: Optional[Sequence[str]] = None, user: Optional[str] = None, image_format: Optional[ImageFormat] = None, aspect_ratio: Optional[ImageAspectRatio] = None, resolution: Optional[ImageResolution] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> batch_pb2.BatchRequest: """Prepares an image generation request for batch processing. @@ -49,13 +53,35 @@ def prepare( **If provided, it must be unique within the batch.** Used to identify the corresponding result when the response is returned. image_url: The URL or base64-encoded string of an input image to use as a starting point. - Cannot be set together with `image_urls`. Only supported for grok-imagine models. + Cannot be set together with `image_file_id`, `image_urls`, or `image_file_ids`. + Only supported for grok-imagine models. + image_file_id: The file ID of an input image to use as a starting point. + Cannot be set together with `image_url`, `image_urls`, or `image_file_ids`. + Only supported for grok-imagine models. image_urls: Optional list of input images for multi-reference image editing. - Cannot be set together with `image_url`. Only supported for grok-imagine models. + Cannot be set together with single-image params (`image_url` / + `image_file_id`). May be combined with `image_file_ids` to mix + URL/base64 and file-ID references in the same request; file IDs + are appended first in that case. + Only supported for grok-imagine models. + image_file_ids: Optional list of input image file IDs for multi-reference image editing. + Cannot be set together with single-image params (`image_url` / + `image_file_id`). May be combined with `image_urls` to mix + URL/base64 and file-ID references in the same request. + Only supported for grok-imagine models. user: A unique identifier representing your end-user. image_format: The format of the image to return ("url" or "base64"). Defaults to "url". aspect_ratio: The aspect ratio of the image to generate. resolution: The image resolution to generate ("1k" or "2k"). + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.png"} # store privately, no expiry + storage_options={"filename": "output.png", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.png", "public_url": True} # + shareable URL + storage_options={"filename": "output.png", "public_url": {"expires_after": 86400}} # + expiring URL Returns: A `BatchRequest` proto ready to be added to a batch. @@ -91,11 +117,14 @@ def prepare( prompt, model, image_url=image_url, + image_file_id=image_file_id, image_urls=image_urls, + image_file_ids=image_file_ids, user=user, image_format=image_format, aspect_ratio=aspect_ratio, resolution=resolution, + storage_options=storage_options, ) return batch_pb2.BatchRequest( image_request=request, @@ -108,11 +137,14 @@ def sample( model: Union[ImageGenerationModel, str], *, image_url: Optional[str] = None, + image_file_id: Optional[str] = None, image_urls: Optional[Sequence[str]] = None, + image_file_ids: Optional[Sequence[str]] = None, user: Optional[str] = None, image_format: Optional[ImageFormat] = None, aspect_ratio: Optional[ImageAspectRatio] = None, resolution: Optional[ImageResolution] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> "ImageResponse": """Samples a single image based on the provided prompt. @@ -120,11 +152,22 @@ def sample( prompt: The prompt to generate an image from. model: The model to use for image generation. image_url: The URL or base64-encoded string of an input image to use as a starting point for generation. - This field cannot be set together with `image_urls`. + This field cannot be set together with `image_file_id`, `image_urls`, or `image_file_ids`. + Only supported for grok-imagine models. + image_file_id: The file ID of an input image to use as a starting point for generation. + Cannot be set together with `image_url`, `image_urls`, or `image_file_ids`. Only supported for grok-imagine models. image_urls: Optional list of input images for multi-reference image editing. Each image is a URL or base64-encoded string, matching the `image_url` format. - This field cannot be set together with `image_url`. + Cannot be set together with single-image params (`image_url` / + `image_file_id`). May be combined with `image_file_ids` to mix + URL/base64 and file-ID references in the same request; file IDs + are appended first in that case. + Only supported for grok-imagine models. + image_file_ids: Optional list of input image file IDs for multi-reference image editing. + Cannot be set together with single-image params (`image_url` / + `image_file_id`). May be combined with `image_urls` to mix + URL/base64 and file-ID references in the same request. Only supported for grok-imagine models. user: A unique identifier representing your end-user, which can help xAI to monitor and detect abuse. image_format: The format of the image to return. One of: @@ -150,6 +193,15 @@ def sample( - `"1k"`: ~1 megapixel total. Dimensions vary by aspect ratio. - `"2k"`: ~4 megapixels total. Dimensions vary by aspect ratio. Only supported for grok-imagine models. + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.png"} # store privately, no expiry + storage_options={"filename": "output.png", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.png", "public_url": True} # + shareable URL + storage_options={"filename": "output.png", "public_url": {"expires_after": 86400}} # + expiring URL Returns: An `ImageResponse` object allowing access to the generated image. @@ -158,11 +210,14 @@ def sample( prompt, model, image_url=image_url, + image_file_id=image_file_id, image_urls=image_urls, + image_file_ids=image_file_ids, user=user, image_format=image_format, aspect_ratio=aspect_ratio, resolution=resolution, + storage_options=storage_options, ) with tracer.start_as_current_span( name=f"image.sample {model}", @@ -181,11 +236,14 @@ def sample_batch( n: int, *, image_url: Optional[str] = None, + image_file_id: Optional[str] = None, image_urls: Optional[Sequence[str]] = None, + image_file_ids: Optional[Sequence[str]] = None, user: Optional[str] = None, image_format: Optional[ImageFormat] = None, aspect_ratio: Optional[ImageAspectRatio] = None, resolution: Optional[ImageResolution] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> Sequence["ImageResponse"]: """Samples a batch of images based on the provided prompt. @@ -194,11 +252,22 @@ def sample_batch( model: The model to use for image generation. n: The number of images to generate. image_url: The URL or base64-encoded string of an input image to use as a starting point for generation. - This field cannot be set together with `image_urls`. + This field cannot be set together with `image_file_id`, `image_urls`, or `image_file_ids`. + Only supported for grok-imagine models. + image_file_id: The file ID of an input image to use as a starting point for generation. + Cannot be set together with `image_url`, `image_urls`, or `image_file_ids`. Only supported for grok-imagine models. image_urls: Optional list of input images for multi-reference image editing. Each image is a URL or base64-encoded string, matching the `image_url` format. - This field cannot be set together with `image_url`. + Cannot be set together with single-image params (`image_url` / + `image_file_id`). May be combined with `image_file_ids` to mix + URL/base64 and file-ID references in the same request; file IDs + are appended first in that case. + Only supported for grok-imagine models. + image_file_ids: Optional list of input image file IDs for multi-reference image editing. + Cannot be set together with single-image params (`image_url` / + `image_file_id`). May be combined with `image_urls` to mix + URL/base64 and file-ID references in the same request. Only supported for grok-imagine models. user: A unique identifier representing your end-user, which can help xAI to monitor and detect abuse. image_format: The format of the image to return. One of: @@ -224,6 +293,15 @@ def sample_batch( - `"1k"`: ~1 megapixel total. Dimensions vary by aspect ratio. - `"2k"`: ~4 megapixels total. Dimensions vary by aspect ratio. Only supported for grok-imagine models. + storage_options: Persist the results to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.png"} # store privately, no expiry + storage_options={"filename": "output.png", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.png", "public_url": True} # + shareable URL + storage_options={"filename": "output.png", "public_url": {"expires_after": 86400}} # + expiring URL Returns: A sequence of `ImageResponse` objects, one for each image generated. @@ -233,11 +311,14 @@ def sample_batch( model, n=n, image_url=image_url, + image_file_id=image_file_id, image_urls=image_urls, + image_file_ids=image_file_ids, user=user, image_format=image_format, aspect_ratio=aspect_ratio, resolution=resolution, + storage_options=storage_options, ) with tracer.start_as_current_span( name=f"image.sample_batch {model}", diff --git a/src/xai_sdk/sync/video.py b/src/xai_sdk/sync/video.py index 9e3cbc6..cd3ef7e 100644 --- a/src/xai_sdk/sync/video.py +++ b/src/xai_sdk/sync/video.py @@ -5,8 +5,9 @@ from opentelemetry.trace import SpanKind +from ..files import StorageOptions from ..poll_timer import PollTimer -from ..proto import batch_pb2, deferred_pb2, video_pb2 +from ..proto import batch_pb2, deferred_pb2, image_pb2, video_pb2 from ..telemetry import get_tracer from ..types import VideoGenerationModel from ..video import ( @@ -38,11 +39,15 @@ def prepare( *, batch_request_id: Optional[str] = None, image_url: Optional[str] = None, + image_file_id: Optional[str] = None, video_url: Optional[str] = None, + video_file_id: Optional[str] = None, duration: Optional[int] = None, aspect_ratio: Optional[VideoAspectRatio] = None, resolution: Optional[VideoResolution] = None, reference_image_urls: Optional[Sequence[str]] = None, + reference_image_file_ids: Optional[Sequence[str]] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> batch_pb2.BatchRequest: """Prepares a video generation request for batch processing. @@ -56,13 +61,35 @@ def prepare( **If provided, it must be unique within the batch.** Used to identify the corresponding result when the response is returned. image_url: The URL of an input image to use as a starting frame. + Cannot be set together with ``image_file_id``. + image_file_id: The file ID of an input image to use as a starting frame. + Cannot be set together with ``image_url``. video_url: The URL of an input video to use as a starting point. + Cannot be set together with ``video_file_id``. + video_file_id: The file ID of an input video to use as a starting point. + Cannot be set together with ``video_url``. duration: The duration of the video to generate in seconds. aspect_ratio: The aspect ratio of the video to generate. resolution: The video resolution to generate. reference_image_urls: Optional list of reference image URLs for reference-to-video (R2V) generation. When provided (and `image_url` is not set), generates video using these images as style/content references. + May be combined with ``reference_image_file_ids`` to mix URL/base64 + and file-ID references in the same request; file IDs are appended + first in that case. + reference_image_file_ids: Optional list of reference image file IDs for + reference-to-video (R2V) generation. May be combined with + ``reference_image_urls`` to mix URL/base64 and file-ID references + in the same request. + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.mp4"} # store privately, no expiry + storage_options={"filename": "output.mp4", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.mp4", "public_url": True} # + shareable URL + storage_options={"filename": "output.mp4", "public_url": {"expires_after": 86400}} # + expiring URL Returns: A `BatchRequest` proto ready to be added to a batch. @@ -98,11 +125,15 @@ def prepare( prompt, model, image_url=image_url, + image_file_id=image_file_id, video_url=video_url, + video_file_id=video_file_id, duration=duration, aspect_ratio=aspect_ratio, resolution=resolution, reference_image_urls=reference_image_urls, + reference_image_file_ids=reference_image_file_ids, + storage_options=storage_options, ) return batch_pb2.BatchRequest( @@ -114,10 +145,12 @@ def prepare_extension( self, prompt: str, model: Union[VideoGenerationModel, str], - video_url: str, + video_url: Optional[str] = None, *, + video_file_id: Optional[str] = None, batch_request_id: Optional[str] = None, duration: Optional[int] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> batch_pb2.BatchRequest: """Prepares a video extension request for batch processing. @@ -128,10 +161,22 @@ def prepare_extension( prompt: Prompt describing what should happen next in the video. model: The model to use for video extension. video_url: The URL of the input video to extend. + Cannot be set together with ``video_file_id``. + video_file_id: The file ID of the input video to extend. + Cannot be set together with ``video_url``. batch_request_id: An optional user-provided identifier for the batch request. **If provided, it must be unique within the batch.** duration: Duration of the extension segment in seconds (1-10). Defaults to 6 seconds if not specified. + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.mp4"} # store privately, no expiry + storage_options={"filename": "output.mp4", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.mp4", "public_url": True} # + shareable URL + storage_options={"filename": "output.mp4", "public_url": {"expires_after": 86400}} # + expiring URL Returns: A `BatchRequest` proto ready to be added to a batch. @@ -139,8 +184,10 @@ def prepare_extension( request = _make_extend_request( prompt, model, - video_url, + video_url=video_url, + video_file_id=video_file_id, duration=duration, + storage_options=storage_options, ) return batch_pb2.BatchRequest( @@ -154,22 +201,33 @@ def start( model: Union[VideoGenerationModel, str], *, image_url: Optional[str] = None, + image_file_id: Optional[str] = None, video_url: Optional[str] = None, + video_file_id: Optional[str] = None, duration: Optional[int] = None, aspect_ratio: Optional[VideoAspectRatio] = None, resolution: Optional[VideoResolution] = None, reference_image_urls: Optional[Sequence[str]] = None, + reference_image_file_ids: Optional[Sequence[str]] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> deferred_pb2.StartDeferredResponse: - """Starts a video generation request and returns a request_id for polling.""" + """Starts a video generation request and returns a request_id for polling. + + See `generate()` for full parameter documentation. + """ request = _make_generate_request( prompt, model, image_url=image_url, + image_file_id=image_file_id, video_url=video_url, + video_file_id=video_file_id, duration=duration, aspect_ratio=aspect_ratio, resolution=resolution, reference_image_urls=reference_image_urls, + reference_image_file_ids=reference_image_file_ids, + storage_options=storage_options, ) with tracer.start_as_current_span( @@ -190,11 +248,15 @@ def generate( model: Union[VideoGenerationModel, str], *, image_url: Optional[str] = None, + image_file_id: Optional[str] = None, video_url: Optional[str] = None, + video_file_id: Optional[str] = None, duration: Optional[int] = None, aspect_ratio: Optional[VideoAspectRatio] = None, resolution: Optional[VideoResolution] = None, reference_image_urls: Optional[Sequence[str]] = None, + reference_image_file_ids: Optional[Sequence[str]] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, timeout: Optional[datetime.timedelta] = None, interval: Optional[datetime.timedelta] = None, ) -> VideoResponse: @@ -206,18 +268,26 @@ def generate( Supports four generation modes depending on which optional inputs are provided: - **Text-to-video**: Only a `prompt` is provided. - - **Image-to-video**: An `image_url` is provided; the image is used as the first frame. - - **Reference-to-video**: `reference_image_urls` are provided; generates video using the - images as style/content references. - - **Video editing**: A `video_url` is provided; the video is edited based on the prompt. + - **Image-to-video**: An `image_url` or `image_file_id` is provided; the image is + used as the first frame. + - **Reference-to-video**: `reference_image_urls` or `reference_image_file_ids` are + provided; generates video using the images as style/content references. + - **Video editing**: A `video_url` or `video_file_id` is provided; the video is edited + based on the prompt. Args: prompt: The text prompt to generate a video from. model: The model to use for video generation. image_url: The URL or base64-encoded data URL of an input image to use as - the first frame (image-to-video). Cannot be combined with `video_url`. + the first frame (image-to-video). Cannot be combined with `image_file_id` + or `video_url`. + image_file_id: The file ID of an input image to use as the first frame + (image-to-video). Cannot be combined with `image_url`. video_url: The URL or base64-encoded data URL of an input video to edit - based on the prompt (video-to-video). Cannot be combined with `image_url`. + based on the prompt (video-to-video). Cannot be combined with `video_file_id` + or `image_url`. + video_file_id: The file ID of an input video to edit based on the prompt + (video-to-video). Cannot be combined with `video_url`. duration: Duration of the video to generate in seconds (1-15). Defaults to 8 seconds if not specified. aspect_ratio: The aspect ratio of the video to generate. @@ -227,6 +297,22 @@ def generate( reference_image_urls: Optional list of reference image URLs for reference-to-video (R2V) generation. When provided (and `image_url` is not set), generates video using these images as style/content references. + May be combined with `reference_image_file_ids` to mix URL/base64 + and file-ID references in the same request; file IDs are appended + first in that case. + reference_image_file_ids: Optional list of reference image file IDs for + reference-to-video (R2V) generation. May be combined with + `reference_image_urls` to mix URL/base64 and file-ID references + in the same request. + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.mp4"} # store privately, no expiry + storage_options={"filename": "output.mp4", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.mp4", "public_url": True} # + shareable URL + storage_options={"filename": "output.mp4", "public_url": {"expires_after": 86400}} # + expiring URL timeout: Maximum time to wait for video generation to complete. Defaults to 10 minutes. interval: Polling interval between status checks. @@ -290,11 +376,15 @@ def generate( prompt, model, image_url=image_url, + image_file_id=image_file_id, video_url=video_url, + video_file_id=video_file_id, duration=duration, aspect_ratio=aspect_ratio, resolution=resolution, reference_image_urls=reference_image_urls, + reference_image_file_ids=reference_image_file_ids, + storage_options=storage_options, ) with tracer.start_as_current_span( @@ -336,9 +426,11 @@ def extend_start( self, prompt: str, model: Union[VideoGenerationModel, str], - video_url: str, + video_url: Optional[str] = None, *, + video_file_id: Optional[str] = None, duration: Optional[int] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> deferred_pb2.StartDeferredResponse: """Starts a video extension request and returns a request_id for polling. @@ -347,8 +439,20 @@ def extend_start( model: The model to use for video extension. video_url: The URL of the input video to extend. The extension continues from the end of this video. Input video must be between 2 and 30 seconds long. + Cannot be set together with ``video_file_id``. + video_file_id: The file ID of the input video to extend. The extension continues + from the end of this video. Cannot be set together with ``video_url``. duration: Duration of the extension segment to generate in seconds (1-10). Defaults to 6 seconds if not specified. + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.mp4"} # store privately, no expiry + storage_options={"filename": "output.mp4", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.mp4", "public_url": True} # + shareable URL + storage_options={"filename": "output.mp4", "public_url": {"expires_after": 86400}} # + expiring URL Returns: A `StartDeferredResponse` containing the `request_id` for polling. @@ -356,8 +460,10 @@ def extend_start( request = _make_extend_request( prompt, model, - video_url, + video_url=video_url, + video_file_id=video_file_id, duration=duration, + storage_options=storage_options, ) with tracer.start_as_current_span( @@ -371,9 +477,11 @@ def extend( self, prompt: str, model: Union[VideoGenerationModel, str], - video_url: str, + video_url: Optional[str] = None, *, + video_file_id: Optional[str] = None, duration: Optional[int] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, timeout: Optional[datetime.timedelta] = None, interval: Optional[datetime.timedelta] = None, ) -> VideoResponse: @@ -392,8 +500,20 @@ def extend( video_url: The URL or base64-encoded data URL of the input video to extend. The extension continues from the end of this video. Input video must be between 2 and 30 seconds long. + Cannot be set together with ``video_file_id``. + video_file_id: The file ID of the input video to extend. The extension continues + from the end of this video. Cannot be set together with ``video_url``. duration: Duration of the extension segment to generate in seconds (1-10). Defaults to 6 seconds if not specified. + storage_options: Persist the result to the Files API. Accepts a dict + with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. + Set ``public_url`` to also create a publicly shareable URL. + Examples:: + + storage_options={"filename": "output.mp4"} # store privately, no expiry + storage_options={"filename": "output.mp4", "expires_after": 7200} # auto-delete after 2h + storage_options={"filename": "output.mp4", "public_url": True} # + shareable URL + storage_options={"filename": "output.mp4", "public_url": {"expires_after": 86400}} # + expiring URL timeout: Maximum time to wait for video extension to complete. Defaults to 10 minutes. interval: Polling interval between status checks. @@ -439,8 +559,10 @@ def extend( request_pb = _make_extend_request( prompt, model, - video_url, + video_url=video_url, + video_file_id=video_file_id, duration=duration, + storage_options=storage_options, ) with tracer.start_as_current_span( diff --git a/src/xai_sdk/video.py b/src/xai_sdk/video.py index 9006d7f..b110896 100644 --- a/src/xai_sdk/video.py +++ b/src/xai_sdk/video.py @@ -4,6 +4,7 @@ import grpc from .cost import cost_usd_from_usage +from .files import StorageOptions, _resolve_storage_options_pb from .meta import ProtoDecorator from .proto import image_pb2, usage_pb2, video_pb2, video_pb2_grpc from .telemetry import should_disable_sensitive_attributes @@ -87,48 +88,127 @@ def duration(self) -> int: """Duration of the generated video in seconds.""" return self._video.duration + @property + def file_output(self) -> Optional[image_pb2.FileOutput]: + """The full FileOutput proto if the asset was stored, or None otherwise.""" + if self._video.HasField("file_output"): + return self._video.file_output + return None + + @property + def storage_error(self) -> Optional[str]: + """Error message if storage was requested but failed, or None on success.""" + if self._video.storage_error: + return self._video.storage_error + return None + + @property + def public_url(self) -> Optional[str]: + """Public URL for the stored file, or None if not requested or creation failed.""" + file_output = self.file_output + if file_output is not None and file_output.public_url: + return file_output.public_url + return None + + @property + def public_url_error(self) -> Optional[str]: + """Error message if public URL creation failed, or None on success.""" + file_output = self.file_output + if file_output is not None and file_output.public_url_error: + return file_output.public_url_error + return None + + +def _validate_video_inputs( + *, + image_url: Optional[str] = None, + image_file_id: Optional[str] = None, + video_url: Optional[str] = None, + video_file_id: Optional[str] = None, +) -> None: + """Validates mutual exclusion constraints on video input parameters.""" + if image_url is not None and image_file_id is not None: + raise ValueError("Only one of image_url or image_file_id can be set for a request.") + if video_url is not None and video_file_id is not None: + raise ValueError("Only one of video_url or video_file_id can be set for a request.") + def _make_generate_request( prompt: str, model: Union[VideoGenerationModel, str], *, image_url: Optional[str], + image_file_id: Optional[str] = None, video_url: Optional[str], + video_file_id: Optional[str] = None, duration: Optional[int], aspect_ratio: Optional[VideoAspectRatio], resolution: Optional[VideoResolution], reference_image_urls: Optional[Sequence[str]], + reference_image_file_ids: Optional[Sequence[str]] = None, + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> video_pb2.GenerateVideoRequest: + _validate_video_inputs( + image_url=image_url, image_file_id=image_file_id, video_url=video_url, video_file_id=video_file_id + ) + request = video_pb2.GenerateVideoRequest(prompt=prompt, model=model) if image_url is not None: + request.image.CopyFrom(image_pb2.ImageUrlContent(image_url=image_url, detail=image_pb2.ImageDetail.DETAIL_AUTO)) + elif image_file_id is not None: request.image.CopyFrom( - image_pb2.ImageUrlContent( - image_url=image_url, - detail=image_pb2.ImageDetail.DETAIL_AUTO, - ) + image_pb2.ImageUrlContent(file_id=image_file_id, detail=image_pb2.ImageDetail.DETAIL_AUTO) ) if video_url is not None: request.video.CopyFrom(video_pb2.VideoUrlContent(url=video_url)) + elif video_file_id is not None: + request.video.CopyFrom(video_pb2.VideoUrlContent(file_id=video_file_id)) if duration is not None: request.duration = duration if aspect_ratio is not None: request.aspect_ratio = convert_video_aspect_ratio_to_pb(aspect_ratio) if resolution is not None: request.resolution = convert_video_resolution_to_pb(resolution) - if reference_image_urls is not None: + _set_reference_images(request, reference_image_urls, reference_image_file_ids) + if storage_options is not None: + request.storage_options.CopyFrom(_resolve_storage_options_pb(storage_options)) + + return request + + +def _set_reference_images( + request: video_pb2.GenerateVideoRequest, + urls: Optional[Sequence[str]], + file_ids: Optional[Sequence[str]], +) -> None: + """Populates `reference_images` from URL and/or file-ID lists. + + Both lists may be supplied to mix URL/base64 and file-ID references in + the same request. File IDs are appended first so callers can predict the + resulting positional ordering used by the model. + """ + if file_ids is not None: + request.reference_images.extend( + [ + image_pb2.ImageUrlContent( + file_id=fid, + detail=image_pb2.ImageDetail.DETAIL_AUTO, + ) + for fid in file_ids + ] + ) + if urls is not None: request.reference_images.extend( [ image_pb2.ImageUrlContent( image_url=url, detail=image_pb2.ImageDetail.DETAIL_AUTO, ) - for url in reference_image_urls + for url in urls ] ) - return request - def _make_span_request_attributes(request: video_pb2.GenerateVideoRequest) -> dict[str, Any]: """Creates the video generation span request attributes.""" @@ -145,6 +225,15 @@ def _make_span_request_attributes(request: video_pb2.GenerateVideoRequest) -> di attributes["gen_ai.prompt"] = request.prompt + if request.HasField("storage_options"): + attributes["gen_ai.request.storage"] = True + if request.storage_options.filename: + attributes["gen_ai.request.storage.filename"] = request.storage_options.filename + if request.storage_options.expires_after: + attributes["gen_ai.request.storage.expires_after"] = request.storage_options.expires_after + if request.storage_options.HasField("public_url"): + attributes["gen_ai.request.storage.public_url"] = True + if request.HasField("duration"): attributes["gen_ai.request.video.duration"] = request.duration if request.HasField("aspect_ratio"): @@ -183,6 +272,14 @@ def _make_span_response_attributes(request: video_pb2.GenerateVideoRequest, resp if response._video.url: attributes["gen_ai.response.0.video.url"] = response._video.url attributes["gen_ai.response.0.video.duration"] = response.duration + if response.file_output and response.file_output.file_id: + attributes["gen_ai.response.0.video.file_id"] = response.file_output.file_id + if response.public_url: + attributes["gen_ai.response.0.video.public_url"] = response.public_url + if response.public_url_error: + attributes["gen_ai.response.0.video.public_url_error"] = response.public_url_error + if response.storage_error: + attributes["gen_ai.response.0.video.storage_error"] = response.storage_error return attributes @@ -190,18 +287,26 @@ def _make_span_response_attributes(request: video_pb2.GenerateVideoRequest, resp def _make_extend_request( prompt: str, model: Union[VideoGenerationModel, str], - video_url: str, *, + video_url: Optional[str] = None, + video_file_id: Optional[str] = None, duration: Optional[int], + storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> video_pb2.ExtendVideoRequest: - request = video_pb2.ExtendVideoRequest( - prompt=prompt, - model=model, - video=video_pb2.VideoUrlContent(url=video_url), - ) + _validate_video_inputs(video_url=video_url, video_file_id=video_file_id) + if video_url is None and video_file_id is None: + raise ValueError("One of video_url or video_file_id must be set for a request.") + + request = video_pb2.ExtendVideoRequest(prompt=prompt, model=model) + if video_url is not None: + request.video.CopyFrom(video_pb2.VideoUrlContent(url=video_url)) + else: + request.video.CopyFrom(video_pb2.VideoUrlContent(file_id=video_file_id)) if duration is not None: request.duration = duration + if storage_options is not None: + request.storage_options.CopyFrom(_resolve_storage_options_pb(storage_options)) return request @@ -220,6 +325,15 @@ def _make_extend_span_request_attributes(request: video_pb2.ExtendVideoRequest) attributes["gen_ai.prompt"] = request.prompt + if request.HasField("storage_options"): + attributes["gen_ai.request.storage"] = True + if request.storage_options.filename: + attributes["gen_ai.request.storage.filename"] = request.storage_options.filename + if request.storage_options.expires_after: + attributes["gen_ai.request.storage.expires_after"] = request.storage_options.expires_after + if request.storage_options.HasField("public_url"): + attributes["gen_ai.request.storage.public_url"] = True + if request.HasField("duration"): attributes["gen_ai.request.video.duration"] = request.duration @@ -252,6 +366,14 @@ def _make_extend_span_response_attributes( if response._video.url: attributes["gen_ai.response.0.video.url"] = response._video.url attributes["gen_ai.response.0.video.duration"] = response.duration + if response.file_output and response.file_output.file_id: + attributes["gen_ai.response.0.video.file_id"] = response.file_output.file_id + if response.public_url: + attributes["gen_ai.response.0.video.public_url"] = response.public_url + if response.public_url_error: + attributes["gen_ai.response.0.video.public_url_error"] = response.public_url_error + if response.storage_error: + attributes["gen_ai.response.0.video.storage_error"] = response.storage_error return attributes diff --git a/tests/aio/image_test.py b/tests/aio/image_test.py index 6f99d48..33bbed0 100644 --- a/tests/aio/image_test.py +++ b/tests/aio/image_test.py @@ -1,7 +1,9 @@ +import datetime from unittest import mock import pytest import pytest_asyncio +from google.protobuf import timestamp_pb2 from opentelemetry.trace import SpanKind from xai_sdk import AsyncClient @@ -148,7 +150,7 @@ async def test_sample_rejects_both_image_fields(client: AsyncClient): input_image_url = "https://example.com/image.jpg" input_image_urls = ["https://example.com/image1.jpg"] - with pytest.raises(ValueError, match="Only one of image_url or image_urls can be set"): + with pytest.raises(ValueError, match="image_url/image_file_id or image_urls/image_file_ids"): await client.image.sample( prompt="foo", model="grok-imagine-image", @@ -162,7 +164,7 @@ async def test_sample_batch_rejects_both_image_fields(client: AsyncClient): input_image_url = "https://example.com/image.jpg" input_image_urls = ["https://example.com/image1.jpg"] - with pytest.raises(ValueError, match="Only one of image_url or image_urls can be set"): + with pytest.raises(ValueError, match="image_url/image_file_id or image_urls/image_file_ids"): await client.image.sample_batch( prompt="foo", model="grok-imagine-image", @@ -183,7 +185,7 @@ async def test_sample_creates_span_with_correct_attributes( user = "test-user-123" response = await client.image.sample( - prompt="A beautiful sunset", model="grok-2-image", image_format=image_format, user=user + prompt="A beautiful sunset", model="grok-imagine-image", image_format=image_format, user=user ) expected_request_attributes = { @@ -191,20 +193,20 @@ async def test_sample_creates_span_with_correct_attributes( "gen_ai.operation.name": "generate_image", "gen_ai.provider.name": "xai", "gen_ai.output.type": "image", - "gen_ai.request.model": "grok-2-image", + "gen_ai.request.model": "grok-imagine-image", "gen_ai.request.image.format": image_format, "gen_ai.request.image.count": 1, "user_id": user, } mock_tracer.start_as_current_span.assert_called_once_with( - name="image.sample grok-2-image", + name="image.sample grok-imagine-image", kind=SpanKind.CLIENT, attributes=expected_request_attributes, ) expected_response_attributes = { - "gen_ai.response.model": "grok-2-image", + "gen_ai.response.model": "grok-imagine-image", "gen_ai.response.image.format": image_format, "gen_ai.usage.input_tokens": response.usage.prompt_tokens, "gen_ai.usage.output_tokens": response.usage.completion_tokens, @@ -213,7 +215,6 @@ async def test_sample_creates_span_with_correct_attributes( "gen_ai.usage.cached_prompt_text_tokens": response.usage.cached_prompt_text_tokens, "gen_ai.usage.prompt_text_tokens": response.usage.prompt_text_tokens, "gen_ai.usage.prompt_image_tokens": response.usage.prompt_image_tokens, - "gen_ai.response.0.image.up_sampled_prompt": "", "gen_ai.response.0.image.respect_moderation": response.respect_moderation, } @@ -272,7 +273,7 @@ async def test_sample_batch_creates_span_with_correct_attributes( user = "test-user-123" responses = await client.image.sample_batch( - prompt="A beautiful sunset", model="grok-2-image", n=3, image_format=image_format, user=user + prompt="A beautiful sunset", model="grok-imagine-image", n=3, image_format=image_format, user=user ) assert len(responses) == 3 @@ -282,20 +283,20 @@ async def test_sample_batch_creates_span_with_correct_attributes( "gen_ai.operation.name": "generate_image", "gen_ai.provider.name": "xai", "gen_ai.output.type": "image", - "gen_ai.request.model": "grok-2-image", + "gen_ai.request.model": "grok-imagine-image", "gen_ai.request.image.format": image_format, "gen_ai.request.image.count": 3, "user_id": user, } mock_tracer.start_as_current_span.assert_called_once_with( - name="image.sample_batch grok-2-image", + name="image.sample_batch grok-imagine-image", kind=SpanKind.CLIENT, attributes=expected_request_attributes, ) expected_response_attributes = { - "gen_ai.response.model": "grok-2-image", + "gen_ai.response.model": "grok-imagine-image", "gen_ai.response.image.format": image_format, "gen_ai.usage.input_tokens": responses[0].usage.prompt_tokens, "gen_ai.usage.output_tokens": responses[0].usage.completion_tokens, @@ -304,9 +305,6 @@ async def test_sample_batch_creates_span_with_correct_attributes( "gen_ai.usage.cached_prompt_text_tokens": responses[0].usage.cached_prompt_text_tokens, "gen_ai.usage.prompt_text_tokens": responses[0].usage.prompt_text_tokens, "gen_ai.usage.prompt_image_tokens": responses[0].usage.prompt_image_tokens, - "gen_ai.response.0.image.up_sampled_prompt": "", - "gen_ai.response.1.image.up_sampled_prompt": "", - "gen_ai.response.2.image.up_sampled_prompt": "", "gen_ai.response.0.image.respect_moderation": responses[0].respect_moderation, "gen_ai.response.1.image.respect_moderation": responses[1].respect_moderation, "gen_ai.response.2.image.respect_moderation": responses[2].respect_moderation, @@ -361,7 +359,7 @@ async def test_create_with_aspect_ratio_and_resolution(client: AsyncClient): @pytest.mark.asyncio(loop_scope="session") async def test_create_rejects_both_image_fields(client: AsyncClient): """Test that create() rejects both image_url and image_urls.""" - with pytest.raises(ValueError, match="Only one of image_url or image_urls can be set"): + with pytest.raises(ValueError, match="image_url/image_file_id or image_urls/image_file_ids"): client.image.prepare( prompt="foo", model="grok-imagine-image", @@ -384,3 +382,326 @@ def test_image_response_cost_usd_returns_none_when_unset(): usage=usage_pb2.SamplingUsage(), ) assert BaseImageResponse(proto, 0).cost_usd is None + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_passes_image_file_id(client: AsyncClient): + server.clear_last_image_request() + + await client.image.sample(prompt="foo", model="grok-imagine-image", image_file_id="file_abc") + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("image") + assert request.image.file_id == "file_abc" + assert request.image.detail == image_pb2.ImageDetail.DETAIL_AUTO + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_passes_image_file_ids(client: AsyncClient): + server.clear_last_image_request() + + input_file_ids = ["file_abc", "file_def"] + await client.image.sample(prompt="foo", model="grok-imagine-image", image_file_ids=input_file_ids) + + request = server.get_last_image_request() + assert request is not None + assert [image.file_id for image in request.images] == input_file_ids + assert all(image.detail == image_pb2.ImageDetail.DETAIL_AUTO for image in request.images) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_rejects_image_url_and_file_id(client: AsyncClient): + with pytest.raises(ValueError, match="Only one of image_url or image_file_id can be set"): + await client.image.sample( + prompt="foo", + model="grok-imagine-image", + image_url="https://example.com/image.jpg", + image_file_id="file_abc", + ) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_passes_mixed_image_urls_and_file_ids(client: AsyncClient): + """`image_urls` and `image_file_ids` can be combined; file IDs are appended first.""" + server.clear_last_image_request() + + file_ids = ["file_abc", "file_def"] + urls = ["https://example.com/image1.jpg", "https://example.com/image2.jpg"] + await client.image.sample( + prompt="foo", + model="grok-imagine-image", + image_urls=urls, + image_file_ids=file_ids, + ) + + request = server.get_last_image_request() + assert request is not None + assert len(request.images) == 4 + # Documented order: file IDs first, then URLs. + assert request.images[0].file_id == "file_abc" + assert request.images[1].file_id == "file_def" + assert request.images[2].image_url == "https://example.com/image1.jpg" + assert request.images[3].image_url == "https://example.com/image2.jpg" + assert all(image.detail == image_pb2.ImageDetail.DETAIL_AUTO for image in request.images) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_batch_passes_image_file_ids(client: AsyncClient): + server.clear_last_image_request() + + input_file_ids = ["file_abc", "file_def"] + await client.image.sample_batch(prompt="foo", model="grok-imagine-image", n=2, image_file_ids=input_file_ids) + + request = server.get_last_image_request() + assert request is not None + assert [image.file_id for image in request.images] == input_file_ids + assert all(image.detail == image_pb2.ImageDetail.DETAIL_AUTO for image in request.images) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_prepare_passes_image_file_id(client: AsyncClient): + batch_req = client.image.prepare( + prompt="Edit this image", + model="grok-imagine-image", + image_file_id="file_abc", + ) + + assert batch_req.image_request.HasField("image") + assert batch_req.image_request.image.file_id == "file_abc" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_passes_storage_options(client: AsyncClient): + server.clear_last_image_request() + + await client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "my-image.png", "expires_after": 3600}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "my-image.png" + assert request.storage_options.expires_after == 3600 + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_passes_storage_options_timedelta(client: AsyncClient): + server.clear_last_image_request() + + await client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "test.png", "expires_after": datetime.timedelta(hours=1)}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.expires_after == 3600 + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_passes_storage_options_proto(client: AsyncClient): + server.clear_last_image_request() + + proto_opts = image_pb2.StorageOptions(filename="proto.png", expires_after=7200) + await client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options=proto_opts, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "proto.png" + assert request.storage_options.expires_after == 7200 + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_batch_passes_storage_options(client: AsyncClient): + server.clear_last_image_request() + + await client.image.sample_batch( + prompt="foo", + model="grok-imagine-image", + n=2, + storage_options={"filename": "my-image.png", "expires_after": 3600}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "my-image.png" + assert request.storage_options.expires_after == 3600 + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_passes_storage_options_with_filename_only(client: AsyncClient): + server.clear_last_image_request() + + await client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "test.png"}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "test.png" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_omits_storage_options_by_default(client: AsyncClient): + server.clear_last_image_request() + + await client.image.sample(prompt="foo", model="grok-imagine-image") + + request = server.get_last_image_request() + assert request is not None + assert not request.HasField("storage_options") + + +def test_image_response_file_output_properties(): + file_output = image_pb2.FileOutput( + file_id="file-abc123", + filename="my-image.png", + expires_at=timestamp_pb2.Timestamp(seconds=1720000000), + ) + proto = image_pb2.ImageResponse( + images=[image_pb2.GeneratedImage(url="https://example.com/i.png", file_output=file_output)], + ) + response = BaseImageResponse(proto, 0) + assert response.file_output is not None + assert response.file_output.file_id == "file-abc123" + assert response.file_output.filename == "my-image.png" + assert response.file_output.expires_at == timestamp_pb2.Timestamp(seconds=1720000000) + assert response.storage_error is None + + +def test_image_response_no_file_output(): + proto = image_pb2.ImageResponse( + images=[image_pb2.GeneratedImage(url="https://example.com/i.png")], + ) + response = BaseImageResponse(proto, 0) + assert response.file_output is None + assert response.storage_error is None + + +def test_image_response_storage_error(): + proto = image_pb2.ImageResponse( + images=[image_pb2.GeneratedImage(url="https://example.com/i.png", storage_error="quota exceeded")], + ) + response = BaseImageResponse(proto, 0) + assert response.storage_error == "quota exceeded" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_passes_storage_options_with_public_url(client: AsyncClient): + server.clear_last_image_request() + + await client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "my-image.png", "public_url": {"expires_after": 86400}}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "my-image.png" + assert request.storage_options.HasField("public_url") + assert request.storage_options.public_url.expires_after == 86400 + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_passes_storage_options_with_public_url_true(client: AsyncClient): + server.clear_last_image_request() + + await client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "test.png", "public_url": True}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "test.png" + assert request.storage_options.HasField("public_url") + assert not request.storage_options.public_url.HasField("expires_after") + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_passes_storage_options_with_public_url_false(client: AsyncClient): + server.clear_last_image_request() + + await client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "test.png", "public_url": False}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "test.png" + assert not request.storage_options.HasField("public_url") + + +@pytest.mark.asyncio(loop_scope="session") +async def test_sample_passes_storage_options_with_public_url_timedelta(client: AsyncClient): + server.clear_last_image_request() + + await client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "test.png", "public_url": {"expires_after": datetime.timedelta(hours=2)}}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.storage_options.HasField("public_url") + assert request.storage_options.public_url.expires_after == 7200 + + +def test_image_response_public_url_properties(): + file_output = image_pb2.FileOutput( + file_id="file-abc123", + filename="my-image.png", + public_url="https://files-cdn.x.ai/tok/file-abc123.png", + ) + proto = image_pb2.ImageResponse( + images=[image_pb2.GeneratedImage(url="https://example.com/i.png", file_output=file_output)], + ) + response = BaseImageResponse(proto, 0) + assert response.public_url == "https://files-cdn.x.ai/tok/file-abc123.png" + assert response.public_url_error is None + + +def test_image_response_public_url_error(): + file_output = image_pb2.FileOutput( + file_id="file-abc123", + filename="my-image.png", + public_url_error="content type not allowed", + ) + proto = image_pb2.ImageResponse( + images=[image_pb2.GeneratedImage(url="https://example.com/i.png", file_output=file_output)], + ) + response = BaseImageResponse(proto, 0) + assert response.public_url is None + assert response.public_url_error == "content type not allowed" + + +def test_image_response_public_url_none_when_no_file_output(): + proto = image_pb2.ImageResponse( + images=[image_pb2.GeneratedImage(url="https://example.com/i.png")], + ) + response = BaseImageResponse(proto, 0) + assert response.public_url is None + assert response.public_url_error is None diff --git a/tests/aio/video_test.py b/tests/aio/video_test.py index 81e30be..ded74c7 100644 --- a/tests/aio/video_test.py +++ b/tests/aio/video_test.py @@ -3,6 +3,7 @@ import pytest import pytest_asyncio +from google.protobuf import timestamp_pb2 from opentelemetry.trace import SpanKind from xai_sdk import AsyncClient @@ -581,3 +582,435 @@ def test_video_response_cost_usd_returns_none_when_unset(): usage=usage_pb2.SamplingUsage(), ) assert VideoResponse(proto).cost_usd is None + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_passes_image_file_id(client: AsyncClient): + server.clear_last_video_request() + + await client.video.generate(prompt="foo", model="grok-imagine-video", image_file_id="file_abc") + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("image") + assert request.image.file_id == "file_abc" + assert request.image.detail == image_pb2.ImageDetail.DETAIL_AUTO + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_passes_video_file_id(client: AsyncClient): + server.clear_last_video_request() + + await client.video.generate(prompt="foo", model="grok-imagine-video", video_file_id="file_abc") + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("video") + assert request.video.file_id == "file_abc" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_passes_reference_image_file_ids(client: AsyncClient): + server.clear_last_video_request() + + ref_ids = ["file_abc", "file_def"] + await client.video.generate(prompt="foo", model="grok-imagine-video", reference_image_file_ids=ref_ids) + + request = server.get_last_video_request() + assert request is not None + assert len(request.reference_images) == 2 + assert request.reference_images[0].file_id == ref_ids[0] + assert request.reference_images[0].detail == image_pb2.ImageDetail.DETAIL_AUTO + assert request.reference_images[1].file_id == ref_ids[1] + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_rejects_image_url_and_file_id(client: AsyncClient): + with pytest.raises(ValueError, match="Only one of image_url or image_file_id can be set"): + await client.video.generate( + prompt="foo", + model="grok-imagine-video", + image_url="https://example.com/image.jpg", + image_file_id="file_abc", + ) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_rejects_video_url_and_file_id(client: AsyncClient): + with pytest.raises(ValueError, match="Only one of video_url or video_file_id can be set"): + await client.video.generate( + prompt="foo", + model="grok-imagine-video", + video_url="https://example.com/video.mp4", + video_file_id="file_abc", + ) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_passes_mixed_reference_image_urls_and_file_ids(client: AsyncClient): + """`reference_image_urls` and `reference_image_file_ids` can be combined; file IDs first.""" + server.clear_last_video_request() + + file_ids = ["file_abc", "file_def"] + urls = ["https://example.com/ref1.jpg", "https://example.com/ref2.jpg"] + await client.video.generate( + prompt="foo", + model="grok-imagine-video", + reference_image_urls=urls, + reference_image_file_ids=file_ids, + ) + + request = server.get_last_video_request() + assert request is not None + assert len(request.reference_images) == 4 + assert request.reference_images[0].file_id == "file_abc" + assert request.reference_images[1].file_id == "file_def" + assert request.reference_images[2].image_url == "https://example.com/ref1.jpg" + assert request.reference_images[3].image_url == "https://example.com/ref2.jpg" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_extend_passes_video_file_id(client: AsyncClient): + server.clear_last_extend_video_request() + + await client.video.extend( + prompt="Continue the scene", + model="grok-imagine-video", + video_file_id="file_abc", + ) + + request = server.get_last_extend_video_request() + assert request is not None + assert request.HasField("video") + assert request.video.file_id == "file_abc" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_extend_rejects_video_url_and_file_id(client: AsyncClient): + with pytest.raises(ValueError, match="Only one of video_url or video_file_id can be set"): + await client.video.extend( + prompt="foo", + model="grok-imagine-video", + video_url="https://example.com/input.mp4", + video_file_id="file_abc", + ) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_extend_rejects_neither_video_url_nor_file_id(client: AsyncClient): + with pytest.raises(ValueError, match="One of video_url or video_file_id must be set"): + await client.video.extend( + prompt="foo", + model="grok-imagine-video", + ) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_extend_start_passes_video_file_id(client: AsyncClient): + server.clear_last_extend_video_request() + + await client.video.extend_start( + prompt="Continue the scene", + model="grok-imagine-video", + video_file_id="file_abc", + ) + + request = server.get_last_extend_video_request() + assert request is not None + assert request.HasField("video") + assert request.video.file_id == "file_abc" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_extend_start_rejects_video_url_and_file_id(client: AsyncClient): + with pytest.raises(ValueError, match="Only one of video_url or video_file_id can be set"): + await client.video.extend_start( + prompt="foo", + model="grok-imagine-video", + video_url="https://example.com/input.mp4", + video_file_id="file_abc", + ) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_extend_start_rejects_neither_video_url_nor_file_id(client: AsyncClient): + with pytest.raises(ValueError, match="One of video_url or video_file_id must be set"): + await client.video.extend_start( + prompt="foo", + model="grok-imagine-video", + ) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_prepare_passes_image_file_id(client: AsyncClient): + batch_req = client.video.prepare( + prompt="Animate this image", + model="grok-imagine-video", + image_file_id="file_abc", + ) + + assert batch_req.video_request.HasField("image") + assert batch_req.video_request.image.file_id == "file_abc" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_prepare_passes_video_file_id(client: AsyncClient): + batch_req = client.video.prepare( + prompt="Edit this video", + model="grok-imagine-video", + video_file_id="file_abc", + ) + + assert batch_req.video_request.HasField("video") + assert batch_req.video_request.video.file_id == "file_abc" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_prepare_passes_reference_image_file_ids(client: AsyncClient): + ref_ids = ["file_abc", "file_def"] + batch_req = client.video.prepare( + prompt="Generate from references", + model="grok-imagine-video", + reference_image_file_ids=ref_ids, + ) + + assert len(batch_req.video_request.reference_images) == 2 + assert batch_req.video_request.reference_images[0].file_id == ref_ids[0] + assert batch_req.video_request.reference_images[1].file_id == ref_ids[1] + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_passes_storage_options(client: AsyncClient): + server.clear_last_video_request() + + await client.video.generate( + prompt="foo", + model="grok-imagine-video", + storage_options={"filename": "my-video.mp4", "expires_after": 7200}, + ) + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "my-video.mp4" + assert request.storage_options.expires_after == 7200 + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_passes_storage_options_with_filename_only(client: AsyncClient): + server.clear_last_video_request() + + await client.video.generate( + prompt="foo", + model="grok-imagine-video", + storage_options={"filename": "test.mp4"}, + ) + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "test.mp4" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_omits_storage_options_by_default(client: AsyncClient): + server.clear_last_video_request() + + await client.video.generate(prompt="foo", model="grok-imagine-video") + + request = server.get_last_video_request() + assert request is not None + assert not request.HasField("storage_options") + + +@pytest.mark.asyncio(loop_scope="session") +async def test_extend_passes_storage_options(client: AsyncClient): + server.clear_last_extend_video_request() + + await client.video.extend( + prompt="Continue", + model="grok-imagine-video", + video_url="https://example.com/input.mp4", + storage_options={"filename": "extended.mp4"}, + ) + + request = server.get_last_extend_video_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "extended.mp4" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_prepare_passes_storage_options(client: AsyncClient): + batch_req = client.video.prepare( + prompt="Generate video", + model="grok-imagine-video", + storage_options={"filename": "batch.mp4"}, + ) + + assert batch_req.video_request.HasField("storage_options") + assert batch_req.video_request.storage_options.filename == "batch.mp4" + + +def test_video_response_file_output_properties(): + file_output = image_pb2.FileOutput( + file_id="file-xyz789", + filename="my-video.mp4", + expires_at=timestamp_pb2.Timestamp(seconds=1720000000), + ) + proto = video_pb2.VideoResponse( + video=video_pb2.GeneratedVideo(url="https://example.com/v.mp4", duration=5, file_output=file_output), + ) + response = VideoResponse(proto) + assert response.file_output is not None + assert response.file_output.file_id == "file-xyz789" + assert response.file_output.filename == "my-video.mp4" + assert response.file_output.expires_at == timestamp_pb2.Timestamp(seconds=1720000000) + assert response.storage_error is None + + +def test_video_response_no_file_output(): + proto = video_pb2.VideoResponse( + video=video_pb2.GeneratedVideo(url="https://example.com/v.mp4", duration=5), + ) + response = VideoResponse(proto) + assert response.file_output is None + assert response.storage_error is None + + +def test_video_response_storage_error(): + proto = video_pb2.VideoResponse( + video=video_pb2.GeneratedVideo(url="https://example.com/v.mp4", duration=5, storage_error="quota exceeded"), + ) + response = VideoResponse(proto) + assert response.storage_error == "quota exceeded" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_passes_storage_options_with_public_url(client: AsyncClient): + server.clear_last_video_request() + + await client.video.generate( + prompt="foo", + model="grok-imagine-video", + storage_options={"filename": "my-video.mp4", "public_url": {"expires_after": 86400}}, + ) + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "my-video.mp4" + assert request.storage_options.HasField("public_url") + assert request.storage_options.public_url.expires_after == 86400 + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_passes_storage_options_with_public_url_true(client: AsyncClient): + server.clear_last_video_request() + + await client.video.generate( + prompt="foo", + model="grok-imagine-video", + storage_options={"filename": "test.mp4", "public_url": True}, + ) + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "test.mp4" + assert request.storage_options.HasField("public_url") + assert not request.storage_options.public_url.HasField("expires_after") + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_passes_storage_options_with_public_url_false(client: AsyncClient): + server.clear_last_video_request() + + await client.video.generate( + prompt="foo", + model="grok-imagine-video", + storage_options={"filename": "test.mp4", "public_url": False}, + ) + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "test.mp4" + assert not request.storage_options.HasField("public_url") + + +def test_video_response_public_url_properties(): + file_output = image_pb2.FileOutput( + file_id="file-xyz789", + filename="my-video.mp4", + public_url="https://files-cdn.x.ai/tok/file-xyz789.mp4", + ) + proto = video_pb2.VideoResponse( + video=video_pb2.GeneratedVideo(url="https://example.com/v.mp4", duration=5, file_output=file_output), + ) + response = VideoResponse(proto) + assert response.public_url == "https://files-cdn.x.ai/tok/file-xyz789.mp4" + assert response.public_url_error is None + + +def test_video_response_public_url_error(): + file_output = image_pb2.FileOutput( + file_id="file-xyz789", + filename="my-video.mp4", + public_url_error="content type not allowed", + ) + proto = video_pb2.VideoResponse( + video=video_pb2.GeneratedVideo(url="https://example.com/v.mp4", duration=5, file_output=file_output), + ) + response = VideoResponse(proto) + assert response.public_url is None + assert response.public_url_error == "content type not allowed" + + +def test_video_response_public_url_none_when_no_file_output(): + proto = video_pb2.VideoResponse( + video=video_pb2.GeneratedVideo(url="https://example.com/v.mp4", duration=5), + ) + response = VideoResponse(proto) + assert response.public_url is None + assert response.public_url_error is None + + +@pytest.mark.asyncio(loop_scope="session") +async def test_prepare_extension_with_video_file_id(client: AsyncClient): + """Test that prepare_extension() accepts a video_file_id.""" + batch_req = client.video.prepare_extension( + prompt="Continue the scene", + model="grok-imagine-video", + video_file_id="file_abc", + ) + + assert batch_req.video_extension_request.video.file_id == "file_abc" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_prepare_extension_rejects_video_url_and_file_id(client: AsyncClient): + """Test that prepare_extension() rejects setting both video_url and video_file_id.""" + with pytest.raises(ValueError, match="Only one of video_url or video_file_id can be set"): + client.video.prepare_extension( + prompt="foo", + model="grok-imagine-video", + video_url="https://example.com/input.mp4", + video_file_id="file_abc", + ) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_prepare_extension_passes_storage_options(client: AsyncClient): + """Test that prepare_extension() passes storage_options through to the request.""" + batch_req = client.video.prepare_extension( + prompt="Continue the scene", + model="grok-imagine-video", + video_url="https://example.com/input.mp4", + storage_options={"filename": "output.mp4", "public_url": True}, + ) + + request = batch_req.video_extension_request + assert request.HasField("storage_options") + assert request.storage_options.filename == "output.mp4" + assert request.storage_options.HasField("public_url") diff --git a/tests/sync/files_test.py b/tests/sync/files_test.py index 52a912d..7d6e669 100644 --- a/tests/sync/files_test.py +++ b/tests/sync/files_test.py @@ -7,6 +7,7 @@ from typing import Callable, Iterable, Optional from unittest import mock +import pydantic import pytest from google.protobuf import timestamp_pb2 from opentelemetry.trace import SpanKind @@ -16,9 +17,10 @@ _chunk_file_data, _chunk_file_from_path, _order_to_pb, + _resolve_storage_options_pb, _sort_by_to_pb, ) -from xai_sdk.proto import files_pb2 +from xai_sdk.proto import files_pb2, image_pb2 def _extract_file_index_from_chunks(chunks: Iterable[files_pb2.UploadFileChunk]) -> int: @@ -395,6 +397,64 @@ def test_sort_by_conversion(): assert _sort_by_to_pb(None) == files_pb2.FilesSortBy.FILES_SORT_BY_CREATED_AT +def test_resolve_storage_options_pb_proto_passthrough(): + """A StorageOptions proto is returned unchanged.""" + proto = image_pb2.StorageOptions(filename="a.png", expires_after=10) + assert _resolve_storage_options_pb(proto) is proto + + +def test_resolve_storage_options_pb_filename_only(): + """Only filename set; no expiry, no public URL.""" + pb = _resolve_storage_options_pb({"filename": "a.png"}) + assert pb.filename == "a.png" + assert pb.expires_after == 0 + assert not pb.HasField("public_url") + + +def test_resolve_storage_options_pb_expires_after_int_and_timedelta(): + """expires_after accepts both int seconds and timedelta.""" + assert _resolve_storage_options_pb({"filename": "a.png", "expires_after": 3600}).expires_after == 3600 + pb = _resolve_storage_options_pb({"filename": "a.png", "expires_after": datetime.timedelta(hours=1)}) + assert pb.expires_after == 3600 + + +def test_resolve_storage_options_pb_public_url_true(): + """public_url=True creates a public URL with no independent expiry.""" + pb = _resolve_storage_options_pb({"filename": "a.png", "public_url": True}) + assert pb.HasField("public_url") + assert not pb.public_url.HasField("expires_after") + + +def test_resolve_storage_options_pb_public_url_false(): + """public_url=False stores privately (no public URL).""" + pb = _resolve_storage_options_pb({"filename": "a.png", "public_url": False}) + assert not pb.HasField("public_url") + + +def test_resolve_storage_options_pb_public_url_dict(): + """public_url as a dict sets an independent expiry (int or timedelta).""" + pb = _resolve_storage_options_pb({"filename": "a.png", "public_url": {"expires_after": 86400}}) + assert pb.public_url.expires_after == 86400 + pb = _resolve_storage_options_pb( + {"filename": "a.png", "public_url": {"expires_after": datetime.timedelta(hours=2)}} + ) + assert pb.public_url.expires_after == 7200 + + +def test_resolve_storage_options_pb_missing_filename_raises(): + """A missing required filename raises a clear ValidationError, not KeyError.""" + with pytest.raises(pydantic.ValidationError): + _resolve_storage_options_pb({"expires_after": 3600}) # type: ignore[typeddict-item] + + +def test_resolve_storage_options_pb_invalid_types_raise(): + """Wrong field types are rejected at runtime.""" + with pytest.raises(pydantic.ValidationError): + _resolve_storage_options_pb({"filename": 123}) # type: ignore[typeddict-item] + with pytest.raises(pydantic.ValidationError): + _resolve_storage_options_pb({"filename": "a.png", "public_url": ["nope"]}) # type: ignore[typeddict-item] + + def test_chunk_file_data(): """Test file data chunking.""" data = b"A" * (2 * 1024 * 1024) # 2 MiB of data diff --git a/tests/sync/image_test.py b/tests/sync/image_test.py index 94f776a..b26d50d 100644 --- a/tests/sync/image_test.py +++ b/tests/sync/image_test.py @@ -1,6 +1,8 @@ +import datetime from unittest import mock import pytest +from google.protobuf import timestamp_pb2 from opentelemetry.trace import SpanKind from xai_sdk import Client @@ -133,7 +135,7 @@ def test_sample_rejects_both_image_fields(client: Client): input_image_url = "https://example.com/image.jpg" input_image_urls = ["https://example.com/image1.jpg"] - with pytest.raises(ValueError, match="Only one of image_url or image_urls can be set"): + with pytest.raises(ValueError, match="image_url/image_file_id or image_urls/image_file_ids"): client.image.sample( prompt="foo", model="grok-imagine-image", @@ -146,7 +148,7 @@ def test_sample_batch_rejects_both_image_fields(client: Client): input_image_url = "https://example.com/image.jpg" input_image_urls = ["https://example.com/image1.jpg"] - with pytest.raises(ValueError, match="Only one of image_url or image_urls can be set"): + with pytest.raises(ValueError, match="image_url/image_file_id or image_urls/image_file_ids"): client.image.sample_batch( prompt="foo", model="grok-imagine-image", @@ -166,7 +168,7 @@ def test_sample_creates_span_with_correct_attributes( user = "test-user-123" response = client.image.sample( - prompt="A beautiful sunset", model="grok-2-image", image_format=image_format, user=user + prompt="A beautiful sunset", model="grok-imagine-image", image_format=image_format, user=user ) expected_request_attributes = { @@ -174,20 +176,20 @@ def test_sample_creates_span_with_correct_attributes( "gen_ai.operation.name": "generate_image", "gen_ai.provider.name": "xai", "gen_ai.output.type": "image", - "gen_ai.request.model": "grok-2-image", + "gen_ai.request.model": "grok-imagine-image", "gen_ai.request.image.format": image_format, "gen_ai.request.image.count": 1, "user_id": user, } mock_tracer.start_as_current_span.assert_called_once_with( - name="image.sample grok-2-image", + name="image.sample grok-imagine-image", kind=SpanKind.CLIENT, attributes=expected_request_attributes, ) expected_response_attributes = { - "gen_ai.response.model": "grok-2-image", + "gen_ai.response.model": "grok-imagine-image", "gen_ai.response.image.format": image_format, "gen_ai.usage.input_tokens": response.usage.prompt_tokens, "gen_ai.usage.output_tokens": response.usage.completion_tokens, @@ -196,7 +198,6 @@ def test_sample_creates_span_with_correct_attributes( "gen_ai.usage.cached_prompt_text_tokens": response.usage.cached_prompt_text_tokens, "gen_ai.usage.prompt_text_tokens": response.usage.prompt_text_tokens, "gen_ai.usage.prompt_image_tokens": response.usage.prompt_image_tokens, - "gen_ai.response.0.image.up_sampled_prompt": "", "gen_ai.response.0.image.respect_moderation": response.respect_moderation, } @@ -251,7 +252,7 @@ def test_sample_batch_creates_span_with_correct_attributes( user = "test-user-123" responses = client.image.sample_batch( - prompt="A beautiful sunset", model="grok-2-image", n=3, image_format=image_format, user=user + prompt="A beautiful sunset", model="grok-imagine-image", n=3, image_format=image_format, user=user ) assert len(responses) == 3 @@ -261,20 +262,20 @@ def test_sample_batch_creates_span_with_correct_attributes( "gen_ai.operation.name": "generate_image", "gen_ai.provider.name": "xai", "gen_ai.output.type": "image", - "gen_ai.request.model": "grok-2-image", + "gen_ai.request.model": "grok-imagine-image", "gen_ai.request.image.format": image_format, "gen_ai.request.image.count": 3, "user_id": user, } mock_tracer.start_as_current_span.assert_called_once_with( - name="image.sample_batch grok-2-image", + name="image.sample_batch grok-imagine-image", kind=SpanKind.CLIENT, attributes=expected_request_attributes, ) expected_response_attributes = { - "gen_ai.response.model": "grok-2-image", + "gen_ai.response.model": "grok-imagine-image", "gen_ai.response.image.format": image_format, "gen_ai.usage.input_tokens": responses[0].usage.prompt_tokens, "gen_ai.usage.output_tokens": responses[0].usage.completion_tokens, @@ -283,9 +284,6 @@ def test_sample_batch_creates_span_with_correct_attributes( "gen_ai.usage.cached_prompt_text_tokens": responses[0].usage.cached_prompt_text_tokens, "gen_ai.usage.prompt_text_tokens": responses[0].usage.prompt_text_tokens, "gen_ai.usage.prompt_image_tokens": responses[0].usage.prompt_image_tokens, - "gen_ai.response.0.image.up_sampled_prompt": "", - "gen_ai.response.1.image.up_sampled_prompt": "", - "gen_ai.response.2.image.up_sampled_prompt": "", "gen_ai.response.0.image.respect_moderation": responses[0].respect_moderation, "gen_ai.response.1.image.respect_moderation": responses[1].respect_moderation, "gen_ai.response.2.image.respect_moderation": responses[2].respect_moderation, @@ -362,7 +360,7 @@ def test_create_with_image_url(client: Client): def test_create_rejects_both_image_fields(client: Client): """Test that create() rejects both image_url and image_urls.""" - with pytest.raises(ValueError, match="Only one of image_url or image_urls can be set"): + with pytest.raises(ValueError, match="image_url/image_file_id or image_urls/image_file_ids"): client.image.prepare( prompt="foo", model="grok-imagine-image", @@ -385,3 +383,310 @@ def test_image_response_cost_usd_returns_none_when_unset(): usage=usage_pb2.SamplingUsage(), ) assert BaseImageResponse(proto, 0).cost_usd is None + + +def test_sample_passes_image_file_id(client: Client): + server.clear_last_image_request() + + client.image.sample(prompt="foo", model="grok-imagine-image", image_file_id="file_abc") + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("image") + assert request.image.file_id == "file_abc" + assert request.image.detail == image_pb2.ImageDetail.DETAIL_AUTO + + +def test_sample_passes_image_file_ids(client: Client): + server.clear_last_image_request() + + input_file_ids = ["file_abc", "file_def"] + client.image.sample(prompt="foo", model="grok-imagine-image", image_file_ids=input_file_ids) + + request = server.get_last_image_request() + assert request is not None + assert [image.file_id for image in request.images] == input_file_ids + assert all(image.detail == image_pb2.ImageDetail.DETAIL_AUTO for image in request.images) + + +def test_sample_rejects_image_url_and_file_id(client: Client): + with pytest.raises(ValueError, match="Only one of image_url or image_file_id can be set"): + client.image.sample( + prompt="foo", + model="grok-imagine-image", + image_url="https://example.com/image.jpg", + image_file_id="file_abc", + ) + + +def test_sample_passes_mixed_image_urls_and_file_ids(client: Client): + """`image_urls` and `image_file_ids` can be combined; file IDs are appended first.""" + server.clear_last_image_request() + + file_ids = ["file_abc", "file_def"] + urls = ["https://example.com/image1.jpg", "https://example.com/image2.jpg"] + client.image.sample( + prompt="foo", + model="grok-imagine-image", + image_urls=urls, + image_file_ids=file_ids, + ) + + request = server.get_last_image_request() + assert request is not None + assert len(request.images) == 4 + # Documented order: file IDs first, then URLs. + assert request.images[0].file_id == "file_abc" + assert request.images[1].file_id == "file_def" + assert request.images[2].image_url == "https://example.com/image1.jpg" + assert request.images[3].image_url == "https://example.com/image2.jpg" + assert all(image.detail == image_pb2.ImageDetail.DETAIL_AUTO for image in request.images) + + +def test_sample_batch_passes_image_file_ids(client: Client): + server.clear_last_image_request() + + input_file_ids = ["file_abc", "file_def"] + client.image.sample_batch(prompt="foo", model="grok-imagine-image", n=2, image_file_ids=input_file_ids) + + request = server.get_last_image_request() + assert request is not None + assert [image.file_id for image in request.images] == input_file_ids + assert all(image.detail == image_pb2.ImageDetail.DETAIL_AUTO for image in request.images) + + +def test_prepare_passes_image_file_id(client: Client): + batch_req = client.image.prepare( + prompt="Edit this image", + model="grok-imagine-image", + image_file_id="file_abc", + ) + + assert batch_req.image_request.HasField("image") + assert batch_req.image_request.image.file_id == "file_abc" + + +def test_sample_passes_storage_options(client: Client): + server.clear_last_image_request() + + client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "my-image.png", "expires_after": 3600}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "my-image.png" + assert request.storage_options.expires_after == 3600 + + +def test_sample_passes_storage_options_timedelta(client: Client): + server.clear_last_image_request() + + client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "test.png", "expires_after": datetime.timedelta(hours=1)}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.expires_after == 3600 + + +def test_sample_passes_storage_options_proto(client: Client): + server.clear_last_image_request() + + proto_opts = image_pb2.StorageOptions(filename="proto.png", expires_after=7200) + client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options=proto_opts, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "proto.png" + assert request.storage_options.expires_after == 7200 + + +def test_sample_batch_passes_storage_options(client: Client): + server.clear_last_image_request() + + client.image.sample_batch( + prompt="foo", + model="grok-imagine-image", + n=2, + storage_options={"filename": "my-image.png", "expires_after": 3600}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "my-image.png" + assert request.storage_options.expires_after == 3600 + + +def test_sample_passes_storage_options_with_filename_only(client: Client): + server.clear_last_image_request() + + client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "test.png"}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "test.png" + + +def test_sample_omits_storage_options_by_default(client: Client): + server.clear_last_image_request() + + client.image.sample(prompt="foo", model="grok-imagine-image") + + request = server.get_last_image_request() + assert request is not None + assert not request.HasField("storage_options") + + +def test_image_response_file_output_properties(): + file_output = image_pb2.FileOutput( + file_id="file-abc123", + filename="my-image.png", + expires_at=timestamp_pb2.Timestamp(seconds=1720000000), + ) + proto = image_pb2.ImageResponse( + images=[image_pb2.GeneratedImage(url="https://example.com/i.png", file_output=file_output)], + ) + response = BaseImageResponse(proto, 0) + assert response.file_output is not None + assert response.file_output.file_id == "file-abc123" + assert response.file_output.filename == "my-image.png" + assert response.file_output.expires_at == timestamp_pb2.Timestamp(seconds=1720000000) + assert response.storage_error is None + + +def test_image_response_no_file_output(): + proto = image_pb2.ImageResponse( + images=[image_pb2.GeneratedImage(url="https://example.com/i.png")], + ) + response = BaseImageResponse(proto, 0) + assert response.file_output is None + assert response.storage_error is None + + +def test_image_response_storage_error(): + proto = image_pb2.ImageResponse( + images=[image_pb2.GeneratedImage(url="https://example.com/i.png", storage_error="quota exceeded")], + ) + response = BaseImageResponse(proto, 0) + assert response.storage_error == "quota exceeded" + + +def test_sample_passes_storage_options_with_public_url(client: Client): + server.clear_last_image_request() + + client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "my-image.png", "public_url": {"expires_after": 86400}}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "my-image.png" + assert request.storage_options.HasField("public_url") + assert request.storage_options.public_url.expires_after == 86400 + + +def test_sample_passes_storage_options_with_public_url_true(client: Client): + server.clear_last_image_request() + + client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "test.png", "public_url": True}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "test.png" + assert request.storage_options.HasField("public_url") + assert not request.storage_options.public_url.HasField("expires_after") + + +def test_sample_passes_storage_options_with_public_url_false(client: Client): + server.clear_last_image_request() + + client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "test.png", "public_url": False}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "test.png" + assert not request.storage_options.HasField("public_url") + + +def test_sample_passes_storage_options_with_public_url_timedelta(client: Client): + server.clear_last_image_request() + + client.image.sample( + prompt="foo", + model="grok-imagine-image", + storage_options={"filename": "test.png", "public_url": {"expires_after": datetime.timedelta(hours=2)}}, + ) + + request = server.get_last_image_request() + assert request is not None + assert request.storage_options.HasField("public_url") + assert request.storage_options.public_url.expires_after == 7200 + + +def test_image_response_public_url_properties(): + file_output = image_pb2.FileOutput( + file_id="file-abc123", + filename="my-image.png", + public_url="https://files-cdn.x.ai/tok/file-abc123.png", + ) + proto = image_pb2.ImageResponse( + images=[image_pb2.GeneratedImage(url="https://example.com/i.png", file_output=file_output)], + ) + response = BaseImageResponse(proto, 0) + assert response.public_url == "https://files-cdn.x.ai/tok/file-abc123.png" + assert response.public_url_error is None + + +def test_image_response_public_url_error(): + file_output = image_pb2.FileOutput( + file_id="file-abc123", + filename="my-image.png", + public_url_error="content type not allowed", + ) + proto = image_pb2.ImageResponse( + images=[image_pb2.GeneratedImage(url="https://example.com/i.png", file_output=file_output)], + ) + response = BaseImageResponse(proto, 0) + assert response.public_url is None + assert response.public_url_error == "content type not allowed" + + +def test_image_response_public_url_none_when_no_file_output(): + proto = image_pb2.ImageResponse( + images=[image_pb2.GeneratedImage(url="https://example.com/i.png")], + ) + response = BaseImageResponse(proto, 0) + assert response.public_url is None + assert response.public_url_error is None diff --git a/tests/sync/video_test.py b/tests/sync/video_test.py index ce5baae..e2a6533 100644 --- a/tests/sync/video_test.py +++ b/tests/sync/video_test.py @@ -2,6 +2,7 @@ from unittest import mock import pytest +from google.protobuf import timestamp_pb2 from opentelemetry.trace import SpanKind from xai_sdk import Client @@ -556,3 +557,410 @@ def test_video_response_cost_usd_returns_none_when_unset(): usage=usage_pb2.SamplingUsage(), ) assert VideoResponse(proto).cost_usd is None + + +def test_generate_passes_image_file_id(client: Client): + server.clear_last_video_request() + + client.video.generate(prompt="foo", model="grok-imagine-video", image_file_id="file_abc") + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("image") + assert request.image.file_id == "file_abc" + assert request.image.detail == image_pb2.ImageDetail.DETAIL_AUTO + + +def test_generate_passes_video_file_id(client: Client): + server.clear_last_video_request() + + client.video.generate(prompt="foo", model="grok-imagine-video", video_file_id="file_abc") + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("video") + assert request.video.file_id == "file_abc" + + +def test_generate_passes_reference_image_file_ids(client: Client): + server.clear_last_video_request() + + ref_ids = ["file_abc", "file_def"] + client.video.generate(prompt="foo", model="grok-imagine-video", reference_image_file_ids=ref_ids) + + request = server.get_last_video_request() + assert request is not None + assert len(request.reference_images) == 2 + assert request.reference_images[0].file_id == ref_ids[0] + assert request.reference_images[0].detail == image_pb2.ImageDetail.DETAIL_AUTO + assert request.reference_images[1].file_id == ref_ids[1] + assert request.reference_images[1].detail == image_pb2.ImageDetail.DETAIL_AUTO + + +def test_generate_rejects_image_url_and_file_id(client: Client): + with pytest.raises(ValueError, match="Only one of image_url or image_file_id can be set"): + client.video.generate( + prompt="foo", + model="grok-imagine-video", + image_url="https://example.com/image.jpg", + image_file_id="file_abc", + ) + + +def test_generate_rejects_video_url_and_file_id(client: Client): + with pytest.raises(ValueError, match="Only one of video_url or video_file_id can be set"): + client.video.generate( + prompt="foo", + model="grok-imagine-video", + video_url="https://example.com/video.mp4", + video_file_id="file_abc", + ) + + +def test_generate_passes_mixed_reference_image_urls_and_file_ids(client: Client): + """`reference_image_urls` and `reference_image_file_ids` can be combined; file IDs first.""" + server.clear_last_video_request() + + file_ids = ["file_abc", "file_def"] + urls = ["https://example.com/ref1.jpg", "https://example.com/ref2.jpg"] + client.video.generate( + prompt="foo", + model="grok-imagine-video", + reference_image_urls=urls, + reference_image_file_ids=file_ids, + ) + + request = server.get_last_video_request() + assert request is not None + assert len(request.reference_images) == 4 + assert request.reference_images[0].file_id == "file_abc" + assert request.reference_images[1].file_id == "file_def" + assert request.reference_images[2].image_url == "https://example.com/ref1.jpg" + assert request.reference_images[3].image_url == "https://example.com/ref2.jpg" + + +def test_extend_passes_video_file_id(client: Client): + server.clear_last_extend_video_request() + + client.video.extend( + prompt="Continue the scene", + model="grok-imagine-video", + video_file_id="file_abc", + ) + + request = server.get_last_extend_video_request() + assert request is not None + assert request.HasField("video") + assert request.video.file_id == "file_abc" + + +def test_extend_rejects_video_url_and_file_id(client: Client): + with pytest.raises(ValueError, match="Only one of video_url or video_file_id can be set"): + client.video.extend( + prompt="foo", + model="grok-imagine-video", + video_url="https://example.com/input.mp4", + video_file_id="file_abc", + ) + + +def test_extend_rejects_neither_video_url_nor_file_id(client: Client): + with pytest.raises(ValueError, match="One of video_url or video_file_id must be set"): + client.video.extend( + prompt="foo", + model="grok-imagine-video", + ) + + +def test_extend_start_passes_video_file_id(client: Client): + server.clear_last_extend_video_request() + + client.video.extend_start( + prompt="Continue the scene", + model="grok-imagine-video", + video_file_id="file_abc", + ) + + request = server.get_last_extend_video_request() + assert request is not None + assert request.HasField("video") + assert request.video.file_id == "file_abc" + + +def test_extend_start_rejects_video_url_and_file_id(client: Client): + with pytest.raises(ValueError, match="Only one of video_url or video_file_id can be set"): + client.video.extend_start( + prompt="foo", + model="grok-imagine-video", + video_url="https://example.com/input.mp4", + video_file_id="file_abc", + ) + + +def test_extend_start_rejects_neither_video_url_nor_file_id(client: Client): + with pytest.raises(ValueError, match="One of video_url or video_file_id must be set"): + client.video.extend_start( + prompt="foo", + model="grok-imagine-video", + ) + + +def test_prepare_passes_image_file_id(client: Client): + batch_req = client.video.prepare( + prompt="Animate this image", + model="grok-imagine-video", + image_file_id="file_abc", + ) + + assert batch_req.video_request.HasField("image") + assert batch_req.video_request.image.file_id == "file_abc" + + +def test_prepare_passes_video_file_id(client: Client): + batch_req = client.video.prepare( + prompt="Edit this video", + model="grok-imagine-video", + video_file_id="file_abc", + ) + + assert batch_req.video_request.HasField("video") + assert batch_req.video_request.video.file_id == "file_abc" + + +def test_prepare_passes_reference_image_file_ids(client: Client): + ref_ids = ["file_abc", "file_def"] + batch_req = client.video.prepare( + prompt="Generate from references", + model="grok-imagine-video", + reference_image_file_ids=ref_ids, + ) + + assert len(batch_req.video_request.reference_images) == 2 + assert batch_req.video_request.reference_images[0].file_id == ref_ids[0] + assert batch_req.video_request.reference_images[1].file_id == ref_ids[1] + + +def test_generate_passes_storage_options(client: Client): + server.clear_last_video_request() + + client.video.generate( + prompt="foo", + model="grok-imagine-video", + storage_options={"filename": "my-video.mp4", "expires_after": 7200}, + ) + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "my-video.mp4" + assert request.storage_options.expires_after == 7200 + + +def test_generate_passes_storage_options_with_filename_only(client: Client): + server.clear_last_video_request() + + client.video.generate( + prompt="foo", + model="grok-imagine-video", + storage_options={"filename": "test.mp4"}, + ) + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "test.mp4" + + +def test_generate_omits_storage_options_by_default(client: Client): + server.clear_last_video_request() + + client.video.generate(prompt="foo", model="grok-imagine-video") + + request = server.get_last_video_request() + assert request is not None + assert not request.HasField("storage_options") + + +def test_extend_passes_storage_options(client: Client): + server.clear_last_extend_video_request() + + client.video.extend( + prompt="Continue", + model="grok-imagine-video", + video_url="https://example.com/input.mp4", + storage_options={"filename": "extended.mp4"}, + ) + + request = server.get_last_extend_video_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "extended.mp4" + + +def test_prepare_passes_storage_options(client: Client): + batch_req = client.video.prepare( + prompt="Generate video", + model="grok-imagine-video", + storage_options={"filename": "batch.mp4"}, + ) + + assert batch_req.video_request.HasField("storage_options") + assert batch_req.video_request.storage_options.filename == "batch.mp4" + + +def test_video_response_file_output_properties(): + file_output = image_pb2.FileOutput( + file_id="file-xyz789", + filename="my-video.mp4", + expires_at=timestamp_pb2.Timestamp(seconds=1720000000), + ) + proto = video_pb2.VideoResponse( + video=video_pb2.GeneratedVideo(url="https://example.com/v.mp4", duration=5, file_output=file_output), + ) + response = VideoResponse(proto) + assert response.file_output is not None + assert response.file_output.file_id == "file-xyz789" + assert response.file_output.filename == "my-video.mp4" + assert response.file_output.expires_at == timestamp_pb2.Timestamp(seconds=1720000000) + assert response.storage_error is None + + +def test_video_response_no_file_output(): + proto = video_pb2.VideoResponse( + video=video_pb2.GeneratedVideo(url="https://example.com/v.mp4", duration=5), + ) + response = VideoResponse(proto) + assert response.file_output is None + assert response.storage_error is None + + +def test_video_response_storage_error(): + proto = video_pb2.VideoResponse( + video=video_pb2.GeneratedVideo(url="https://example.com/v.mp4", duration=5, storage_error="quota exceeded"), + ) + response = VideoResponse(proto) + assert response.storage_error == "quota exceeded" + + +def test_generate_passes_storage_options_with_public_url(client: Client): + server.clear_last_video_request() + + client.video.generate( + prompt="foo", + model="grok-imagine-video", + storage_options={"filename": "my-video.mp4", "public_url": {"expires_after": 86400}}, + ) + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "my-video.mp4" + assert request.storage_options.HasField("public_url") + assert request.storage_options.public_url.expires_after == 86400 + + +def test_generate_passes_storage_options_with_public_url_true(client: Client): + server.clear_last_video_request() + + client.video.generate( + prompt="foo", + model="grok-imagine-video", + storage_options={"filename": "test.mp4", "public_url": True}, + ) + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "test.mp4" + assert request.storage_options.HasField("public_url") + assert not request.storage_options.public_url.HasField("expires_after") + + +def test_generate_passes_storage_options_with_public_url_false(client: Client): + server.clear_last_video_request() + + client.video.generate( + prompt="foo", + model="grok-imagine-video", + storage_options={"filename": "test.mp4", "public_url": False}, + ) + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("storage_options") + assert request.storage_options.filename == "test.mp4" + assert not request.storage_options.HasField("public_url") + + +def test_video_response_public_url_properties(): + file_output = image_pb2.FileOutput( + file_id="file-xyz789", + filename="my-video.mp4", + public_url="https://files-cdn.x.ai/tok/file-xyz789.mp4", + ) + proto = video_pb2.VideoResponse( + video=video_pb2.GeneratedVideo(url="https://example.com/v.mp4", duration=5, file_output=file_output), + ) + response = VideoResponse(proto) + assert response.public_url == "https://files-cdn.x.ai/tok/file-xyz789.mp4" + assert response.public_url_error is None + + +def test_video_response_public_url_error(): + file_output = image_pb2.FileOutput( + file_id="file-xyz789", + filename="my-video.mp4", + public_url_error="content type not allowed", + ) + proto = video_pb2.VideoResponse( + video=video_pb2.GeneratedVideo(url="https://example.com/v.mp4", duration=5, file_output=file_output), + ) + response = VideoResponse(proto) + assert response.public_url is None + assert response.public_url_error == "content type not allowed" + + +def test_video_response_public_url_none_when_no_file_output(): + proto = video_pb2.VideoResponse( + video=video_pb2.GeneratedVideo(url="https://example.com/v.mp4", duration=5), + ) + response = VideoResponse(proto) + assert response.public_url is None + assert response.public_url_error is None + + +def test_prepare_extension_with_video_file_id(client: Client): + """Test that prepare_extension() accepts a video_file_id.""" + batch_req = client.video.prepare_extension( + prompt="Continue the scene", + model="grok-imagine-video", + video_file_id="file_abc", + ) + + assert batch_req.video_extension_request.video.file_id == "file_abc" + + +def test_prepare_extension_rejects_video_url_and_file_id(client: Client): + """Test that prepare_extension() rejects setting both video_url and video_file_id.""" + with pytest.raises(ValueError, match="Only one of video_url or video_file_id can be set"): + client.video.prepare_extension( + prompt="foo", + model="grok-imagine-video", + video_url="https://example.com/input.mp4", + video_file_id="file_abc", + ) + + +def test_prepare_extension_passes_storage_options(client: Client): + """Test that prepare_extension() passes storage_options through to the request.""" + batch_req = client.video.prepare_extension( + prompt="Continue the scene", + model="grok-imagine-video", + video_url="https://example.com/input.mp4", + storage_options={"filename": "output.mp4", "public_url": True}, + ) + + request = batch_req.video_extension_request + assert request.HasField("storage_options") + assert request.storage_options.filename == "output.mp4" + assert request.storage_options.HasField("public_url")