diff --git a/src/plyngent/config/models.py b/src/plyngent/config/models.py index 242135c..54e38b8 100644 --- a/src/plyngent/config/models.py +++ b/src/plyngent/config/models.py @@ -332,8 +332,8 @@ def _default_openai_models() -> dict[str, ModelConfig]: } -def _default_openai_provider_tools() -> list[dict[str, Any]]: - """Hosted tools when TOML omits ``provider_tools``.""" +def _default_provider_tools() -> list[dict[str, Any]]: + """Hosted tools when TOML omits ``provider_tools`` (OpenAI and DeepSeek).""" return [{"type": "web_search"}] @@ -352,7 +352,7 @@ class OpenAIProvider(ProviderConfig, tag="openai"): """ models: dict[str, ModelConfig] = field(default_factory=_default_openai_models) - provider_tools: list[dict[str, Any]] = field(default_factory=_default_openai_provider_tools) + provider_tools: list[dict[str, Any]] = field(default_factory=_default_provider_tools) class OpenAICompatibleProvider(ProviderConfig, tag="openai-compatible"): @@ -380,12 +380,18 @@ class DeepseekProvider(ProviderConfig, tag="deepseek"): - ``"anthropic"`` → Anthropic Messages API (``POST /messages`` on ``https://api.deepseek.com/anthropic``) + ``provider_tools`` asks for a search tool like OpenAI's does (``[]`` + disables it). DeepSeek runs a hosted search only on the Anthropic surface, so + the other conventions get a local ``web_search`` tool instead (see + ``config.routing.local_web_search``). + ``extras`` keeps arbitrary provider keys; the legacy ``extras.convention`` key is parsed but ignored (prefer the typed ``convention`` field). """ models: dict[str, ModelConfig] = field(default_factory=_default_deepseek_models) convention: DeepSeekConvention = "" + provider_tools: list[dict[str, Any]] = field(default_factory=_default_provider_tools) extras: dict[str, str] = field(default_factory=dict) diff --git a/src/plyngent/config/routing.py b/src/plyngent/config/routing.py index 6631771..d96a78b 100644 --- a/src/plyngent/config/routing.py +++ b/src/plyngent/config/routing.py @@ -147,8 +147,12 @@ def resolve_effective_provider(provider: Provider, *, model: str | None = None) else: url = default_url_for_preset(preset) + # Hosted tools belong to the preset that declares them: OpenAI's Responses + # surface, and DeepSeek's (whose Anthropic surface runs the search inside the + # response while its other surfaces get a local tool — see + # :func:`local_web_search`). provider_tools: list[dict[str, object]] = [] - if preset == "openai" and isinstance(provider, OpenAIProvider) and parent_preset == "openai": + if parent_preset == preset and isinstance(provider, OpenAIProvider | DeepseekProvider): provider_tools = [dict(item) for item in provider.provider_tools] return EffectiveProvider( diff --git a/tests/test_config/test_routing.py b/tests/test_config/test_routing.py index f48685f..e164f23 100644 --- a/tests/test_config/test_routing.py +++ b/tests/test_config/test_routing.py @@ -27,19 +27,18 @@ def _effective( return effective if tools is None else replace(effective, provider_tools=tools) -def test_a_deepseek_provider_asks_for_no_hosted_tool_by_default() -> None: - # OpenAI's preset defaults ``provider_tools`` to web_search; DeepSeek's does - # not, so search stays opt-in there (``provider_tools = [{type = "web_search"}]``). - assert wants_web_search(_effective()) is False - assert wants_web_search(_effective(tools=SEARCH_TOOLS)) is True +def test_a_deepseek_provider_asks_for_a_search_tool_like_openai_does() -> None: + # ``provider_tools`` defaults to web_search for both presets; ``[]`` disables. + assert wants_web_search(_effective()) is True + assert wants_web_search(_effective(tools=[])) is False def test_chat_and_responses_conventions_use_the_local_tool() -> None: # Only the Anthropic surface hosts a search; the others ignore a hosted tool, - # so an asked-for web_search becomes plyngent's own tool there. - assert local_web_search(_effective(convention="", tools=SEARCH_TOOLS)) is True - assert local_web_search(_effective(convention="responses", tools=SEARCH_TOOLS)) is True - assert local_web_search(_effective(convention="anthropic", tools=SEARCH_TOOLS)) is False + # so the default web_search becomes plyngent's own tool there. + assert local_web_search(_effective(convention="")) is True + assert local_web_search(_effective(convention="responses")) is True + assert local_web_search(_effective(convention="anthropic")) is False def test_without_provider_tools_there_is_no_search() -> None: