Skip to content

Repository files navigation

Runware Python SDK

Async Python SDK for the Runware platform. A unified API for image, video, audio, text, and 3D generation — powered by the Runware inference platform.

Documentation · Report a bug

  • One method for everything.run() handles every model type
  • Schema-driven types — generated from Runware's canonical JSON schemas
  • WebSocket and REST transports — persistent connections or stateless HTTP
  • LLM streaming via SSE — token-by-token text generation with .stream()
  • Automatic model resolution — the SDK resolves task types from AIR identifiers
  • Python 3.11+, asyncio-native — single source of truth for all I/O

Installation

pip install runware-sdk

Quick start

Generate an image in five lines:

importasyncioimportosfromrunwareimportRunwareasyncdefmain() ->None:
asyncwithRunware(api_key=os.environ["RUNWARE_API_KEY"]) asclient:
results=awaitclient.run({
"model": "runware:400@1",
"positivePrompt": "A serene mountain landscape at sunset",
"width": 1024,
"height": 1024,
})
print(results[0]["imageURL"])
asyncio.run(main())

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.

More runnable patterns in examples/ — curated models, community fine-tunes, streaming, the WebSocket transport.

Core Concepts

One method for everything

Every inference task goes through .run(). The SDK determines the task type from the model's AIR identifier:

# Image generationimages=awaitclient.run({
"model": "runware:400@1",
"positivePrompt": "Abstract digital art",
"width": 1024, "height": 1024,
})
# Video generationvideos=awaitclient.run({
"model": "google:3@3",
"positivePrompt": "Ocean waves at sunset",
"width": 1280,
"height": 720,
"duration": 8,
})
# Text inference (LLM)responses=awaitclient.run({
"model": "google:gemma@4-31b",
"messages": [{"role": "user", "content": "Explain quantum computing"}],
})
# Audio generation (designs a voice from the prompt, then speaks the text)audio=awaitclient.run({
"model": "alibaba:qwen@3-tts-1.7b-voicedesign",
"positivePrompt": "A calm, friendly young woman with a soft tone",
"speech": {"text": "Hello world", "voice": "design"},
})

Typed params per architecture

When you know the architecture, import its Params TypedDict from runware.types.task_map and annotate your call:

fromrunware.types.task_mapimportSdxlArchParamsparams: SdxlArchParams= {
"model": "civitai:133005@782002",
"taskType": "imageInference",
"positivePrompt": "A professional headshot portrait",
"negativePrompt": "blurry, distorted",
"width": 1024,
"height": 1024,
"steps": 30,
"scheduler": "DPMSolverMultistep",
}
images=awaitclient.run(params)

The generated module ships params and result TypedDicts for every architecture and operation, plus three lookup dicts:

  • architecture_task_typessdxl, flux-1-dev, pony, illustrious, and more
  • modality_task_typesimage, video, audio, text, 3d
  • operation_task_typescaption-image, upscale-image, remove-background-image, prompt-enhance, vectorize, and more

Community and trained models

For models not built into the SDK (community uploads, fine-tunes), the registry won't have them yet — pass taskType explicitly:

images=awaitclient.run({
"model": "runware:exactly-illustrative@my-trained-style",
"taskType": "imageInference",
"positivePrompt": "A lighthouse on a rocky cliff at twilight",
"width": 1024,
"height": 1024,
})

Validation (when enabled) automatically picks the right schema for the AIR — no extra option needed.

Curated-model slugs

The registry indexes every curated model under both its AIR (runware:400@1) and its slug (bfl-flux-2-dev). You can pass either:

# Both call the same model.awaitclient.run({"model": "runware:400@1", "positivePrompt": "..."})
awaitclient.run({"model": "bfl-flux-2-dev", "positivePrompt": "..."})

The SDK rewrites slugs to canonical AIRs before sending. Non-curated identifiers (custom fine-tunes, unknown strings) pass through unchanged.

LLM Streaming

For text models, .stream() delivers tokens as they're generated:

stream=awaitclient.stream({
"model": "google:gemma@4-31b",
"messages": [{"role": "user", "content": "Tell me a story about a robot"}],
})
asyncforwordinstream.text_stream:
print(word, end="", flush=True)

stream() returns a TextStream with multiple ways to consume the response:

