mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-26 01:12:21 +00:00
* ci: run the unit_selection.sh shard files on every event instead of only fork pull requests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ci: rename fork-flag to unit-flag now that it applies on every event * test: move tests/test_litellm root and small trees into tests/unit Pure renames, no content changes. Follow-up commits in this PR fix references, merge the three files that already existed in tests/unit, keep live-provider tests in tests/test_litellm and wire CI. * test: carry tests/test_litellm conftest isolation into tests/unit Callback lists, routing fallbacks, cached HTTP clients, logger state, AWS, proxy-URL and keychain env, and session-end client cleanup now reset for unit tests too. The environment isolation owns its MonkeyPatch so a test's own monkeypatch is undone before the model-cost teardown runs. * test: merge, split and prune the moved root and small-tree tests Merge batches/test_batch_utils.py and the chat_completions and messages dispatch tests into the files that already existed in tests/unit. Keep the live Gemini interactions tests, the async image-fetch format test and the OpenAI embedding scorer test in tests/test_litellm since they need real network or keys. Put test_router.py under tests/unit/test_router so the existing package no longer shadows it. Delete eight tests the audit found superseded by stronger ones kept in this move. * ci: run the moved root and small-tree tests under their legacy flags Add the misc and responses-caching-types flags to unit_selection.sh and CircleCI, extend enterprise-routing and mcp-integration, and point the legacy GHA shards, Makefile, redis-compat workflow, merge smoke manifest and change classifier at the new paths. * test: make the new tests/unit directories packages tests/unit/test_package_layout.py requires every directory to carry an __init__.py, and without one the moved and retained test_litellm_responses_bridge.py modules collide on import. * test: scope the unit socket block to tests/unit in shared sessions The GHA shards collect the legacy test-path and the unit selection in one pytest session. The unit conftest's loopback-only block leaked into legacy modules that reach the network at import. The legacy conftest now lifts the restriction at collect and setup time, and the unit conftest re-applies it when collecting its own modules. * test: give the shard-script tests their own GITHUB_OUTPUT They only passed where the runner set it. The CircleCI unit job's env allowlist drops it, so the script's redirect failed there. * test: point the router and module-deletion checks at tests/unit router_code_coverage and code_qa_check_tests only searched tests/test_litellm, so the moved router tests no longer counted. The two silent-experiment tests the audit deleted were the only direct callers of those methods; they are replaced with tests that assert the forwarded shadow request and the recursion guard. --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
import warnings
|
|
|
|
import pytest
|
|
|
|
from litellm.types.utils import (
|
|
Choices,
|
|
Delta,
|
|
Message,
|
|
ModelResponse,
|
|
ModelResponseStream,
|
|
StreamingChoices,
|
|
)
|
|
|
|
|
|
def test_modelresponse_normalizes_openai_base_models() -> None:
|
|
# OpenAI SDK returns Pydantic BaseModel objects for message/choice.
|
|
# LiteLLM should normalize these into its own internal `Message` / `Choices` types.
|
|
from openai.types.chat.chat_completion import Choice as OpenAIChoice
|
|
from openai.types.chat.chat_completion_message import ChatCompletionMessage
|
|
|
|
message = ChatCompletionMessage(role="assistant", content="hi")
|
|
choice = OpenAIChoice(finish_reason="stop", index=0, message=message, logprobs=None)
|
|
|
|
with warnings.catch_warnings(record=True) as captured:
|
|
warnings.simplefilter("always")
|
|
response = ModelResponse(model="gpt-4o-mini", choices=[choice])
|
|
_ = response.model_dump()
|
|
|
|
assert isinstance(response.choices[0], Choices)
|
|
assert isinstance(response.choices[0].message, Message)
|
|
|
|
assert not any(
|
|
"Pydantic serializer warnings" in str(w.message)
|
|
for w in captured
|
|
if isinstance(w.message, Warning)
|
|
)
|
|
|
|
|
|
def test_modelresponse_serialization_avoids_pydantic_warnings() -> None:
|
|
pytest.importorskip("openai")
|
|
from openai.types.chat import ChatCompletion as OpenAIChatCompletion
|
|
|
|
openai_completion = OpenAIChatCompletion(
|
|
id="test-1",
|
|
created=1719868600,
|
|
model="gpt-4o-mini",
|
|
object="chat.completion",
|
|
choices=[
|
|
{
|
|
"index": 0,
|
|
"finish_reason": "stop",
|
|
"message": {"role": "assistant", "content": "hi"},
|
|
"logprobs": None,
|
|
}
|
|
],
|
|
usage={"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
|
)
|
|
|
|
with warnings.catch_warnings(record=True) as captured:
|
|
warnings.simplefilter("always")
|
|
response = ModelResponse(**openai_completion.model_dump())
|
|
_ = response.model_dump(exclude_none=True)
|
|
|
|
assert not any(
|
|
"PydanticSerializationUnexpectedValue" in str(w.message)
|
|
or "Pydantic serializer warnings" in str(w.message)
|
|
for w in captured
|
|
)
|
|
|
|
|
|
def test_modelresponse_model_dump_json_no_pydantic_warnings() -> None:
|
|
"""model_dump_json() and model_dump() should not trigger any Pydantic
|
|
serialization warnings now that choices is List[Choices] (no Union)."""
|
|
response = ModelResponse(
|
|
model="test-model",
|
|
choices=[
|
|
Choices(
|
|
finish_reason="stop",
|
|
index=0,
|
|
message=Message(content="hello", role="assistant"),
|
|
)
|
|
],
|
|
)
|
|
|
|
with warnings.catch_warnings(record=True) as captured:
|
|
warnings.simplefilter("always")
|
|
_ = response.model_dump_json()
|
|
_ = response.model_dump()
|
|
_ = response.model_dump(exclude_none=True)
|
|
|
|
pydantic_warnings = [
|
|
w
|
|
for w in captured
|
|
if "PydanticSerializationUnexpectedValue" in str(w.message)
|
|
or "Pydantic serializer warnings" in str(w.message)
|
|
]
|
|
assert (
|
|
pydantic_warnings == []
|
|
), f"Unexpected Pydantic serialization warnings: {pydantic_warnings}"
|
|
|
|
|
|
def test_streaming_modelresponsestream_no_pydantic_warnings() -> None:
|
|
"""Streaming responses use ModelResponseStream with List[StreamingChoices]
|
|
and should serialize without warnings."""
|
|
response = ModelResponseStream(
|
|
choices=[
|
|
StreamingChoices(
|
|
finish_reason="stop",
|
|
index=0,
|
|
delta=Delta(content="hello", role="assistant"),
|
|
)
|
|
],
|
|
)
|
|
|
|
with warnings.catch_warnings(record=True) as captured:
|
|
warnings.simplefilter("always")
|
|
_ = response.model_dump_json()
|
|
_ = response.model_dump()
|
|
|
|
pydantic_warnings = [
|
|
w
|
|
for w in captured
|
|
if "PydanticSerializationUnexpectedValue" in str(w.message)
|
|
or "Pydantic serializer warnings" in str(w.message)
|
|
]
|
|
assert (
|
|
pydantic_warnings == []
|
|
), f"Unexpected Pydantic serialization warnings: {pydantic_warnings}"
|