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

@lfnovo/esperanto

v0.1.0

Published

A light-weight, production-ready, unified interface for various AI model providers

Readme

Esperanto TS 🌐

npm version npm downloads Bun Node License: MIT

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 via for 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 transformers provider and to_langchain() are not part of the TS port.

📚 Documentation

By Capability

By Provider

Advanced Topics

Installation 🚀

bun add @lfnovo/esperanto
# or
npm install @lfnovo/esperanto

That'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 text

Model 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 format

Standardized 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 text

Reranking 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 match

Task-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 queries
  • RETRIEVAL_DOCUMENT - Optimize for document storage
  • SIMILARITY - General text similarity
  • CLASSIFICATION - Text classification tasks
  • CLUSTERING - Document clustering
  • CODE_RETRIEVAL - Code search optimization
  • QUESTION_ANSWERING - Optimize for Q&A tasks
  • FACT_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 serve with 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 providers

Priority Order

  1. Config parameter (highest priority)
  2. Environment variable
  3. 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 JSON

Tool 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:

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 🛠️

  1. Clone the repository:
git clone https://github.com/lfnovo/esperanto-js.git
cd esperanto-js
  1. Install dependencies:
bun install
  1. Run tests:
bun test