mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(proxy): read blocked_user_list per call and make cache eviction best-effort
This commit is contained in:
parent
a4aeec11c4
commit
b521aee43e
4 changed files with 99 additions and 10 deletions
|
|
@ -7,7 +7,7 @@
|
|||
## This accepts a list of user id's for whom calls will be rejected
|
||||
|
||||
|
||||
from typing import Optional, Literal
|
||||
from typing import List, Literal, Optional
|
||||
import litellm
|
||||
from litellm.proxy.utils import PrismaClient
|
||||
from litellm.caching.caching import DualCache
|
||||
|
|
@ -23,18 +23,13 @@ class _ENTERPRISE_BlockedUserList(CustomLogger):
|
|||
self.prisma_client = prisma_client
|
||||
|
||||
blocked_user_list = litellm.blocked_user_list
|
||||
if blocked_user_list is None:
|
||||
self.blocked_user_list = None
|
||||
return
|
||||
|
||||
if isinstance(blocked_user_list, list):
|
||||
self.blocked_user_list = blocked_user_list
|
||||
self.file_blocked_user_list: Optional[List[str]] = None
|
||||
|
||||
if isinstance(blocked_user_list, str): # assume it's a filepath
|
||||
try:
|
||||
with open(blocked_user_list, "r") as file:
|
||||
data = file.read()
|
||||
self.blocked_user_list = data.split("\n")
|
||||
self.file_blocked_user_list = data.split("\n")
|
||||
except FileNotFoundError:
|
||||
raise Exception(
|
||||
f"File not found. blocked_user_list={blocked_user_list}"
|
||||
|
|
@ -44,6 +39,15 @@ class _ENTERPRISE_BlockedUserList(CustomLogger):
|
|||
f"An error occurred: {str(e)}, blocked_user_list={blocked_user_list}"
|
||||
)
|
||||
|
||||
@property
|
||||
def blocked_user_list(self) -> Optional[List[str]]:
|
||||
"""Read the configured list on every call, so /customer/unblock takes effect immediately"""
|
||||
if self.file_blocked_user_list is not None:
|
||||
return self.file_blocked_user_list
|
||||
if isinstance(litellm.blocked_user_list, list):
|
||||
return litellm.blocked_user_list
|
||||
return None
|
||||
|
||||
def print_verbose(self, print_statement, level: Literal["INFO", "DEBUG"] = "DEBUG"):
|
||||
if level == "INFO":
|
||||
verbose_proxy_logger.info(print_statement)
|
||||
|
|
|
|||
|
|
@ -54,8 +54,11 @@ async def _invalidate_end_user_cache(user_ids: Sequence[str]) -> None:
|
|||
from litellm.proxy.proxy_server import user_api_key_cache
|
||||
|
||||
for user_id in user_ids:
|
||||
await user_api_key_cache.async_delete_cache(key=f"end_user_id:{user_id}")
|
||||
await user_api_key_cache.async_delete_cache(key=f"litellm:end_user_id:{user_id}")
|
||||
try:
|
||||
await user_api_key_cache.async_delete_cache(key=f"end_user_id:{user_id}")
|
||||
await user_api_key_cache.async_delete_cache(key=f"litellm:end_user_id:{user_id}")
|
||||
except Exception as e: # noqa: BLE001 # the row is already written; a cached view expires on its own
|
||||
verbose_proxy_logger.error(f"Failed to evict cached end-user {user_id} - {e!s}")
|
||||
|
||||
|
||||
def _to_customer_response(record: BaseModel) -> CustomerResponse:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../../.."))
|
||||
|
||||
import litellm
|
||||
from litellm.caching.caching import DualCache
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hook():
|
||||
from enterprise.enterprise_hooks.blocked_user_list import (
|
||||
_ENTERPRISE_BlockedUserList,
|
||||
)
|
||||
|
||||
return _ENTERPRISE_BlockedUserList(prisma_client=None)
|
||||
|
||||
|
||||
async def _call(hook, user_id: str):
|
||||
return await hook.async_pre_call_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
cache=DualCache(),
|
||||
data={"user": user_id},
|
||||
call_type="completion",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hook_reads_blocked_user_list_on_every_call(monkeypatch, hook):
|
||||
"""Regression: the hook used to keep the list object it read at init, so /customer/unblock
|
||||
rebinding litellm.blocked_user_list left the hook rejecting an already unblocked customer.
|
||||
"""
|
||||
monkeypatch.setattr(litellm, "blocked_user_list", ["blocked-1"])
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await _call(hook, "blocked-1")
|
||||
assert "blocked-1" in str(exc_info.value.detail)
|
||||
|
||||
monkeypatch.setattr(litellm, "blocked_user_list", [])
|
||||
|
||||
assert await _call(hook, "blocked-1") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hook_keeps_filepath_backed_list(monkeypatch, tmp_path):
|
||||
from enterprise.enterprise_hooks.blocked_user_list import (
|
||||
_ENTERPRISE_BlockedUserList,
|
||||
)
|
||||
|
||||
blocked_users_file = tmp_path / "blocked_users.txt"
|
||||
blocked_users_file.write_text("blocked-1")
|
||||
monkeypatch.setattr(litellm, "blocked_user_list", str(blocked_users_file))
|
||||
|
||||
file_backed_hook = _ENTERPRISE_BlockedUserList(prisma_client=None)
|
||||
|
||||
monkeypatch.setattr(litellm, "blocked_user_list", [])
|
||||
|
||||
with pytest.raises(HTTPException):
|
||||
await _call(file_backed_hook, "blocked-1")
|
||||
|
|
@ -870,6 +870,25 @@ def test_unblock_customer_without_enterprise_callback_still_persists(
|
|||
mock_prisma_client.db.litellm_endusertable.update_many.assert_awaited_once()
|
||||
|
||||
|
||||
def test_unblock_customer_survives_cache_eviction_failure(
|
||||
mock_prisma_client, mock_user_api_key_auth, mock_end_user_cache, monkeypatch
|
||||
):
|
||||
"""An unreachable cache backend must not turn an already committed unblock into a 500."""
|
||||
monkeypatch.setattr(litellm, "blocked_user_list", ["blocked-1"])
|
||||
mock_prisma_client.db.litellm_endusertable.update_many = AsyncMock(return_value=1)
|
||||
mock_end_user_cache.async_delete_cache = AsyncMock(side_effect=Exception("redis down"))
|
||||
|
||||
response = client.post(
|
||||
"/customer/unblock",
|
||||
json={"user_ids": ["blocked-1"]},
|
||||
headers={"Authorization": "Bearer test-key"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"blocked_users": []}
|
||||
mock_prisma_client.db.litellm_endusertable.update_many.assert_awaited_once()
|
||||
|
||||
|
||||
def test_unblock_customer_rejects_filepath_blocked_user_list(
|
||||
mock_prisma_client, mock_user_api_key_auth, mock_end_user_cache, monkeypatch
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue