mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
* 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.
164 lines
6.4 KiB
Python
164 lines
6.4 KiB
Python
import json
|
|
from pathlib import Path
|
|
from typing import Final
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from litellm.containers import endpoint_factory
|
|
from litellm.containers.endpoint_factory import (
|
|
RESPONSE_TYPES,
|
|
_load_endpoints_config,
|
|
create_sync_endpoint_function,
|
|
generate_container_endpoints,
|
|
get_all_endpoint_names,
|
|
get_async_endpoint_names,
|
|
)
|
|
from litellm.types.containers.main import (
|
|
ContainerFileListResponse,
|
|
ContainerFileObject,
|
|
DeleteContainerFileResponse,
|
|
)
|
|
|
|
_SYNC_NAMES = [
|
|
"list_container_files",
|
|
"upload_container_file",
|
|
"retrieve_container_file",
|
|
"delete_container_file",
|
|
"retrieve_container_file_content",
|
|
]
|
|
_ASYNC_NAMES = ["a" + n for n in _SYNC_NAMES]
|
|
|
|
|
|
class TestEndpointsConfig:
|
|
def test_config_exposes_every_declared_endpoint(self):
|
|
config = _load_endpoints_config()
|
|
assert [e["name"] for e in config["endpoints"]] == _SYNC_NAMES
|
|
|
|
def test_every_endpoint_declares_the_keys_the_factory_reads(self):
|
|
for endpoint in _load_endpoints_config()["endpoints"]:
|
|
assert set(endpoint) >= {
|
|
"name",
|
|
"async_name",
|
|
"path",
|
|
"method",
|
|
"path_params",
|
|
"response_type",
|
|
}
|
|
|
|
def test_async_name_is_the_sync_name_prefixed_with_a(self):
|
|
for endpoint in _load_endpoints_config()["endpoints"]:
|
|
assert endpoint["async_name"] == "a" + endpoint["name"]
|
|
|
|
def test_config_is_reread_rather_than_shared_between_callers(self):
|
|
first = _load_endpoints_config()
|
|
second = _load_endpoints_config()
|
|
assert first is not second
|
|
assert first["endpoints"] is not second["endpoints"]
|
|
assert first == second
|
|
|
|
@pytest.mark.parametrize("document", ['["only-in-the-file"]', '"only-in-the-file"', "null"])
|
|
def test_a_packaged_file_that_is_not_a_json_object_is_rejected_without_quoting_it(
|
|
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, document: str
|
|
):
|
|
(tmp_path / "endpoints.json").write_text(document)
|
|
monkeypatch.setattr(endpoint_factory, "__file__", str(tmp_path / "endpoint_factory.py"))
|
|
|
|
with pytest.raises(ValidationError) as rejected:
|
|
_load_endpoints_config()
|
|
|
|
assert "only-in-the-file" not in str(rejected.value)
|
|
|
|
def test_a_packaged_json_object_is_returned_as_written(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
|
|
document: Final = {"endpoints": [{"name": "list_container_files"}], "schema_version": 2}
|
|
(tmp_path / "endpoints.json").write_text(json.dumps(document))
|
|
monkeypatch.setattr(endpoint_factory, "__file__", str(tmp_path / "endpoint_factory.py"))
|
|
|
|
assert _load_endpoints_config() == document
|
|
|
|
|
|
class TestResponseTypeMapping:
|
|
def test_mapping_resolves_every_named_response_type(self):
|
|
assert RESPONSE_TYPES == {
|
|
"ContainerFileListResponse": ContainerFileListResponse,
|
|
"ContainerFileObject": ContainerFileObject,
|
|
"DeleteContainerFileResponse": DeleteContainerFileResponse,
|
|
}
|
|
|
|
@pytest.mark.parametrize(
|
|
"endpoint_name,expected",
|
|
[
|
|
("list_container_files", ContainerFileListResponse),
|
|
("upload_container_file", ContainerFileObject),
|
|
("retrieve_container_file", ContainerFileObject),
|
|
("delete_container_file", DeleteContainerFileResponse),
|
|
],
|
|
)
|
|
def test_each_endpoint_maps_to_its_declared_response_type(self, endpoint_name, expected):
|
|
config = next(e for e in _load_endpoints_config()["endpoints"] if e["name"] == endpoint_name)
|
|
assert RESPONSE_TYPES[config["response_type"]] is expected
|
|
|
|
def test_raw_response_type_is_deliberately_unmapped(self):
|
|
config = next(
|
|
e for e in _load_endpoints_config()["endpoints"] if e["name"] == "retrieve_container_file_content"
|
|
)
|
|
assert config["response_type"] == "raw"
|
|
assert RESPONSE_TYPES.get(config["response_type"]) is None
|
|
|
|
|
|
class TestGeneratedEndpoints:
|
|
def test_generates_exactly_one_sync_and_one_async_function_per_endpoint(self):
|
|
assert set(generate_container_endpoints()) == set(_SYNC_NAMES) | set(_ASYNC_NAMES)
|
|
|
|
def test_every_generated_value_is_callable(self):
|
|
assert all(callable(f) for f in generate_container_endpoints().values())
|
|
|
|
def test_sync_and_async_entries_are_distinct_objects(self):
|
|
endpoints = generate_container_endpoints()
|
|
for name in _SYNC_NAMES:
|
|
assert endpoints[name] is not endpoints["a" + name]
|
|
|
|
def test_each_call_builds_fresh_functions(self):
|
|
assert (
|
|
generate_container_endpoints()["list_container_files"]
|
|
is not generate_container_endpoints()["list_container_files"]
|
|
)
|
|
|
|
def test_module_exports_are_wired_and_not_none(self):
|
|
for name in _SYNC_NAMES + _ASYNC_NAMES:
|
|
assert getattr(endpoint_factory, name) is not None
|
|
|
|
|
|
class TestEndpointNameHelpers:
|
|
def test_all_endpoint_names_interleaves_sync_then_async_per_endpoint(self):
|
|
expected = [n for name in _SYNC_NAMES for n in (name, "a" + name)]
|
|
assert get_all_endpoint_names() == expected
|
|
|
|
def test_async_endpoint_names_are_only_the_async_ones(self):
|
|
assert get_async_endpoint_names() == _ASYNC_NAMES
|
|
|
|
def test_async_names_are_a_strict_subset_of_all_names(self):
|
|
assert set(get_async_endpoint_names()) < set(get_all_endpoint_names())
|
|
|
|
|
|
class TestSyncEndpointFactory:
|
|
def test_returns_a_callable_for_a_minimal_config(self):
|
|
assert callable(
|
|
create_sync_endpoint_function({"name": "x", "response_type": "ContainerFileObject", "path_params": []})
|
|
)
|
|
|
|
def test_missing_path_params_defaults_to_empty_rather_than_raising(self):
|
|
assert callable(create_sync_endpoint_function({"name": "x", "response_type": "ContainerFileObject"}))
|
|
|
|
def test_unknown_response_type_is_tolerated_at_build_time(self):
|
|
assert callable(
|
|
create_sync_endpoint_function({"name": "x", "response_type": "NotARealType", "path_params": []})
|
|
)
|
|
|
|
def test_missing_name_is_a_build_time_error(self):
|
|
with pytest.raises(KeyError):
|
|
create_sync_endpoint_function({"response_type": "ContainerFileObject"})
|
|
|
|
def test_missing_response_type_is_a_build_time_error(self):
|
|
with pytest.raises(KeyError):
|
|
create_sync_endpoint_function({"name": "x"})
|