runware-sdk 1.2.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.
- runware_sdk-1.2.0/LICENSE +21 -0
- runware_sdk-1.2.0/PKG-INFO +665 -0
- runware_sdk-1.2.0/README.md +628 -0
- runware_sdk-1.2.0/pyproject.toml +182 -0
- runware_sdk-1.2.0/runware/__init__.py +245 -0
- runware_sdk-1.2.0/runware/_docs_cache.py +26 -0
- runware_sdk-1.2.0/runware/_schemas_version.py +12 -0
- runware_sdk-1.2.0/runware/client.py +1130 -0
- runware_sdk-1.2.0/runware/config.py +61 -0
- runware_sdk-1.2.0/runware/constants.py +4 -0
- runware_sdk-1.2.0/runware/content.py +184 -0
- runware_sdk-1.2.0/runware/errors.py +410 -0
- runware_sdk-1.2.0/runware/logger.py +124 -0
- runware_sdk-1.2.0/runware/py.typed +0 -0
- runware_sdk-1.2.0/runware/registry.py +361 -0
- runware_sdk-1.2.0/runware/stream.py +390 -0
- runware_sdk-1.2.0/runware/transport/__init__.py +0 -0
- runware_sdk-1.2.0/runware/transport/rest.py +229 -0
- runware_sdk-1.2.0/runware/transport/websocket.py +481 -0
- runware_sdk-1.2.0/runware/types/__init__.py +0 -0
- runware_sdk-1.2.0/runware/types/content.py +175 -0
- runware_sdk-1.2.0/runware/types/sdk.py +111 -0
- runware_sdk-1.2.0/runware/types/stream.py +33 -0
- runware_sdk-1.2.0/runware/types/task_map.py +13725 -0
- runware_sdk-1.2.0/runware/types/transport.py +31 -0
- runware_sdk-1.2.0/runware/utils/__init__.py +0 -0
- runware_sdk-1.2.0/runware/utils/file.py +43 -0
- runware_sdk-1.2.0/runware/utils/retry.py +88 -0
- runware_sdk-1.2.0/runware/validate.py +163 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Runware
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runware-sdk
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Async Python SDK for the Runware platform — image, video, audio, text, and 3D generation, plus upscaling, background removal, and more, over REST or WebSocket with typed parameters and runtime validation.
|
|
5
|
+
Keywords: runware,ai,sdk,image-generation,video-generation,audio-generation,text-generation,inference,generative-ai,stable-diffusion,flux,websocket,async,asyncio,llm,streaming
|
|
6
|
+
Author: Runware
|
|
7
|
+
Author-email: Runware <support@runware.ai>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Information Technology
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Framework :: AsyncIO
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
24
|
+
Classifier: Topic :: Multimedia :: Video
|
|
25
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Dist: aiohttp>=3.10
|
|
28
|
+
Requires-Dist: websockets>=13
|
|
29
|
+
Requires-Dist: fastjsonschema>=2.20
|
|
30
|
+
Requires-Python: >=3.11
|
|
31
|
+
Project-URL: Homepage, https://runware.ai
|
|
32
|
+
Project-URL: Documentation, https://runware.ai/docs
|
|
33
|
+
Project-URL: Repository, https://github.com/runware/runware-python
|
|
34
|
+
Project-URL: Issues, https://github.com/runware/runware-python/issues
|
|
35
|
+
Project-URL: Changelog, https://github.com/runware/runware-python/releases
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# Runware Python SDK
|
|
39
|
+
|
|
40
|
+
Async Python SDK for the Runware platform. A unified API for image, video, audio, text, and 3D generation — powered by the [Runware inference platform](https://runware.ai).
|
|
41
|
+
|
|
42
|
+
[Documentation](https://runware.ai/docs) · [Report a bug](https://github.com/runware/runware-python/issues)
|
|
43
|
+
|
|
44
|
+
- **One method for everything** — `.run()` handles every model type
|
|
45
|
+
- **Schema-driven types** — generated from Runware's canonical JSON schemas
|
|
46
|
+
- **WebSocket and REST transports** — persistent connections or stateless HTTP
|
|
47
|
+
- **LLM streaming via SSE** — token-by-token text generation with `.stream()`
|
|
48
|
+
- **Automatic model resolution** — the SDK resolves task types from AIR identifiers
|
|
49
|
+
- **Python 3.11+, asyncio-native** — single source of truth for all I/O
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install runware-sdk
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
Generate an image in five lines:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import asyncio
|
|
64
|
+
import os
|
|
65
|
+
from runware import Runware
|
|
66
|
+
|
|
67
|
+
async def main() -> None:
|
|
68
|
+
async with Runware(api_key=os.environ["RUNWARE_API_KEY"]) as client:
|
|
69
|
+
results = await client.run({
|
|
70
|
+
"model": "runware:400@1",
|
|
71
|
+
"positivePrompt": "A serene mountain landscape at sunset",
|
|
72
|
+
"width": 1024,
|
|
73
|
+
"height": 1024,
|
|
74
|
+
})
|
|
75
|
+
print(results[0]["imageURL"])
|
|
76
|
+
|
|
77
|
+
asyncio.run(main())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The SDK resolves `runware:400@1` to the right task type automatically. `RUNWARE_API_KEY` is read from the environment if you don't pass `api_key=...` explicitly.
|
|
81
|
+
|
|
82
|
+
More runnable patterns in [`examples/`](./examples/) — curated models, community fine-tunes, streaming, the WebSocket transport.
|
|
83
|
+
|
|
84
|
+
## Core Concepts
|
|
85
|
+
|
|
86
|
+
### One method for everything
|
|
87
|
+
|
|
88
|
+
Every inference task goes through `.run()`. The SDK determines the task type from the model's AIR identifier:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
# Image generation
|
|
92
|
+
images = await client.run({
|
|
93
|
+
"model": "runware:400@1",
|
|
94
|
+
"positivePrompt": "Abstract digital art",
|
|
95
|
+
"width": 1024, "height": 1024,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
# Video generation
|
|
99
|
+
videos = await client.run({
|
|
100
|
+
"model": "google:3@3",
|
|
101
|
+
"positivePrompt": "Ocean waves at sunset",
|
|
102
|
+
"duration": 8,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
# Text inference (LLM)
|
|
106
|
+
responses = await client.run({
|
|
107
|
+
"model": "google:gemma@4-31b",
|
|
108
|
+
"messages": [{"role": "user", "content": "Explain quantum computing"}],
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
# Audio generation (designs a voice from the prompt, then speaks the text)
|
|
112
|
+
audio = await client.run({
|
|
113
|
+
"model": "alibaba:qwen@3-tts-1.7b-voicedesign",
|
|
114
|
+
"positivePrompt": "A calm, friendly young woman with a soft tone",
|
|
115
|
+
"speech": {"text": "Hello world", "voice": "design"},
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Typed params per architecture
|
|
120
|
+
|
|
121
|
+
When you know the architecture, import its `Params` TypedDict from `runware.types.task_map` and annotate your call:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from runware.types.task_map import SdxlArchParams
|
|
125
|
+
|
|
126
|
+
params: SdxlArchParams = {
|
|
127
|
+
"model": "civitai:133005@782002",
|
|
128
|
+
"taskType": "imageInference",
|
|
129
|
+
"positivePrompt": "A professional headshot portrait",
|
|
130
|
+
"negativePrompt": "blurry, distorted",
|
|
131
|
+
"width": 1024,
|
|
132
|
+
"height": 1024,
|
|
133
|
+
"steps": 30,
|
|
134
|
+
"scheduler": "DPMSolverMultistep",
|
|
135
|
+
}
|
|
136
|
+
images = await client.run(params)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The generated module ships params and result TypedDicts for every architecture and operation, plus three lookup dicts:
|
|
140
|
+
|
|
141
|
+
- `architecture_task_types` — `sdxl`, `flux-1-dev`, `pony`, `illustrious`, and more
|
|
142
|
+
- `modality_task_types` — `image`, `video`, `audio`, `text`, `3d`
|
|
143
|
+
- `operation_task_types` — `caption-image`, `upscale-image`, `remove-background-image`, `prompt-enhance`, `vectorize`, and more
|
|
144
|
+
|
|
145
|
+
### Community and trained models
|
|
146
|
+
|
|
147
|
+
For models not built into the SDK (community uploads, fine-tunes), the registry won't have them yet — pass `taskType` explicitly:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
images = await client.run({
|
|
151
|
+
"model": "runware:exactly-illustrative@my-trained-style",
|
|
152
|
+
"taskType": "imageInference",
|
|
153
|
+
"positivePrompt": "A lighthouse on a rocky cliff at twilight",
|
|
154
|
+
"width": 1024,
|
|
155
|
+
"height": 1024,
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Validation (when enabled) automatically picks the right schema for the AIR — no extra option needed.
|
|
160
|
+
|
|
161
|
+
### Curated-model slugs
|
|
162
|
+
|
|
163
|
+
The registry indexes every curated model under both its AIR (`runware:400@1`) and its slug (`bfl-flux-2-dev`). You can pass either:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
# Both call the same model.
|
|
167
|
+
await client.run({"model": "runware:400@1", "positivePrompt": "..."})
|
|
168
|
+
await client.run({"model": "bfl-flux-2-dev", "positivePrompt": "..."})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The SDK rewrites slugs to canonical AIRs before sending. Non-curated identifiers (custom fine-tunes, unknown strings) pass through unchanged.
|
|
172
|
+
|
|
173
|
+
## LLM Streaming
|
|
174
|
+
|
|
175
|
+
For text models, `.stream()` delivers tokens as they're generated:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
stream = await client.stream({
|
|
179
|
+
"model": "google:gemma@4-31b",
|
|
180
|
+
"messages": [{"role": "user", "content": "Tell me a story about a robot"}],
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
async for word in stream.text_stream:
|
|
184
|
+
print(word, end="", flush=True)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`stream()` returns a `TextStream` with multiple ways to consume the response:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
stream = await client.stream({
|
|
191
|
+
"model": "google:gemma@4-31b",
|
|
192
|
+
"messages": [{"role": "user", "content": "Explain gravity"}],
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
# Iterate text deltas
|
|
196
|
+
async for word in stream.text_stream:
|
|
197
|
+
print(word, end="", flush=True)
|
|
198
|
+
|
|
199
|
+
# Iterate reasoning content (reasoning models)
|
|
200
|
+
async for thought in stream.reasoning_stream:
|
|
201
|
+
print(f"[thinking] {thought}")
|
|
202
|
+
|
|
203
|
+
# Get the full text at once (awaits the entire stream)
|
|
204
|
+
full_text = await stream.text()
|
|
205
|
+
|
|
206
|
+
# Get the final result with metadata
|
|
207
|
+
result = await stream.result()
|
|
208
|
+
print(result.text)
|
|
209
|
+
print(result.finish_reason) # "stop", "length", etc.
|
|
210
|
+
print(result.usage) # {"promptTokens": ..., "completionTokens": ...}
|
|
211
|
+
print(result.cost) # USD cost
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
> **Note:** `stream()` only supports `numberResults: 1`. For multiple completions use `run()` — `stream()` raises if you pass `numberResults > 1`.
|
|
215
|
+
|
|
216
|
+
## Transport Options
|
|
217
|
+
|
|
218
|
+
### WebSocket (default)
|
|
219
|
+
|
|
220
|
+
Best when you make multiple requests or want real-time feedback:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
async with Runware(transport_type="websocket") as client:
|
|
224
|
+
images = await client.run({"model": "runware:400@1", "positivePrompt": "..."})
|
|
225
|
+
videos = await client.run({"model": "google:3@3", "positivePrompt": "..."})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
WebSocket connections are automatically recovered on network interruptions. The SDK re-authenticates with the same session UUID and the server replays pending results.
|
|
229
|
+
|
|
230
|
+
### REST
|
|
231
|
+
|
|
232
|
+
Best for serverless functions or one-off requests:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
async with Runware(transport_type="rest") as client:
|
|
236
|
+
images = await client.run({
|
|
237
|
+
"model": "runware:400@1",
|
|
238
|
+
"positivePrompt": "A landscape painting",
|
|
239
|
+
"width": 1024, "height": 1024,
|
|
240
|
+
})
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Concurrent Operations
|
|
244
|
+
|
|
245
|
+
Run multiple tasks in parallel with `asyncio.gather`:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
import asyncio
|
|
249
|
+
|
|
250
|
+
async with Runware() as client:
|
|
251
|
+
images, upscaled, caption = await asyncio.gather(
|
|
252
|
+
client.run({
|
|
253
|
+
"model": "runware:400@1",
|
|
254
|
+
"positivePrompt": "Abstract art",
|
|
255
|
+
"numberResults": 3,
|
|
256
|
+
}),
|
|
257
|
+
client.run({
|
|
258
|
+
"model": "runware:504@1",
|
|
259
|
+
"taskType": "upscale",
|
|
260
|
+
"inputs": {"image": "https://example.com/photo.jpg"},
|
|
261
|
+
}),
|
|
262
|
+
client.run({
|
|
263
|
+
"model": "runware:150@2",
|
|
264
|
+
"taskType": "caption",
|
|
265
|
+
"inputs": {"image": "https://example.com/photo.jpg"},
|
|
266
|
+
}),
|
|
267
|
+
)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Cancellation
|
|
271
|
+
|
|
272
|
+
Pass an `asyncio.Event` and `.set()` it to cancel mid-flight. Works for `run()` and `stream()`, on both transports.
|
|
273
|
+
|
|
274
|
+
> **Heads-up:** cancel is client-side only. The server keeps processing the task and **you will be billed for it**. Cancelling just stops the SDK from waiting for the result.
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
import asyncio
|
|
278
|
+
from runware import RunOptions, RunwareError
|
|
279
|
+
|
|
280
|
+
async def main() -> None:
|
|
281
|
+
async with Runware() as client:
|
|
282
|
+
cancel = asyncio.Event()
|
|
283
|
+
|
|
284
|
+
async def deadline() -> None:
|
|
285
|
+
await asyncio.sleep(5)
|
|
286
|
+
cancel.set()
|
|
287
|
+
|
|
288
|
+
asyncio.create_task(deadline())
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
await client.run(
|
|
292
|
+
{"model": "runware:400@1", "positivePrompt": "A detailed scene"},
|
|
293
|
+
RunOptions(cancel_event=cancel),
|
|
294
|
+
)
|
|
295
|
+
except RunwareError as exc:
|
|
296
|
+
if exc.code == "aborted":
|
|
297
|
+
print("Cancelled")
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
For streams, set the cancel event to end iteration:
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
from runware import StreamOptions
|
|
304
|
+
|
|
305
|
+
stream = await client.stream(
|
|
306
|
+
{"model": "google:gemma@4-31b", "messages": [{"role": "user", "content": "..."}]},
|
|
307
|
+
StreamOptions(cancel_event=cancel),
|
|
308
|
+
)
|
|
309
|
+
async for word in stream.text_stream:
|
|
310
|
+
print(word, end="")
|
|
311
|
+
if some_condition:
|
|
312
|
+
cancel.set()
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Result and progress callbacks
|
|
316
|
+
|
|
317
|
+
Two callbacks let you observe a task as it unfolds:
|
|
318
|
+
|
|
319
|
+
- **`on_result(item)`** — fires once per item the moment it reaches a terminal state (`success` or `error`). For `numberResults > 1`, fires up to N times. Useful for streaming results into a UI as they appear.
|
|
320
|
+
- **`on_progress(item)`** — fires when an item's `progress` field changes (0-100). Currently only a handful of long-running models emit progress (mostly training).
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
from runware import RunOptions
|
|
324
|
+
|
|
325
|
+
def watch(item: dict) -> None:
|
|
326
|
+
if item.get("status") == "success":
|
|
327
|
+
print("ready:", item.get("imageURL"))
|
|
328
|
+
else:
|
|
329
|
+
print("failed:", item.get("error"))
|
|
330
|
+
|
|
331
|
+
def progress(item: dict) -> None:
|
|
332
|
+
print(f"{item.get('progress')}%")
|
|
333
|
+
|
|
334
|
+
results = await client.run(
|
|
335
|
+
{"model": "google:3@3", "positivePrompt": "Ocean waves", "numberResults": 3},
|
|
336
|
+
RunOptions(on_result=watch, on_progress=progress),
|
|
337
|
+
)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Error items fire `on_result` **before** the call raises — so when a per-result failure happens (provider hiccup, one of N results moderated, etc.), you still see the successful items via callback before the call raises. Same behavior on both WebSocket and REST. Request-level failures (`validation`, `auth`, `quota`, `rateLimit`) are the exception: they raise at submit time, before any results exist.
|
|
341
|
+
|
|
342
|
+
## Error Handling
|
|
343
|
+
|
|
344
|
+
All SDK errors are `RunwareError` instances:
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
from runware import Runware, RunwareError
|
|
348
|
+
|
|
349
|
+
async with Runware() as client:
|
|
350
|
+
try:
|
|
351
|
+
results = await client.run({
|
|
352
|
+
"model": "runware:400@1",
|
|
353
|
+
"positivePrompt": "A detailed rendering",
|
|
354
|
+
})
|
|
355
|
+
except RunwareError as exc:
|
|
356
|
+
print(exc.code) # 'validation' | 'auth' | 'quota' | ...
|
|
357
|
+
print(exc.retryable) # True for provider/timeout/connection/rateLimit/serverError
|
|
358
|
+
print(exc.message) # Human-readable description
|
|
359
|
+
print(exc.parameter) # Which param caused the error, if any
|
|
360
|
+
print(exc.documentation) # Link to model / utility / errors docs
|
|
361
|
+
print(exc.task_uuid) # Request UUID
|
|
362
|
+
print(exc.status_code) # HTTP status, when applicable
|
|
363
|
+
print(exc.validation_errors) # Per-field errors when validate=True
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
For cross-realm setups (different asyncio loops, subprocess boundaries) use `is_runware_error(exc)` instead of `isinstance`:
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
from runware import is_runware_error
|
|
370
|
+
|
|
371
|
+
if is_runware_error(exc):
|
|
372
|
+
...
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
`code` is a small, stable enum — `validation`, `auth`, `quota`, `rateLimit`, `safety`, `provider`, `timeout`, `notFound`, `serverError`, `connection`, `aborted`, `unknown`. Switch on it for high-level handling. The server's raw error identifier (hundreds of unstable values) is intentionally not exposed.
|
|
376
|
+
|
|
377
|
+
```python
|
|
378
|
+
if exc.code == "validation":
|
|
379
|
+
# Show form error, use exc.parameter to highlight the field
|
|
380
|
+
...
|
|
381
|
+
elif exc.code == "quota":
|
|
382
|
+
# Redirect to billing
|
|
383
|
+
...
|
|
384
|
+
elif exc.retryable:
|
|
385
|
+
# Backoff and retry
|
|
386
|
+
...
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Raising your own RunwareError
|
|
390
|
+
|
|
391
|
+
If you're wrapping the SDK behind another layer and want to surface errors with the same shape, build one with `create_runware_error`:
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
from runware import create_runware_error
|
|
395
|
+
|
|
396
|
+
raise create_runware_error(
|
|
397
|
+
"invalidParameter",
|
|
398
|
+
"Width must be a multiple of 64",
|
|
399
|
+
parameter="width",
|
|
400
|
+
task_type="imageInference",
|
|
401
|
+
)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
The constructor derives `code` and `documentation` URL from the raw code + model/parameter context — same logic the SDK uses internally.
|
|
405
|
+
|
|
406
|
+
## Configuration
|
|
407
|
+
|
|
408
|
+
`Runware(...)` accepts keyword arguments matching the `SDKConfig` dataclass:
|
|
409
|
+
|
|
410
|
+
| Field | Default | Notes |
|
|
411
|
+
|---|---|---|
|
|
412
|
+
| `api_key` | from `RUNWARE_API_KEY` | required |
|
|
413
|
+
| `transport_type` | `"websocket"` | or `"rest"` |
|
|
414
|
+
| `http_base_url` | `https://api.runware.ai/v1` | include the version path |
|
|
415
|
+
| `ws_base_url` | `wss://ws-api.runware.ai/v1` | include the version path |
|
|
416
|
+
| `timeout` | `1_200_000` (ms) | per-HTTP-call (one POST, one `getResponse` poll) |
|
|
417
|
+
| `poll_timeout` | `1_200_000` (ms) | end-to-end polling budget on either transport |
|
|
418
|
+
| `auth_timeout` | `15_000` (ms) | WebSocket auth handshake |
|
|
419
|
+
| `max_retries` | `3` | REST retries |
|
|
420
|
+
| `retry_delay` | `1_000` (ms) | base backoff |
|
|
421
|
+
| `retry_strategy` | `"exponential"` | or `"linear"` |
|
|
422
|
+
| `max_reconnect_attempts` | `inf` | WebSocket reconnect cap |
|
|
423
|
+
| `debug` | `False` | enable structured debug logs |
|
|
424
|
+
| `validate` | `False` | enable client-side schema validation |
|
|
425
|
+
| `dependencies` | `None` | inject a custom `aiohttp.ClientSession` and/or `ws_connect` |
|
|
426
|
+
| `log_sink` | `None` | pluggable destination for log entries |
|
|
427
|
+
|
|
428
|
+
### Custom log sink
|
|
429
|
+
|
|
430
|
+
By default, logs go through Python's stdlib `logging` under the `runware` logger. To send them elsewhere (Datadog, Sentry, a file, an aggregator), pass a `log_sink`:
|
|
431
|
+
|
|
432
|
+
```python
|
|
433
|
+
from runware import LogEntry, Runware
|
|
434
|
+
|
|
435
|
+
def sink(entry: LogEntry) -> None:
|
|
436
|
+
# entry: { category, message, data, timestamp }
|
|
437
|
+
print(entry.category, entry.message, entry.data)
|
|
438
|
+
|
|
439
|
+
async with Runware(debug=True, log_sink=sink) as client:
|
|
440
|
+
...
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Categories: `connection`, `auth`, `heartbeat`, `send`, `receive`, `request`, `retry`, `error`, `warn`, `info`. With `debug=False`, the logger is a noop — every call drops, no I/O.
|
|
444
|
+
|
|
445
|
+
### Picking up newly-launched models
|
|
446
|
+
|
|
447
|
+
New Runware models become usable automatically — no SDK update needed. To force a refresh immediately (instead of waiting for the next 5-minute background cycle):
|
|
448
|
+
|
|
449
|
+
```python
|
|
450
|
+
await client.refresh_registry()
|
|
451
|
+
results = await client.run({"model": "newprovider:1@1", "positivePrompt": "..."})
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
The registry caches the model map for 5 minutes. A bundled snapshot ships with the package and is used as a fallback when the network is unreachable.
|
|
455
|
+
|
|
456
|
+
### Async delivery
|
|
457
|
+
|
|
458
|
+
The SDK sends `deliveryMethod: "async"` by default for all inference tasks. On both transports, the server stores the result and the SDK polls `getResponse` until the task completes — that's why the same `poll_timeout` controls behavior on REST and WebSocket alike (default: 20 minutes).
|
|
459
|
+
|
|
460
|
+
For long tasks (video, training, large upscale), raise `poll_timeout`:
|
|
461
|
+
|
|
462
|
+
```python
|
|
463
|
+
client = Runware(poll_timeout=1_800_000) # 30 minutes
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Or per-call via `RunOptions`:
|
|
467
|
+
|
|
468
|
+
```python
|
|
469
|
+
videos = await client.run(
|
|
470
|
+
{"model": "google:3@3", "positivePrompt": "Ocean waves"},
|
|
471
|
+
RunOptions(timeout=600_000),
|
|
472
|
+
)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
#### Opting into sync delivery
|
|
476
|
+
|
|
477
|
+
For fast tasks (text inference, fast image gen, captioning) you can skip the polling round-trips by setting `deliveryMethod: "sync"`. The server holds the response open and pushes back the result in one round trip:
|
|
478
|
+
|
|
479
|
+
```python
|
|
480
|
+
responses = await client.run({
|
|
481
|
+
"model": "google:gemma@4-31b",
|
|
482
|
+
"messages": [{"role": "user", "content": "Hello"}],
|
|
483
|
+
"deliveryMethod": "sync",
|
|
484
|
+
})
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
On **WebSocket** this is where the persistent connection pays off — one frame in, one frame back, no polling.
|
|
488
|
+
On **REST** it's a single HTTP request with the full result in the response body.
|
|
489
|
+
|
|
490
|
+
Pick sync when the task finishes inside the server's connection budget (~120s for WebSocket sync, the HTTP read timeout for REST). For anything longer — video, 3D, large upscale, multi-result batches — stick with the async default.
|
|
491
|
+
|
|
492
|
+
### Per-call options
|
|
493
|
+
|
|
494
|
+
The second argument to `client.run()` and `client.stream()` is a `RunOptions` / `StreamOptions` instance — per-call overrides that don't belong on the client:
|
|
495
|
+
|
|
496
|
+
```python
|
|
497
|
+
from runware import RunOptions
|
|
498
|
+
|
|
499
|
+
await client.run(
|
|
500
|
+
{"model": "runware:400@1", "positivePrompt": "A landscape"},
|
|
501
|
+
RunOptions(
|
|
502
|
+
timeout=600_000, # ms — override config.poll_timeout for this call
|
|
503
|
+
cancel_event=cancel, # asyncio.Event — cancel this call
|
|
504
|
+
on_result=watch, # fires per item as it completes
|
|
505
|
+
on_progress=progress, # fires when an item's progress % changes
|
|
506
|
+
validate=True, # override config.validate for this call
|
|
507
|
+
),
|
|
508
|
+
)
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
`stream()` accepts `StreamOptions` with `timeout`, `cancel_event`, and `validate` (no polling means no per-item callbacks).
|
|
512
|
+
|
|
513
|
+
## Validation
|
|
514
|
+
|
|
515
|
+
Enable client-side validation to catch invalid parameters before they reach the API:
|
|
516
|
+
|
|
517
|
+
```python
|
|
518
|
+
async with Runware(validate=True) as client:
|
|
519
|
+
await client.run({"model": "...", ...}) # raises RunwareError(code='validation') on bad params
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
The schema for each model is fetched on first use and cached per-process. Works the same for curated models and community fine-tunes — pass nothing beyond `validate=True`.
|
|
523
|
+
|
|
524
|
+
If the schema can't be fetched (network failure, model unknown to the registry), validation is silently skipped and the server still validates as the source of truth.
|
|
525
|
+
|
|
526
|
+
Validation errors come back as a `RunwareError` with `code="validation"` and structured details on `validation_errors`:
|
|
527
|
+
|
|
528
|
+
```python
|
|
529
|
+
from runware import RunwareError
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
await client.run({...})
|
|
533
|
+
except RunwareError as exc:
|
|
534
|
+
if exc.code == "validation":
|
|
535
|
+
print(exc.task_type) # "imageInference"
|
|
536
|
+
print(exc.validation_errors) # [{message, path, rule, rule_definition}]
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Validation can also be toggled **per call** via `RunOptions.validate`, which overrides `config.validate`:
|
|
540
|
+
|
|
541
|
+
```python
|
|
542
|
+
# Force on for one call even if config.validate is False
|
|
543
|
+
await client.run({...}, RunOptions(validate=True))
|
|
544
|
+
|
|
545
|
+
# Skip for one call even if config.validate is True
|
|
546
|
+
await client.run({...}, RunOptions(validate=False))
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
To clear the in-process validator cache (e.g., after a server-side schema change without restarting):
|
|
550
|
+
|
|
551
|
+
```python
|
|
552
|
+
from runware import clear_validator_cache
|
|
553
|
+
clear_validator_cache()
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## Utility Methods
|
|
557
|
+
|
|
558
|
+
```python
|
|
559
|
+
# Search for available models
|
|
560
|
+
models = await client.model_search({
|
|
561
|
+
"search": "portrait",
|
|
562
|
+
"category": "checkpoint",
|
|
563
|
+
"architecture": "sdxl",
|
|
564
|
+
"limit": 10,
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
# Upload an image for use as input
|
|
568
|
+
uploaded = await client.image_upload({
|
|
569
|
+
"image": "https://example.com/photo.jpg", # URL, Data URI, or Base64
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
# Get account details
|
|
573
|
+
account = await client.account_management({"operation": "getDetails"})
|
|
574
|
+
|
|
575
|
+
# Retrieve a previously executed task
|
|
576
|
+
archived = await client.get_task_details({"taskUUID": "abc-123"})
|
|
577
|
+
|
|
578
|
+
# Poll for an async task result (used internally by run() — rarely needed directly)
|
|
579
|
+
result = await client.get_response({"taskUUID": "abc-123"})
|
|
580
|
+
|
|
581
|
+
# Upload a custom model
|
|
582
|
+
await client.model_upload({
|
|
583
|
+
"category": "checkpoint",
|
|
584
|
+
"architecture": "sdxl",
|
|
585
|
+
"format": "safetensors",
|
|
586
|
+
# ... plus model file details
|
|
587
|
+
})
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
`get_task_details` vs `get_response`: use `get_task_details` for "look up something I ran before" — it queries the task archive. `get_response` is the polling mechanism the SDK uses internally during async `.run()`; you generally don't need to call it directly.
|
|
591
|
+
|
|
592
|
+
## Content metadata
|
|
593
|
+
|
|
594
|
+
`client.content.*` exposes Runware's curated model catalog as read-only metadata — names, AIRs, headlines, capabilities, pricing, examples. Public information, no extra cost.
|
|
595
|
+
|
|
596
|
+
```python
|
|
597
|
+
# List curated models, optionally filtered
|
|
598
|
+
models = await client.content.list_models({
|
|
599
|
+
"capability": "io:text-to-image",
|
|
600
|
+
"category": "image",
|
|
601
|
+
"creator": "black-forest-labs",
|
|
602
|
+
"search": "flux",
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
# Single curated model by id
|
|
606
|
+
model = await client.content.get_model("alibaba-z-image-turbo")
|
|
607
|
+
|
|
608
|
+
# Sample input/output pairs the model can produce
|
|
609
|
+
examples = await client.content.get_model_examples("flux-1-dev")
|
|
610
|
+
|
|
611
|
+
# Pricing summary and per-configuration examples
|
|
612
|
+
pricing = await client.content.get_model_pricing("flux-1-dev")
|
|
613
|
+
|
|
614
|
+
# Discover the capability taxonomy (io:*, op:*, form:*)
|
|
615
|
+
capabilities = await client.content.list_capabilities()
|
|
616
|
+
|
|
617
|
+
# Collections (Runware-defined model groupings) with full model objects inlined
|
|
618
|
+
collections = await client.content.list_collections({"category": "image"})
|
|
619
|
+
|
|
620
|
+
# Creators with their curated models inlined
|
|
621
|
+
creators = await client.content.list_creators()
|
|
622
|
+
google = await client.content.get_creator("google")
|
|
623
|
+
|
|
624
|
+
# Pagination — pass paginate=True to get {"total", "limit", "offset", "items"}
|
|
625
|
+
page = await client.content.list_models({"paginate": True, "limit": 25, "offset": 0})
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
`creator`, `capabilities`, and `architecture` on each model are returned as id strings — resolve them against `list_creators`, `list_capabilities`, and the architecture id respectively when you need the human-readable label. Collections and creators are the only endpoints that resolve their inner `models` array to full objects.
|
|
629
|
+
|
|
630
|
+
## File helpers
|
|
631
|
+
|
|
632
|
+
`file_to_data_uri` encodes a local file as a `data:` URI for passing as input:
|
|
633
|
+
|
|
634
|
+
```python
|
|
635
|
+
from pathlib import Path
|
|
636
|
+
from runware import file_to_data_uri
|
|
637
|
+
|
|
638
|
+
data_uri = file_to_data_uri(Path("photo.jpg"))
|
|
639
|
+
await client.image_upload({"image": data_uri})
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Accepts both `Path` and `bytes` — `bytes` is useful when the file lives in memory (e.g. a freshly downloaded blob).
|
|
643
|
+
|
|
644
|
+
## Custom dependencies
|
|
645
|
+
|
|
646
|
+
For testing, proxies, or custom auth flows, pass a `RuntimeDependencies`:
|
|
647
|
+
|
|
648
|
+
```python
|
|
649
|
+
import aiohttp
|
|
650
|
+
from runware import Runware, RuntimeDependencies
|
|
651
|
+
|
|
652
|
+
session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=60))
|
|
653
|
+
|
|
654
|
+
async with Runware(
|
|
655
|
+
api_key="...",
|
|
656
|
+
dependencies=RuntimeDependencies(session=session),
|
|
657
|
+
) as client:
|
|
658
|
+
...
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
Pass `ws_connect=...` similarly to override the WebSocket connect path (defaults to `websockets.connect`). Injected sessions are not closed on `client.close()` — you own the lifecycle.
|
|
662
|
+
|
|
663
|
+
## License
|
|
664
|
+
|
|
665
|
+
MIT
|