runapi-flux-2 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.
- runapi_flux_2-0.1.0/PKG-INFO +78 -0
- runapi_flux_2-0.1.0/README.md +65 -0
- runapi_flux_2-0.1.0/pyproject.toml +30 -0
- runapi_flux_2-0.1.0/src/runapi/flux_2/__init__.py +24 -0
- runapi_flux_2-0.1.0/src/runapi/flux_2/client.py +29 -0
- runapi_flux_2-0.1.0/src/runapi/flux_2/py.typed +0 -0
- runapi_flux_2-0.1.0/src/runapi/flux_2/resources/__init__.py +4 -0
- runapi_flux_2-0.1.0/src/runapi/flux_2/resources/remix_image.py +78 -0
- runapi_flux_2-0.1.0/src/runapi/flux_2/resources/text_to_image.py +74 -0
- runapi_flux_2-0.1.0/src/runapi/flux_2/types.py +48 -0
- runapi_flux_2-0.1.0/tests/test_client.py +153 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runapi-flux-2
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Flux 2 text-to-image and remix-image client for RunAPI
|
|
5
|
+
Project-URL: Homepage, https://runapi.ai/models/flux-2
|
|
6
|
+
Project-URL: Documentation, https://runapi.ai/docs#sdk-flux-2
|
|
7
|
+
Author-email: RunAPI <contact@runapi.ai>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: ai,flux,flux-2,runapi,sdk,text-to-image
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: runapi-core
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Flux 2 Python SDK for RunAPI
|
|
15
|
+
|
|
16
|
+
The flux 2 Python SDK is the language-specific package for Flux 2 on RunAPI. Use this flux 2 package for text-to-image, remix-image, and creative production flows when your application needs JSON request bodies, task status lookup, and consistent RunAPI errors in Python.
|
|
17
|
+
|
|
18
|
+
This flux 2 README is the Python package guide inside the public `flux-2-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/flux-2; for API reference, use https://runapi.ai/docs#flux-2; for SDK docs, use https://runapi.ai/docs#sdk-flux-2.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install runapi-flux-2
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from runapi.flux_2 import Flux2Client
|
|
30
|
+
|
|
31
|
+
client = Flux2Client() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
32
|
+
|
|
33
|
+
task = client.text_to_image.create(
|
|
34
|
+
model="flux-2-pro-text-to-image",
|
|
35
|
+
prompt="A cinematic product photo on warm paper",
|
|
36
|
+
aspect_ratio="1:1",
|
|
37
|
+
)
|
|
38
|
+
status = client.text_to_image.get(task.id)
|
|
39
|
+
|
|
40
|
+
remix = client.remix_image.create(
|
|
41
|
+
model="flux-2-pro-remix-image",
|
|
42
|
+
prompt="Turn this product shot into a warm editorial photo",
|
|
43
|
+
source_image_urls=["https://example.com/source.jpg"],
|
|
44
|
+
aspect_ratio="auto",
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use `create` when you want to submit a task and return quickly, `get` when you need the latest task state, and `run` when a script should create and poll until completion:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
result = client.text_to_image.run(
|
|
52
|
+
model="flux-2-pro-text-to-image",
|
|
53
|
+
prompt="A futuristic cityscape at dusk",
|
|
54
|
+
)
|
|
55
|
+
print(result.images[0].url)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a worker is not held open.
|
|
59
|
+
|
|
60
|
+
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.
|
|
61
|
+
|
|
62
|
+
## Language notes
|
|
63
|
+
|
|
64
|
+
Pass parameters as keyword arguments and catch the `runapi.flux_2` error classes when building image jobs or scripts. The available resources are `text_to_image` and `remix_image`. Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API keys or callback secrets.
|
|
65
|
+
|
|
66
|
+
## Links
|
|
67
|
+
|
|
68
|
+
- Model page: https://runapi.ai/models/flux-2
|
|
69
|
+
- SDK docs: https://runapi.ai/docs#sdk-flux-2
|
|
70
|
+
- Product docs: https://runapi.ai/docs#flux-2
|
|
71
|
+
- Pricing and rate limits: https://runapi.ai/models/flux-2/pro-text-to-image
|
|
72
|
+
- Provider comparison: https://runapi.ai/providers/black-forest-labs
|
|
73
|
+
- Full catalog: https://runapi.ai/models
|
|
74
|
+
- Repository: https://github.com/runapi-ai/flux-2-sdk
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Flux 2 Python SDK for RunAPI
|
|
2
|
+
|
|
3
|
+
The flux 2 Python SDK is the language-specific package for Flux 2 on RunAPI. Use this flux 2 package for text-to-image, remix-image, and creative production flows when your application needs JSON request bodies, task status lookup, and consistent RunAPI errors in Python.
|
|
4
|
+
|
|
5
|
+
This flux 2 README is the Python package guide inside the public `flux-2-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/flux-2; for API reference, use https://runapi.ai/docs#flux-2; for SDK docs, use https://runapi.ai/docs#sdk-flux-2.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install runapi-flux-2
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from runapi.flux_2 import Flux2Client
|
|
17
|
+
|
|
18
|
+
client = Flux2Client() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
19
|
+
|
|
20
|
+
task = client.text_to_image.create(
|
|
21
|
+
model="flux-2-pro-text-to-image",
|
|
22
|
+
prompt="A cinematic product photo on warm paper",
|
|
23
|
+
aspect_ratio="1:1",
|
|
24
|
+
)
|
|
25
|
+
status = client.text_to_image.get(task.id)
|
|
26
|
+
|
|
27
|
+
remix = client.remix_image.create(
|
|
28
|
+
model="flux-2-pro-remix-image",
|
|
29
|
+
prompt="Turn this product shot into a warm editorial photo",
|
|
30
|
+
source_image_urls=["https://example.com/source.jpg"],
|
|
31
|
+
aspect_ratio="auto",
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use `create` when you want to submit a task and return quickly, `get` when you need the latest task state, and `run` when a script should create and poll until completion:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
result = client.text_to_image.run(
|
|
39
|
+
model="flux-2-pro-text-to-image",
|
|
40
|
+
prompt="A futuristic cityscape at dusk",
|
|
41
|
+
)
|
|
42
|
+
print(result.images[0].url)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a worker is not held open.
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
## Language notes
|
|
50
|
+
|
|
51
|
+
Pass parameters as keyword arguments and catch the `runapi.flux_2` error classes when building image jobs or scripts. The available resources are `text_to_image` and `remix_image`. Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API keys or callback secrets.
|
|
52
|
+
|
|
53
|
+
## Links
|
|
54
|
+
|
|
55
|
+
- Model page: https://runapi.ai/models/flux-2
|
|
56
|
+
- SDK docs: https://runapi.ai/docs#sdk-flux-2
|
|
57
|
+
- Product docs: https://runapi.ai/docs#flux-2
|
|
58
|
+
- Pricing and rate limits: https://runapi.ai/models/flux-2/pro-text-to-image
|
|
59
|
+
- Provider comparison: https://runapi.ai/providers/black-forest-labs
|
|
60
|
+
- Full catalog: https://runapi.ai/models
|
|
61
|
+
- Repository: https://github.com/runapi-ai/flux-2-sdk
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
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-flux-2"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Flux 2 text-to-image and remix-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", "flux", "flux-2", "text-to-image", "ai", "sdk"]
|
|
14
|
+
dependencies = ["runapi-core"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://runapi.ai/models/flux-2"
|
|
18
|
+
Documentation = "https://runapi.ai/docs#sdk-flux-2"
|
|
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
|
+
"""Flux 2 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 Flux2Client
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Flux2Client",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"InsufficientCreditsError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"TaskFailedError",
|
|
23
|
+
"TaskTimeoutError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Flux 2 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.remix_image import RemixImage
|
|
10
|
+
from .resources.text_to_image import TextToImage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Flux2Client:
|
|
14
|
+
"""Flux 2 text-to-image and remix-image client.
|
|
15
|
+
|
|
16
|
+
Example::
|
|
17
|
+
|
|
18
|
+
client = Flux2Client(api_key="sk-...")
|
|
19
|
+
result = client.text_to_image.run(
|
|
20
|
+
model="flux-2-pro-text-to-image", prompt="A futuristic cityscape"
|
|
21
|
+
)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, api_key: Optional[str] = None, **options: Any) -> None:
|
|
25
|
+
resolved_api_key = resolve_api_key(api_key)
|
|
26
|
+
client_options = ClientOptions(api_key=resolved_api_key, **options)
|
|
27
|
+
http = client_options.http_client or HttpClient(client_options)
|
|
28
|
+
self.text_to_image = TextToImage(http)
|
|
29
|
+
self.remix_image = RemixImage(http)
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Flux 2 remix-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
|
+
OUTPUT_RESOLUTIONS,
|
|
11
|
+
REMIX_ASPECT_RATIOS,
|
|
12
|
+
REMIX_MODELS,
|
|
13
|
+
CompletedRemixImageResponse,
|
|
14
|
+
RemixImageResponse,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RemixImage(Resource):
|
|
19
|
+
"""Remix one or more source images into a new image with Flux 2 models."""
|
|
20
|
+
|
|
21
|
+
ENDPOINT = "/api/v1/flux_2/remix_image"
|
|
22
|
+
|
|
23
|
+
RESPONSE_CLASS = RemixImageResponse
|
|
24
|
+
COMPLETED_RESPONSE_CLASS = CompletedRemixImageResponse
|
|
25
|
+
|
|
26
|
+
def run(self, **params: Any) -> Any:
|
|
27
|
+
"""Create a remix-image task and poll until it completes.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
**params: Remix-image parameters (model, prompt, ...).
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The completed remix-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 remix-image task and return immediately with an id.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
**params: Remix-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 remix-image task.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
id: Task id.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The current remix-image status.
|
|
59
|
+
"""
|
|
60
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
61
|
+
|
|
62
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
63
|
+
model = params.get("model")
|
|
64
|
+
if not model:
|
|
65
|
+
raise ValidationError("model is required")
|
|
66
|
+
if model not in REMIX_MODELS:
|
|
67
|
+
joined = ", ".join(REMIX_MODELS)
|
|
68
|
+
raise ValidationError(f"Invalid model: {model}. Must be one of: {joined}")
|
|
69
|
+
|
|
70
|
+
if not params.get("prompt"):
|
|
71
|
+
raise ValidationError("prompt is required")
|
|
72
|
+
|
|
73
|
+
self._validate_optional(params, "aspect_ratio", REMIX_ASPECT_RATIOS)
|
|
74
|
+
self._validate_optional(params, "output_resolution", OUTPUT_RESOLUTIONS)
|
|
75
|
+
|
|
76
|
+
urls = params.get("source_image_urls")
|
|
77
|
+
if not urls:
|
|
78
|
+
raise ValidationError("source_image_urls is required")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Flux 2 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
|
+
OUTPUT_RESOLUTIONS,
|
|
12
|
+
TEXT_TO_IMAGE_MODELS,
|
|
13
|
+
CompletedTextToImageResponse,
|
|
14
|
+
TextToImageResponse,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TextToImage(Resource):
|
|
19
|
+
"""Generate images from text prompts with Flux 2 models."""
|
|
20
|
+
|
|
21
|
+
ENDPOINT = "/api/v1/flux_2/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 TEXT_TO_IMAGE_MODELS:
|
|
70
|
+
joined = ", ".join(TEXT_TO_IMAGE_MODELS)
|
|
71
|
+
raise ValidationError(f"Invalid model: {model}. Must be one of: {joined}")
|
|
72
|
+
|
|
73
|
+
self._validate_optional(params, "aspect_ratio", ASPECT_RATIOS)
|
|
74
|
+
self._validate_optional(params, "output_resolution", OUTPUT_RESOLUTIONS)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Flux 2 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 = [
|
|
8
|
+
"flux-2-pro-text-to-image",
|
|
9
|
+
"flux-2-pro-remix-image",
|
|
10
|
+
"flux-2-flex-text-to-image",
|
|
11
|
+
"flux-2-flex-remix-image",
|
|
12
|
+
]
|
|
13
|
+
TEXT_TO_IMAGE_MODELS = ["flux-2-pro-text-to-image", "flux-2-flex-text-to-image"]
|
|
14
|
+
REMIX_MODELS = ["flux-2-pro-remix-image", "flux-2-flex-remix-image"]
|
|
15
|
+
|
|
16
|
+
ASPECT_RATIOS = ["1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3"]
|
|
17
|
+
REMIX_ASPECT_RATIOS = ["1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "auto"]
|
|
18
|
+
OUTPUT_RESOLUTIONS = ["1k", "2k"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Image(BaseModel):
|
|
22
|
+
url = optional(str)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TextToImageResponse(TaskResponse):
|
|
26
|
+
"""Task status/result for Flux 2 text-to-image."""
|
|
27
|
+
id = required(str)
|
|
28
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
29
|
+
images = optional([lambda: Image])
|
|
30
|
+
error = optional(str)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CompletedTextToImageResponse(TextToImageResponse):
|
|
34
|
+
"""Returned by ``text_to_image.run()`` once polling observes completion.
|
|
35
|
+
|
|
36
|
+
``images`` is required so callers never have to null-check it on success.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
images = required([lambda: Image])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RemixImageResponse(TextToImageResponse):
|
|
43
|
+
"""Task status/result for Flux 2 remix-image."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CompletedRemixImageResponse(CompletedTextToImageResponse):
|
|
48
|
+
pass
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from runapi.core import config
|
|
4
|
+
from runapi.core.errors import AuthenticationError, ValidationError
|
|
5
|
+
from runapi.flux_2 import Flux2Client
|
|
6
|
+
from runapi.flux_2.resources.remix_image import RemixImage
|
|
7
|
+
from runapi.flux_2.resources.text_to_image import TextToImage
|
|
8
|
+
from runapi.flux_2.types import CompletedTextToImageResponse, TextToImageResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FakeHttp:
|
|
12
|
+
"""Records (method, path, body) and replays preset responses by call order."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *responses):
|
|
15
|
+
self._responses = list(responses)
|
|
16
|
+
self.calls = []
|
|
17
|
+
|
|
18
|
+
def request(self, method, path, body=None, options=None):
|
|
19
|
+
self.calls.append((method, path, body))
|
|
20
|
+
if self._responses:
|
|
21
|
+
return self._responses.pop(0)
|
|
22
|
+
return {"id": "task_1", "status": "pending"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture(autouse=True)
|
|
26
|
+
def reset_config(monkeypatch):
|
|
27
|
+
monkeypatch.delenv("RUNAPI_API_KEY", raising=False)
|
|
28
|
+
monkeypatch.setattr(config, "api_key", None)
|
|
29
|
+
yield
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# --- authentication -------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_accepts_api_key_parameter():
|
|
36
|
+
assert isinstance(Flux2Client(api_key="param-key", http_client=FakeHttp()), Flux2Client)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_falls_back_to_global(monkeypatch):
|
|
40
|
+
monkeypatch.setattr(config, "api_key", "global-key")
|
|
41
|
+
assert isinstance(Flux2Client(http_client=FakeHttp()), Flux2Client)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_falls_back_to_env(monkeypatch):
|
|
45
|
+
monkeypatch.setenv("RUNAPI_API_KEY", "env-key")
|
|
46
|
+
assert isinstance(Flux2Client(http_client=FakeHttp()), Flux2Client)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_raises_without_api_key():
|
|
50
|
+
with pytest.raises(AuthenticationError, match="API key is required"):
|
|
51
|
+
Flux2Client()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --- transport injection / accessors --------------------------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_uses_injected_http_client():
|
|
58
|
+
fake = FakeHttp()
|
|
59
|
+
client = Flux2Client(api_key="k", http_client=fake)
|
|
60
|
+
assert client.text_to_image._http is fake
|
|
61
|
+
assert client.remix_image._http is fake
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_exposes_resource_accessors():
|
|
65
|
+
client = Flux2Client(api_key="k", http_client=FakeHttp())
|
|
66
|
+
assert isinstance(client.text_to_image, TextToImage)
|
|
67
|
+
assert isinstance(client.remix_image, RemixImage)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --- request shapes -------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_create_posts_compacted_body():
|
|
74
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
75
|
+
client = Flux2Client(api_key="k", http_client=fake)
|
|
76
|
+
result = client.text_to_image.create(
|
|
77
|
+
model="flux-2-pro-text-to-image",
|
|
78
|
+
prompt="hello",
|
|
79
|
+
aspect_ratio="1:1",
|
|
80
|
+
seed=None,
|
|
81
|
+
)
|
|
82
|
+
assert fake.calls == [
|
|
83
|
+
("post", "/api/v1/flux_2/text_to_image", {"model": "flux-2-pro-text-to-image", "prompt": "hello", "aspect_ratio": "1:1"}),
|
|
84
|
+
]
|
|
85
|
+
assert isinstance(result, TextToImageResponse)
|
|
86
|
+
assert result.id == "t1"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_get_fetches_by_id():
|
|
90
|
+
fake = FakeHttp({"id": "t1", "status": "processing"})
|
|
91
|
+
client = Flux2Client(api_key="k", http_client=fake)
|
|
92
|
+
client.text_to_image.get("t1")
|
|
93
|
+
assert fake.calls == [("get", "/api/v1/flux_2/text_to_image/t1", None)]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_run_polls_and_narrows_completed_type():
|
|
97
|
+
fake = FakeHttp(
|
|
98
|
+
{"id": "t1", "status": "pending"},
|
|
99
|
+
{"id": "t1", "status": "completed", "images": [{"url": "https://x/y.png"}]},
|
|
100
|
+
)
|
|
101
|
+
client = Flux2Client(api_key="k", http_client=fake)
|
|
102
|
+
result = client.text_to_image.run(model="flux-2-pro-text-to-image", prompt="hi")
|
|
103
|
+
|
|
104
|
+
assert isinstance(result, CompletedTextToImageResponse)
|
|
105
|
+
assert result.images[0].url == "https://x/y.png"
|
|
106
|
+
assert [call[0] for call in fake.calls] == ["post", "get"]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# --- validation -----------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_create_requires_model():
|
|
113
|
+
client = Flux2Client(api_key="k", http_client=FakeHttp())
|
|
114
|
+
with pytest.raises(ValidationError, match="model is required"):
|
|
115
|
+
client.text_to_image.create(prompt="hi")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_create_requires_prompt():
|
|
119
|
+
client = Flux2Client(api_key="k", http_client=FakeHttp())
|
|
120
|
+
with pytest.raises(ValidationError, match="prompt is required"):
|
|
121
|
+
client.text_to_image.create(model="flux-2-pro-text-to-image")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_create_rejects_unknown_model():
|
|
125
|
+
client = Flux2Client(api_key="k", http_client=FakeHttp())
|
|
126
|
+
with pytest.raises(ValidationError, match="Invalid model"):
|
|
127
|
+
client.text_to_image.create(model="not-a-model", prompt="hi")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_create_rejects_invalid_enum():
|
|
131
|
+
client = Flux2Client(api_key="k", http_client=FakeHttp())
|
|
132
|
+
with pytest.raises(ValidationError, match="Invalid aspect_ratio"):
|
|
133
|
+
client.text_to_image.create(model="flux-2-pro-text-to-image", prompt="hi", aspect_ratio="99:1")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_remix_requires_source_image_urls():
|
|
137
|
+
client = Flux2Client(api_key="k", http_client=FakeHttp())
|
|
138
|
+
with pytest.raises(ValidationError, match="source_image_urls is required"):
|
|
139
|
+
client.remix_image.create(model="flux-2-pro-remix-image", prompt="hi")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_remix_accepts_auto_aspect_ratio():
|
|
143
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
144
|
+
client = Flux2Client(api_key="k", http_client=fake)
|
|
145
|
+
client.remix_image.create(
|
|
146
|
+
model="flux-2-pro-remix-image",
|
|
147
|
+
prompt="hi",
|
|
148
|
+
source_image_urls=["https://example.com/a.jpg"],
|
|
149
|
+
aspect_ratio="auto",
|
|
150
|
+
)
|
|
151
|
+
_, path, body = fake.calls[0]
|
|
152
|
+
assert path == "/api/v1/flux_2/remix_image"
|
|
153
|
+
assert body["aspect_ratio"] == "auto"
|