runapi-veo-3-1 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: runapi-veo-3-1
3
+ Version: 0.1.0
4
+ Summary: Veo 3.1 video client for RunAPI
5
+ Project-URL: Homepage, https://runapi.ai/models/veo-3-1
6
+ Project-URL: Documentation, https://runapi.ai/docs#sdk-veo-3-1
7
+ Author-email: RunAPI <contact@runapi.ai>
8
+ License-Expression: Apache-2.0
9
+ Keywords: ai,runapi,sdk,text-to-video,veo,veo-3.1
10
+ Requires-Python: >=3.9
11
+ Requires-Dist: runapi-core
12
+ Description-Content-Type: text/markdown
13
+
14
+ # Veo API Python SDK for RunAPI
15
+
16
+ The veo api Python SDK is the language-specific package for Veo 3 on RunAPI. Use this veo api package for text-to-video, video extension, and video upscaling flows when your application needs JSON request bodies, task status lookup, and consistent RunAPI errors in Python.
17
+
18
+ This veo api README is the Python package guide inside the public `veo3-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/veo-3.1; for API reference, use https://runapi.ai/docs#veo-3.1; for SDK docs, use https://runapi.ai/docs#sdk-veo-3.1.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install runapi-veo-3-1
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ ```python
29
+ from runapi.veo_3_1 import Veo31Client
30
+
31
+ client = Veo31Client() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
32
+
33
+ task = client.text_to_video.create(
34
+ model="veo-3.1-fast",
35
+ prompt="A drone shot over mountains at sunset",
36
+ duration_seconds=8,
37
+ )
38
+ status = client.text_to_video.get(task.id)
39
+
40
+ extension = client.extend_video.create(
41
+ source_task_id=task.id,
42
+ prompt="Continue the flight toward the coastline",
43
+ )
44
+
45
+ upscale = client.upscale_video.create(
46
+ source_task_id=task.id,
47
+ output_resolution="4k",
48
+ )
49
+ ```
50
+
51
+ Use `create` to submit a task and return quickly, `get` to fetch the latest task state, and `run` when a script should create and poll until completion:
52
+
53
+ ```python
54
+ result = client.text_to_video.run(
55
+ model="veo-3.1-fast",
56
+ prompt="A serene mountain lake at dawn",
57
+ )
58
+ print(result.videos[0].url)
59
+ ```
60
+
61
+ In web request handlers, prefer `create` plus webhook or later `get` polling so a worker is not held open.
62
+
63
+ RunAPI-generated file URLs are temporary. Download and store generated images, videos, audio, or other files in your own durable storage within 7 days; do not treat returned URLs as long-term assets.
64
+
65
+ ## Language notes
66
+
67
+ Pass parameters as keyword arguments and catch the `runapi.veo_3_1` error classes when building video jobs or scripts. Text-to-video request bodies can include `duration` with `4`, `6`, or `8` seconds. The available resources are `text_to_video`, `extend_video`, and `upscale_video`. Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API keys or callback secrets.
68
+
69
+ ## Links
70
+
71
+ - Model page: https://runapi.ai/models/veo-3.1
72
+ - SDK docs: https://runapi.ai/docs#sdk-veo-3.1
73
+ - Product docs: https://runapi.ai/docs#veo-3.1
74
+ - Pricing and rate limits: https://runapi.ai/models/veo-3.1/veo-3.1
75
+ - Provider comparison: https://runapi.ai/providers/google
76
+ - Full catalog: https://runapi.ai/models
77
+ - Repository: https://github.com/runapi-ai/veo3-sdk
78
+
79
+ ## License
80
+
81
+ Licensed under the Apache License, Version 2.0.
@@ -0,0 +1,68 @@
1
+ # Veo API Python SDK for RunAPI
2
+
3
+ The veo api Python SDK is the language-specific package for Veo 3 on RunAPI. Use this veo api package for text-to-video, video extension, and video upscaling flows when your application needs JSON request bodies, task status lookup, and consistent RunAPI errors in Python.
4
+
5
+ This veo api README is the Python package guide inside the public `veo3-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/veo-3.1; for API reference, use https://runapi.ai/docs#veo-3.1; for SDK docs, use https://runapi.ai/docs#sdk-veo-3.1.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install runapi-veo-3-1
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```python
16
+ from runapi.veo_3_1 import Veo31Client
17
+
18
+ client = Veo31Client() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
19
+
20
+ task = client.text_to_video.create(
21
+ model="veo-3.1-fast",
22
+ prompt="A drone shot over mountains at sunset",
23
+ duration_seconds=8,
24
+ )
25
+ status = client.text_to_video.get(task.id)
26
+
27
+ extension = client.extend_video.create(
28
+ source_task_id=task.id,
29
+ prompt="Continue the flight toward the coastline",
30
+ )
31
+
32
+ upscale = client.upscale_video.create(
33
+ source_task_id=task.id,
34
+ output_resolution="4k",
35
+ )
36
+ ```
37
+
38
+ Use `create` to submit a task and return quickly, `get` to fetch the latest task state, and `run` when a script should create and poll until completion:
39
+
40
+ ```python
41
+ result = client.text_to_video.run(
42
+ model="veo-3.1-fast",
43
+ prompt="A serene mountain lake at dawn",
44
+ )
45
+ print(result.videos[0].url)
46
+ ```
47
+
48
+ In web request handlers, prefer `create` plus webhook or later `get` polling so a worker is not held open.
49
+
50
+ RunAPI-generated file URLs are temporary. Download and store generated images, videos, audio, or other files in your own durable storage within 7 days; do not treat returned URLs as long-term assets.
51
+
52
+ ## Language notes
53
+
54
+ Pass parameters as keyword arguments and catch the `runapi.veo_3_1` error classes when building video jobs or scripts. Text-to-video request bodies can include `duration` with `4`, `6`, or `8` seconds. The available resources are `text_to_video`, `extend_video`, and `upscale_video`. Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API keys or callback secrets.
55
+
56
+ ## Links
57
+
58
+ - Model page: https://runapi.ai/models/veo-3.1
59
+ - SDK docs: https://runapi.ai/docs#sdk-veo-3.1
60
+ - Product docs: https://runapi.ai/docs#veo-3.1
61
+ - Pricing and rate limits: https://runapi.ai/models/veo-3.1/veo-3.1
62
+ - Provider comparison: https://runapi.ai/providers/google
63
+ - Full catalog: https://runapi.ai/models
64
+ - Repository: https://github.com/runapi-ai/veo3-sdk
65
+
66
+ ## License
67
+
68
+ Licensed under the Apache License, Version 2.0.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "runapi-veo-3-1"
7
+ version = "0.1.0"
8
+ description = "Veo 3.1 video client for RunAPI"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "Apache-2.0"
12
+ authors = [{ name = "RunAPI", email = "contact@runapi.ai" }]
13
+ keywords = ["runapi", "veo", "veo-3.1", "text-to-video", "ai", "sdk"]
14
+ dependencies = ["runapi-core"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://runapi.ai/models/veo-3-1"
18
+ Documentation = "https://runapi.ai/docs#sdk-veo-3-1"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["src/runapi"]
22
+
23
+ [tool.uv]
24
+ package = true
25
+
26
+ [tool.uv.sources]
27
+ runapi-core = { workspace = true }
28
+
29
+ [dependency-groups]
30
+ dev = ["pytest>=8"]
31
+
32
+ [tool.runapi]
33
+ slug = "veo-3.1"
@@ -0,0 +1,24 @@
1
+ """Veo 3.1 client for RunAPI."""
2
+
3
+ from runapi.core import (
4
+ AuthenticationError,
5
+ InsufficientCreditsError,
6
+ NotFoundError,
7
+ RateLimitError,
8
+ TaskFailedError,
9
+ TaskTimeoutError,
10
+ ValidationError,
11
+ )
12
+
13
+ from .client import Veo31Client
14
+
15
+ __all__ = [
16
+ "Veo31Client",
17
+ "AuthenticationError",
18
+ "RateLimitError",
19
+ "InsufficientCreditsError",
20
+ "NotFoundError",
21
+ "ValidationError",
22
+ "TaskFailedError",
23
+ "TaskTimeoutError",
24
+ ]
@@ -0,0 +1,31 @@
1
+ """Veo 3.1 client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from runapi.core import ClientOptions, HttpClient, resolve_api_key
8
+
9
+ from .resources.extend_video import ExtendVideo
10
+ from .resources.text_to_video import TextToVideo
11
+ from .resources.upscale_video import UpscaleVideo
12
+
13
+
14
+ class Veo31Client:
15
+ """Veo 3.1 video API client.
16
+
17
+ Example::
18
+
19
+ client = Veo31Client(api_key="sk-...")
20
+ result = client.text_to_video.run(
21
+ model="veo-3.1-fast", prompt="A drone shot over mountains at sunset"
22
+ )
23
+ """
24
+
25
+ def __init__(self, api_key: Optional[str] = None, **options: Any) -> None:
26
+ resolved_api_key = resolve_api_key(api_key)
27
+ client_options = ClientOptions(api_key=resolved_api_key, **options)
28
+ http = client_options.http_client or HttpClient(client_options)
29
+ self.text_to_video = TextToVideo(http)
30
+ self.extend_video = ExtendVideo(http)
31
+ self.upscale_video = UpscaleVideo(http)
@@ -0,0 +1,65 @@
1
+ # Code generated by SdkContractGenerator. DO NOT EDIT.
2
+
3
+ CONTRACT = {
4
+ "extend-video": {
5
+ "models": [],
6
+ "fields_by_model": {
7
+ "_": {
8
+ "seeds": {
9
+ "type": "integer"
10
+ },
11
+ "source_task_id": {
12
+ "required": True
13
+ }
14
+ }
15
+ }
16
+ },
17
+ "text-to-video": {
18
+ "models": ["veo-3.1", "veo-3.1-fast"],
19
+ "fields_by_model": {
20
+ "veo-3.1": {
21
+ "aspect_ratio": {
22
+ "enum": ["16:9", "9:16", "auto"]
23
+ },
24
+ "duration_seconds": {
25
+ "enum": [4, 6, 8],
26
+ "type": "integer"
27
+ },
28
+ "input_mode": {
29
+ "enum": ["text", "first_and_last_frames", "reference"]
30
+ },
31
+ "seeds": {
32
+ "type": "integer"
33
+ }
34
+ },
35
+ "veo-3.1-fast": {
36
+ "aspect_ratio": {
37
+ "enum": ["16:9", "9:16", "auto"]
38
+ },
39
+ "duration_seconds": {
40
+ "enum": [4, 6, 8],
41
+ "type": "integer"
42
+ },
43
+ "input_mode": {
44
+ "enum": ["text", "first_and_last_frames", "reference"]
45
+ },
46
+ "seeds": {
47
+ "type": "integer"
48
+ }
49
+ }
50
+ }
51
+ },
52
+ "upscale-video": {
53
+ "models": [],
54
+ "fields_by_model": {
55
+ "_": {
56
+ "index": {
57
+ "type": "integer"
58
+ },
59
+ "output_resolution": {
60
+ "enum": ["1080p", "4k"]
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
File without changes
@@ -0,0 +1,5 @@
1
+ from .extend_video import ExtendVideo
2
+ from .text_to_video import TextToVideo
3
+ from .upscale_video import UpscaleVideo
4
+
5
+ __all__ = ["TextToVideo", "ExtendVideo", "UpscaleVideo"]
@@ -0,0 +1,60 @@
1
+ """Veo 3.1 video extension resource."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict
6
+
7
+ from runapi.core import Resource, ValidationError
8
+
9
+ from ..types import CompletedExtendVideoResponse, ExtendVideoResponse
10
+
11
+
12
+ class ExtendVideo(Resource):
13
+ """Extend a previously generated video by referencing its task id."""
14
+
15
+ ENDPOINT = "/api/v1/veo_3_1/extend_video"
16
+
17
+ RESPONSE_CLASS = ExtendVideoResponse
18
+ COMPLETED_RESPONSE_CLASS = CompletedExtendVideoResponse
19
+
20
+ def run(self, **params: Any) -> Any:
21
+ """Extend a video and poll until it completes.
22
+
23
+ Args:
24
+ **params: extend-video parameters (model, ...).
25
+
26
+ Returns:
27
+ The completed (narrowed) extend-video response.
28
+ """
29
+ task = self.create(**params)
30
+ return self._poll_until_complete(lambda: self.get(task.id))
31
+
32
+ def create(self, **params: Any) -> Any:
33
+ """Create an extend-video task and return immediately with an id.
34
+
35
+ Args:
36
+ **params: extend-video parameters (model, ...).
37
+
38
+ Returns:
39
+ The task creation result with an id.
40
+ """
41
+ compacted = self._compact_params(params)
42
+ self._validate_params(compacted)
43
+ return self._request("post", self.ENDPOINT, body=compacted)
44
+
45
+ def get(self, id: str) -> Any:
46
+ """Fetch the current status of an extend-video task.
47
+
48
+ Args:
49
+ id: The task id returned by ``create``.
50
+
51
+ Returns:
52
+ The current task status.
53
+ """
54
+ return self._request("get", f"{self.ENDPOINT}/{id}")
55
+
56
+ def _validate_params(self, params: Dict[str, Any]) -> None:
57
+ if not params.get("source_task_id"):
58
+ raise ValidationError("source_task_id is required")
59
+ if not params.get("prompt"):
60
+ raise ValidationError("prompt is required")
@@ -0,0 +1,100 @@
1
+ """Veo 3.1 text-to-video resource."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict
6
+
7
+ from runapi.core import Resource, ValidationError
8
+
9
+ from ..contract_gen import CONTRACT
10
+ from ..types import (
11
+ CompletedTextToVideoResponse,
12
+ TextToVideoResponse,
13
+ )
14
+
15
+
16
+ class TextToVideo(Resource):
17
+ """Generate videos from text, image, and reference-image prompts."""
18
+
19
+ ENDPOINT = "/api/v1/veo_3_1/text_to_video"
20
+
21
+ RESPONSE_CLASS = TextToVideoResponse
22
+ COMPLETED_RESPONSE_CLASS = CompletedTextToVideoResponse
23
+
24
+ def run(self, **params: Any) -> Any:
25
+ """Generate a video and poll until it completes.
26
+
27
+ Args:
28
+ **params: text-to-video parameters (model, ...).
29
+
30
+ Returns:
31
+ The completed (narrowed) text-to-video response.
32
+ """
33
+ task = self.create(**params)
34
+ return self._poll_until_complete(lambda: self.get(task.id))
35
+
36
+ def create(self, **params: Any) -> Any:
37
+ """Create a text-to-video task and return immediately with an id.
38
+
39
+ Args:
40
+ **params: text-to-video parameters (model, ...).
41
+
42
+ Returns:
43
+ The task creation result with an id.
44
+ """
45
+ compacted = self._compact_params(params)
46
+ self._validate_params(compacted)
47
+ return self._request("post", self.ENDPOINT, body=compacted)
48
+
49
+ def get(self, id: str) -> Any:
50
+ """Fetch the current status of a text-to-video task.
51
+
52
+ Args:
53
+ id: The task id returned by ``create``.
54
+
55
+ Returns:
56
+ The current task status.
57
+ """
58
+ return self._request("get", f"{self.ENDPOINT}/{id}")
59
+
60
+ def _validate_params(self, params: Dict[str, Any]) -> None:
61
+ self._validate_contract(CONTRACT["text-to-video"], params)
62
+
63
+ if not params.get("prompt"):
64
+ raise ValidationError("prompt is required")
65
+
66
+ self._validate_input_mode(params)
67
+
68
+ def _validate_input_mode(self, params: Dict[str, Any]) -> None:
69
+ input_mode = params.get("input_mode")
70
+ if not input_mode:
71
+ return
72
+
73
+ if input_mode == "first_and_last_frames":
74
+ if not self._field_present(params, "first_frame_image_url"):
75
+ raise ValidationError("first_frame_image_url is required for first_and_last_frames")
76
+ if self._field_present(params, "reference_image_urls"):
77
+ raise ValidationError("reference_image_urls requires input_mode reference")
78
+ elif input_mode == "reference":
79
+ urls = params.get("reference_image_urls")
80
+ if not urls:
81
+ raise ValidationError("reference_image_urls is required for reference")
82
+ if not (isinstance(urls, list) and 1 <= len(urls) <= 3):
83
+ raise ValidationError("reference_image_urls must contain 1-3 items for reference")
84
+ model = params.get("model")
85
+ if model != "veo-3.1-fast":
86
+ raise ValidationError("reference requires model veo-3.1-fast")
87
+ ar = params.get("aspect_ratio")
88
+ if ar and ar != "16:9":
89
+ raise ValidationError("reference requires aspect_ratio 16:9")
90
+ if self._field_present(params, "first_frame_image_url") or self._field_present(params, "last_frame_image_url"):
91
+ raise ValidationError(
92
+ "first_frame_image_url and last_frame_image_url require input_mode first_and_last_frames"
93
+ )
94
+ else:
95
+ if self._field_present(params, "first_frame_image_url") or self._field_present(params, "last_frame_image_url"):
96
+ raise ValidationError(
97
+ "first_frame_image_url and last_frame_image_url require input_mode first_and_last_frames"
98
+ )
99
+ if self._field_present(params, "reference_image_urls"):
100
+ raise ValidationError("reference_image_urls requires input_mode reference")
@@ -0,0 +1,65 @@
1
+ """Veo 3.1 video upscaling resource."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict
6
+
7
+ from runapi.core import Resource, ValidationError
8
+
9
+ from ..types import (
10
+ OUTPUT_RESOLUTIONS,
11
+ CompletedUpscaleVideoResponse,
12
+ UpscaleVideoResponse,
13
+ )
14
+
15
+
16
+ class UpscaleVideo(Resource):
17
+ """Upscale a previously generated video by referencing its task id."""
18
+
19
+ ENDPOINT = "/api/v1/veo_3_1/upscale_video"
20
+
21
+ RESPONSE_CLASS = UpscaleVideoResponse
22
+ COMPLETED_RESPONSE_CLASS = CompletedUpscaleVideoResponse
23
+
24
+ def run(self, **params: Any) -> Any:
25
+ """Upscale a video and poll until it completes.
26
+
27
+ Args:
28
+ **params: video upscale parameters (model, ...).
29
+
30
+ Returns:
31
+ The completed (narrowed) video upscale response.
32
+ """
33
+ task = self.create(**params)
34
+ return self._poll_until_complete(lambda: self.get(task.id))
35
+
36
+ def create(self, **params: Any) -> Any:
37
+ """Create a video upscale task and return immediately with an id.
38
+
39
+ Args:
40
+ **params: video upscale parameters (model, ...).
41
+
42
+ Returns:
43
+ The task creation result with an id.
44
+ """
45
+ compacted = self._compact_params(params)
46
+ self._validate_params(compacted)
47
+ return self._request("post", self.ENDPOINT, body=compacted)
48
+
49
+ def get(self, id: str) -> Any:
50
+ """Fetch the current status of a video upscale task.
51
+
52
+ Args:
53
+ id: The task id returned by ``create``.
54
+
55
+ Returns:
56
+ The current task status.
57
+ """
58
+ return self._request("get", f"{self.ENDPOINT}/{id}")
59
+
60
+ def _validate_params(self, params: Dict[str, Any]) -> None:
61
+ if not params.get("source_task_id"):
62
+ raise ValidationError("source_task_id is required")
63
+ output_resolution = params.get("output_resolution")
64
+ if output_resolution not in OUTPUT_RESOLUTIONS:
65
+ raise ValidationError(f"output_resolution must be one of: {', '.join(OUTPUT_RESOLUTIONS)}")
@@ -0,0 +1,65 @@
1
+ """Veo 3.1 model lists, enums, and response models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from runapi.core import BaseModel, TaskResponse, optional, required
6
+
7
+ OUTPUT_RESOLUTIONS = ["1080p", "4k"]
8
+
9
+
10
+ class Video(BaseModel):
11
+ url = optional(str)
12
+ resolution = optional(str)
13
+ has_audio = optional()
14
+
15
+
16
+ class Source(BaseModel):
17
+ url = optional(str)
18
+
19
+
20
+ class AsyncTaskResponse(TaskResponse):
21
+ """Veo 3.1 async task status response."""
22
+
23
+ id = required(str)
24
+ status = optional(str, enum=lambda: TaskResponse.Status.ALL)
25
+
26
+
27
+ class TextToVideoResponse(AsyncTaskResponse):
28
+ """Veo 3.1 text-to-video task status response."""
29
+
30
+ videos = optional([lambda: Video])
31
+ sources = optional([lambda: Source])
32
+
33
+
34
+ class ExtendVideoResponse(AsyncTaskResponse):
35
+ """Veo 3.1 extend-video task status response."""
36
+
37
+ videos = optional([lambda: Video])
38
+ sources = optional([lambda: Source])
39
+
40
+
41
+ class UpscaleVideoResponse(AsyncTaskResponse):
42
+ """Veo 3.1 video upscale task status response."""
43
+
44
+ source_task_id = optional(str)
45
+ videos = optional([lambda: Video])
46
+ sources = optional([lambda: Source])
47
+ media_ids = optional([str])
48
+
49
+
50
+ class CompletedTextToVideoResponse(TextToVideoResponse):
51
+ """Narrowed response from ``run()`` once polling observes completion."""
52
+
53
+ videos = required([lambda: Video])
54
+
55
+
56
+ class CompletedExtendVideoResponse(ExtendVideoResponse):
57
+ """Narrowed response from ``run()`` once polling observes completion."""
58
+
59
+ videos = required([lambda: Video])
60
+
61
+
62
+ class CompletedUpscaleVideoResponse(UpscaleVideoResponse):
63
+ """Narrowed response from ``run()`` once polling observes completion."""
64
+
65
+ videos = required([lambda: Video])
@@ -0,0 +1,252 @@
1
+ import pytest
2
+
3
+ from runapi.core import config
4
+ from runapi.core.errors import AuthenticationError, ValidationError
5
+ from runapi.veo_3_1 import Veo31Client
6
+ from runapi.veo_3_1.resources.extend_video import ExtendVideo
7
+ from runapi.veo_3_1.resources.text_to_video import TextToVideo
8
+ from runapi.veo_3_1.resources.upscale_video import UpscaleVideo
9
+ from runapi.veo_3_1.types import (
10
+ CompletedExtendVideoResponse,
11
+ CompletedTextToVideoResponse,
12
+ CompletedUpscaleVideoResponse,
13
+ ExtendVideoResponse,
14
+ TextToVideoResponse,
15
+ UpscaleVideoResponse,
16
+ )
17
+
18
+
19
+ class FakeHttp:
20
+ def __init__(self, *responses):
21
+ self._responses = list(responses)
22
+ self.calls = []
23
+
24
+ def request(self, method, path, body=None, options=None):
25
+ self.calls.append((method, path, body))
26
+ if self._responses:
27
+ return self._responses.pop(0)
28
+ return {"id": "task_1", "status": "pending"}
29
+
30
+
31
+ @pytest.fixture(autouse=True)
32
+ def reset_config(monkeypatch):
33
+ monkeypatch.delenv("RUNAPI_API_KEY", raising=False)
34
+ monkeypatch.setattr(config, "api_key", None)
35
+ yield
36
+
37
+
38
+ # --- auth -----------------------------------------------------------------
39
+
40
+
41
+ def test_accepts_api_key_parameter():
42
+ assert isinstance(Veo31Client(api_key="k", http_client=FakeHttp()), Veo31Client)
43
+
44
+
45
+ def test_falls_back_to_global(monkeypatch):
46
+ monkeypatch.setattr(config, "api_key", "global-key")
47
+ assert isinstance(Veo31Client(http_client=FakeHttp()), Veo31Client)
48
+
49
+
50
+ def test_falls_back_to_env(monkeypatch):
51
+ monkeypatch.setenv("RUNAPI_API_KEY", "env-key")
52
+ assert isinstance(Veo31Client(http_client=FakeHttp()), Veo31Client)
53
+
54
+
55
+ def test_raises_without_api_key():
56
+ with pytest.raises(AuthenticationError, match="API key is required"):
57
+ Veo31Client()
58
+
59
+
60
+ # --- injection / accessors ------------------------------------------------
61
+
62
+
63
+ def test_uses_injected_http_client():
64
+ fake = FakeHttp()
65
+ client = Veo31Client(api_key="k", http_client=fake)
66
+ assert client.text_to_video._http is fake
67
+ assert client.extend_video._http is fake
68
+ assert client.upscale_video._http is fake
69
+
70
+
71
+ def test_exposes_resource_accessors():
72
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
73
+ assert isinstance(client.text_to_video, TextToVideo)
74
+ assert isinstance(client.extend_video, ExtendVideo)
75
+ assert isinstance(client.upscale_video, UpscaleVideo)
76
+
77
+
78
+ # --- request shapes -------------------------------------------------------
79
+
80
+
81
+ def test_text_to_video_create_posts_compacted_body():
82
+ fake = FakeHttp({"id": "t1", "status": "pending"})
83
+ client = Veo31Client(api_key="k", http_client=fake)
84
+ result = client.text_to_video.create(
85
+ model="veo-3.1-fast", prompt="a kite", aspect_ratio="16:9", duration_seconds=None
86
+ )
87
+ assert fake.calls == [
88
+ ("post", "/api/v1/veo_3_1/text_to_video", {"model": "veo-3.1-fast", "prompt": "a kite", "aspect_ratio": "16:9"}),
89
+ ]
90
+ assert isinstance(result, TextToVideoResponse)
91
+
92
+
93
+ def test_text_to_video_get_fetches_by_id():
94
+ fake = FakeHttp({"id": "t1", "status": "processing"})
95
+ client = Veo31Client(api_key="k", http_client=fake)
96
+ client.text_to_video.get("t1")
97
+ assert fake.calls == [("get", "/api/v1/veo_3_1/text_to_video/t1", None)]
98
+
99
+
100
+ def test_extend_video_create_posts_source_task_id():
101
+ fake = FakeHttp({"id": "e1", "status": "pending"})
102
+ client = Veo31Client(api_key="k", http_client=fake)
103
+ result = client.extend_video.create(source_task_id="t1", prompt="keep going")
104
+ assert fake.calls == [
105
+ ("post", "/api/v1/veo_3_1/extend_video", {"source_task_id": "t1", "prompt": "keep going"}),
106
+ ]
107
+ assert isinstance(result, ExtendVideoResponse)
108
+
109
+
110
+ def test_extend_video_get_fetches_by_id():
111
+ fake = FakeHttp({"id": "e1", "status": "processing"})
112
+ client = Veo31Client(api_key="k", http_client=fake)
113
+ client.extend_video.get("e1")
114
+ assert fake.calls == [("get", "/api/v1/veo_3_1/extend_video/e1", None)]
115
+
116
+
117
+ def test_upscale_video_create_posts_source_task_id():
118
+ fake = FakeHttp({"id": "u1", "status": "pending"})
119
+ client = Veo31Client(api_key="k", http_client=fake)
120
+ result = client.upscale_video.create(source_task_id="t1", output_resolution="4k")
121
+ assert fake.calls == [
122
+ ("post", "/api/v1/veo_3_1/upscale_video", {"source_task_id": "t1", "output_resolution": "4k"}),
123
+ ]
124
+ assert isinstance(result, UpscaleVideoResponse)
125
+
126
+
127
+ def test_upscale_video_get_fetches_by_id():
128
+ fake = FakeHttp({"id": "u1", "status": "processing"})
129
+ client = Veo31Client(api_key="k", http_client=fake)
130
+ client.upscale_video.get("u1")
131
+ assert fake.calls == [("get", "/api/v1/veo_3_1/upscale_video/u1", None)]
132
+
133
+
134
+ # --- run narrowing --------------------------------------------------------
135
+
136
+
137
+ def test_text_to_video_run_narrows_completed_type():
138
+ fake = FakeHttp(
139
+ {"id": "t1", "status": "pending"},
140
+ {"id": "t1", "status": "completed", "videos": [{"url": "https://x/y.mp4"}]},
141
+ )
142
+ client = Veo31Client(api_key="k", http_client=fake)
143
+ result = client.text_to_video.run(model="veo-3.1-fast", prompt="a serene lake")
144
+ assert isinstance(result, CompletedTextToVideoResponse)
145
+ assert result.videos[0].url == "https://x/y.mp4"
146
+
147
+
148
+ def test_extend_video_run_narrows_completed_type():
149
+ fake = FakeHttp(
150
+ {"id": "e1", "status": "pending"},
151
+ {"id": "e1", "status": "completed", "videos": [{"url": "https://x/e.mp4"}]},
152
+ )
153
+ client = Veo31Client(api_key="k", http_client=fake)
154
+ result = client.extend_video.run(source_task_id="t1", prompt="continue")
155
+ assert isinstance(result, CompletedExtendVideoResponse)
156
+ assert result.videos[0].url == "https://x/e.mp4"
157
+
158
+
159
+ def test_upscale_video_run_narrows_completed_type():
160
+ fake = FakeHttp(
161
+ {"id": "u1", "status": "pending"},
162
+ {"id": "u1", "status": "completed", "videos": [{"url": "https://x/u.mp4"}]},
163
+ )
164
+ client = Veo31Client(api_key="k", http_client=fake)
165
+ result = client.upscale_video.run(source_task_id="t1", output_resolution="1080p")
166
+ assert isinstance(result, CompletedUpscaleVideoResponse)
167
+ assert result.videos[0].url == "https://x/u.mp4"
168
+
169
+
170
+ # --- validation -----------------------------------------------------------
171
+
172
+
173
+ def test_text_to_video_requires_model():
174
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
175
+ with pytest.raises(ValidationError, match="model must be one of: veo-3.1, veo-3.1-fast"):
176
+ client.text_to_video.create(prompt="hi")
177
+
178
+
179
+ def test_text_to_video_requires_prompt():
180
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
181
+ with pytest.raises(ValidationError, match="prompt is required"):
182
+ client.text_to_video.create(model="veo-3.1-fast")
183
+
184
+
185
+ def test_text_to_video_rejects_unknown_model():
186
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
187
+ with pytest.raises(ValidationError, match="model must be one of: veo-3.1, veo-3.1-fast"):
188
+ client.text_to_video.create(model="nope", prompt="hi")
189
+
190
+
191
+ def test_text_to_video_rejects_invalid_aspect_ratio():
192
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
193
+ with pytest.raises(ValidationError, match="aspect_ratio must be one of: 16:9, 9:16, auto"):
194
+ client.text_to_video.create(model="veo-3.1-fast", prompt="hi", aspect_ratio="4:3")
195
+
196
+
197
+ def test_text_to_video_rejects_invalid_duration():
198
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
199
+ with pytest.raises(ValidationError, match="duration_seconds must be one of: 4, 6, 8"):
200
+ client.text_to_video.create(model="veo-3.1-fast", prompt="hi", duration_seconds=5)
201
+
202
+
203
+ def test_first_and_last_frames_requires_first_frame():
204
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
205
+ with pytest.raises(ValidationError, match="first_frame_image_url is required for first_and_last_frames"):
206
+ client.text_to_video.create(model="veo-3.1-fast", prompt="hi", input_mode="first_and_last_frames")
207
+
208
+
209
+ def test_reference_requires_reference_image_urls():
210
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
211
+ with pytest.raises(ValidationError, match="reference_image_urls is required for reference"):
212
+ client.text_to_video.create(model="veo-3.1-fast", prompt="hi", input_mode="reference")
213
+
214
+
215
+ def test_reference_requires_fast_model():
216
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
217
+ with pytest.raises(ValidationError, match="reference requires model veo-3.1-fast"):
218
+ client.text_to_video.create(
219
+ model="veo-3.1", prompt="hi", input_mode="reference", reference_image_urls=["https://x/a.png"]
220
+ )
221
+
222
+
223
+ def test_reference_image_urls_without_reference_mode_rejected():
224
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
225
+ with pytest.raises(ValidationError, match="reference_image_urls requires input_mode reference"):
226
+ client.text_to_video.create(
227
+ model="veo-3.1-fast", prompt="hi", input_mode="text", reference_image_urls=["https://x/a.png"]
228
+ )
229
+
230
+
231
+ def test_extend_video_requires_source_task_id():
232
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
233
+ with pytest.raises(ValidationError, match="source_task_id is required"):
234
+ client.extend_video.create(prompt="continue")
235
+
236
+
237
+ def test_extend_video_requires_prompt():
238
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
239
+ with pytest.raises(ValidationError, match="prompt is required"):
240
+ client.extend_video.create(source_task_id="t1")
241
+
242
+
243
+ def test_upscale_video_requires_source_task_id():
244
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
245
+ with pytest.raises(ValidationError, match="source_task_id is required"):
246
+ client.upscale_video.create(output_resolution="4k")
247
+
248
+
249
+ def test_upscale_video_rejects_invalid_output_resolution():
250
+ client = Veo31Client(api_key="k", http_client=FakeHttp())
251
+ with pytest.raises(ValidationError, match="output_resolution must be one of"):
252
+ client.upscale_video.create(source_task_id="t1", output_resolution="720p")