core/agent: keep a Messages round's provider blocks on the assistant message

DeepSeek's Anthropic compat answers a hosted search inside the turn: the response carries the `thinking` that picked the query, the `server_tool_use`, and the `web_search_tool_result`. The response mapping kept text and tool_use only, so the round lost its evidence — and the `thinking` signature, which only the provider can verify, could not go back at all. `AssistantChatMessage.provider_blocks` carries those blocks as raw JSON now (`thinking` text *and* signature, the query, the result entries with an `encrypted_content` the API rejects when trimmed) and the assistant turn hands them back first, ahead of its text and tool_use blocks; `thinking` also lands in `reasoning_content`, so the chain of thought streams as reasoning like every other surface's.

`AnthropicRawContentBlock` names the provider fields a streamed block carries, and `AnthropicServerToolUseContent` keeps the `caller` metadata the wire sends.
This commit is contained in:
2026-10-09 20:09:20 +08:00
parent 6c4c8b9328
commit 2fdbc3ce5d
6 changed files with 232 additions and 10 deletions
+49
View File
@@ -6,11 +6,14 @@ import pytest
from plyngent.lmproto.anthropic import AnthropicClient
from plyngent.lmproto.anthropic.config import AnthropicConfig
from plyngent.lmproto.anthropic.model import (
AnthropicAssistantMessage,
AnthropicContentBlockDelta,
AnthropicMessageResponse,
AnthropicMessagesParam,
AnthropicMessageStop,
AnthropicRawContentBlock,
AnthropicResponseText,
AnthropicTextContent,
AnthropicThinkingConfig,
AnthropicUsage,
AnthropicUserMessage,
@@ -126,6 +129,52 @@ def test_messages_param_keeps_the_thinking_type_on_the_wire() -> None:
assert data["thinking"] == {"type": "enabled", "budget_tokens": 16384}
def test_stream_block_keeps_the_provider_fields() -> None:
"""A streamed block carries what a rebuild puts back on the wire."""
result = msgspec.json.decode(
b'{"type":"web_search_tool_result","tool_use_id":"call_00_1",'
b'"content":[{"type":"web_search_result","encrypted_content":"sd9"}]}',
type=AnthropicRawContentBlock,
)
assert result.tool_use_id == "call_00_1"
assert result.content == [{"type": "web_search_result", "encrypted_content": "sd9"}]
thinking = msgspec.json.decode(
b'{"type":"thinking","thinking":"hmm","signature":"sig-1"}',
type=AnthropicRawContentBlock,
)
assert (thinking.thinking, thinking.signature) == ("hmm", "sig-1")
def test_assistant_message_encodes_provider_blocks_verbatim() -> None:
"""An echoed block keeps its own keys — ``encrypted_content`` included."""
param = AnthropicMessagesParam(
model="deepseek-flash",
messages=[
AnthropicAssistantMessage(
content=[
{"type": "thinking", "thinking": "hmm", "signature": "sig-1"},
{
"type": "web_search_tool_result",
"tool_use_id": "call_00_1",
"content": [{"type": "web_search_result", "encrypted_content": "sd9"}],
},
AnthropicTextContent(text="done"),
]
)
],
)
data = msgspec.json.decode(msgspec.json.encode(param))
assert data["messages"][0]["content"] == [
{"type": "thinking", "thinking": "hmm", "signature": "sig-1"},
{
"type": "web_search_tool_result",
"tool_use_id": "call_00_1",
"content": [{"type": "web_search_result", "encrypted_content": "sd9"}],
},
{"type": "text", "text": "done"},
]
async def test_client_messages_create(monkeypatch: pytest.MonkeyPatch) -> None:
client = AnthropicClient(AnthropicConfig(api_key="sk-test", base_url="https://example/v1"))