npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xmodeai/sdk

v0.7.2

Published

Official TypeScript/JavaScript SDK for the xMode API — AI image and video generation.

Readme

@xmodeai/sdk

Official TypeScript / JavaScript SDK for the xMode API — AI image and video generation.

  • Zero runtime dependencies. Uses the platform fetch — works on Node 18+, Bun, Deno, Cloudflare Workers and other edge runtimes.
  • Async-first. Submit a job, then poll or receive a webhook. Helpers do the polling for you.
  • Forward-compatible. New models and fields the API adds later never break an installed version.
  • Typed. First-class TypeScript types with autocomplete for known models, tolerant of everything else.

Install

npm install @xmodeai/sdk

Quickstart

import XMode from '@xmodeai/sdk';

const client = new XMode({ apiKey: process.env.XMODE_API_KEY });

// Submit and wait for the result in one call.
const result = await client.generations.createAndWait({
  endUserEmail: '[email protected]',
  model: 'xSD4.5',
  prompt: 'Editorial photo of a red panda astronaut, studio lighting, 85mm',
});

if (result.status === 'succeeded') {
  console.log(result.images[0]?.url); // signed URL, valid ~23h — download or re-host
} else if (result.status === 'failed') {
  console.error(result.error.code, result.error.message);
}

The API key is read from the XMODE_API_KEY environment variable when you don't pass apiKey explicitly.

Two retention clocks, worth keeping apart: the signed URLs last about 23 hours (re-reading the record re-signs them, but does not extend the window), while the record itself — metadata, cost, error — is kept for 90 days from creation and then removed. Anything you need beyond that belongs in your own store.

Some models take extra knobs — negativePrompt, and prompt rewriting via promptExtend / promptExtendMode / enableThinking. They are per-model and off by default; the ones a given model accepts are on its page in the docs, and a model that does not take one ignores it. The exception is a field whose silent absence would change what you get or pay for — references on a text-to-image-only model, sequential: 'auto' on a model without series — which raises XModeValidationError before you are charged. Note that rewriting makes seed non-reproducible: the prompt that runs is regenerated on each attempt.

Polling vs. webhooks

createAndWait polls for you (every 5s by default) and resolves once the job reaches a terminal status. If you'd rather drive it yourself:

const queued = await client.generations.create({ endUserEmail, model: 'xSD4.5', prompt });
// ... later
const record = await client.generations.get(queued.requestId);
// or poll an existing job until terminal:
const done = await client.generations.wait(queued.requestId, { pollInterval: 5000 });

// Delete a generation and its stored files once it's terminal
// (409 ConflictError while it's still queued/processing):
await client.generations.delete(queued.requestId);

// Delete many at once (≤200 per call, one HTTP request, partial success).
// deleteMany does NOT chunk — loop yourself for larger sets:
for (let i = 0; i < ids.length; i += 200) {
  const res = await client.generations.deleteMany(ids.slice(i, i + 200));
  // res.deleted / res.conflicts (still in-flight) / res.notFound
}

For production, prefer webhooks: pass webhookUrl to create and verify the delivery (see below) instead of polling.

createAndWait / wait return the terminal record — including failed and expired. They only throw XModePollTimeoutError if the wait budget (default 10 min for images, 20 min for videos) elapses first.

Videos

Video models come in two families, and CreateVideoInput is the superset of both — every field is valid on at least one model and rejected on the others. client.models.list() is the authority on which model accepts what.

All-in-one (xW3.0s, xW3.0sPro) — the fields you send pick the capability: none for text-to-video, first_frame/last_frame to pin exact frames, or reference_images/reference_videos/reference_audios to compose from supplied material. Keyframes and references cannot be mixed in one request.

const video = await client.videos.createAndWait({
  endUserEmail: '[email protected]',
  model: 'xW3.0s',
  prompt: 'Image 1 walks into the room and sits down, camera slowly pushing in',
  reference_images: ['https://your-cdn.example.com/character.png'],
  ratio: '16:9',
  duration: 10,
  // Same model and the same output quality, finished much sooner, at a higher
  // per-second rate. Both rates are in client.models.list().
  fast: true,
});

if (video.status === 'succeeded') console.log(video.videos[0]?.url);

Image-to-video (xW2.7S) — image is required, and prompt_extend defaults to true, so a seed alone does not reproduce a result unless you turn it off.

