mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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:
commit
f2cfa86713
16 changed files with 199 additions and 86 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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={},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue