@picsart/ai-sdk
v5.35.1
Published
Type-safe SDK for 100+ AI models — image, video, audio, and text generation with Picsart
Maintainers
Readme
@picsart/ai-sdk
Generate images, video, audio, and text with 100+ AI models.
Documentation
Full guides, the model catalog, and the API reference live on the Picsart API Platform:
- Documentation -- overview and guides
- Quickstart -- install, authenticate, first generation
- SDK guide -- client setup,
generate/generateText, Drive, lifecycle - Authentication -- create an API key
- Model catalog -- browse every supported model
- API reference
Repository: github.com/PicsArt/ai-sdk
Quick Start
npm install @picsart/ai-sdkimport { createClient, Models, Model, catalog } from '@picsart/ai-sdk'
// Create a client — pass your Picsart API key
const ai = createClient({
apiKey: process.env.PICSART_API_KEY,
apiUrl: 'https://api.picsart.com',
})
// Generate (Models.* are typed model-id constants)
const result = await ai.generate(Models.Flux2Pro, { prompt: 'a cat on mars' })
console.log(result.url)
// Browse models — the `catalog` accessor
catalog.all() // every model
catalog.find({ output: 'video' }) // video models only
catalog.search('kling') // search by name/id/provider
// Model metadata & params — the `Model` accessor
Model(Models.Flux2Pro).name // 'Flux 2 Pro'
Model(Models.Flux2Pro).meta() // mode, provider, badges, …
Model(Models.Flux2Pro).params() // accepted parameters
Model(Models.Flux2Pro).params().toSchema() // param schema
// Validate input (never throws) → { valid, errors? }
Model(Models.Flux2Pro).validate({ prompt: 'a cat' })Authentication
Pass apiKey and the SDK sends Authorization: Bearer <apiKey> on every request
(a leading Bearer is stripped if present). Create a key from the
Authentication guide.
const ai = createClient({
apiKey: process.env.PICSART_API_KEY,
apiUrl: 'https://api.picsart.com',
})Keep the key server-side — anything shipped to a browser is public.
Apps that already handle auth themselves (session cookies, token refresh, a
backend proxy) can pass their own fetch instead. It takes precedence over
apiKey when both are set:
const ai = createClient({
fetch: myAuthenticatedFetch,
apiUrl: 'https://api.picsart.com',
})createClient throws if neither apiKey nor fetch is provided.
Drive Integration
Auto-save generations to Picsart Drive:
const ai = createClient({
apiKey: process.env.PICSART_API_KEY,
apiUrl: 'https://api.picsart.com',
drive: { folder: 'AI Playground' },
})
// Generates and auto-saves to the root folder
const result = await ai.generate(Models.Flux2Pro, { prompt: 'a cat' })
// result.drive = { uid: '...', folder: { name: 'AI Playground', uid: '...' } }
// Save to a subfolder
const board = await ai.drive.ensureFolder('Cats')
const result = await ai.generate(Models.Flux2Pro, { prompt: 'a cat' }, { folder: board })
// Browse Drive
const folders = await ai.drive.folders()
const items = await ai.drive.list()Text Generation (LLMs)
Claude, GPT, and Gemini text models are called with generateText(). Single-shot:
pass a prompt and optional image(s)/video, get text back. They surface in the catalog
as mode: 'text'. Each vendor uses its own native workflow, so capabilities aren't lost
— Gemini accepts video input, Claude uses the Anthropic-native messages API.
import { createClient, Models } from '@picsart/ai-sdk'
const ai = createClient({ apiKey: process.env.PICSART_API_KEY, apiUrl: 'https://api.picsart.com' })
// Text in, text out
const { text } = await ai.generateText(Models.ClaudeOpus48, { prompt: 'Explain RAG in one line.' })
console.log(text)
// Optional vision input + reasoning level (OpenAI / Gemini)
const res = await ai.generateText(Models.Gpt55, {
prompt: 'What is in this image?',
imageUrls: ['https://cdn.example.com/photo.jpg'],
thinking: 'high', // 'off' | 'low' | 'medium' | 'high'
})
console.log(res.text)
console.log(res.raw) // full backend response — usage, finish_reason, etc.
// Gemini accepts video input
await ai.generateText(Models.Gemini3Pro, {
prompt: 'Summarize this clip',
videoUrl: 'https://cdn.example.com/clip.mp4',
})
// Browse text models
catalog.find({ output: 'text' })Thinking level maps per vendor: OpenAI →
reasoning_effort, Gemini →thinkingConfig.thinkingLevel(LOW/HIGH). Claude’sclaude/v1/messagesworkflow exposes no thinking knob, so Claude models omitthinking.
generateText() is type-narrowed to text models (TextModelId); calling it with an
image/video model throws, and generate() throws on a text model — use the matching
method for each.
Voice, Avatar & Template Catalogs
Models with catalog-backed params (voices, avatars, effect / caption templates) serve their option lists
from platform catalog tasks (<vendor>/v1/catalog/<voices|avatars|templates|…>) — nothing
is bundled; the workers cache the lists and answer fast. Fetch them via
ai.catalogs:
// One page at a time — load more on scroll/pagination via nextCursor
const page = await ai.catalogs.voices('heygen-video-avatar')
// page: { items: CatalogItem[], nextCursor: string | null }
const more = await ai.catalogs.voices('heygen-video-avatar', { cursor: page.nextCursor! })
// Optional: preload the first page of every bound catalog at client creation
const ai = createClient({ apiKey, catalogs: { preload: true } })
// Fetched pages accumulate into the model's options, so existing accessors
// (and every picker built on them) see everything loaded so far:
Model('heygen-video-avatar').params().catalog('videoId')?.catalogOptionsCatalogItem is the standard shape across all vendors:
{ id, name, description?, tags, preview? { imageUrl | videoUrl | audioUrl }, meta? } —
id is sent back verbatim on generate as the bound param's value. Validation
never requires hydration: catalog-bound params accept any id and the platform
validates for real.
Advanced Lifecycle
For progress tracking, cancellation, and job recovery:
// Submit without waiting
const handle = await ai.submit(Models.KlingV3Pro, { prompt: 'a sunset' })
// Subscribe to status updates
for await (const update of ai.subscribe(handle)) {
console.log(update.status, update.progress?.percent)
}
// Or poll manually
const status = await ai.status(handle)Error Handling
Every failure thrown by generate(), generateText(), submit(), and result()
is an ApiError with the same four fields, so you can branch on the error
instead of pattern-matching its message:
import { createClient, Models, ApiError } from '@picsart/ai-sdk'
try {
const result = await ai.generate(Models.Flux2Pro, { prompt: 'a cat on mars' })
} catch (err) {
if (err instanceof ApiError) {
err.status // 402 — HTTP status, or its synthesized equivalent
err.code // 'payment_required' — platform `reason`, else an SDK code
err.reason // same value as `code`, named after the platform's own field
err.message // 'Submit failed (402): Not enough credits'
if (err.status === 402) return topUpCredits()
if (err.status === 429 || err.status >= 500) return retry()
if (err.code === 'validation_error') return showFormError(err.message)
}
throw err
}code carries the platform's reason verbatim whenever the API supplies one
(content_moderation, unauthorized, …). When it doesn't, the SDK fills in a
conventional slug for the status — payment_required for 402, rate_limited
for 429, and so on.
Failures that never reach the network get the status they semantically deserve, so one retry predicate covers every case:
| Failure | status | code |
|---------|----------|--------|
| Unknown model id | 400 | unknown_model |
| generate() on a text model (or the reverse) | 400 | wrong_model_mode |
| Parameter validation | 400 | validation_error |
| Async lifecycle on an execute-only transport | 400 | unsupported_transport |
| HTTP error from the API | the response's status | platform reason, else the status slug |
| Poll deadline exceeded | 408 | timeout |
| Aborted via options.signal, or a canceled job | 499 | aborted / canceled |
| Job finished FAILED | the task's statusCode, else 502 | platform reason, else generation_failed |
| Response the SDK can't parse | 502 | invalid_response |
Aborts raised by fetch itself are deliberately not wrapped, so
err.name === 'AbortError' keeps working on the DOMException.
message is human-readable and may change between versions — branch on status
and code, not on the message text.
Public API
The SDK exports 8 symbols:
| Export | Type | Description |
|--------|------|-------------|
| createClient | function | Create an AI client from an API key (or a custom authenticated fetch) |
| Models | object | Model catalog: 108 models + list/search/validate/toSchema |
| GenerateResult | type | { url, model, handle, drive? } |
| ClientConfig | type | { apiKey?, fetch?, apiUrl, drive? } — one of apiKey / fetch required |
| AuthenticatedFetch | type | (url, init?) => Promise<Response> — for the custom-fetch path |
| SdkTransport | type | Advanced: custom transport interface |
| WorkflowJobHandle | type | Job handle for submit/status/cancel |
| ApiError | class | Unified error: { status, code, reason, message } — see Error Handling |
Package Structure
packages/ai-sdk/
package.json
tsconfig.json
tsup.config.ts
src/
index.ts # Public API entry (7 exports)
client/
types.ts # ClientConfig, GenerateResult, DriveConfig
transport.ts # Authenticated fetch → SdkTransport
prepare.ts # Validate input, build payload, parse result
drive.ts # Drive folder management + file saving
index.ts # createClient() factory
core/
types.ts # ModelDefinition, ParamConfig, GenerationContext
workflow.ts # Generic polling/execution engine
contracts.ts # Runtime input validation
schema.ts # ParamConfig → JSON Schema
response.ts # Vendor-agnostic result extraction
pricing.ts # ToolId resolution
model-registry.ts # Model lookup indexes
providers.ts # Provider colors, labels, names
voices.ts # Voice catalogs (ElevenLabs, OpenAI, Gemini)
helpers.ts # Vendor utilities
generated/
model-constants.ts # AUTO-GENERATED: Models object + 108 constants
model-input-types.ts # AUTO-GENERATED: per-model TypeScript input types
vendors/
define.ts # defineModels() framework + params.* helpers
presets.ts # Reusable paramConfig factories
catalog/
index.ts # Aggregation: ALL_MODELS, VENDOR_CATALOGS
kling.ts # One file per vendor (31 total)
flux.ts
...
__tests__/ # SDK tests
scripts/ # Build scriptsAdding a New Model
Standard flow (pass-through payload)
When the backend accepts param values as-is (no field renaming needed):
- Add config in
src/vendors/catalog/{vendor}.tsviadefineModels():buildPayloadis optional — omit it and param values pass through as-is
- Run
npm run build:model-constantsto regenerate constants - Run
npm run build:model-input-typesto regenerate TypeScript types - Model automatically appears in
catalog.all()and as theModels.NewModelid constant
With payload transforms
When the vendor API uses different field names or value formats:
- Define the model config as above (no
buildPayload) - Run
npm run build:model-input-types— generates typed input for your model - Create
src/vendors/catalog/{vendor}.payloads.ts:import type { ModelInput } from '../../generated/model-input-types.ts'; import { registerPayloads } from '../define.ts'; import { SPECS, MODELS } from './{vendor}.ts'; registerPayloads({ SPECS, MODELS }, { 'model-id': (input: ModelInput<'model-id'>) => ({ prompt: input.prompt, aspect_ratio: input.aspectRatio, // rename for vendor API }), }); - Import the
.payloads.tsfile insrc/vendors/catalog/index.ts(after the vendor import)
See src/vendors/catalog/luma.ts + src/vendors/catalog/luma.payloads.ts for a working example.
