litellm/tests/unit/llms/custom_httpx/test_container_handler.py
Mateo Wang 8b9cd12a89
test: catch regressions in the validation lines #44798 added (#47420)
* test: pin the validation lines #44798 kept with failing cases

* fix(containers): accept endpoint table entries that omit their param lists

* test: keep the endpoint table and xAI adapter as on main

The endpoint table ships inside the package, so optional param lists fixed no
reachable state and turned a loud failure into silently dropped params. Drops
the Gemini case that main's tests already cover.

* test: cover the cost map backup and Gemini image config validation lines

The cost map backup adapter's hide flag, and the validation itself, could be
removed with every test still green. The Gemini imageConfig adapter could be
narrowed with only the integration suite noticing, which does not run on PRs.

The endpoint table cases now read a real file through the module's location
instead of replacing open, so reading the file another way keeps them green.

* test: pin the container handler's loud table failure and loosen two pinned drops

A container endpoint table entry without path_params or query_params fails
the call on main, and no test held that. The new case trims each key from the
packaged table and expects the validation error.

The Gemini imageConfig case now accepts a drop or UnsupportedParamsError for a
non-object value, and the Bedrock beta case checks only that numbers come back
as strings, so neither pins behavior the drop_params convention may change.

* test: check a numeric beta array is never split as text and use the latest Gemini image model

The Bedrock case now also fails when a JSON array of numbers falls through to
the comma split, which returns bracketed fragments, while a drop or a string
coercion both stay allowed.
2026-10-20 01:06:37 +00:00

150 lines
5.1 KiB
Python

import json
from importlib.resources import files
from pathlib import Path
from typing import Final
from unittest.mock import MagicMock
import httpx
import pytest
from pydantic import ValidationError
import litellm
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.custom_httpx import container_handler
from litellm.llms.custom_httpx.container_handler import generic_container_handler
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.types.containers.main import DeleteContainerFileResponse
from litellm.types.router import GenericLiteLLMParams
from litellm.utils import ProviderConfigManager
FILE_NOT_FOUND_BODY = {
"error": {
"message": "File not found.",
"type": "invalid_request_error",
"param": None,
"code": None,
}
}
def _sync_client(response: httpx.Response) -> HTTPHandler:
handler = HTTPHandler()
handler.client = httpx.Client(transport=httpx.MockTransport(lambda _request: response))
return handler
def _async_client(response: httpx.Response) -> AsyncHTTPHandler:
handler = AsyncHTTPHandler()
handler.client = httpx.AsyncClient(transport=httpx.MockTransport(lambda _request: response))
return handler
def _handle(endpoint_name: str, client, **overrides):
return generic_container_handler.handle(
endpoint_name=endpoint_name,
container_provider_config=ProviderConfigManager.get_provider_container_config(
provider=litellm.LlmProviders.OPENAI
),
litellm_params=GenericLiteLLMParams(api_key="sk-test"),
logging_obj=MagicMock(),
client=client,
container_id="cntr_real",
file_id="cfile_nonexistent",
**overrides,
)
def test_binary_endpoint_raises_on_error_status():
with pytest.raises(BaseLLMException) as exc_info:
_handle(
"retrieve_container_file_content",
_sync_client(httpx.Response(404, json=FILE_NOT_FOUND_BODY)),
)
assert exc_info.value.status_code == 404
assert exc_info.value.message == "File not found."
@pytest.mark.asyncio
async def test_async_binary_endpoint_raises_on_error_status():
with pytest.raises(BaseLLMException) as exc_info:
await _handle(
"aretrieve_container_file_content",
_async_client(httpx.Response(404, json=FILE_NOT_FOUND_BODY)),
_is_async=True,
)
assert exc_info.value.status_code == 404
assert exc_info.value.message == "File not found."
def test_binary_endpoint_returns_raw_content_on_success():
content = _handle(
"retrieve_container_file_content",
_sync_client(httpx.Response(200, content=b"\x00binary-payload")),
)
assert content == b"\x00binary-payload"
def test_error_status_with_non_json_body_surfaces_response_text():
with pytest.raises(BaseLLMException) as exc_info:
_handle(
"retrieve_container_file_content",
_sync_client(httpx.Response(502, content=b"<html>bad gateway</html>")),
)
assert exc_info.value.status_code == 502
assert exc_info.value.message == "<html>bad gateway</html>"
def test_json_endpoint_still_raises_provider_error_message():
with pytest.raises(BaseLLMException) as exc_info:
_handle(
"retrieve_container_file",
_sync_client(httpx.Response(404, json=FILE_NOT_FOUND_BODY)),
)
assert exc_info.value.status_code == 404
assert exc_info.value.message == "File not found."
def test_json_endpoint_sends_the_configured_route_and_parses_its_response_model():
handler: Final = HTTPHandler()
handler.client = httpx.Client(
transport=httpx.MockTransport(
lambda request: httpx.Response(
200,
json={
"id": request.url.path,
"object": "container.file.deleted",
"deleted": request.method == "DELETE",
},
)
)
)
response: Final = _handle("delete_container_file", handler)
assert type(response) is DeleteContainerFileResponse
assert response.id.endswith("/containers/cntr_real/files/cfile_nonexistent")
assert response.deleted is True
@pytest.mark.parametrize("missing_key", ["path_params", "query_params"])
def test_an_endpoint_table_without_a_param_list_fails_the_call(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, missing_key: str
):
packaged: Final = json.loads(files("litellm.containers").joinpath("endpoints.json").read_text())
trimmed: Final = {
"endpoints": [
{key: value for key, value in entry.items() if key != missing_key} for entry in packaged["endpoints"]
]
}
(tmp_path / "containers").mkdir()
(tmp_path / "containers" / "endpoints.json").write_text(json.dumps(trimmed))
monkeypatch.setattr(container_handler, "__file__", str(tmp_path / "llms" / "custom_httpx" / "container_handler.py"))
empty_list: Final = {"object": "list", "data": [], "first_id": None, "last_id": None, "has_more": False}
with pytest.raises(ValidationError):
_handle("list_container_files", _sync_client(httpx.Response(200, json=empty_list)))