Merge pull request #35397 from BerriAI/litellm_daily_any_cleanup_07_31_2026_c

chore(typing): replace Any kwargs unpacking with validated model parsing
This commit is contained in:
Mateo Wang 2026-07-31 19:54:04 -07:00 committed by GitHub
commit f2cfa86713
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 199 additions and 86 deletions

View file

@ -1,6 +1,6 @@
{
"reportAny": {
"limit": 31903
"limit": 31256
},
"reportArgumentType": {
"limit": 2645
@ -24,7 +24,7 @@
"limit": 42
},
"reportExplicitAny": {
"limit": 10214
"limit": 10208
},
"reportFunctionMemberAccess": {
"limit": 11
@ -33,7 +33,7 @@
"limit": 227
},
"reportIncompatibleMethodOverride": {
"limit": 78
"limit": 77
},
"reportIncompatibleVariableOverride": {
"limit": 12
@ -99,7 +99,7 @@
"limit": 0
},
"reportUnknownArgumentType": {
"limit": 45366
"limit": 45357
},
"reportUnknownLambdaType": {
"limit": 113

View file

@ -215,7 +215,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
)
if result:
return LiteLLM_ManagedFileTable(**result)
return LiteLLM_ManagedFileTable.model_validate(result)
## CHECK DB
db_object = await self.prisma_client.db.litellm_managedfiletable.find_first(
@ -223,7 +223,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
)
if db_object:
return LiteLLM_ManagedFileTable(**db_object.model_dump())
return LiteLLM_ManagedFileTable.model_validate(db_object.model_dump())
return None
async def delete_unified_file_id(
@ -349,7 +349,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
if isinstance(batch.file_object, str)
else batch.file_object
)
batch_obj = LiteLLMBatch(**batch_data)
batch_obj = LiteLLMBatch.model_validate(batch_data)
batch_obj.id = batch.unified_object_id
batch_objects.append(batch_obj)
@ -382,7 +382,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
"flat_model_file_ids": {"hasSome": model_object_ids},
}
)
return [OpenAIFileObject(**file_object.file_object) for file_object in file_ids]
return [OpenAIFileObject.model_validate(file_object.file_object) for file_object in file_ids]
async def check_managed_file_id_access(
self, data: Dict, user_api_key_dict: UserAPIKeyAuth

View file

@ -131,8 +131,8 @@ class CustomStreamWrapper:
self.sent_last_chunk = False
self._stream_created_time: float = time.time()
litellm_params: GenericLiteLLMParams = GenericLiteLLMParams(
**self.logging_obj.model_call_details.get("litellm_params", {})
litellm_params: GenericLiteLLMParams = GenericLiteLLMParams.model_validate(
dict(**self.logging_obj.model_call_details.get("litellm_params", {}))
)
self.merge_reasoning_content_in_choices: bool = litellm_params.merge_reasoning_content_in_choices or False
self.sent_first_thinking_block = False

View file

@ -66,7 +66,7 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
)
# Create ResponseReasoningItem object from the item data
reasoning_item = ResponseReasoningItem(**item_data)
reasoning_item = ResponseReasoningItem.model_validate(item_data)
# Convert back to dict with exclude_none=True to exclude None fields
dict_reasoning_item = reasoning_item.model_dump(exclude_none=True)
@ -346,4 +346,4 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
from litellm.llms.azure.chat.gpt_transformation import AzureOpenAIError
raise AzureOpenAIError(message=raw_response.text, status_code=raw_response.status_code)
return ResponsesAPIResponse(**raw_response_json)
return ResponsesAPIResponse.model_validate(raw_response_json)

View file

@ -6184,10 +6184,12 @@ class BaseLLMHTTPHandler:
import websockets
from websockets.asyncio.client import ClientConnection
litellm_params = GenericLiteLLMParams(
api_base=api_base,
api_key=api_key,
**kwargs,
litellm_params = GenericLiteLLMParams.model_validate(
{
"api_base": api_base,
"api_key": api_key,
**kwargs,
}
)
headers = responses_api_provider_config.validate_environment(
headers={},

View file

@ -217,7 +217,7 @@ class ManusResponsesAPIConfig(OpenAIResponsesAPIConfig):
raw_response_json["id"] = f"unknown-{uuid.uuid4().hex[:8]}"
try:
response = ResponsesAPIResponse(**raw_response_json)
response = ResponsesAPIResponse.model_validate(raw_response_json)
except Exception:
verbose_logger.debug(f"Error constructing ResponsesAPIResponse: {raw_response_json}, using model_construct")
response = ResponsesAPIResponse.model_construct(**raw_response_json)
@ -305,7 +305,7 @@ class ManusResponsesAPIConfig(OpenAIResponsesAPIConfig):
raw_response_json["id"] = f"unknown-{uuid.uuid4().hex[:8]}"
try:
response = ResponsesAPIResponse(**raw_response_json)
response = ResponsesAPIResponse.model_validate(raw_response_json)
except Exception:
verbose_logger.debug(f"Error constructing ResponsesAPIResponse: {raw_response_json}, using model_construct")
response = ResponsesAPIResponse.model_construct(**raw_response_json)

View file

@ -6,8 +6,6 @@ import asyncio
from datetime import datetime, timezone
from typing import Optional
from pydantic import BaseModel
import litellm
from litellm._logging import verbose_proxy_logger
from litellm._uuid import uuid
@ -16,7 +14,6 @@ from litellm.proxy._types import (
CommonProxyErrors,
LiteLLM_AuditLogs,
Litellm_EntityType,
LiteLLM_UserTable,
LitellmTableNames,
NewUserRequest,
NewUserResponse,
@ -58,20 +55,20 @@ class UserManagementEventHooks:
try:
if prisma_client is None:
raise Exception(CommonProxyErrors.db_not_connected_error.value)
user_row: BaseModel = await UserRepository(prisma_client).table.find_first(
where={"user_id": response.user_id}
)
user_row_litellm_typed = LiteLLM_UserTable(**user_row.model_dump(exclude_none=True))
if response.user_id is None:
raise Exception("no user_id returned for the newly created user")
user_row = await UserRepository(prisma_client).find_by_id(response.user_id)
if user_row is None:
raise Exception(f"no user row found for user_id={response.user_id}")
asyncio.create_task(
UserManagementEventHooks.create_internal_user_audit_log(
user_id=user_row_litellm_typed.user_id,
user_id=user_row.user_id,
action="created",
litellm_changed_by=user_api_key_dict.user_id,
user_api_key_dict=user_api_key_dict,
litellm_proxy_admin_name=litellm_proxy_admin_name,
before_value=None,
after_value=user_row_litellm_typed.model_dump_json(exclude_none=True),
after_value=user_row.model_dump_json(exclude_none=True),
)
)
except Exception as e:

View file

@ -3137,7 +3137,7 @@ class SSOAuthenticationHandler:
_default_team_params = deepcopy(litellm.default_team_params)
_new_team_request = team_request.model_dump()
_new_team_request.update(_default_team_params)
team_request = NewTeamRequest(**_new_team_request)
team_request = NewTeamRequest.model_validate(_new_team_request)
return team_request
@staticmethod
@ -3271,14 +3271,6 @@ class SSOAuthenticationHandler:
# User might not be already created on first generation of key
# But if it is, we want their models preferences
default_ui_key_values: Dict[str, Any] = {
"duration": LITELLM_UI_SESSION_DURATION,
"key_max_budget": litellm.max_ui_session_budget,
"aliases": {},
"config": {},
"spend": 0,
"team_id": "litellm-dashboard",
}
user_defined_values: Optional[SSOUserDefinedValues] = None
if user_custom_sso is not None:
@ -3338,10 +3330,20 @@ class SSOAuthenticationHandler:
verbose_proxy_logger.info(f"user_defined_values for creating ui key: {user_defined_values}")
default_ui_key_values.update(user_defined_values)
default_ui_key_values["request_type"] = "key"
response = await generate_key_helper_fn(
**default_ui_key_values, # type: ignore
request_type="key",
duration=LITELLM_UI_SESSION_DURATION,
key_max_budget=litellm.max_ui_session_budget,
aliases={},
config={},
spend=0,
team_id="litellm-dashboard",
models=user_defined_values["models"],
user_id=user_defined_values["user_id"],
user_email=user_defined_values["user_email"],
user_role=user_defined_values["user_role"],
max_budget=user_defined_values["max_budget"],
budget_duration=user_defined_values["budget_duration"],
table_name="key",
)

View file

@ -1035,7 +1035,7 @@ async def get_batch_from_database(
if isinstance(db_batch_object.file_object, str)
else db_batch_object.file_object
)
response = LiteLLMBatch(**batch_data)
response = LiteLLMBatch.model_validate(batch_data)
response.id = batch_id
# The stored batch object has the raw provider input_file_id. Resolve to unified ID.

View file

@ -3,61 +3,56 @@ User repository for database operations on LiteLLM_UserTable.
"""
import json
from typing import Any, Dict, List, Optional, Type
from typing import Any, Dict, List, Mapping, Optional, Type
from litellm.models.user import LiteLLM_UserTable
from litellm.repositories.base_repository import BaseRepository
from litellm.repositories.base_repository import BaseRepository, DbRecord, record_to_dict
_JSON_ENCODED_COLUMNS = frozenset({"metadata", "model_spend", "model_max_budget"})
class UserRepository(BaseRepository[LiteLLM_UserTable]):
"""Repository for user database operations."""
@property
def table(self) -> Any:
def table(self) -> Any: # any-ok: Prisma table actions are reached through the untyped client wrapper
return self.prisma_client.db.litellm_usertable
@property
def model_class(self) -> Type[LiteLLM_UserTable]:
return LiteLLM_UserTable
def _to_model(self, record: Any) -> Optional[LiteLLM_UserTable]:
def _to_model(self, record: Optional[DbRecord]) -> Optional[LiteLLM_UserTable]:
"""Convert a database record to a User model."""
if record is None:
return None
data = record.dict() if hasattr(record, "dict") else dict(record)
return LiteLLM_UserTable.model_validate(
{
column: json.loads(value) if column in _JSON_ENCODED_COLUMNS and isinstance(value, str) else value
for column, value in record_to_dict(record).items()
}
)
json_fields = ["metadata", "model_spend", "model_max_budget"]
for field in json_fields:
if isinstance(data.get(field), str):
data[field] = json.loads(data[field])
return LiteLLM_UserTable(**data)
async def find_by_id(self, user_id: str, id_field: str = "user_id") -> Optional[LiteLLM_UserTable]:
return await super().find_by_id(user_id, id_field)
async def find_by_id(self, id_value: str, id_field: str = "user_id") -> Optional[LiteLLM_UserTable]:
return await super().find_by_id(id_value, id_field)
async def find_by_email(self, user_email: str) -> Optional[LiteLLM_UserTable]:
"""Find a user by email."""
records = await self.table.find_many(where={"user_email": user_email})
if records:
return self._to_model(records[0])
return None
records = await self.find_many(where={"user_email": user_email})
return records[0] if records else None
async def find_by_sso_id(self, sso_user_id: str) -> Optional[LiteLLM_UserTable]:
"""Find a user by SSO ID."""
record = await self.table.find_unique(where={"sso_user_id": sso_user_id})
return self._to_model(record)
return await self.find_by_id(sso_user_id, id_field="sso_user_id")
async def find_by_organization_id(self, organization_id: str) -> List[LiteLLM_UserTable]:
"""Find all users in an organization."""
records = await self.table.find_many(where={"organization_id": organization_id})
return self._to_model_list(records)
return await self.find_many(where={"organization_id": organization_id})
async def find_by_team_id(self, team_id: str) -> List[LiteLLM_UserTable]:
"""Find all users in a team."""
records = await self.table.find_many(where={"teams": {"has": team_id}})
return self._to_model_list(records)
return await self.find_many(where={"teams": {"has": team_id}})
async def count_billable_users(self) -> int:
"""Number of users that count toward the license seat limit.
@ -86,7 +81,7 @@ class UserRepository(BaseRepository[LiteLLM_UserTable]):
max_budget: Optional[float] = None,
user_email: Optional[str] = None,
models: Optional[List[str]] = None,
metadata: Optional[Dict[str, Any]] = None,
metadata: Optional[Mapping[str, object]] = None,
max_parallel_requests: Optional[int] = None,
tpm_limit: Optional[int] = None,
rpm_limit: Optional[int] = None,
@ -96,7 +91,7 @@ class UserRepository(BaseRepository[LiteLLM_UserTable]):
object_permission_id: Optional[str] = None,
) -> LiteLLM_UserTable:
"""Create a new user."""
data: Dict[str, Any] = {"user_id": user_id}
data: Dict[str, object] = {"user_id": user_id}
if user_alias is not None:
data["user_alias"] = user_alias
if team_id is not None:
@ -149,7 +144,7 @@ class UserRepository(BaseRepository[LiteLLM_UserTable]):
max_budget: Optional[float] = None,
user_email: Optional[str] = None,
models: Optional[List[str]] = None,
metadata: Optional[Dict[str, Any]] = None,
metadata: Optional[Mapping[str, object]] = None,
max_parallel_requests: Optional[int] = None,
tpm_limit: Optional[int] = None,
rpm_limit: Optional[int] = None,
@ -159,7 +154,7 @@ class UserRepository(BaseRepository[LiteLLM_UserTable]):
object_permission_id: Optional[str] = None,
) -> Optional[LiteLLM_UserTable]:
"""Update a user."""
data: Dict[str, Any] = {}
data: Dict[str, object] = {}
if user_alias is not None:
data["user_alias"] = user_alias
if team_id is not None:
@ -212,11 +207,7 @@ class UserRepository(BaseRepository[LiteLLM_UserTable]):
if not await self.exists(user_id, id_field="user_id"):
return None
record = await self.table.update(
where={"user_id": user_id},
data={"teams": {"push": team_id}},
)
return self._to_model(record)
return await self.update(user_id, {"teams": {"push": team_id}}, id_field="user_id")
async def remove_from_team(self, user_id: str, team_id: str) -> Optional[LiteLLM_UserTable]:
"""Remove a user from a team.

View file

@ -8613,9 +8613,9 @@ class Router:
deployment = self.get_deployment(model_id=model_id)
if deployment is None or self._is_deployment_blocked(deployment):
return None
return CredentialLiteLLMParams(**deployment.litellm_params.model_dump(exclude_none=True)).model_dump(
exclude_none=True
)
return CredentialLiteLLMParams.model_validate(
deployment.litellm_params.model_dump(exclude_none=True)
).model_dump(exclude_none=True)
def get_deployment_by_model_group_name(self, model_group_name: str) -> Optional[Deployment]:
"""
@ -8755,9 +8755,9 @@ class Router:
return None
# Get basic credentials
credentials = CredentialLiteLLMParams(**deployment.litellm_params.model_dump(exclude_none=True)).model_dump(
exclude_none=True
)
credentials = CredentialLiteLLMParams.model_validate(
deployment.litellm_params.model_dump(exclude_none=True)
).model_dump(exclude_none=True)
# Resolve litellm_credential_name to actual credentials
if deployment.litellm_params.litellm_credential_name is not None:

View file

@ -329,7 +329,7 @@ class ComplexityRouter(CustomLogger):
# Parse config - always create a new instance to avoid singleton mutation
if complexity_router_config:
self.config = ComplexityRouterConfig(**complexity_router_config)
self.config = ComplexityRouterConfig.model_validate(complexity_router_config)
else:
self.config = ComplexityRouterConfig()

View file

@ -24,7 +24,7 @@
"limit": 130
},
"ANN401": {
"limit": 2010
"limit": 2009
},
"ASYNC230": {
"limit": 14
@ -324,7 +324,7 @@
"limit": 879
},
"UP006": {
"limit": 12138
"limit": 12135
},
"UP007": {
"limit": 2526

View file

@ -0,0 +1,99 @@
import asyncio
import json
import sys
from types import SimpleNamespace
from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock, patch
import pytest
import litellm
from litellm.proxy._types import NewUserRequest, NewUserResponse, UserAPIKeyAuth
from litellm.proxy.hooks.user_management_event_hooks import UserManagementEventHooks
class FakeUserTable:
def __init__(self, rows: List[Dict[str, Any]]):
self._rows = rows
async def find_unique(self, where: Dict[str, Any]) -> Optional[Dict[str, Any]]:
return next(
(row for row in self._rows if all(row.get(key) == value for key, value in where.items())),
None,
)
class FakePrismaClient:
def __init__(self, rows: List[Dict[str, Any]]):
self.db = SimpleNamespace(litellm_usertable=FakeUserTable(rows))
async def _run_created_hook(prisma_client: FakePrismaClient, audit_log: AsyncMock) -> None:
proxy_server = SimpleNamespace(
prisma_client=prisma_client,
litellm_proxy_admin_name="admin-user",
)
with (
patch.dict(sys.modules, {"litellm.proxy.proxy_server": proxy_server}),
patch.object(litellm, "store_audit_logs", True),
patch(
"litellm.proxy.hooks.user_management_event_hooks.create_audit_log_for_update",
audit_log,
),
patch.object(
UserManagementEventHooks,
"async_send_user_invitation_email",
AsyncMock(),
),
):
await UserManagementEventHooks.async_user_created_hook(
data=NewUserRequest(user_email="new@example.com", send_invite_email=False),
response=NewUserResponse(
user_id="user-1",
user_email="new@example.com",
key="sk-test",
),
user_api_key_dict=UserAPIKeyAuth(user_id="admin-user", api_key="sk-admin"),
)
await asyncio.sleep(0)
@pytest.mark.asyncio
async def test_created_hook_audit_logs_the_row_read_back_from_the_database():
"""The audit entry must describe the persisted user row, not the /user/new response."""
prisma_client = FakePrismaClient(
[
{
"user_id": "user-1",
"user_email": "new@example.com",
"user_role": "proxy_admin",
"models": ["gpt-4"],
"teams": ["team-a"],
"metadata": '{"source": "api"}',
}
]
)
audit_log = AsyncMock()
await _run_created_hook(prisma_client, audit_log)
audit_log.assert_awaited_once()
request_data = audit_log.await_args.kwargs["request_data"]
assert request_data.object_id == "user-1"
assert request_data.action == "created"
updated_values = json.loads(request_data.updated_values)
assert updated_values["user_role"] == "proxy_admin"
assert updated_values["models"] == ["gpt-4"]
assert updated_values["teams"] == ["team-a"]
assert updated_values["metadata"] == {"source": "api"}
@pytest.mark.asyncio
async def test_created_hook_skips_the_audit_log_when_no_user_row_exists():
"""A user id that resolves to nothing must not produce an audit entry."""
audit_log = AsyncMock()
await _run_created_hook(FakePrismaClient([]), audit_log)
audit_log.assert_not_awaited()

View file

@ -1905,6 +1905,28 @@ class TestUserRepositoryExtended:
)
assert updated.user_email == "new@example.com"
@pytest.mark.asyncio
async def test_find_by_id_decodes_only_the_json_encoded_columns(self, repo):
repo._prisma_client.db.litellm_usertable._records["user-json"] = {
"user_id": "user-json",
"user_email": "json@example.com",
"user_role": '{"not": "json"}',
"teams": [],
"models": [],
"metadata": '{"department": "engineering"}',
"model_spend": '{"gpt-4": 10.5}',
"model_max_budget": '{"gpt-4": 100.0}',
}
user = await repo.find_by_id("user-json")
assert user is not None
assert user.metadata == {"department": "engineering"}
assert user.model_spend == {"gpt-4": 10.5}
assert user.model_max_budget == {"gpt-4": 100.0}
assert user.user_email == "json@example.com"
assert user.user_role == '{"not": "json"}'
class TestProjectRepositoryExtended:
@pytest.fixture

View file

@ -1,9 +1,9 @@
{
"LIT001": {
"limit": 23253
"limit": 23250
},
"LIT002": {
"limit": 27280
"limit": 27277
},
"LIT003": {
"limit": 292
@ -24,6 +24,6 @@
"limit": 1004
},
"LIT009": {
"limit": 2474
"limit": 2473
}
}