Skip to content

Latest commit

 

History

History
947 lines (738 loc) · 53.9 KB

File metadata and controls

947 lines (738 loc) · 53.9 KB

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

[2.27.0] - 2026-09-05

Added

  • MiniMax M3 and text-to-speech support. MiniMax now defaults to MiniMax-M3 with its 1M-token context window and supports native TTS through /v1/t2a_v2, including sync/async generation and account voice discovery. (#265)

  • SiliconFlow provider. New built-in OpenAI-compatible profile (siliconflow) for SiliconFlow's hosted DeepSeek / Qwen / GLM / Kimi models, with static model discovery via AIFactory.get_provider_models("siliconflow"). Defaults to the global endpoint (api.siliconflow.com); set SILICONFLOW_BASE_URL for a mainland China (api.siliconflow.cn) account. (#235)

Documentation

  • MiniMax Anthropic-compatible endpoint. Documented how to route the native anthropic provider at MiniMax's Anthropic-protocol API via base_url, with the regional (international / mainland China) URLs. (#278)

Fixed

  • Provider credentials are redacted from object representations. API keys and configuration dictionaries no longer appear in dataclass repr output, preventing failed requests and test tracebacks from leaking credentials.

[2.26.0] - 2026-07-29

Added

  • Automatic embedding batching across all providers. embed() / aembed() now transparently split large inputs into requests that respect each provider's per-request limit and concatenate the results in input order, so a list that worked on one provider no longer breaks after a provider swap (applies the Hot-Swap-First Defaults principle to embeddings). Ceilings: OpenAI / Azure / OpenAI-compatible / Jina 2048, Voyage 1000, Cohere / OpenRouter 96, Mistral 64, Google 250, Vertex 25 (conservative — :predict also caps at 20k tokens), Ollama unbatched; transformers keeps its own internal batching. Lower it per model with config={"embed_batch_size": N} (clamped to the provider maximum; N <= 0 raises ValueError); embed([]) makes zero API calls. Google and Vertex now use the native :batchEmbedContents / multi-instance :predict endpoints instead of one HTTP call per text — a large speedup for big inputs. (#108, #203)

Fixed

  • Anthropic no longer defaults to a retired model. Anthropic has withdrawn the entire claude-3 family, and every Anthropic model hardcoded in Esperanto belonged to it — including the default, claude-3-7-sonnet-20250219, so create_language("anthropic") without an explicit model_name failed outright. The default is now claude-sonnet-5, and the provider's fallback list plus get_provider_models("anthropic") now carry the current family (claude-opus-5, claude-sonnet-5, claude-opus-4-5-20251101, claude-sonnet-4-5-20250929, claude-haiku-4-5-20251001). Docs updated.

    Note that claude-sonnet-5 is an undated alias: it tracks Anthropic's current Sonnet 5 release rather than staying fixed. Pass a dated id (e.g. claude-sonnet-4-5-20250929) when you need the model pinned.

  • Schema-driven structured output works again on OpenAI and Anthropic. structured={"type": "json_schema", "schema": <PydanticModel>} was rejected before the request ever ran: OpenAI's strict mode requires additionalProperties: false on every object, Anthropic's output_config requires the same, and Pydantic's model_json_schema() emits neither. The schema is now normalized — recursively, so nested models under $defs are covered too — on a copy, leaving a caller's own dict untouched. Verified live against OpenAI, Anthropic, Google, Groq, Mistral and Cohere.

    OpenAI's strict mode additionally requires every property to be listed in required, which a schema with optional fields cannot satisfy. Rather than fail the call or silently promote optional fields to required (which would change your schema's meaning), such a request is sent with strict: false and a debug log. The response is still validated against the schema locally, so nothing is weakened beyond the provider-side guarantee.

  • Google and Vertex no longer default to a retired model. Google has withdrawn gemini-2.0-flash — it still appears in the models listing but any generateContent call returns "This model is no longer available", so create_language("google") without an explicit model_name failed outright. Both providers now default to gemini-2.5-flash, and the hardcoded fallback model lists drop the withdrawn gemini-2.0-flash, gemini-1.5-pro, gemini-1.5-flash and gemini-pro in favor of gemini-2.5-flash / gemini-2.5-pro. Docs updated to stop recommending the retired model.

    Note when upgrading: gemini-2.5-flash is a thinking model, and its reasoning tokens are drawn from the same budget as the answer. If you pass a tight max_tokens (roughly under a few hundred) you may now see truncated output where gemini-2.0-flash fit comfortably — raise the budget or pass an explicit model_name.

  • to_langchain() now forwards a custom base URL for Google. Converting a Google (Gemini) model configured with a custom endpoint (GEMINI_API_BASE_URL) to LangChain previously reconnected to the official generativelanguage.googleapis.com. ChatGoogleGenerativeAI accepts a plain base_url, so the custom host is now forwarded (with the /v1beta suffix stripped). The default endpoint is still omitted so LangChain uses its own default. Completes the to_langchain() base URL audit started in #229. (#248)

  • Every non-Whisper OpenAI and Azure transcription model now works. Both providers requested response_format=verbose_json for any model that didn't match a narrow gpt-4o-*-transcribe name shape (Azure hardcoded it outright), so gpt-transcribe, gpt-4o-transcribe-diarize, gpt-live-transcribe and every Azure gpt-4o-*-transcribe deployment failed before transcription started with response_format 'verbose_json' is not compatible with model .... verbose_json is a Whisper-only capability, so the check is now an allowlist: Whisper models get verbose_json, anything unrecognized degrades to json (segments and duration stay None) instead of hard-failing — a new model no longer breaks on arrival. config={"response_format": ...} overrides the detection, which matters on Azure where the deployment name is user-chosen and may not reveal the underlying model. (#269, #263)

[2.25.1] - 2026-07-19

Fixed

  • Vertex AI now authenticates from a service-account key file across all modalities. VertexTextToSpeechModel and VertexEmbeddingModel previously ignored a configured key and shelled out to the gcloud CLI (unusable in containers where gcloud isn't installed). All three Vertex providers now share one auth path (VertexAuthMixin): explicit key file (credentials_file or credentials_path config, or GOOGLE_APPLICATION_CREDENTIALS) → Application Default Credentials → gcloud CLI as a last resort. A missing gcloud binary now raises a clear error instead of an opaque crash. (#249)
  • to_langchain() now forwards a custom base URL for Anthropic and Cohere. Previously, converting an Anthropic- or Cohere-compatible model (configured with a custom base_url) to LangChain silently reconnected to the official API. Anthropic strips the /v1 suffix that ChatAnthropic doesn't expect. The default endpoint is still omitted so LangChain uses its own default. (#229)

Changed

  • ElevenLabs default models bumped to the latest. Speech-to-text now defaults to scribe_v2 (was scribe_v1) and lists it in model discovery; text-to-speech now defaults to eleven_v3 (was eleven_multilingual_v2). Both older models remain usable by passing an explicit model_name. Verified against the live ElevenLabs API. (#245)

[2.25.0] - 2026-07-18

Added

  • oMLX built-in profile + requires_api_key flagoMLX, a local OpenAI-compatible MLX server for Apple Silicon, is now a built-in profile (omlx, capabilities={"language", "embedding"}, base URL http://localhost:11435/v1, override via OMLX_API_BASE). A new requires_api_key flag on OpenAICompatibleProfile (default True) lets local/no-auth endpoints tolerate a missing API key — it falls back to "not-required" instead of raising. oMLX sets no default models (bring your own), so pass a model_name. (#228)

  • Multi-modality OpenAI-compatible profiles — a registered OpenAICompatibleProfile can now serve embedding, speech_to_text, and text_to_speech in addition to language, via a new opt-in capabilities field (default: {"language"}) and per-modality default_models. The four factories (create_language/create_embedding/create_speech_to_text/ create_text_to_speech) resolve profiles through one shared precedence chain (ProfileAwareMixin). Requesting a modality a profile doesn't declare raises the new typed ProviderCapabilityError (unless a first-class class of the same name covers it — e.g. xAI is a language profile and a first-class TTS class, which keeps working). A profile that declares a modality but sets no default model raises a clear error rather than falling back to a placeholder the endpoint has never heard of. New base exception EsperantoError (ProviderCapabilityError extends it), seeding the normalized exception hierarchy. (#230)

  • Schema-driven structured outputs across all LLM providers — set config={"structured": {"type": "json_schema", "schema": MyPydanticModel}} (or a JSON Schema dict) and read the parsed, validated result from response.structured. Works consistently across OpenAI, Azure, OpenAI-compatible (incl. profile providers like DeepSeek/xAI/DashScope/MiniMax), Google (Gemini), Vertex AI, Anthropic (output_config.format), Groq, Mistral, Ollama, OpenRouter, Perplexity, and Cohere. The parsed object is the source of truth on each choice's message (choices[i].message.structured), surfaced at the top level via response.structured (mirrors content), so multi-choice (n>1) responses each carry their own parsed value. Schema mode is non-streaming in v1 (stream=True raises ValueError); providers/models that can't honor a json_schema request fail fast with a clear error rather than silently degrading. (#95)

  • PayPerQ (PPQ) provider — LLM, embedding, STT, TTS — PayPerQ is now selectable across all four modalities via AIFactory.create_language, create_embedding, create_speech_to_text, and create_text_to_speech with provider "ppq". PPQ is a pay-as-you-go gateway exposing hundreds of models from many labs through a single OpenAI-compatible endpoint. Added as a built-in multi-modality OpenAICompatibleProfile (base URL https://api.ppq.ai/v1, PPQ_API_KEY, optional PPQ_BASE_URL) with per-modality defaults (auto, openai/text-embedding-3-small, nova-3, deepgram_aura_2), so it inherits streaming, tool calling, and JSON mode from the OpenAI-compatible implementation. Ships with profile unit tests and provider docs. (#238)

  • OpenRouter text-to-speech and speech-to-text — OpenRouter is now selectable for TTS via AIFactory.create_text_to_speech("openrouter", ...) and for STT via AIFactory.create_speech_to_text("openrouter", ...), using OPENROUTER_API_KEY. Rounds out OpenRouter to LLM + TTS + STT. (#224)

  • Novita AI language provider — Novita is now selectable via AIFactory.create_language("novita", "moonshotai/kimi-k2.5") with NOVITA_API_KEY. Novita is a pay-as-you-go gateway to many open-source models through a single OpenAI-compatible endpoint, added as a built-in OpenAICompatibleProfile (base URL https://api.novita.ai/openai, optional NOVITA_BASE_URL). (#120)

Deprecated

  • OpenAICompatibleProfile(default_model=...) — use default_models={"language": ...} instead. The old field still works as a back-compat alias for the language default and now emits a DeprecationWarning. (#230)

[2.24.0] - 2026-06-23

Added

  • Deepgram speech-to-text provider — Deepgram is now selectable for STT via AIFactory.create_speech_to_text("deepgram", "nova-3"), complementing the existing Deepgram TTS provider. Posts to /v1/listen with utterances=true, authenticates with the shared DEEPGRAM_API_KEY, and normalizes the response to a TranscriptionResponse: text always, plus segments (built from results.utterances, with Deepgram-specific extras — confidence, channel, id, speaker — under each segment's metadata). When Deepgram returns no utterance timing, segments stays None rather than being fabricated. Ships with mocked-API unit tests. (#202)

Fixed

  • OpenAI STT gpt-4o-transcribe compatibility — the OpenAI STT provider no longer hardcodes response_format="verbose_json", which only whisper-1 accepts. It now sends verbose_json for the Whisper family (preserving segments and duration) and json for the gpt-4o-transcribe / gpt-4o-mini-transcribe family, which previously failed with a compatibility error. When those models omit segments/duration, the corresponding TranscriptionResponse fields stay None. (#204)
  • mypy --strict re-exports — the package-level __all__ is now a static literal instead of a runtime-built list, so downstream packages compiling with mypy --strict (which implies no_implicit_reexport) can from esperanto import AIFactory without "does not explicitly export attribute" errors. Runtime import behavior, including graceful degradation for missing optional dependencies, is unchanged. (#190)

[2.23.0] - 2026-06-16

Added

  • Cohere provider (LLM, Embedding, Reranker) — Cohere is now a first-class provider across three capabilities, integrated via its native v2 API (/v2/chat, /v2/embed, /v2/rerank) rather than an OpenAI-compatible profile, because its differentiators (documents-based RAG, citations, input_type embeddings, native tool format) are not exposed on the compatibility endpoint. Authentication is shared via COHERE_API_KEY.
    • LLM (AIFactory.create_language("cohere", "command-a-03-2025")): chat, streaming, and tool calling. Esperanto's top_p maps to Cohere's p. Cohere-specific RAG fields (documents, citation_options, connectors) pass through via config without changing the universal chat_complete surface; returned citations are not surfaced on ChatCompletion in this version (out of scope).
    • Embedding (AIFactory.create_embedding("cohere", "embed-v4.0")): input_type-aware (default search_document, configurable per-call), with automatic batching to Cohere's 96-texts-per-request limit.
    • Reranker (AIFactory.create_reranker("cohere", "rerank-v4.0-pro")): maps top_k to Cohere's top_n, returns a normalized RerankResponse.
    • Model discovery: AIFactory.get_provider_models("cohere") lists chat, embed, and rerank models live via /v1/models, tagged by Esperanto type. All three capabilities ship with mocked-API unit tests and documentation in docs/providers/cohere.md. (#110)

[2.22.0] - 2026-05-22

Added

  • STT segments + Whisper/Voxtral usage — Speech-to-text responses now carry timestamped segments and STT-specific usage on TranscriptionResponse. Two new common types are exported from esperanto.common_types:
    • TranscriptionSegment (text, start, end, optional metadata: Dict[str, Any]) — the per-item metadata escape hatch holds provider-specific extras such as avg_logprob, compression_ratio, no_speech_prob, confidence, and speaker, so the top-level interface stays uniform across providers.
    • TranscriptionUsage (input_seconds, input_tokens, output_tokens, total_tokens) — STT-aware usage with input_seconds for audio billing, distinct from the LLM Usage type. TranscriptionResponse gains a segments: Optional[List[TranscriptionSegment]] field (defaults to None). For Whisper-family providers (OpenAI, Groq, Azure), Esperanto now automatically requests response_format=verbose_json in the underlying HTTP call so callers receive segments and duration without any extra configuration ("Hot-Swap-First Defaults"). Mistral natively returns segments and a usage block with prompt_audio_seconds — both are now mapped onto the response. ElevenLabs and Google leave segments=None (they don't return them and Esperanto never synthesizes segments from text alone, per the "Unsupported Response Fields Stay None" principle). Resolves #146 (Whisper-family segments + usage) and #193 (Azure parity). (#146, #193)

Changed

  • TranscriptionResponse.usage is now Optional[TranscriptionUsage] (was Optional[Usage], the LLM token-usage type). No existing STT provider in Esperanto populated usage before this release, so callers reading from STT responses are not affected in practice. New STT-specific fields (input_seconds) are only available on the new type.

[2.21.0] - 2026-05-15

Added

  • Deepgram TTS provider — first-class DeepgramTextToSpeechModel for Deepgram's Aura model family. Supports the Aura-2 voice catalog across English, Spanish, French, German, Italian, Japanese, and Dutch, plus Aura-1 legacy English voices. Authenticates via DEEPGRAM_API_KEY (header form Authorization: Token …). Sync (generate_speech) and async (agenerate_speech) parity, encoding/container/sample_rate/bit_rate/speed query-param passthrough (with container driving the response content_type MIME), and a 2000-char input limit per request. Accessible via AIFactory.create_text_to_speech("deepgram", "aura-2-thalia-en"). (#50)
  • extra_body passthrough on OpenAICompatibleLanguageModel — instance-level via config={"extra_body": {...}} (applied to every request) and per-call via the new keyword-only extra_body= argument on chat_complete / achat_complete. Per-call extras shallow-merge over instance-level (per-call wins on key collision). Esperanto stays neutral: no validation, no interpretation — the merged dict is injected into the top-level outgoing JSON payload as-is, so the upstream server (vLLM, LocalAI, etc.) sees top_k, min_p, repetition_penalty, guided_json, and any other server-specific extras directly. A small set of reserved keys is silently stripped from extra_body because they have first-class arguments and overriding them via extra_body would desync the wire request from Python-side state: stream (controls the response-parsing branch via the stream= argument) and tools / tool_choice / parallel_tool_calls (used by the tool-call validator against the resolved tool set). Scoped to the openai-compatible class only; first-class providers keep their explicit param surfaces. New docs/providers/vllm.md covers the typical vLLM recipe. (#70)
  • Real-API release tests for STT, TTS, and rerankertests/integration/test_stt_real.py, tests/integration/test_tts_real.py, and tests/integration/test_reranker_real.py cover all providers per type. Tests are gated with @pytest.mark.release and excluded from the default uv run pytest run; invoke with uv run pytest -m release. (#169)
  • Per-call max_tokens, temperature, top_p overrideschat_complete() and achat_complete() now accept max_tokens, temperature, and top_p keyword arguments that override the instance-level values for a single request. Supported by all LLM providers. For Anthropic, top_p is silently dropped when temperature is also set, consistent with the mutual-exclusivity rule enforced by that provider's API.
  • Real-API embedding integration teststests/integration/test_embedding_real.py covers all embedding providers (OpenAI, Google, Vertex AI, Azure, Jina, Voyage, Mistral, Transformers, Ollama, OpenRouter, OpenAI-Compatible) with sync, async, and batch embed tests. Task-type translation tested for Google and Jina (native task param). Gated by @pytest.mark.release; excluded from default test runs.
  • Mistral TTS providerMistralTextToSpeechModel using the Voxtral (voxtral-mini-tts-2603) model. Supports pcm, wav, mp3, flac, and opus response formats. Reuses the existing MISTRAL_API_KEY environment variable. Voice discovery via available_voices calls GET /v1/audio/voices.
  • Mistral Speech-to-Text provider — new MistralSpeechToTextModel supporting voxtral-mini-latest (default) and voxtral-small-latest. Unlike OpenAI Whisper, Mistral returns the detected language in the response body, which is surfaced as TranscriptionResponse.language. Accessible via AIFactory.create_speech_to_text("mistral").
  • TranscriptionResponse.provider — STT responses now expose the originating provider name as an optional field, matching the existing AudioResponse.provider field. All STT providers (openai, elevenlabs, azure) already passed this kwarg, but Pydantic was silently dropping it. Existing callers continue to work unchanged. (#126)
  • Release-gated integration tests for chat completion — Added tests/integration/test_chat_completion_real.py with sync/async and streaming coverage across all LLM providers (OpenAI, Anthropic, Google, Vertex, Azure, Mistral, Ollama, Groq, OpenRouter, Perplexity, DeepSeek, xAI, DashScope, MiniMax).

Changed

  • ARCHITECTURE.md — new principle "Unsupported Response Fields Stay None". When a provider's native endpoint doesn't return the data required for a common-type field (timestamped segments, word-level timing, speaker diarization, etc.), leave the field as None and document the provider as "unsupported"; do not synthesize via prompt-engineering or heuristic aggregation. Pairs with Per-item Metadata Escape Hatch and Demand-Driven Abstraction Extension. Originates from issue #185. (#195)
  • Cross-provider config-dict propagation test suite. New tests/providers/test_config_dict_propagation.py and additions to tests/test_factory.py verify that config={"api_key": ..., "base_url": ...} reaches every provider's constructor across both direct instantiation and AIFactory.create_*(). 38 parametrized tests cover LLM, embedding, reranker, STT, and TTS providers (including provider-specific quirks like Google's hardcoded base_url and Azure's azure_endpoint). Closes #91. (#192)
  • Google embedding default model updated from text-embedding-004 to gemini-embedding-001text-embedding-004 was removed from the Google v1beta API. gemini-embedding-001 is the current recommended model (3072-dimensional output; override with model_name= if you need 768-d vectors from text-embedding-005). (#177)
  • Test-infrastructure cleanup — mocked integration tests removed from tests/integration/ (moved to per-provider test files under tests/providers/). A release pytest marker introduced: real-API tests are now tagged @pytest.mark.release, excluded from the default uv run pytest run, and invoked explicitly with uv run pytest -m release before each release. Unique to_langchain() coverage previously in tests/integration/ moved to the corresponding per-provider test files. (#166, #141)
  • Ollama num_ctx default lowered from 128,000 to 8,192. The previous 128K default caused out-of-memory errors on consumer GPUs with 8 GB VRAM. 8,192 tokens works reliably on common hardware while still being large enough for typical chat workloads. Override with config={"num_ctx": N} when you need a larger context window. (#107)
  • Lint and type-check the codebase clean. Ruff (ruff check .) and mypy (mypy src/esperanto) now report zero errors. Most fixes are type-only and do not change runtime behavior. Notable structural changes:
    • HttpConnectionMixin now declares client: httpx.Client and async_client: httpx.AsyncClient as non-Optional. The Optional[Client] = None dataclass fields previously redeclared on every provider base class have been removed; clients are still assigned by _create_http_clients() during __post_init__, so the runtime contract is unchanged.
    • Removed a duplicate _get_default_model definition in the Mistral provider (returned the same value as the original).
  • CI: lint/type-check job. Pull requests now run ruff check and mypy via a new .github/workflows/lint.yml workflow.
  • types-jsonschema added to dev dependencies so jsonschema is properly type-checked.
  • Ruff exclusions: notebooks/, .harny/, and examples/ are now excluded from lint runs (the first two are gitignored scratch directories; examples/ contains illustrative scripts).

Removed

  • CI: claude-code-review workflow removed entirely. The workflow file .github/workflows/claude-code-review.yml is deleted. cubic-dev-ai covers automated PR review; the claude.yml workflow remains for @claude mentions in comments. Closes #138.

Fixed

  • Google TTS default model switched to gemini-3.1-flash-tts-preview (was gemini-2.5-flash-preview-tts). The previous default now returns HTTP 500 INTERNAL from Google's API on every request, regardless of payload shape. The new default works on bare text but rejects the systemInstruction quirk from #178, so the payload field is now gated to the legacy gemini-2.5-* family via _needs_system_instruction(model_name). Pass model_name="gemini-2.5-flash-preview-tts" explicitly to keep the legacy quirk wired up. Applied across all 4 payload sites (sync/async × single-voice/multi-speaker). (#198)
  • AIFactory.get_provider_models() accepts a nested config={...} dict matching the provider-creation factory methods. Previously, passing config={"api_key": ..., "base_url": ...} was silently dropped, so OpenAI model discovery against custom base URLs (LiteLLM proxies, vLLM, local OpenAI-compatible servers) fell back to https://api.openai.com/v1/models. Direct keyword arguments still take precedence over nested config. (#103, #125)
  • Google TTS prompt format — Added a systemInstruction to the Gemini TTS request payload so raw text is accepted by the legacy gemini-2.5-* API. Previously, the API rejected bare contents text with "Model tried to generate text, but it should only be used for TTS". Superseded by #198 for the new default model. (#178)
  • _guess_audio_content_type returns audio/mpeg for .webm, .mp4, .mpeg files — an explicit extension allowlist now maps these video-container extensions to their correct audio MIME types (audio/webm, audio/mp4, audio/mpeg) instead of the generic audio/mpeg fallback. (#160)
  • OpenRouter providers send malformed request bodies — both the LLM and embedding OpenRouter providers were posting payloads via httpx's data=json.dumps(payload) instead of json=payload. In httpx, data= with a string is treated as a form-encoded body (Content-Type application/x-www-form-urlencoded), so requests carried JSON bytes with the wrong content type. The four affected call sites now use json=payload, which serializes the dict and sets Content-Type: application/json automatically. Tests updated to assert on the json kwarg so a regression would fail loudly. (#127)
  • TransformersEmbeddingModel quantization not applied_initialize_model was constructing a quantization_config dict with load_in_4bit / load_in_8bit and spreading it as top-level kwargs to AutoModel.from_pretrained, an API form that recent transformers versions deprecated in favor of a BitsAndBytesConfig object. The provider now constructs BitsAndBytesConfig(load_in_4bit=..., load_in_8bit=...) and passes it via quantization_config=. Added mocked unit tests (parametrized over 4bit/8bit) that fail if the config stops reaching from_pretrained. (#129)
  • tests/test_deprecation_warnings.py — 8 tests asserted on warnings without ever triggering them. Each test created a model instance, then entered a warnings.catch_warnings(record=True) block but never accessed the deprecated .models property inside the block, so the recorded list was always empty and assert len(w) == 1 always failed. The tests now bind the instance to a variable and call model.models inside the with block, so the deprecation warning is actually captured. The file is also added back into the gated test scope (.github/workflows/test.yml and the CLAUDE.md validator command), and the "known-broken tests" note in CLAUDE.md is removed since it no longer applies. (#130)
  • Streaming providers passed list[dict] to DeltaMessage.tool_calls (typed list[ToolCall]). Four streaming LLM providers (Anthropic, Google, Vertex, Ollama) constructed DeltaMessage(tool_calls=...) with raw dicts. Pydantic was coercing them to ToolCall via field validators at runtime, so tests passed, but the type contract was wrong — downstream consumers couldn't safely call delta.tool_calls[0].function.name. All four streaming normalizers now build proper ToolCall(id=..., type=..., function=FunctionCall(...), index=...) objects, mirroring the non-streaming path in each provider. The four # type: ignore[arg-type/list-item] stopgaps and their TODO comments are removed. Tests assert isinstance(tool_call, ToolCall) so a regression would fail loudly. (#128)
  • Embedding providers crash with opaque TypeError on null values from OpenAI-compatible endpoints. When an upstream embeddings endpoint returns null for a vector (or an element within a vector) — typical of llama.cpp llama-server when the input is too short or contains only special tokens — the response-parsing list comprehensions called float(None), which raised TypeError and was re-wrapped as a generic RuntimeError: Failed to generate embeddings: .... Affected providers: openai, openai_compatible, azure, ollama, mistral, voyage, jina (and by inheritance openrouter, deepseek, xai, etc.). Each now enumerates the response, detects null embeddings, empty vectors, or null elements, and raises a clear RuntimeError that names the input index and suggests filtering very short inputs. Jina previously silently skipped null embeddings (data loss); it now raises consistent with the rest. (#119)

[2.20.1] - 2026-04-13

Fixed

  • Ollama thinking models return empty content — Models like Qwen 3.5 that return a separate thinking field in the JSON response (instead of inline <think> tags) now have their thinking content correctly merged into the response. Use message.cleaned_content for the actual response and message.thinking for the reasoning trace. (#112)
  • Trailing slash in base URL causes double-slash and 301 redirect — All providers now strip trailing slashes from base_url during HTTP client initialization, preventing double-slash URLs (e.g., http://localhost:11434//api/chat) that caused silent failures with empty responses. (#113)

[2.20.0] - 2026-03-21

Added

  • OpenAI-compatible provider profiles — config-driven virtual providers that eliminate the need for separate Python classes for each OpenAI-compatible endpoint. Add a new provider with 6 lines of config instead of a new file. Users can also register custom profiles at runtime via AIFactory.register_openai_compatible_profile().
  • DashScope (Qwen) provider — Alibaba Cloud's Qwen models via OpenAI-compatible profile (qwen-turbo, qwen-plus, qwen-max).
  • MiniMax provider — MiniMax models via OpenAI-compatible profile (MiniMax-M2.5, MiniMax-M2.5-highspeed with 204K context).
  • OpenAICompatibleProfile exported from esperanto for custom provider registration.
  • ARCHITECTURE.md — design principles and contributor guide for the project.
  • CONTRIBUTING.md — rewritten with practical guidance, provider addition checklist, and link to ARCHITECTURE.md.

Changed

  • DeepSeek and xAI providers migrated to profiles — both now use OpenAICompatibleLanguageModel configured by profile data instead of dedicated provider classes. The factory API is unchanged: AIFactory.create_language("deepseek", ...) works exactly as before.
  • OpenAICompatibleLanguageModel._normalize_response now extracts tool calls — previously tool_calls in API responses were silently dropped. This was a pre-existing bug that surfaced during the migration.

Deprecated

  • DeepSeekLanguageModel direct instantiation — use AIFactory.create_language("deepseek", ...) instead. Direct import still works but emits DeprecationWarning.
  • XAILanguageModel direct instantiation — use AIFactory.create_language("xai", ...) instead. Direct import still works but emits DeprecationWarning.

[2.19.7] - 2026-03-11

Fixed

  • Azure embedding provider ignores api_key and base_url from config - Fixed AzureEmbeddingModel reading api_key and base_url from kwargs instead of self.*, causing config-provided values to be silently ignored.
  • Embedding providers have redundant kwargs.get() fallbacks - Cleaned up dead code in OpenAI, Voyage, Google, and Jina embedding providers that redundantly read api_key/base_url from kwargs after the base class already set them on self.*.

[2.19.6] - 2026-03-11

Fixed

  • OllamaEmbeddingModel ignores base_url from config - Fixed OllamaEmbeddingModel.__post_init__ reading base_url from kwargs instead of self.base_url, causing config-provided and factory-provided base URLs to be silently overwritten by the environment variable fallback or localhost default.

[2.19.5] - 2026-03-10

Fixed

  • OpenAI-compatible embeddings return base64 instead of float - Added encoding_format: "float" to OpenAI-compatible embedding payloads (both sync and async). Some providers (e.g., OpenRouter) return base64-encoded embeddings when encoding_format is not explicitly set, causing "could not convert string to float" errors at parse time.

Changed

  • Azure documentation: recommend latest API version - Updated all Azure provider documentation examples to recommend 2024-10-21 (latest stable) instead of the outdated 2024-02-01.

[2.19.4] - 2026-02-17

Fixed

  • TTS response_format and content_type support - OpenAI-compatible TTS providers now correctly pass response_format in the API request and map it to the appropriate MIME type in AudioResponse.content_type. Previously, response_format was never sent and content_type was always hardcoded to "audio/mp3", causing failures when providers return non-MP3 audio (e.g., WAV from mlx-audio/Kokoro).
    • Affected providers: OpenAI, OpenAI-compatible, Azure TTS
    • Default behavior unchanged: omitting response_format still defaults to "mp3"

[2.19.2] - 2026-02-14

Fixed

  • TTS Factory Config Support - Aligned create_text_to_speech() signature with other factory methods by adding config dict parameter. Previously, config={"api_key": "..."} was silently ignored, causing authentication failures for callers using the standard config pattern.

Deprecated

  • Direct api_key and base_url parameters on create_text_to_speech() — use config={"api_key": "...", "base_url": "..."} instead.
  • Removed api_key parameter from create_tts() deprecated alias.

[2.19.1] - 2026-02-12

Fixed

  • Config Dict API Key Resolution in Embedding Providers - Fixed embedding providers ignoring api_key passed via config dict
    • Affected providers: OpenAI, Google, Jina, Voyage (embedding)
    • Providers now correctly honor API keys resolved from config before env fallback
    • Added regression tests to ensure config={"api_key": "..."} works consistently

[2.19.0] - 2026-02-07

Added

  • Vertex AI Credentials Support - Added credentials_file parameter and full credentials priority chain
    • Supports explicit service account JSON file via credentials_file parameter
    • Supports GOOGLE_APPLICATION_CREDENTIALS environment variable
    • Supports Application Default Credentials (ADC) via google.auth.default()
    • Falls back to gcloud CLI when google-auth is not installed

Changed

  • Vertex AI LangChain Migration - Migrated from deprecated ChatVertexAI to ChatGoogleGenerativeAI

    • Uses langchain-google-genai (already a dependency) instead of langchain-google-vertexai
    • Removed langchain-google-vertexai from dev dependencies
    • LangChain model instances are now cached (matching Google provider pattern)
  • Proxy Configuration - Simplified proxy handling by delegating entirely to httpx

    • Esperanto now uses standard environment variables: HTTP_PROXY, HTTPS_PROXY, NO_PROXY
    • BREAKING: Removed ESPERANTO_PROXY environment variable support
    • BREAKING: Removed config={"proxy": "..."} parameter support
    • Migration: Replace ESPERANTO_PROXY with HTTP_PROXY and HTTPS_PROXY

[2.17.2] - 2026-01-24

Fixed

  • LangChain Connection Error from Garbage Collection - Fixed "Connection error" when using to_langchain() (#73)
    • When Esperanto model was garbage collected, shared httpx clients were closed, breaking LangChain
    • Now creates fresh httpx clients for LangChain with same configuration (timeout, SSL, proxy)
    • Affected providers: OpenAI, Groq, Perplexity, OpenAI-compatible, Azure
    • Fixes: lfnovo/open-notebook#460

[2.17.1] - 2026-01-24

Fixed

  • Config Dict API Key Not Unpacked - Fixed providers ignoring api_key passed via config dict (#68)
    • Affected providers: OpenRouter, DeepSeek, xAI (LLM), Groq (STT)
    • These providers inherit from OpenAI-compatible parent classes and were checking for api_key before the config dict was unpacked
    • Now correctly extracts api_key and base_url from config dict before setting provider defaults
    • Example that now works:
      model = AIFactory.create_language(
          "openrouter",
          "anthropic/claude-3.5-sonnet",
          config={"api_key": "sk-or-v1-xxxxx"}
      )

[2.17.0] - 2026-01-23

Added

  • Unified Tool Calling - Added tool/function calling support across all LLM providers (#67)

    • Define tools once using Tool and ToolFunction types, use with any provider
    • Consistent interface: chat_complete(messages, tools=tools)
    • Support for tool_choice parameter: "auto", "required", "none", or specific tool
    • Support for parallel_tool_calls parameter
    • Multi-turn conversations with tool results (role="tool" messages)
    • Tool call validation with validate_tool_calls=True parameter
    • New types: Tool, ToolFunction, ToolCall, FunctionCall, ToolCallValidationError
    • Validation utilities: validate_tool_call(), validate_tool_calls(), find_tool_by_name()
    • Tested providers: OpenAI, Anthropic, Google, Groq, Mistral, DeepSeek, xAI, OpenRouter, Azure, Ollama
    • Full documentation at docs/features/tool-calling.md
    • Examples at examples/tool_calling/
  • Real Integration Tests for Tool Calling - Added tests that call actual APIs (#71)

    • Validates tool calling works correctly across 10 providers
    • Tests both basic tool calls and multi-turn conversations
    • Perplexity skipped (doesn't support tool calling)

Fixed

  • Streaming Validation Warning - Added warning when validate_tool_calls=True is used with streaming (#71)
    • Tool call validation requires the complete response
    • Now emits UserWarning instead of silently ignoring the parameter
    • Affects all providers consistently

Changed

  • Moved mocked tool calling tests from tests/integration/ to tests/unit/

[2.16.0] - 2026-01-21

Added

  • Ollama Context Window Configuration - Added num_ctx support for Ollama provider

    • Default context window increased to 128,000 tokens (Ollama's default of 2,048 was causing context truncation)
    • Configurable via config={"num_ctx": 32768}
    • Passed to LangChain's ChatOllama via to_langchain()
  • Ollama Keep Alive Configuration - Added keep_alive support for Ollama provider

    • Controls how long models stay loaded in memory
    • No default set (doesn't force memory usage on users)
    • Examples: "5m" (5 minutes), "0" (unload immediately), "-1" (keep indefinitely)
    • Configurable via config={"keep_alive": "10m"}

[2.15.0] - 2026-01-16

Added

  • Message Thinking Properties - Added thinking and cleaned_content properties to Message class
    • thinking: Extracts content inside <think> tags (reasoning trace from models like Qwen3, DeepSeek R1)
    • cleaned_content: Returns content with <think> tags removed (actual response)
    • Multiple <think> blocks are concatenated
    • Returns None for thinking if no tags present

[2.14.1] - 2026-01-16

Fixed

  • LM Studio Compatibility - Fixed response_format parameter rejection by LM Studio (#46)
    • LM Studio only supports json_schema response format, not json_object
    • Added automatic detection for LM Studio (port 1234 heuristic)
    • Added graceful degradation: retries without response_format if endpoint rejects it
    • Affects both direct API calls and LangChain integration
    • See also: lmstudio-ai/lmstudio-bug-tracker#189

[2.14.0] - 2026-01-16

Added

  • Proxy Configuration - Added HTTP proxy support for all provider connections
    • Configuration via environment variable: ESPERANTO_PROXY
    • Configuration via config dict: config={"proxy": "http://proxy:8080"}
    • Supports HTTP, HTTPS, and authenticated proxies
    • Priority order: config dict > environment variable > none
    • Example:
      # Via environment variable
      export ESPERANTO_PROXY="http://proxy.example.com:8080"
      
      # Via config dict
      model = AIFactory.create_language(
          "openai", "gpt-4",
          config={"proxy": "http://proxy.example.com:8080"}
      )

Changed

  • Updated dependencies to latest versions
  • General codebase cleanup and maintenance

Fixed

  • HTTP Client Resource Management - Fixed async client not being properly closed when deleting model instances (#60)
    • Added HttpConnectionMixin consolidating timeout, SSL, and client lifecycle management
    • Providers now support context managers (with model: and async with model:)
    • Added explicit close() and aclose() methods for resource cleanup
    • Destructor now properly cleans up sync clients

[2.13.0] - 2026-01-04

Changed

  • Dependency Updates - Updated all dependencies to latest major versions (#55)

Fixed

  • Fixed CONTRIBUTING.md to use correct uv sync command for PEP735 dependency groups (#54)
  • Fixed unused openai import that could cause errors in tests (#53)

[2.12.1] - 2025-12-15

Fixed

  • Ollama LangChain JSON Format - Fixed OllamaLanguageModel.to_langchain() to correctly pass format="json" when structured={"type": "json"} is configured (#49)

[2.12.0] - 2025-12-14

Fixed

  • SSL Configuration for LangChain - Extended SSL verification configuration to all LangChain integrations (#47)
    • Anthropic, Groq, Mistral, and Ollama providers now pass SSL settings to LangChain

[2.11.0] - 2025-12-14

Added

  • SSL Verification Configuration - Added ability to disable SSL verification or use custom CA bundles (#45)
    • See [2.9.1] for full feature documentation (feature was backported)

[2.10.0] - 2025-11-27

Added

  • OpenRouter Embedding Provider - Access 60+ embedding providers through OpenRouter's unified API (#44)

    • Inherits from OpenAIEmbeddingModel following existing patterns
    • Supports OPENROUTER_API_KEY and OPENROUTER_BASE_URL environment variables
    • Model discovery filters embedding models from OpenRouter's /models endpoint
    • Example:
      model = AIFactory.create_embedding(
          "openrouter",
          "openai/text-embedding-3-small"
      )
      embeddings = model.embed(["Hello", "World"])
  • Google Speech-to-Text - Added STT support using Gemini API's audio transcription (#43)

    • Supports gemini-2.5-flash and gemini-2.0-flash models
    • Base64 audio encoding with automatic MIME type detection
    • Supports MP3, WAV, AIFF, AAC, OGG, FLAC formats
    • Language hints and custom prompts for improved accuracy
    • Example:
      model = AIFactory.create_speech_to_text("google", "gemini-2.5-flash")
      response = model.transcribe("audio.mp3", language="en")

Fixed

  • Anthropic Temperature/Top-p Conflict - Fixed issue where passing both temperature and top_p to Anthropic LangChain integration caused errors (#42, #39)
    • Now prioritizes temperature over top_p when both are set

[2.9.2] - 2025-11-11

Fixed

  • Ollama Embeddings API - Updated to use newer /api/embed endpoint for compatibility with recent Ollama versions (#37)

[2.9.1] - 2025-11-27

Added

  • SSL Verification Configuration - Added ability to disable SSL verification or use custom CA bundles for local providers with self-signed certificates (Ollama, LM Studio, etc.)
    • Configuration priority hierarchy: config dict > environment variables > default (True)
    • Config parameter verify_ssl (boolean) to disable SSL verification
    • Config parameter ssl_ca_bundle (string path) for custom CA certificates
    • Environment variables ESPERANTO_SSL_VERIFY and ESPERANTO_SSL_CA_BUNDLE
    • Security warning emitted when SSL verification is disabled
    • Type validation for verify_ssl accepts booleans, integers, and common string representations ("true", "false", "yes", "no", "0", "1")
    • Available across all provider types: LLM, Embedding, STT, TTS, Reranker
    • Example:
      # Disable SSL verification (development only)
      model = AIFactory.create_language(
          "ollama",
          "llama3",
          config={"verify_ssl": False}
      )
      
      # Use custom CA bundle (recommended for self-signed certs)
      model = AIFactory.create_language(
          "ollama",
          "llama3",
          config={"ssl_ca_bundle": "/path/to/ca-bundle.pem"}
      )

[2.8.0] - 2025-10-25

Added

  • Azure OpenAI Speech-to-Text Support - Added Whisper model support via Azure deployments

    • Direct HTTP implementation using httpx (no SDK dependencies)
    • Modality-specific environment variables: AZURE_OPENAI_API_KEY_STT, AZURE_OPENAI_ENDPOINT_STT, AZURE_OPENAI_API_VERSION_STT
    • Fallback to generic Azure environment variables
    • Full async support with transcribe() and atranscribe() methods
    • Example:
      model = AIFactory.create_speech_to_text("azure", "whisper-deployment")
      response = model.transcribe("audio.mp3")
  • Azure OpenAI Text-to-Speech Support - Added TTS model support via Azure deployments

    • Direct HTTP implementation using httpx (no SDK dependencies)
    • Modality-specific environment variables: AZURE_OPENAI_API_KEY_TTS, AZURE_OPENAI_ENDPOINT_TTS, AZURE_OPENAI_API_VERSION_TTS
    • Fallback to generic Azure environment variables
    • Supports all OpenAI voices: alloy, echo, fable, onyx, nova, shimmer
    • Full async support with generate_speech() and agenerate_speech() methods
    • Example:
      model = AIFactory.create_text_to_speech("azure", "tts-deployment")
      response = model.generate_speech("Hello!", voice="alloy")
  • Static Model Discovery - New AIFactory.get_provider_models() method for discovering available models without creating provider instances

    • Supports all 15 providers with intelligent caching (1-hour TTL)
    • Type filtering for multi-model providers (OpenAI)
    • Pass provider-specific configuration (API keys, base URLs, etc.)
    • Example:
      # Discover models without creating instances
      models = AIFactory.get_provider_models("openai", api_key="...")
      
      # Filter by type
      language_models = AIFactory.get_provider_models(
          "openai",
          api_key="...",
          model_type="language"
      )
  • Model Discovery Functions - 16 provider-specific discovery functions in model_discovery.py:

    • API-based: OpenAI, OpenAI-Compatible, Google/Gemini, Vertex AI, Mistral, Groq, xAI, OpenRouter, Ollama
    • Hardcoded lists: Anthropic, DeepSeek, Perplexity, Jina, Voyage
    • Special cases: Azure (deployments), Transformers (local models)
    • OpenAI-Compatible supports any endpoint implementing the OpenAI API specification (LM Studio, vLLM, custom endpoints)
  • ModelCache Utility - Thread-safe caching system for model discovery with configurable TTL

Deprecated

  • .models property on all provider instances (LanguageModel, EmbeddingModel, RerankerModel, SpeechToTextModel, TextToSpeechModel)
    • Will be removed in version 3.0.0
    • Emits DeprecationWarning with migration guidance
    • Use AIFactory.get_provider_models() instead

Migration Guide - Static Model Discovery

Why This Change?

The new static discovery approach provides several benefits:

  1. No Instance Creation Required - List models without creating provider instances
  2. Performance - Cached results (1 hour TTL) reduce unnecessary API calls
  3. Consistency - Unified interface across all providers
  4. Flexibility - Pass configuration parameters as needed

Quick Migration Examples

Before (Deprecated):

# ❌ Creating instance just to list models
model = AIFactory.create_language("openai", "gpt-4", config={"api_key": "..."})
available_models = model.models  # Deprecated

After (Recommended):

# ✅ Static discovery without creating instances
available_models = AIFactory.get_provider_models("openai", api_key="...")

Detailed Migration Examples

Basic Model Discovery:

# Before
model = AIFactory.create_language(
    "openai",
    "gpt-4",
    config={"api_key": "sk-..."}
)
models = model.models

# After
models = AIFactory.get_provider_models("openai", api_key="sk-...")

Listing Embedding Models:

# Before
embedder = AIFactory.create_embedding(
    "openai",
    "text-embedding-3-small",
    config={"api_key": "sk-..."}
)
embedding_models = embedder.models

# After
embedding_models = AIFactory.get_provider_models(
    "openai",
    api_key="sk-...",
    model_type="embedding"
)

Provider-Specific Classes:

# Before
from esperanto.providers.llm.anthropic import AnthropicLanguageModel

model = AnthropicLanguageModel(api_key="sk-ant-...")
claude_models = model.models

# After
from esperanto import AIFactory

claude_models = AIFactory.get_provider_models("anthropic")

Type Filtering (OpenAI):

# Before
model = OpenAILanguageModel(api_key="sk-...")
all_models = model.models
language_models = [m for m in all_models if m.id.startswith("gpt")]

# After
language_models = AIFactory.get_provider_models(
    "openai",
    api_key="sk-...",
    model_type="language"  # or 'embedding', 'speech_to_text', 'text_to_speech'
)

Timeline

  • Version 2.8.0 (Current) - .models property deprecated, warnings emitted
  • Version 3.0.0 (Future) - .models property will be removed

Suppressing Warnings (Temporary)

If you need time to migrate but want to suppress warnings temporarily:

import warnings

# Suppress only Esperanto deprecation warnings
warnings.filterwarnings('ignore', category=DeprecationWarning, module='esperanto')

Note: Suppressing warnings is not a long-term solution. Plan to migrate your code before version 3.0.0.

Provider-Specific Notes

OpenAI:

  • Supports model_type parameter for filtering
  • Requires API key (or OPENAI_API_KEY environment variable)
  • Results are cached for 1 hour

Anthropic:

  • Returns hardcoded list of Claude models
  • No API key required for discovery
  • Includes context window information

Google/Gemini:

  • Fetches models via API
  • Requires API key (or GOOGLE_API_KEY/GEMINI_API_KEY environment variable)
  • Results are cached for 1 hour

OpenAI-Compatible:

  • Fetches models from any OpenAI-compatible endpoint
  • Requires base_url parameter (e.g., http://localhost:1234/v1 for LM Studio)
  • Optional api_key if the endpoint requires authentication
  • Supports type filtering
  • Results are cached for 1 hour
  • Example:
    models = AIFactory.get_provider_models(
        "openai-compatible",
        base_url="http://localhost:1234/v1"
    )

Ollama:

  • Lists locally available models
  • Requires Ollama to be running locally
  • Default base URL: http://localhost:11434

Transformers:

  • Currently returns empty list (local models are not auto-discovered)
  • You need to specify model names explicitly when creating instances

Azure:

  • Returns empty list (Azure uses deployments, not discoverable models)
  • You need to specify your deployment names explicitly

API Reference

New Method:

AIFactory.get_provider_models(
    provider: str,                    # Provider name (e.g., "openai", "anthropic")
    model_type: Optional[str] = None, # Filter by type (OpenAI only)
    **config                          # Provider-specific configuration (api_key, base_url, etc.)
) -> List[Model]

Model Object:

@dataclass(frozen=True)
class Model:
    id: str                           # Model identifier (e.g., "gpt-4")
    owned_by: str                     # Owner organization (e.g., "openai")
    context_window: Optional[int]     # Max context size in tokens
    type: Optional[str] = None        # Model type (optional)

Migration Checklist

  • Replace all uses of .models with AIFactory.get_provider_models()
  • Update API key passing from instance creation to discovery method
  • Add model_type parameter where needed (OpenAI)
  • Test that model discovery works with your provider configuration
  • Remove any manual filtering code (use model_type instead)
  • Update documentation/comments in your codebase
  • Verify no deprecation warnings are emitted

Code Search Tips

To find all uses of the deprecated API in your codebase:

# Search for .models property access
grep -r "\.models" --include="*.py" .

# Search for specific providers
grep -r "LanguageModel.*\.models" --include="*.py" .
grep -r "EmbeddingModel.*\.models" --include="*.py" .

Internal Changes

  • Renamed internal models() method to _get_models() across all 34 provider implementations
  • Added comprehensive test coverage (37 new tests)
  • Improved code coverage to 68%

Planned for 3.0.0

  • Remove deprecated .models property from all provider base classes

For older versions, see Git history.