""" 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"