@tchavi/sdk
v0.1.7
Published
Official Node.js SDK for the Tchavi AI API gateway
Readme
@tchavi/sdk
Official Node.js SDK for the Tchavi AI API gateway — use OpenAI-compatible models with credit-based billing, without managing provider keys yourself.
Requirements
- Node.js 20+ (uses native
fetchandReadableStream) - TypeScript 5+ (optional but recommended)
Installation
npm install @tchavi/sdk
# or
pnpm add @tchavi/sdk
# or
yarn add @tchavi/sdkQuick start
import Tchavi from '@tchavi/sdk';
const client = new Tchavi({ apiKey: 'sk-tch-...' });
const response = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'Hello!' }],
});
console.log(response.choices[0].message.content);
console.log('Credits used:', response.tchavi.credits_used);Get your API key from the Tchavi dashboard.
Chat completions
Non-streaming
const completion = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'What is the capital of France?' },
],
temperature: 0.7,
max_tokens: 256,
});
console.log(completion.choices[0].message.content);
// Credit metadata appended by Tchavi:
console.log(completion.tchavi.credits_used);
console.log(completion.tchavi.credits_remaining);Streaming
const stream = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Tell me a story.' }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
}
// Credit metadata available after iteration completes:
const meta = stream.finalMeta();
console.log('\nCredits used:', meta?.credits_used);Chat with a PDF
Attach PDFs to a user message by passing an array of content blocks. Use the document_url block with a public URL (we recommend storing the PDF on the Tchavi platform bucket first via the upload endpoint).
const completion = await client.chat.completions.create({
model: 'claude-sonnet-4-6',
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'Summarize this document in 5 bullet points.' },
{
type: 'document_url',
document_url: { url: 'https://your-bucket.r2.dev/contract.pdf' },
},
],
},
],
});
console.log(completion.choices[0].message.content);Up to 5 PDFs per request. Only models with supportsDocuments: true accept document inputs — currently claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5-20251001, gpt-4.1, and gpt-4.1-mini. Check the capability before sending:
const models = await client.models.list();
const docModels = models.data.filter((m) => m.supportsDocuments);Embeddings
const result = await client.embeddings.create({
model: 'text-embedding-3-small',
input: 'The quick brown fox jumps over the lazy dog',
});
console.log(result.data[0].embedding); // number[]Image generation
Nano Banana (Gemini native)
const result = await client.images.generations.create({
prompt: 'A futuristic city skyline at sunset, cinematic lighting',
model: 'nano-banana-pro',
resolution: '2K',
aspect_ratio: '16:9',
output_format: 'webp',
compression_quality: 80,
temperature: 0.8,
n: 2,
});
console.log(result.data[0].b64_json);
console.log('Credits used:', result.tchavi.credits_used);Image editing with reference images:
const result = await client.images.generations.create({
prompt: 'Change the background to a beach sunset',
model: 'nano-banana-pro',
reference_images: [{ b64_json: 'BASE64...' }],
resolution: '1K',
});| Model | Resolutions | Credits (default 1K) |
|---|---|---|
| nano-banana | 1K | 128 |
| nano-banana-2 | 0.5K, 1K, 2K, 4K | 220 |
| nano-banana-pro | 1K, 2K, 4K | 439 |
Imagen 4
const result = await client.images.generations.create({
prompt: 'A professional flyer for a sandwich restaurant, warm colors',
model: 'imagen-4',
negative_prompt: 'blurry, low quality, watermark',
aspect_ratio: '9:16',
resolution: '2K',
enhance_prompt: true,
seed: 42,
n: 1,
});| Model | Resolutions | Notes |
|---|---|---|
| imagen-4-fast | — | Fastest, no resolution control |
| imagen-4 | 1K, 2K | Standard |
| imagen-4-ultra | 1K, 2K | Highest quality |
GPT Image
const result = await client.images.generations.create({
prompt: 'A clean app-store icon for a budgeting app, simple, high contrast',
model: 'gpt-image-1-mini',
size: '1024x1024',
quality: 'medium',
output_format: 'webp',
output_compression: 80,
background: 'transparent',
n: 1,
});Image editing with reference images (auto-routes to /v1/images/edits):
const result = await client.images.generations.create({
prompt: 'Add a floral pattern to the vase',
model: 'gpt-image-1',
images: ['data:image/png;base64,...'],
input_fidelity: 'high',
// mask: 'data:image/png;base64,...', // optional — transparent pixels = edit area
});| Model | Sizes | Qualities | Notes |
|---|---|---|---|
| gpt-image-1-mini | 1024x1024, 1536x1024, 1024x1536 | low/medium/high | Most affordable |
| gpt-image-1 | same | low/medium/high | Supports input_fidelity |
| gpt-image-1.5 | same | low/medium/high | Latest flagship |
All image parameters
| Parameter | Nano Banana | Imagen 4 | GPT Image | Description |
|---|---|---|---|---|
| prompt | ✓ | ✓ | ✓ | Main prompt |
| n | 1–4 | 1–4 | 1–4 | Number of images |
| negative_prompt | appended | ✓ | — | What to exclude |
| aspect_ratio | 14 values | 5 values | — | Image aspect ratio |
| resolution | 0.5K–4K | 1K–2K | — | Output resolution (Google) |
| size | — | — | 3 values | Output dimensions (OpenAI) |
| quality | — | — | low/medium/high | Render quality (OpenAI) |
| output_format | png/jpeg/webp | png/jpeg | png/jpeg/webp | File format |
| compression_quality | 0–100 | 0–100 | — | JPEG/WebP quality (Google) |
| output_compression | — | — | 0–100 | JPEG/WebP quality (OpenAI) |
| images / reference_images | up to 14 | — | up to 16 | Input images for editing |
| mask | — | — | ✓ | Edit mask (transparent = edit area) |
| input_fidelity | — | — | gpt-image-1 only | Preserve facial features |
| background | — | — | ✓ | transparent / opaque / auto |
| temperature | 0–2 | — | — | Creativity control |
| seed | — | ✓ | — | Reproducible output |
| enhance_prompt | — | ✓ | — | Auto-rewrite prompt |
| person_generation | — | ✓ | — | Person policy |
| safety_level | ✓ | ✓ | — | Safety filter level |
Audio
Text-to-speech
client.audio.speech.create() returns the raw Response. Call .arrayBuffer() to get the audio bytes or .body to stream them directly.
const response = await client.audio.speech.create({
model: 'tts-1',
input: 'Hello, welcome to Tchavi.',
voice: 'alloy',
response_format: 'mp3', // mp3 | opus | aac | flac | wav | pcm
speed: 1.0, // 0.25–4.0, default 1.0
});
// Save to file (Node.js)
import { writeFile } from 'node:fs/promises';
const buffer = Buffer.from(await response.arrayBuffer());
await writeFile('speech.mp3', buffer);Available voices: alloy, ash, ballad, cedar, coral, echo, fable, marin, nova, onyx, sage, shimmer.
Available models: tts-1 (faster, lower cost), tts-1-hd (higher quality).
Transcription (Whisper)
import { readFile } from 'node:fs/promises';
const audioBuffer = await readFile('recording.mp3');
const file = new File([audioBuffer], 'recording.mp3', { type: 'audio/mpeg' });
const result = await client.audio.transcriptions.create({
model: 'whisper-1',
file,
language: 'en', // optional — omit for auto-detect
response_format: 'json', // json | text | srt | vtt | verbose_json
});
console.log(result.text);
console.log('Duration (min):', result.tchavi.duration_minutes);
console.log('Credits used:', result.tchavi.credits_used);Video generation
Video models (Seedance) run asynchronously. Submit a job with
client.videos.generations.create(...) and poll
client.jobs.retrieve(id) until it reaches completed or failed. For most
cases, the createAndWait(...) helper does both in a single call.
One-liner
const job = await client.videos.generations.createAndWait({
model: 'seedance-2',
prompt: 'A cinematic shot of the Cotonou Amazone statue at golden hour',
duration: 5, // 1–15 or -1 (auto, billed at 10s)
resolution: '480p', // '480p' | '720p'
aspect_ratio: '9:16',
generate_audio: true,
});
if (job.status === 'completed') {
console.log('Video URL:', job.output?.video_url);
console.log('Credits used:', job.tchavi?.credits_used);
} else {
console.error('Failed:', job.error?.message);
}Options: pollIntervalMs (default 5s), timeoutMs (default 5 min), signal
(AbortSignal to stop polling — the backend job keeps running), and onPoll
to track progress:
const controller = new AbortController();
setTimeout(() => controller.abort(), 2 * 60_000);
const job = await client.videos.generations.createAndWait(
{ model: 'seedance-2', prompt: '...', duration: 10 },
{ pollIntervalMs: 3000, signal: controller.signal, onPoll: (j) => console.log(j.status) },
);Fire-and-poll (advanced)
When you need to surface intermediate progress in your own UI:
const submission = await client.videos.generations.create({
model: 'seedance-2',
prompt: '...',
resolution: '720p',
});
let job = await client.jobs.retrieve(submission.id);
while (job.status === 'pending' || job.status === 'processing') {
await new Promise((r) => setTimeout(r, 5000));
job = await client.jobs.retrieve(submission.id);
}
console.log(job.status, job.output?.video_url);Image-to-video with references
await client.videos.generations.createAndWait({
model: 'seedance-2',
prompt: 'Animate this character walking forward',
image_url: 'https://cdn.example.com/character.jpg',
// OR — instead of image_url, pass up to 9 reference_images
// reference_images: ['https://.../ref1.jpg', 'https://.../ref2.jpg'],
duration: 10,
resolution: '720p',
seed: 42,
});Handling credit / timeout errors
import Tchavi, {
TchaviInsufficientCreditsError,
TchaviTimeoutError,
} from '@tchavi/sdk';
try {
const job = await client.videos.generations.createAndWait({
model: 'seedance-2',
prompt: '...',
duration: 10,
resolution: '720p',
}, { timeoutMs: 3 * 60_000 });
} catch (err) {
if (err instanceof TchaviInsufficientCreditsError) {
console.log('Top up:', err.creditsRemaining, 'credits left');
} else if (err instanceof TchaviTimeoutError) {
// Job is still running server-side — poll manually with err.jobId.
const job = await client.jobs.retrieve(err.jobId);
console.log('Eventual status:', job.status);
} else {
throw err;
}
}Listing past jobs
const { data } = await client.jobs.list({ status: 'completed', limit: 10 });
for (const job of data) {
console.log(job.created_at, job.model, job.output?.video_url);
}Error handling
import Tchavi, {
TchaviAuthenticationError,
TchaviInsufficientCreditsError,
TchaviRateLimitError,
TchaviProviderError,
TchaviTimeoutError,
TchaviAPIError,
TchaviError,
} from '@tchavi/sdk';
try {
const response = await client.chat.completions.create({ ... });
} catch (err) {
if (err instanceof TchaviInsufficientCreditsError) {
// 402 — top up credits
console.log('Credits remaining:', err.creditsRemaining);
} else if (err instanceof TchaviRateLimitError) {
// 429 — back off and retry
console.log('Retry after (s):', err.retryAfter); // number | null
} else if (err instanceof TchaviAuthenticationError) {
// 401 — invalid or revoked API key
console.log('Check your API key');
} else if (err instanceof TchaviProviderError) {
// 502/503 — upstream AI provider failed, retry later
console.log('Provider issue:', err.provider, err.message);
} else if (err instanceof TchaviAPIError) {
// Other HTTP errors
console.log(err.statusCode, err.errorCode, err.message);
} else if (err instanceof TchaviError) {
// Network/timeout errors
console.log('Network error:', err.message);
}
}All error messages are clean English regardless of the server's locale. The original server message is available on err.serverMessage.
| Error class | Status | Extra fields |
|---|---|---|
| TchaviError | — | Base class for all SDK errors |
| TchaviAPIError | any | statusCode, errorCode, serverMessage, headers |
| TchaviAuthenticationError | 401 | — |
| TchaviInsufficientCreditsError | 402 | creditsRemaining: number |
| TchaviRateLimitError | 429 | retryAfter: number \| null |
| TchaviProviderError | 502/503 | provider?: string |
Account management
These methods use JWT auth — pass email and password instead of apiKey:
const client = new Tchavi({
email: '[email protected]',
password: 'my-password',
});Credits
const { creditsBalance } = await client.credits.getBalance();Billing
// Estimate credits before topping up (public — no auth required)
const estimate = await client.billing.estimate({ amount: 5000, currency: 'XOF' });
console.log(estimate.credits, estimate.bonusLabel);
// Top up via mobile money
const topup = await client.billing.topup({
amount: 5000,
currency: 'XOF', // optional — defaults to 'XOF'
phoneNumber: '22900000000',
correspondent: 'MTN_MOMO_BEN',
});
// Current balance and account level
const balance = await client.billing.getBalance();
console.log(balance.credits, balance.level); // e.g. 1200, 'BUILDER'
// Top-up history
const history = await client.billing.getHistory({ page: 1, limit: 20 });API keys
// List all keys
const keys = await client.apiKeys.list();
// Create a new key (full secret shown only once)
const { key, ...apiKey } = await client.apiKeys.create({ label: 'Production' });
console.log('Save this:', key);
// Retrieve / update / delete
const existing = await client.apiKeys.retrieve('key-id');
await client.apiKeys.update('key-id', { label: 'Renamed' });
await client.apiKeys.delete('key-id');Usage
const stats = await client.usage.getStats({ period: 'month' }); // 'day' | 'week' | 'month'
const history = await client.usage.getHistory({ page: 1, limit: 20 });Payments
// Initiate a credit package purchase
const payment = await client.payments.initiate({ packId: 'pack-id', phone: '+22900000000' });
// Check payment status
const status = await client.payments.getStatus(payment.paymentId);
// Payment history
const history = await client.payments.getHistory();Available models
const models = await client.models.list();
// models[].modelId — use this as the `model` field in requestsAuth & user profile
// Explicit login (sets the JWT for subsequent requests)
const { accessToken, user } = await client.auth.login('[email protected]', 'password');
// Register a new account
await client.auth.register({ email: '[email protected]', password: 'secret', name: 'Alice' });
// Logout (invalidates the session)
await client.auth.logout();
// Get the authenticated user's profile
const me = await client.users.me();
// Update profile
await client.users.update({ name: 'Alice', language: 'fr' });
// Change password
await client.users.update({ currentPassword: 'old', newPassword: 'new' });Configuration
const client = new Tchavi({
// Authentication (one of):
apiKey: 'sk-tch-...', // For proxy endpoints (chat, embeddings, images, audio)
email: '[email protected]', // For account management endpoints
password: 'my-password', // (requires email)
// Optional:
baseURL: 'https://tchavi.com/api', // Default: https://tchavi.com/api
maxRetries: 2, // Retries on 429/502/503. Default: 2
timeout: 60_000, // Request timeout in ms. Default: 60000
});| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | string | — | API key (sk-tch-...) for proxy auth |
| email | string | — | Email for JWT auth |
| password | string | — | Password for JWT auth |
| baseURL | string | https://tchavi.com/api | API base URL |
| maxRetries | number | 2 | Max retries on transient errors |
| timeout | number | 60000 | Request timeout (ms) |
License
MIT
