Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/ui_crud_endpoints/user_banner_endpoints.py: 88%
54 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1import asyncio
2import json
3from typing import Annotated, Final, Literal
5from fastapi import APIRouter, Depends, HTTPException
6from pydantic import BaseModel, Field, ValidationError, model_validator
8from litellm._uuid import uuid4
9from litellm.proxy._types import LitellmTableNames, LitellmUserRoles, UserAPIKeyAuth
10from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
11from litellm.repositories.user_banner_repository import USER_BANNER_ROW_ID, UserBannerRepository
13router: Final = APIRouter()
15USER_BANNER_MAX_MESSAGE_LENGTH: Final = 4000
17UserBannerSeverity = Literal["info", "warning", "error"]
20class UserBannerUpdate(BaseModel):
21 enabled: bool = Field(
22 default=False,
23 description="If true, the banner is shown to all authenticated dashboard users.",
24 )
25 message: str = Field(
26 default="",
27 max_length=USER_BANNER_MAX_MESSAGE_LENGTH,
28 description="Banner text shown to dashboard users. Markdown is supported.",
29 )
30 severity: UserBannerSeverity = Field(
31 default="info",
32 description="Visual style of the banner.",
33 )
35 @model_validator(mode="after")
36 def _require_message_when_enabled(self) -> "UserBannerUpdate":
37 if self.enabled and not self.message.strip():
38 raise ValueError("message must be non-empty when the banner is enabled")
39 return self
42class UserBanner(UserBannerUpdate):
43 revision: str = Field(
44 default="",
45 description=(
46 "Server-stamped opaque publish identity; a fresh value is generated on every "
47 "update so clients re-surface dismissed banners on republish."
48 ),
49 )
52class UpdateUserBannerResponse(BaseModel):
53 message: str
54 banner: UserBanner
57def parse_user_banner(raw_settings: object) -> UserBanner:
58 if raw_settings is None:
59 return UserBanner()
60 try:
61 parsed: Final = json.loads(raw_settings) if isinstance(raw_settings, str) else raw_settings
62 return UserBanner.model_validate(parsed)
63 except (json.JSONDecodeError, ValidationError):
64 return UserBanner()
67@router.get(
68 "/get/user_banner",
69 tags=["UI Settings"], # mutable-ok: FastAPI's route decorator only accepts a list
70 dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI's route decorator only accepts a list
71 response_model=UserBanner,
72)
73async def get_user_banner() -> UserBanner:
74 """
75 Get the admin-published dashboard banner.
76 Readable by any authenticated user; rendered on every dashboard page.
77 """
78 from litellm.proxy.proxy_server import prisma_client
80 if prisma_client is None: 80 ↛ 81line 80 didn't jump to line 81 because the condition on line 80 was never true
81 return UserBanner()
83 raw_settings: Final = await UserBannerRepository(prisma_client).get_raw_settings()
84 return parse_user_banner(raw_settings)
87@router.patch(
88 "/update/user_banner",
89 tags=["UI Settings"], # mutable-ok: FastAPI's route decorator only accepts a list
90 response_model=UpdateUserBannerResponse,
91)
92async def update_user_banner(
93 banner_update: UserBannerUpdate,
94 user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
95) -> UpdateUserBannerResponse:
96 """
97 Publish, edit, or unpublish the dashboard banner.
98 Only proxy admins are allowed to modify it.
99 """
100 from litellm.proxy.proxy_server import create_config_audit_log, prisma_client
102 if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true
103 raise HTTPException(status_code=403, detail="Only proxy admins can update the user banner.")
105 if prisma_client is None: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true
106 raise HTTPException(status_code=500, detail="Database not connected. Please connect a database.")
108 repository: Final = UserBannerRepository(prisma_client)
109 before: Final = parse_user_banner(await repository.get_raw_settings())
110 banner: Final = UserBanner(
111 enabled=banner_update.enabled,
112 message=banner_update.message,
113 severity=banner_update.severity,
114 revision=uuid4().hex,
115 )
117 await repository.upsert_settings(json.dumps(banner.model_dump()))
119 asyncio.create_task(
120 create_config_audit_log(
121 param_name=USER_BANNER_ROW_ID,
122 action="updated",
123 before_value=before.model_dump(),
124 after_value=banner.model_dump(),
125 user_api_key_dict=user_api_key_dict,
126 table_name=LitellmTableNames.UI_SETTINGS_TABLE_NAME,
127 )
128 )
130 return UpdateUserBannerResponse(message="User banner updated successfully", banner=banner)