Skip to content

Repository files navigation

Runware SDK

The official Runware SDK for TypeScript and JavaScript. A unified API for image, video, audio, text, and 3D generation — powered by the Runware inference platform.

Documentation · npm · Report a bug

  • One method for everything.run() handles every model type
  • Schema-driven types — generated from Runware's canonical JSON schemas, always accurate
  • 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
  • Node.js 18+ and browsers — works everywhere, no polyfills needed

Installation

npm install @runware/sdk

Quick Start

Generate an image in five lines:

import{createClient}from'@runware/sdk'constclient=awaitcreateClient({apiKey: process.env.RUNWARE_API_KEY??'your-api-key'})awaitclient.connect()// required for the default WebSocket transport — skip for RESTconstimages=awaitclient.run({model: 'runware:400@1',positivePrompt: 'A serene mountain landscape at sunset',width: 1024,height: 1024,})console.log('Image URL:',images[0].imageURL)

The SDK automatically resolves runware:400@1 to the correct task type and generates fully typed responses.

More runnable patterns in examples/ — curated models, community fine-tunes, streaming with abort, and the modelSearch → run flow.

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 generationconstimages=awaitclient.run({model: 'runware:400@1',positivePrompt: 'Abstract digital art',width: 1024,height: 1024,})// Video generationconstvideos=awaitclient.run({model: 'google:3@3',positivePrompt: 'Ocean waves at sunset',width: 1280,height: 720,duration: 8,})// Text inference (LLM)constresponses=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)constaudio=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'},})

Architecture generics for typed params

When you know the model architecture, use a schema generic to get full type safety:

