runapi-gpt-image 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,29 @@
1
+ # Build artifacts
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ *.egg
6
+
7
+ # Bytecode
8
+ __pycache__/
9
+ *.py[cod]
10
+
11
+ # Virtual environments
12
+ .venv/
13
+ venv/
14
+
15
+ # uv
16
+ uv.lock
17
+
18
+ # Test / type caches
19
+ .pytest_cache/
20
+ .mypy_cache/
21
+ .ruff_cache/
22
+ .coverage
23
+ htmlcov/
24
+
25
+ # IDE / OS
26
+ .idea/
27
+ .vscode/
28
+ *.swp
29
+ .DS_Store
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: runapi-gpt-image
3
+ Version: 0.1.0
4
+ Summary: GPT Image text-to-image and edit-image client for RunAPI
5
+ Project-URL: Homepage, https://runapi.ai/models/gpt-image
6
+ Project-URL: Documentation, https://runapi.ai/docs#sdk-gpt-image
7
+ Author-email: RunAPI <contact@runapi.ai>
8
+ License-Expression: Apache-2.0
9
+ Keywords: ai,edit-image,gpt-image,runapi,sdk,text-to-image
10
+ Requires-Python: >=3.9
11
+ Requires-Dist: runapi-core
12
+ Description-Content-Type: text/markdown
13
+
14
+ # GPT Image Python SDK for RunAPI
15
+
16
+ The GPT Image Python SDK is the language-specific package for GPT Image on
17
+ RunAPI. Use it for text-to-image and edit-image flows when your application needs
18
+ JSON request bodies, task status lookup, and consistent RunAPI errors in Python.
19
+
20
+ For model details, use https://runapi.ai/models/gpt-image; for API reference, use
21
+ https://runapi.ai/docs#gpt-image; for SDK docs, use https://runapi.ai/docs#sdk-gpt-image.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install runapi-gpt-image
27
+ ```
28
+
29
+ ## Quick start
30
+
31
+ ```python
32
+ from runapi.gpt_image import GptImageClient
33
+
34
+ client = GptImageClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
35
+
36
+ task = client.text_to_image.create(
37
+ model="gpt-image-1.5",
38
+ prompt="A futuristic cityscape at dusk, cinematic",
39
+ aspect_ratio="1:1",
40
+ quality="high",
41
+ )
42
+ status = client.text_to_image.get(task.id)
43
+
44
+ edit = client.edit_image.create(
45
+ model="gpt-image-1.5",
46
+ prompt="Transform into an oil painting",
47
+ source_image_urls=["https://example.com/source.jpg"],
48
+ aspect_ratio="1:1",
49
+ quality="high",
50
+ )
51
+ ```
52
+
53
+ Use `create` to submit a task and return quickly, `get` to fetch the latest task
54
+ state, and `run` to create and poll until completion:
55
+
56
+ ```python
57
+ result = client.text_to_image.run(
58
+ model="gpt-image-1.5",
59
+ prompt="A serene mountain lake at dawn",
60
+ aspect_ratio="3:2",
61
+ quality="high",
62
+ )
63
+ print(result.images[0].url)
64
+ ```
65
+
66
+ In web request handlers, prefer `create` plus webhook or later `get` polling so a
67
+ worker is not held open.
68
+
69
+ RunAPI-generated file URLs are temporary. Download and store generated images in
70
+ your own durable storage within 7 days; do not treat returned URLs as long-term
71
+ assets.
72
+
73
+ ## Language notes
74
+
75
+ Pass parameters as keyword arguments and catch the `runapi.gpt_image` error
76
+ classes when building image jobs or scripts. The available resources are
77
+ `text_to_image` and `edit_image`. Keep `RUNAPI_API_KEY` in the environment or
78
+ your secret manager; never commit API keys or callback secrets.
79
+
80
+ ## Links
81
+
82
+ - Model page: https://runapi.ai/models/gpt-image
83
+ - SDK docs: https://runapi.ai/docs#sdk-gpt-image
84
+ - Product docs: https://runapi.ai/docs#gpt-image
85
+ - Pricing and rate limits: https://runapi.ai/models/gpt-image
86
+ - Full catalog: https://runapi.ai/models
87
+
88
+ ## License
89
+
90
+ Licensed under the Apache License, Version 2.0.
@@ -0,0 +1,77 @@
1
+ # GPT Image Python SDK for RunAPI
2
+
3
+ The GPT Image Python SDK is the language-specific package for GPT Image on
4
+ RunAPI. Use it for text-to-image and edit-image flows when your application needs
5
+ JSON request bodies, task status lookup, and consistent RunAPI errors in Python.
6
+
7
+ For model details, use https://runapi.ai/models/gpt-image; for API reference, use
8
+ https://runapi.ai/docs#gpt-image; for SDK docs, use https://runapi.ai/docs#sdk-gpt-image.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install runapi-gpt-image
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ ```python
19
+ from runapi.gpt_image import GptImageClient
20
+
21
+ client = GptImageClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
22
+
23
+ task = client.text_to_image.create(
24
+ model="gpt-image-1.5",
25
+ prompt="A futuristic cityscape at dusk, cinematic",
26
+ aspect_ratio="1:1",
27
+ quality="high",
28
+ )
29
+ status = client.text_to_image.get(task.id)
30
+
31
+ edit = client.edit_image.create(
32
+ model="gpt-image-1.5",
33
+ prompt="Transform into an oil painting",
34
+ source_image_urls=["https://example.com/source.jpg"],
35
+ aspect_ratio="1:1",
36
+ quality="high",
37
+ )
38
+ ```
39
+
40
+ Use `create` to submit a task and return quickly, `get` to fetch the latest task
41
+ state, and `run` to create and poll until completion:
42
+
43
+ ```python
44
+ result = client.text_to_image.run(
45
+ model="gpt-image-1.5",
46
+ prompt="A serene mountain lake at dawn",
47
+ aspect_ratio="3:2",
48
+ quality="high",
49
+ )
50
+ print(result.images[0].url)
51
+ ```
52
+
53
+ In web request handlers, prefer `create` plus webhook or later `get` polling so a
54
+ worker is not held open.
55
+
56
+ RunAPI-generated file URLs are temporary. Download and store generated images in
57
+ your own durable storage within 7 days; do not treat returned URLs as long-term
58
+ assets.
59
+
60
+ ## Language notes
61
+
62
+ Pass parameters as keyword arguments and catch the `runapi.gpt_image` error
63
+ classes when building image jobs or scripts. The available resources are
64
+ `text_to_image` and `edit_image`. Keep `RUNAPI_API_KEY` in the environment or
65
+ your secret manager; never commit API keys or callback secrets.
66
+
67
+ ## Links
68
+
69
+ - Model page: https://runapi.ai/models/gpt-image
70
+ - SDK docs: https://runapi.ai/docs#sdk-gpt-image
71
+ - Product docs: https://runapi.ai/docs#gpt-image
72
+ - Pricing and rate limits: https://runapi.ai/models/gpt-image
73
+ - Full catalog: https://runapi.ai/models
74
+
75
+ ## License
76
+
77
+ Licensed under the Apache License, Version 2.0.
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "runapi-gpt-image"
7
+ version = "0.1.0"
8
+ description = "GPT Image text-to-image and edit-image 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", "gpt-image", "text-to-image", "edit-image", "ai", "sdk"]
14
+ dependencies = ["runapi-core"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://runapi.ai/models/gpt-image"
18
+ Documentation = "https://runapi.ai/docs#sdk-gpt-image"
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 = { path = "../runapi-core", editable = true }
28
+
29
+ [dependency-groups]
30
+ dev = ["pytest>=8"]
@@ -0,0 +1,24 @@
1
+ """GPT Image 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 GptImageClient
14
+
15
+ __all__ = [
16
+ "GptImageClient",
17
+ "AuthenticationError",
18
+ "RateLimitError",
19
+ "InsufficientCreditsError",
20
+ "NotFoundError",
21
+ "ValidationError",
22
+ "TaskFailedError",
23
+ "TaskTimeoutError",
24
+ ]
@@ -0,0 +1,30 @@
1
+ """GPT Image 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.edit_image import EditImage
10
+ from .resources.text_to_image import TextToImage
11
+
12
+
13
+ class GptImageClient:
14
+ """GPT Image text-to-image and edit-image client.
15
+
16
+ Example::
17
+
18
+ client = GptImageClient(api_key="sk-...")
19
+ result = client.text_to_image.run(
20
+ model="gpt-image-1.5", prompt="A futuristic cityscape",
21
+ aspect_ratio="1:1", quality="high",
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_image = TextToImage(http)
30
+ self.edit_image = EditImage(http)
File without changes
@@ -0,0 +1,4 @@
1
+ from .edit_image import EditImage
2
+ from .text_to_image import TextToImage
3
+
4
+ __all__ = ["TextToImage", "EditImage"]
@@ -0,0 +1,81 @@
1
+ """GPT Image edit-image 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
+ ASPECT_RATIOS,
11
+ EDIT_MODELS,
12
+ QUALITY_VALUES,
13
+ CompletedEditImageResponse,
14
+ EditImageResponse,
15
+ )
16
+
17
+
18
+ class EditImage(Resource):
19
+ """Edit images from a prompt and source images with GPT Image models."""
20
+
21
+ ENDPOINT = "/api/v1/gpt_image/edit_image"
22
+
23
+ RESPONSE_CLASS = EditImageResponse
24
+ COMPLETED_RESPONSE_CLASS = CompletedEditImageResponse
25
+
26
+ def run(self, **params: Any) -> Any:
27
+ """Create an edit-image task and poll until it completes.
28
+
29
+ Args:
30
+ **params: Edit-image parameters (model, prompt, ...).
31
+
32
+ Returns:
33
+ The completed edit-image response.
34
+ """
35
+ task = self.create(**params)
36
+ return self._poll_until_complete(lambda: self.get(task.id))
37
+
38
+ def create(self, **params: Any) -> Any:
39
+ """Create an edit-image task and return immediately with an id.
40
+
41
+ Args:
42
+ **params: Edit-image parameters (model, prompt, ...).
43
+
44
+ Returns:
45
+ The task creation result with an id.
46
+ """
47
+ compacted = self._compact_params(params)
48
+ self._validate_params(compacted)
49
+ return self._request("post", self.ENDPOINT, body=compacted)
50
+
51
+ def get(self, id: str) -> Any:
52
+ """Fetch the current status of an edit-image task.
53
+
54
+ Args:
55
+ id: Task id.
56
+
57
+ Returns:
58
+ The current edit-image status.
59
+ """
60
+ return self._request("get", f"{self.ENDPOINT}/{id}")
61
+
62
+ def _validate_params(self, params: Dict[str, Any]) -> None:
63
+ if not params.get("model"):
64
+ raise ValidationError("model is required")
65
+ if not params.get("prompt"):
66
+ raise ValidationError("prompt is required")
67
+
68
+ model = params.get("model")
69
+ if model not in EDIT_MODELS:
70
+ raise ValidationError(f"Invalid model: {model}. Must be: {', '.join(EDIT_MODELS)}")
71
+
72
+ urls = params.get("source_image_urls")
73
+ if urls is None or (hasattr(urls, "__len__") and len(urls) == 0):
74
+ raise ValidationError("source_image_urls is required for image editing")
75
+
76
+ if not params.get("aspect_ratio"):
77
+ raise ValidationError("aspect_ratio is required")
78
+ self._validate_optional(params, "aspect_ratio", ASPECT_RATIOS)
79
+ if not params.get("quality"):
80
+ raise ValidationError("quality is required")
81
+ self._validate_optional(params, "quality", QUALITY_VALUES)
@@ -0,0 +1,77 @@
1
+ """GPT Image text-to-image 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
+ ASPECT_RATIOS,
11
+ GENERATION_MODELS,
12
+ QUALITY_VALUES,
13
+ CompletedTextToImageResponse,
14
+ TextToImageResponse,
15
+ )
16
+
17
+
18
+ class TextToImage(Resource):
19
+ """Generate images from text prompts with GPT Image models."""
20
+
21
+ ENDPOINT = "/api/v1/gpt_image/text_to_image"
22
+
23
+ RESPONSE_CLASS = TextToImageResponse
24
+ COMPLETED_RESPONSE_CLASS = CompletedTextToImageResponse
25
+
26
+ def run(self, **params: Any) -> Any:
27
+ """Create a text-to-image task and poll until it completes.
28
+
29
+ Args:
30
+ **params: Text-to-image parameters (model, prompt, ...).
31
+
32
+ Returns:
33
+ The completed text-to-image response.
34
+ """
35
+ task = self.create(**params)
36
+ return self._poll_until_complete(lambda: self.get(task.id))
37
+
38
+ def create(self, **params: Any) -> Any:
39
+ """Create a text-to-image task and return immediately with an id.
40
+
41
+ Args:
42
+ **params: Text-to-image parameters (model, prompt, ...).
43
+
44
+ Returns:
45
+ The task creation result with an id.
46
+ """
47
+ compacted = self._compact_params(params)
48
+ self._validate_params(compacted)
49
+ return self._request("post", self.ENDPOINT, body=compacted)
50
+
51
+ def get(self, id: str) -> Any:
52
+ """Fetch the current status of a text-to-image task.
53
+
54
+ Args:
55
+ id: Task id.
56
+
57
+ Returns:
58
+ The current text-to-image status.
59
+ """
60
+ return self._request("get", f"{self.ENDPOINT}/{id}")
61
+
62
+ def _validate_params(self, params: Dict[str, Any]) -> None:
63
+ if not params.get("model"):
64
+ raise ValidationError("model is required")
65
+ if not params.get("prompt"):
66
+ raise ValidationError("prompt is required")
67
+
68
+ model = params.get("model")
69
+ if model not in GENERATION_MODELS:
70
+ raise ValidationError(f"Invalid model: {model}. Must be: {', '.join(GENERATION_MODELS)}")
71
+
72
+ if not params.get("aspect_ratio"):
73
+ raise ValidationError("aspect_ratio is required")
74
+ self._validate_optional(params, "aspect_ratio", ASPECT_RATIOS)
75
+ if not params.get("quality"):
76
+ raise ValidationError("quality is required")
77
+ self._validate_optional(params, "quality", QUALITY_VALUES)
@@ -0,0 +1,36 @@
1
+ """GPT Image model lists, enums, and response models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from runapi.core import BaseModel, TaskResponse, optional, required
6
+
7
+ MODELS = ["gpt-image-1.5"]
8
+ GENERATION_MODELS = MODELS
9
+ EDIT_MODELS = MODELS
10
+
11
+ ASPECT_RATIOS = ["1:1", "2:3", "3:2"]
12
+ QUALITY_VALUES = ["medium", "high"]
13
+
14
+
15
+ class Image(BaseModel):
16
+ url = optional(str)
17
+
18
+
19
+ class TextToImageResponse(TaskResponse):
20
+ """Task status/result for GPT Image text-to-image."""
21
+ id = required(str)
22
+ status = optional(str, enum=lambda: TaskResponse.Status.ALL)
23
+ images = optional([lambda: Image])
24
+ error = optional(str)
25
+
26
+
27
+ EditImageResponse = TextToImageResponse
28
+
29
+
30
+ class CompletedTextToImageResponse(TextToImageResponse):
31
+ """Narrowed response from ``run()`` once polling observes completion."""
32
+
33
+ images = required([lambda: Image])
34
+
35
+
36
+ CompletedEditImageResponse = CompletedTextToImageResponse
@@ -0,0 +1,198 @@
1
+ import pytest
2
+
3
+ from runapi.core import config
4
+ from runapi.core.errors import AuthenticationError, ValidationError
5
+ from runapi.gpt_image import GptImageClient
6
+ from runapi.gpt_image.resources.edit_image import EditImage
7
+ from runapi.gpt_image.resources.text_to_image import TextToImage
8
+ from runapi.gpt_image.types import CompletedTextToImageResponse, TextToImageResponse
9
+
10
+
11
+ class FakeHttp:
12
+ def __init__(self, *responses):
13
+ self._responses = list(responses)
14
+ self.calls = []
15
+
16
+ def request(self, method, path, body=None, options=None):
17
+ self.calls.append((method, path, body))
18
+ if self._responses:
19
+ return self._responses.pop(0)
20
+ return {"id": "task_1", "status": "pending"}
21
+
22
+
23
+ @pytest.fixture(autouse=True)
24
+ def reset_config(monkeypatch):
25
+ monkeypatch.delenv("RUNAPI_API_KEY", raising=False)
26
+ monkeypatch.setattr(config, "api_key", None)
27
+ yield
28
+
29
+
30
+ # --- auth -----------------------------------------------------------------
31
+
32
+
33
+ def test_accepts_api_key_parameter():
34
+ assert isinstance(GptImageClient(api_key="k", http_client=FakeHttp()), GptImageClient)
35
+
36
+
37
+ def test_falls_back_to_global(monkeypatch):
38
+ monkeypatch.setattr(config, "api_key", "global-key")
39
+ assert isinstance(GptImageClient(http_client=FakeHttp()), GptImageClient)
40
+
41
+
42
+ def test_falls_back_to_env(monkeypatch):
43
+ monkeypatch.setenv("RUNAPI_API_KEY", "env-key")
44
+ assert isinstance(GptImageClient(http_client=FakeHttp()), GptImageClient)
45
+
46
+
47
+ def test_raises_without_api_key():
48
+ with pytest.raises(AuthenticationError, match="API key is required"):
49
+ GptImageClient()
50
+
51
+
52
+ # --- injection / accessors ------------------------------------------------
53
+
54
+
55
+ def test_uses_injected_http_client():
56
+ fake = FakeHttp()
57
+ client = GptImageClient(api_key="k", http_client=fake)
58
+ assert client.text_to_image._http is fake
59
+ assert client.edit_image._http is fake
60
+
61
+
62
+ def test_exposes_resource_accessors():
63
+ client = GptImageClient(api_key="k", http_client=FakeHttp())
64
+ assert isinstance(client.text_to_image, TextToImage)
65
+ assert isinstance(client.edit_image, EditImage)
66
+
67
+
68
+ # --- request shapes -------------------------------------------------------
69
+
70
+
71
+ def test_text_to_image_create_posts_compacted_body():
72
+ fake = FakeHttp({"id": "t1", "status": "pending"})
73
+ client = GptImageClient(api_key="k", http_client=fake)
74
+ result = client.text_to_image.create(
75
+ model="gpt-image-1.5",
76
+ prompt="hello world",
77
+ aspect_ratio="1:1",
78
+ quality="high",
79
+ seed=None,
80
+ )
81
+ assert fake.calls == [
82
+ (
83
+ "post",
84
+ "/api/v1/gpt_image/text_to_image",
85
+ {"model": "gpt-image-1.5", "prompt": "hello world", "aspect_ratio": "1:1", "quality": "high"},
86
+ ),
87
+ ]
88
+ assert isinstance(result, TextToImageResponse)
89
+
90
+
91
+ def test_text_to_image_get_fetches_by_id():
92
+ fake = FakeHttp({"id": "t1", "status": "processing"})
93
+ client = GptImageClient(api_key="k", http_client=fake)
94
+ client.text_to_image.get("t1")
95
+ assert fake.calls == [("get", "/api/v1/gpt_image/text_to_image/t1", None)]
96
+
97
+
98
+ def test_edit_image_create_posts_compacted_body():
99
+ fake = FakeHttp({"id": "t1", "status": "pending"})
100
+ client = GptImageClient(api_key="k", http_client=fake)
101
+ result = client.edit_image.create(
102
+ model="gpt-image-1.5",
103
+ prompt="oil painting",
104
+ source_image_urls=["https://example.com/a.jpg"],
105
+ aspect_ratio="2:3",
106
+ quality="medium",
107
+ )
108
+ assert fake.calls == [
109
+ (
110
+ "post",
111
+ "/api/v1/gpt_image/edit_image",
112
+ {
113
+ "model": "gpt-image-1.5",
114
+ "prompt": "oil painting",
115
+ "source_image_urls": ["https://example.com/a.jpg"],
116
+ "aspect_ratio": "2:3",
117
+ "quality": "medium",
118
+ },
119
+ ),
120
+ ]
121
+ assert isinstance(result, TextToImageResponse)
122
+
123
+
124
+ def test_edit_image_get_fetches_by_id():
125
+ fake = FakeHttp({"id": "t1", "status": "processing"})
126
+ client = GptImageClient(api_key="k", http_client=fake)
127
+ client.edit_image.get("t1")
128
+ assert fake.calls == [("get", "/api/v1/gpt_image/edit_image/t1", None)]
129
+
130
+
131
+ def test_run_narrows_completed_type():
132
+ fake = FakeHttp(
133
+ {"id": "t1", "status": "pending"},
134
+ {"id": "t1", "status": "completed", "images": [{"url": "https://x/y.png"}]},
135
+ )
136
+ client = GptImageClient(api_key="k", http_client=fake)
137
+ result = client.text_to_image.run(
138
+ model="gpt-image-1.5", prompt="a serene lake", aspect_ratio="1:1", quality="high"
139
+ )
140
+ assert isinstance(result, CompletedTextToImageResponse)
141
+ assert result.images[0].url == "https://x/y.png"
142
+
143
+
144
+ # --- validation -----------------------------------------------------------
145
+
146
+
147
+ def test_requires_model():
148
+ client = GptImageClient(api_key="k", http_client=FakeHttp())
149
+ with pytest.raises(ValidationError, match="model is required"):
150
+ client.text_to_image.create(prompt="hi there")
151
+
152
+
153
+ def test_requires_prompt():
154
+ client = GptImageClient(api_key="k", http_client=FakeHttp())
155
+ with pytest.raises(ValidationError, match="prompt is required"):
156
+ client.text_to_image.create(model="gpt-image-1.5")
157
+
158
+
159
+ def test_rejects_unknown_model():
160
+ client = GptImageClient(api_key="k", http_client=FakeHttp())
161
+ with pytest.raises(ValidationError, match="Invalid model: nope. Must be: gpt-image-1.5"):
162
+ client.text_to_image.create(model="nope", prompt="hi there")
163
+
164
+
165
+ def test_requires_aspect_ratio():
166
+ client = GptImageClient(api_key="k", http_client=FakeHttp())
167
+ with pytest.raises(ValidationError, match="aspect_ratio is required"):
168
+ client.text_to_image.create(model="gpt-image-1.5", prompt="hi there", quality="high")
169
+
170
+
171
+ def test_rejects_invalid_aspect_ratio():
172
+ client = GptImageClient(api_key="k", http_client=FakeHttp())
173
+ with pytest.raises(ValidationError, match="Invalid aspect_ratio"):
174
+ client.text_to_image.create(
175
+ model="gpt-image-1.5", prompt="hi there", aspect_ratio="9:16", quality="high"
176
+ )
177
+
178
+
179
+ def test_requires_quality():
180
+ client = GptImageClient(api_key="k", http_client=FakeHttp())
181
+ with pytest.raises(ValidationError, match="quality is required"):
182
+ client.text_to_image.create(model="gpt-image-1.5", prompt="hi there", aspect_ratio="1:1")
183
+
184
+
185
+ def test_rejects_invalid_quality():
186
+ client = GptImageClient(api_key="k", http_client=FakeHttp())
187
+ with pytest.raises(ValidationError, match="Invalid quality"):
188
+ client.text_to_image.create(
189
+ model="gpt-image-1.5", prompt="hi there", aspect_ratio="1:1", quality="low"
190
+ )
191
+
192
+
193
+ def test_edit_requires_source_image_urls():
194
+ client = GptImageClient(api_key="k", http_client=FakeHttp())
195
+ with pytest.raises(ValidationError, match="source_image_urls is required for image editing"):
196
+ client.edit_image.create(
197
+ model="gpt-image-1.5", prompt="make it pop", aspect_ratio="1:1", quality="high"
198
+ )