config: default DeepSeek to web_search and honour its provider_tools

``provider_tools`` reached the effective provider only for the OpenAI preset,
so a DeepSeek ``provider_tools`` in TOML was dropped on the way — and the preset
defaulted to none, leaving search unavailable for every convention.

DeepSeek now defaults to ``[{type="web_search"}]`` like OpenAI does (``[]``
turns it off) and its list reaches the agent: the Anthropic convention runs the
hosted search inside the response, the Responses and chat conventions get the
local ``web_search`` tool (see ``config.routing.local_web_search``).
This commit is contained in:
2026-10-06 23:59:01 +08:00
parent 27a901083a
commit e877df9d94
3 changed files with 22 additions and 13 deletions
+9 -3
View File
@@ -332,8 +332,8 @@ def _default_openai_models() -> dict[str, ModelConfig]:
} }
def _default_openai_provider_tools() -> list[dict[str, Any]]: def _default_provider_tools() -> list[dict[str, Any]]:
"""Hosted tools when TOML omits ``provider_tools``.""" """Hosted tools when TOML omits ``provider_tools`` (OpenAI and DeepSeek)."""
return [{"type": "web_search"}] return [{"type": "web_search"}]
@@ -352,7 +352,7 @@ class OpenAIProvider(ProviderConfig, tag="openai"):
""" """
models: dict[str, ModelConfig] = field(default_factory=_default_openai_models) 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"): class OpenAICompatibleProvider(ProviderConfig, tag="openai-compatible"):
@@ -380,12 +380,18 @@ class DeepseekProvider(ProviderConfig, tag="deepseek"):
- ``"anthropic"`` → Anthropic Messages API (``POST /messages`` on - ``"anthropic"`` → Anthropic Messages API (``POST /messages`` on
``https://api.deepseek.com/anthropic``) ``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`` ``extras`` keeps arbitrary provider keys; the legacy ``extras.convention``
key is parsed but ignored (prefer the typed ``convention`` field). key is parsed but ignored (prefer the typed ``convention`` field).
""" """
models: dict[str, ModelConfig] = field(default_factory=_default_deepseek_models) models: dict[str, ModelConfig] = field(default_factory=_default_deepseek_models)
convention: DeepSeekConvention = "" convention: DeepSeekConvention = ""
provider_tools: list[dict[str, Any]] = field(default_factory=_default_provider_tools)
extras: dict[str, str] = field(default_factory=dict) extras: dict[str, str] = field(default_factory=dict)
+5 -1
View File
@@ -147,8 +147,12 @@ def resolve_effective_provider(provider: Provider, *, model: str | None = None)
else: else:
url = default_url_for_preset(preset) 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]] = [] 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] provider_tools = [dict(item) for item in provider.provider_tools]
return EffectiveProvider( return EffectiveProvider(
+8 -9
View File
@@ -27,19 +27,18 @@ def _effective(
return effective if tools is None else replace(effective, provider_tools=tools) 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: def test_a_deepseek_provider_asks_for_a_search_tool_like_openai_does() -> None:
# OpenAI's preset defaults ``provider_tools`` to web_search; DeepSeek's does # ``provider_tools`` defaults to web_search for both presets; ``[]`` disables.
# not, so search stays opt-in there (``provider_tools = [{type = "web_search"}]``). assert wants_web_search(_effective()) is True
assert wants_web_search(_effective()) is False assert wants_web_search(_effective(tools=[])) is False
assert wants_web_search(_effective(tools=SEARCH_TOOLS)) is True
def test_chat_and_responses_conventions_use_the_local_tool() -> None: def test_chat_and_responses_conventions_use_the_local_tool() -> None:
# Only the Anthropic surface hosts a search; the others ignore a hosted tool, # 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. # so the default 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="")) is True
assert local_web_search(_effective(convention="responses", tools=SEARCH_TOOLS)) is True assert local_web_search(_effective(convention="responses")) is True
assert local_web_search(_effective(convention="anthropic", tools=SEARCH_TOOLS)) is False assert local_web_search(_effective(convention="anthropic")) is False
def test_without_provider_tools_there_is_no_search() -> None: def test_without_provider_tools_there_is_no_search() -> None: