diff --git a/CHANGELOG.md b/CHANGELOG.md index 816cf1f8..2f5a0752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Chat model provider `gemini` renamed to `google`, matching pydantic-ai. Update `provider: gemini` to `provider: google`. + ### Added - `lancedb.databases` configures a named set of local or remote databases. @@ -33,6 +37,7 @@ - Inspector search results mark truncated previews with an ellipsis. - Document titles, URIs, headings and database names render as text, not Rich markup, in `search` output, chat citations and the chat document filter. +- An unrecognized chat model provider raises `Unknown model provider ''` instead of reaching pydantic-ai as a `provider:name` string, and outranks the `api_key` check, so an unusable provider is no longer reported as a missing vendor environment variable. ## [0.78.0] - 2026-08-24 diff --git a/docs/configuration/providers.md b/docs/configuration/providers.md index 90d76f26..5111dd33 100644 --- a/docs/configuration/providers.md +++ b/docs/configuration/providers.md @@ -127,7 +127,7 @@ qa: Same mechanism, opposite direction. Without `extra_body` the Gemma-4 chat template defaults to non-thinking and dumps a verbose answer straight into `content`. With it on, vLLM (started with `--reasoning-parser`) populates the parsed `reasoning` field and leaves `content` as the concise final answer. -**Provider support:** honored by openai, ollama, anthropic, and groq via pydantic-ai's `ModelSettings.extra_body`. Silently ignored by gemini and bedrock. +**Provider support:** honored by openai, ollama, anthropic, and groq via pydantic-ai's `ModelSettings.extra_body`. Silently ignored by google and bedrock. ## Embedding Providers @@ -393,7 +393,7 @@ Any provider supported by Pydantic AI can be used. Examples: # Google Gemini qa: model: - provider: gemini + provider: google name: gemini-1.5-flash # Groq diff --git a/haiku_rag_slim/haiku/rag/config/models.py b/haiku_rag_slim/haiku/rag/config/models.py index e019ecd5..a8a938c5 100644 --- a/haiku_rag_slim/haiku/rag/config/models.py +++ b/haiku_rag_slim/haiku/rag/config/models.py @@ -36,7 +36,7 @@ class ModelConfig(ConfigModel): `ModelSettings.extra_body`. Provider-side escape hatch for keys haiku.rag doesn't model explicitly (e.g. vLLM's `chat_template_kwargs.enable_thinking: false` for Qwen3). - Honored by openai/ollama/anthropic/groq; ignored by gemini/bedrock. + Honored by openai/ollama/anthropic/groq; ignored by google/bedrock. """ provider: str = "ollama" diff --git a/haiku_rag_slim/haiku/rag/utils.py b/haiku_rag_slim/haiku/rag/utils.py index 8e115f02..125da1fc 100644 --- a/haiku_rag_slim/haiku/rag/utils.py +++ b/haiku_rag_slim/haiku/rag/utils.py @@ -62,6 +62,32 @@ def check_api_key_supported( ) +def _check_provider_known(provider: str) -> None: + """Reject a chat provider pydantic-ai cannot resolve. + + Providers we do not branch on reach pydantic-ai as a `provider:name` string, + so without this an unusable name fails deep inside pydantic-ai with nothing + naming the config key it came from. Asking pydantic-ai's own resolver rather + than keeping a list here leaves a newly added provider working with no + release of ours. + """ + from pydantic_ai.providers import infer_provider_class + + try: + infer_provider_class(provider) + except ImportError: + # pydantic-ai knows the name, its SDK is just not installed here. That + # failure names the extra to install, so leave it to be raised in place. + return + except ValueError: + raise ValueError( + f"Unknown model provider '{provider}'. See " + "https://ai.pydantic.dev/models/ for the providers pydantic-ai " + "supports. An OpenAI-compatible server (vLLM, sglang, LM Studio) " + "uses provider 'openai' with base_url." + ) from None + + def cosine_similarity(vec1: list[float], vec2: list[float]) -> float: """Compute cosine similarity between two vectors.""" dot_product = sum(a * b for a, b in zip(vec1, vec2)) @@ -169,6 +195,7 @@ def get_model( provider = model_config.provider model = model_config.name + _check_provider_known(provider) check_api_key_supported(model_config, {"openai", "ollama"}) if provider == "ollama": @@ -261,7 +288,7 @@ def get_model( return AnthropicModel(model_name=model, settings=anthropic_settings) - elif provider == "gemini": + elif provider == "google": from pydantic_ai.models.google import GoogleModel return GoogleModel( diff --git a/tests/test_utils.py b/tests/test_utils.py index 0a48950f..92224dd3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -390,23 +390,23 @@ def test_get_model_anthropic_thinking_off_disables_adaptive_models(): @pytest.mark.skipif(not HAS_GOOGLE, reason="Google not installed") -def test_get_model_gemini(): - """Test get_model returns GoogleModel for Gemini.""" +def test_get_model_google(): + """Test get_model returns GoogleModel for Google.""" from pydantic_ai.models.google import GoogleModel - model_config = ModelConfig(provider="gemini", name="gemini-2.0-flash-exp") + model_config = ModelConfig(provider="google", name="gemini-2.0-flash-exp") result = get_model(model_config) assert isinstance(result, GoogleModel) @pytest.mark.skipif(not HAS_GOOGLE, reason="Google not installed") @pytest.mark.parametrize("enable_thinking", [True, False]) -def test_get_model_gemini_with_thinking(enable_thinking): - """Test get_model configures thinking for Gemini.""" +def test_get_model_google_with_thinking(enable_thinking): + """Test get_model configures thinking for Google.""" from pydantic_ai.models.google import GoogleModel model_config = ModelConfig( - provider="gemini", + provider="google", name="gemini-2.0-flash-thinking-exp", enable_thinking=enable_thinking, ) @@ -536,14 +536,45 @@ def test_get_model_bedrock_rejects_mantle_only_model(): get_model(model_config) -def test_get_model_unknown_provider(): - """Test get_model returns string format for unknown providers.""" +def test_get_model_passthrough_for_unbranched_provider(): + """A provider pydantic-ai knows but we do not branch on passes through as a + string, so a new pydantic-ai provider needs no haiku.rag release.""" model_config = ModelConfig(provider="mistral", name="mistral-large-latest") result = get_model(model_config) assert isinstance(result, str) assert result == "mistral:mistral-large-latest" +def test_get_model_accepts_provider_whose_sdk_is_missing(monkeypatch): + """A missing vendor SDK is not an unknown provider: that ImportError names + the extra to install, so it must reach the caller unchanged. + + Uses a provider whose SDK *is* installed, so the patch is what produces the + ImportError rather than the environment. + """ + import pydantic_ai.providers + + def _missing_sdk(provider: str): + raise ImportError("Please install the `cohere` package") + + monkeypatch.setattr(pydantic_ai.providers, "infer_provider_class", _missing_sdk) + result = get_model(ModelConfig(provider="cohere", name="command-r")) + + assert result == "cohere:command-r" + + +@pytest.mark.parametrize("provider", ["nonsense", "vllm", "gemini"]) +def test_get_model_rejects_unknown_provider(provider): + """An unknown provider is named here rather than passed through to fail + inside pydantic-ai, where nothing identifies the config it came from. + + `vllm` and `gemini` get no special case: both were haiku.rag's own + vocabulary, and both fail the same way as a typo. + """ + with pytest.raises(ValueError, match=provider): + get_model(ModelConfig(provider=provider, name="whatever")) + + def test_get_package_versions(): """Test get_package_versions returns expected keys.""" from haiku.rag.utils import get_package_versions