Calling models with TypeScript
Run inference from TypeScript: transports, typed parameters, validation, concurrency, streaming, and the utilities around them.
Introduction
Everything below assumes a client from the overview, which is where installation and authentication live. What follows is how you drive it: which transport carries the request, how the parameters are typed, and what the SDK does with more than one call at a time.
Choosing a transport
The SDK ships two transports behind the same API. They differ in connection model and in how the server delivers the result.
| Transport | When to use it |
|---|---|
rest | Serverless functions, edge runtimes, low-volume scripts. No persistent socket. |
websocket (default) | Many requests per process, lower per-call latency, push-based progress on long-running tasks. |
Construct with the transport you want:
const client = await createClient({
apiKey: process.env.RUNWARE_API_KEY,
transport: 'rest', // or 'websocket'
})Either transport supports two delivery modes, set with deliveryMethod. Sync waits for the result and returns it in one round trip, which works well for fast tasks like image inference. Async returns a task UUID immediately and the SDK polls for completion, which is what you want for video and other long-running operations.
// Sync (recommended for image inference and other fast tasks)
const images = await client.run({
taskType: 'imageInference',
model: 'runware:101@1',
positivePrompt: 'A coastal town at dusk',
width: 1024,
height: 1024,
deliveryMethod: 'sync',
})
// Async (the SDK polls for you; recommended for video)
const videos = await client.run({
taskType: 'videoInference',
model: 'google:3@3',
positivePrompt: 'Waves crashing on a beach',
width: 1280,
height: 720,
duration: 8,
// deliveryMethod defaults to 'async'
})Over WebSocket, requests and results travel on the same persistent socket. With async delivery the SDK gets a taskUUID and polls for completion over that connection. With sync the result comes straight back. Either way, the SDK reconciles each frame with its awaiting call. On Node, the SDK imports ws lazily. Browsers and edge runtimes use the platform WebSocket directly.
Typed parameters
When you know the task you're running, import the matching type and TypeScript validates the request shape at compile time:
import { createClient, type ImageInferenceParams } from '@runware/sdk'
const client = await createClient({ apiKey: process.env.RUNWARE_API_KEY })
const params: ImageInferenceParams = {
taskType: 'imageInference',
model: 'runware:101@1',
positivePrompt: 'A professional headshot portrait',
negativePrompt: 'blurry, distorted',
width: 1024,
height: 1024,
steps: 30,
}
const images = await client.run(params)The SDK ships one type per supported task. A wrong field name or a missing required field surfaces in your editor before the code runs.
Schema validation
The SDK can validate your request against the model's JSON Schema before it leaves the process. Errors (a missing model, or a width that isn't a valid enum) surface as a typed RunwareError with the offending field name, not as a 400 from the server hundreds of milliseconds later.
Validation is off by default and relies on ajv, an optional peer dependency. Install it, then turn validation on for a call (or globally via validate: true in createClient):
npm install ajvconst result = await client.run(payload, { validate: true })Concurrent requests
Promise.all is the canonical way to fan out:
const client = await createClient({
apiKey: process.env.RUNWARE_API_KEY,
transport: 'websocket',
})
await client.connect()
const [images, alt, noBg] = await Promise.all([
client.run({
taskType: 'imageInference',
model: 'runware:101@1',
positivePrompt: 'Abstract digital art',
width: 1024,
height: 1024,
}),
client.run({
taskType: 'imageInference',
model: 'runware:101@1',
positivePrompt: 'A neon-lit alley at night',
width: 1024,
height: 1024,
}),
client.run({
taskType: 'imageBackgroundRemoval',
inputImage: 'https://example.com/portrait.jpg',
}),
])WebSocket has a real edge here. All three requests share one socket and the responses stream back independently. REST would issue three HTTP requests in parallel and tear them down after each.
LLM streaming
Text inference supports Server-Sent Events streaming for low-latency generation. Call client.stream() and iterate over the resulting TextStream:
const stream = await client.stream({
taskType: 'textInference',
model: 'minimax:m2.7@0',
messages: [
{ role: 'user', content: 'Write a haiku about the ocean.' },
],
})
for await (const delta of stream.textStream) {
process.stdout.write(delta)
}
const result = await stream.result()
console.log(`\nFinish reason: ${result.finishReason}`)The stream exposes two async iterators (textStream and reasoningStream) plus a result() promise that resolves to the final accumulated text, finish reason, and usage stats. Iteration errors surface as typed exceptions, so a half-truncated stream never silently ends.
stream() handles a single completion only. Pass numberResults greater than 1 and it throws, use run() for batch text generation.
Content namespace
client.content reaches Runware's public model catalog and does not consume credits. Use it to list curated models, fetch pricing, pull example payloads, or browse the catalog by collection or creator.
// Search the curated catalog
const models = await client.content.listModels({
category: 'image',
creator: 'black-forest-labs',
search: 'flux dev',
})
// Inspect one
const flux = await client.content.getModel('flux-1-dev')
console.log(flux?.headline)
// Pull curated examples to seed prompts
const examples = await client.content.getModelExamples('flux-1-dev')
// Pricing for budget-driven decisions
const pricing = await client.content.getModelPricing('flux-1-dev')
// Browse the capability taxonomy, collections, and creators
const capabilities = await client.content.listCapabilities()
const collections = await client.content.listCollections({ category: 'image' })
const creators = await client.content.listCreators()The per-model methods (getModel, getModelExamples, getModelPricing) accept either the model's AIR or its catalog slug (the model field returned by listModels).
This is the same data the model picker and pricing pages render from. List endpoints accept paginate: true if you want a paginated envelope instead of a flat array.
Utility methods
Beyond inference, the client exposes the platform's utility endpoints. Each is a typed method that takes the same RunOptions second argument as run():
// Search the full live model catalog
const models = await client.modelSearch({ search: 'portrait', architecture: 'sdxl', limit: 10 })
// Store media for reuse as input (URL, data URI, or Base64)
const uploaded = await client.mediaStorage({ operation: 'upload', media: 'https://example.com/photo.jpg' })
// Account details (credits, limits)
const account = await client.accountManagement({ operation: 'getDetails' })
// Look up a task you ran earlier, by UUID
const archived = await client.getTaskDetails({ taskUUID: 'abc-123' })
// Upload a custom model
await client.modelUpload({ category: 'checkpoint', architecture: 'sdxl', format: 'safetensors' })modelSearch queries the full live catalog, including non-curated models. To browse the curated set as metadata without spending credits, use the content namespace above.
File helpers
fileToDataURI encodes a local file or in-memory buffer as a data: URI you can pass anywhere an image input is accepted:
import { fileToDataURI } from '@runware/sdk'
import { readFile } from 'node:fs/promises'
const dataUri = await fileToDataURI(await readFile('photo.jpg'))
await client.mediaStorage({ operation: 'upload', media: dataUri })Cancellation and progress
Every call accepts an AbortSignal. Aborting it causes the in-flight call (REST poll, WebSocket subscription, or LLM stream) to terminate cleanly and throw a RunwareError with code === 'aborted'.
Aborting is client-side only. The server keeps processing the task and you are still billed for it. Aborting just stops the SDK from waiting for the result.
For long-running tasks, two callbacks let you observe a task as it unfolds. onResult fires once per item as it reaches a terminal state, and onProgress fires when an item's progress field (0-100) changes. Only a few long-running models, mostly training, emit progress:
const result = await client.run(payload, {
onResult: (item) => {
console.log('Got partial:', item.imageUUID ?? item.videoUUID)
},
onProgress: (item) => {
console.log(`${item.progress}%`)
},
})