Esperanto
Unified interface library for working with multiple AI models (LLM, embedding, reranking, speech-to-text, text-to-speech) from different providers.
Core Value Proposition
Esperanto provides a consistent, provider-agnostic interface for AI models. Users can switch providers by changing one parameter, with identical code otherwise.
Key principle: Consistency across providers is the main value proposition. When adding features, maintain interface uniformity.
Project Structure
esperanto/
├── src/esperanto/
│ ├── __init__.py # Public API exports
│ ├── factory.py # AIFactory for creating provider instances
│ ├── model_discovery.py # Static model discovery system
│ ├── providers/ # Provider implementations
│ │ ├── llm/ # Language model providers
│ │ ├── embedding/ # Embedding providers
│ │ ├── reranker/ # Reranker providers
│ │ ├── stt/ # Speech-to-text providers
│ │ └── tts/ # Text-to-speech providers
│ ├── common_types/ # Shared type definitions
│ └── utils/ # Cross-cutting utilities
└── tests/ # Test suite
See detailed documentation in subdirectory CLAUDE.md files.
Module Documentation
- src/esperanto/providers/: All provider implementations
- llm/: Language models (OpenAI, Anthropic, Google, etc.)
- embedding/: Embedding models (OpenAI, Jina, Voyage, etc.)
- reranker/: Reranking models (Jina, Voyage, Transformers)
- stt/: Speech-to-text (OpenAI, Groq, Google, Azure)
- tts/: Text-to-speech (OpenAI, ElevenLabs, Google, Azure)
- src/esperanto/common_types/: Response types and models
- src/esperanto/utils/: Timeout, SSL, caching utilities
Key Files
factory.py
Central factory for creating provider instances:
AIFactory.create_language(): Create LLM providerAIFactory.create_embedding(): Create embedding providerAIFactory.create_reranker(): Create reranker providerAIFactory.create_speech_to_text(): Create STT providerAIFactory.create_text_to_speech(): Create TTS providerAIFactory.get_provider_models(): Static model discovery (no instance needed)
Registration: All providers registered in _provider_modules dict by type and name.
model_discovery.py
Static model discovery system:
PROVIDER_MODELS_REGISTRY: Maps provider names to discovery functions- Discovery functions return
List[Model]without creating provider instances - Results cached with
ModelCache(1 hour TTL)
init.py
Public API surface:
- Exports
AIFactory(primary interface) - Exports base classes (
LanguageModel,EmbeddingModel, etc.) - Conditionally imports provider classes (handles missing dependencies)
Architecture Patterns
Provider Pattern
All providers follow consistent architecture:
- Base class defines interface (abstract methods)
- Provider implementations inherit and implement interface
- Factory registration makes provider discoverable
- Common response types ensure consistency
Configuration Priority
Three-tier configuration system (highest to lowest):
- Config dict:
config={"timeout": 120} - Environment variables:
ESPERANTO_LLM_TIMEOUT=90 - Defaults: Provider type defaults
Mixin Composition
Providers inherit functionality via mixins:
TimeoutMixin: Configurable HTTP timeoutsSSLMixin: Configurable SSL verification- Base class (e.g.,
LanguageModel): Provider-specific interface - Provider implementation: Actual API integration
Adding a New Provider
When building a provider:
- Research existing implementations: Check base class and 2-3 sibling providers for patterns
- Follow the interface exactly: Consistency is critical for user experience
- Implement all abstract methods: Don't skip required methods
- Register in factory: Add to
factory._provider_modules["{type}"] - Add optional import: In
src/esperanto/__init__.pywith try/except - Write tests: Use
uv run pytest -vto verify
Critical pattern - __post_init__():
def __post_init__(self):
super().__post_init__() # ALWAYS call first
self.api_key = self.api_key or os.getenv("PROVIDER_API_KEY")
self.base_url = self.base_url or "https://api.provider.com/v1"
self._create_http_clients() # ALWAYS call last
Steps for New Provider
- Identify provider type (language, embedding, reranker, stt, tts)
- Create
{provider_name}.pyin appropriatesrc/esperanto/providers/{type}/directory - Import base class from
.base - Implement all abstract methods
- Follow
__post_init__()pattern above - Add to
factory._provider_modules["{type}"]["{provider}"] - Add optional import to
src/esperanto/__init__.py - Write tests in
tests/providers/{type}/test_{provider}.py - Run tests:
uv run pytest -v - Add docs in
docs/providers/{provider}.md
Integration Points
AIFactory ↔ Providers
Factory imports providers dynamically via _import_provider_class():
- Avoids loading all providers at import time
- Handles missing dependencies gracefully
- Raises helpful errors for missing packages
Providers ↔ Common Types
All providers convert API responses to Esperanto's common types:
- Language:
ChatCompletion/ChatCompletionChunk - Language (tools):
Tool,ToolFunction,ToolCall,FunctionCall - Embedding:
List[List[float]] - Reranker:
RerankResponse - STT:
TranscriptionResponse - TTS:
AudioResponse
Tool Calling
Esperanto provides unified tool/function calling across all LLM providers:
from esperanto import AIFactory
from esperanto.common_types import Tool, ToolFunction
# Define tools once - works with any provider
tools = [
Tool(
type="function",
function=ToolFunction(
name="get_weather",
description="Get weather for a city",
parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
)
)
]
# Use with any provider - identical code
model = AIFactory.create_language("openai", "gpt-4o") # or "anthropic", "google", etc.
response = model.chat_complete(messages, tools=tools)
# Tool calls in response
if response.choices[0].message.tool_calls:
for tc in response.choices[0].message.tool_calls:
print(f"{tc.function.name}: {tc.function.arguments}")
See docs/features/tool-calling.md for full documentation.
Providers ↔ Utils
All providers use utility mixins:
TimeoutMixin._get_timeout()for HTTP timeout configurationSSLMixin._get_ssl_verify()for SSL verification settingsModelCachefor caching model lists (via model_discovery)
Gotchas
Adding Providers
- Consistency is key: Look at existing providers before implementing
- Base class inspection: Always check the base class for the provider type
- Super call order:
super().__post_init__()must be called first - Client creation timing:
_create_http_clients()must be called last (needs api_key, base_url) - Factory registration: Provider won't work until added to
factory._provider_modules - Optional dependencies: Don't make Esperanto depend on all provider SDKs - handle ImportError
Interface Consistency
- Method signatures: Must match base class exactly (don't add required params)
- Return types: Must use common types (don't return provider-specific objects)
- Error handling: Raise
RuntimeErrorfor API errors,ValueErrorfor validation - Response normalization: Always convert provider responses to Esperanto types
Testing
- Test after writing:
uv run pytest -vto verify functionality - Check all providers: Changes to base classes affect all providers
- Integration tests: Test provider switching (same code, different provider)
API Keys
- Environment variables: Follow pattern
{PROVIDER}_API_KEY(all caps) - Validation: Always check for None in
__post_init__and raise helpful ValueError - Security: Never log API keys or include in error messages
Documentation
- User docs: Update
docs/providers/{provider}.mdfor human users - AI docs: Keep CLAUDE.md files updated for AI context (this file structure)
- Consistency: Documentation should reflect actual implementation
Development Workflow
- Before implementing: Read relevant base class + 2-3 provider examples
- During implementation: Follow patterns exactly, check tests frequently
- After implementation: Run full test suite, update docs
- Before committing: Ensure tests pass, check consistency with sibling providers
Common Commands
- Run all tests:
uv run pytest -v - Run specific test:
uv run pytest tests/providers/llm/test_openai.py -v - Run integration tests:
uv run pytest tests/integration/ -v - Check types:
uv run mypy src/esperanto - Format code:
uv run black src/ tests/
Critical Principles
- Consistency > Features: If a feature can't be consistent across providers, reconsider
- Interface First: Design interfaces before implementing providers
- Test Driven: Write tests as you implement, run frequently
- Documentation: Keep both human and AI docs in sync with code
- Provider Parity: New features should work across multiple providers when possible