mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 10:44:24 +00:00
[UI] Bug Fix - Show created_at and updated_at for Users Page (#10033)
* add created_at and updated_at as fields for internal user table * test_get_users_includes_timestamps
This commit is contained in:
parent
70d740332f
commit
d32d6fe03e
2 changed files with 107 additions and 34 deletions
|
@ -650,9 +650,9 @@ class GenerateRequestBase(LiteLLMPydanticObjectBase):
|
||||||
allowed_cache_controls: Optional[list] = []
|
allowed_cache_controls: Optional[list] = []
|
||||||
config: Optional[dict] = {}
|
config: Optional[dict] = {}
|
||||||
permissions: Optional[dict] = {}
|
permissions: Optional[dict] = {}
|
||||||
model_max_budget: Optional[
|
model_max_budget: Optional[dict] = (
|
||||||
dict
|
{}
|
||||||
] = {} # {"gpt-4": 5.0, "gpt-3.5-turbo": 5.0}, defaults to {}
|
) # {"gpt-4": 5.0, "gpt-3.5-turbo": 5.0}, defaults to {}
|
||||||
|
|
||||||
model_config = ConfigDict(protected_namespaces=())
|
model_config = ConfigDict(protected_namespaces=())
|
||||||
model_rpm_limit: Optional[dict] = None
|
model_rpm_limit: Optional[dict] = None
|
||||||
|
@ -908,12 +908,12 @@ class NewCustomerRequest(BudgetNewRequest):
|
||||||
alias: Optional[str] = None # human-friendly alias
|
alias: Optional[str] = None # human-friendly alias
|
||||||
blocked: bool = False # allow/disallow requests for this end-user
|
blocked: bool = False # allow/disallow requests for this end-user
|
||||||
budget_id: Optional[str] = None # give either a budget_id or max_budget
|
budget_id: Optional[str] = None # give either a budget_id or max_budget
|
||||||
allowed_model_region: Optional[
|
allowed_model_region: Optional[AllowedModelRegion] = (
|
||||||
AllowedModelRegion
|
None # require all user requests to use models in this specific region
|
||||||
] = None # require all user requests to use models in this specific region
|
)
|
||||||
default_model: Optional[
|
default_model: Optional[str] = (
|
||||||
str
|
None # if no equivalent model in allowed region - default all requests to this model
|
||||||
] = None # if no equivalent model in allowed region - default all requests to this model
|
)
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -935,12 +935,12 @@ class UpdateCustomerRequest(LiteLLMPydanticObjectBase):
|
||||||
blocked: bool = False # allow/disallow requests for this end-user
|
blocked: bool = False # allow/disallow requests for this end-user
|
||||||
max_budget: Optional[float] = None
|
max_budget: Optional[float] = None
|
||||||
budget_id: Optional[str] = None # give either a budget_id or max_budget
|
budget_id: Optional[str] = None # give either a budget_id or max_budget
|
||||||
allowed_model_region: Optional[
|
allowed_model_region: Optional[AllowedModelRegion] = (
|
||||||
AllowedModelRegion
|
None # require all user requests to use models in this specific region
|
||||||
] = None # require all user requests to use models in this specific region
|
)
|
||||||
default_model: Optional[
|
default_model: Optional[str] = (
|
||||||
str
|
None # if no equivalent model in allowed region - default all requests to this model
|
||||||
] = None # if no equivalent model in allowed region - default all requests to this model
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeleteCustomerRequest(LiteLLMPydanticObjectBase):
|
class DeleteCustomerRequest(LiteLLMPydanticObjectBase):
|
||||||
|
@ -1076,9 +1076,9 @@ class BlockKeyRequest(LiteLLMPydanticObjectBase):
|
||||||
|
|
||||||
class AddTeamCallback(LiteLLMPydanticObjectBase):
|
class AddTeamCallback(LiteLLMPydanticObjectBase):
|
||||||
callback_name: str
|
callback_name: str
|
||||||
callback_type: Optional[
|
callback_type: Optional[Literal["success", "failure", "success_and_failure"]] = (
|
||||||
Literal["success", "failure", "success_and_failure"]
|
"success_and_failure"
|
||||||
] = "success_and_failure"
|
)
|
||||||
callback_vars: Dict[str, str]
|
callback_vars: Dict[str, str]
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
|
@ -1335,9 +1335,9 @@ class ConfigList(LiteLLMPydanticObjectBase):
|
||||||
stored_in_db: Optional[bool]
|
stored_in_db: Optional[bool]
|
||||||
field_default_value: Any
|
field_default_value: Any
|
||||||
premium_field: bool = False
|
premium_field: bool = False
|
||||||
nested_fields: Optional[
|
nested_fields: Optional[List[FieldDetail]] = (
|
||||||
List[FieldDetail]
|
None # For nested dictionary or Pydantic fields
|
||||||
] = None # For nested dictionary or Pydantic fields
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
|
class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
|
||||||
|
@ -1604,9 +1604,9 @@ class LiteLLM_OrganizationMembershipTable(LiteLLMPydanticObjectBase):
|
||||||
budget_id: Optional[str] = None
|
budget_id: Optional[str] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
user: Optional[
|
user: Optional[Any] = (
|
||||||
Any
|
None # You might want to replace 'Any' with a more specific type if available
|
||||||
] = None # You might want to replace 'Any' with a more specific type if available
|
)
|
||||||
litellm_budget_table: Optional[LiteLLM_BudgetTable] = None
|
litellm_budget_table: Optional[LiteLLM_BudgetTable] = None
|
||||||
|
|
||||||
model_config = ConfigDict(protected_namespaces=())
|
model_config = ConfigDict(protected_namespaces=())
|
||||||
|
@ -1671,6 +1671,8 @@ class LiteLLM_UserTable(LiteLLMPydanticObjectBase):
|
||||||
budget_duration: Optional[str] = None
|
budget_duration: Optional[str] = None
|
||||||
budget_reset_at: Optional[datetime] = None
|
budget_reset_at: Optional[datetime] = None
|
||||||
metadata: Optional[dict] = None
|
metadata: Optional[dict] = None
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -2352,9 +2354,9 @@ class TeamModelDeleteRequest(BaseModel):
|
||||||
# Organization Member Requests
|
# Organization Member Requests
|
||||||
class OrganizationMemberAddRequest(OrgMemberAddRequest):
|
class OrganizationMemberAddRequest(OrgMemberAddRequest):
|
||||||
organization_id: str
|
organization_id: str
|
||||||
max_budget_in_organization: Optional[
|
max_budget_in_organization: Optional[float] = (
|
||||||
float
|
None # Users max budget within the organization
|
||||||
] = None # Users max budget within the organization
|
)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationMemberDeleteRequest(MemberDeleteRequest):
|
class OrganizationMemberDeleteRequest(MemberDeleteRequest):
|
||||||
|
@ -2543,9 +2545,9 @@ class ProviderBudgetResponse(LiteLLMPydanticObjectBase):
|
||||||
Maps provider names to their budget configs.
|
Maps provider names to their budget configs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
providers: Dict[
|
providers: Dict[str, ProviderBudgetResponseObject] = (
|
||||||
str, ProviderBudgetResponseObject
|
{}
|
||||||
] = {} # Dictionary mapping provider names to their budget configurations
|
) # Dictionary mapping provider names to their budget configurations
|
||||||
|
|
||||||
|
|
||||||
class ProxyStateVariables(TypedDict):
|
class ProxyStateVariables(TypedDict):
|
||||||
|
@ -2673,9 +2675,9 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase):
|
||||||
enforce_rbac: bool = False
|
enforce_rbac: bool = False
|
||||||
roles_jwt_field: Optional[str] = None # v2 on role mappings
|
roles_jwt_field: Optional[str] = None # v2 on role mappings
|
||||||
role_mappings: Optional[List[RoleMapping]] = None
|
role_mappings: Optional[List[RoleMapping]] = None
|
||||||
object_id_jwt_field: Optional[
|
object_id_jwt_field: Optional[str] = (
|
||||||
str
|
None # can be either user / team, inferred from the role mapping
|
||||||
] = None # can be either user / team, inferred from the role mapping
|
)
|
||||||
scope_mappings: Optional[List[ScopeMapping]] = None
|
scope_mappings: Optional[List[ScopeMapping]] = None
|
||||||
enforce_scope_based_access: bool = False
|
enforce_scope_based_access: bool = False
|
||||||
enforce_team_based_model_access: bool = False
|
enforce_team_based_model_access: bool = False
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
@ -10,7 +11,12 @@ sys.path.insert(
|
||||||
) # Adds the parent directory to the system path
|
) # Adds the parent directory to the system path
|
||||||
|
|
||||||
from litellm.proxy._types import LiteLLM_UserTableFiltered, UserAPIKeyAuth
|
from litellm.proxy._types import LiteLLM_UserTableFiltered, UserAPIKeyAuth
|
||||||
from litellm.proxy.management_endpoints.internal_user_endpoints import ui_view_users
|
from litellm.proxy.management_endpoints.internal_user_endpoints import (
|
||||||
|
LiteLLM_UserTableWithKeyCount,
|
||||||
|
get_user_key_counts,
|
||||||
|
get_users,
|
||||||
|
ui_view_users,
|
||||||
|
)
|
||||||
from litellm.proxy.proxy_server import app
|
from litellm.proxy.proxy_server import app
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
@ -82,3 +88,68 @@ def test_user_daily_activity_types():
|
||||||
assert not hasattr(
|
assert not hasattr(
|
||||||
daily_spend_metadata, field
|
daily_spend_metadata, field
|
||||||
), f"Field {field} is reported in DailySpendMetadata"
|
), f"Field {field} is reported in DailySpendMetadata"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_users_includes_timestamps(mocker):
|
||||||
|
"""
|
||||||
|
Test that /user/list endpoint returns users with created_at and updated_at fields.
|
||||||
|
"""
|
||||||
|
# Mock the prisma client
|
||||||
|
mock_prisma_client = mocker.MagicMock()
|
||||||
|
|
||||||
|
# Create mock user data with timestamps
|
||||||
|
mock_user_data = {
|
||||||
|
"user_id": "test-user-timestamps",
|
||||||
|
"user_email": "timestamps@example.com",
|
||||||
|
"user_role": "internal_user",
|
||||||
|
"created_at": datetime.now(timezone.utc),
|
||||||
|
"updated_at": datetime.now(timezone.utc),
|
||||||
|
}
|
||||||
|
mock_user_row = mocker.MagicMock()
|
||||||
|
mock_user_row.model_dump.return_value = mock_user_data
|
||||||
|
|
||||||
|
# Setup the mock find_many response as an async function
|
||||||
|
async def mock_find_many(*args, **kwargs):
|
||||||
|
return [mock_user_row]
|
||||||
|
|
||||||
|
# Setup the mock count response as an async function
|
||||||
|
async def mock_count(*args, **kwargs):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
|
||||||
|
mock_prisma_client.db.litellm_usertable.count = mock_count
|
||||||
|
|
||||||
|
# Patch the prisma client import in the endpoint
|
||||||
|
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||||
|
|
||||||
|
# Mock the helper function get_user_key_counts
|
||||||
|
async def mock_get_user_key_counts(*args, **kwargs):
|
||||||
|
return {"test-user-timestamps": 0}
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"litellm.proxy.management_endpoints.internal_user_endpoints.get_user_key_counts",
|
||||||
|
mock_get_user_key_counts,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Call get_users function directly
|
||||||
|
response = await get_users(page=1, page_size=1)
|
||||||
|
|
||||||
|
print("user /list response: ", response)
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
assert response is not None
|
||||||
|
assert "users" in response
|
||||||
|
assert "total" in response
|
||||||
|
assert response["total"] == 1
|
||||||
|
assert len(response["users"]) == 1
|
||||||
|
|
||||||
|
user_response = response["users"][0]
|
||||||
|
assert user_response.user_id == "test-user-timestamps"
|
||||||
|
assert user_response.created_at is not None
|
||||||
|
assert isinstance(user_response.created_at, datetime)
|
||||||
|
assert user_response.updated_at is not None
|
||||||
|
assert isinstance(user_response.updated_at, datetime)
|
||||||
|
assert user_response.created_at == mock_user_data["created_at"]
|
||||||
|
assert user_response.updated_at == mock_user_data["updated_at"]
|
||||||
|
assert user_response.key_count == 0
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue