@lfnovo/esperanto
v0.1.0
Published
A light-weight, production-ready, unified interface for various AI model providers
Maintainers
Readme
Esperanto TS 🌐
Esperanto is a lightweight TypeScript library that provides a unified interface for interacting with various Large Language Model (LLM) providers. It simplifies the process of working with different AI models (LLMs, Embedders, Transcribers, and TTS) APIs by offering a consistent interface while maintaining provider-specific optimizations.
It is the TypeScript port of the Python esperanto library — same providers, same configuration, same normalized responses, in camelCase and fully async.
Why Esperanto? 🚀
🪶 Ultra-Lightweight Architecture
- Direct HTTP Communication: All providers communicate directly via the runtime's native
fetch- no bulky vendor SDKs required - Zero Runtime Dependencies: Nothing but the standard library - a tiny footprint with zero overhead layers
- Production-Ready Performance: Direct API calls mean faster response times and lower memory usage
🔄 True Provider Flexibility
- Standardized Responses: Switch between any provider (OpenAI ↔ Anthropic ↔ Google ↔ etc.) without changing a single line of code
- Consistent Interface: Same methods, same response objects, same patterns across all 15+ providers
- Future-Proof: Add new providers or change existing ones without refactoring your application
⚡ Perfect for Production
- Prototyping to Production: Start experimenting and deploy the same code to production
- No Vendor Lock-in: Test different providers, optimize costs, and maintain flexibility
- Runs Anywhere: Bun ≥ 1.0 and Node ≥ 18, fully typed, ESM
Whether you're building a quick prototype or a production application serving millions of requests, Esperanto gives you the performance of direct API calls with the convenience of a unified interface.
Features ✨
- Unified Interface: Work with multiple LLM providers using a consistent API
- Provider Support:
- OpenAI (GPT-4o, o1, o3, o4, Whisper, TTS)
- OpenAI-Compatible (LM Studio, Ollama, vLLM, custom endpoints)
- Anthropic (Claude models)
- OpenRouter (Access to multiple models)
- xAI (Grok, TTS)
- Perplexity (Sonar models)
- Groq (Mixtral, Llama, Whisper)
- Google GenAI (Gemini LLM, Speech-to-Text, Text-to-Speech, Embedding with native task optimization)
- Vertex AI (Google Cloud, LLM, Embedding, TTS)
- Ollama (Local deployment, multiple models)
- ElevenLabs (Text-to-Speech, Speech-to-Text)
- Azure OpenAI (Chat, Embedding, Whisper, TTS)
- Mistral (Mistral Large, Small, Embedding, STT, TTS)
- DeepSeek (deepseek-chat)
- DashScope / Qwen (qwen-turbo, qwen-plus, qwen-max)
- MiniMax (MiniMax-M2.5)
- Deepgram (Speech-to-Text, Aura TTS)
- Voyage (Embeddings, Reranking)
- Jina (Advanced embedding models with task optimization, Reranking)
- Cohere (LLM, Embeddings, Reranking)
- Embedding Support: Multiple embedding providers for vector representations
- Reranking Support: Universal reranking interface for improving search relevance
- Speech-to-Text Support: Transcribe audio using multiple providers
- Text-to-Speech Support: Generate speech using multiple providers
- Fully Async: Every call returns a
Promise; streaming viafor await - Streaming: Support for streaming responses
- Structured Output: JSON output formatting (where supported)
- Tool Calling: Unified tool/function calling with optional built-in schema validation
Coming from the Python library? See Differences from Python — notably, the local
transformersprovider andto_langchain()are not part of the TS port.
📚 Documentation
- Quick Start Guide - Get started in 5 minutes
- Documentation Index - Complete documentation hub
- Provider Comparison - Choose the right provider
- Configuration Guide - Environment setup
By Capability
- Language Models (LLM) - Text generation and chat
- Embeddings - Vector representations
- Reranking - Search relevance
- Speech-to-Text - Audio transcription
- Text-to-Speech - Voice generation
By Provider
- Provider Setup Guides - Complete setup for all providers
Advanced Topics
- Tool Calling
- Task-Aware Embeddings
- Timeout Configuration
- SSL Configuration
- Model Discovery
- Differences from Python
Installation 🚀
bun add @lfnovo/esperanto
# or
npm install @lfnovo/esperantoThat's it — Esperanto has zero runtime dependencies. No optional extras, no vendor SDKs. Works on Bun ≥ 1.0 and Node ≥ 18 (ESM).
Provider Support Matrix
| Provider | LLM Support | Embedding Support | Reranking Support | Speech-to-Text | Text-to-Speech | JSON Mode | |--------------|-------------|------------------|-------------------|----------------|----------------|-----------| | OpenAI | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | | OpenAI-Compatible | ✅ | ✅ | ❌ | ✅ | ✅ | ⚠️* | | Anthropic | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | | Groq | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | | Google (GenAI) | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | | Vertex AI | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | | Ollama | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | | Perplexity | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | | ElevenLabs | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | | Deepgram | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | | Azure OpenAI | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | | Mistral | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | | DeepSeek | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | | Voyage | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | | Jina | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | | Cohere | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | | xAI | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | | DashScope | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | | MiniMax | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | | OpenRouter | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
*⚠️ OpenAI-Compatible: JSON mode support depends on the specific endpoint implementation
Quick Start 🏃♂️
You can use Esperanto in two ways: directly with provider-specific classes or through the AI Factory.
Using AI Factory
The AI Factory provides a convenient way to create model instances and discover available providers:
import { AIFactory } from "@lfnovo/esperanto";
// Get available providers for each model type
const providers = AIFactory.getAvailableProviders();
console.log(providers);
// {
// language: ['anthropic', 'azure', 'cohere', 'dashscope', 'deepseek', 'google', 'groq', 'minimax', 'mistral', 'ollama', 'openai', 'openai-compatible', 'openrouter', 'perplexity', 'vertex', 'xai'],
// embedding: ['openai', 'openai-compatible', 'google', 'ollama', 'vertex', 'voyage', 'mistral', 'azure', 'jina', 'openrouter', 'cohere'],
// speech_to_text: ['openai', 'groq', 'elevenlabs', 'openai-compatible', 'azure', 'google', 'mistral', 'deepgram'],
// text_to_speech: ['openai', 'elevenlabs', 'google', 'vertex', 'openai-compatible', 'azure', 'xai', 'mistral', 'deepgram'],
// reranker: ['jina', 'voyage', 'cohere']
// }
// Create model instances
const model = AIFactory.createLanguage("openai", "gpt-4o-mini", {
structured: { type: "json" },
}); // Language model
const embedder = AIFactory.createEmbedding("openai", "text-embedding-3-small"); // Embedding model
const reranker = AIFactory.createReranker("jina", "jina-reranker-v2-base-multilingual"); // Reranker model
const transcriber = AIFactory.createSpeechToText("openai", "whisper-1"); // Speech-to-text model
const speaker = AIFactory.createTextToSpeech("openai", "tts-1"); // Text-to-speech model
const messages = [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "What's the capital of France?" },
];
const response = await model.chatComplete(messages);
// Create embeddings
const texts = ["Hello, world!", "Another text"];
const embeddings = await embedder.embed(texts); // number[][] — one vector per textModel Discovery 🔍
Esperanto provides a convenient way to discover available models from providers without creating instances:
import { AIFactory } from "@lfnovo/esperanto";
// Discover available models from OpenAI
const models = await AIFactory.getProviderModels("openai", { apiKey: "your-api-key" });
for (const model of models) {
console.log(`${model.id} - owned by ${model.ownedBy}`);
}
// Filter by model type (for providers like OpenAI that support multiple types)
const languageModels = await AIFactory.getProviderModels("openai", {
apiKey: "your-api-key",
modelType: "language", // 'language', 'embedding', 'speech_to_text', 'text_to_speech'
});
// Some providers return hardcoded lists (e.g., Anthropic)
const claudeModels = await AIFactory.getProviderModels("anthropic");
for (const model of claudeModels) {
console.log(`${model.id} - Context: ${model.contextWindow} tokens`);
}
// OpenAI-compatible endpoints (requires baseUrl)
const localModels = await AIFactory.getProviderModels("openai-compatible", {
baseUrl: "http://localhost:1234/v1", // LM Studio, vLLM, etc.
});
for (const model of localModels) {
console.log(`${model.id} - ${model.ownedBy}`);
}Benefits of Static Discovery:
- ✅ No instance creation required - Query models without setting up providers
- ✅ Cached results - Model lists are cached for 1 hour to reduce API calls
- ✅ Flexible configuration - Pass provider-specific config (API keys, base URLs, etc.)
- ✅ Type filtering - Filter models by type for multi-model providers
See Model Discovery for the per-provider details.
Using Provider-Specific Classes
Here's a simple example to get you started:
import { OpenAILanguageModel } from "@lfnovo/esperanto";
// Initialize a provider with structured output
const model = new OpenAILanguageModel("gpt-4o", {
apiKey: "your-api-key", // Or set OPENAI_API_KEY env var
structured: { type: "json" }, // Optional, for JSON output
});
// Simple chat completion
const messages = [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "List three colors in JSON format" },
];
const response = await model.chatComplete(messages);
console.log(response.choices[0].message.content); // Will be in JSON formatStandardized Responses
All providers in Esperanto return standardized response objects, making it easy to work with different models without changing your code.
LLM Responses
import { AIFactory } from "@lfnovo/esperanto";
const model = AIFactory.createLanguage("openai", "gpt-4o-mini", {
structured: { type: "json" },
});
const messages = [{ role: "user", content: "Hello!" }];
// All LLM responses follow this structure
const response = await model.chatComplete(messages);
console.log(response.choices[0].message.content); // The actual response text
console.log(response.choices[0].message.role); // 'assistant'
console.log(response.model); // The model used
console.log(response.usage?.totalTokens); // Token usage information
console.log(response.content); // Shortcut for response.choices[0].message.content
// For streaming responses
for await (const chunk of model.chatCompleteStream(messages)) {
process.stdout.write(chunk.choices[0]?.delta.content ?? "");
}Handling Reasoning Traces
Some models (like Qwen3, DeepSeek R1) include chain-of-thought reasoning in <think> tags. The Message class provides convenient getters to handle this:
const response = await model.chatComplete(messages);
const msg = response.choices[0].message;
// Full response including reasoning
msg.content; // '<think>Let me analyze...</think>\n\n{"answer": 42}'
// Just the reasoning (null if no <think> tags)
msg.thinking; // "Let me analyze..."
// Just the actual response (with <think> tags removed)
msg.cleanedContent; // '{"answer": 42}'Embedding Responses
import { AIFactory } from "@lfnovo/esperanto";
const model = AIFactory.createEmbedding("openai", "text-embedding-3-small");
const texts = ["Hello, world!", "Another text"];
// embed() returns one vector per input text, in order
const embeddings = await model.embed(texts); // number[][]
console.log(embeddings[0].length); // Vector dimensionality
console.log(embeddings[0]); // Vector for the first textReranking Responses
import { AIFactory } from "@lfnovo/esperanto";
const reranker = AIFactory.createReranker("jina", "jina-reranker-v2-base-multilingual");
const query = "What is machine learning?";
const documents = [
"Machine learning is a subset of artificial intelligence.",
"The weather is nice today.",
"Python is a programming language used in ML.",
];
// All reranking responses follow this structure
const response = await reranker.rerank(query, documents, 2);
console.log(response.results[0].document); // Highest ranked document
console.log(response.results[0].relevanceScore); // Normalized 0-1 relevance score
console.log(response.results[0].index); // Original document index
console.log(response.model); // The model used
console.log(response.topResult); // Shortcut for the best matchTask-Aware Embeddings 🎯
Esperanto supports advanced task-aware embeddings that optimize vector representations for specific use cases. This works across all embedding providers through a universal interface:
import { AIFactory, EmbeddingTaskType } from "@lfnovo/esperanto";
// Task-optimized embeddings work with ANY provider
const model = AIFactory.createEmbedding("jina", "jina-embeddings-v3", {
taskType: EmbeddingTaskType.RETRIEVAL_QUERY, // Optimize for search queries
lateChunking: true, // Better long-context handling
outputDimensions: 512, // Control vector size
});
// Generate optimized embeddings
const query = "What is machine learning?";
const embeddings = await model.embed([query]);Universal Task Types:
RETRIEVAL_QUERY- Optimize for search queriesRETRIEVAL_DOCUMENT- Optimize for document storageSIMILARITY- General text similarityCLASSIFICATION- Text classification tasksCLUSTERING- Document clusteringCODE_RETRIEVAL- Code search optimizationQUESTION_ANSWERING- Optimize for Q&A tasksFACT_VERIFICATION- Optimize for fact checking
Provider Support:
- Jina: Native API support for all features
- Google: Native task type translation to Gemini API
- OpenAI: Task optimization via intelligent text prefixes
- Others: Graceful degradation with consistent interface
The standardized response objects ensure consistency across different providers, making it easy to:
- Switch between providers without changing your application code
- Handle responses in a uniform way
- Access common attributes like token usage and model information
Provider Configuration 🔧
OpenAI
import { OpenAILanguageModel } from "@lfnovo/esperanto";
const model = new OpenAILanguageModel("gpt-4o", {
apiKey: "your-api-key", // Or set OPENAI_API_KEY env var
temperature: 0.7, // Optional
maxTokens: 850, // Optional
topP: 0.9, // Optional
structured: { type: "json" }, // Optional, for JSON output
baseUrl: undefined, // Optional, for custom endpoint
organization: undefined, // Optional, for org-specific API
});OpenAI-Compatible Endpoints
Use any OpenAI-compatible endpoint (LM Studio, Ollama, vLLM, custom deployments) with the same interface:
import { AIFactory } from "@lfnovo/esperanto";
// Using factory config
const model = AIFactory.createLanguage("openai-compatible", "your-model-name", {
baseUrl: "http://localhost:1234/v1", // Your endpoint URL (required)
apiKey: "your-api-key", // Your API key (optional)
});
// Or set environment variables
// Generic (works for all provider types):
// OPENAI_COMPATIBLE_BASE_URL=http://localhost:1234/v1
// OPENAI_COMPATIBLE_API_KEY=your-api-key # Optional for endpoints that don't require auth
// Provider-specific (takes precedence over generic):
// OPENAI_COMPATIBLE_BASE_URL_LLM=http://localhost:1234/v1
// OPENAI_COMPATIBLE_API_KEY_LLM=your-api-key
const envModel = AIFactory.createLanguage("openai-compatible", "your-model-name");
// Works with any OpenAI-compatible endpoint
const messages = [{ role: "user", content: "Hello!" }];
const response = await model.chatComplete(messages);
console.log(response.content);
// Streaming support
for await (const chunk of model.chatCompleteStream(messages)) {
process.stdout.write(chunk.choices[0]?.delta.content ?? "");
}Common Use Cases:
- LM Studio: Local model serving with GUI
- Ollama:
ollama servewith OpenAI compatibility - vLLM: High-performance inference server
- Custom Deployments: Any server implementing OpenAI chat completions API
See the OpenAI-Compatible provider guide for the full environment variable reference and configuration precedence.
Custom OpenAI-Compatible Profiles
Built-in profiles (deepseek, xai, dashscope, minimax) let you use those providers by name. You can register your own:
import { AIFactory } from "@lfnovo/esperanto";
AIFactory.registerOpenAICompatibleProfile({
name: "my-provider",
baseUrl: "https://api.my-provider.com/v1",
apiKeyEnv: "MY_PROVIDER_API_KEY",
defaultModel: "my-model",
});
const model = AIFactory.createLanguage("my-provider", "my-model");Perplexity
Perplexity uses an OpenAI-compatible API but includes additional parameters for controlling search behavior:
import { PerplexityLanguageModel } from "@lfnovo/esperanto";
const model = new PerplexityLanguageModel("sonar-pro", {
apiKey: "your-api-key", // Or set PERPLEXITY_API_KEY env var
temperature: 0.7, // Optional
maxTokens: 850, // Optional
topP: 0.9, // Optional
structured: { type: "json" }, // Optional, for JSON output
// Perplexity-specific parameters
searchDomainFilter: ["example.com", "-excluded.com"], // Optional, limit search domains
returnImages: false, // Optional, include images in search results
returnRelatedQuestions: true, // Optional, return related questions
searchRecencyFilter: "week", // Optional ('day', 'week', 'month', 'year')
webSearchOptions: { search_context_size: "high" }, // Optional ('low', 'medium', 'high')
});Timeout Configuration ⏱️
Esperanto provides flexible timeout configuration across all provider types with intelligent defaults and multiple configuration methods. Timeouts are expressed in seconds, exactly like the Python library.
Default Timeouts
- LLM, Embedding, Reranking: 60 seconds (text processing operations)
- Speech-to-Text, Text-to-Speech: 300 seconds (audio processing operations)
Configuration Methods
1. Config Object (Highest Priority)
import { AIFactory } from "@lfnovo/esperanto";
// LLM with custom timeout
const model = AIFactory.createLanguage("openai", "gpt-4o", { timeout: 120 }); // 2 minutes
// Embedding with custom timeout
const embedder = AIFactory.createEmbedding("openai", "text-embedding-3-small", {
timeout: 90, // 1.5 minutes
});
// Speech-to-Text with longer timeout for large files
const transcriber = AIFactory.createSpeechToText("openai", "whisper-1", {
timeout: 600, // 10 minutes
});2. Environment Variables
Set global defaults for all instances of a provider type:
export ESPERANTO_LLM_TIMEOUT=90 # 90 seconds for all LLM providers
export ESPERANTO_EMBEDDING_TIMEOUT=120 # 2 minutes for all embedding providers
export ESPERANTO_RERANKER_TIMEOUT=75 # 75 seconds for all reranker providers
export ESPERANTO_STT_TIMEOUT=600 # 10 minutes for all STT providers
export ESPERANTO_TTS_TIMEOUT=400 # ~6.5 minutes for all TTS providersPriority Order
- Config parameter (highest priority)
- Environment variable
- Provider type default (lowest priority)
Values must be positive numbers up to 3600 seconds (1 hour); invalid values throw with a descriptive message. See Timeout Configuration for details and production patterns.
Streaming Responses 🌊
Stream responses token by token with chatCompleteStream:
const model = AIFactory.createLanguage("openai", "gpt-4o");
for await (const chunk of model.chatCompleteStream(messages)) {
process.stdout.write(chunk.choices[0]?.delta.content ?? "");
}Structured Output 📊
Request JSON-formatted responses (supported by OpenAI and some other providers):
const model = AIFactory.createLanguage("openai", "gpt-4o", {
structured: { type: "json" },
});
const messages = [{ role: "user", content: "List three European capitals as JSON" }];
const response = await model.chatComplete(messages);
// response.content will be JSONTool Calling 🔧
Let models call your functions with a unified interface across providers:
const tools = [
{
type: "function",
function: {
name: "get_weather",
description: "Get the current weather for a city",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
},
];
const response = await model.chatComplete(
[{ role: "user", content: "What's the weather in Paris?" }],
{ tools },
);
const call = response.choices[0].message.toolCalls?.[0];
if (call) {
const args = JSON.parse(call.function.arguments); // { city: "Paris" }
}See the Tool Calling guide for the full round-trip pattern and built-in argument validation.
Documentation 📚
Complete documentation is available in the docs directory:
- Quick Start Guide - Get up and running in 5 minutes
- Documentation Index - Navigation hub for all documentation
- Provider Comparison - Compare and choose providers
- Capability Guides - Learn about LLM, Embeddings, Reranking, STT, TTS
- Provider Setup Guides - Setup instructions for all providers
- Advanced Topics - Task-aware embeddings, timeouts, model discovery, and more
- Differences from Python - Migration notes for Python users
Contributing 🤝
We welcome contributions! Please open an issue or pull request on GitHub.
License 📄
This project is licensed under the MIT License - see the LICENSE file for details.
Development 🛠️
- Clone the repository:
git clone https://github.com/lfnovo/esperanto-js.git
cd esperanto-js- Install dependencies:
bun install- Run tests:
bun test