litellm/tests/test_litellm/llms/anthropic/batches/test_handler.py
yuneng-jiang 6a0d03914c
test: drop the cwd-relative sys.path.insert calls from the test suite (#37802)
* test: drop the cwd-relative sys.path.insert calls from the test suite

TQ003 stands at 1,077 across 1,058 files, and 1,015 of them are the same shape:
sys.path.insert(0, os.path.abspath("../..")) and its deeper siblings. The
argument resolves against the working directory rather than the file, so from
the repo root, where every job runs pytest, it inserts the directory two levels
above the checkout. It has never pointed at litellm. The package is installed
into the environment anyway, which is what actually makes the import work, and
what the rule's message has said all along.

Removing them leaves 1,634 imports of sys and os with no remaining reference,
and those go too, except where another test module imports the name back out of
the file. The rest of TQ003 is 62 call sites that resolve against __file__ or a
variable, which are a different question and are left alone.

Collection is identical either way: 45,871 tests and the same 51 pre-existing
collection errors before and after, and ruff reports no new undefined name.

* test: drop the duplicate imports the sys.path sweep exposed to F811

* test(pre-call-utils): restore the os import the new bedrock tests need
2026-08-22 09:25:58 -07:00

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"