Add tests for file and batch feat for anthropic

This commit is contained in:
Sameer Kankute 2025-12-10 17:14:47 +05:30
parent ec3c9191f3
commit b9d3d7c059

View File

@ -0,0 +1,639 @@
"""
Test Anthropic Files Handler and Batch Retrieval
Tests for:
1. AnthropicFilesHandler.afile_content() - retrieving batch results
2. AnthropicBatchesConfig.transform_retrieve_batch_response() - transforming batch responses
3. Transformation of Anthropic batch results to OpenAI format
"""
import json
import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch
sys.path.insert(0, os.path.abspath("../../../../"))
import httpx
import pytest
from litellm.llms.anthropic.batches.transformation import AnthropicBatchesConfig
from litellm.llms.anthropic.files.handler import AnthropicFilesHandler
from litellm.types.llms.openai import FileContentRequest, HttpxBinaryResponseContent
class TestAnthropicFilesHandler:
"""Test Anthropic Files Handler for batch results retrieval"""
@pytest.fixture
def handler(self):
"""Create AnthropicFilesHandler instance"""
return AnthropicFilesHandler()
@pytest.fixture
def mock_anthropic_batch_results_succeeded(self):
"""Mock Anthropic batch results with succeeded status"""
return json.dumps({
"custom_id": "test-request-1",
"result": {
"type": "succeeded",
"message": {
"id": "msg_123",
"model": "claude-3-5-sonnet-20241022",
"role": "assistant",
"content": [
{
"type": "text",
"text": "Hello, world!"
}
],
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {
"input_tokens": 10,
"output_tokens": 5
}
}
}
}).encode("utf-8")
@pytest.fixture
def mock_anthropic_batch_results_errored(self):
"""Mock Anthropic batch results with errored status"""
return json.dumps({
"custom_id": "test-request-2",
"result": {
"type": "errored",
"error": {
"error": {
"type": "invalid_request_error",
"message": "Invalid request"
},
"request_id": "req_456"
}
}
}).encode("utf-8")
@pytest.fixture
def mock_anthropic_batch_results_canceled(self):
"""Mock Anthropic batch results with canceled status"""
return json.dumps({
"custom_id": "test-request-3",
"result": {
"type": "canceled"
}
}).encode("utf-8")
@pytest.fixture
def mock_anthropic_batch_results_mixed(self):
"""Mock Anthropic batch results with multiple result types"""
lines = [
json.dumps({
"custom_id": "test-request-1",
"result": {
"type": "succeeded",
"message": {
"id": "msg_123",
"model": "claude-3-5-sonnet-20241022",
"role": "assistant",
"content": [{"type": "text", "text": "Success"}],
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 5}
}
}
}),
json.dumps({
"custom_id": "test-request-2",
"result": {
"type": "errored",
"error": {
"error": {
"type": "rate_limit_error",
"message": "Rate limit exceeded"
},
"request_id": "req_456"
}
}
}),
json.dumps({
"custom_id": "test-request-3",
"result": {
"type": "expired"
}
})
]
return "\n".join(lines).encode("utf-8")
@pytest.mark.asyncio
async def test_afile_content_success(self, handler, mock_anthropic_batch_results_succeeded):
"""Test successful file content retrieval and transformation"""
file_content_request: FileContentRequest = {
"file_id": "batch_123",
"extra_headers": None,
"extra_body": None
}
# Mock the httpx client
mock_response = httpx.Response(
status_code=200,
content=mock_anthropic_batch_results_succeeded,
headers={"content-type": "application/json"},
request=httpx.Request(method="GET", url="https://api.anthropic.com/v1/messages/batches/batch_123/results")
)
with patch("litellm.llms.anthropic.files.handler.get_async_httpx_client") as mock_get_client:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_get_client.return_value = mock_client
with patch.object(handler.anthropic_model_info, "get_api_key", return_value="test-api-key"):
with patch.object(handler.anthropic_model_info, "get_api_base", return_value="https://api.anthropic.com"):
result = await handler.afile_content(
file_content_request=file_content_request,
api_key="test-api-key"
)
# Verify result
assert isinstance(result, HttpxBinaryResponseContent)
assert result.response.status_code == 200
# Verify transformation to OpenAI format
content = result.response.content.decode("utf-8")
lines = [line for line in content.strip().split("\n") if line.strip()]
assert len(lines) == 1
transformed_result = json.loads(lines[0])
assert transformed_result["custom_id"] == "test-request-1"
assert transformed_result["response"]["status_code"] == 200
assert "body" in transformed_result["response"]
# Verify body has required OpenAI format fields
assert "id" in transformed_result["response"]["body"]
assert transformed_result["response"]["body"]["object"] == "chat.completion"
assert "choices" in transformed_result["response"]["body"]
# Verify request_id matches the original message id
assert transformed_result["response"]["request_id"] == "msg_123"
@pytest.mark.asyncio
async def test_afile_content_with_prefix(self, handler, mock_anthropic_batch_results_succeeded):
"""Test file content retrieval with anthropic_batch_results: prefix"""
file_content_request: FileContentRequest = {
"file_id": "anthropic_batch_results:batch_123",
"extra_headers": None,
"extra_body": None
}
mock_response = httpx.Response(
status_code=200,
content=mock_anthropic_batch_results_succeeded,
headers={"content-type": "application/json"},
request=httpx.Request(method="GET", url="https://api.anthropic.com/v1/messages/batches/batch_123/results")
)
with patch("litellm.llms.anthropic.files.handler.get_async_httpx_client") as mock_get_client:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_get_client.return_value = mock_client
with patch.object(handler.anthropic_model_info, "get_api_key", return_value="test-api-key"):
with patch.object(handler.anthropic_model_info, "get_api_base", return_value="https://api.anthropic.com"):
result = await handler.afile_content(
file_content_request=file_content_request,
api_key="test-api-key"
)
assert isinstance(result, HttpxBinaryResponseContent)
# Verify the URL was constructed correctly (batch_id extracted from prefix)
mock_client.get.assert_called_once()
call_url = mock_client.get.call_args[1]["url"]
assert "batch_123" in call_url
@pytest.mark.asyncio
async def test_afile_content_errored_result(self, handler, mock_anthropic_batch_results_errored):
"""Test transformation of errored batch results"""
file_content_request: FileContentRequest = {
"file_id": "batch_123",
"extra_headers": None,
"extra_body": None
}
mock_response = httpx.Response(
status_code=200,
content=mock_anthropic_batch_results_errored,
headers={"content-type": "application/json"},
request=httpx.Request(method="GET", url="https://api.anthropic.com/v1/messages/batches/batch_123/results")
)
with patch("litellm.llms.anthropic.files.handler.get_async_httpx_client") as mock_get_client:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_get_client.return_value = mock_client
with patch.object(handler.anthropic_model_info, "get_api_key", return_value="test-api-key"):
with patch.object(handler.anthropic_model_info, "get_api_base", return_value="https://api.anthropic.com"):
result = await handler.afile_content(
file_content_request=file_content_request,
api_key="test-api-key"
)
content = result.response.content.decode("utf-8")
lines = [line for line in content.strip().split("\n") if line.strip()]
assert len(lines) == 1
transformed_result = json.loads(lines[0])
assert transformed_result["custom_id"] == "test-request-2"
assert transformed_result["response"]["status_code"] == 400 # invalid_request_error maps to 400
assert transformed_result["response"]["body"]["error"]["type"] == "invalid_request_error"
assert transformed_result["response"]["body"]["error"]["message"] == "Invalid request"
@pytest.mark.asyncio
async def test_afile_content_canceled_result(self, handler, mock_anthropic_batch_results_canceled):
"""Test transformation of canceled batch results"""
file_content_request: FileContentRequest = {
"file_id": "batch_123",
"extra_headers": None,
"extra_body": None
}
mock_response = httpx.Response(
status_code=200,
content=mock_anthropic_batch_results_canceled,
headers={"content-type": "application/json"},
request=httpx.Request(method="GET", url="https://api.anthropic.com/v1/messages/batches/batch_123/results")
)
with patch("litellm.llms.anthropic.files.handler.get_async_httpx_client") as mock_get_client:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_get_client.return_value = mock_client
with patch.object(handler.anthropic_model_info, "get_api_key", return_value="test-api-key"):
with patch.object(handler.anthropic_model_info, "get_api_base", return_value="https://api.anthropic.com"):
result = await handler.afile_content(
file_content_request=file_content_request,
api_key="test-api-key"
)
content = result.response.content.decode("utf-8")
lines = [line for line in content.strip().split("\n") if line.strip()]
assert len(lines) == 1
transformed_result = json.loads(lines[0])
assert transformed_result["custom_id"] == "test-request-3"
assert transformed_result["response"]["status_code"] == 400
assert "Batch request was canceled" in transformed_result["response"]["body"]["error"]["message"]
@pytest.mark.asyncio
async def test_afile_content_mixed_results(self, handler, mock_anthropic_batch_results_mixed):
"""Test transformation of mixed batch results (succeeded, errored, expired)"""
file_content_request: FileContentRequest = {
"file_id": "batch_123",
"extra_headers": None,
"extra_body": None
}
mock_response = httpx.Response(
status_code=200,
content=mock_anthropic_batch_results_mixed,
headers={"content-type": "application/json"},
request=httpx.Request(method="GET", url="https://api.anthropic.com/v1/messages/batches/batch_123/results")
)
with patch("litellm.llms.anthropic.files.handler.get_async_httpx_client") as mock_get_client:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_get_client.return_value = mock_client
with patch.object(handler.anthropic_model_info, "get_api_key", return_value="test-api-key"):
with patch.object(handler.anthropic_model_info, "get_api_base", return_value="https://api.anthropic.com"):
result = await handler.afile_content(
file_content_request=file_content_request,
api_key="test-api-key"
)
content = result.response.content.decode("utf-8")
lines = [line for line in content.strip().split("\n") if line.strip()]
assert len(lines) == 3
# Check first result (succeeded)
result1 = json.loads(lines[0])
assert result1["response"]["status_code"] == 200
# Check second result (errored)
result2 = json.loads(lines[1])
assert result2["response"]["status_code"] == 429 # rate_limit_error maps to 429
# Check third result (expired)
result3 = json.loads(lines[2])
assert result3["response"]["status_code"] == 400
assert "expired" in result3["response"]["body"]["error"]["message"]
@pytest.mark.asyncio
async def test_afile_content_missing_api_key(self, handler):
"""Test file content retrieval with missing API key"""
file_content_request: FileContentRequest = {
"file_id": "batch_123",
"extra_headers": None,
"extra_body": None
}
with patch.object(handler.anthropic_model_info, "get_api_key", return_value=None):
with pytest.raises(ValueError, match="Missing Anthropic API Key"):
await handler.afile_content(
file_content_request=file_content_request,
api_key=None
)
@pytest.mark.asyncio
async def test_afile_content_missing_file_id(self, handler):
"""Test file content retrieval with missing file_id"""
file_content_request: FileContentRequest = {
"file_id": None,
"extra_headers": None,
"extra_body": None
}
with pytest.raises(ValueError, match="file_id is required"):
await handler.afile_content(
file_content_request=file_content_request,
api_key="test-api-key"
)
@pytest.mark.asyncio
async def test_afile_content_http_error(self, handler):
"""Test file content retrieval with HTTP error"""
file_content_request: FileContentRequest = {
"file_id": "batch_123",
"extra_headers": None,
"extra_body": None
}
mock_response = httpx.Response(
status_code=404,
content=b"Not Found",
request=httpx.Request(method="GET", url="https://api.anthropic.com/v1/messages/batches/batch_123/results")
)
mock_response.raise_for_status = MagicMock(side_effect=httpx.HTTPStatusError("Not Found", request=mock_response.request, response=mock_response))
with patch("litellm.llms.anthropic.files.handler.get_async_httpx_client") as mock_get_client:
mock_client = AsyncMock()
mock_client.get = AsyncMock(return_value=mock_response)
mock_get_client.return_value = mock_client
with patch.object(handler.anthropic_model_info, "get_api_key", return_value="test-api-key"):
with patch.object(handler.anthropic_model_info, "get_api_base", return_value="https://api.anthropic.com"):
with pytest.raises(httpx.HTTPStatusError):
await handler.afile_content(
file_content_request=file_content_request,
api_key="test-api-key"
)
class TestAnthropicBatchesConfig:
"""Test Anthropic Batches Config for batch retrieval transformation"""
@pytest.fixture
def config(self):
"""Create AnthropicBatchesConfig instance"""
return AnthropicBatchesConfig()
@pytest.fixture
def mock_anthropic_batch_response_in_progress(self):
"""Mock Anthropic batch response with in_progress status"""
return {
"id": "batch_123",
"processing_status": "in_progress",
"created_at": "2024-01-01T00:00:00Z",
"expires_at": "2024-01-02T00:00:00Z",
"request_counts": {
"processing": 5,
"succeeded": 3,
"errored": 1,
"canceled": 0,
"expired": 0
}
}
@pytest.fixture
def mock_anthropic_batch_response_completed(self):
"""Mock Anthropic batch response with completed status"""
return {
"id": "batch_456",
"processing_status": "ended",
"created_at": "2024-01-01T00:00:00Z",
"ended_at": "2024-01-01T12:00:00Z",
"expires_at": "2024-01-02T00:00:00Z",
"request_counts": {
"processing": 0,
"succeeded": 10,
"errored": 0,
"canceled": 0,
"expired": 0
}
}
@pytest.fixture
def mock_anthropic_batch_response_canceling(self):
"""Mock Anthropic batch response with canceling status"""
return {
"id": "batch_789",
"processing_status": "canceling",
"created_at": "2024-01-01T00:00:00Z",
"cancel_initiated_at": "2024-01-01T06:00:00Z",
"ended_at": "2024-01-01T07:00:00Z",
"expires_at": "2024-01-02T00:00:00Z",
"request_counts": {
"processing": 0,
"succeeded": 5,
"errored": 0,
"canceled": 3,
"expired": 0
}
}
def test_get_retrieve_batch_url(self, config):
"""Test URL construction for batch retrieval"""
url = config.get_retrieve_batch_url(
api_base="https://api.anthropic.com",
batch_id="batch_123",
optional_params={},
litellm_params={}
)
assert url == "https://api.anthropic.com/v1/messages/batches/batch_123"
# Test with trailing slash
url = config.get_retrieve_batch_url(
api_base="https://api.anthropic.com/",
batch_id="batch_123",
optional_params={},
litellm_params={}
)
assert url == "https://api.anthropic.com/v1/messages/batches/batch_123"
def test_transform_retrieve_batch_response_in_progress(self, config, mock_anthropic_batch_response_in_progress):
"""Test transformation of in_progress batch response"""
mock_response = httpx.Response(
status_code=200,
content=json.dumps(mock_anthropic_batch_response_in_progress).encode("utf-8"),
request=httpx.Request(method="GET", url="https://api.anthropic.com/v1/messages/batches/batch_123")
)
logging_obj = MagicMock()
batch = config.transform_retrieve_batch_response(
model="claude-3-5-sonnet-20241022",
raw_response=mock_response,
logging_obj=logging_obj,
litellm_params={}
)
assert batch.id == "batch_123"
assert batch.object == "batch"
assert batch.status == "in_progress"
assert batch.endpoint == "/v1/messages"
assert batch.output_file_id == "batch_123"
assert batch.request_counts.total == 9 # 5 + 3 + 1
assert batch.request_counts.completed == 3
assert batch.request_counts.failed == 1
assert batch.in_progress_at is not None
assert batch.completed_at is None
def test_transform_retrieve_batch_response_completed(self, config, mock_anthropic_batch_response_completed):
"""Test transformation of completed batch response"""
mock_response = httpx.Response(
status_code=200,
content=json.dumps(mock_anthropic_batch_response_completed).encode("utf-8"),
request=httpx.Request(method="GET", url="https://api.anthropic.com/v1/messages/batches/batch_456")
)
logging_obj = MagicMock()
batch = config.transform_retrieve_batch_response(
model="claude-3-5-sonnet-20241022",
raw_response=mock_response,
logging_obj=logging_obj,
litellm_params={}
)
assert batch.id == "batch_456"
assert batch.status == "completed"
assert batch.completed_at is not None
assert batch.request_counts.total == 10
assert batch.request_counts.completed == 10
assert batch.request_counts.failed == 0
def test_transform_retrieve_batch_response_canceling(self, config, mock_anthropic_batch_response_canceling):
"""Test transformation of canceling batch response"""
mock_response = httpx.Response(
status_code=200,
content=json.dumps(mock_anthropic_batch_response_canceling).encode("utf-8"),
request=httpx.Request(method="GET", url="https://api.anthropic.com/v1/messages/batches/batch_789")
)
logging_obj = MagicMock()
batch = config.transform_retrieve_batch_response(
model="claude-3-5-sonnet-20241022",
raw_response=mock_response,
logging_obj=logging_obj,
litellm_params={}
)
assert batch.id == "batch_789"
assert batch.status == "cancelling"
assert batch.cancelling_at is not None
assert batch.cancelled_at is not None
assert batch.request_counts.total == 8 # 5 + 3
def test_transform_retrieve_batch_response_invalid_json(self, config):
"""Test transformation with invalid JSON response"""
mock_response = httpx.Response(
status_code=200,
content=b"invalid json",
request=httpx.Request(method="GET", url="https://api.anthropic.com/v1/messages/batches/batch_123")
)
logging_obj = MagicMock()
with pytest.raises(ValueError, match="Failed to parse Anthropic batch response"):
config.transform_retrieve_batch_response(
model="claude-3-5-sonnet-20241022",
raw_response=mock_response,
logging_obj=logging_obj,
litellm_params={}
)
def test_transform_retrieve_batch_response_timestamp_parsing(self, config):
"""Test timestamp parsing in batch response"""
batch_data = {
"id": "batch_123",
"processing_status": "ended",
"created_at": "2024-01-01T12:00:00Z",
"ended_at": "2024-01-01T13:30:45Z",
"expires_at": "2024-01-02T12:00:00Z",
"archived_at": "2024-01-03T00:00:00Z",
"request_counts": {
"processing": 0,
"succeeded": 1,
"errored": 0,
"canceled": 0,
"expired": 0
}
}
mock_response = httpx.Response(
status_code=200,
content=json.dumps(batch_data).encode("utf-8"),
request=httpx.Request(method="GET", url="https://api.anthropic.com/v1/messages/batches/batch_123")
)
logging_obj = MagicMock()
batch = config.transform_retrieve_batch_response(
model="claude-3-5-sonnet-20241022",
raw_response=mock_response,
logging_obj=logging_obj,
litellm_params={}
)
# Verify timestamps are parsed correctly
assert batch.created_at is not None
assert batch.completed_at is not None
assert batch.expires_at is not None
assert batch.expired_at is not None
# Verify timestamps are integers (Unix timestamps)
assert isinstance(batch.created_at, int)
assert isinstance(batch.completed_at, int)
assert isinstance(batch.expires_at, int)
assert isinstance(batch.expired_at, int)
def test_transform_retrieve_batch_response_missing_fields(self, config):
"""Test transformation with missing optional fields"""
batch_data = {
"id": "batch_123",
"processing_status": "in_progress",
"request_counts": {
"processing": 1,
"succeeded": 0,
"errored": 0,
"canceled": 0,
"expired": 0
}
}
mock_response = httpx.Response(
status_code=200,
content=json.dumps(batch_data).encode("utf-8"),
request=httpx.Request(method="GET", url="https://api.anthropic.com/v1/messages/batches/batch_123")
)
logging_obj = MagicMock()
batch = config.transform_retrieve_batch_response(
model="claude-3-5-sonnet-20241022",
raw_response=mock_response,
logging_obj=logging_obj,
litellm_params={}
)
# Should still work with missing optional fields
assert batch.id == "batch_123"
assert batch.status == "in_progress"
assert batch.created_at is not None # Should default to current time if missing
assert batch.expires_at is None
assert batch.completed_at is None