mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47:54 +08:00
core/agent: run a hosted search as the surface's own tool
DeepSeek's Anthropic compat runs a hosted ``web_search`` itself and answers with
the blocks that search produced — ``thinking``, ``server_tool_use`` (the query)
and ``web_search_tool_result`` — so the content union that knew only text and
tool_use could not decode a live search turn. It carries them now, ``tools``
accepts the opaque server-tool dicts the surface declares, and
``web_search_tool_result.content`` stays loose on purpose: its
``web_search_result`` entries hold an ``encrypted_content`` the API demands back
unchanged (a trimmed result is answered with 422 "missing field
``encrypted_content``").
``provider_tools`` reached only the Responses surface, and the Anthropic one
wants its own definition of the same intent: a bare ``web_search`` becomes
``{type="web_search_20250305", name="web_search"}`` (the server tool Claude
declares and DeepSeek's compat reads, keeping a ``max_uses`` cap or any other
key), merged after the local function tools.
Such a tool is the server's: it streams ``input_json_delta`` fragments like a
call, and reading them as one asked the registry for a tool the server had
already run. Block types are now tracked, so only a ``tool_use`` block becomes a
tool call; the search blocks and ``thinking`` are skipped by the response
mapping (the answer the model wrote already used them) and never executed.
This commit is contained in:
@@ -13,6 +13,7 @@ from plyngent.lmproto.anthropic.model import (
|
||||
AnthropicResponseText,
|
||||
AnthropicUsage,
|
||||
AnthropicUserMessage,
|
||||
AnthropicWebSearchToolResultContent,
|
||||
)
|
||||
|
||||
|
||||
@@ -44,6 +45,75 @@ def test_messages_param_encode_omits_defaults() -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_decodes_a_server_side_search_response(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""DeepSeek's Anthropic compat answers a search turn with server tool blocks.
|
||||
|
||||
A live turn reads ``thinking, server_tool_use, web_search_tool_result, text``
|
||||
(the results carry an ``encrypted_content`` the API demands back unchanged);
|
||||
the response must decode, even though none of those blocks is a local call.
|
||||
"""
|
||||
body = {
|
||||
"id": "msg_search",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "deepseek-flash",
|
||||
"stop_reason": "end_turn",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "Let me search.", "signature": "b032a4ad-87fe-42ce-801d-e33dd743d8b9"},
|
||||
{
|
||||
"type": "server_tool_use",
|
||||
"id": "call_00_joE6PEEC1otU8pvSe4zY3017",
|
||||
"name": "web_search",
|
||||
"input": {"query": "DeepSeek latest news"},
|
||||
"caller": {"type": "direct"},
|
||||
},
|
||||
{
|
||||
"type": "web_search_tool_result",
|
||||
"tool_use_id": "call_00_joE6PEEC1otU8pvSe4zY3017",
|
||||
"content": [
|
||||
{
|
||||
"type": "web_search_result",
|
||||
"title": "Change Log | DeepSeek API Docs",
|
||||
"url": "https://api-docs.deepseek.com/updates/",
|
||||
"encrypted_content": "sd918tZXSvVp6j7ermvF4cl/lspbqHzths9SMQw6ziEw",
|
||||
}
|
||||
],
|
||||
},
|
||||
{"type": "text", "text": "DeepSeek shipped a new model."},
|
||||
],
|
||||
"usage": {"input_tokens": 16592, "output_tokens": 1419, "server_tool_use": {"web_search_requests": 4}},
|
||||
}
|
||||
client = AnthropicClient(AnthropicConfig(api_key="sk-test", base_url="https://example/v1"))
|
||||
|
||||
class _Resp:
|
||||
status_code = 200
|
||||
content = msgspec.json.encode(body)
|
||||
|
||||
async def fake_post(path: str, **kwargs: object) -> _Resp:
|
||||
return _Resp()
|
||||
|
||||
monkeypatch.setattr(client.session, "post", fake_post)
|
||||
result = await client.messages(AnthropicMessagesParam(model="deepseek-flash", messages=[]))
|
||||
kinds = [block.__struct_config__.tag for block in result.content]
|
||||
assert kinds == ["thinking", "server_tool_use", "web_search_tool_result", "text"]
|
||||
search = result.content[2]
|
||||
assert isinstance(search, AnthropicWebSearchToolResultContent)
|
||||
assert search.tool_use_id == "call_00_joE6PEEC1otU8pvSe4zY3017"
|
||||
assert search.content[0]["encrypted_content"].startswith("sd918tZXSvVp")
|
||||
assert result.content[3].text == "DeepSeek shipped a new model."
|
||||
|
||||
|
||||
def test_messages_param_encodes_server_tool_dicts() -> None:
|
||||
"""A hosted tool goes on the wire as the server-tool dict the surface wants."""
|
||||
param = AnthropicMessagesParam(
|
||||
model="deepseek-flash",
|
||||
messages=[AnthropicUserMessage(content="hi")],
|
||||
tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 3}],
|
||||
)
|
||||
data = msgspec.json.decode(msgspec.json.encode(param))
|
||||
assert data["tools"] == [{"type": "web_search_20250305", "name": "web_search", "max_uses": 3}]
|
||||
|
||||
|
||||
async def test_client_messages_create(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
client = AnthropicClient(AnthropicConfig(api_key="sk-test", base_url="https://example/v1"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user