Unified interface library for working with multiple AI models (LLM, embedding, reranking, speech-to-text, text-to-speech) from different providers.
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.
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 AGENTS.md files.
- src/esperanto/providers/: All provider implementations
- llm/: Language models
- embedding/: Embedding models
- reranker/: Reranking models
- stt/: Speech-to-text
- tts/: Text-to-speech
- src/esperanto/common_types/: Response types and models
- src/esperanto/utils/: Timeout, SSL, caching utilities
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.
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)
Public API surface:
- Exports
AIFactory(primary interface) - Exports base classes (
LanguageModel,EmbeddingModel, etc.) - Conditionally imports provider classes (handles missing dependencies)
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
Three-tier configuration system (highest to lowest):
- Config dict:
config={"timeout": 120} - Environment variables:
ESPERANTO_LLM_TIMEOUT=90 - Defaults: Provider type defaults
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
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- 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
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
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
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.
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)
- 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
- 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
- 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)
- 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
- User docs: Update
docs/providers/{provider}.mdfor human users - AI docs: Keep AGENTS.md files updated for AI context (this file structure)
- Consistency: Documentation should reflect actual implementation
- 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
- 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 - Validate package artifacts:
make package-check - Check types:
uv run mypy src/esperanto - Fix lint issues:
uv run ruff check . --fix
If you are an automated coding agent (harny, Claude Code in headless mode, etc.) running against this repo, read this section before deciding what to validate.
The single command to confirm a change is acceptable:
uv sync --all-extras && uv run pytest tests/providers tests/unit tests/common_types tests/test_deprecation_warnings.py -q --no-covThis runs ~895 tests (mocked, no real API calls) in roughly 70 seconds. Pass = exit 0. The same scope is gated in CI via .github/workflows/test.yml.
For a stricter local check that mirrors CI, also run:
uv run ruff check .
uv run mypy src/esperantoBoth are clean on main and gated on every PR by .github/workflows/lint.yml. If you introduce a new ruff or mypy error, fix it before opening the PR.
tests/integration/ requires real provider API keys and external network. Always exclude from automated runs unless the user has explicitly set up credentials and asked for it.
Real-API tests live in tests/integration/ and are gated by the release pytest marker. Run them with:
uv run pytest -m releaseThese tests cost real money (they make actual API calls to provider endpoints) and require provider keys set in a .env file at the repo root. CI does not run them — they are a local-only ritual intended to be executed by a maintainer before publishing a release. Never include them in automated agent validation runs.
- The
notebooks/directory is local-only (gitignored). If you see modifications there, leave them alone — they aren't part of the project. - Do not commit
.env,google-credentials.json, or any other credential file. The.gitignorecovers the common cases but always double-check before staging.
See ARCHITECTURE.md for the full design principles. The key rules:
- Provider Parity: New features MUST work across all (or most) providers. Partial implementations are not acceptable — they break the core promise of a provider-agnostic interface.
- Consistency > Features: If a feature can't be consistent across providers, reconsider. We'd rather ship later with full support than early with partial support.
- Interface First: Design interfaces before implementing providers.
- Provider Tiers: Not every provider needs its own class. OpenAI-compatible providers should use
OpenAICompatibleLanguageModelunless they have fundamentally different APIs. See ARCHITECTURE.md for tier definitions. - Test Driven: Write tests as you implement, run frequently. Every feature must be tested across all affected providers.
- Documentation: Keep both human and AI docs in sync with code.
Maintainer profile lives in .maintainer/; do not run release, triage or discussions workflows without it.