Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai>
Co-authored-by: Clément Siriex <clement.sirieix@mistral.ai>
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-authored-by: Michel Thomazo <michel.thomazo@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-02-17 16:23:28 +01:00 committed by GitHub
parent 51fecc67d9
commit ec7f3b25ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
107 changed files with 8002 additions and 535 deletions

View file

@ -0,0 +1,586 @@
from __future__ import annotations
import json
import pytest
from vibe.core.config import ProviderConfig
from vibe.core.llm.backend.anthropic import AnthropicAdapter, AnthropicMapper
from vibe.core.types import (
AvailableFunction,
AvailableTool,
FunctionCall,
LLMMessage,
Role,
ToolCall,
)
@pytest.fixture
def mapper():
return AnthropicMapper()
@pytest.fixture
def adapter():
return AnthropicAdapter()
@pytest.fixture
def provider():
return ProviderConfig(
name="anthropic",
api_base="https://api.anthropic.com",
api_key_env_var="ANTHROPIC_API_KEY",
api_style="anthropic",
)
class TestMapperPrepareMessages:
def test_system_extracted(self, mapper):
messages = [
LLMMessage(role=Role.system, content="You are helpful."),
LLMMessage(role=Role.user, content="Hi"),
]
system, converted = mapper.prepare_messages(messages)
assert system == "You are helpful."
assert len(converted) == 1
assert converted[0]["role"] == "user"
def test_user_message(self, mapper):
messages = [LLMMessage(role=Role.user, content="Hello")]
_, converted = mapper.prepare_messages(messages)
assert converted[0]["content"] == [{"type": "text", "text": "Hello"}]
def test_assistant_text(self, mapper):
messages = [LLMMessage(role=Role.assistant, content="Sure")]
_, converted = mapper.prepare_messages(messages)
assert converted[0]["role"] == "assistant"
content = converted[0]["content"]
assert any(b.get("type") == "text" and b.get("text") == "Sure" for b in content)
def test_assistant_with_reasoning_content_and_signature(self, mapper):
messages = [
LLMMessage(
role=Role.assistant,
content="Answer",
reasoning_content="hmm",
reasoning_signature="sig",
)
]
_, converted = mapper.prepare_messages(messages)
content = converted[0]["content"]
assert content[0] == {"type": "thinking", "thinking": "hmm", "signature": "sig"}
assert content[1]["type"] == "text"
def test_assistant_with_reasoning_content(self, mapper):
messages = [
LLMMessage(
role=Role.assistant, content="Answer", reasoning_content="thinking..."
)
]
_, converted = mapper.prepare_messages(messages)
content = converted[0]["content"]
assert content[0] == {"type": "thinking", "thinking": "thinking..."}
def test_assistant_with_tool_calls(self, mapper):
messages = [
LLMMessage(
role=Role.assistant,
content="Let me search.",
tool_calls=[
ToolCall(
id="tc_1",
index=0,
function=FunctionCall(name="search", arguments='{"q": "test"}'),
)
],
)
]
_, converted = mapper.prepare_messages(messages)
content = converted[0]["content"]
tool_block = [b for b in content if b["type"] == "tool_use"][0]
assert tool_block["name"] == "search"
assert tool_block["input"] == {"q": "test"}
def test_tool_result_appended_to_user(self, mapper):
messages = [
LLMMessage(role=Role.user, content="Do it"),
LLMMessage(role=Role.tool, content="result", tool_call_id="tc_1"),
]
_, converted = mapper.prepare_messages(messages)
# tool_result is merged into the preceding user message
assert len(converted) == 1
assert converted[0]["role"] == "user"
blocks = converted[0]["content"]
assert any(b.get("type") == "tool_result" for b in blocks)
def test_tool_result_new_user_when_no_prior(self, mapper):
messages = [LLMMessage(role=Role.tool, content="result", tool_call_id="tc_1")]
_, converted = mapper.prepare_messages(messages)
assert converted[0]["role"] == "user"
assert converted[0]["content"][0]["type"] == "tool_result"
class TestMapperPrepareTools:
def test_none_returns_none(self, mapper):
assert mapper.prepare_tools(None) is None
def test_empty_returns_none(self, mapper):
assert mapper.prepare_tools([]) is None
def test_converts_tools(self, mapper):
tools = [
AvailableTool(
function=AvailableFunction(
name="search",
description="Search things",
parameters={"type": "object"},
)
)
]
result = mapper.prepare_tools(tools)
assert len(result) == 1
assert result[0]["name"] == "search"
assert result[0]["input_schema"] == {"type": "object"}
class TestMapperToolChoice:
def test_none(self, mapper):
assert mapper.prepare_tool_choice(None) is None
def test_auto(self, mapper):
assert mapper.prepare_tool_choice("auto") == {"type": "auto"}
def test_none_str(self, mapper):
assert mapper.prepare_tool_choice("none") == {"type": "none"}
def test_any(self, mapper):
assert mapper.prepare_tool_choice("any") == {"type": "any"}
def test_required(self, mapper):
assert mapper.prepare_tool_choice("required") == {"type": "any"}
def test_specific_tool(self, mapper):
tool = AvailableTool(
function=AvailableFunction(name="search", description="", parameters={})
)
assert mapper.prepare_tool_choice(tool) == {"type": "tool", "name": "search"}
class TestMapperParseResponse:
def test_text(self, mapper):
data = {
"content": [{"type": "text", "text": "Hello"}],
"usage": {"input_tokens": 10, "output_tokens": 5},
}
chunk = mapper.parse_response(data)
assert chunk.message.content == "Hello"
assert chunk.usage.prompt_tokens == 10
def test_thinking(self, mapper):
data = {
"content": [
{"type": "thinking", "thinking": "hmm", "signature": "sig"},
{"type": "text", "text": "Answer"},
],
"usage": {"input_tokens": 1, "output_tokens": 1},
}
chunk = mapper.parse_response(data)
assert chunk.message.content == "Answer"
assert chunk.message.reasoning_content == "hmm"
assert chunk.message.reasoning_signature == "sig"
def test_redacted_thinking(self, mapper):
data = {
"content": [
{"type": "redacted_thinking", "data": "xyz"},
{"type": "text", "text": "Answer"},
],
"usage": {"input_tokens": 1, "output_tokens": 1},
}
chunk = mapper.parse_response(data)
assert chunk.message.content == "Answer"
assert chunk.message.reasoning_content is None
def test_tool_use(self, mapper):
data = {
"content": [
{"type": "tool_use", "id": "t1", "name": "search", "input": {"q": "hi"}}
],
"usage": {"input_tokens": 1, "output_tokens": 1},
}
chunk = mapper.parse_response(data)
assert chunk.message.tool_calls[0].function.name == "search"
assert json.loads(chunk.message.tool_calls[0].function.arguments) == {"q": "hi"}
def test_cache_tokens(self, mapper):
data = {
"content": [{"type": "text", "text": "x"}],
"usage": {
"input_tokens": 10,
"cache_creation_input_tokens": 5,
"cache_read_input_tokens": 3,
"output_tokens": 7,
},
}
chunk = mapper.parse_response(data)
assert chunk.usage.prompt_tokens == 18
assert chunk.usage.completion_tokens == 7
class TestMapperStreamingEvents:
def test_text_delta(self, mapper):
chunk, idx = mapper.parse_streaming_event(
"content_block_delta",
{"delta": {"type": "text_delta", "text": "hi"}, "index": 0},
0,
)
assert chunk.message.content == "hi"
def test_thinking_delta(self, mapper):
chunk, _ = mapper.parse_streaming_event(
"content_block_delta",
{"delta": {"type": "thinking_delta", "thinking": "hmm"}, "index": 0},
0,
)
assert chunk.message.reasoning_content == "hmm"
def test_tool_use_start(self, mapper):
chunk, idx = mapper.parse_streaming_event(
"content_block_start",
{
"content_block": {"type": "tool_use", "id": "t1", "name": "search"},
"index": 2,
},
0,
)
assert chunk.message.tool_calls[0].id == "t1"
assert idx == 2
def test_input_json_delta(self, mapper):
chunk, _ = mapper.parse_streaming_event(
"content_block_delta",
{
"delta": {"type": "input_json_delta", "partial_json": '{"q":'},
"index": 1,
},
0,
)
assert chunk.message.tool_calls[0].function.arguments == '{"q":'
def test_message_start_usage(self, mapper):
chunk, _ = mapper.parse_streaming_event(
"message_start",
{"message": {"usage": {"input_tokens": 50, "cache_read_input_tokens": 10}}},
0,
)
assert chunk.usage.prompt_tokens == 60
def test_message_delta_usage(self, mapper):
chunk, _ = mapper.parse_streaming_event(
"message_delta", {"usage": {"output_tokens": 42}}, 0
)
assert chunk.usage.completion_tokens == 42
def test_unknown_event(self, mapper):
chunk, idx = mapper.parse_streaming_event("ping", {}, 5)
assert chunk is None
assert idx == 5
def test_signature_delta(self, mapper):
chunk, _ = mapper.parse_streaming_event(
"content_block_delta",
{"delta": {"type": "signature_delta", "signature": "sig"}, "index": 0},
0,
)
assert chunk is not None
assert chunk.message.reasoning_signature == "sig"
class TestAdapterPrepareRequest:
def test_basic(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert payload["model"] == "claude-sonnet-4-20250514"
assert payload["max_tokens"] == 1024
assert payload["temperature"] == 0.5
assert req.endpoint == "/v1/messages"
assert req.headers["anthropic-version"] == "2023-06-01"
def test_beta_features(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
assert "prompt-caching-2024-07-31" in req.headers["anthropic-beta"]
assert "interleaved-thinking-2025-05-14" in req.headers["anthropic-beta"]
assert "fine-grained-tool-streaming-2025-05-14" in req.headers["anthropic-beta"]
def test_api_key_header(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
api_key="sk-test-key",
)
assert req.headers["x-api-key"] == "sk-test-key"
def test_streaming(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=True,
provider=provider,
)
assert json.loads(req.body)["stream"] is True
def test_default_max_tokens(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=None,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
assert json.loads(req.body)["max_tokens"] == AnthropicAdapter.DEFAULT_MAX_TOKENS
def test_with_thinking(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
thinking="medium",
)
payload = json.loads(req.body)
assert payload["thinking"] == {"type": "enabled", "budget_tokens": 10000}
assert payload["max_tokens"] == 1024
assert payload["temperature"] == 1
def test_system_cached(self, adapter, provider):
messages = [
LLMMessage(role=Role.system, content="Be helpful."),
LLMMessage(role=Role.user, content="Hello"),
]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert payload["system"][0]["cache_control"] == {"type": "ephemeral"}
def test_with_tools(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
tools = [
AvailableTool(
function=AvailableFunction(
name="test_tool",
description="A test tool",
parameters={"type": "object", "properties": {}},
)
)
]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=tools,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert len(payload["tools"]) == 1
assert payload["tools"][0]["name"] == "test_tool"
@pytest.mark.parametrize(
"level,expected_budget", [("low", 1024), ("medium", 10_000), ("high", 32_000)]
)
def test_thinking_levels_budget_model(
self, adapter, provider, level, expected_budget
):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=None,
tool_choice=None,
enable_streaming=False,
provider=provider,
thinking=level,
)
payload = json.loads(req.body)
assert payload["thinking"] == {
"type": "enabled",
"budget_tokens": expected_budget,
}
assert payload["temperature"] == 1
assert payload["max_tokens"] == expected_budget + 8192
@pytest.mark.parametrize("level", ["low", "medium", "high"])
def test_thinking_levels_adaptive_model(self, adapter, provider, level):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-opus-4-6-20260101",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=None,
tool_choice=None,
enable_streaming=False,
provider=provider,
thinking=level,
)
payload = json.loads(req.body)
assert payload["thinking"] == {"type": "adaptive"}
assert payload["output_config"] == {"effort": level}
assert payload["temperature"] == 1
assert payload["max_tokens"] == 32_768
def test_history_forced_thinking_budget_model(self, adapter, provider):
messages = [
LLMMessage(role=Role.user, content="Hello"),
LLMMessage(
role=Role.assistant,
content="Answer",
reasoning_content="thinking...",
reasoning_signature="sig",
),
LLMMessage(role=Role.user, content="Follow up"),
]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=None,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert payload["thinking"] == {"type": "enabled", "budget_tokens": 10_000}
assert payload["temperature"] == 1
assert payload["max_tokens"] == 18_192
def test_history_forced_thinking_adaptive_model(self, adapter, provider):
messages = [
LLMMessage(role=Role.user, content="Hello"),
LLMMessage(
role=Role.assistant,
content="Answer",
reasoning_content="thinking...",
reasoning_signature="sig",
),
LLMMessage(role=Role.user, content="Follow up"),
]
req = adapter.prepare_request(
model_name="claude-opus-4-6-20260101",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=None,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert payload["thinking"] == {"type": "adaptive"}
assert payload["output_config"] == {"effort": "medium"}
assert payload["max_tokens"] == 32_768
class TestAdapterParseResponse:
def test_non_streaming(self, adapter, provider):
data = {
"content": [{"type": "text", "text": "Hello!"}],
"usage": {"input_tokens": 10, "output_tokens": 5},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Hello!"
assert chunk.usage.prompt_tokens == 10
def test_streaming_text_delta(self, adapter, provider):
data = {
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": "Hi"},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Hi"
def test_streaming_message_start(self, adapter, provider):
data = {"type": "message_start", "message": {"usage": {"input_tokens": 100}}}
chunk = adapter.parse_response(data, provider)
assert chunk.usage.prompt_tokens == 100
def test_streaming_unknown_returns_empty(self, adapter, provider):
data = {"type": "ping"}
chunk = adapter.parse_response(data, provider)
assert chunk.message.role == Role.assistant
assert chunk.message.content is None
def test_cache_control_last_user_message(self, adapter):
messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}]
adapter._add_cache_control_to_last_user_message(messages)
assert messages[0]["content"][0]["cache_control"] == {"type": "ephemeral"}
def test_cache_control_skips_non_user(self, adapter):
messages = [
{"role": "assistant", "content": [{"type": "text", "text": "Hello"}]}
]
adapter._add_cache_control_to_last_user_message(messages)
assert "cache_control" not in messages[0]["content"][0]
def test_cache_control_empty(self, adapter):
messages: list[dict] = []
adapter._add_cache_control_to_last_user_message(messages)
assert messages == []

View file

@ -0,0 +1,591 @@
from __future__ import annotations
import json
from unittest.mock import patch
import pytest
from vibe.core.config import ProviderConfig
from vibe.core.llm.backend.vertex import (
VertexAnthropicAdapter,
build_vertex_base_url,
build_vertex_endpoint,
)
from vibe.core.types import AvailableFunction, AvailableTool, LLMMessage, Role
@pytest.fixture
def adapter():
return VertexAnthropicAdapter()
@pytest.fixture
def provider():
return ProviderConfig(
name="vertex",
api_base="",
project_id="test-project",
region="us-central1",
api_style="vertex-anthropic",
)
class TestBuildVertexEndpoint:
def test_non_streaming(self):
endpoint = build_vertex_endpoint(
"us-central1", "my-project", "claude-3-5-sonnet"
)
assert endpoint == (
"/v1/projects/my-project/locations/us-central1/"
"publishers/anthropic/models/claude-3-5-sonnet:rawPredict"
)
def test_streaming(self):
endpoint = build_vertex_endpoint(
"us-central1", "my-project", "claude-3-5-sonnet", streaming=True
)
assert endpoint == (
"/v1/projects/my-project/locations/us-central1/"
"publishers/anthropic/models/claude-3-5-sonnet:streamRawPredict"
)
def test_base_url(self):
base = build_vertex_base_url("us-central1")
assert base == "https://us-central1-aiplatform.googleapis.com"
def test_global_endpoint(self):
endpoint = build_vertex_endpoint("global", "my-project", "claude-3-5-sonnet")
assert endpoint == (
"/v1/projects/my-project/locations/global/"
"publishers/anthropic/models/claude-3-5-sonnet:rawPredict"
)
def test_global_base_url(self):
base = build_vertex_base_url("global")
assert base == "https://aiplatform.googleapis.com"
class TestPrepareRequest:
@patch(
"vibe.core.llm.backend.vertex.get_vertex_access_token",
return_value="fake-token",
)
def test_basic_request(self, mock_token, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert payload["anthropic_version"] == "vertex-2023-10-16"
assert "model" not in payload
assert payload["max_tokens"] == 1024
assert payload["temperature"] == 0.5
assert req.headers["Authorization"] == "Bearer fake-token"
assert req.headers["anthropic-beta"] == adapter.BETA_FEATURES
assert "rawPredict" in req.endpoint
assert "streamRawPredict" not in req.endpoint
assert req.base_url == "https://us-central1-aiplatform.googleapis.com"
@patch(
"vibe.core.llm.backend.vertex.get_vertex_access_token",
return_value="fake-token",
)
def test_streaming_request(self, mock_token, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=True,
provider=provider,
)
payload = json.loads(req.body)
assert payload.get("stream") is True
assert "streamRawPredict" in req.endpoint
@patch(
"vibe.core.llm.backend.vertex.get_vertex_access_token",
return_value="fake-token",
)
def test_no_beta_features_for_vertex(self, mock_token, adapter, provider):
"""Vertex AI doesn't support the same beta features as direct Anthropic API."""
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
# Vertex AI doesn't support prompt-caching or other beta features
assert req.headers.get("anthropic-beta", "") == ""
@patch(
"vibe.core.llm.backend.vertex.get_vertex_access_token",
return_value="fake-token",
)
def test_with_extended_thinking(self, mock_token, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
thinking="medium",
)
payload = json.loads(req.body)
assert payload["thinking"] == {"type": "enabled", "budget_tokens": 10000}
assert payload["max_tokens"] == 1024
assert payload["temperature"] == 1
@patch(
"vibe.core.llm.backend.vertex.get_vertex_access_token",
return_value="fake-token",
)
def test_with_tools(self, mock_token, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
tools = [
AvailableTool(
function=AvailableFunction(
name="test_tool",
description="A test tool",
parameters={"type": "object", "properties": {}},
)
)
]
req = adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=messages,
temperature=0.5,
tools=tools,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert len(payload["tools"]) == 1
assert payload["tools"][0]["name"] == "test_tool"
def test_missing_project_id(self, adapter):
provider = ProviderConfig(
name="vertex",
api_base="",
region="us-central1",
api_style="vertex-anthropic",
)
with pytest.raises(ValueError, match="project_id"):
adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=[LLMMessage(role=Role.user, content="Hello")],
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
def test_missing_region(self, adapter):
provider = ProviderConfig(
name="vertex",
api_base="",
project_id="test-project",
api_style="vertex-anthropic",
)
with pytest.raises(ValueError, match="region"):
adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=[LLMMessage(role=Role.user, content="Hello")],
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
@patch(
"vibe.core.llm.backend.vertex.get_vertex_access_token",
return_value="fake-token",
)
def test_default_max_tokens(self, mock_token, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=None,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert payload["max_tokens"] == adapter.DEFAULT_MAX_TOKENS
class TestParseFullResponse:
def test_simple_text_response(self, adapter, provider):
data = {
"content": [{"type": "text", "text": "Hello there!"}],
"usage": {"input_tokens": 10, "output_tokens": 5},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Hello there!"
assert chunk.usage.prompt_tokens == 10
assert chunk.usage.completion_tokens == 5
def test_response_with_tool_calls(self, adapter, provider):
data = {
"content": [
{"type": "text", "text": "Let me help."},
{
"type": "tool_use",
"id": "tool_123",
"name": "search",
"input": {"query": "test"},
},
],
"usage": {"input_tokens": 20, "output_tokens": 15},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Let me help."
assert len(chunk.message.tool_calls) == 1
assert chunk.message.tool_calls[0].id == "tool_123"
assert chunk.message.tool_calls[0].function.name == "search"
assert json.loads(chunk.message.tool_calls[0].function.arguments) == {
"query": "test"
}
def test_response_with_thinking(self, adapter, provider):
data = {
"content": [
{
"type": "thinking",
"thinking": "Let me think...",
"signature": "sig123",
},
{"type": "text", "text": "Here's my answer."},
],
"usage": {"input_tokens": 30, "output_tokens": 20},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Here's my answer."
assert chunk.message.reasoning_content == "Let me think..."
assert chunk.message.reasoning_signature == "sig123"
def test_response_with_cache_tokens(self, adapter, provider):
data = {
"content": [{"type": "text", "text": "Hello"}],
"usage": {
"input_tokens": 10,
"cache_creation_input_tokens": 5,
"cache_read_input_tokens": 3,
"output_tokens": 7,
},
}
chunk = adapter.parse_response(data, provider)
assert chunk.usage.prompt_tokens == 18
assert chunk.usage.completion_tokens == 7
def test_response_with_redacted_thinking(self, adapter, provider):
data = {
"content": [
{"type": "redacted_thinking", "data": "redacted_data_here"},
{"type": "text", "text": "Answer."},
],
"usage": {"input_tokens": 10, "output_tokens": 5},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Answer."
assert chunk.message.reasoning_content is None
def test_response_empty_usage(self, adapter, provider):
data = {"content": [{"type": "text", "text": "Hello"}], "usage": {}}
chunk = adapter.parse_response(data, provider)
assert chunk.usage.prompt_tokens == 0
assert chunk.usage.completion_tokens == 0
class TestStreamingEvents:
def test_message_start(self, adapter, provider):
data = {
"type": "message_start",
"message": {
"usage": {
"input_tokens": 100,
"cache_creation_input_tokens": 20,
"cache_read_input_tokens": 10,
}
},
}
chunk = adapter.parse_response(data, provider)
assert chunk.usage is not None
assert chunk.usage.prompt_tokens == 130
assert chunk.usage.completion_tokens == 0
def test_message_start_without_usage(self, adapter, provider):
data = {"type": "message_start", "message": {}}
chunk = adapter.parse_response(data, provider)
assert chunk.message.role == Role.assistant
def test_content_block_start_tool_use(self, adapter, provider):
data = {
"type": "content_block_start",
"index": 0,
"content_block": {"type": "tool_use", "id": "tool_abc", "name": "search"},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.tool_calls is not None
assert len(chunk.message.tool_calls) == 1
assert chunk.message.tool_calls[0].id == "tool_abc"
assert chunk.message.tool_calls[0].function.name == "search"
assert chunk.message.tool_calls[0].index == 0
def test_content_block_start_thinking(self, adapter, provider):
data = {
"type": "content_block_start",
"index": 0,
"content_block": {"type": "thinking", "thinking": ""},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.reasoning_content is not None
def test_content_block_start_redacted_thinking(self, adapter, provider):
data = {
"type": "content_block_start",
"index": 0,
"content_block": {"type": "redacted_thinking", "data": "abc"},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content is None
assert chunk.message.reasoning_content is None
def test_content_block_delta_text(self, adapter, provider):
data = {
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": "Hello"},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Hello"
def test_content_block_delta_thinking(self, adapter, provider):
data = {
"type": "content_block_delta",
"index": 0,
"delta": {"type": "thinking_delta", "thinking": "I think..."},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.reasoning_content == "I think..."
def test_content_block_delta_input_json(self, adapter, provider):
data = {
"type": "content_block_delta",
"index": 1,
"delta": {"type": "input_json_delta", "partial_json": '{"key":'},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.tool_calls is not None
assert chunk.message.tool_calls[0].function.arguments == '{"key":'
def test_content_block_stop(self, adapter, provider):
data = {"type": "content_block_stop", "index": 0}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content is None
assert chunk.message.reasoning_content is None
def test_message_delta_with_usage(self, adapter, provider):
data = {"type": "message_delta", "usage": {"output_tokens": 42}}
chunk = adapter.parse_response(data, provider)
assert chunk.usage is not None
assert chunk.usage.completion_tokens == 42
assert chunk.usage.prompt_tokens == 0
def test_message_delta_without_usage(self, adapter, provider):
data = {"type": "message_delta", "usage": {}}
chunk = adapter.parse_response(data, provider)
assert chunk.message.role == Role.assistant
def test_unknown_event_returns_empty_chunk(self, adapter, provider):
data = {"type": "ping"}
chunk = adapter.parse_response(data, provider)
assert chunk.message.role == Role.assistant
assert chunk.message.content is None
def test_signature_delta(self, adapter, provider):
data = {
"type": "content_block_delta",
"index": 0,
"delta": {"type": "signature_delta", "signature": "sig_abc"},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.reasoning_signature == "sig_abc"
def test_message_start_resets_state(self, adapter, provider):
adapter._current_index = 5
data = {"type": "message_start", "message": {"usage": {"input_tokens": 10}}}
adapter.parse_response(data, provider)
assert adapter._current_index == 0
def test_full_streaming_sequence(self, adapter, provider):
chunks = []
# message_start
chunks.append(
adapter.parse_response(
{"type": "message_start", "message": {"usage": {"input_tokens": 50}}},
provider,
)
)
assert chunks[-1].usage.prompt_tokens == 50
# thinking block
adapter.parse_response(
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "thinking", "thinking": ""},
},
provider,
)
chunks.append(
adapter.parse_response(
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "thinking_delta", "thinking": "Analyzing..."},
},
provider,
)
)
assert chunks[-1].message.reasoning_content == "Analyzing..."
adapter.parse_response({"type": "content_block_stop", "index": 0}, provider)
# text block
chunks.append(
adapter.parse_response(
{
"type": "content_block_delta",
"index": 1,
"delta": {"type": "text_delta", "text": "Here's the result."},
},
provider,
)
)
assert chunks[-1].message.content == "Here's the result."
# tool use
chunks.append(
adapter.parse_response(
{
"type": "content_block_start",
"index": 2,
"content_block": {
"type": "tool_use",
"id": "tool_1",
"name": "search",
},
},
provider,
)
)
assert chunks[-1].message.tool_calls[0].function.name == "search"
# message_delta with final usage
chunks.append(
adapter.parse_response(
{"type": "message_delta", "usage": {"output_tokens": 100}}, provider
)
)
assert chunks[-1].usage.completion_tokens == 100
class TestHelperMethods:
def test_has_thinking_content_true(self, adapter):
messages = [
{"role": "user", "content": "Hello"},
{
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "Let me think..."},
{"type": "text", "text": "Answer"},
],
},
]
assert adapter._has_thinking_content(messages) is True
def test_has_thinking_content_false(self, adapter):
messages = [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Just text"},
]
assert adapter._has_thinking_content(messages) is False
def test_has_thinking_content_empty(self, adapter):
assert adapter._has_thinking_content([]) is False
def test_has_thinking_content_non_list_content(self, adapter):
messages = [
{"role": "assistant", "content": [{"type": "text", "text": "no thinking"}]}
]
assert adapter._has_thinking_content(messages) is False
def test_add_cache_control_to_last_user_message(self, adapter):
messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}]
adapter._add_cache_control_to_last_user_message(messages)
assert messages[0]["content"][0]["cache_control"] == {"type": "ephemeral"}
def test_add_cache_control_skips_non_user(self, adapter):
messages = [
{"role": "assistant", "content": [{"type": "text", "text": "Hello"}]}
]
adapter._add_cache_control_to_last_user_message(messages)
assert "cache_control" not in messages[0]["content"][0]
def test_add_cache_control_skips_string_content(self, adapter):
messages = [{"role": "user", "content": "Hello"}]
adapter._add_cache_control_to_last_user_message(messages)
assert messages[0]["content"] == "Hello"
def test_add_cache_control_tool_result(self, adapter):
messages = [
{
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": "123", "content": "result"}
],
}
]
adapter._add_cache_control_to_last_user_message(messages)
assert messages[0]["content"][0]["cache_control"] == {"type": "ephemeral"}
def test_add_cache_control_empty_messages(self, adapter):
messages: list[dict] = []
adapter._add_cache_control_to_last_user_message(messages)
assert messages == []