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_KEYin 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_KEYin 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_KEYin 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-4bitvia 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: