runapi-gpt-image-2.5 0.1.1__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.
- runapi_gpt_image_2_5-0.1.1/PKG-INFO +64 -0
- runapi_gpt_image_2_5-0.1.1/README.md +47 -0
- runapi_gpt_image_2_5-0.1.1/pyproject.toml +34 -0
- runapi_gpt_image_2_5-0.1.1/src/runapi/gpt_image_2_5/__init__.py +24 -0
- runapi_gpt_image_2_5-0.1.1/src/runapi/gpt_image_2_5/client.py +28 -0
- runapi_gpt_image_2_5-0.1.1/src/runapi/gpt_image_2_5/contract_gen.py +134 -0
- runapi_gpt_image_2_5-0.1.1/src/runapi/gpt_image_2_5/py.typed +0 -0
- runapi_gpt_image_2_5-0.1.1/src/runapi/gpt_image_2_5/resources/__init__.py +4 -0
- runapi_gpt_image_2_5-0.1.1/src/runapi/gpt_image_2_5/resources/edit_image.py +58 -0
- runapi_gpt_image_2_5-0.1.1/src/runapi/gpt_image_2_5/resources/text_to_image.py +58 -0
- runapi_gpt_image_2_5-0.1.1/src/runapi/gpt_image_2_5/types.py +30 -0
- runapi_gpt_image_2_5-0.1.1/tests/test_client.py +155 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: runapi-gpt-image-2.5
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: RunAPI GPT Image 2.5 SDK for text-to-image and image editing workflows in JavaScript, Python, Ruby, Go, Java, and PHP
|
|
5
|
+
Project-URL: Homepage, https://runapi.ai/models/gpt-image-2.5
|
|
6
|
+
Project-URL: Documentation, https://runapi.ai/docs/resources/sdks
|
|
7
|
+
Project-URL: Source, https://github.com/runapi-ai/gpt-image-2.5-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/runapi-ai/gpt-image-2.5-sdk/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/runapi-ai/gpt-image-2.5-sdk/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Release Notes, https://github.com/runapi-ai/gpt-image-2.5-sdk/releases/tag/python%2Fv0.1.1
|
|
11
|
+
Author-email: RunAPI <contact@runapi.ai>
|
|
12
|
+
License-Expression: Apache-2.0
|
|
13
|
+
Keywords: api,golang,gpt-image-2-5,gpt-image-2-5-api,gradle,image-api,image-generation,java,maven,python,ruby,runapi,runapi-ai,sdk,text-to-image,typescript
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Requires-Dist: runapi-core>=0.8.2
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# GPT Image 2.5 Python SDK for RunAPI
|
|
19
|
+
|
|
20
|
+
The GPT Image 2.5 Python SDK provides text-to-image and image editing clients. Requests select `gpt-image-2.5-flare` or `gpt-image-2.5-sunburst`.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install runapi-gpt-image-2.5
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from runapi.gpt_image_2_5 import GptImage25Client
|
|
32
|
+
|
|
33
|
+
client = GptImage25Client()
|
|
34
|
+
task = client.text_to_image.create(
|
|
35
|
+
model="gpt-image-2.5-flare",
|
|
36
|
+
prompt="A precise product render on white marble",
|
|
37
|
+
aspect_ratio="1:1",
|
|
38
|
+
output_resolution="1k",
|
|
39
|
+
)
|
|
40
|
+
status = client.text_to_image.get(task.id)
|
|
41
|
+
|
|
42
|
+
edit = client.edit_image.create(
|
|
43
|
+
model="gpt-image-2.5-sunburst",
|
|
44
|
+
prompt="Change the background to a botanical studio",
|
|
45
|
+
source_image_urls=["https://cdn.runapi.ai/public/samples/image.jpg"],
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Use `create` to submit, `get` to fetch task status, and `run` to create and poll until completion. Keep `RUNAPI_API_KEY` in the environment or a secret manager.
|
|
50
|
+
|
|
51
|
+
RunAPI-generated file URLs are temporary. Store generated files in durable storage within 7 days.
|
|
52
|
+
|
|
53
|
+
## Links
|
|
54
|
+
|
|
55
|
+
- Model overview: https://runapi.ai/models/gpt-image-2.5
|
|
56
|
+
- Flare: https://runapi.ai/models/gpt-image-2.5/flare
|
|
57
|
+
- Sunburst: https://runapi.ai/models/gpt-image-2.5/sunburst
|
|
58
|
+
- API reference: https://runapi.ai/docs/api/gpt-image-2-5/text-to-image
|
|
59
|
+
- SDK docs: https://runapi.ai/docs/resources/sdks
|
|
60
|
+
- Repository: https://github.com/runapi-ai/gpt-image-2.5-sdk
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# GPT Image 2.5 Python SDK for RunAPI
|
|
2
|
+
|
|
3
|
+
The GPT Image 2.5 Python SDK provides text-to-image and image editing clients. Requests select `gpt-image-2.5-flare` or `gpt-image-2.5-sunburst`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install runapi-gpt-image-2.5
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from runapi.gpt_image_2_5 import GptImage25Client
|
|
15
|
+
|
|
16
|
+
client = GptImage25Client()
|
|
17
|
+
task = client.text_to_image.create(
|
|
18
|
+
model="gpt-image-2.5-flare",
|
|
19
|
+
prompt="A precise product render on white marble",
|
|
20
|
+
aspect_ratio="1:1",
|
|
21
|
+
output_resolution="1k",
|
|
22
|
+
)
|
|
23
|
+
status = client.text_to_image.get(task.id)
|
|
24
|
+
|
|
25
|
+
edit = client.edit_image.create(
|
|
26
|
+
model="gpt-image-2.5-sunburst",
|
|
27
|
+
prompt="Change the background to a botanical studio",
|
|
28
|
+
source_image_urls=["https://cdn.runapi.ai/public/samples/image.jpg"],
|
|
29
|
+
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Use `create` to submit, `get` to fetch task status, and `run` to create and poll until completion. Keep `RUNAPI_API_KEY` in the environment or a secret manager.
|
|
33
|
+
|
|
34
|
+
RunAPI-generated file URLs are temporary. Store generated files in durable storage within 7 days.
|
|
35
|
+
|
|
36
|
+
## Links
|
|
37
|
+
|
|
38
|
+
- Model overview: https://runapi.ai/models/gpt-image-2.5
|
|
39
|
+
- Flare: https://runapi.ai/models/gpt-image-2.5/flare
|
|
40
|
+
- Sunburst: https://runapi.ai/models/gpt-image-2.5/sunburst
|
|
41
|
+
- API reference: https://runapi.ai/docs/api/gpt-image-2-5/text-to-image
|
|
42
|
+
- SDK docs: https://runapi.ai/docs/resources/sdks
|
|
43
|
+
- Repository: https://github.com/runapi-ai/gpt-image-2.5-sdk
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "runapi-gpt-image-2.5"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "RunAPI GPT Image 2.5 SDK for text-to-image and image editing workflows in JavaScript, Python, Ruby, Go, Java, and PHP"
|
|
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", "runapi-ai", "gpt-image-2-5", "api", "sdk", "typescript", "python", "ruby", "golang", "java", "maven", "gradle", "image-generation", "text-to-image", "image-api", "gpt-image-2-5-api"]
|
|
14
|
+
dependencies = ["runapi-core>=0.8.2"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://runapi.ai/models/gpt-image-2.5"
|
|
18
|
+
Documentation = "https://runapi.ai/docs/resources/sdks"
|
|
19
|
+
Source = "https://github.com/runapi-ai/gpt-image-2.5-sdk"
|
|
20
|
+
Issues = "https://github.com/runapi-ai/gpt-image-2.5-sdk/issues"
|
|
21
|
+
Changelog = "https://github.com/runapi-ai/gpt-image-2.5-sdk/blob/main/CHANGELOG.md"
|
|
22
|
+
"Release Notes" = "https://github.com/runapi-ai/gpt-image-2.5-sdk/releases/tag/python%2Fv0.1.1"
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/runapi"]
|
|
26
|
+
|
|
27
|
+
[tool.uv]
|
|
28
|
+
package = true
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = ["pytest>=8"]
|
|
32
|
+
|
|
33
|
+
[tool.runapi]
|
|
34
|
+
slug = "gpt-image-2.5"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""GPT Image 2.5 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 GptImage25Client
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"GptImage25Client",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"InsufficientCreditsError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"TaskFailedError",
|
|
23
|
+
"TaskTimeoutError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""GPT Image 2.5 client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from runapi.core import ProviderClient
|
|
8
|
+
|
|
9
|
+
from .resources.edit_image import EditImage
|
|
10
|
+
from .resources.text_to_image import TextToImage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GptImage25Client(ProviderClient):
|
|
14
|
+
"""GPT Image 2.5 text-to-image and edit-image client.
|
|
15
|
+
|
|
16
|
+
Example::
|
|
17
|
+
|
|
18
|
+
client = GptImage25Client(api_key="sk-...")
|
|
19
|
+
result = client.text_to_image.run(
|
|
20
|
+
model="gpt-image-2.5-flare", prompt="A futuristic cityscape at night"
|
|
21
|
+
)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, api_key: Optional[str] = None, **options: Any) -> None:
|
|
25
|
+
super().__init__(api_key, **options)
|
|
26
|
+
http = self._http
|
|
27
|
+
self.text_to_image = TextToImage(http)
|
|
28
|
+
self.edit_image = EditImage(http)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
CONTRACT = {
|
|
2
|
+
"edit-image": {
|
|
3
|
+
"models": ["gpt-image-2.5-flare", "gpt-image-2.5-sunburst"],
|
|
4
|
+
"fields_by_model": {
|
|
5
|
+
"gpt-image-2.5-flare": {
|
|
6
|
+
"aspect_ratio": {
|
|
7
|
+
"enum": ["auto", "1:1", "3:2", "2:3", "4:3", "3:4", "5:4", "4:5", "16:9", "9:16", "2:1", "1:2", "3:1", "1:3", "21:9", "9:21"]
|
|
8
|
+
},
|
|
9
|
+
"output_resolution": {
|
|
10
|
+
"enum": ["1k", "2k", "4k"]
|
|
11
|
+
},
|
|
12
|
+
"prompt": {
|
|
13
|
+
"required": True
|
|
14
|
+
},
|
|
15
|
+
"source_image_urls": {
|
|
16
|
+
"required": True,
|
|
17
|
+
"max_items": 16
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"gpt-image-2.5-sunburst": {
|
|
21
|
+
"aspect_ratio": {
|
|
22
|
+
"enum": ["auto", "1:1", "3:2", "2:3", "4:3", "3:4", "5:4", "4:5", "16:9", "9:16", "2:1", "1:2", "3:1", "1:3", "21:9", "9:21"]
|
|
23
|
+
},
|
|
24
|
+
"output_resolution": {
|
|
25
|
+
"enum": ["1k", "2k", "4k"]
|
|
26
|
+
},
|
|
27
|
+
"prompt": {
|
|
28
|
+
"required": True
|
|
29
|
+
},
|
|
30
|
+
"source_image_urls": {
|
|
31
|
+
"required": True,
|
|
32
|
+
"max_items": 16
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"rules": [{
|
|
37
|
+
"when": {
|
|
38
|
+
"aspect_ratio": "1:1",
|
|
39
|
+
"output_resolution": "4k"
|
|
40
|
+
},
|
|
41
|
+
"forbidden": ["aspect_ratio"]
|
|
42
|
+
}, {
|
|
43
|
+
"when": {
|
|
44
|
+
"aspect_ratio": "auto",
|
|
45
|
+
"output_resolution": "4k"
|
|
46
|
+
},
|
|
47
|
+
"forbidden": ["aspect_ratio"]
|
|
48
|
+
}, {
|
|
49
|
+
"when": {
|
|
50
|
+
"aspect_ratio": {
|
|
51
|
+
"present": False
|
|
52
|
+
},
|
|
53
|
+
"output_resolution": "4k"
|
|
54
|
+
},
|
|
55
|
+
"forbidden": ["output_resolution"]
|
|
56
|
+
}, {
|
|
57
|
+
"when": {
|
|
58
|
+
"aspect_ratio": "auto",
|
|
59
|
+
"output_resolution": "2k"
|
|
60
|
+
},
|
|
61
|
+
"forbidden": ["aspect_ratio"]
|
|
62
|
+
}, {
|
|
63
|
+
"when": {
|
|
64
|
+
"aspect_ratio": {
|
|
65
|
+
"present": False
|
|
66
|
+
},
|
|
67
|
+
"output_resolution": "2k"
|
|
68
|
+
},
|
|
69
|
+
"forbidden": ["output_resolution"]
|
|
70
|
+
}]
|
|
71
|
+
},
|
|
72
|
+
"text-to-image": {
|
|
73
|
+
"models": ["gpt-image-2.5-flare", "gpt-image-2.5-sunburst"],
|
|
74
|
+
"fields_by_model": {
|
|
75
|
+
"gpt-image-2.5-flare": {
|
|
76
|
+
"aspect_ratio": {
|
|
77
|
+
"enum": ["auto", "1:1", "3:2", "2:3", "4:3", "3:4", "5:4", "4:5", "16:9", "9:16", "2:1", "1:2", "3:1", "1:3", "21:9", "9:21"]
|
|
78
|
+
},
|
|
79
|
+
"output_resolution": {
|
|
80
|
+
"enum": ["1k", "2k", "4k"]
|
|
81
|
+
},
|
|
82
|
+
"prompt": {
|
|
83
|
+
"required": True
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"gpt-image-2.5-sunburst": {
|
|
87
|
+
"aspect_ratio": {
|
|
88
|
+
"enum": ["auto", "1:1", "3:2", "2:3", "4:3", "3:4", "5:4", "4:5", "16:9", "9:16", "2:1", "1:2", "3:1", "1:3", "21:9", "9:21"]
|
|
89
|
+
},
|
|
90
|
+
"output_resolution": {
|
|
91
|
+
"enum": ["1k", "2k", "4k"]
|
|
92
|
+
},
|
|
93
|
+
"prompt": {
|
|
94
|
+
"required": True
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"rules": [{
|
|
99
|
+
"when": {
|
|
100
|
+
"aspect_ratio": "1:1",
|
|
101
|
+
"output_resolution": "4k"
|
|
102
|
+
},
|
|
103
|
+
"forbidden": ["aspect_ratio"]
|
|
104
|
+
}, {
|
|
105
|
+
"when": {
|
|
106
|
+
"aspect_ratio": "auto",
|
|
107
|
+
"output_resolution": "4k"
|
|
108
|
+
},
|
|
109
|
+
"forbidden": ["aspect_ratio"]
|
|
110
|
+
}, {
|
|
111
|
+
"when": {
|
|
112
|
+
"aspect_ratio": {
|
|
113
|
+
"present": False
|
|
114
|
+
},
|
|
115
|
+
"output_resolution": "4k"
|
|
116
|
+
},
|
|
117
|
+
"forbidden": ["output_resolution"]
|
|
118
|
+
}, {
|
|
119
|
+
"when": {
|
|
120
|
+
"aspect_ratio": "auto",
|
|
121
|
+
"output_resolution": "2k"
|
|
122
|
+
},
|
|
123
|
+
"forbidden": ["aspect_ratio"]
|
|
124
|
+
}, {
|
|
125
|
+
"when": {
|
|
126
|
+
"aspect_ratio": {
|
|
127
|
+
"present": False
|
|
128
|
+
},
|
|
129
|
+
"output_resolution": "2k"
|
|
130
|
+
},
|
|
131
|
+
"forbidden": ["output_resolution"]
|
|
132
|
+
}]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""GPT Image 2.5 edit-image resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource, RequestOptions
|
|
8
|
+
|
|
9
|
+
from ..contract_gen import CONTRACT
|
|
10
|
+
from ..types import (
|
|
11
|
+
CompletedEditImageResponse,
|
|
12
|
+
EditImageResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EditImage(Resource):
|
|
17
|
+
"""Edit images from a prompt and source images with GPT Image 2.5."""
|
|
18
|
+
|
|
19
|
+
ENDPOINT = "/api/v1/gpt_image_2_5/edit_image"
|
|
20
|
+
|
|
21
|
+
RESPONSE_CLASS = EditImageResponse
|
|
22
|
+
COMPLETED_RESPONSE_CLASS = CompletedEditImageResponse
|
|
23
|
+
|
|
24
|
+
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
25
|
+
"""Create an edit-image task and poll until it completes.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
**params: Edit-image parameters (model, prompt, ...).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The completed edit-image response.
|
|
32
|
+
"""
|
|
33
|
+
task = self.create(options=options, **params)
|
|
34
|
+
return self._poll_until_complete(lambda: self.get(task.id, options=options))
|
|
35
|
+
|
|
36
|
+
def create(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
37
|
+
"""Create an edit-image task and return immediately with an id.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
**params: Edit-image parameters (model, prompt, ...).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The task creation result with an id.
|
|
44
|
+
"""
|
|
45
|
+
compacted = self._compact_params(params)
|
|
46
|
+
self._validate_contract(CONTRACT["edit-image"], compacted)
|
|
47
|
+
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
48
|
+
|
|
49
|
+
def get(self, id: str, options: Optional[RequestOptions] = None) -> Any:
|
|
50
|
+
"""Fetch the current status of an edit-image task.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
id: Task id.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The current edit-image status.
|
|
57
|
+
"""
|
|
58
|
+
return self._request("get", f"{self.ENDPOINT}/{id}", options=options)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""GPT Image 2.5 text-to-image resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource, RequestOptions
|
|
8
|
+
|
|
9
|
+
from ..contract_gen import CONTRACT
|
|
10
|
+
from ..types import (
|
|
11
|
+
CompletedTextToImageResponse,
|
|
12
|
+
TextToImageResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TextToImage(Resource):
|
|
17
|
+
"""Generate images from text prompts with GPT Image 2.5."""
|
|
18
|
+
|
|
19
|
+
ENDPOINT = "/api/v1/gpt_image_2_5/text_to_image"
|
|
20
|
+
|
|
21
|
+
RESPONSE_CLASS = TextToImageResponse
|
|
22
|
+
COMPLETED_RESPONSE_CLASS = CompletedTextToImageResponse
|
|
23
|
+
|
|
24
|
+
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
25
|
+
"""Create a text-to-image task and poll until it completes.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
**params: Text-to-image parameters (model, prompt, ...).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The completed text-to-image response.
|
|
32
|
+
"""
|
|
33
|
+
task = self.create(options=options, **params)
|
|
34
|
+
return self._poll_until_complete(lambda: self.get(task.id, options=options))
|
|
35
|
+
|
|
36
|
+
def create(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
37
|
+
"""Create a text-to-image task and return immediately with an id.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
**params: Text-to-image parameters (model, prompt, ...).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The task creation result with an id.
|
|
44
|
+
"""
|
|
45
|
+
compacted = self._compact_params(params)
|
|
46
|
+
self._validate_contract(CONTRACT["text-to-image"], compacted)
|
|
47
|
+
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
48
|
+
|
|
49
|
+
def get(self, id: str, options: Optional[RequestOptions] = None) -> Any:
|
|
50
|
+
"""Fetch the current status of a text-to-image task.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
id: Task id.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The current text-to-image status.
|
|
57
|
+
"""
|
|
58
|
+
return self._request("get", f"{self.ENDPOINT}/{id}", options=options)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""GPT Image 2.5 model lists, enums, and response models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from runapi.core import BaseModel, TaskResponse, optional, required
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Image(BaseModel):
|
|
9
|
+
url = optional(str)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TextToImageResponse(TaskResponse):
|
|
13
|
+
"""Task status/result for GPT Image 2.5 text-to-image."""
|
|
14
|
+
id = required(str)
|
|
15
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
16
|
+
task_replayed = optional(bool)
|
|
17
|
+
images = optional([lambda: Image])
|
|
18
|
+
error = optional(str)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
EditImageResponse = TextToImageResponse
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CompletedTextToImageResponse(TextToImageResponse):
|
|
25
|
+
"""Narrowed response from ``run()`` once polling observes completion."""
|
|
26
|
+
|
|
27
|
+
images = required([lambda: Image])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
CompletedEditImageResponse = CompletedTextToImageResponse
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from runapi.core import config
|
|
4
|
+
from runapi.core.errors import AuthenticationError, ValidationError
|
|
5
|
+
from runapi.gpt_image_2_5 import GptImage25Client
|
|
6
|
+
from runapi.gpt_image_2_5.resources.edit_image import EditImage
|
|
7
|
+
from runapi.gpt_image_2_5.resources.text_to_image import TextToImage
|
|
8
|
+
from runapi.gpt_image_2_5.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(GptImage25Client(api_key="k", http_client=FakeHttp()), GptImage25Client)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_falls_back_to_global(monkeypatch):
|
|
38
|
+
monkeypatch.setattr(config, "api_key", "global-key")
|
|
39
|
+
assert isinstance(GptImage25Client(http_client=FakeHttp()), GptImage25Client)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_falls_back_to_env(monkeypatch):
|
|
43
|
+
monkeypatch.setenv("RUNAPI_API_KEY", "env-key")
|
|
44
|
+
assert isinstance(GptImage25Client(http_client=FakeHttp()), GptImage25Client)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_raises_without_api_key():
|
|
48
|
+
with pytest.raises(AuthenticationError, match="API key is required"):
|
|
49
|
+
GptImage25Client()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# --- injection / accessors ------------------------------------------------
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_uses_injected_http_client():
|
|
56
|
+
fake = FakeHttp()
|
|
57
|
+
client = GptImage25Client(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 = GptImage25Client(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_create_posts_compacted_body():
|
|
72
|
+
fake = FakeHttp({"id": "t1", "status": "pending", "task_replayed": True})
|
|
73
|
+
client = GptImage25Client(api_key="k", http_client=fake)
|
|
74
|
+
result = client.text_to_image.create(
|
|
75
|
+
model="gpt-image-2.5-flare", prompt="hello world", aspect_ratio="1:1", output_resolution=None
|
|
76
|
+
)
|
|
77
|
+
assert fake.calls == [
|
|
78
|
+
("post", "/api/v1/gpt_image_2_5/text_to_image", {"model": "gpt-image-2.5-flare", "prompt": "hello world", "aspect_ratio": "1:1"}),
|
|
79
|
+
]
|
|
80
|
+
assert isinstance(result, TextToImageResponse)
|
|
81
|
+
assert result.task_replayed is True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_get_fetches_by_id():
|
|
85
|
+
fake = FakeHttp({"id": "t1", "status": "processing"})
|
|
86
|
+
client = GptImage25Client(api_key="k", http_client=fake)
|
|
87
|
+
client.text_to_image.get("t1")
|
|
88
|
+
assert fake.calls == [("get", "/api/v1/gpt_image_2_5/text_to_image/t1", None)]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_edit_create_posts_compacted_body():
|
|
92
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
93
|
+
client = GptImage25Client(api_key="k", http_client=fake)
|
|
94
|
+
client.edit_image.create(
|
|
95
|
+
model="gpt-image-2.5-flare",
|
|
96
|
+
prompt="oil painting",
|
|
97
|
+
source_image_urls=["https://x/a.png"],
|
|
98
|
+
)
|
|
99
|
+
assert fake.calls == [
|
|
100
|
+
(
|
|
101
|
+
"post",
|
|
102
|
+
"/api/v1/gpt_image_2_5/edit_image",
|
|
103
|
+
{"model": "gpt-image-2.5-flare", "prompt": "oil painting", "source_image_urls": ["https://x/a.png"]},
|
|
104
|
+
),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_run_narrows_completed_type():
|
|
109
|
+
fake = FakeHttp(
|
|
110
|
+
{"id": "t1", "status": "pending"},
|
|
111
|
+
{"id": "t1", "status": "completed", "images": [{"url": "https://x/y.png"}]},
|
|
112
|
+
)
|
|
113
|
+
client = GptImage25Client(api_key="k", http_client=fake)
|
|
114
|
+
result = client.text_to_image.run(model="gpt-image-2.5-flare", prompt="a serene lake")
|
|
115
|
+
assert isinstance(result, CompletedTextToImageResponse)
|
|
116
|
+
assert result.images[0].url == "https://x/y.png"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --- validation -----------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_requires_model():
|
|
123
|
+
client = GptImage25Client(api_key="k", http_client=FakeHttp())
|
|
124
|
+
with pytest.raises(ValidationError, match="model must be one of: gpt-image-2.5-flare"):
|
|
125
|
+
client.text_to_image.create(prompt="hi there")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_requires_prompt():
|
|
129
|
+
client = GptImage25Client(api_key="k", http_client=FakeHttp())
|
|
130
|
+
with pytest.raises(ValidationError, match="prompt is required"):
|
|
131
|
+
client.text_to_image.create(model="gpt-image-2.5-flare")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_rejects_unknown_model():
|
|
135
|
+
client = GptImage25Client(api_key="k", http_client=FakeHttp())
|
|
136
|
+
with pytest.raises(ValidationError, match="model must be one of: gpt-image-2.5-flare"):
|
|
137
|
+
client.text_to_image.create(model="nope", prompt="hi there")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_rejects_invalid_aspect_ratio():
|
|
141
|
+
client = GptImage25Client(api_key="k", http_client=FakeHttp())
|
|
142
|
+
with pytest.raises(ValidationError, match="aspect_ratio must be one of"):
|
|
143
|
+
client.text_to_image.create(model="gpt-image-2.5-flare", prompt="hi there", aspect_ratio="5:5")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_rejects_invalid_output_resolution():
|
|
147
|
+
client = GptImage25Client(api_key="k", http_client=FakeHttp())
|
|
148
|
+
with pytest.raises(ValidationError, match="output_resolution must be one of"):
|
|
149
|
+
client.text_to_image.create(model="gpt-image-2.5-flare", prompt="hi there", output_resolution="8k")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_edit_requires_source_image_urls():
|
|
153
|
+
client = GptImage25Client(api_key="k", http_client=FakeHttp())
|
|
154
|
+
with pytest.raises(ValidationError, match="source_image_urls is required"):
|
|
155
|
+
client.edit_image.create(model="gpt-image-2.5-flare", prompt="make it pop")
|