stream=awaitclient.stream({
"model": "google:gemma@4-31b",
"messages": [{"role": "user", "content": "Explain gravity"}],
})
# Iterate text deltasasyncforwordinstream.text_stream:
print(word, end="", flush=True)
# Iterate reasoning content (reasoning models)asyncforthoughtinstream.reasoning_stream:
print(f"[thinking] {thought}")
# Get the full text at once (awaits the entire stream)full_text=awaitstream.text()
# Get the final result with metadataresult=awaitstream.result()
print(result.text)
print(result.finish_reason) # "stop", "length", etc.print(result.usage) # {"promptTokens": ..., "completionTokens": ...}print(result.cost) # USD cost

Note:stream() only supports numberResults: 1. For multiple completions use run()stream() raises if you pass numberResults > 1.

Transport Options

WebSocket (default)

Best when you make multiple requests or want real-time feedback:

asyncwithRunware(transport="websocket") asclient:
images=awaitclient.run({"model": "runware:400@1", "positivePrompt": "..."})
videos=awaitclient.run({"model": "google:3@3", "positivePrompt": "...", "width": 1280, "height": 720})

WebSocket connections are automatically recovered on network interruptions. The SDK re-authenticates with the same session UUID and the server replays pending results.

REST

Best for serverless functions or one-off requests:

asyncwithRunware(transport="rest") asclient:
images=awaitclient.run({
"model": "runware:400@1",
"positivePrompt": "A landscape painting",
"width": 1024, "height": 1024,
})

Concurrent Operations

Run multiple tasks in parallel with asyncio.gather:

importasyncioasyncwithRunware() asclient:
images, upscaled, caption=awaitasyncio.gather(
client.run({
"model": "runware:400@1",
"positivePrompt": "Abstract art",
"numberResults": 3,
}),
client.run({
"model": "runware:504@1",
"taskType": "upscale",
"inputs": {"image": "https://example.com/photo.jpg"},
}),
client.run({
"model": "runware:150@2",
"taskType": "caption",
"inputs": {"image": "https://example.com/photo.jpg"},
}),
)

Cancellation

Pass an asyncio.Event and .set() it to cancel mid-flight. Works for run() and stream(), on both transports.

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.

importasynciofromrunwareimportRunOptions, RunwareErrorasyncdefmain() ->None:
asyncwithRunware() asclient:
cancel=asyncio.Event()
asyncdefdeadline() ->None:
awaitasyncio.sleep(5)
cancel.set()
asyncio.create_task(deadline())
try:
awaitclient.run(
{"model": "runware:400@1", "positivePrompt": "A detailed scene"},
RunOptions(cancel_event=cancel),
)
exceptRunwareErrorasexc:
ifexc.code=="aborted":
print("Cancelled")

For streams, set the cancel event to end iteration:

fromrunwareimportStreamOptionsstream=awaitclient.stream(
{"model": "google:gemma@4-31b", "messages": [{"role": "user", "content": "..."}]},
StreamOptions(cancel_event=cancel),
)
asyncforwordinstream.text_stream:
print(word, end="")
ifsome_condition:
cancel.set()

Result and progress callbacks

Two callbacks let you observe a task as it unfolds:

  • 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.
  • 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).
fromrunwareimportRunOptionsdefwatch(item: dict) ->None:
ifitem.get("status") =="success":
print("ready:", item.get("imageURL"))
else:
print("failed:", item.get("error"))
defprogress(item: dict) ->None:
print(f"{item.get('progress')}%")
results=awaitclient.run(
{"model": "google:3@3", "positivePrompt": "Ocean waves", "width": 1280, "height": 720, "numberResults": 3},
RunOptions(on_result=watch, on_progress=progress),
)

Error items fire on_resultbefore 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.

Error Handling

All SDK errors are RunwareError instances:

fromrunwareimportRunware, RunwareErrorasyncwithRunware() asclient:
try:
results=awaitclient.run({
"model": "runware:400@1",
"positivePrompt": "A detailed rendering",
})
exceptRunwareErrorasexc:
print(exc.code) # 'validation' | 'auth' | 'quota' | ...print(exc.retryable) # True for provider/timeout/connection/rateLimit/serverErrorprint(exc.message) # Human-readable descriptionprint(exc.parameter) # Which param caused the error, if anyprint(exc.documentation) # Link to model / utility / errors docsprint(exc.task_uuid) # Request UUIDprint(exc.status_code) # HTTP status, when applicableprint(exc.validation_errors) # Per-field errors when validate=True

