Chatoyant - v0.9.0
    Preparing search index...

    Chatoyant - v0.9.0

    Chatoyant ✨

    npm version CI TypeScript License: MIT

    Chatoyant Logo

    Unified TypeScript SDK for LLM providers (OpenAI, Anthropic, xAI, and any local OpenAI-compatible server) with streaming, structured outputs, and zero dependencies.

    chatoyant /ʃəˈtɔɪənt/ — having a changeable lustre, like a cat's eye in the dark


    npm install chatoyant
    

    Tree-shakable submodules — import only what you need:

    import { genText, Chat, Tool } from "chatoyant/core";
    import { createOpenAIClient } from "chatoyant/providers/openai";
    import { Schema } from "chatoyant/schema";
    import * as tokens from "chatoyant/tokens";

    The unified API works across OpenAI, Anthropic, xAI, and local models. Set your API key via environment variable (OPENAI_API_KEY, ANTHROPIC_API_KEY, or XAI_API_KEY). The provider is auto-detected from the model name, or defaults to OpenAI when using presets. For local models, set LOCAL_BASE_URL instead — see Local Models below.

    import { genText, genData, genStream, Schema } from "chatoyant";

    // One-shot text generation
    const answer = await genText("What is 2+2?");

    // Structured output with type safety
    class Person extends Schema {
    name = Schema.String();
    age = Schema.Integer();
    }
    const person = await genData("Extract: Alice is 30 years old", Person);
    console.log(person.name, person.age); // "Alice" 30

    // Streaming
    for await (const chunk of genStream("Write a haiku about TypeScript")) {
    process.stdout.write(chunk);
    }

    Model presets — use intent, not model names:

    await genText("Hello", { model: "fast" }); // Fastest response
    await genText("Hello", { model: "best" }); // Highest quality
    await genText("Hello", { model: "cheap" }); // Lowest cost
    await genText("Hello", { model: "balanced" }); // Good tradeoff

    Unified options — same API, any provider:

    await genText("Explain quantum physics", {
    model: "gpt-5.1", // Provider detected from model name
    reasoning: "high", // 'off' | 'low' | 'medium' | 'high'
    creativity: "balanced", // 'precise' | 'balanced' | 'creative' | 'wild'
    maxTokens: 1000,
    });

    // Or explicitly choose provider with presets
    await genText("Hello", { model: "fast", provider: "anthropic" });

    Use Chat for multi-turn conversations with tools:

    import { Chat, createTool, Schema } from "chatoyant";

    // Define a tool
    class WeatherParams extends Schema {
    city = Schema.String({ description: "City name" });
    }

    const weatherTool = createTool({
    name: "get_weather",
    description: "Get current weather for a city",
    parameters: WeatherParams,
    execute: async ({ args }) => {
    return { temperature: 22, conditions: "sunny" }; // Your API call here
    },
    });

    // Create chat with tool
    const chat = new Chat({ model: "gpt-4o" });
    chat.system("You are a helpful assistant with weather access.");
    chat.addTool(weatherTool);

    // Multi-turn conversation — tools are called automatically
    const reply = await chat.user("What's the weather in Tokyo?").generate();
    console.log(reply); // "The weather in Tokyo is 22°C and sunny!"

    // Usage metadata is always available after generate() or stream()
    console.log(chat.lastResult?.usage); // { inputTokens, outputTokens, ... }
    console.log(chat.lastResult?.cost); // { estimatedUsd }
    console.log(chat.lastResult?.iterations); // 2 (1 tool call + 1 final response)

    // Continue the conversation
    const followUp = await chat.user("How about Paris?").generate();

    // Streaming also populates lastResult after the generator completes
    for await (const chunk of chat.user("Tell me more").stream()) {
    process.stdout.write(chunk);
    }
    console.log(chat.lastResult?.timing); // { latencyMs }

    // Serialize for persistence
    const json = chat.toJSON();
    const restored = Chat.fromJSON(json);

    For direct provider access with full control, use the low-level clients below.


    Full client for GPT models, embeddings, and image generation.

    API Key: Set OPENAI_API_KEY in your environment.

    import { createOpenAIClient } from "chatoyant/providers/openai";

    const client = createOpenAIClient({
    apiKey: process.env.OPENAI_API_KEY!,
    });

    // Chat
    const text = await client.chatSimple([{ role: "user", content: "Hello!" }]);

    // Stream
    for await (const delta of client.streamContent([
    { role: "user", content: "Write a haiku" },
    ])) {
    process.stdout.write(delta.content);
    }

    // Structured output
    const data = await client.chatStructured<{ name: string; age: number }>(
    [{ role: "user", content: "Extract: Alice is 30" }],
    {
    name: "person",
    schema: {
    type: "object",
    properties: { name: { type: "string" }, age: { type: "number" } },
    },
    }
    );

    Full client for Claude models with streaming and tool use.

    API Key: Set ANTHROPIC_API_KEY in your environment.

    import { createAnthropicClient } from "chatoyant/providers/anthropic";

    const client = createAnthropicClient({
    apiKey: process.env.ANTHROPIC_API_KEY!,
    });

    // Chat
    const text = await client.messageSimple([{ role: "user", content: "Hello!" }]);

    // Stream
    for await (const delta of client.streamContent([
    { role: "user", content: "Write a haiku" },
    ])) {
    process.stdout.write(delta.text);
    }

    Full client for Grok models with native web search.

    API Key: Set XAI_API_KEY in your environment.

    import { createXAIClient } from "chatoyant/providers/xai";

    const client = createXAIClient({
    apiKey: process.env.XAI_API_KEY!,
    });

    // Chat
    const text = await client.chatSimple([{ role: "user", content: "Hello!" }]);

    // Web search (xAI-exclusive)
    const response = await client.chatWithWebSearch([
    { role: "user", content: "What happened in the news today?" },
    ]);

    // Reasoning models
    const result = await client.chat(
    [{ role: "user", content: "Solve this step by step..." }],
    {
    model: "grok-3-mini",
    reasoningEffort: "high",
    }
    );

    xAI provides grok-imagine-image for image generation and editing with natural language. Supports aspect ratios, resolution control, and multi-image editing.

    // Generate an image
    const url = await client.generateImageUrl("A futuristic cityscape at sunset", {
    aspectRatio: "16:9",
    resolution: "2k",
    });

    // Edit an existing image
    const edited = await client.editImageUrl(
    "Render this as a pencil sketch with detailed shading",
    "https://example.com/photo.png"
    );

    // Compose multiple images
    const composed = await client.editMultipleImages(
    "Add the cat from the first image to the second one",
    ["https://example.com/cat.jpg", "https://example.com/scene.jpg"]
    );

    xAI's grok-imagine-video supports text-to-video, image-to-video, and video editing. The API is asynchronous — polling is handled automatically.

    // Text-to-video (waits for completion)
    const video = await client.generateVideo("A timelapse of a flower blooming", {
    duration: 10,
    aspectRatio: "16:9",
    resolution: "720p",
    });
    console.log(video.url, `${video.duration}s`);

    // Animate a still image
    const animated = await client.generateVideoFromImage(
    "Gentle waves and flowing clouds",
    "https://example.com/landscape.jpg"
    );

    // Manual polling for long-running jobs
    const { requestId } = await client.startVideoGeneration("An epic scene...", {
    duration: 15,
    });
    // ... poll later:
    const status = await client.getVideoStatus(requestId);
    if (status.status === "done") console.log(status.video?.url);

    Cost calculation for media generation is available via chatoyant/tokens:

    import { calculateImageCost, calculateVideoCost } from "chatoyant/tokens";

    calculateImageCost({ model: "grok-imagine-image", count: 4 }); // $0.08
    calculateVideoCost({ model: "grok-imagine-video", durationSeconds: 10 }); // $0.50

    Chatoyant supports any server that speaks the OpenAI-compatible chat API — Ollama, LM Studio, llama.cpp, vLLM, LocalAI, and oMLX (great for running models natively on Apple Silicon via MLX).

    Tested: Qwen3.5-4B-MLX-4bit via oMLX — text generation, streaming, and multi-step tool calling all work out of the box.

    Zero config if you set the env var:

    export LOCAL_BASE_URL=http://127.0.0.1:11434/v1   # Ollama default
    export LOCAL_API_KEY=your-key # optional, defaults to "local"

    Any model name that chatoyant doesn't recognise as OpenAI / Anthropic / xAI is automatically routed to the local server:

    import { genText, genStream, Chat, createTool, Schema } from "chatoyant";

    // Text generation — model name auto-routes to LOCAL_BASE_URL
    const text = await genText("Write a haiku about local LLMs.", {
    model: "Qwen3.5-4B-MLX-4bit",
    });

    // Streaming
    for await (const chunk of genStream("Count from 1 to 5.", {
    model: "llama3.2:3b",
    })) {
    process.stdout.write(chunk);
    }

    Inline config (no env vars needed):

    const chat = new Chat({
    model: "Qwen3.5-4B-MLX-4bit",
    localBaseUrl: "http://127.0.0.1:8765/v1",
    localApiKey: "my-key", // optional
    localTimeout: 120_000, // optional, ms — useful for large models
    });

    Explicit provider: 'local' (useful when you want to force local even for ambiguous model names):

    await genText("Hello", { model: "my-fine-tune", provider: "local" });
    

    Multi-step tool calling works identically to cloud providers:

    class CalcParams extends Schema {
    operation = Schema.Enum(["add", "subtract", "multiply", "divide"]);
    a = Schema.Number();
    b = Schema.Number();
    }

    const calc = createTool({
    name: "calculate",
    description: "Perform one arithmetic operation.",
    parameters: CalcParams,
    execute: async ({ args }) => {
    const { operation, a, b } = args;
    return operation === "add" ? a + b
    : operation === "subtract" ? a - b
    : operation === "multiply" ? a * b
    : a / b;
    },
    });

    const chat = new Chat({ model: "Qwen3.5-4B-MLX-4bit" });
    chat.system("Use the calculate tool for every arithmetic step.");
    chat.addTool(calc);

    const answer = await chat
    .user("What is (6 * 7) - (20 / 4)?")
    .generate({ maxIterations: 8 });
    // → "The result is 37."

    Direct client for lower-level access:

    import { createLocalClient } from "chatoyant/providers/local";

    const client = createLocalClient({
    baseUrl: "http://127.0.0.1:11434/v1",
    apiKey: "local", // optional
    timeout: 120_000, // optional
    });

    const models = await client.listModelIds();
    const text = await client.chatSimple([{ role: "user", content: "Hello!" }]);

    Zero-dependency utilities for token estimation, cost calculation, and context management.

    import {
    estimateTokens,
    estimateChatTokens,
    calculateCost,
    getContextWindow,
    splitText,
    fitMessages,
    PRICING,
    CONTEXT_WINDOWS,
    } from "chatoyant/tokens";

    // Estimate tokens in text
    const tokens = estimateTokens("Hello, world!"); // ~3

    // Estimate tokens for a chat conversation
    const chatTokens = estimateChatTokens([
    { role: "system", content: "You are helpful." },
    { role: "user", content: "Hello!" },
    ]);

    // Calculate cost for an API call
    const cost = calculateCost({
    model: "gpt-4o",
    inputTokens: 1000,
    outputTokens: 500,
    });
    console.log(`Total: $${cost.total.toFixed(4)}`);

    // Get context window for a model
    const maxTokens = getContextWindow("claude-3-opus"); // 200000

    // Split long text into chunks for embeddings/RAG
    const chunks = splitText(longDocument, { maxTokens: 512, overlap: 50 });

    // Fit messages into context budget
    const fitted = fitMessages(messages, {
    maxTokens: 4000,
    reserveForResponse: 1000,
    });

    Typesafe JSON Schema builder with two-way casting. Define once, get perfect type inference and runtime validation.

    import { Schema } from "chatoyant/schema";

    class User extends Schema {
    name = Schema.String({ minLength: 1 });
    age = Schema.Integer({ minimum: 0 });
    email = Schema.String({ format: "email", optional: true });
    roles = Schema.Array(Schema.Enum(["admin", "user", "guest"]));
    }

    // Create with full type inference
    const user = Schema.create(User);
    user.name = "Alice";
    user.age = 30;
    user.roles = ["admin"];

    // Validate unknown data
    const isValid = Schema.validate(user, unknownData);

    // Convert to JSON Schema (for LLM structured outputs)
    const jsonSchema = Schema.toJSON(user);

    // Parse JSON into typed instance
    Schema.parse(user, jsonData);

    Types: String, Number, Integer, Boolean, Array, Object, Enum, Literal, Nullable


    If this package helps your project, consider sponsoring its maintenance:

    GitHub Sponsors


    AnonyfoxMIT License