mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
e2ee937d8f
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.
116 lines
4.3 KiB
Python
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")
|