constimages=awaitclient.run<'sdxl'>({model: 'civitai:133005@782002',positivePrompt: 'A professional headshot portrait',negativePrompt: 'blurry, distorted',width: 1024,height: 1024,steps: 30,scheduler: 'DPMSolverMultistep',// ^ TypeScript knows every valid param for SDXL})// images[0].imageURL — fully typed

The SDK ships generics for:

  • Modalitiesimage, video, audio, text, 3d
  • Image architecturessdxl, sdxl-lcm, sdxl-turbo, sdxl-hyper, sdxl-lightning, sdxl-distilled, sd-1-5, sd-1-5-lcm, sd-1-5-hyper, sd-1-5-distilled, sd-2-1, sd3, flux-1-dev, flux-1-schnell, flux-1-kontext-dev, pony, illustrious, noobai, z-image, z-image-turbo, exactly-illustrative
  • Operationscaption, caption-image, caption-video, upscale, upscale-image, upscale-video, remove-background, remove-background-image, remove-background-video, masking, controlnet-preprocess, prompt-enhance, vectorize, training

To discover the full list in your IDE, hover the alias:

importtype{SchemaKey}from'@runware/sdk'typeAllGenerics=SchemaKey// ^? hover to see the union of every valid generic

Community and trained models

For models not built into the SDK (community uploads, fine-tuned models, etc.), the SDK can't resolve the task type automatically. Pass taskType explicitly and use the architecture generic for typed params:

constimages=awaitclient.run<'exactly-illustrative'>({model: 'runware:exactly-illustrative@my-trained-style',taskType: 'imageInference',positivePrompt: 'A lighthouse on a rocky cliff at twilight',width: 1024,height: 1024,})
  • <'exactly-illustrative'> — TypeScript types (compile-time only, erased at runtime)
  • taskType — tells the SDK which API endpoint to use
  • 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:

conststream=awaitclient.stream({model: 'google:gemma@4-31b',messages: [{role: 'user',content: 'Tell me a story about a robot'}],})// Iterate text deltas as they arriveforawait(constwordofstream.textStream){process.stdout.write(word)}

The .stream() method returns a TextStream object with multiple ways to consume the response:

conststream=awaitclient.stream({model: 'google:gemma@4-31b',messages: [{role: 'user',content: 'Explain gravity'}],})// Iterate text deltasforawait(constwordofstream.textStream){process.stdout.write(word)}// Iterate reasoning content (for reasoning models)forawait(constthoughtofstream.reasoningStream){console.log('[thinking]',thought)}// Get the full text at once (awaits the entire stream)constfullText=awaitstream.text()// Get the final result with metadataconstresult=awaitstream.result()console.log(result.text)console.log(result.finishReason)// 'stop', 'length', etc.console.log(result.usage)// { promptTokens, completionTokens, totalTokens }console.log(result.cost)// USD cost

Note:stream() only supports numberResults: 1. For multiple completions in one call, use run() instead — stream() will throw if you pass numberResults > 1.

Transport Options

WebSocket (default)

Best for applications making multiple requests or needing real-time feedback:

constclient=awaitcreateClient({apiKey: process.env.RUNWARE_API_KEY??'your-api-key',transport: 'websocket',// default})awaitclient.connect()// Persistent connection — low latency for multiple operationsconstimages=awaitclient.run({model: 'runware:400@1',positivePrompt: '...'})constvideos=awaitclient.run({model: 'google:3@3',positivePrompt: '...',width: 1280,height: 720})awaitclient.disconnect()

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

Call client.disconnect() to close the connection cleanly. This also stops any in-flight reconnect attempts — subsequent connect() calls re-enable reconnection.

REST

Best for serverless functions or simple one-off requests:

constclient=awaitcreateClient({apiKey: process.env.RUNWARE_API_KEY??'your-api-key',transport: 'rest',})// No connect() needed — each request is a standalone HTTP callconstimages=awaitclient.run({model: 'runware:400@1',positivePrompt: 'A landscape painting',width: 1024,height: 1024,})

Concurrent Operations

Run multiple tasks simultaneously:

const[images,upscaled,caption]=awaitPromise.all([client.run({model: 'runware:400@1',positivePrompt: 'Abstract art',numberResults: 3,}),client.run<'upscale-image'>({model: 'runware:504@1',inputs: {image: 'https://example.com/photo.jpg'},}),client.run<'caption'>({model: 'runware:150@2',inputs: {image: 'https://example.com/photo.jpg'},}),])

Cancellation

Pass an AbortSignal to cancel a request mid-flight. Works for run() and stream(), on both transports:

Heads-up: abort is client-side only. The server keeps processing the task and you will be billed for it. Aborting just stops the SDK from waiting for the result.

constcontroller=newAbortController()// Cancel after 5 secondssetTimeout(()=>controller.abort(),5000)try{constimages=awaitclient.run({model: 'runware:400@1',positivePrompt: 'A detailed scene',},{signal: controller.signal})}catch(error){if(error.code==='aborted'){console.log('Request was cancelled')}}

For streams, abort closes the SSE connection cleanly:

conststream=awaitclient.stream({model: 'google:gemma@4-31b',messages: [{role: 'user',content: 'Tell me a long story'}],},{signal: controller.signal})forawait(constwordofstream.textStream){process.stdout.write(word)if(someCondition){controller.abort()}// ends the iteration}

Result and progress callbacks

Two callbacks let you observe a task as it unfolds:

  • onResult(item) — fires once per item the moment it reaches a terminal state (success or error). For numberResults > 1, fires up to N times — one per result as it completes. Useful for streaming results into a UI as they appear.
  • onProgress(item) — fires when an item's progress field changes (0-100). Currently only a few long-running models emit this (mostly training); skip it for typical image/video inference.
constimages=awaitclient.run({model: 'google:3@3',positivePrompt: 'Ocean waves at sunset',width: 1280,height: 720,numberResults: 3,},{onResult: (item)=>{if(item.status==='success'){console.log('ready:',item.imageURL)}else{console.log('failed:',item.error)}},onProgress: (item)=>{console.log(`${item.progress}%`)},})

Error items fire onResultbefore the promise rejects — so when a per-result runtime failure happens (provider hiccup, content moderation flagging one of several outputs, etc.), you still see the successful results via callback before the promise rejects with the failure. Same behaviour on both WebSocket and REST transports. Request-level failures (validation, auth, quota, rateLimit) are the exception: they reject at submit time, before any results exist to report.

Error Handling

All errors thrown by the SDK are RunwareError instances with structured fields:

import{RunwareError}from'@runware/sdk'try{constimages=awaitclient.run({model: 'runware:400@1',positivePrompt: 'A detailed rendering',})}catch(error){if(errorinstanceofRunwareError){console.error(error.code)// 'validation' | 'auth' | 'quota' | ...console.error(error.retryable)// true for provider/timeout/connection/rateLimit/serverErrorconsole.error(error.message)// Human-readable descriptionconsole.error(error.parameter)// Which param caused the error, if anyconsole.error(error.documentation)// Link to model / utility / errors docs}}

(For serialization-survived errors or cross-realm setups, use the isRunwareError(error) helper instead of instanceof.)

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 (which has hundreds of values and is unstable) is intentionally not exposed — the code enum + parameter + message cover what you need to react.

if(error.code==='validation'){// Show form error, use error.parameter to highlight the field}elseif(error.code==='quota'){// Redirect to billing}elseif(error.retryable){// Backoff and retry}

Validation failures from the optional client-side validate: true flag come back as code: 'validation', with an error.validationErrors array of AJV error objects.

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

import{createRunwareError}from'@runware/sdk'throwcreateRunwareError('invalidParameter','Width must be a multiple of 64',{parameter: 'width',taskType: 'imageInference'},)

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

Configuration

createClient({...}) accepts an SDKConfig object:

FieldDefaultNotes
apiKeyfrom RUNWARE_API_KEYrequired
transport'websocket'or 'rest'
httpBaseUrlhttps://api.runware.ai/v1include the version path
wsBaseUrlwss://ws-api.runware.ai/v1include the version path
timeout1_200_000 (ms)per-HTTP-call (one POST, one getResponse poll)
pollTimeout1_200_000 (ms)end-to-end polling budget on either transport
authTimeout15_000 (ms)WebSocket auth handshake
maxRetries3REST retries
retryDelay1_000 (ms)base backoff
retryStrategy'exponential'or 'linear'
maxReconnectAttemptsInfinityWebSocket reconnect cap
debugfalseenable structured debug logs
validatefalseenable client-side schema validation
dependenciesundefinedinject a custom WebSocket and/or fetch
logSinkundefinedpluggable destination for log entries

Custom log sink

By default, debug logs go to console.*. To send logs to a custom destination (Datadog, Sentry, file, etc.), pass a logSink:

import{createClient,typeLogEntry}from'@runware/sdk'constclient=awaitcreateClient({apiKey: process.env.RUNWARE_API_KEY??'your-api-key',debug: true,logSink: (entry: LogEntry)=>{// entry: { category, message, data?, timestamp }myLogger.log(entry.category,entry.message,entry.data)},})

Picking up newly-launched models

New Runware models become usable in your code automatically — no SDK update needed. If you want to force the SDK to pick up a freshly-launched model immediately (instead of waiting for the next background refresh):

awaitclient.refreshRegistry()constimages=awaitclient.run({model: 'newprovider:1@1',positivePrompt: 'A landscape',})

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 pollTimeout config controls behaviour on REST and WebSocket alike (default: 20 minutes).

For long-running tasks like video generation or model training, increase the poll timeout further:

constclient=awaitcreateClient({apiKey: process.env.RUNWARE_API_KEY??'your-api-key',pollTimeout: 1_800_000,// 30 minutes})

Or per-call via RunOptions:

constvideos=awaitclient.run({model: 'google:3@3',positivePrompt: 'Ocean waves',width: 1280,height: 720,},{timeout: 600000})

Opting into sync delivery

For tasks that complete quickly (text inference, fast image generation, captioning, etc.) you can skip the polling round-trips entirely by setting deliveryMethod: 'sync'. The server holds the connection until the result is ready and pushes it back in a single response:

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

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

Pick sync when you're confident 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 so the SDK can poll safely.

Per-call options

The second argument to client.run() / client.stream() and the utility methods (modelSearch, modelUpload, etc.) is a RunOptions object — per-call overrides that don't belong on the client itself:

awaitclient.run({model: 'runware:400@1',positivePrompt: 'A landscape',},{timeout: 600_000,// ms — override config.pollTimeout for this callsignal: controller.signal,// AbortSignal — cancel this call (see Cancellation)onResult: (item)=>{ ... },// fires per item as it completes (see Result and progress callbacks)onProgress: (item)=>{ ... },// fires when an item's progress % changesvalidate: true,// override config.validate for this call (see Validation)})

stream() accepts the same shape but only uses signal and validate (no polling means no timeout, no per-item callbacks).

Validation

Enable client-side validation to catch invalid parameters before they reach the API. Install AJV (optional peer dependency):

npm install ajv

Then enable it:

constclient=awaitcreateClient({apiKey: process.env.RUNWARE_API_KEY??'your-api-key',validate: true,})// Throws a RunwareError (code: 'validation') with details before sendingawaitclient.run({model: 'runware:400@1',positivePrompt: 'A landscape',width: -1,// ← caught locally})

The schema for each model is fetched on first use and cached per-process. Works the same for curated models and community fine-tunes — you don't pass anything beyond validate: true.

If the schema can't be fetched (network failure, model unknown to the registry, etc.), 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 the AJV details on validationErrors:

import{RunwareError}from'@runware/sdk'try{awaitclient.run({ ... })}catch(error){if(errorinstanceofRunwareError&&error.code==='validation'){console.error(error.taskType)// 'imageInference'console.error(error.validationErrors)// AJV error objects}}

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

// Force validation for just this call, even if config.validate is falseawaitclient.run({ ... },{validate: true})// Skip validation for just this call, even if config.validate is trueawaitclient.run({ ... },{validate: false})

Precedence: options.validate ?? config.validate.

To force a refresh of cached validators (e.g., after a server-side schema change), call:

import{clearValidatorCache}from'@runware/sdk'clearValidatorCache()

Utility Methods

// Search for available modelsconstmodels=awaitclient.modelSearch({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 imageUpload.constuploaded=awaitclient.mediaStorage({operation: 'upload',media: 'https://example.com/photo.jpg',// URL, Data URI, or Base64})// Delete stored media by its mediaUUIDawaitclient.mediaStorage({operation: 'delete',media: uploaded[0].mediaUUID})// Get account detailsconstaccount=awaitclient.accountManagement({operation: 'getDetails',})// Retrieve a previously executed task by UUID (archive lookup)constarchived=awaitclient.getTaskDetails({taskUUID: 'abc-123'})// Poll for an async task result (used internally by .run() — rarely needed directly)constresult=awaitclient.getResponse({taskUUID: 'abc-123'})// Upload a custom modelawaitclient.modelUpload({category: 'checkpoint',architecture: 'sdxl',format: 'safetensors',// ...plus model file details})

getTaskDetails vs getResponse: use getTaskDetails for "look up something I ran before" — it queries the task archive. getResponse 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 filteredconstmodels=awaitclient.content.listModels({capability: 'io:text-to-image',category: 'image',creator: 'black-forest-labs',search: 'flux',})// Single curated model by idconstmodel=awaitclient.content.getModel('alibaba-z-image-turbo')// Sample input/output pairs the model can produceconstexamples=awaitclient.content.getModelExamples('flux-1-dev')// Pricing summary and per-configuration examplesconstpricing=awaitclient.content.getModelPricing('flux-1-dev')// Discover the capability taxonomy (io:*, op:*, form:*)constcapabilities=awaitclient.content.listCapabilities()// Collections (Runware-defined model groupings) with full model objects inlinedconstcollections=awaitclient.content.listCollections({category: 'image'})// Creators with their curated models inlinedconstcreators=awaitclient.content.listCreators()constgoogle=awaitclient.content.getCreator('google')// Pagination — pass paginate: true to get { total, limit, offset, items }constpage=awaitclient.content.listModels({paginate: true,limit: 25,offset: 0})

The per-model methods (getModel, getModelExamples, getModelPricing) accept either the model's AIR or its catalog slug (the model field returned by listModels).

creator, capabilities, and architecture on each model are returned as id strings — resolve them against listCreators, listCapabilities, 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

fileToDataURI encodes a local file or in-memory buffer as a data: URI for passing as input:

import{fileToDataURI}from'@runware/sdk'import{readFile}from'node:fs/promises'constdataUri=awaitfileToDataURI(awaitreadFile('photo.jpg'))awaitclient.mediaStorage({operation: 'upload',media: dataUri})

In Node you usually don't need this for inputs: run() and mediaStorage auto-encode local file paths. Any string value (recursively, including nested objects and arrays) 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. In the browser there's no filesystem, so this step is a no-op.

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

Custom dependencies

For testing, proxies, or environments with non-standard runtimes, inject your own WebSocket constructor and/or fetch implementation:

constclient=awaitcreateClient({apiKey: process.env.RUNWARE_API_KEY??'your-api-key',dependencies: {WebSocket: CustomWebSocketClass,fetch: customFetchFunction,},})

The SDK works in Node.js 18+ and modern browsers with no polyfills.

TypeScript

Types are generated from Runware's canonical JSON schemas and ship with the SDK:

importtype{RunwareClient,SchemaKey,SchemaMap,TextStream,TextStreamResult,RunwareError,}from'@runware/sdk'// Schema-driven types — always match the APItypeSdxlParams=SchemaMap['sdxl']['params']typeSdxlResult=SchemaMap['sdxl']['result']// Type-safe run callconstimages=awaitclient.run<'sdxl'>({model: 'civitai:133005@782002',positivePrompt: 'test',width: 1024,height: 1024,})// images is typed as ImageInferenceResult[]consturl: string=images[0].imageURL

License

MIT

About

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

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages