test: cover the response transforms and model round-trips touched by the Any cleanup

The typing pass swapped dict-unpacking for model_validate in several response
paths that had no unit coverage, so codecov flagged the diff. These tests pin the
contracts those lines carry rather than just executing them.

Video response transforms are driven through a mock httpx.Response and assert the
payload parses and the provider id is wrapped so it decodes back to the raw id.
The Azure and OpenAI assistants paths use a real SDK client on an httpx mock
transport, so the SDK's own parsing runs, and assert the message status defaults
to "completed" when the provider omits it and that Thread keeps its declared
fields. The SSO default-team helper and the router credential lookup assert their
projections directly. Each test fails when the line it covers is reverted.
This commit is contained in:
mateo-berri 2026-08-01 03:03:07 +00:00
parent cd191b3da1
commit a48ae47604
No known key found for this signature in database
5 changed files with 430 additions and 2 deletions

View file

@ -0,0 +1,117 @@
import httpx
import pytest
from openai import AsyncAzureOpenAI
from litellm.llms.azure.assistants import AzureAssistantsAPI
from litellm.types.llms.openai import OpenAIMessage, Thread
_MESSAGE_PAYLOAD = {
"id": "msg_123",
"object": "thread.message",
"created_at": 1700000000,
"thread_id": "thread_123",
"role": "assistant",
"status": "in_progress",
"content": [{"type": "text", "text": {"value": "hi", "annotations": []}}],
"metadata": {"origin": "unit-test"},
"run_id": "run_123",
"assistant_id": "asst_123",
"unexpected_upstream_field": "kept",
}
_THREAD_PAYLOAD = {
"id": "thread_123",
"object": "thread",
"created_at": 1700000000,
"metadata": {"origin": "unit-test"},
"unexpected_upstream_field": "kept",
}
_COMMON_ARGS = {
"api_key": "test-key",
"api_base": "https://test.openai.azure.com",
"api_version": "2024-05-01-preview",
"azure_ad_token": None,
"timeout": 60.0,
"max_retries": 2,
}
def _client(payload: dict) -> AsyncAzureOpenAI:
"""A real AsyncAzureOpenAI wired to a mock transport, so the SDK's own response
parsing runs and the handler under test receives exactly what production would."""
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json=payload)
return AsyncAzureOpenAI(
api_key="test-key",
api_version="2024-05-01-preview",
azure_endpoint="https://test.openai.azure.com",
http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
)
@pytest.mark.asyncio
async def test_a_add_message_preserves_upstream_message_fields():
"""The returned message is rebuilt from the upstream one, so every field the
provider sent (including ones the SDK model does not declare) must survive."""
result = await AzureAssistantsAPI().a_add_message(
thread_id="thread_123",
message_data={"role": "user", "content": "hi"},
client=_client(_MESSAGE_PAYLOAD),
**_COMMON_ARGS,
)
assert isinstance(result, OpenAIMessage)
assert result.id == "msg_123"
assert result.thread_id == "thread_123"
assert result.status == "in_progress"
assert result.role == "assistant"
assert result.metadata == {"origin": "unit-test"}
assert result.model_dump()["unexpected_upstream_field"] == "kept"
@pytest.mark.asyncio
async def test_a_add_message_defaults_missing_status_to_completed():
"""Some deployments omit `status` on the created message; it is filled in before
the message is handed back so callers never see a status-less message."""
payload = {k: v for k, v in _MESSAGE_PAYLOAD.items() if k != "status"}
result = await AzureAssistantsAPI().a_add_message(
thread_id="thread_123",
message_data={"role": "user", "content": "hi"},
client=_client(payload),
**_COMMON_ARGS,
)
assert result.status == "completed"
assert result.id == "msg_123"
assert result.metadata == {"origin": "unit-test"}
@pytest.mark.asyncio
async def test_thread_responses_preserve_declared_fields():
api = AzureAssistantsAPI()
created = await api.async_create_thread(
metadata={"origin": "unit-test"},
messages=None,
client=_client(_THREAD_PAYLOAD),
**_COMMON_ARGS,
)
retrieved = await api.async_get_thread(
thread_id="thread_123",
client=_client(_THREAD_PAYLOAD),
**_COMMON_ARGS,
)
for thread in (created, retrieved):
assert isinstance(thread, Thread)
assert thread.id == "thread_123"
assert thread.created_at == 1700000000
assert thread.object == "thread"
assert thread.metadata == {"origin": "unit-test"}
# LiteLLM's Thread declares its own fields, so anything the provider adds on
# top is dropped rather than carried through.
assert "unexpected_upstream_field" not in thread.model_dump()

View file

@ -0,0 +1,92 @@
import httpx
import pytest
from openai import AsyncOpenAI, OpenAI
from litellm.llms.openai.openai import OpenAIAssistantsAPI
from litellm.types.llms.openai import Thread
_THREAD_PAYLOAD = {
"id": "thread_123",
"object": "thread",
"created_at": 1700000000,
"metadata": {"origin": "unit-test"},
"unexpected_upstream_field": "kept",
}
_COMMON_ARGS = {
"api_key": "test-key",
"api_base": "https://api.openai.com/v1",
"timeout": 60.0,
"max_retries": 2,
"organization": None,
}
def _handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json=_THREAD_PAYLOAD)
def _async_client() -> AsyncOpenAI:
"""A real AsyncOpenAI wired to a mock transport, so the SDK's own response parsing
runs and the handler under test receives exactly what production would."""
return AsyncOpenAI(
api_key="test-key",
http_client=httpx.AsyncClient(transport=httpx.MockTransport(_handler)),
)
def _sync_client() -> OpenAI:
return OpenAI(
api_key="test-key",
http_client=httpx.Client(transport=httpx.MockTransport(_handler)),
)
def _assert_thread(thread: object) -> None:
assert isinstance(thread, Thread)
assert thread.id == "thread_123"
assert thread.created_at == 1700000000
assert thread.object == "thread"
assert thread.metadata == {"origin": "unit-test"}
# LiteLLM's Thread declares its own fields, so anything the provider adds on top
# is dropped rather than carried through.
assert "unexpected_upstream_field" not in thread.model_dump()
@pytest.mark.asyncio
async def test_async_thread_responses_preserve_declared_fields():
api = OpenAIAssistantsAPI()
created = await api.async_create_thread(
metadata={"origin": "unit-test"},
messages=None,
client=_async_client(),
**_COMMON_ARGS,
)
retrieved = await api.async_get_thread(
thread_id="thread_123",
client=_async_client(),
**_COMMON_ARGS,
)
_assert_thread(created)
_assert_thread(retrieved)
def test_sync_thread_responses_preserve_declared_fields():
api = OpenAIAssistantsAPI()
created = api.create_thread(
metadata={"origin": "unit-test"},
messages=None,
client=_sync_client(),
**_COMMON_ARGS,
)
retrieved = api.get_thread(
thread_id="thread_123",
client=_sync_client(),
**_COMMON_ARGS,
)
_assert_thread(created)
_assert_thread(retrieved)

View file

@ -2,10 +2,15 @@ import io
import os
import httpx
import pytest
from litellm.llms.openai.videos.transformation import OpenAIVideoConfig
from litellm.types.router import GenericLiteLLMParams
from litellm.types.videos.utils import encode_character_id_with_provider
from litellm.types.videos.utils import (
decode_video_id_with_provider,
encode_character_id_with_provider,
extract_original_video_id,
)
def test_video_content_request_encodes_video_id_path_segment():
@ -145,3 +150,114 @@ def test_video_create_response_parses_payload_with_unknown_fields():
assert video_obj.id == "video_123"
assert video_obj.seconds == "8"
assert video_obj.usage == {"duration_seconds": 8.0}
def _video_payload(video_id: str = "video_123", **overrides: object) -> dict:
payload = {
"id": video_id,
"object": "video",
"status": "completed",
"seconds": "8",
"size": "1280x720",
"unexpected_upstream_field": {"nested": True},
}
payload.update(overrides)
return payload
def _json_response(payload: dict) -> httpx.Response:
return httpx.Response(
200,
json=payload,
request=httpx.Request("POST", "https://api.openai.com/v1/videos"),
)
@pytest.mark.parametrize(
"method_name",
[
"transform_video_remix_response",
"transform_video_status_retrieve_response",
"transform_video_edit_response",
"transform_video_extension_response",
],
)
def test_video_response_transforms_parse_payload_and_wrap_id(method_name):
"""Every /videos response transform parses the upstream body into a VideoObject
and swaps in a LiteLLM-managed id that decodes back to the provider's id."""
config = OpenAIVideoConfig()
transform = getattr(config, method_name)
video_obj = transform(
raw_response=_json_response(_video_payload()),
logging_obj=None,
custom_llm_provider="openai",
)
assert video_obj.status == "completed"
assert video_obj.seconds == "8"
assert video_obj.size == "1280x720"
assert video_obj.id != "video_123"
assert extract_original_video_id(video_obj.id) == "video_123"
assert decode_video_id_with_provider(video_obj.id)["custom_llm_provider"] == "openai"
@pytest.mark.parametrize(
"method_name",
[
"transform_video_remix_response",
"transform_video_status_retrieve_response",
"transform_video_edit_response",
"transform_video_extension_response",
],
)
def test_video_response_transforms_leave_id_raw_without_provider(method_name):
config = OpenAIVideoConfig()
transform = getattr(config, method_name)
video_obj = transform(
raw_response=_json_response(_video_payload()),
logging_obj=None,
)
assert video_obj.id == "video_123"
def test_video_delete_response_parses_payload_without_wrapping_id():
"""Delete returns the deleted object as-is; it must still parse, and it must not
re-encode the id (the caller already holds the managed one)."""
config = OpenAIVideoConfig()
video_obj = config.transform_video_delete_response(
raw_response=_json_response(_video_payload(status="deleted")),
logging_obj=None,
)
assert video_obj.id == "video_123"
assert video_obj.status == "deleted"
@pytest.mark.parametrize(
"method_name",
["transform_video_create_character_response", "transform_video_get_character_response"],
)
def test_character_response_transforms_parse_payload(method_name):
config = OpenAIVideoConfig()
transform = getattr(config, method_name)
character = transform(
raw_response=_json_response(
{
"id": "char_123",
"object": "character",
"created_at": 1700000000,
"name": "hero",
"unexpected_upstream_field": "ignored",
}
),
logging_obj=None,
)
assert character.id == "char_123"
assert character.name == "hero"
assert character.created_at == 1700000000

View file

@ -15,7 +15,7 @@ sys.path.insert(
) # Adds the parent directory to the system path
import litellm
from litellm.proxy._types import LiteLLM_UserTable, NewUserResponse
from litellm.proxy._types import LiteLLM_UserTable, NewTeamRequest, NewUserResponse
from litellm.proxy.auth.handle_jwt import JWTHandler
from litellm.proxy.management_endpoints.sso import CustomMicrosoftSSO
from litellm.proxy.management_endpoints.types import CustomOpenID
@ -7905,3 +7905,53 @@ class TestPersistReturnToCookieSharedHelper:
resp = Response()
_persist_return_to_cookie(resp, "https://cp.example.com/ui?page=models")
assert "litellm_cp_return_to=" in self._cookie(resp)
def test_cast_and_deepcopy_default_team_params_from_dict():
"""A dict-shaped litellm.default_team_params is turned into a NewTeamRequest with
the SSO group's id and name layered on top, and the caller's dict is not mutated."""
default_team_params = {"max_budget": 10.0, "budget_duration": "1d", "models": ["special-gpt-5"]}
result = SSOAuthenticationHandler._cast_and_deepcopy_litellm_default_team_params(
default_team_params=default_team_params,
team_request=NewTeamRequest(team_id="ignored", team_alias="ignored"),
litellm_team_id="team-abc",
litellm_team_name="Engineering",
)
assert isinstance(result, NewTeamRequest)
assert result.team_id == "team-abc"
assert result.team_alias == "Engineering"
assert result.max_budget == 10.0
assert result.budget_duration == "1d"
assert result.models == ["special-gpt-5"]
assert default_team_params == {
"max_budget": 10.0,
"budget_duration": "1d",
"models": ["special-gpt-5"],
}
def test_cast_and_deepcopy_default_team_params_from_model():
"""A DefaultTeamSSOParams-shaped default overlays the incoming request, so the
incoming team id survives while the configured defaults win on the fields they set."""
litellm.default_team_params = DefaultTeamSSOParams(
max_budget=25.0, budget_duration="7d", models=["special-gpt-5"]
)
try:
result = SSOAuthenticationHandler._cast_and_deepcopy_litellm_default_team_params(
default_team_params=litellm.default_team_params,
team_request=NewTeamRequest(team_id="team-abc", team_alias="Engineering"),
litellm_team_id="team-abc",
litellm_team_name="Engineering",
)
finally:
litellm.default_team_params = None
assert isinstance(result, NewTeamRequest)
assert result.team_id == "team-abc"
assert result.team_alias == "Engineering"
assert result.max_budget == 25.0
assert result.budget_duration == "7d"
assert result.models == ["special-gpt-5"]

View file

@ -6574,3 +6574,56 @@ def test_model_info_is_active_for_environment_matrix(monkeypatch):
monkeypatch.delenv("LITELLM_ENVIRONMENT")
with pytest.raises(ValueError, match="LITELLM_ENVIRONMENT"):
model_info_is_active_for_environment(model_info={"supported_environments": ["production"]})
def test_get_deployment_credentials_returns_credential_fields_only():
"""
get_deployment_credentials projects a deployment's litellm_params through
CredentialLiteLLMParams, so credential fields survive and routing-only params
(tpm/rpm and friends) are filtered out.
"""
router = litellm.Router(
model_list=[
{
"model_name": "bedrock-claude-model",
"litellm_params": {
"model": "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0",
"aws_access_key_id": "test-access-key",
"aws_secret_access_key": "test-secret-key",
"aws_region_name": "us-east-1",
"tpm": 1000,
},
"model_info": {"id": "deployment-1"},
}
],
)
credentials = router.get_deployment_credentials(model_id="deployment-1")
assert credentials is not None
assert credentials["aws_access_key_id"] == "test-access-key"
assert credentials["aws_secret_access_key"] == "test-secret-key"
assert credentials["aws_region_name"] == "us-east-1"
assert "tpm" not in credentials
def test_get_deployment_credentials_returns_none_for_blocked_deployment():
"""
A paused deployment must not hand out credentials, so passthrough file / batch
callers cannot keep using it by resolving credentials directly.
"""
router = litellm.Router(
model_list=[
{
"model_name": "bedrock-claude-model",
"litellm_params": {
"model": "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0",
"aws_access_key_id": "test-access-key",
},
"model_info": {"id": "deployment-1", "blocked": True},
}
],
)
assert router.get_deployment_credentials(model_id="deployment-1") is None
assert router.get_deployment_credentials(model_id="does-not-exist") is None