@vidtory/ai-sdk
v1.1.0
Published
Vidtory AI SDK — generate text, images, video, and audio with a clean, type-safe API
Maintainers
Readme
Vidtory AI SDK
✨ Features
- Google AI SDK Style — Modular interface:
ai.models,ai.jobs,ai.media,ai.voices - Zero Production Dependencies — Uses native
globalThis.fetch, no bloat - Automated Polling — Async jobs resolve automatically with progress callbacks
- Retry with Exponential Backoff — Auto-recover from transient errors (429, 500, 502, 503, 504)
- AbortController Support — Cancel any generation or polling operation
- Per-Request Timeout — Prevent hung connections from blocking your app
- Debug Logging — Transparent request/response logging for development
- Middleware Hooks —
onRequest/onResponsehooks for custom logic - Dual CJS/ESM — Works everywhere: Node.js, Next.js, Vite, Deno, Edge, etc.
- TypeScript First — Full type safety with autocomplete from Vidtory's Swagger schemas
- Request ID Tracking — Every request tagged with a unique ID for debugging
📦 Installation
npm install @vidtory/ai-sdk🚀 Quick Start
1. Initialize the client
import { VidtoryAI } from '@vidtory/ai-sdk';
const ai = new VidtoryAI({
apiKey: process.env.VIDTORY_API_KEY,
});2. Generate text
const response = await ai.models.generateText({
prompt: 'Write a catchy headline for an organic coffee brand.',
});
console.log(response.result);
// → "Wake Up to Nature's Best Brew — Pure, Organic, Unforgettable."3. Generate an image
const response = await ai.models.generateImage({
prompt: 'A sleek ceramic mug on a wooden table, morning light, photorealistic',
aspectRatio: 'IMAGE_ASPECT_RATIO_LANDSCAPE',
resolution: '4K',
});
console.log(response.result);
// → "https://b2b.vidtory.net/.../image.jpg"4. Generate a video
const response = await ai.models.generateVideo({
prompt: 'Coffee pouring in slow motion, steam rising, macro lens',
duration: 8,
aspectRatio: 'VIDEO_ASPECT_RATIO_LANDSCAPE',
});
console.log(response.result);
// → "https://b2b.vidtory.net/.../video.mp4"5. Generate audio (TTS)
const response = await ai.models.generateAudio({
prompt: 'Xin chào, tôi là trợ lý ảo Vidtory.',
voiceId: 'tuan-voice-id',
languageCode: 'vi',
});
console.log(response.result);
// → "https://b2b.vidtory.net/.../audio.mp3"⚙️ Client Configuration
const ai = new VidtoryAI({
// ─── Authentication ───────────────────────────────────────
apiKey: 'vidtory_xxx', // Or set VIDTORY_API_KEY env var
baseURL: 'https://bapi.vidtory.net', // Or set VIDTORY_BASE_URL env var
// ─── Retry & Resilience ───────────────────────────────────
maxRetries: 3, // Auto-retry on transient errors (default: 3)
retryDelay: 500, // Base delay in ms (exponential backoff) (default: 500)
retryOn: [429, 500, 502, 503, 504], // HTTP status codes to retry on
// ─── Timeout ──────────────────────────────────────────────
timeout: 30_000, // Per-request HTTP timeout in ms (default: 30s)
// ─── Debug ────────────────────────────────────────────────
debug: true, // Or set VIDTORY_DEBUG=true env var
// ─── Custom Headers ───────────────────────────────────────
httpHeaders: {
'X-Custom-Tag': 'my-app',
},
// ─── Custom Fetch ─────────────────────────────────────────
fetch: customFetchFn, // Inject custom fetch (testing, proxy, edge)
// ─── Middleware Hooks ─────────────────────────────────────
onRequest: (url, init) => {
// Modify request before it's sent
const headers = new Headers(init.headers);
headers.set('X-Trace-ID', crypto.randomUUID());
return { ...init, headers };
},
onResponse: (response, url) => {
// Log or track responses
console.log(`${url} → ${response.status}`);
},
});Configuration Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | string | env.VIDTORY_API_KEY | API key for authentication |
| baseURL | string | 'https://bapi.vidtory.net' | Base URL of the API |
| maxRetries | number | 3 | Max retries for transient errors |
| retryDelay | number | 500 | Base retry delay in ms |
| retryOn | number[] | [429,500,502,503,504] | Retryable HTTP status codes |
| timeout | number | 30000 | Per-request timeout in ms |
| debug | boolean | false | Enable debug logging to stderr |
| httpHeaders | Record<string, string> | {} | Custom headers for all requests |
| fetch | typeof fetch | globalThis.fetch | Custom fetch implementation |
| onRequest | Function | — | Hook before each request |
| onResponse | Function | — | Hook after each response |
🔄 Polling & Async Control
All generative methods (generateText, generateImage, generateVideo, generateAudio, upscale) accept a second parameter for fine-grained polling control:
const response = await ai.models.generateImage(
{ prompt: 'A futuristic floating city' },
{
// ─── Polling Control ────────────────────────────────────
awaitResult: true, // true = wait for completion (default)
pollIntervalMs: 3000, // Check every 3s (default: 2000)
timeoutMs: 600_000, // Timeout after 10 min (default: 5 min)
// ─── Progress Tracking ──────────────────────────────────
onProgress: (status) => {
console.log(`Status: ${status}`); // 'PENDING' | 'PROCESSING' | 'COMPLETED'
},
// ─── Cancellation ───────────────────────────────────────
signal: controller.signal, // AbortSignal for cancellation
}
);Fire-and-Forget Mode
Skip automatic polling to manage jobs manually:
const response = await ai.models.generateImage(
{ prompt: 'An astronaut on Mars' },
{ awaitResult: false }
);
console.log('Job ID:', response.data.generationHistoryId);
// → Check status later with ai.jobs.getStatus()🛑 Cancellation (AbortController)
Cancel any in-flight generation or polling loop:
const controller = new AbortController();
const promise = ai.models.generateVideo(
{ prompt: 'A cinematic sunset timelapse' },
{ signal: controller.signal }
);
// Cancel after 30 seconds
setTimeout(() => controller.abort(), 30_000);
try {
const result = await promise;
} catch (error) {
if (error instanceof VidtoryAbortError) {
console.log('Generation was cancelled.');
}
}🔁 Automatic Retry
The SDK automatically retries transient errors with exponential backoff + jitter:
Attempt 1: immediate
Attempt 2: ~500ms + jitter
Attempt 3: ~1000ms + jitter
Attempt 4: ~2000ms + jitterFor HTTP 429 (Rate Limited), the SDK respects the Retry-After header from the server.
Disable retries for specific use cases:
const ai = new VidtoryAI({
apiKey: 'xxx',
maxRetries: 0, // No automatic retries
});📁 Media Management
Upload a reference image
import fs from 'fs';
const media = await ai.media.upload({
file: fs.readFileSync('./my-character.png'),
fileName: 'my-character.png',
metadata: { category: 'character', name: 'barista' },
});
console.log('Media ID:', media.id);
console.log('Media URL:', media.url);Use uploaded media as reference
const video = await ai.models.generateVideo({
prompt: 'A barista making latte art',
mode: 'i2v',
refImageUrl: media.url,
});Full CRUD operations
// List with metadata filter
const files = await ai.media.list({
metadata: { category: 'character' },
});
// Get by ID
const file = await ai.media.get('media-uuid');
// Update metadata
await ai.media.updateMetadata('media-uuid', {
category: 'hero',
verified: true,
});
// Delete
await ai.media.delete('media-uuid');📊 Jobs & Usage Tracking
Check job status
const status = await ai.jobs.getStatus('generation-history-id');
if (status.data.status === 'COMPLETED') {
console.log('Output URL:', status.data.result.url);
}List jobs with filters
const jobs = await ai.jobs.list({
status: 'COMPLETED',
type: 'image',
limit: 20,
offset: 0,
});
console.log(`${jobs.total} total jobs`);Auto-paginate all jobs
for await (const job of ai.jobs.listAll({ status: 'COMPLETED' })) {
console.log(job.id, job.type, job.status);
}Usage statistics
const stats = await ai.jobs.getStats('2026-01-01', '2026-01-31');
console.log('Total requests:', stats.data.totalRequests);
console.log('Credits used:', stats.data.totalCostInCredits);
console.log('Balance:', stats.data.currentBalance);🎙️ Text-to-Speech Voices
// List all voices
const allVoices = await ai.voices.list();
// Filter by language
const viVoices = await ai.voices.list({ language: 'vi' });
console.log(`${viVoices.data.length} Vietnamese voices available`);
viVoices.data.forEach((v) => {
console.log(` ${v.name} (${v.gender}) — ${v.descriptive}`);
});🔍 Debug Logging
Enable detailed request/response logging:
const ai = new VidtoryAI({
apiKey: 'xxx',
debug: true, // or set VIDTORY_DEBUG=true
});Output:
[VidtoryAI 2026-05-24T06:30:00.000Z] POST /generative-core/text → 200 (1234ms)
[VidtoryAI 2026-05-24T06:30:02.000Z] GET /generative-core/jobs/abc-123/status → 200 (245ms)
[VidtoryAI 2026-05-24T06:30:02.000Z] Poll #1 for abc-123: PENDING
[VidtoryAI 2026-05-24T06:30:04.000Z] Retry attempt 1/3 after 502ms❌ Error Handling
The SDK provides a structured error hierarchy:
import {
VidtoryAI,
VidtoryAPIError, // Non-2xx HTTP responses
VidtoryRateLimitError, // HTTP 429 (extends VidtoryAPIError)
VidtoryJobFailedError, // Job status = FAILED
VidtoryTimeoutError, // Polling timeout exceeded
VidtoryAbortError, // Request cancelled via AbortSignal
} from '@vidtory/ai-sdk';
try {
const result = await ai.models.generateImage({ prompt: '...' });
} catch (error) {
if (error instanceof VidtoryRateLimitError) {
console.error(`Rate limited. Retry after ${error.retryAfter}s`);
} else if (error instanceof VidtoryAPIError) {
console.error(`API Error ${error.statusCode}: ${error.message}`);
console.error('Request ID:', error.requestId);
console.error('Response:', error.responseData);
} else if (error instanceof VidtoryJobFailedError) {
console.error('Job failed:', error.message);
console.error('Job state:', error.job);
} else if (error instanceof VidtoryTimeoutError) {
console.error('Timed out. Job ID:', error.generationHistoryId);
} else if (error instanceof VidtoryAbortError) {
console.log('Request was cancelled.');
}
}Error Class Reference
| Error Class | When Thrown | Key Properties |
|---|---|---|
| VidtoryAPIError | Non-2xx HTTP response | statusCode, responseData, requestId |
| VidtoryRateLimitError | HTTP 429 | retryAfter (seconds) |
| VidtoryJobFailedError | Job status = FAILED | job (full Job object) |
| VidtoryTimeoutError | Polling exceeds timeout | generationHistoryId |
| VidtoryAbortError | AbortSignal triggered | — |
📖 Complete API Reference
ai.models.* — Generative AI
| Method | Default Model | Returns |
|---|---|---|
| generateText(params, pollOptions?) | gemini-3-flash-preview | GenerationResult<string> |
| generateImage(params, pollOptions?) | gemini-3.1-flash-image-preview | GenerationResult<string> (URL) |
| generateVideo(params, pollOptions?) | veo-3.1-fast-generate-001 | GenerationResult<string> (URL) |
| generateAudio(params, pollOptions?) | eleven_v3 | GenerationResult<string> (URL) |
| upscale(params, pollOptions?) | — | GenerationResult<string> (URL) |
ai.jobs.* — Job Tracking
| Method | Description |
|---|---|
| getStatus(id) | Get status and result of a job |
| list(params?) | List jobs with filters and pagination |
| listAll(params?) | Auto-paginating async iterator |
| getStats(startDate, endDate) | Credit usage statistics |
ai.media.* — Media Management
| Method | Description |
|---|---|
| upload(params) | Upload a file (max 20 MB) |
| list(params?) | List files with metadata filter |
| get(id) | Get file details |
| delete(id) | Delete a file |
| updateMetadata(id, metadata) | Update file metadata |
ai.voices.* — TTS Voices
| Method | Description |
|---|---|
| list(params?) | List voices with language filter |
🏗️ Framework Integration Examples
Next.js (App Router)
// app/api/generate/route.ts
import { VidtoryAI } from '@vidtory/ai-sdk';
import { NextResponse } from 'next/server';
const ai = new VidtoryAI(); // reads VIDTORY_API_KEY from env
export async function POST(request: Request) {
const { prompt } = await request.json();
const result = await ai.models.generateImage({ prompt });
return NextResponse.json({ imageUrl: result.result });
}Express.js
import express from 'express';
import { VidtoryAI, VidtoryAPIError } from '@vidtory/ai-sdk';
const app = express();
const ai = new VidtoryAI();
app.post('/generate', async (req, res) => {
try {
const result = await ai.models.generateText({
prompt: req.body.prompt,
});
res.json({ text: result.result });
} catch (error) {
if (error instanceof VidtoryAPIError) {
res.status(error.statusCode || 500).json({ error: error.message });
} else {
res.status(500).json({ error: 'Internal Server Error' });
}
}
});Convex (HTTP Action)
import { httpAction } from './_generated/server';
import { VidtoryAI } from '@vidtory/ai-sdk';
export const generate = httpAction(async (ctx, request) => {
const ai = new VidtoryAI({
apiKey: process.env.VIDTORY_API_KEY,
});
const { prompt } = await request.json();
const result = await ai.models.generateImage({ prompt });
return new Response(JSON.stringify({ url: result.result }), {
headers: { 'Content-Type': 'application/json' },
});
});📋 Environment Variables
| Variable | Description |
|---|---|
| VIDTORY_API_KEY | API key (fallback if not in constructor) |
| VIDTORY_BASE_URL | Base URL (fallback, default: https://bapi.vidtory.net) |
| VIDTORY_DEBUG | Set to 'true' to enable debug logging |
🔧 Build & Development
# Install dependencies
npm install
# Build (CJS + ESM + declarations)
npm run build
# Type-check only
npm run build:tsc
# Run tests
npm testBuild Output
dist/
├── index.js # CommonJS
├── index.mjs # ES Module
├── index.d.ts # TypeScript declarations (CJS)
├── index.d.mts # TypeScript declarations (ESM)
├── index.js.map # Source map (CJS)
└── index.mjs.map # Source map (ESM)📄 License
MIT