const clip = await client.videos.createAndWait({
  endUserEmail: '[email protected]',
  model: 'xW2.7S',
  prompt: 'camera slowly pushing in, gentle motion',
  image: 'https://your-cdn.example.com/input.png',
  resolution: '1080p',
  duration: 5,
});

// Removal is not split by kind — there is no `client.videos.delete`:
await client.generations.delete(clip.requestId);

Text

Text generation is the one synchronous endpoint: the answer comes back in the same call, so there is no request to poll and no webhook to register.

const { text, finishReason, cost } = await client.chat.create({
  endUserEmail: '[email protected]',
  model: 'xTL1.0',
  messages: [
    { role: 'system', content: 'You reply with a single paragraph and no preamble.' },
    { role: 'user', content: 'Describe a neon-lit alley after rain.' },
  ],
  maxTokens: 400,
});

if (finishReason === 'truncated') {
  // The answer hit `maxTokens`. Raise it, or ask for less.
}

Images in

A message's content can be an array of parts instead of a string, and one of those parts can be an image. This is the shape behind "describe this frame" and "turn this frame into a video prompt":

const { text } = await client.chat.create({
  endUserEmail: '[email protected]',
  model: 'xTL1.0',
  messages: [
    {
      role: 'user',
      content: [
        { type: 'image_url', image_url: { url: 'https://cdn.your-product.com/frames/a1b2c3.jpg' } },
        { type: 'text', text: 'Describe this image in detail.' },
      ],
    },
  ],
  maxTokens: 600,
});

Parts keep their order on the way to the model, so an image before the instruction reads differently from one after it.

