Reject unknown chat model providers and rename gemini to google
This commit is contained in:
parent
d0c66eef8f
commit
ad100ecd4d
5 changed files with 75 additions and 12 deletions
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Chat model provider `gemini` renamed to `google`, matching pydantic-ai. Update `provider: gemini` to `provider: google`.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `lancedb.databases` configures a named set of local or remote databases.
|
- `lancedb.databases` configures a named set of local or remote databases.
|
||||||
|
|
@ -33,6 +37,7 @@
|
||||||
- Inspector search results mark truncated previews with an ellipsis.
|
- Inspector search results mark truncated previews with an ellipsis.
|
||||||
- Document titles, URIs, headings and database names render as text, not Rich
|
- Document titles, URIs, headings and database names render as text, not Rich
|
||||||
markup, in `search` output, chat citations and the chat document filter.
|
markup, in `search` output, chat citations and the chat document filter.
|
||||||
|
- An unrecognized chat model provider raises `Unknown model provider '<name>'` 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
|
## [0.78.0] - 2026-08-24
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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
|
## Embedding Providers
|
||||||
|
|
||||||
|
|
@ -393,7 +393,7 @@ Any provider supported by Pydantic AI can be used. Examples:
|
||||||
# Google Gemini
|
# Google Gemini
|
||||||
qa:
|
qa:
|
||||||
model:
|
model:
|
||||||
provider: gemini
|
provider: google
|
||||||
name: gemini-1.5-flash
|
name: gemini-1.5-flash
|
||||||
|
|
||||||
# Groq
|
# Groq
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ class ModelConfig(ConfigModel):
|
||||||
`ModelSettings.extra_body`. Provider-side escape hatch for
|
`ModelSettings.extra_body`. Provider-side escape hatch for
|
||||||
keys haiku.rag doesn't model explicitly (e.g. vLLM's
|
keys haiku.rag doesn't model explicitly (e.g. vLLM's
|
||||||
`chat_template_kwargs.enable_thinking: false` for Qwen3).
|
`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"
|
provider: str = "ollama"
|
||||||
|
|
|
||||||
|
|
@ -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:
|
def cosine_similarity(vec1: list[float], vec2: list[float]) -> float:
|
||||||
"""Compute cosine similarity between two vectors."""
|
"""Compute cosine similarity between two vectors."""
|
||||||
dot_product = sum(a * b for a, b in zip(vec1, vec2))
|
dot_product = sum(a * b for a, b in zip(vec1, vec2))
|
||||||
|
|
@ -169,6 +195,7 @@ def get_model(
|
||||||
|
|
||||||
provider = model_config.provider
|
provider = model_config.provider
|
||||||
model = model_config.name
|
model = model_config.name
|
||||||
|
_check_provider_known(provider)
|
||||||
check_api_key_supported(model_config, {"openai", "ollama"})
|
check_api_key_supported(model_config, {"openai", "ollama"})
|
||||||
|
|
||||||
if provider == "ollama":
|
if provider == "ollama":
|
||||||
|
|
@ -261,7 +288,7 @@ def get_model(
|
||||||
|
|
||||||
return AnthropicModel(model_name=model, settings=anthropic_settings)
|
return AnthropicModel(model_name=model, settings=anthropic_settings)
|
||||||
|
|
||||||
elif provider == "gemini":
|
elif provider == "google":
|
||||||
from pydantic_ai.models.google import GoogleModel
|
from pydantic_ai.models.google import GoogleModel
|
||||||
|
|
||||||
return GoogleModel(
|
return GoogleModel(
|
||||||
|
|
|
||||||
|
|
@ -390,23 +390,23 @@ def test_get_model_anthropic_thinking_off_disables_adaptive_models():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GOOGLE, reason="Google not installed")
|
@pytest.mark.skipif(not HAS_GOOGLE, reason="Google not installed")
|
||||||
def test_get_model_gemini():
|
def test_get_model_google():
|
||||||
"""Test get_model returns GoogleModel for Gemini."""
|
"""Test get_model returns GoogleModel for Google."""
|
||||||
from pydantic_ai.models.google import GoogleModel
|
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)
|
result = get_model(model_config)
|
||||||
assert isinstance(result, GoogleModel)
|
assert isinstance(result, GoogleModel)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GOOGLE, reason="Google not installed")
|
@pytest.mark.skipif(not HAS_GOOGLE, reason="Google not installed")
|
||||||
@pytest.mark.parametrize("enable_thinking", [True, False])
|
@pytest.mark.parametrize("enable_thinking", [True, False])
|
||||||
def test_get_model_gemini_with_thinking(enable_thinking):
|
def test_get_model_google_with_thinking(enable_thinking):
|
||||||
"""Test get_model configures thinking for Gemini."""
|
"""Test get_model configures thinking for Google."""
|
||||||
from pydantic_ai.models.google import GoogleModel
|
from pydantic_ai.models.google import GoogleModel
|
||||||
|
|
||||||
model_config = ModelConfig(
|
model_config = ModelConfig(
|
||||||
provider="gemini",
|
provider="google",
|
||||||
name="gemini-2.0-flash-thinking-exp",
|
name="gemini-2.0-flash-thinking-exp",
|
||||||
enable_thinking=enable_thinking,
|
enable_thinking=enable_thinking,
|
||||||
)
|
)
|
||||||
|
|
@ -536,14 +536,45 @@ def test_get_model_bedrock_rejects_mantle_only_model():
|
||||||
get_model(model_config)
|
get_model(model_config)
|
||||||
|
|
||||||
|
|
||||||
def test_get_model_unknown_provider():
|
def test_get_model_passthrough_for_unbranched_provider():
|
||||||
"""Test get_model returns string format for unknown providers."""
|
"""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")
|
model_config = ModelConfig(provider="mistral", name="mistral-large-latest")
|
||||||
result = get_model(model_config)
|
result = get_model(model_config)
|
||||||
assert isinstance(result, str)
|
assert isinstance(result, str)
|
||||||
assert result == "mistral:mistral-large-latest"
|
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():
|
def test_get_package_versions():
|
||||||
"""Test get_package_versions returns expected keys."""
|
"""Test get_package_versions returns expected keys."""
|
||||||
from haiku.rag.utils import get_package_versions
|
from haiku.rag.utils import get_package_versions
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue