mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-27 01:22:18 +00:00
283 lines
9.6 KiB
Python
283 lines
9.6 KiB
Python
"""
|
|
Unit tests for litellm/llms/anthropic/batches/handler.py
|
|
|
|
AnthropicBatchesHandler is the HTTP/auth glue for retrieving Anthropic Message
|
|
Batches. It resolves credentials, builds the retrieve URL + auth headers via the
|
|
provider config, fires a single GET against the async httpx client, and hands the
|
|
response to the config's transform. These tests mock ONLY the genuine I/O seams -
|
|
the async httpx client (network) and credential resolution (secret managers /
|
|
env) - and assert exactly which seam fired, with what URL/headers, and that the
|
|
parsed result is the LiteLLMBatch the transform produced.
|
|
|
|
The sync ``retrieve_batch`` dispatch (``_is_async`` true -> coroutine, false ->
|
|
asyncio.run) is exercised directly, mirroring the dispatch-contract discipline in
|
|
tests/test_litellm/batches/test_main.py.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
|
|
from litellm.llms.anthropic.batches.handler import AnthropicBatchesHandler
|
|
from litellm.types.utils import LiteLLMBatch
|
|
|
|
|
|
def _ok_batch_response():
|
|
"""A real httpx.Response shaped like an Anthropic MessageBatch retrieval."""
|
|
return httpx.Response(
|
|
status_code=200,
|
|
json={
|
|
"id": "msgbatch_abc",
|
|
"processing_status": "ended",
|
|
"created_at": "2024-09-24T10:00:00Z",
|
|
"ended_at": "2024-09-24T11:00:00Z",
|
|
"request_counts": {"succeeded": 2, "errored": 0},
|
|
},
|
|
request=httpx.Request(
|
|
"GET", "https://api.anthropic.com/v1/messages/batches/msgbatch_abc"
|
|
),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def handler():
|
|
return AnthropicBatchesHandler()
|
|
|
|
|
|
@pytest.fixture
|
|
def patched_client():
|
|
"""Patch the async httpx client seam; yield the (fake_client, factory)."""
|
|
fake_client = MagicMock()
|
|
fake_client.get = AsyncMock(return_value=_ok_batch_response())
|
|
with patch(
|
|
"litellm.llms.anthropic.batches.handler.get_async_httpx_client",
|
|
return_value=fake_client,
|
|
) as factory:
|
|
yield fake_client, factory
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_aretrieve_batch_fires_get_with_correct_url_and_headers(
|
|
handler, patched_client
|
|
):
|
|
fake_client, factory = patched_client
|
|
|
|
batch = await handler.aretrieve_batch(
|
|
batch_id="msgbatch_abc",
|
|
api_base="https://api.anthropic.com",
|
|
api_key="sk-ant-test",
|
|
timeout=60.0,
|
|
max_retries=0,
|
|
)
|
|
|
|
# The single network seam fired exactly once.
|
|
fake_client.get.assert_awaited_once()
|
|
_, call_kwargs = fake_client.get.call_args
|
|
# Exact URL built by get_retrieve_batch_url.
|
|
assert call_kwargs["url"] == (
|
|
"https://api.anthropic.com/v1/messages/batches/msgbatch_abc"
|
|
)
|
|
# Auth + version + beta headers built by validate_environment.
|
|
headers = call_kwargs["headers"]
|
|
assert headers["x-api-key"] == "sk-ant-test"
|
|
assert headers["anthropic-version"] == "2023-06-01"
|
|
assert headers["anthropic-beta"] == "message-batches-2024-09-24"
|
|
|
|
# Response parsed through the config transform.
|
|
assert isinstance(batch, LiteLLMBatch)
|
|
assert batch.id == "msgbatch_abc"
|
|
assert batch.status == "completed"
|
|
assert batch.request_counts.completed == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_aretrieve_batch_uses_anthropic_provider_for_client(
|
|
handler, patched_client
|
|
):
|
|
from litellm.types.utils import LlmProviders
|
|
|
|
_, factory = patched_client
|
|
await handler.aretrieve_batch(
|
|
batch_id="msgbatch_abc",
|
|
api_base="https://api.anthropic.com",
|
|
api_key="sk-ant-test",
|
|
timeout=60.0,
|
|
max_retries=0,
|
|
)
|
|
_, kwargs = factory.call_args
|
|
assert kwargs["llm_provider"] == LlmProviders.ANTHROPIC
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_aretrieve_batch_resolves_api_key_from_model_info(
|
|
handler, patched_client
|
|
):
|
|
fake_client, _ = patched_client
|
|
# api_key=None -> handler falls back to AnthropicModelInfo.get_api_key().
|
|
with patch.object(
|
|
handler.anthropic_model_info, "get_api_key", return_value="sk-from-env"
|
|
):
|
|
await handler.aretrieve_batch(
|
|
batch_id="msgbatch_abc",
|
|
api_base="https://api.anthropic.com",
|
|
api_key=None,
|
|
timeout=60.0,
|
|
max_retries=0,
|
|
)
|
|
_, call_kwargs = fake_client.get.call_args
|
|
assert call_kwargs["headers"]["x-api-key"] == "sk-from-env"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_aretrieve_batch_missing_api_key_raises(handler, patched_client):
|
|
fake_client, _ = patched_client
|
|
# No api_key and resolver yields None -> hard error before any network call.
|
|
with patch.object(
|
|
handler.anthropic_model_info, "get_api_key", return_value=None
|
|
):
|
|
with pytest.raises(ValueError, match="Missing Anthropic API Key"):
|
|
await handler.aretrieve_batch(
|
|
batch_id="msgbatch_abc",
|
|
api_base="https://api.anthropic.com",
|
|
api_key=None,
|
|
timeout=60.0,
|
|
max_retries=0,
|
|
)
|
|
fake_client.get.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_aretrieve_batch_resolves_default_api_base(handler, patched_client):
|
|
fake_client, _ = patched_client
|
|
# api_base=None -> resolved via get_api_base() default before URL build.
|
|
with patch.object(
|
|
handler.anthropic_model_info,
|
|
"get_api_base",
|
|
return_value="https://api.anthropic.com",
|
|
):
|
|
await handler.aretrieve_batch(
|
|
batch_id="msgbatch_abc",
|
|
api_base=None,
|
|
api_key="sk-ant-test",
|
|
timeout=60.0,
|
|
max_retries=0,
|
|
)
|
|
_, call_kwargs = fake_client.get.call_args
|
|
assert call_kwargs["url"] == (
|
|
"https://api.anthropic.com/v1/messages/batches/msgbatch_abc"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_aretrieve_batch_raises_for_status(handler):
|
|
# A non-2xx response must surface via raise_for_status (no silent parse).
|
|
error_response = httpx.Response(
|
|
status_code=404,
|
|
json={"error": "not found"},
|
|
request=httpx.Request(
|
|
"GET", "https://api.anthropic.com/v1/messages/batches/missing"
|
|
),
|
|
)
|
|
fake_client = MagicMock()
|
|
fake_client.get = AsyncMock(return_value=error_response)
|
|
with patch(
|
|
"litellm.llms.anthropic.batches.handler.get_async_httpx_client",
|
|
return_value=fake_client,
|
|
):
|
|
with pytest.raises(httpx.HTTPStatusError):
|
|
await handler.aretrieve_batch(
|
|
batch_id="missing",
|
|
api_base="https://api.anthropic.com",
|
|
api_key="sk-ant-test",
|
|
timeout=60.0,
|
|
max_retries=0,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_aretrieve_batch_invokes_pre_call_logging(handler, patched_client):
|
|
fake_client, _ = patched_client
|
|
logging_obj = MagicMock()
|
|
await handler.aretrieve_batch(
|
|
batch_id="msgbatch_abc",
|
|
api_base="https://api.anthropic.com",
|
|
api_key="sk-ant-test",
|
|
timeout=60.0,
|
|
max_retries=0,
|
|
logging_obj=logging_obj,
|
|
)
|
|
logging_obj.pre_call.assert_called_once()
|
|
pre_kwargs = logging_obj.pre_call.call_args.kwargs
|
|
assert pre_kwargs["input"] == "msgbatch_abc"
|
|
assert pre_kwargs["api_key"] == "sk-ant-test"
|
|
# The logged api_base is the full retrieve URL, not the bare base.
|
|
assert pre_kwargs["additional_args"]["api_base"] == (
|
|
"https://api.anthropic.com/v1/messages/batches/msgbatch_abc"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_aretrieve_batch_builds_default_logging_obj_when_absent(
|
|
handler, patched_client
|
|
):
|
|
# logging_obj=None -> handler constructs a real Logging object; the call
|
|
# must still complete (no AttributeError on a missing logger).
|
|
_, _ = patched_client
|
|
with patch(
|
|
"litellm.litellm_core_utils.litellm_logging.Logging"
|
|
) as logging_cls:
|
|
logging_cls.return_value = MagicMock()
|
|
batch = await handler.aretrieve_batch(
|
|
batch_id="msgbatch_abc",
|
|
api_base="https://api.anthropic.com",
|
|
api_key="sk-ant-test",
|
|
timeout=60.0,
|
|
max_retries=0,
|
|
logging_obj=None,
|
|
)
|
|
logging_cls.assert_called_once()
|
|
# call_type wires through to the constructed logging object.
|
|
assert logging_cls.call_args.kwargs["call_type"] == "batch_retrieve"
|
|
assert batch.id == "msgbatch_abc"
|
|
|
|
|
|
# =========================================================================== #
|
|
# retrieve_batch dispatch (sync wrapper)
|
|
# =========================================================================== #
|
|
|
|
|
|
async def test_retrieve_batch_async_returns_coroutine(handler, patched_client):
|
|
# _is_async=True -> returns the un-awaited coroutine (caller awaits it).
|
|
import asyncio
|
|
|
|
coro = handler.retrieve_batch(
|
|
_is_async=True,
|
|
batch_id="msgbatch_abc",
|
|
api_base="https://api.anthropic.com",
|
|
api_key="sk-ant-test",
|
|
timeout=60.0,
|
|
max_retries=0,
|
|
)
|
|
assert asyncio.iscoroutine(coro)
|
|
# Await directly - robust under asyncio_mode=auto's session-scoped loop
|
|
# (manually driving get_event_loop().run_until_complete() breaks when prior
|
|
# async tests in the suite have already used/closed that loop).
|
|
batch = await coro
|
|
assert batch.id == "msgbatch_abc"
|
|
|
|
|
|
def test_retrieve_batch_sync_runs_to_result(handler, patched_client):
|
|
# _is_async=False -> asyncio.run(...) returns the resolved LiteLLMBatch.
|
|
batch = handler.retrieve_batch(
|
|
_is_async=False,
|
|
batch_id="msgbatch_abc",
|
|
api_base="https://api.anthropic.com",
|
|
api_key="sk-ant-test",
|
|
timeout=60.0,
|
|
max_retries=0,
|
|
)
|
|
assert isinstance(batch, LiteLLMBatch)
|
|
assert batch.id == "msgbatch_abc"
|
|
assert batch.status == "completed"
|