litellm/tests/unit/batch_completion/test_main.py
devin-ai-integration[bot] 8a103e26ab
test: move routing, caching and callback tests out of local_testing (#45761)
* test: move routing, caching and callback tests out of local_testing

Moves the 37 routing, caching and callback files in tests/local_testing into mirrored tests/unit paths and tests/integration sdk and security groups per the migration manifest, and deletes the 8 rows it marks for deletion

* test: cover least-busy routing and tighten migrated router, cache and pass-through tests

Add router-level least-busy tests for the three legacy rows that had no
destination, move completion fallback and model alias tests to their mirror
files, move router batch tests next to the router, and strengthen tests
whose mutants survived (specific deployment, batch pair grouping, custom
cache key, router cache ttl). Add a unit test for configured pass-through
Authorization forwarding.

* test: tighten migrated callback, proxy config and cache ttl tests

Assert the full StandardLoggingPayload key set and the wire user on callback
tests, add an async embedding failure callback test, drive the prometheus
redis failure through a real RedisCache, use the real salt key in proxy
config tests, pin the redis ttl value and rename fallback test helpers

* test: fix semantic cache assertions and order-dependent router and callback tests

Count semantic cache embeddings on the route, drain async cache writes before
the second lookup, echo the requested model from the callback provider mock so
response_cost does not depend on model_cost entries other tests register, and
move the two-key authorization assertion to the auth failure test it belongs to

* test: cover model_list fan-out and the usage-based routing tpm limit lost in the move

* test: wait for the streamed cache entry in redis and drop deleted files from the hosted mock guard

* test: restore sync callback, audio redaction and langfuse export coverage, keep redis-compat filter on the moved file

* ci: run the remaining router-keyword legacy tests on one node

* test: refuse the redis write in-process instead of resolving an invalid host

---------

Co-authored-by: yuneng <yuneng@berri.ai>
2026-10-10 09:33:09 -07:00

166 lines
5.7 KiB
Python

import json
from typing import Final, cast
import httpx
import pytest
import respx
import litellm
from litellm.types.utils import ModelResponse
msg1 = [{"role": "user", "content": "hi 1"}]
msg2 = [{"role": "user", "content": "hi 2"}]
def test_batch_completion_return_exceptions_true(respx_mock: respx.MockRouter):
"""Test batch_completion's return_exceptions.
With an invalid API key, we expect an error to be returned rather than raised.
The error type may be AuthenticationError (from API) or InternalServerError
(from connection issues), depending on network conditions.
"""
respx_mock.post("https://api.openai.com/v1/chat/completions").mock(
return_value=httpx.Response(
401,
json={
"error": {
"message": "Incorrect API key provided: sk_xxx.",
"type": "invalid_request_error",
"code": "invalid_api_key",
}
},
)
)
res = litellm.batch_completion(
model="gpt-3.5-turbo",
messages=[msg1, msg2],
api_key="sk_xxx",
)
assert isinstance(
res[0],
(
litellm.exceptions.AuthenticationError,
litellm.exceptions.InternalServerError,
),
), f"Expected AuthenticationError or InternalServerError, got {type(res[0])}"
def test_batch_completion_returns_one_response_per_message(
respx_mock: respx.MockRouter,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
route: Final = respx_mock.post("https://api.openai.com/v1/chat/completions").mock(
return_value=httpx.Response(
200,
json={
"id": "chatcmpl-batch",
"object": "chat.completion",
"created": 1,
"model": "gpt-4o-mini",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "batch answer"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
},
)
)
responses: Final[list[ModelResponse]] = cast(
list[ModelResponse],
litellm.batch_completion(
model="openai/gpt-4o-mini",
messages=[
[{"role": "user", "content": "first prompt"}],
[{"role": "user", "content": "second prompt"}],
],
api_key="test-key",
max_workers=1,
),
)
request_bodies: Final = tuple(json.loads(call.request.content) for call in route.calls)
assert route.call_count == 2
assert tuple(body["messages"][0]["content"] for body in request_bodies) == (
"first prompt",
"second prompt",
)
assert tuple(response.choices[0].message.content for response in responses) == (
"batch answer",
"batch answer",
)
def _chat_reply(content: str) -> httpx.Response:
return httpx.Response(
200,
json={
"id": "chatcmpl-deployment",
"object": "chat.completion",
"created": 1,
"model": "gpt-4o-mini",
"choices": [{"index": 0, "message": {"role": "assistant", "content": content}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
},
)
def test_completion_with_model_list_returns_the_first_deployment_that_succeeds(
respx_mock: respx.MockRouter,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
failing: Final = respx_mock.post("https://failing.deployment.test/v1/chat/completions").mock(
return_value=httpx.Response(401, json={"error": {"message": "bad key", "type": "invalid_request_error"}})
)
healthy: Final = respx_mock.post("https://healthy.deployment.test/v1/chat/completions").mock(
return_value=_chat_reply("from the healthy deployment")
)
other_group: Final = respx_mock.post("https://other-group.deployment.test/v1/chat/completions").mock(
return_value=_chat_reply("from another model group")
)
model_list: Final = [
{
"model_name": "mistral-7b-instruct",
"litellm_params": {
"model": "openai/failing-model",
"api_base": "https://failing.deployment.test/v1",
"api_key": "failing-key",
},
},
{
"model_name": "mistral-7b-instruct",
"litellm_params": {
"model": "openai/healthy-model",
"api_base": "https://healthy.deployment.test/v1",
"api_key": "healthy-key",
},
},
{
"model_name": "another-group",
"litellm_params": {
"model": "openai/other-model",
"api_base": "https://other-group.deployment.test/v1",
"api_key": "other-key",
},
},
]
response: Final = cast(
ModelResponse,
litellm.completion(
model="mistral-7b-instruct",
messages=[{"role": "user", "content": "Hey, how's it going?"}],
model_list=model_list,
max_retries=0,
),
)
assert response.choices[0].message.content == "from the healthy deployment"
assert (failing.call_count, healthy.call_count, other_group.call_count) == (1, 1, 0)
assert json.loads(healthy.calls[0].request.content)["model"] == "healthy-model"
assert healthy.calls[0].request.headers["authorization"] == "Bearer healthy-key"