|
5 | 5 | import httpx |
6 | 6 | import pytest |
7 | 7 |
|
8 | | -from zhub.brains.base import ChatChunk |
9 | 8 | from zhub.brains.anthropic import AnthropicAdapter |
10 | 9 |
|
11 | 10 |
|
@@ -98,6 +97,55 @@ async def test_stream_parses_anthropic_sse(): |
98 | 97 | assert headers["anthropic-version"] |
99 | 98 |
|
100 | 99 |
|
| 100 | +@pytest.mark.asyncio |
| 101 | +async def test_stream_surfaces_tool_use_as_deltas(): |
| 102 | + """A tool_use turn opens with content_block_start (id + name), streams its |
| 103 | + arguments as input_json_delta fragments, and ends stop_reason=tool_use. |
| 104 | + The adapter must surface a tool_call_delta opener, the concatenable argument |
| 105 | + fragments under the same block index, and map the finish to 'tool_calls' so |
| 106 | + the hub's auto-resolution fires.""" |
| 107 | + lines = [ |
| 108 | + 'data: {"type":"message_start","message":{"id":"msg_x"}}', |
| 109 | + 'data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_42","name":"get_weather","input":{}}}', |
| 110 | + 'data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\\"city\\":"}}', |
| 111 | + 'data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\\"Paris\\"}"}}', |
| 112 | + 'data: {"type":"message_delta","delta":{"stop_reason":"tool_use"}}', |
| 113 | + 'data: {"type":"message_stop"}', |
| 114 | + ] |
| 115 | + fake = _FakeAsyncClient(lines) |
| 116 | + adapter = AnthropicAdapter(api_key="sk-ant-xyz", model="claude-sonnet-4-5", http=fake) |
| 117 | + out = [c async for c in adapter.stream( |
| 118 | + [{"role": "user", "content": "weather?"}], |
| 119 | + tools=[{"type": "function", "function": {"name": "get_weather"}}], |
| 120 | + )] |
| 121 | + |
| 122 | + # tools forwarded into the request body |
| 123 | + assert fake.last_call["json"]["tools"][0]["function"]["name"] == "get_weather" |
| 124 | + |
| 125 | + tcds = [c.tool_call_delta for c in out if c.tool_call_delta] |
| 126 | + # opener carries id + name under the block index |
| 127 | + assert tcds[0]["index"] == 1 |
| 128 | + assert tcds[0]["id"] == "toolu_42" |
| 129 | + assert tcds[0]["function"]["name"] == "get_weather" |
| 130 | + # argument fragments concatenate to valid JSON for the hub |
| 131 | + args = "".join(t["function"].get("arguments", "") for t in tcds) |
| 132 | + assert args == '{"city":"Paris"}' |
| 133 | + assert all(t["index"] == 1 for t in tcds) |
| 134 | + # finish mapped from anthropic's "tool_use" to the hub's "tool_calls" |
| 135 | + assert out[-1].done is True |
| 136 | + assert out[-1].finish_reason == "tool_calls" |
| 137 | + |
| 138 | + |
| 139 | +@pytest.mark.asyncio |
| 140 | +async def test_stream_omits_tools_when_none(): |
| 141 | + fake = _FakeAsyncClient([ |
| 142 | + 'data: {"type":"message_stop"}', |
| 143 | + ]) |
| 144 | + adapter = AnthropicAdapter(api_key="sk-ant-xyz", model="claude-sonnet-4-5", http=fake) |
| 145 | + [c async for c in adapter.stream([{"role": "user", "content": "hi"}])] |
| 146 | + assert "tools" not in fake.last_call["json"] |
| 147 | + |
| 148 | + |
101 | 149 | def test_anthropic_in_default_registry(): |
102 | 150 | from zhub.brains import REGISTRY |
103 | 151 | names = [c.name for c in REGISTRY] |
|
0 commit comments