Files
agent-framework/python/packages/anthropic/tests/test_anthropic_client.py
Evan Mattson fe4cd3cddc Revert to public MCP server and skip on transient upstream errors (#5296)
The local MCP server can't be used for hosted tools tests because
Anthropic's backend needs to reach the MCP URL from their infrastructure
(not localhost on the CI runner). Revert to learn.microsoft.com/api/mcp
but catch BadRequestError, InternalServerError, APIConnectionError, and
APITimeoutError and pytest.skip so upstream outages don't block the
merge queue.
2026-04-16 11:46:49 +09:00

2708 lines
93 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import os
import re
from pathlib import Path
from typing import Annotated, Any
from unittest.mock import MagicMock, patch
import pytest
from agent_framework import (
ChatMiddlewareLayer,
ChatOptions,
ChatResponseUpdate,
Content,
FunctionInvocationLayer,
Message,
SupportsChatGetResponse,
tool,
)
from agent_framework._settings import load_settings
from agent_framework._tools import SHELL_TOOL_KIND_VALUE
from agent_framework.observability import ChatTelemetryLayer
from anthropic.types.beta import (
BetaMessage,
BetaTextBlock,
BetaToolUseBlock,
BetaUsage,
)
from pydantic import BaseModel, Field
from agent_framework_anthropic import AnthropicClient, RawAnthropicClient
from agent_framework_anthropic._chat_client import AnthropicSettings
# Test constants
VALID_PNG_BASE64 = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
skip_if_anthropic_integration_tests_disabled = pytest.mark.skipif(
os.getenv("ANTHROPIC_API_KEY", "") in ("", "test-api-key-12345"),
reason="No real ANTHROPIC_API_KEY provided; skipping integration tests.",
)
def create_test_anthropic_client(
mock_anthropic_client: MagicMock,
model: str | None = None,
anthropic_settings: AnthropicSettings | None = None,
) -> AnthropicClient:
"""Helper function to create AnthropicClient instances for testing, bypassing normal validation."""
from agent_framework._tools import normalize_function_invocation_configuration
if anthropic_settings is None:
anthropic_settings = load_settings(
AnthropicSettings,
env_prefix="ANTHROPIC_",
api_key="test-api-key-12345",
chat_model="claude-3-5-sonnet-20241022",
)
# Create client instance directly
client = object.__new__(AnthropicClient)
# Set attributes directly
client.anthropic_client = mock_anthropic_client
client.model = model or anthropic_settings["chat_model"]
client._last_call_id_name = None
client._tool_name_aliases = {}
client.additional_properties = {}
client.middleware = None
client.additional_beta_flags = []
client.chat_middleware = []
client.function_middleware = []
client._cached_chat_middleware_pipeline = None
client._cached_function_middleware_pipeline = None
client.function_invocation_configuration = normalize_function_invocation_configuration(None)
return client
# Settings Tests
def test_anthropic_settings_init(anthropic_unit_test_env: dict[str, str]) -> None:
"""Test AnthropicSettings initialization."""
settings = load_settings(AnthropicSettings, env_prefix="ANTHROPIC_")
assert settings["api_key"] is not None
assert settings["api_key"].get_secret_value() == anthropic_unit_test_env["ANTHROPIC_API_KEY"]
assert settings["chat_model"] == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"]
def test_anthropic_settings_init_with_explicit_values() -> None:
"""Test AnthropicSettings initialization with explicit values."""
settings = load_settings(
AnthropicSettings,
env_prefix="ANTHROPIC_",
api_key="custom-api-key",
chat_model="claude-3-opus-20240229",
)
assert settings["api_key"] is not None
assert settings["api_key"].get_secret_value() == "custom-api-key"
assert settings["chat_model"] == "claude-3-opus-20240229"
@pytest.mark.parametrize("exclude_list", [["ANTHROPIC_API_KEY"]], indirect=True)
def test_anthropic_settings_missing_api_key(
anthropic_unit_test_env: dict[str, str],
) -> None:
"""Test AnthropicSettings when API key is missing."""
settings = load_settings(AnthropicSettings, env_prefix="ANTHROPIC_")
assert settings["api_key"] is None
assert settings["chat_model"] == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"]
# Client Initialization Tests
def test_anthropic_client_init_with_client(mock_anthropic_client: MagicMock) -> None:
"""Test AnthropicClient initialization with existing anthropic_client."""
client = create_test_anthropic_client(mock_anthropic_client, model="claude-3-5-sonnet-20241022")
assert client.anthropic_client is mock_anthropic_client
assert client.model == "claude-3-5-sonnet-20241022"
assert isinstance(client, SupportsChatGetResponse)
def test_anthropic_client_wraps_raw_client_with_standard_layer_order() -> None:
"""Test AnthropicClient composes the standard public layer stack around the raw client."""
assert issubclass(AnthropicClient, RawAnthropicClient)
mro = AnthropicClient.__mro__
assert mro.index(FunctionInvocationLayer) < mro.index(ChatMiddlewareLayer)
assert mro.index(ChatMiddlewareLayer) < mro.index(ChatTelemetryLayer)
assert mro.index(ChatTelemetryLayer) < mro.index(RawAnthropicClient)
# RawAnthropicClient must not include the convenience layers
assert not issubclass(RawAnthropicClient, FunctionInvocationLayer)
assert not issubclass(RawAnthropicClient, ChatMiddlewareLayer)
assert not issubclass(RawAnthropicClient, ChatTelemetryLayer)
def test_anthropic_client_init_auto_create_client(
anthropic_unit_test_env: dict[str, str],
) -> None:
"""Test AnthropicClient initialization with auto-created anthropic_client."""
client = AnthropicClient(
api_key=anthropic_unit_test_env["ANTHROPIC_API_KEY"],
model=anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"],
)
assert client.anthropic_client is not None
assert client.model == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"]
def test_anthropic_client_init_missing_api_key() -> None:
"""Test AnthropicClient initialization when API key is missing."""
with patch("agent_framework_anthropic._chat_client.load_settings") as mock_load:
mock_load.return_value = {
"api_key": None,
"chat_model": "claude-3-5-sonnet-20241022",
}
with pytest.raises(ValueError, match="Anthropic API key is required"):
AnthropicClient()
def test_anthropic_client_service_url(mock_anthropic_client: MagicMock) -> None:
"""Test service_url method."""
client = create_test_anthropic_client(mock_anthropic_client)
assert client.service_url() == "https://api.anthropic.com"
# Message Conversion Tests
def test_prepare_message_for_anthropic_text(mock_anthropic_client: MagicMock) -> None:
"""Test converting text message to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
message = Message(role="user", contents=["Hello, world!"])
result = client._prepare_message_for_anthropic(message)
assert result["role"] == "user"
assert len(result["content"]) == 1
assert result["content"][0]["type"] == "text"
assert result["content"][0]["text"] == "Hello, world!"
def test_prepare_message_for_anthropic_function_call(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting function call message to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
message = Message(
role="assistant",
contents=[
Content.from_function_call(
call_id="call_123",
name="get_weather",
arguments={"location": "San Francisco"},
)
],
)
result = client._prepare_message_for_anthropic(message)
assert result["role"] == "assistant"
assert len(result["content"]) == 1
assert result["content"][0]["type"] == "tool_use"
assert result["content"][0]["id"] == "call_123"
assert result["content"][0]["name"] == "get_weather"
assert result["content"][0]["input"] == {"location": "San Francisco"}
def test_prepare_message_for_anthropic_function_result(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting function result message to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
message = Message(
role="tool",
contents=[
Content.from_function_result(
call_id="call_123",
result="Sunny, 72°F",
)
],
)
result = client._prepare_message_for_anthropic(message)
assert result["role"] == "user"
assert len(result["content"]) == 1
assert result["content"][0]["type"] == "tool_result"
assert result["content"][0]["tool_use_id"] == "call_123"
tool_content = result["content"][0]["content"]
assert isinstance(tool_content, list)
assert len(tool_content) == 1
assert tool_content[0]["type"] == "text"
assert "Sunny" in tool_content[0]["text"]
assert "72" in tool_content[0]["text"]
assert result["content"][0]["is_error"] is False
def test_prepare_message_for_anthropic_function_result_with_data_image(
mock_anthropic_client: MagicMock,
) -> None:
"""Test function result with a data-type image item produces a base64 image block."""
client = create_test_anthropic_client(mock_anthropic_client)
image_content = Content.from_data(data=b"fake_image_bytes", media_type="image/png")
message = Message(
role="tool",
contents=[
Content.from_function_result(
call_id="call_img",
result=[Content.from_text("Here is the image"), image_content],
)
],
)
result = client._prepare_message_for_anthropic(message)
assert result["role"] == "user"
tool_result = result["content"][0]
assert tool_result["type"] == "tool_result"
assert tool_result["tool_use_id"] == "call_img"
content = tool_result["content"]
assert len(content) == 2
assert content[0]["type"] == "text"
assert content[0]["text"] == "Here is the image"
assert content[1]["type"] == "image"
assert content[1]["source"]["type"] == "base64"
assert content[1]["source"]["media_type"] == "image/png"
def test_prepare_message_for_anthropic_function_result_with_uri_image(
mock_anthropic_client: MagicMock,
) -> None:
"""Test function result with a uri-type image item produces a URL image block."""
client = create_test_anthropic_client(mock_anthropic_client)
uri_content = Content.from_uri(uri="https://example.com/image.png", media_type="image/png")
message = Message(
role="tool",
contents=[
Content.from_function_result(
call_id="call_uri",
result=[uri_content],
)
],
)
result = client._prepare_message_for_anthropic(message)
tool_result = result["content"][0]
content = tool_result["content"]
assert len(content) == 1
assert content[0]["type"] == "image"
assert content[0]["source"]["type"] == "url"
assert content[0]["source"]["url"] == "https://example.com/image.png"
def test_prepare_message_for_anthropic_function_result_with_unsupported_media(
mock_anthropic_client: MagicMock,
) -> None:
"""Test function result with unsupported media type skips the item."""
client = create_test_anthropic_client(mock_anthropic_client)
audio_content = Content.from_data(data=b"audio_bytes", media_type="audio/wav")
message = Message(
role="tool",
contents=[
Content.from_function_result(
call_id="call_audio",
result=[Content.from_text("Some text"), audio_content],
)
],
)
result = client._prepare_message_for_anthropic(message)
tool_result = result["content"][0]
content = tool_result["content"]
# Audio should be skipped, only text remains
assert len(content) == 1
assert content[0]["type"] == "text"
assert content[0]["text"] == "Some text"
def test_prepare_message_for_anthropic_function_result_all_unsupported_media(
mock_anthropic_client: MagicMock,
) -> None:
"""Test function result where all items are unsupported falls back to string result."""
client = create_test_anthropic_client(mock_anthropic_client)
audio_content = Content.from_data(data=b"audio_bytes", media_type="audio/wav")
message = Message(
role="tool",
contents=[
Content.from_function_result(
call_id="call_all_unsupported",
result=[audio_content],
)
],
)
result = client._prepare_message_for_anthropic(message)
tool_result = result["content"][0]
# All items unsupported → tool_content is empty → falls back to string result
assert tool_result["content"] == ""
def test_prepare_message_for_anthropic_text_reasoning(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting text reasoning message to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
message = Message(
role="assistant",
contents=[Content.from_text_reasoning(text="Let me think about this...")],
)
result = client._prepare_message_for_anthropic(message)
assert result["role"] == "assistant"
assert len(result["content"]) == 1
assert result["content"][0]["type"] == "thinking"
assert result["content"][0]["thinking"] == "Let me think about this..."
assert "signature" not in result["content"][0]
def test_prepare_message_for_anthropic_text_reasoning_with_signature(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting text reasoning message with signature to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
message = Message(
role="assistant",
contents=[Content.from_text_reasoning(text="Let me think about this...", protected_data="sig_abc123")],
)
result = client._prepare_message_for_anthropic(message)
assert result["role"] == "assistant"
assert len(result["content"]) == 1
assert result["content"][0]["type"] == "thinking"
assert result["content"][0]["thinking"] == "Let me think about this..."
assert result["content"][0]["signature"] == "sig_abc123"
def test_prepare_message_for_anthropic_mcp_server_tool_call(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting MCP server tool call message to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
message = Message(
role="assistant",
contents=[
Content.from_mcp_server_tool_call(
call_id="mcp_call_123",
tool_name="search_docs",
server_name="microsoft-learn",
arguments={"query": "Azure Functions"},
)
],
)
result = client._prepare_message_for_anthropic(message)
assert result["role"] == "assistant"
assert len(result["content"]) == 1
assert result["content"][0]["type"] == "mcp_tool_use"
assert result["content"][0]["id"] == "mcp_call_123"
assert result["content"][0]["name"] == "search_docs"
assert result["content"][0]["server_name"] == "microsoft-learn"
assert result["content"][0]["input"] == {"query": "Azure Functions"}
def test_prepare_message_for_anthropic_mcp_server_tool_call_no_server_name(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting MCP server tool call with no server name defaults to empty string."""
client = create_test_anthropic_client(mock_anthropic_client)
message = Message(
role="assistant",
contents=[
Content.from_mcp_server_tool_call(
call_id="mcp_call_456",
tool_name="list_files",
arguments=None,
)
],
)
result = client._prepare_message_for_anthropic(message)
assert result["role"] == "assistant"
assert len(result["content"]) == 1
assert result["content"][0]["type"] == "mcp_tool_use"
assert result["content"][0]["id"] == "mcp_call_456"
assert result["content"][0]["name"] == "list_files"
assert result["content"][0]["server_name"] == ""
assert result["content"][0]["input"] == {}
def test_prepare_message_for_anthropic_mcp_server_tool_result(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting MCP server tool result message to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
message = Message(
role="tool",
contents=[
Content.from_mcp_server_tool_result(
call_id="mcp_call_123",
output="Found 3 results for Azure Functions.",
)
],
)
result = client._prepare_message_for_anthropic(message)
assert result["role"] == "user"
assert len(result["content"]) == 1
assert result["content"][0]["type"] == "mcp_tool_result"
assert result["content"][0]["tool_use_id"] == "mcp_call_123"
assert result["content"][0]["content"] == "Found 3 results for Azure Functions."
def test_prepare_message_for_anthropic_mcp_server_tool_result_none_output(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting MCP server tool result with None output defaults to empty string."""
client = create_test_anthropic_client(mock_anthropic_client)
message = Message(
role="tool",
contents=[
Content.from_mcp_server_tool_result(
call_id="mcp_call_789",
output=None,
)
],
)
result = client._prepare_message_for_anthropic(message)
assert result["role"] == "user"
assert len(result["content"]) == 1
assert result["content"][0]["type"] == "mcp_tool_result"
assert result["content"][0]["tool_use_id"] == "mcp_call_789"
assert result["content"][0]["content"] == ""
def test_prepare_messages_for_anthropic_with_system(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting messages list with system message."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [
Message(role="system", contents=["You are a helpful assistant."]),
Message(role="user", contents=["Hello!"]),
]
result = client._prepare_messages_for_anthropic(messages)
# System message should be skipped
assert len(result) == 1
assert result[0]["role"] == "user"
assert result[0]["content"][0]["text"] == "Hello!"
def test_prepare_messages_for_anthropic_without_system(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting messages list without system message."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [
Message(role="user", contents=["Hello!"]),
Message(role="assistant", contents=["Hi there!"]),
]
result = client._prepare_messages_for_anthropic(messages)
assert len(result) == 2
assert result[0]["role"] == "user"
assert result[1]["role"] == "assistant"
# Tool Conversion Tests
def test_prepare_tools_for_anthropic_tool(mock_anthropic_client: MagicMock) -> None:
"""Test converting FunctionTool to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="Location to get weather for")],
) -> str:
"""Get weather for a location."""
return f"Weather for {location}"
chat_options = ChatOptions(tools=[get_weather])
result = client._prepare_tools_for_anthropic(chat_options)
assert result is not None
assert "tools" in result
assert len(result["tools"]) == 1
assert result["tools"][0]["type"] == "custom"
assert result["tools"][0]["name"] == "get_weather"
assert "Get weather for a location" in result["tools"][0]["description"]
def test_prepare_tools_for_anthropic_web_search(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting web_search dict tool to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(tools=[client.get_web_search_tool()])
result = client._prepare_tools_for_anthropic(chat_options)
assert result is not None
assert "tools" in result
assert len(result["tools"]) == 1
assert result["tools"][0]["type"] == "web_search_20250305"
assert result["tools"][0]["name"] == "web_search"
def test_prepare_tools_for_anthropic_code_interpreter(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting code_interpreter dict tool to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(tools=[client.get_code_interpreter_tool()])
result = client._prepare_tools_for_anthropic(chat_options)
assert result is not None
assert "tools" in result
assert len(result["tools"]) == 1
assert result["tools"][0]["type"] == "code_execution_20250825"
assert result["tools"][0]["name"] == "code_execution"
def _dummy_bash(command: str) -> str:
return f"executed: {command}"
def test_prepare_tools_for_anthropic_shell_tool(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting tool-decorated FunctionTool to Anthropic bash format."""
client = create_test_anthropic_client(mock_anthropic_client)
@tool(kind=SHELL_TOOL_KIND_VALUE)
def run_bash(command: str) -> str:
return _dummy_bash(command)
chat_options = ChatOptions(tools=[run_bash])
result = client._prepare_tools_for_anthropic(chat_options)
assert result is not None
assert "tools" in result
assert len(result["tools"]) == 1
assert result["tools"][0]["type"] == "bash_20250124"
assert result["tools"][0]["name"] == "bash"
def test_prepare_tools_for_anthropic_shell_tool_custom_type(
mock_anthropic_client: MagicMock,
) -> None:
"""Test shell tool with custom type via additional_properties."""
client = create_test_anthropic_client(mock_anthropic_client)
@tool(kind=SHELL_TOOL_KIND_VALUE, additional_properties={"type": "bash_20241022"})
def run_bash(command: str) -> str:
return _dummy_bash(command)
chat_options = ChatOptions(tools=[run_bash])
result = client._prepare_tools_for_anthropic(chat_options)
assert result is not None
assert "tools" in result
assert result["tools"][0]["type"] == "bash_20241022"
assert result["tools"][0]["name"] == "bash"
def test_prepare_tools_for_anthropic_shell_tool_does_not_mutate_name(
mock_anthropic_client: MagicMock,
) -> None:
"""Shell tool API name should be 'bash' without mutating local FunctionTool name."""
client = create_test_anthropic_client(mock_anthropic_client)
@tool(
name="run_local_shell",
approval_mode="never_require",
kind=SHELL_TOOL_KIND_VALUE,
)
def run_local_shell(command: str) -> str:
return command
chat_options = ChatOptions(tools=[run_local_shell])
result = client._prepare_tools_for_anthropic(chat_options)
assert result is not None
assert result["tools"][0]["name"] == "bash"
assert run_local_shell.name == "run_local_shell"
def test_get_shell_tool_reuses_function_tool_instance(
mock_anthropic_client: MagicMock,
) -> None:
"""Passing a FunctionTool should update and return the same tool instance."""
client = create_test_anthropic_client(mock_anthropic_client)
@tool(name="run_shell", approval_mode="never_require")
def run_shell(command: str) -> str:
return command
shell_tool = client.get_shell_tool(
func=run_shell,
description="Run local bash",
approval_mode="always_require",
)
assert shell_tool is run_shell
assert shell_tool.kind == SHELL_TOOL_KIND_VALUE
assert shell_tool.description == "Run local bash"
assert shell_tool.approval_mode == "always_require"
def test_prepare_tools_for_anthropic_mcp_tool(mock_anthropic_client: MagicMock) -> None:
"""Test converting MCP dict tool to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(tools=[client.get_mcp_tool(name="test-mcp", url="https://example.com/mcp")])
result = client._prepare_tools_for_anthropic(chat_options)
assert result is not None
assert "mcp_servers" in result
assert len(result["mcp_servers"]) == 1
assert result["mcp_servers"][0]["type"] == "url"
assert result["mcp_servers"][0]["name"] == "test-mcp"
assert result["mcp_servers"][0]["url"] == "https://example.com/mcp"
def test_prepare_tools_for_anthropic_mcp_with_auth(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting MCP dict tool with authorization token."""
client = create_test_anthropic_client(mock_anthropic_client)
# Use the static method with authorization_token
mcp_tool = client.get_mcp_tool(
name="test-mcp",
url="https://example.com/mcp",
authorization_token="Bearer token123",
)
chat_options = ChatOptions(tools=[mcp_tool])
result = client._prepare_tools_for_anthropic(chat_options)
assert result is not None
assert "mcp_servers" in result
# The authorization_token should be passed through
assert "authorization_token" in result["mcp_servers"][0]
assert result["mcp_servers"][0]["authorization_token"] == "Bearer token123"
def test_prepare_tools_for_anthropic_dict_tool(
mock_anthropic_client: MagicMock,
) -> None:
"""Test converting dict tool to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(tools=[{"type": "custom", "name": "custom_tool", "description": "A custom tool"}])
result = client._prepare_tools_for_anthropic(chat_options)
assert result is not None
assert "tools" in result
assert len(result["tools"]) == 1
assert result["tools"][0]["name"] == "custom_tool"
def test_prepare_tools_for_anthropic_none(mock_anthropic_client: MagicMock) -> None:
"""Test converting None tools."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions()
result = client._prepare_tools_for_anthropic(chat_options)
assert result is None
# Run Options Tests
async def test_prepare_options_basic(mock_anthropic_client: MagicMock) -> None:
"""Test _prepare_options with basic ChatOptions."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=["Hello"])]
chat_options = ChatOptions(max_tokens=100, temperature=0.7)
run_options = client._prepare_options(messages, chat_options)
assert run_options["model"] == client.model
assert run_options["max_tokens"] == 100
assert run_options["temperature"] == 0.7
assert "messages" in run_options
async def test_prepare_options_with_system_message(
mock_anthropic_client: MagicMock,
) -> None:
"""Test _prepare_options with system message."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [
Message(role="system", contents=["You are helpful."]),
Message(role="user", contents=["Hello"]),
]
chat_options = ChatOptions()
run_options = client._prepare_options(messages, chat_options)
assert run_options["system"] == "You are helpful."
assert len(run_options["messages"]) == 1 # System message not in messages list
async def test_anthropic_shell_tool_is_invoked_in_function_loop(
mock_anthropic_client: MagicMock,
) -> None:
"""Function invocation loop should execute shell tool when Anthropic returns bash tool_use."""
client = create_test_anthropic_client(mock_anthropic_client)
executed_commands: list[str] = []
def run_local_shell(command: str) -> str:
executed_commands.append(command)
return f"executed: {command}"
shell_tool_instance = client.get_shell_tool(func=run_local_shell, approval_mode="never_require")
mock_tool_use = MagicMock()
mock_tool_use.type = "tool_use"
mock_tool_use.id = "call_bash_loop"
mock_tool_use.name = "bash"
mock_tool_use.input = {"command": "pwd"}
first_message = MagicMock()
first_message.id = "msg_1"
first_message.content = [mock_tool_use]
first_message.usage = None
first_message.model = "claude-test"
first_message.stop_reason = "tool_use"
mock_text_block = MagicMock()
mock_text_block.type = "text"
mock_text_block.text = "Done"
second_message = MagicMock()
second_message.id = "msg_2"
second_message.content = [mock_text_block]
second_message.usage = None
second_message.model = "claude-test"
second_message.stop_reason = "end_turn"
mock_anthropic_client.beta.messages.create.side_effect = [
first_message,
second_message,
]
await client.get_response(
messages=[Message(role="user", contents=["Run pwd"])],
options={"tools": [shell_tool_instance], "max_tokens": 64},
)
assert executed_commands == ["pwd"]
assert mock_anthropic_client.beta.messages.create.call_count == 2
second_request_messages = mock_anthropic_client.beta.messages.create.call_args_list[1].kwargs["messages"]
tool_results = [
block
for message in second_request_messages
for block in message.get("content", [])
if block.get("type") == "tool_result"
]
assert len(tool_results) == 1
assert tool_results[0]["tool_use_id"] == "call_bash_loop"
tool_content = tool_results[0]["content"]
assert isinstance(tool_content, list)
assert any("executed: pwd" in item.get("text", "") for item in tool_content)
async def test_prepare_options_with_tool_choice_auto(
mock_anthropic_client: MagicMock,
) -> None:
"""Test _prepare_options with auto tool choice."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=["Hello"])]
chat_options = ChatOptions(tool_choice="auto", allow_multiple_tool_calls=False)
run_options = client._prepare_options(messages, chat_options)
assert run_options["tool_choice"]["type"] == "auto"
assert run_options["tool_choice"]["disable_parallel_tool_use"] is True
assert "allow_multiple_tool_calls" not in run_options
async def test_prepare_options_with_tool_choice_required(
mock_anthropic_client: MagicMock,
) -> None:
"""Test _prepare_options with required tool choice."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=["Hello"])]
# For required with specific function, need to pass as dict
chat_options = ChatOptions(tool_choice={"mode": "required", "required_function_name": "get_weather"})
run_options = client._prepare_options(messages, chat_options)
assert run_options["tool_choice"]["type"] == "tool"
assert run_options["tool_choice"]["name"] == "get_weather"
async def test_prepare_options_with_tool_choice_none(
mock_anthropic_client: MagicMock,
) -> None:
"""Test _prepare_options with none tool choice."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=["Hello"])]
chat_options = ChatOptions(tool_choice="none")
run_options = client._prepare_options(messages, chat_options)
assert run_options["tool_choice"]["type"] == "none"
async def test_prepare_options_with_tools(mock_anthropic_client: MagicMock) -> None:
"""Test _prepare_options with tools."""
client = create_test_anthropic_client(mock_anthropic_client)
@tool(approval_mode="never_require")
def get_weather(location: str) -> str:
"""Get weather for a location."""
return f"Weather for {location}"
messages = [Message(role="user", contents=["Hello"])]
chat_options = ChatOptions(tools=[get_weather])
run_options = client._prepare_options(messages, chat_options)
assert "tools" in run_options
assert len(run_options["tools"]) == 1
async def test_prepare_options_with_stop_sequences(
mock_anthropic_client: MagicMock,
) -> None:
"""Test _prepare_options with stop sequences."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=["Hello"])]
chat_options = ChatOptions(stop=["STOP", "END"])
run_options = client._prepare_options(messages, chat_options)
assert run_options["stop_sequences"] == ["STOP", "END"]
async def test_prepare_options_with_top_p(mock_anthropic_client: MagicMock) -> None:
"""Test _prepare_options with top_p."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=["Hello"])]
chat_options = ChatOptions(top_p=0.9)
run_options = client._prepare_options(messages, chat_options)
assert run_options["top_p"] == 0.9
async def test_prepare_options_excludes_stream_option(
mock_anthropic_client: MagicMock,
) -> None:
"""Test _prepare_options excludes stream when stream is provided in options."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=["Hello"])]
chat_options: dict[str, Any] = {"stream": True, "max_tokens": 100}
run_options = client._prepare_options(messages, chat_options)
assert "stream" not in run_options
async def test_prepare_options_filters_internal_kwargs(
mock_anthropic_client: MagicMock,
) -> None:
"""Test _prepare_options filters internal framework kwargs.
Internal kwargs like _function_middleware_pipeline, thread, and middleware
should be filtered out before being passed to the Anthropic API.
"""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=["Hello"])]
chat_options: ChatOptions = {}
# Simulate internal kwargs that get passed through the middleware pipeline
internal_kwargs = {
"_function_middleware_pipeline": object(),
"_chat_middleware_pipeline": object(),
"_any_underscore_prefixed": object(),
"thread": object(),
"middleware": [object()],
}
run_options = client._prepare_options(messages, chat_options, **internal_kwargs)
# Internal kwargs should be filtered out
assert "_function_middleware_pipeline" not in run_options
assert "_chat_middleware_pipeline" not in run_options
assert "_any_underscore_prefixed" not in run_options
assert "thread" not in run_options
assert "middleware" not in run_options
# Response Processing Tests
def test_process_message_basic(mock_anthropic_client: MagicMock) -> None:
"""Test _process_message with basic text response."""
client = create_test_anthropic_client(mock_anthropic_client)
mock_message = MagicMock(spec=BetaMessage)
mock_message.id = "msg_123"
mock_message.model = "claude-3-5-sonnet-20241022"
mock_message.content = [BetaTextBlock(type="text", text="Hello there!")]
mock_message.usage = BetaUsage(input_tokens=10, output_tokens=5)
mock_message.stop_reason = "end_turn"
response = client._process_message(mock_message, {})
assert response.response_id == "msg_123"
assert response.model == "claude-3-5-sonnet-20241022"
assert len(response.messages) == 1
assert response.messages[0].role == "assistant"
assert len(response.messages[0].contents) == 1
assert response.messages[0].contents[0].type == "text"
assert response.messages[0].contents[0].text == "Hello there!"
assert response.finish_reason == "stop"
assert response.usage_details is not None
assert response.usage_details["input_token_count"] == 10
assert response.usage_details["output_token_count"] == 5
def test_process_message_with_dict_response_format(mock_anthropic_client: MagicMock) -> None:
"""_process_message should preserve dict response_format values for response.value parsing."""
client = create_test_anthropic_client(mock_anthropic_client)
mock_message = MagicMock(spec=BetaMessage)
mock_message.id = "msg_123"
mock_message.model = "claude-3-5-sonnet-20241022"
mock_message.content = [BetaTextBlock(type="text", text='{"greeting": "Hello"}')]
mock_message.usage = BetaUsage(input_tokens=10, output_tokens=5)
mock_message.stop_reason = "end_turn"
response = client._process_message(
mock_message,
options={"response_format": {"type": "object", "properties": {"greeting": {"type": "string"}}}},
)
assert response.value is not None
assert isinstance(response.value, dict)
assert response.value["greeting"] == "Hello"
def test_process_message_with_tool_use(mock_anthropic_client: MagicMock) -> None:
"""Test _process_message with tool use."""
client = create_test_anthropic_client(mock_anthropic_client)
mock_message = MagicMock(spec=BetaMessage)
mock_message.id = "msg_123"
mock_message.model = "claude-3-5-sonnet-20241022"
mock_message.content = [
BetaToolUseBlock(
type="tool_use",
id="call_123",
name="get_weather",
input={"location": "San Francisco"},
)
]
mock_message.usage = BetaUsage(input_tokens=10, output_tokens=5)
mock_message.stop_reason = "tool_use"
response = client._process_message(mock_message, {})
assert len(response.messages[0].contents) == 1
assert response.messages[0].contents[0].type == "function_call"
assert response.messages[0].contents[0].call_id == "call_123"
assert response.messages[0].contents[0].name == "get_weather"
assert response.finish_reason == "tool_calls"
def test_parse_usage_from_anthropic_basic(mock_anthropic_client: MagicMock) -> None:
"""Test _parse_usage_from_anthropic with basic usage."""
client = create_test_anthropic_client(mock_anthropic_client)
usage = BetaUsage(input_tokens=10, output_tokens=5)
result = client._parse_usage_from_anthropic(usage)
assert result is not None
assert result["input_token_count"] == 10
assert result["output_token_count"] == 5
def test_parse_usage_from_anthropic_none(mock_anthropic_client: MagicMock) -> None:
"""Test _parse_usage_from_anthropic with None usage."""
client = create_test_anthropic_client(mock_anthropic_client)
result = client._parse_usage_from_anthropic(None)
assert result is None
def test_parse_contents_from_anthropic_text(mock_anthropic_client: MagicMock) -> None:
"""Test _parse_contents_from_anthropic with text content."""
client = create_test_anthropic_client(mock_anthropic_client)
content = [BetaTextBlock(type="text", text="Hello!")]
result = client._parse_contents_from_anthropic(content)
assert len(result) == 1
assert result[0].type == "text"
assert result[0].text == "Hello!"
def test_parse_contents_from_anthropic_tool_use(
mock_anthropic_client: MagicMock,
) -> None:
"""Test _parse_contents_from_anthropic with tool use."""
client = create_test_anthropic_client(mock_anthropic_client)
content = [
BetaToolUseBlock(
type="tool_use",
id="call_123",
name="get_weather",
input={"location": "SF"},
)
]
result = client._parse_contents_from_anthropic(content)
assert len(result) == 1
assert result[0].type == "function_call"
assert result[0].call_id == "call_123"
assert result[0].name == "get_weather"
def test_parse_contents_from_anthropic_input_json_delta_no_duplicate_name(
mock_anthropic_client: MagicMock,
) -> None:
"""Test that input_json_delta events have empty name to prevent duplicate ToolCallStartEvents.
When streaming tool calls, the initial tool_use event provides the name,
and subsequent input_json_delta events should have name="" to prevent
ag-ui from emitting duplicate ToolCallStartEvents.
"""
client = create_test_anthropic_client(mock_anthropic_client)
# First, simulate a tool_use event that sets _last_call_id_name
tool_use_content = MagicMock()
tool_use_content.type = "tool_use"
tool_use_content.id = "call_123"
tool_use_content.name = "get_weather"
tool_use_content.input = {}
result = client._parse_contents_from_anthropic([tool_use_content])
assert len(result) == 1
assert result[0].type == "function_call"
assert result[0].call_id == "call_123"
assert result[0].name == "get_weather" # Initial event has name
# Now simulate input_json_delta events (argument streaming)
delta_content_1 = MagicMock()
delta_content_1.type = "input_json_delta"
delta_content_1.partial_json = '{"location":'
result = client._parse_contents_from_anthropic([delta_content_1])
assert len(result) == 1
assert result[0].type == "function_call"
assert result[0].call_id == "call_123"
assert result[0].name == "" # Delta events should have empty name
assert result[0].arguments == '{"location":'
# Another delta
delta_content_2 = MagicMock()
delta_content_2.type = "input_json_delta"
delta_content_2.partial_json = '"San Francisco"}'
result = client._parse_contents_from_anthropic([delta_content_2])
assert len(result) == 1
assert result[0].type == "function_call"
assert result[0].call_id == "call_123"
assert result[0].name == "" # Still empty name for subsequent deltas
assert result[0].arguments == '"San Francisco"}'
def test_parse_contents_server_tool_use_input_json_delta_ignored(
mock_anthropic_client: MagicMock,
) -> None:
"""Regression test: input_json_delta events are ignored after a server_tool_use block.
Server-managed tools have their execution handled server-side, so streaming
input_json_delta events must not produce Content.from_function_call(name='')
entries that would cause Anthropic API 400 errors on subsequent turns.
"""
client = create_test_anthropic_client(mock_anthropic_client)
# Simulate a server_tool_use event that sets _last_call_content_type
server_tool_content = MagicMock()
server_tool_content.type = "server_tool_use"
server_tool_content.id = "srvtool_abc"
server_tool_content.name = "web_search"
server_tool_content.input = {}
result = client._parse_contents_from_anthropic([server_tool_content])
# server_tool_use falls through to function_call (not mcp_tool_use / code_execution)
assert len(result) == 1
assert result[0].type == "function_call"
assert client._last_call_content_type == "server_tool_use" # type: ignore[attr-defined]
# input_json_delta events after server_tool_use must be silently ignored
delta_content = MagicMock()
delta_content.type = "input_json_delta"
delta_content.partial_json = '{"query": "latest news"}'
result = client._parse_contents_from_anthropic([delta_content])
assert result == [], "input_json_delta after server_tool_use should produce no content, but got: %r" % result
# A second delta must also be ignored
delta_content_2 = MagicMock()
delta_content_2.type = "input_json_delta"
delta_content_2.partial_json = '{"extra": true}'
result = client._parse_contents_from_anthropic([delta_content_2])
assert result == [], (
"subsequent input_json_delta after server_tool_use should also be ignored, but got: %r" % result
)
# Stream Processing Tests
def test_process_stream_event_simple(mock_anthropic_client: MagicMock) -> None:
"""Test _process_stream_event with simple mock event."""
client = create_test_anthropic_client(mock_anthropic_client)
# Test with a basic mock event - the actual implementation will handle real events
mock_event = MagicMock()
mock_event.type = "message_stop"
result = client._process_stream_event(mock_event)
# message_stop events return None
assert result is None
async def test_inner_get_response(mock_anthropic_client: MagicMock) -> None:
"""Test _inner_get_response method."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create a mock message response
mock_message = MagicMock(spec=BetaMessage)
mock_message.id = "msg_test"
mock_message.model = "claude-3-5-sonnet-20241022"
mock_message.content = [BetaTextBlock(type="text", text="Hello!")]
mock_message.usage = BetaUsage(input_tokens=5, output_tokens=3)
mock_message.stop_reason = "end_turn"
mock_anthropic_client.beta.messages.create.return_value = mock_message
messages = [Message(role="user", contents=["Hi"])]
chat_options = ChatOptions(max_tokens=10)
response = await client._inner_get_response( # type: ignore[attr-defined]
messages=messages, options=chat_options
)
assert response is not None
assert response.response_id == "msg_test"
assert len(response.messages) == 1
async def test_inner_get_response_ignores_options_stream_non_streaming(
mock_anthropic_client: MagicMock,
) -> None:
"""Test stream option in options does not conflict in non-streaming mode."""
client = create_test_anthropic_client(mock_anthropic_client)
mock_message = MagicMock(spec=BetaMessage)
mock_message.id = "msg_test"
mock_message.model = "claude-3-5-sonnet-20241022"
mock_message.content = [BetaTextBlock(type="text", text="Hello!")]
mock_message.usage = BetaUsage(input_tokens=5, output_tokens=3)
mock_message.stop_reason = "end_turn"
mock_anthropic_client.beta.messages.create.return_value = mock_message
messages = [Message(role="user", contents=["Hi"])]
options: dict[str, Any] = {"max_tokens": 10, "stream": True}
await client._inner_get_response( # type: ignore[attr-defined]
messages=messages,
options=options,
)
assert mock_anthropic_client.beta.messages.create.call_count == 1
assert mock_anthropic_client.beta.messages.create.call_args.kwargs["stream"] is False
async def test_inner_get_response_streaming(mock_anthropic_client: MagicMock) -> None:
"""Test _inner_get_response method with streaming."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create mock streaming response
async def mock_stream():
mock_event = MagicMock()
mock_event.type = "message_stop"
yield mock_event
mock_anthropic_client.beta.messages.create.return_value = mock_stream()
messages = [Message(role="user", contents=["Hi"])]
chat_options = ChatOptions(max_tokens=10)
chunks: list[ChatResponseUpdate] = []
async for chunk in client._inner_get_response( # type: ignore[attr-defined]
messages=messages, options=chat_options, stream=True
):
if chunk:
chunks.append(chunk)
# We should get at least some response (even if empty due to message_stop)
assert isinstance(chunks, list)
async def test_inner_get_response_ignores_options_stream_streaming(
mock_anthropic_client: MagicMock,
) -> None:
"""Test stream option in options does not conflict in streaming mode."""
client = create_test_anthropic_client(mock_anthropic_client)
async def mock_stream():
mock_event = MagicMock()
mock_event.type = "message_stop"
yield mock_event
mock_anthropic_client.beta.messages.create.return_value = mock_stream()
messages = [Message(role="user", contents=["Hi"])]
options: dict[str, Any] = {"max_tokens": 10, "stream": False}
async for _ in client._inner_get_response( # type: ignore[attr-defined]
messages=messages,
options=options,
stream=True,
):
pass
assert mock_anthropic_client.beta.messages.create.call_count == 1
assert mock_anthropic_client.beta.messages.create.call_args.kwargs["stream"] is True
def test_process_stream_event_message_start_sets_assistant_role(mock_anthropic_client: MagicMock) -> None:
"""Test that message_start streaming event sets role='assistant'.
This is critical: without role='assistant', _process_update cannot detect
a role boundary between a prior tool message and the new assistant turn,
causing tool_use blocks to collapse into a user-role message and triggering
Anthropic's '`tool_use` blocks can only be in `assistant` messages' error.
"""
client = create_test_anthropic_client(mock_anthropic_client)
mock_event = MagicMock()
mock_event.type = "message_start"
mock_event.message.id = "msg_abc"
mock_event.message.role = "assistant"
mock_event.message.model = "claude-3-5-sonnet-20241022"
mock_event.message.content = []
mock_event.message.stop_reason = None
mock_event.message.usage = None
result = client._process_stream_event(mock_event)
assert result is not None
assert result.role == "assistant"
def test_process_stream_event_message_start_role_prevents_tool_use_collapse() -> None:
"""Regression test: tool_use blocks must not end up in a user-role message.
Simulates two consecutive streaming tool-call iterations:
Iteration 1: assistant emits tool_use → framework appends tool result (role=tool)
Iteration 2: assistant starts a new message_start → must create a NEW message
Without role='assistant' on the message_start update, _process_update sees
update.role=None (falsy) and appends to the last message (role='tool'),
producing {"role": "user", "content": [tool_result, tool_use]} which
Anthropic rejects with HTTP 400.
"""
from agent_framework import ChatResponse, ChatResponseUpdate, Content, Message
# Simulate what the streaming tool loop produces after iteration 1:
# an existing 'tool' message is the last in the response
existing_tool_message = Message(
role="tool",
contents=[Content.from_function_result(call_id="call_1", result="some result")],
)
response = ChatResponse(messages=[existing_tool_message])
# Now simulate the message_start update from iteration 2 — WITH role set
message_start_update = ChatResponseUpdate(
role="assistant",
response_id="msg_iter2",
)
# Simulate a content_block_start carrying a tool_use — no role on this one (correct)
tool_use_update = ChatResponseUpdate(
contents=[
Content.from_function_call(
call_id="call_2",
name="get_weather",
arguments={"location": "NYC"},
)
],
)
# Apply updates exactly as from_updates / _process_update would
from agent_framework._types import _process_update
_process_update(response, message_start_update)
_process_update(response, tool_use_update)
# Must have TWO messages: the original tool message + a new assistant message
assert len(response.messages) == 2, "tool_use from iteration 2 collapsed into the tool message from iteration 1"
assert response.messages[0].role == "tool"
assert response.messages[1].role == "assistant"
# The assistant message must contain the tool_use, not the tool result
assert response.messages[1].contents[0].type == "function_call"
assert response.messages[1].contents[0].call_id == "call_2"
def test_process_stream_event_message_start_without_role_reproduces_bug() -> None:
"""Documents the original bug: missing role causes tool_use to collapse into tool message.
This test demonstrates WHY the fix (adding role='assistant') was necessary.
It intentionally reproduces the broken behavior when role is absent.
"""
from agent_framework import ChatResponse, ChatResponseUpdate, Content, Message
from agent_framework._types import _process_update
existing_tool_message = Message(
role="tool",
contents=[Content.from_function_result(call_id="call_1", result="some result")],
)
response = ChatResponse(messages=[existing_tool_message])
# message_start WITHOUT role (the original broken state)
message_start_update = ChatResponseUpdate(
role=None,
response_id="msg_iter2",
)
tool_use_update = ChatResponseUpdate(
contents=[
Content.from_function_call(
call_id="call_2",
name="get_weather",
arguments={"location": "NYC"},
)
],
)
_process_update(response, message_start_update)
_process_update(response, tool_use_update)
# BUG: only 1 message — tool_use collapsed into the tool message
assert len(response.messages) == 1, "Expected bug: should still be 1 message without the fix"
# The single message has role='tool' but contains a function_call — invalid for Anthropic API
assert response.messages[0].role == "tool"
has_function_call = any(c.type == "function_call" for c in response.messages[0].contents)
assert has_function_call, "Expected bug: function_call leaked into tool message"
# Integration Tests
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a location."""
return f"The weather in {location} is sunny and 72°F"
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_anthropic_integration_tests_disabled
async def test_anthropic_client_integration_basic_chat() -> None:
"""Integration test for basic chat completion."""
client = AnthropicClient()
messages = [Message(role="user", contents=["Say 'Hello, World!' and nothing else."])]
response = await client.get_response(messages=messages, options={"max_tokens": 50})
assert response is not None
assert len(response.messages) > 0
assert response.messages[0].role == "assistant"
assert len(response.messages[0].text) > 0
assert response.usage_details is not None
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_anthropic_integration_tests_disabled
async def test_anthropic_client_integration_streaming_chat() -> None:
"""Integration test for streaming chat completion."""
client = AnthropicClient()
messages = [Message(role="user", contents=["Count from 1 to 5."])]
chunks = []
async for chunk in client.get_response(messages=messages, stream=True, options={"max_tokens": 50}):
chunks.append(chunk)
assert len(chunks) > 0
assert any(chunk.contents for chunk in chunks)
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_anthropic_integration_tests_disabled
async def test_anthropic_client_integration_function_calling() -> None:
"""Integration test for function calling."""
client = AnthropicClient()
messages = [Message(role="user", contents=["What's the weather in San Francisco?"])]
tools = [get_weather]
response = await client.get_response(
messages=messages,
options={"tools": tools, "max_tokens": 100},
)
assert response is not None
# Should contain function call
has_function_call = any(content.type == "function_call" for msg in response.messages for content in msg.contents)
assert has_function_call
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_anthropic_integration_tests_disabled
async def test_anthropic_client_integration_hosted_tools() -> None:
"""Integration test for hosted tools."""
import anthropic
client = AnthropicClient()
messages = [Message(role="user", contents=["What tools do you have available?"])]
tools = [
AnthropicClient.get_web_search_tool(),
AnthropicClient.get_code_interpreter_tool(),
AnthropicClient.get_mcp_tool(
name="example-mcp",
url="https://learn.microsoft.com/api/mcp",
),
]
try:
response = await client.get_response(
messages=messages,
options={"tools": tools, "max_tokens": 100},
)
except (
anthropic.BadRequestError,
anthropic.InternalServerError,
anthropic.APIConnectionError,
anthropic.APITimeoutError,
) as e:
pytest.skip(f"Upstream MCP server unavailable: {e}")
assert response is not None
assert response.text is not None
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_anthropic_integration_tests_disabled
async def test_anthropic_client_integration_with_system_message() -> None:
"""Integration test with system message."""
client = AnthropicClient()
messages = [
Message(role="system", contents=["You are a pirate. Always respond like a pirate."]),
Message(role="user", contents=["Hello!"]),
]
response = await client.get_response(messages=messages, options={"max_tokens": 50})
assert response is not None
assert len(response.messages) > 0
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_anthropic_integration_tests_disabled
async def test_anthropic_client_integration_temperature_control() -> None:
"""Integration test with temperature control."""
client = AnthropicClient()
messages = [Message(role="user", contents=["Say hello."])]
response = await client.get_response(
messages=messages,
options={"max_tokens": 20, "temperature": 0.0},
)
assert response is not None
assert response.messages[0].text is not None
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_anthropic_integration_tests_disabled
async def test_anthropic_client_integration_ordering() -> None:
"""Integration test with ordering."""
client = AnthropicClient()
messages = [
Message(role="user", contents=["Say hello."]),
Message(role="user", contents=["Then say goodbye."]),
Message(role="assistant", contents=["Thank you for chatting!"]),
Message(role="assistant", contents=["Let me know if I can help."]),
Message(role="user", contents=["Just testing things."]),
]
response = await client.get_response(messages=messages)
assert response is not None
assert response.messages[0].text is not None
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_anthropic_integration_tests_disabled
async def test_anthropic_client_integration_images() -> None:
"""Integration test with images."""
client = AnthropicClient()
# get a image from the assets folder
image_path = Path(__file__).parent / "assets" / "sample_image.jpg"
with open(image_path, "rb") as img_file: # noqa [ASYNC230]
image_bytes = img_file.read()
messages = [
Message(
role="user",
contents=[
Content.from_text(text="Describe this image"),
Content.from_data(media_type="image/jpeg", data=image_bytes),
],
),
]
response = await client.get_response(messages=messages)
assert response is not None
assert response.messages[0].text is not None
text = response.messages[0].text.lower()
assert re.search(r"\b(house|home|building|cottage|mansion|villa)\b", text)
# Response Format Tests
def test_prepare_response_format_openai_style(mock_anthropic_client: MagicMock) -> None:
"""Test response_format with OpenAI-style json_schema."""
client = create_test_anthropic_client(mock_anthropic_client)
response_format = {
"json_schema": {
"schema": {
"type": "object",
"properties": {"name": {"type": "string"}},
}
}
}
result = client._prepare_response_format(response_format)
assert result["type"] == "json_schema"
assert result["schema"]["additionalProperties"] is False
assert result["schema"]["properties"]["name"]["type"] == "string"
def test_prepare_response_format_direct_schema(
mock_anthropic_client: MagicMock,
) -> None:
"""Test response_format with direct schema key."""
client = create_test_anthropic_client(mock_anthropic_client)
response_format = {
"schema": {
"type": "object",
"properties": {"value": {"type": "number"}},
}
}
result = client._prepare_response_format(response_format)
assert result["type"] == "json_schema"
assert result["schema"]["additionalProperties"] is False
assert result["schema"]["properties"]["value"]["type"] == "number"
def test_prepare_response_format_raw_schema(mock_anthropic_client: MagicMock) -> None:
"""Test response_format with raw schema dict."""
client = create_test_anthropic_client(mock_anthropic_client)
response_format = {
"type": "object",
"properties": {"count": {"type": "integer"}},
}
result = client._prepare_response_format(response_format)
assert result["type"] == "json_schema"
assert result["schema"]["additionalProperties"] is False
assert result["schema"]["properties"]["count"]["type"] == "integer"
def test_prepare_response_format_pydantic_model(
mock_anthropic_client: MagicMock,
) -> None:
"""Test response_format with Pydantic BaseModel."""
client = create_test_anthropic_client(mock_anthropic_client)
class TestModel(BaseModel):
name: str
age: int
result = client._prepare_response_format(TestModel)
assert result["type"] == "json_schema"
assert result["schema"]["additionalProperties"] is False
assert "properties" in result["schema"]
# Message Preparation Tests
def test_prepare_message_with_image_data(mock_anthropic_client: MagicMock) -> None:
"""Test preparing messages with base64-encoded image data."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create message with image data content
message = Message(
role="user",
contents=[Content.from_data(media_type="image/png", data=VALID_PNG_BASE64)],
)
result = client._prepare_message_for_anthropic(message)
assert result["role"] == "user"
assert len(result["content"]) == 1
assert result["content"][0]["type"] == "image"
assert result["content"][0]["source"]["type"] == "base64"
assert result["content"][0]["source"]["media_type"] == "image/png"
def test_prepare_message_with_image_uri(mock_anthropic_client: MagicMock) -> None:
"""Test preparing messages with image URI."""
client = create_test_anthropic_client(mock_anthropic_client)
message = Message(
role="user",
contents=[Content.from_uri(uri="https://example.com/image.jpg", media_type="image/jpeg")],
)
result = client._prepare_message_for_anthropic(message)
assert result["role"] == "user"
assert len(result["content"]) == 1
assert result["content"][0]["type"] == "image"
assert result["content"][0]["source"]["type"] == "url"
assert result["content"][0]["source"]["url"] == "https://example.com/image.jpg"
def test_prepare_message_with_unsupported_data_type(
mock_anthropic_client: MagicMock,
) -> None:
"""Test preparing messages with unsupported data content type."""
client = create_test_anthropic_client(mock_anthropic_client)
message = Message(
role="user",
contents=[Content.from_data(media_type="application/pdf", data=b"PDF data")],
)
result = client._prepare_message_for_anthropic(message)
# PDF should be ignored
assert result["role"] == "user"
assert len(result["content"]) == 0
def test_prepare_message_with_unsupported_uri_type(
mock_anthropic_client: MagicMock,
) -> None:
"""Test preparing messages with unsupported URI content type."""
client = create_test_anthropic_client(mock_anthropic_client)
message = Message(
role="user",
contents=[Content.from_uri(uri="https://example.com/video.mp4", media_type="video/mp4")],
)
result = client._prepare_message_for_anthropic(message)
# Video should be ignored
assert result["role"] == "user"
assert len(result["content"]) == 0
# Content Parsing Tests
def test_parse_contents_mcp_tool_use(mock_anthropic_client: MagicMock) -> None:
"""Test parsing MCP tool use content."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create mock MCP tool use block
mock_block = MagicMock()
mock_block.type = "mcp_tool_use"
mock_block.id = "call_123"
mock_block.name = "test_tool"
mock_block.input = {"arg": "value"}
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "mcp_server_tool_call"
def test_parse_contents_code_execution_tool(mock_anthropic_client: MagicMock) -> None:
"""Test parsing code execution tool use."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create mock code execution tool use block
mock_block = MagicMock()
mock_block.type = "tool_use"
mock_block.id = "call_456"
mock_block.name = "code_execution_tool"
mock_block.input = "print('hello')"
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "code_interpreter_tool_call"
def test_parse_contents_mcp_tool_result_list_content(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing MCP tool result with list content."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_123", "test_tool")
# Create mock MCP tool result with list content
mock_text_block = MagicMock()
mock_text_block.type = "text"
mock_text_block.text = "Result text"
mock_block = MagicMock()
mock_block.type = "mcp_tool_result"
mock_block.tool_use_id = "call_123"
mock_block.content = [mock_text_block]
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "mcp_server_tool_result"
def test_parse_contents_mcp_tool_result_string_content(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing MCP tool result with string content."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_123", "test_tool")
# Create mock MCP tool result with string content
mock_block = MagicMock()
mock_block.type = "mcp_tool_result"
mock_block.tool_use_id = "call_123"
mock_block.content = "Simple string result"
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "mcp_server_tool_result"
def test_parse_contents_mcp_tool_result_bytes_content(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing MCP tool result with bytes content."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_123", "test_tool")
# Create mock MCP tool result with bytes content
mock_block = MagicMock()
mock_block.type = "mcp_tool_result"
mock_block.tool_use_id = "call_123"
mock_block.content = b"Binary data"
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "mcp_server_tool_result"
def test_parse_contents_mcp_tool_result_object_content(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing MCP tool result with object content."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_123", "test_tool")
# Create mock MCP tool result with object content
mock_content_obj = MagicMock()
mock_content_obj.type = "text"
mock_content_obj.text = "Object content"
mock_block = MagicMock()
mock_block.type = "mcp_tool_result"
mock_block.tool_use_id = "call_123"
mock_block.content = mock_content_obj
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "mcp_server_tool_result"
def test_parse_contents_web_search_tool_result(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing web search tool result."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_789", "web_search")
# Create mock web search tool result
mock_block = MagicMock()
mock_block.type = "web_search_tool_result"
mock_block.tool_use_id = "call_789"
mock_block.content = "Search results"
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "function_result"
def test_parse_contents_web_fetch_tool_result(mock_anthropic_client: MagicMock) -> None:
"""Test parsing web fetch tool result."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_101", "web_fetch")
# Create mock web fetch tool result
mock_block = MagicMock()
mock_block.type = "web_fetch_tool_result"
mock_block.tool_use_id = "call_101"
mock_block.content = "Fetched content"
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "function_result"
# MCP Tool Configuration Tests
def test_get_mcp_tool_with_allowed_tools() -> None:
"""Test get_mcp_tool with allowed_tools parameter."""
result = AnthropicClient.get_mcp_tool(
name="Test Server",
url="https://example.com/mcp",
allowed_tools=["tool1", "tool2"],
)
assert result["type"] == "mcp"
assert result["server_label"] == "Test_Server"
assert result["server_url"] == "https://example.com/mcp"
assert result["allowed_tools"] == ["tool1", "tool2"]
def test_get_mcp_tool_without_allowed_tools() -> None:
"""Test get_mcp_tool without allowed_tools parameter."""
result = AnthropicClient.get_mcp_tool(name="Test Server", url="https://example.com/mcp")
assert result["type"] == "mcp"
assert result["server_label"] == "Test_Server"
assert result["server_url"] == "https://example.com/mcp"
assert "allowed_tools" not in result
def test_prepare_tools_mcp_with_allowed_tools(mock_anthropic_client: MagicMock) -> None:
"""Test MCP tool with allowed_tools configuration."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
mcp_tool = {
"type": "mcp",
"server_label": "test_server",
"server_url": "https://example.com/mcp",
"allowed_tools": ["tool1", "tool2"],
}
options = {"tools": [mcp_tool]}
result = client._prepare_options(messages, options)
assert "mcp_servers" in result
assert len(result["mcp_servers"]) == 1
assert result["mcp_servers"][0]["tool_configuration"]["allowed_tools"] == [
"tool1",
"tool2",
]
# Tool Choice Mode Tests
def test_tool_choice_auto_with_allow_multiple(mock_anthropic_client: MagicMock) -> None:
"""Test tool_choice auto mode with allow_multiple=False."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
@tool(approval_mode="never_require")
def test_func() -> str:
"""Test function."""
return "test"
options = {
"tools": [test_func],
"tool_choice": "auto",
"allow_multiple_tool_calls": False,
}
result = client._prepare_options(messages, options)
assert result["tool_choice"]["type"] == "auto"
assert result["tool_choice"]["disable_parallel_tool_use"] is True
def test_tool_choice_required_any(mock_anthropic_client: MagicMock) -> None:
"""Test tool_choice required mode without specific function."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
@tool(approval_mode="never_require")
def test_func() -> str:
"""Test function."""
return "test"
options = {"tools": [test_func], "tool_choice": "required"}
result = client._prepare_options(messages, options)
assert result["tool_choice"]["type"] == "any"
def test_tool_choice_required_specific_function(
mock_anthropic_client: MagicMock,
) -> None:
"""Test tool_choice required mode with specific function."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
@tool(approval_mode="never_require")
def test_func() -> str:
"""Test function."""
return "test"
options = {
"tools": [test_func],
"tool_choice": {"mode": "required", "required_function_name": "test_func"},
}
result = client._prepare_options(messages, options)
assert result["tool_choice"]["type"] == "tool"
assert result["tool_choice"]["name"] == "test_func"
def test_tool_choice_none(mock_anthropic_client: MagicMock) -> None:
"""Test tool_choice none mode."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
@tool(approval_mode="never_require")
def test_func() -> str:
"""Test function."""
return "test"
options = {"tools": [test_func], "tool_choice": "none"}
result = client._prepare_options(messages, options)
assert result["tool_choice"]["type"] == "none"
def test_tool_choice_required_allows_parallel_use(
mock_anthropic_client: MagicMock,
) -> None:
"""Test tool choice required mode with allow_multiple=True."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
@tool(approval_mode="never_require")
def test_func() -> str:
"""Test function."""
return "test"
options = {
"tools": [test_func],
"tool_choice": "required",
"allow_multiple_tool_calls": True,
}
# This tests line 739: setting disable_parallel_tool_use in required mode
result = client._prepare_options(messages, options)
assert result["tool_choice"]["type"] == "any"
assert result["tool_choice"]["disable_parallel_tool_use"] is False
# Options Preparation Tests
def test_prepare_options_with_instructions(mock_anthropic_client: MagicMock) -> None:
"""Test prepare_options with instructions parameter."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
options = {"instructions": "You are a helpful assistant"}
result = client._prepare_options(messages, options)
# Instructions should be prepended as system message
assert result["model"] == "claude-3-5-sonnet-20241022"
assert result["max_tokens"] == 1024
def test_prepare_options_missing_model(mock_anthropic_client: MagicMock) -> None:
"""Test prepare_options raises error when model is missing."""
client = create_test_anthropic_client(mock_anthropic_client)
client.model = "" # Set empty model
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
options = {}
try:
client._prepare_options(messages, options)
raise AssertionError("Expected ValueError")
except ValueError as e:
assert "model must be a non-empty string" in str(e)
def test_prepare_options_translates_model_option(mock_anthropic_client: MagicMock) -> None:
"""Test prepare_options translates model to model for runtime option compatibility."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
result = client._prepare_options(messages, {"model": "claude-3-5-sonnet-20241022"})
assert result["model"] == "claude-3-5-sonnet-20241022"
assert "model_id" not in result
def test_prepare_options_translates_model_kwarg(mock_anthropic_client: MagicMock) -> None:
"""Test prepare_options translates model passed as a direct keyword argument."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
result = client._prepare_options(messages, {}, model="claude-3-5-sonnet-20241022")
assert result["model"] == "claude-3-5-sonnet-20241022"
assert "model_id" not in result
def test_prepare_options_with_user_metadata(mock_anthropic_client: MagicMock) -> None:
"""Test prepare_options maps user to metadata.user_id."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
options = {"user": "user123"}
result = client._prepare_options(messages, options)
assert "user" not in result
assert result["metadata"]["user_id"] == "user123"
def test_prepare_options_user_metadata_no_override(
mock_anthropic_client: MagicMock,
) -> None:
"""Test user option doesn't override existing user_id in metadata."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
options = {"user": "user123", "metadata": {"user_id": "existing_user"}}
result = client._prepare_options(messages, options)
# Existing user_id should be preserved
assert result["metadata"]["user_id"] == "existing_user"
def test_process_stream_event_message_stop(mock_anthropic_client: MagicMock) -> None:
"""Test processing message_stop event."""
client = create_test_anthropic_client(mock_anthropic_client)
# message_stop events don't produce output
mock_event = MagicMock()
mock_event.type = "message_stop"
result = client._process_stream_event(mock_event)
assert result is None
def test_parse_usage_with_cache_tokens(mock_anthropic_client: MagicMock) -> None:
"""Test parsing usage with cache creation and read tokens."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create mock usage with cache tokens
mock_usage = MagicMock()
mock_usage.input_tokens = 100
mock_usage.output_tokens = 50
mock_usage.cache_creation_input_tokens = 20
mock_usage.cache_read_input_tokens = 30
result = client._parse_usage_from_anthropic(mock_usage)
assert result is not None
assert result["output_token_count"] == 50
assert result["input_token_count"] == 100
assert result["anthropic.cache_creation_input_tokens"] == 20
assert result["anthropic.cache_read_input_tokens"] == 30
# Code Execution Result Tests
def test_parse_code_execution_result_with_error(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing code execution result with error."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_code1", "code_execution_tool")
# Create mock code execution result with error
from anthropic.types.beta.beta_code_execution_tool_result_error import (
BetaCodeExecutionToolResultError,
)
mock_block = MagicMock()
mock_block.type = "code_execution_tool_result"
mock_block.tool_use_id = "call_code1"
mock_block.content = BetaCodeExecutionToolResultError(
type="code_execution_tool_result_error", error_code="execution_time_exceeded"
)
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "code_interpreter_tool_result"
def test_parse_code_execution_result_with_stdout(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing code execution result with stdout."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_code2", "code_execution_tool")
# Create mock code execution result with stdout
mock_content = MagicMock()
mock_content.stdout = "Hello, world!"
mock_content.stderr = None
mock_content.content = []
mock_block = MagicMock()
mock_block.type = "code_execution_tool_result"
mock_block.tool_use_id = "call_code2"
mock_block.content = mock_content
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "code_interpreter_tool_result"
def test_parse_code_execution_result_with_stderr(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing code execution result with stderr."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_code3", "code_execution_tool")
# Create mock code execution result with stderr
mock_content = MagicMock()
mock_content.stdout = None
mock_content.stderr = "Warning message"
mock_content.content = []
mock_block = MagicMock()
mock_block.type = "code_execution_tool_result"
mock_block.tool_use_id = "call_code3"
mock_block.content = mock_content
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "code_interpreter_tool_result"
def test_parse_code_execution_result_with_files(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing code execution result with file outputs."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_code4", "code_execution_tool")
# Create mock file output
mock_file = MagicMock()
mock_file.file_id = "file_123"
# Create mock code execution result with files
mock_content = MagicMock()
mock_content.stdout = None
mock_content.stderr = None
mock_content.content = [mock_file]
mock_block = MagicMock()
mock_block.type = "code_execution_tool_result"
mock_block.tool_use_id = "call_code4"
mock_block.content = mock_content
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "code_interpreter_tool_result"
# Bash Execution Result Tests
def test_parse_bash_execution_result_with_stdout(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing bash execution result with stdout."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_bash2", "bash_code_execution")
# Create mock bash execution result with stdout
mock_content = MagicMock()
mock_content.stdout = "Output text"
mock_content.stderr = None
mock_content.return_code = 0
mock_content.content = []
mock_block = MagicMock()
mock_block.type = "bash_code_execution_tool_result"
mock_block.tool_use_id = "call_bash2"
mock_block.content = mock_content
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "shell_tool_result"
assert result[0].call_id == "call_bash2"
assert result[0].outputs is not None
assert len(result[0].outputs) == 1
assert result[0].outputs[0].type == "shell_command_output"
assert result[0].outputs[0].stdout == "Output text"
assert result[0].outputs[0].exit_code == 0
assert result[0].outputs[0].timed_out is False
def test_parse_bash_execution_result_with_stderr(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing bash execution result with stderr."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_bash3", "bash_code_execution")
# Create mock bash execution result with stderr
mock_content = MagicMock()
mock_content.stdout = None
mock_content.stderr = "Error output"
mock_content.return_code = 1
mock_content.content = []
mock_block = MagicMock()
mock_block.type = "bash_code_execution_tool_result"
mock_block.tool_use_id = "call_bash3"
mock_block.content = mock_content
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "shell_tool_result"
assert result[0].call_id == "call_bash3"
assert result[0].outputs is not None
assert result[0].outputs[0].type == "shell_command_output"
assert result[0].outputs[0].stderr == "Error output"
assert result[0].outputs[0].exit_code == 1
def test_parse_bash_execution_result_with_error(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing bash execution error produces shell_tool_result with error info."""
from anthropic.types.beta.beta_bash_code_execution_tool_result_error import (
BetaBashCodeExecutionToolResultError,
)
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_bash_err", "bash_code_execution")
mock_error = MagicMock(spec=BetaBashCodeExecutionToolResultError)
mock_error.error_code = "execution_time_exceeded"
mock_block = MagicMock()
mock_block.type = "bash_code_execution_tool_result"
mock_block.tool_use_id = "call_bash_err"
mock_block.content = mock_error
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "shell_tool_result"
assert result[0].outputs is not None
assert result[0].outputs[0].type == "shell_command_output"
assert result[0].outputs[0].stderr == "execution_time_exceeded"
assert result[0].outputs[0].timed_out is True
# Text Editor Result Tests
def test_parse_text_editor_result_error(mock_anthropic_client: MagicMock) -> None:
"""Test parsing text editor result with error."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_editor1", "text_editor_code_execution")
# Create mock text editor result with error
mock_content = MagicMock()
mock_content.type = "text_editor_code_execution_tool_result_error"
mock_content.error = "File not found"
mock_block = MagicMock()
mock_block.type = "text_editor_code_execution_tool_result"
mock_block.tool_use_id = "call_editor1"
mock_block.content = mock_content
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "function_result"
def test_parse_text_editor_result_view(mock_anthropic_client: MagicMock) -> None:
"""Test parsing text editor view result."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_editor2", "text_editor_code_execution")
# Create mock text editor view result
mock_content = MagicMock()
mock_content.type = "text_editor_code_execution_view_result"
mock_content.content = "File content"
mock_content.start_line = 10
mock_content.num_lines = 5
mock_block = MagicMock()
mock_block.type = "text_editor_code_execution_tool_result"
mock_block.tool_use_id = "call_editor2"
mock_block.content = mock_content
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "function_result"
def test_parse_text_editor_result_str_replace(mock_anthropic_client: MagicMock) -> None:
"""Test parsing text editor string replace result."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_editor3", "text_editor_code_execution")
# Create mock text editor str_replace result
mock_content = MagicMock()
mock_content.type = "text_editor_code_execution_str_replace_result"
mock_content.old_start = 5
mock_content.old_lines = 3
mock_content.new_start = 5
mock_content.new_lines = 4
mock_content.lines = ["line1", "line2", "line3", "line4"]
mock_block = MagicMock()
mock_block.type = "text_editor_code_execution_tool_result"
mock_block.tool_use_id = "call_editor3"
mock_block.content = mock_content
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "function_result"
def test_parse_text_editor_result_file_create(mock_anthropic_client: MagicMock) -> None:
"""Test parsing text editor file create result."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_editor4", "text_editor_code_execution")
# Create mock text editor create result
mock_content = MagicMock()
mock_content.type = "text_editor_code_execution_create_result"
mock_content.is_file_update = False
mock_block = MagicMock()
mock_block.type = "text_editor_code_execution_tool_result"
mock_block.tool_use_id = "call_editor4"
mock_block.content = mock_content
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "function_result"
# Thinking Block Tests
def test_parse_thinking_block(mock_anthropic_client: MagicMock) -> None:
"""Test parsing thinking content block."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create mock thinking block
mock_block = MagicMock()
mock_block.type = "thinking"
mock_block.thinking = "Let me think about this..."
mock_block.signature = "sig_abc123"
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "text_reasoning"
assert result[0].protected_data == "sig_abc123"
def test_parse_thinking_delta_block(mock_anthropic_client: MagicMock) -> None:
"""Test parsing thinking delta content block."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create mock thinking delta block
mock_block = MagicMock()
mock_block.type = "thinking_delta"
mock_block.thinking = "more thinking..."
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "text_reasoning"
def test_parse_signature_delta_block(mock_anthropic_client: MagicMock) -> None:
"""Test parsing signature delta content block."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create mock signature delta block
mock_block = MagicMock()
mock_block.type = "signature_delta"
mock_block.signature = "sig_xyz789"
result = client._parse_contents_from_anthropic([mock_block])
assert len(result) == 1
assert result[0].type == "text_reasoning"
assert result[0].text is None
assert result[0].protected_data == "sig_xyz789"
# Citation Tests
def test_parse_citations_char_location(mock_anthropic_client: MagicMock) -> None:
"""Test parsing citations with char_location."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create mock text block with citations
mock_citation = MagicMock()
mock_citation.type = "char_location"
mock_citation.title = "Source Title"
mock_citation.cited_text = "Citation snippet"
mock_citation.start_char_index = 0
mock_citation.end_char_index = 10
mock_citation.file_id = None
mock_block = MagicMock()
mock_block.type = "text"
mock_block.text = "Text with citation"
mock_block.citations = [mock_citation]
result = client._parse_citations_from_anthropic(mock_block)
assert len(result) > 0
def test_parse_citations_page_location(mock_anthropic_client: MagicMock) -> None:
"""Test parsing citations with page_location."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create mock citation with page location
mock_citation = MagicMock()
mock_citation.type = "page_location"
mock_citation.document_title = "Document Title"
mock_citation.cited_text = "Cited text from page"
mock_citation.start_page_number = 1
mock_citation.end_page_number = 3
mock_citation.file_id = None
mock_block = MagicMock()
mock_block.type = "text"
mock_block.text = "Text with page citation"
mock_block.citations = [mock_citation]
result = client._parse_citations_from_anthropic(mock_block)
assert len(result) > 0
def test_parse_citations_content_block_location(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing citations with content_block_location."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create mock citation with content block location
mock_citation = MagicMock()
mock_citation.type = "content_block_location"
mock_citation.document_title = "Document Title"
mock_citation.cited_text = "Cited text from content blocks"
mock_citation.start_block_index = 0
mock_citation.end_block_index = 2
mock_citation.file_id = None
mock_block = MagicMock()
mock_block.type = "text"
mock_block.text = "Text with block citation"
mock_block.citations = [mock_citation]
result = client._parse_citations_from_anthropic(mock_block)
assert len(result) > 0
def test_parse_citations_web_search_location(mock_anthropic_client: MagicMock) -> None:
"""Test parsing citations with web_search_result_location."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create mock citation with web search location
mock_citation = MagicMock()
mock_citation.type = "web_search_result_location"
mock_citation.title = "Search Result"
mock_citation.cited_text = "Cited text from search"
mock_citation.url = "https://example.com"
mock_citation.file_id = None
mock_block = MagicMock()
mock_block.type = "text"
mock_block.text = "Text with web citation"
mock_block.citations = [mock_citation]
result = client._parse_citations_from_anthropic(mock_block)
assert len(result) > 0
def test_parse_citations_search_result_location(
mock_anthropic_client: MagicMock,
) -> None:
"""Test parsing citations with search_result_location."""
client = create_test_anthropic_client(mock_anthropic_client)
# Create mock citation with search result location
mock_citation = MagicMock()
mock_citation.type = "search_result_location"
mock_citation.title = "Search Result"
mock_citation.cited_text = "Cited text"
mock_citation.source = "https://source.com"
mock_citation.start_block_index = 0
mock_citation.end_block_index = 1
mock_citation.file_id = None
mock_block = MagicMock()
mock_block.type = "text"
mock_block.text = "Text with search citation"
mock_block.citations = [mock_citation]
result = client._parse_citations_from_anthropic(mock_block)
assert len(result) > 0
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_anthropic_integration_tests_disabled
async def test_anthropic_client_integration_tool_rich_content_image() -> None:
"""Integration test: a tool returns an image and the model describes it."""
image_path = Path(__file__).parent / "assets" / "sample_image.jpg"
image_bytes = image_path.read_bytes()
@tool(approval_mode="never_require")
def get_test_image() -> Content:
"""Return a test image for analysis."""
return Content.from_data(data=image_bytes, media_type="image/jpeg")
client = AnthropicClient()
client.function_invocation_configuration["max_iterations"] = 2
messages = [Message(role="user", contents=["Call the get_test_image tool and describe what you see."])]
response = await client.get_response(
messages=messages,
options={"tools": [get_test_image], "tool_choice": "auto", "max_tokens": 200},
)
assert response is not None
assert response.text is not None
assert len(response.text) > 0
# sample_image.jpg contains a photo of a house; the model should mention it.
assert "house" in response.text.lower(), f"Model did not describe the house image. Response: {response.text}"