Files
plyngent/tests/test_lmproto/test_deepseek_anthropic_search.py
T
NCBM e2ee937d8f lmproto: search through DeepSeek's Anthropic-compatible endpoint
DeepSeek runs ``web_search`` inside a turn — the request declares the server
tool, the response carries a ``server_tool_use`` per query and the
``web_search_tool_result`` it produced — so a search is one ``/messages`` call
whose hits are read out of the content blocks. The turn stops as soon as it has
searched (a live probe with 64 output tokens spent all of them before the search
and returned nothing, 512 answers with the hits).

Hits carry a title, a URL and an ``encrypted_content`` only the index can read,
so the hit list is all a caller gets: the page bodies stay server-side.
2026-10-06 23:45:29 +08:00

116 lines
4.3 KiB
Python

"""DeepSeek server-side search backend (Anthropic-compatible endpoint)."""
from __future__ import annotations
import msgspec
import pytest
from plyngent.lmproto.anthropic.config import AnthropicConfig
from plyngent.lmproto.deepseek.anthropic.search import (
DeepseekSearchClient,
SearchHit,
hits_from_content,
)
def test_hits_from_content_dedupes_and_skips_noise() -> None:
content = [
{"type": "thinking", "thinking": "search"},
{
"type": "web_search_tool_result",
"tool_use_id": "call_1",
"content": [
{"type": "web_search_result", "title": "First", "url": "https://a.example/x"},
{"type": "web_search_result", "title": "First again", "url": "https://a.example/x"},
{"type": "web_search_result", "url": "https://b.example/y"},
{"type": "web_search_result", "title": "no url"},
],
},
{
"type": "web_search_tool_result",
"tool_use_id": "call_2",
"content": [{"type": "web_search_tool_result_error", "error_code": "max_uses_exceeded"}],
},
]
hits = hits_from_content(content)
assert hits == [
SearchHit(title="First", url="https://a.example/x"),
SearchHit(title="", url="https://b.example/y"),
]
assert hits_from_content("not a list") == []
class _Resp:
def __init__(self, status_code: int, body: bytes) -> None:
self.status_code = status_code
self.content = body
def _search_body() -> bytes:
return msgspec.json.encode(
{
"id": "msg_1",
"content": [
{
"type": "server_tool_use",
"id": "call_00_1",
"name": "web_search",
"input": {"query": "DeepSeek-V4"},
},
{
"type": "web_search_tool_result",
"tool_use_id": "call_00_1",
"content": [
{
"type": "web_search_result",
"title": "DeepSeek-V4 preview",
"url": "https://api-docs.deepseek.com/news/news260424/",
"encrypted_content": "opaque",
}
],
},
],
"usage": {"input_tokens": 9932, "output_tokens": 377},
}
)
@pytest.mark.asyncio
async def test_search_sends_the_hosted_tool_and_reads_its_hits(monkeypatch: pytest.MonkeyPatch) -> None:
client = DeepseekSearchClient(AnthropicConfig(api_key="sk-test", base_url="https://api.deepseek.com/anthropic"))
sent: dict[str, object] = {}
async def fake_post(path: str, **kwargs: object) -> _Resp:
sent["path"] = path
sent.update(kwargs)
return _Resp(200, _search_body())
monkeypatch.setattr(client.session, "post", fake_post)
hits = await client.search("DeepSeek-V4", model="deepseek-flash")
assert hits == [SearchHit(title="DeepSeek-V4 preview", url="https://api-docs.deepseek.com/news/news260424/")]
assert sent["path"] == "/messages"
assert sent["stream"] is False
data = sent["data"]
assert isinstance(data, bytes)
payload = msgspec.json.decode(data, type=dict[str, object])
assert payload["tools"] == [{"type": "web_search_20250305", "name": "web_search", "max_uses": 3}]
# A search turn stops as soon as it has searched; the answer is not needed.
assert payload["max_tokens"] == 512
assert payload["messages"] == [{"role": "user", "content": "DeepSeek-V4"}]
headers = sent["headers"]
assert isinstance(headers, dict)
assert headers["x-api-key"] == "sk-test"
@pytest.mark.asyncio
async def test_search_raises_with_the_endpoint_detail(monkeypatch: pytest.MonkeyPatch) -> None:
client = DeepseekSearchClient(AnthropicConfig(api_key="sk-test", base_url="https://api.deepseek.com/anthropic"))
async def fake_post(path: str, **kwargs: object) -> _Resp:
return _Resp(422, b'{"error": {"message": "tools[0]: unknown variant"}}')
monkeypatch.setattr(client.session, "post", fake_post)
with pytest.raises(RuntimeError, match="unknown variant"):
_ = await client.search("DeepSeek-V4", model="deepseek-flash")