honcho/tests/llm/test_backends/test_anthropic.py

136 lines
4.3 KiB
Python

from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
import pytest
from anthropic.types import TextBlock, ThinkingBlock, ToolUseBlock
from pydantic import BaseModel
from src.llm.backends.anthropic import AnthropicBackend
@pytest.mark.asyncio
async def test_anthropic_backend_extracts_text_thinking_and_tool_calls() -> None:
client = Mock()
client.messages.create = AsyncMock(
return_value=SimpleNamespace(
content=[
ThinkingBlock(
type="thinking",
thinking="internal reasoning",
signature="sig_123",
),
TextBlock(type="text", text="Hello from Anthropic"),
ToolUseBlock(
type="tool_use",
id="tool_1",
name="search",
input={"query": "honcho"},
),
],
usage=SimpleNamespace(
input_tokens=10,
output_tokens=5,
cache_creation_input_tokens=3,
cache_read_input_tokens=2,
),
stop_reason="tool_use",
)
)
backend = AnthropicBackend(client)
result = await backend.complete(
model="claude-haiku-4-5",
messages=[
{"role": "system", "content": "System prompt"},
{"role": "user", "content": "Hello"},
],
max_tokens=100,
tools=[
{
"name": "search",
"description": "Search for information",
"input_schema": {
"type": "object",
"properties": {"query": {"type": "string"}},
},
}
],
thinking_budget_tokens=2048,
tool_choice="required",
)
assert result.content == "Hello from Anthropic"
assert result.thinking_content == "internal reasoning"
assert result.thinking_blocks == [
{
"type": "thinking",
"thinking": "internal reasoning",
"signature": "sig_123",
}
]
assert result.tool_calls[0].name == "search"
assert result.input_tokens == 15
assert result.output_tokens == 5
assert result.finish_reason == "tool_use"
await_args = client.messages.create.await_args
if await_args is None:
raise AssertionError("Expected Anthropic client call")
call = await_args.kwargs
assert call["model"] == "claude-haiku-4-5"
assert call["system"][0]["text"] == "System prompt"
assert call["thinking"] == {"type": "enabled", "budget_tokens": 2048}
assert call["tool_choice"] == {"type": "any"}
class StructuredResponse(BaseModel):
answer: str
@pytest.mark.asyncio
async def test_anthropic_backend_skips_assistant_prefill_for_claude_4_models() -> None:
client = Mock()
client.messages.create = AsyncMock(
return_value=SimpleNamespace(
content=[TextBlock(type="text", text='{"answer":"ok"}')],
usage=SimpleNamespace(
input_tokens=10,
output_tokens=5,
cache_creation_input_tokens=0,
cache_read_input_tokens=0,
),
stop_reason="end_turn",
)
)
backend = AnthropicBackend(client)
result = await backend.complete(
model="claude-sonnet-4-5",
messages=[{"role": "user", "content": "Hello"}],
max_tokens=100,
response_format=StructuredResponse,
)
assert isinstance(result.content, StructuredResponse)
assert result.content.answer == "ok"
await_args = client.messages.create.await_args
if await_args is None:
raise AssertionError("Expected Anthropic client call")
call = await_args.kwargs
assert len(call["messages"]) == 1
assert call["messages"][0]["role"] == "user"
assert call["messages"][0]["content"].startswith("Hello\n\nRespond with valid JSON")
@pytest.mark.asyncio
async def test_anthropic_backend_rejects_thinking_effort() -> None:
backend = AnthropicBackend(Mock())
with pytest.raises(ValueError, match="does not support thinking_effort"):
await backend.complete(
model="claude-haiku-4-5",
messages=[{"role": "user", "content": "Hello"}],
max_tokens=100,
thinking_effort="high",
)