For cross-realm setups (different asyncio loops, subprocess boundaries) use is_runware_error(exc) instead of isinstance:

fromrunwareimportis_runware_errorifis_runware_error(exc):
...

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.

ifexc.code=="validation":
# Show form error, use exc.parameter to highlight the field
...
elifexc.code=="quota":
# Redirect to billing
...
elifexc.retryable:
# Backoff and retry
...

Raising your own RunwareError

If you're wrapping the SDK behind another layer and want to surface errors with the same shape, build one with create_runware_error:

fromrunwareimportcreate_runware_errorraisecreate_runware_error(
"invalidParameter",
"Width must be a multiple of 64",
parameter="width",
task_type="imageInference",
)

The constructor derives code and documentation URL from the raw code + model/parameter context — same logic the SDK uses internally.

Configuration

Runware(...) accepts keyword arguments matching the SDKConfig dataclass:

FieldDefaultNotes
api_keyfrom RUNWARE_API_KEYrequired
transport"websocket"or "rest"
http_base_urlhttps://api.runware.ai/v1include the version path
ws_base_urlwss://ws-api.runware.ai/v1include the version path
timeout1_200_000 (ms)per-HTTP-call (one POST, one getResponse poll)
poll_timeout1_200_000 (ms)end-to-end polling budget on either transport
auth_timeout15_000 (ms)WebSocket auth handshake
max_retries3REST retries
retry_delay1_000 (ms)base backoff
retry_strategy"exponential"or "linear"
max_reconnect_attemptsinfWebSocket reconnect cap
debugFalseenable structured debug logs
validateFalseenable client-side schema validation
dependenciesNoneinject a custom aiohttp.ClientSession and/or ws_connect
log_sinkNonepluggable destination for log entries

Custom log sink

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:

fromrunwareimportLogEntry, Runwaredefsink(entry: LogEntry) ->None:
# entry: { category, message, data, timestamp }print(entry.category, entry.message, entry.data)
asyncwithRunware(debug=True, log_sink=sink) asclient:
...

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.

Picking up newly-launched models

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):

awaitclient.refresh_registry()
results=awaitclient.run({"model": "newprovider:1@1", "positivePrompt": "..."})

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.

Async delivery

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).

For long tasks (video, training, large upscale), raise poll_timeout:

client=Runware(poll_timeout=1_800_000) # 30 minutes

Or per-call via RunOptions:

videos=awaitclient.run(
{"model": "google:3@3", "positivePrompt": "Ocean waves", "width": 1280, "height": 720},
RunOptions(timeout=600_000),
)

Opting into sync delivery

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:

responses=awaitclient.run({
"model": "google:gemma@4-31b",
"messages": [{"role": "user", "content": "Hello"}],
"deliveryMethod": "sync",
})

On WebSocket this is where the persistent connection pays off — one frame in, one frame back, no polling. On REST it's a single HTTP request with the full result in the response body.

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.

Per-call options

The second argument to client.run() and client.stream() is a RunOptions / StreamOptions instance — per-call overrides that don't belong on the client:

fromrunwareimportRunOptionsawaitclient.run(
{"model": "runware:400@1", "positivePrompt": "A landscape"},
RunOptions(
timeout=600_000, # ms — override config.poll_timeout for this callcancel_event=cancel, # asyncio.Event — cancel this callon_result=watch, # fires per item as it completeson_progress=progress, # fires when an item's progress % changesvalidate=True, # override config.validate for this call
),
)

stream() accepts StreamOptions with timeout, cancel_event, and validate (no polling means no per-item callbacks).

Validation

Enable client-side validation to catch invalid parameters before they reach the API:

asyncwithRunware(validate=True) asclient:
awaitclient.run({"model": "...", ...}) # raises RunwareError(code='validation') on bad params

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.

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.

Validation errors come back as a RunwareError with code="validation" and structured details on validation_errors:

fromrunwareimportRunwareErrortry:
awaitclient.run({...})
exceptRunwareErrorasexc:
ifexc.code=="validation":
print(exc.task_type) # "imageInference"print(exc.validation_errors) # [{message, path, rule, rule_definition}]