URLs only. The image must be https and reachable from the public internet without authentication; a data: URI is refused by name, as is a URL carrying credentials (https://user:pass@…). A redirect is followed, so a share link or a bucket that points at its CDN works. The API fetches each URL to check it before the call is charged, so a dead link costs nothing — it comes back as XModeValidationError naming the index (Image at index 0 ...) rather than echoing your URL. Budget a second or two for that check.

The number of images one call may carry is per model, and so is the byte ceiling: client.models.list() publishes both as maxImagesPerCall and maxInputBytes on the entry whose type is 'text'. The byte ceiling counts text only — an image URL's length is free — and the price is flat per call whichever way you use it.

A full-length answer takes 15-25 seconds. The SDK waits up to 95 seconds for this endpoint regardless of the client-wide timeout, because that default (30s) is shorter than the endpoint needs — pass timeout per call to change it.

Billing is per call, not per token. The amount is the same whatever the length, cost.xTokens says what was charged, and a call that fails is refunded automatically. Like the other paying creates, this one is retried only on 429 — a retry after a call that actually succeeded would be a second charge.

Two things to know before you port a prompt from elsewhere:

  • The input limit is counted in UTF-8 bytes, not characters. Plain English is one byte per character; accented and non-Latin text is two to four.
  • Field names are camelCase (maxTokens, topP), and the endpoint rejects anything it does not recognise instead of ignoring it — so an OpenAI-shaped max_tokens comes back as a 400 naming the key rather than silently doing nothing. Streaming, multiple completions, tool calling and structured-output modes are not supported.

client.models.list() covers text models too, as type: 'text' entries carrying maxInputBytes, maxImagesPerCall, maxOutputTokens and defaultOutputTokens. Branch on type with a default — the union has gained a member once already.

Verifying webhooks

The webhook helper is published at the @xmodeai/sdk/webhooks subpath and uses Web Crypto, so it runs on Node and every edge runtime. Pass the raw request body — do not re-serialize the parsed JSON.

import { verifyWebhook } from '@xmodeai/sdk/webhooks';

// Example: a framework that gives you the raw body string.
const { event } = await verifyWebhook({
  payload: rawBody,
  headers: request.headers,
  secret: process.env.XMODE_WEBHOOK_SECRET,
});

if (event.status === 'succeeded') {
  // event is the same shape as generations.get / videos.get
}

Verification fails (throws WebhookVerificationError with a reason) on a bad signature, a timestamp outside the 5-minute tolerance, or missing headers.

Pagination

List methods return a value you can await for one page or for await to stream every item across all pages.

const firstPage = await client.generations.list({ pageSize: 50 });

for await (const generation of client.generations.list({ status: 'failed' })) {
  console.log(generation.requestId);
}

Model limits

Limits are not uniform across models — they differ in which size presets they accept and which one is the default, in the window for an explicit WxH, in how many reference and output images a request may carry, and in whether they can produce a connected series at all. Read them instead of hard-coding them: the values come from the same place the API validates against, so they cannot drift from what a call will actually accept.

const models = await client.models.list();

const m = models.find((x) => x.id === 'xSD5Pro');
if (m?.type === 'image') {
  m.sizePresets; // exhaustive; anything else is rejected before any debit
  m.defaultSize; // applied when `size` is omitted — differs between models
  m.explicitSize; // { minTotalPixels, maxTotalPixels, minAspectRatio, maxAspectRatio, minSide?, maxSide? }
  m.maxReferences; // 0 → text-to-image only; `references` is refused
  m.maxImagesPerRequest; // ceiling on `n`
  m.capabilities.sequential; // false → a series is refused; use `n` or several requests
}

Only models you can call right now are returned. The list is short and changes rarely — fetch it once at startup and cache it for the process lifetime.

Entries carry a billing shape (per_image / per_second) but never a rate: rates change without an SDK release, so live numbers live in the dashboard.

Balance and end-users

const { xTokens } = await client.balance.get();

await client.endUsers.block('[email protected]');
await client.endUsers.unblock('[email protected]');
await client.endUsers.delete('[email protected]'); // 409 ConflictError if jobs are pending

Error handling

Every API error is an instance of XModeAPIError, subclassed by the stable error code:

| Class | Code | HTTP | |---|---|---| | ValidationError | validation_error | 400 | | AuthenticationError | unauthorized, account_blocked | 401 | | PaymentRequiredError | payment_required | 402 | | PermissionDeniedError | forbidden | 403 | | NotFoundError | not_found | 404 | | ConflictError | conflict | 409 | | ContentPolicyError | content_policy | 422 | | RateLimitError | rate_limited | 429 | | ProviderError | provider_error, provider_timeout | 5xx | | TemporarilyUnavailableError | temporarily_unavailable | 503 | | InternalServerError | internal_error | 5xx |

import { RateLimitError, ContentPolicyError } from '@xmodeai/sdk';

try {
  await client.generations.create({ endUserEmail, model: 'xSD4.5', prompt });
} catch (err) {
  if (err instanceof ContentPolicyError) {/* prompt was rejected */}
  else if (err instanceof RateLimitError) {/* back off; err.retryAfter is in seconds */}
  else throw err;
}

Network failures throw APIConnectionError / APIConnectionTimeoutError. An unrecognized error code yields a plain XModeAPIError rather than crashing, so a new server-side code never breaks your handling.

Retries

Retryable requests use exponential backoff with jitter and honor Retry-After. By default:

  • GET and idempotent operations (block, unblock, delete) retry on network errors, 408, 429 and 5xx (default maxRetries: 2).
  • generations.create / videos.create retry only on 429. They debit xTokens and have no idempotency key, so a timed-out create is never retried automatically — it might have succeeded server-side.

Using new API features before they're in the SDK

A new model that uses an existing endpoint just works — model accepts any string. For a brand-new parameter or endpoint that isn't typed yet, use the low-level escape hatch (same auth, retry and error handling):

await client.request({
  method: 'POST',
  path: '/v1/videos',
  body: { endUserEmail, model: 'xSV2.0', prompt, image, cameraMotion: 'orbit' },
});

Configuration

new XMode({
  apiKey: '...',            // default: process.env.XMODE_API_KEY
  baseUrl: '...',           // default: https://api.xmode.ai
  timeout: 30_000,          // per-attempt timeout in ms
  maxRetries: 2,            // automatic retries for retryable requests
  fetch: customFetch,       // custom fetch (tests / older runtimes)
  defaultHeaders: { ... },  // sent with every request
});

A custom baseUrl receives your Authorization: Bearer key on every request — only point it at an xMode-operated host.

Runtime support

Node 18.17+, Bun, Deno, Cloudflare Workers and other fetch-capable runtimes. The SDK can run in a browser, but doing so exposes your API key — keep keys server-side.

Versioning

Pre-1.0, breaking changes bump the minor version and additive changes bump the patch version. From 1.0.0 onward the SDK follows standard semver.

License

MIT