Embeddings stop breaking when you swap providers — and three first-class providers stop shipping a default model that no longer exists. Validated with the canonical suite (1633 tests) plus the real-API release tests across OpenAI, Anthropic, Google, Groq, Mistral, Cohere, Azure, Jina, Voyage, OpenRouter and ElevenLabs.
✨ New features
Automatic embedding batching across all providers (#108, #203)
embed() / aembed() now transparently split large inputs into requests that respect each provider's per-request limit, then concatenate the results in input order. A list of 500 texts that worked on OpenAI no longer fails after switching to Vertex — the same code just works, which is the whole point of the library.
Ceilings applied automatically:
| Provider | Ceiling |
|---|---|
| OpenAI · Azure · OpenAI-compatible · Jina | 2048 |
| Voyage | 1000 |
| Cohere · OpenRouter | 96 |
| Mistral | 64 |
| 250 | |
| Vertex | 25 (conservative — :predict also caps at 20k tokens) |
| Ollama | unbatched (single request) |
| transformers | keeps its own internal batching |
Lower it per model with config={"embed_batch_size": N} — values above the provider maximum are clamped, N <= 0 raises ValueError. embed([]) makes zero API calls.
⚡ Performance
Google and Vertex batch natively (#203). Both previously issued one HTTP call per text. They now use the native :batchEmbedContents (Google) and multi-instance :predict (Vertex) endpoints — a large speedup on big inputs.
🐛 Notable fixes
Three providers were defaulting to withdrawn models (#272, #274)
Google withdrew gemini-2.0-flash and Anthropic withdrew the entire claude-3 family. Those were the default models for google, vertex and anthropic, so this failed outright:
AIFactory.create_language("google") # This model is no longer available
AIFactory.create_language("anthropic") # model: claude-3-7-sonnet-20250219Model discovery was recommending them too. Defaults are now gemini-2.5-flash and claude-sonnet-5, the hardcoded lists carry the current families, and the docs stop pointing at ghosts. Tests now pin the defaults and assert no withdrawn family appears in any list — so the next withdrawal fails a test instead of reaching users.
Schema-driven structured output was broken on OpenAI and Anthropic (#273)
structured={"type": "json_schema", "schema": MyModel} was rejected before the request ran. OpenAI's strict mode requires additionalProperties: false on every object; Anthropic's output_config requires the same; Pydantic emits neither. Schemas are now normalized recursively (nested models under $defs included) on a copy, so your own dict is never mutated.
Where a schema has optional fields — which strict mode forbids outright, since it also demands every property be required — the request is sent with strict: false rather than silently promoting your optional fields to required. The response is still validated against the schema locally.
Every non-Whisper OpenAI and Azure transcription model now works (#269, #263)
Both providers requested response_format=verbose_json for anything that didn't match a narrow gpt-4o-*-transcribe name shape — Azure hardcoded it. But verbose_json is Whisper-only, so gpt-transcribe, gpt-4o-transcribe-diarize and every Azure gpt-4o-*-transcribe deployment failed before transcription started. In practice whisper-1 was the only OpenAI STT model that worked.
The check is now an allowlist: Whisper gets verbose_json, anything unrecognized degrades to json — segments and duration come back None instead of the call hard-failing, so a new model arrives working rather than broken. On Azure, where deployment names are user-chosen and may not reveal the model, config={"response_format": "verbose_json"} forces it.
to_langchain() forwards a custom base URL for Google (#248). Converting a Gemini model configured with GEMINI_API_BASE_URL previously reconnected to the official endpoint. Completes the audit started in #229.
⚠️ Breaking changes & deprecations
No API breaks. Three behavior changes worth knowing when you upgrade:
-
gemini-2.5-flashis a thinking model. Its reasoning tokens come from the same budget as the answer, so a tightmax_tokens(under a few hundred) can now truncate output wheregemini-2.0-flashfit comfortably. Raise the budget, or pass an explicitmodel_name. -
claude-sonnet-5is an undated alias. It tracks Anthropic's current Sonnet 5 release rather than staying fixed, so the default's behavior can shift without Esperanto publishing anything. Pass a dated id (e.g.claude-sonnet-4-5-20250929) when you need it pinned. -
Non-Whisper STT models return
segments=Noneandduration=Noneinstead of raising. If your code assumed segments were always present, that path was previously only reachable via Whisper — but it's worth a null check.
🔍 Not verified in this cycle
Stated plainly rather than implied: Vertex could not be exercised against the live API — no Google Cloud project was configured in the release environment, so Vertex LLM calls error on VERTEX_PROJECT before reaching Google. Its default moved alongside Google's by inference (the withdrawal is model-side, not endpoint-side), not by observed pass. Likewise the Azure non-Whisper deployment path, which needs a gpt-4o-*-transcribe deployment to point at. Both are covered by mocked tests.
🙏 Thanks
- @HariHaranDFX — reported #263, the Azure half of the
verbose_jsonbug, with a reproduction against a live Azure resource, the exact offending line, and the observation that Azure deployment names are user-chosen. That last point directly shaped theconfigescape hatch in the fix. Also flagged the related #262. - Thanks to everyone filing issues and reproductions. Several fixes here started as a downstream report rather than something we caught ourselves — #263 among them, and the whole withdrawn-model sweep came out of running the real-API suite that those reports prompted us to take more seriously.
Full details in the CHANGELOG.
Install: pip install esperanto==2.26.0 (extras: esperanto[openai,anthropic,...])