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

@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.

npm version License: MIT

Requirements

  • Node.js 20+ (uses native fetch and ReadableStream)
  • TypeScript 5+ (optional but recommended)

Installation

npm install @tchavi/sdk
# or
pnpm add @tchavi/sdk
# or
yarn add @tchavi/sdk

Quick 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 requests

Auth & 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