Validation can also be toggled per call via RunOptions.validate, which overrides config.validate:

# Force on for one call even if config.validate is Falseawaitclient.run({...}, RunOptions(validate=True))
# Skip for one call even if config.validate is Trueawaitclient.run({...}, RunOptions(validate=False))

To clear the in-process validator cache (e.g., after a server-side schema change without restarting):

fromrunwareimportclear_validator_cacheclear_validator_cache()

Utility Methods

# Search for available modelsmodels=awaitclient.model_search({
"search": "portrait",
"category": "checkpoint",
"architecture": "sdxl",
"limit": 10,
})
# Store media (images, video, audio, 3D models) for reuse as input. Returns a mediaUUID.# Supersedes the deprecated, image-only image_upload.uploaded=awaitclient.media_storage({
"operation": "upload",
"media": "https://example.com/photo.jpg", # URL, Data URI, or Base64
})
# Delete stored media by its mediaUUIDawaitclient.media_storage({"operation": "delete", "media": uploaded[0]["mediaUUID"]})
# Get account detailsaccount=awaitclient.account_management({"operation": "getDetails"})
# Retrieve a previously executed taskarchived=awaitclient.get_task_details({"taskUUID": "abc-123"})
# Poll for an async task result (used internally by run() — rarely needed directly)result=awaitclient.get_response({"taskUUID": "abc-123"})
# Upload a custom modelawaitclient.model_upload({
"category": "checkpoint",
"architecture": "sdxl",
"format": "safetensors",
# ... plus model file details
})

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.

Content metadata

client.content.* exposes Runware's curated model catalog as read-only metadata — names, AIRs, headlines, capabilities, pricing, examples. Public information, no extra cost.

# List curated models, optionally filteredmodels=awaitclient.content.list_models({
"capability": "io:text-to-image",
"category": "image",
"creator": "black-forest-labs",
"search": "flux",
})
# Single curated model by idmodel=awaitclient.content.get_model("alibaba-z-image-turbo")
# Sample input/output pairs the model can produceexamples=awaitclient.content.get_model_examples("flux-1-dev")
# Pricing summary and per-configuration examplespricing=awaitclient.content.get_model_pricing("flux-1-dev")
# Discover the capability taxonomy (io:*, op:*, form:*)capabilities=awaitclient.content.list_capabilities()
# Collections (Runware-defined model groupings) with full model objects inlinedcollections=awaitclient.content.list_collections({"category": "image"})
# Creators with their curated models inlinedcreators=awaitclient.content.list_creators()
google=awaitclient.content.get_creator("google")
# Pagination — pass paginate=True to get {"total", "limit", "offset", "items"}page=awaitclient.content.list_models({"paginate": True, "limit": 25, "offset": 0})

The per-model methods (get_model, get_model_examples, get_model_pricing) accept either the model's AIR or its catalog slug (the model field returned by list_models).

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.

File helpers

file_to_data_uri encodes a local file as a data: URI for passing as input:

frompathlibimportPathfromrunwareimportfile_to_data_uridata_uri=file_to_data_uri(Path("photo.jpg"))
awaitclient.media_storage({"operation": "upload", "media": data_uri})

Accepts both Path and bytesbytes is useful when the file lives in memory (e.g. a freshly downloaded blob).

file_to_base64 does the same read but returns raw base64 with no data: prefix or MIME type (the server sniffs the real format from the bytes).

You usually don't need either helper for inputs: run() and media_storage auto-encode local file paths. Any string value (recursively, including nested dicts and lists) that points to an existing file on disk is read and replaced with its base64 before the request is sent. URLs, UUIDs, data URIs, existing base64, and prompts pass through untouched.

awaitclient.run({"model": "...", "seedImage": "./photo.jpg"})
awaitclient.run({"model": "...", "referenceImages": ["./a.jpg", "./b.jpg"]})

Custom dependencies

For testing, proxies, or custom auth flows, pass a RuntimeDependencies:

importaiohttpfromrunwareimportRunware, RuntimeDependenciessession=aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=60))
asyncwithRunware(
api_key="...",
dependencies=RuntimeDependencies(session=session),
) asclient:
...

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.

License

MIT

About

Runware SDK for Python — image, video, audio, text, and 3D generation

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages