"""Unit tests for ``AzureBatchesAPI`` (litellm/llms/azure/batches/handler.py). The Azure batches handler is HTTP/auth glue: each public method (create/retrieve/cancel/list) resolves an Azure OpenAI client via the inherited ``get_azure_openai_client`` seam, branches on ``_is_async`` (returning the ``a*`` coroutine in the async case, calling the sync client otherwise), validates the client type, and parses the SDK response into ``LiteLLMBatch``. We mock only true boundaries: * ``get_azure_openai_client`` - the credential/client-construction seam. We assert the EXACT auth args (api_key / api_base / api_version / client / _is_async / litellm_params) forwarded to it. * the returned Azure OpenAI client's ``batches.*`` methods - the network call. We assert the request data forwarded and that the SDK response is parsed into ``LiteLLMBatch`` (sibling SDK methods asserted NOT called). Pure logic (the _is_async branch, the isinstance guards, the model_dump parse) runs for real. """ from __future__ import annotations import asyncio from unittest.mock import AsyncMock, MagicMock, patch import pytest from openai import AsyncOpenAI, OpenAI # noqa: E402 from litellm.llms.azure.azure import AsyncAzureOpenAI, AzureOpenAI # noqa: E402 from litellm.llms.azure.batches.handler import AzureBatchesAPI # noqa: E402 from litellm.types.utils import LiteLLMBatch # noqa: E402 GET_CLIENT = "litellm.llms.azure.batches.handler.AzureBatchesAPI.get_azure_openai_client" AUTH_KW = dict( api_key="sk-azure-test", api_base="https://my-azure.openai.azure.com", api_version="2024-12-01", timeout=600.0, max_retries=3, ) CREATE_DATA = { "completion_window": "24h", "endpoint": "/v1/chat/completions", "input_file_id": "file-abc", } RETRIEVE_DATA = {"batch_id": "batch-123"} CANCEL_DATA = {"batch_id": "batch-123"} def _batch_dict(batch_id: str = "batch-123", status: str = "completed") -> dict: """A minimal-but-valid dict for ``LiteLLMBatch(**response.model_dump())``.""" return { "id": batch_id, "completion_window": "24h", "created_at": 1700000000, "endpoint": "/v1/chat/completions", "input_file_id": "file-abc", "object": "batch", "status": status, "output_file_id": "file-out-xyz", } def _sdk_response(batch_dict: dict) -> MagicMock: """An object that mimics the OpenAI SDK Batch: only ``.model_dump()`` is used.""" resp = MagicMock() resp.model_dump.return_value = batch_dict return resp def _sync_client() -> MagicMock: """A sync Azure client (passes ``isinstance(.., AzureOpenAI)``).""" return MagicMock(spec=AzureOpenAI) def _async_client() -> MagicMock: """An async Azure client (passes ``isinstance(.., AsyncAzureOpenAI)``). The ``batches.*`` SDK methods are awaited by the handler, so they must be AsyncMocks. """ client = MagicMock(spec=AsyncAzureOpenAI) client.batches.create = AsyncMock() client.batches.retrieve = AsyncMock() client.batches.cancel = AsyncMock() client.batches.list = AsyncMock() return client @pytest.fixture def handler() -> AzureBatchesAPI: return AzureBatchesAPI() # =========================================================================== # # create_batch - sync path # =========================================================================== # def test_create_sync_forwards_auth_to_client_seam(handler): client = _sync_client() client.batches.create.return_value = _sdk_response(_batch_dict()) with patch(GET_CLIENT, return_value=client) as get_client: result = handler.create_batch( _is_async=False, create_batch_data=CREATE_DATA, **AUTH_KW ) # EXACT auth args forwarded to the client-construction seam. assert get_client.call_count == 1 kw = get_client.call_args.kwargs assert kw["api_key"] == "sk-azure-test" assert kw["api_base"] == "https://my-azure.openai.azure.com" assert kw["api_version"] == "2024-12-01" assert kw["_is_async"] is False assert kw["client"] is None # litellm_params defaults to {} (not None) when not supplied. assert kw["litellm_params"] == {} # PAYLOAD: request data forwarded verbatim to the SDK as kwargs. client.batches.create.assert_called_once_with(**CREATE_DATA) # sibling SDK seams untouched. client.batches.retrieve.assert_not_called() client.batches.cancel.assert_not_called() # RESULT: parsed into LiteLLMBatch from the SDK response's model_dump. assert isinstance(result, LiteLLMBatch) assert result.id == "batch-123" assert result.status == "completed" assert result.output_file_id == "file-out-xyz" def test_create_sync_passes_litellm_params_through(handler): client = _sync_client() client.batches.create.return_value = _sdk_response(_batch_dict()) lp = {"azure_ad_token": "tok", "tenant_id": "t1"} with patch(GET_CLIENT, return_value=client) as get_client: handler.create_batch( _is_async=False, create_batch_data=CREATE_DATA, litellm_params=lp, **AUTH_KW, ) assert get_client.call_args.kwargs["litellm_params"] == lp def test_create_sync_explicit_client_forwarded_to_seam(handler): sentinel_client = _sync_client() sentinel_client.batches.create.return_value = _sdk_response(_batch_dict()) with patch(GET_CLIENT, return_value=sentinel_client) as get_client: handler.create_batch( _is_async=False, create_batch_data=CREATE_DATA, client=sentinel_client, **AUTH_KW, ) assert get_client.call_args.kwargs["client"] is sentinel_client def test_create_raises_when_client_is_none(handler): with patch(GET_CLIENT, return_value=None): with pytest.raises(ValueError, match="client is not initialized"): handler.create_batch( _is_async=False, create_batch_data=CREATE_DATA, **AUTH_KW ) # =========================================================================== # # create_batch - async path # =========================================================================== # @pytest.mark.asyncio async def test_create_async_returns_coroutine_and_awaits_async_client(handler): client = _async_client() client.batches.create.return_value = _sdk_response(_batch_dict()) with patch(GET_CLIENT, return_value=client) as get_client: coro = handler.create_batch( _is_async=True, create_batch_data=CREATE_DATA, **AUTH_KW ) assert asyncio.iscoroutine(coro) result = await coro assert get_client.call_args.kwargs["_is_async"] is True client.batches.create.assert_awaited_once_with(**CREATE_DATA) assert isinstance(result, LiteLLMBatch) assert result.id == "batch-123" @pytest.mark.asyncio async def test_create_async_rejects_sync_client(handler): """_is_async=True but seam returns a sync client -> ValueError, no network.""" sync_client = _sync_client() with patch(GET_CLIENT, return_value=sync_client): with pytest.raises(ValueError, match="not an instance of AsyncOpenAI"): handler.create_batch( _is_async=True, create_batch_data=CREATE_DATA, **AUTH_KW ) sync_client.batches.create.assert_not_called() @pytest.mark.asyncio async def test_acreate_batch_parses_response(handler): client = _async_client() client.batches.create.return_value = _sdk_response(_batch_dict(status="validating")) result = await handler.acreate_batch( create_batch_data=CREATE_DATA, azure_client=client ) client.batches.create.assert_awaited_once_with(**CREATE_DATA) assert isinstance(result, LiteLLMBatch) assert result.status == "validating" # =========================================================================== # # retrieve_batch # =========================================================================== # def test_retrieve_sync_dispatch_payload_and_result(handler): client = _sync_client() client.batches.retrieve.return_value = _sdk_response(_batch_dict()) with patch(GET_CLIENT, return_value=client) as get_client: result = handler.retrieve_batch( _is_async=False, retrieve_batch_data=RETRIEVE_DATA, **AUTH_KW ) assert get_client.call_args.kwargs["_is_async"] is False client.batches.retrieve.assert_called_once_with(**RETRIEVE_DATA) client.batches.create.assert_not_called() client.batches.cancel.assert_not_called() assert isinstance(result, LiteLLMBatch) assert result.id == "batch-123" def test_retrieve_raises_when_client_is_none(handler): with patch(GET_CLIENT, return_value=None): with pytest.raises(ValueError, match="client is not initialized"): handler.retrieve_batch( _is_async=False, retrieve_batch_data=RETRIEVE_DATA, **AUTH_KW ) @pytest.mark.asyncio async def test_retrieve_async_returns_coroutine_and_awaits(handler): client = _async_client() client.batches.retrieve.return_value = _sdk_response(_batch_dict()) with patch(GET_CLIENT, return_value=client) as get_client: coro = handler.retrieve_batch( _is_async=True, retrieve_batch_data=RETRIEVE_DATA, **AUTH_KW ) assert asyncio.iscoroutine(coro) result = await coro assert get_client.call_args.kwargs["_is_async"] is True client.batches.retrieve.assert_awaited_once_with(**RETRIEVE_DATA) assert isinstance(result, LiteLLMBatch) @pytest.mark.asyncio async def test_retrieve_async_rejects_sync_client(handler): sync_client = _sync_client() with patch(GET_CLIENT, return_value=sync_client): with pytest.raises(ValueError, match="not an instance of AsyncOpenAI"): handler.retrieve_batch( _is_async=True, retrieve_batch_data=RETRIEVE_DATA, **AUTH_KW ) sync_client.batches.retrieve.assert_not_called() @pytest.mark.asyncio async def test_aretrieve_batch_parses_response(handler): client = _async_client() client.batches.retrieve.return_value = _sdk_response(_batch_dict()) result = await handler.aretrieve_batch( retrieve_batch_data=RETRIEVE_DATA, client=client ) client.batches.retrieve.assert_awaited_once_with(**RETRIEVE_DATA) assert isinstance(result, LiteLLMBatch) # =========================================================================== # # cancel_batch (has an EXTRA sync-side isinstance guard the others lack) # =========================================================================== # def test_cancel_sync_dispatch_payload_and_result(handler): client = _sync_client() client.batches.cancel.return_value = _sdk_response(_batch_dict(status="cancelled")) with patch(GET_CLIENT, return_value=client) as get_client: result = handler.cancel_batch( _is_async=False, cancel_batch_data=CANCEL_DATA, **AUTH_KW ) assert get_client.call_args.kwargs["_is_async"] is False client.batches.cancel.assert_called_once_with(**CANCEL_DATA) client.batches.create.assert_not_called() client.batches.retrieve.assert_not_called() assert isinstance(result, LiteLLMBatch) assert result.status == "cancelled" def test_cancel_raises_when_client_is_none(handler): with patch(GET_CLIENT, return_value=None): with pytest.raises(ValueError, match="client is not initialized"): handler.cancel_batch( _is_async=False, cancel_batch_data=CANCEL_DATA, **AUTH_KW ) def test_cancel_sync_rejects_non_sync_client(handler): """cancel_batch has a unique sync-side guard: if _is_async is False but the resolved client is async (neither AzureOpenAI nor OpenAI), it must raise rather than call .cancel().""" async_client = _async_client() with patch(GET_CLIENT, return_value=async_client): with pytest.raises(ValueError, match="sync client"): handler.cancel_batch( _is_async=False, cancel_batch_data=CANCEL_DATA, **AUTH_KW ) async_client.batches.cancel.assert_not_called() @pytest.mark.asyncio async def test_cancel_async_returns_coroutine_and_awaits(handler): client = _async_client() client.batches.cancel.return_value = _sdk_response(_batch_dict(status="cancelled")) with patch(GET_CLIENT, return_value=client) as get_client: coro = handler.cancel_batch( _is_async=True, cancel_batch_data=CANCEL_DATA, **AUTH_KW ) assert asyncio.iscoroutine(coro) result = await coro assert get_client.call_args.kwargs["_is_async"] is True client.batches.cancel.assert_awaited_once_with(**CANCEL_DATA) assert isinstance(result, LiteLLMBatch) assert result.status == "cancelled" @pytest.mark.asyncio async def test_cancel_async_rejects_sync_client(handler): sync_client = _sync_client() with patch(GET_CLIENT, return_value=sync_client): with pytest.raises(ValueError, match="async client"): handler.cancel_batch( _is_async=True, cancel_batch_data=CANCEL_DATA, **AUTH_KW ) sync_client.batches.cancel.assert_not_called() @pytest.mark.asyncio async def test_acancel_batch_parses_response(handler): client = _async_client() client.batches.cancel.return_value = _sdk_response(_batch_dict(status="cancelled")) result = await handler.acancel_batch(cancel_batch_data=CANCEL_DATA, client=client) client.batches.cancel.assert_awaited_once_with(**CANCEL_DATA) assert isinstance(result, LiteLLMBatch) assert result.status == "cancelled" # =========================================================================== # # list_batches (returns the raw SDK response, NOT a LiteLLMBatch) # =========================================================================== # def test_list_sync_forwards_after_limit_and_returns_raw_response(handler): client = _sync_client() raw = MagicMock(name="raw_list_response") client.batches.list.return_value = raw with patch(GET_CLIENT, return_value=client) as get_client: result = handler.list_batches( _is_async=False, after="cur-1", limit=20, **AUTH_KW ) assert get_client.call_args.kwargs["_is_async"] is False client.batches.list.assert_called_once_with(after="cur-1", limit=20) # list returns the SDK response untouched (no LiteLLMBatch parsing). assert result is raw def test_list_sync_defaults_after_and_limit_to_none(handler): client = _sync_client() client.batches.list.return_value = MagicMock() with patch(GET_CLIENT, return_value=client): handler.list_batches(_is_async=False, **AUTH_KW) client.batches.list.assert_called_once_with(after=None, limit=None) def test_list_raises_when_client_is_none(handler): with patch(GET_CLIENT, return_value=None): with pytest.raises(ValueError, match="client is not initialized"): handler.list_batches(_is_async=False, **AUTH_KW) @pytest.mark.asyncio async def test_list_async_returns_coroutine_and_awaits(handler): client = _async_client() raw = MagicMock(name="raw_async_list_response") client.batches.list.return_value = raw with patch(GET_CLIENT, return_value=client) as get_client: coro = handler.list_batches( _is_async=True, after="cur-2", limit=7, **AUTH_KW ) assert asyncio.iscoroutine(coro) result = await coro assert get_client.call_args.kwargs["_is_async"] is True client.batches.list.assert_awaited_once_with(after="cur-2", limit=7) assert result is raw @pytest.mark.asyncio async def test_list_async_rejects_sync_client(handler): sync_client = _sync_client() with patch(GET_CLIENT, return_value=sync_client): with pytest.raises(ValueError, match="not an instance of AsyncOpenAI"): handler.list_batches(_is_async=True, **AUTH_KW) sync_client.batches.list.assert_not_called() @pytest.mark.asyncio async def test_alist_batches_returns_raw_response(handler): client = _async_client() raw = MagicMock(name="raw") client.batches.list.return_value = raw result = await handler.alist_batches(client=client, after="a", limit=2) client.batches.list.assert_awaited_once_with(after="a", limit=2) assert result is raw # =========================================================================== # # Cross-cutting: an OpenAI (non-Azure) client also satisfies the type guards, # since the Union allows OpenAI / AsyncOpenAI (Azure-v1 path returns these). # =========================================================================== # def test_create_sync_accepts_plain_openai_client(handler): client = MagicMock(spec=OpenAI) client.batches.create.return_value = _sdk_response(_batch_dict()) with patch(GET_CLIENT, return_value=client): result = handler.create_batch( _is_async=False, create_batch_data=CREATE_DATA, **AUTH_KW ) assert isinstance(result, LiteLLMBatch) @pytest.mark.asyncio async def test_create_async_accepts_plain_async_openai_client(handler): client = MagicMock(spec=AsyncOpenAI) client.batches.create = AsyncMock(return_value=_sdk_response(_batch_dict())) with patch(GET_CLIENT, return_value=client): result = await handler.create_batch( _is_async=True, create_batch_data=CREATE_DATA, **AUTH_KW ) assert isinstance(result, LiteLLMBatch)