mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
* fix(proxy): scan batch records with the content hooks that are not guardrails Guardrails were made to run on batch uploads by scanning each record through the pre-call hook with the walk limited to guardrails. That limit exists because the same branch carries the rate limiters and budget accounting, which must count an upload once rather than once per line. It also excluded every enforcement hook written as a plain CustomLogger, so prompt-injection detection, Azure content safety, banned keywords and the blocked-user check never saw a batch record at all. Content that is a hard 400 online reached the provider verbatim through batch. A CustomLogger now declares whether its pre-call hook judges the payload or merely counts the request. The four that judge it opt in, the walk admits them, and both short-circuits learn about them, including the one that decides whether the file is streamed off disk in the first place: a proxy configured only with one of these hooks was skipping the scan entirely. Nothing that counts a request is marked, so an upload still costs one slot and one budget check. * refactor(proxy): drop the per-hook comment the attribute contract already states * test(proxy): make the classification a ledger, and pin the wiring with a real hook The classification test listed the two non-enterprise hooks by hand, so unmarking either enterprise one changed nothing and the mutation matrix passed with both surviving. It now walks the hook registries and fails on any pre-call CustomLogger that is on neither side, which also gives the flag the forcing function it lacked: an enforcement hook added later would otherwise default to off and silently skip batch records, which is the bug being fixed here. Nothing exercised the path the bug actually lived on either, since every test raised its own exception rather than a real hook's. One test now drives the shipped prompt-injection hook through the scan, which pins the part no synthetic exception reaches: a chained exception reads as a failure to judge, so refactoring any of these hooks to `raise ... from` would turn every per-record drop into an aborted upload. Also records why a hook that rewrites the payload for routing stays unmarked, and that only the leaf class is consulted. * test(proxy): set the callback list through monkeypatch rather than writing the global
125 lines
4.8 KiB
Python
125 lines
4.8 KiB
Python
# +------------------------------+
|
|
#
|
|
# Blocked User List
|
|
#
|
|
# +------------------------------+
|
|
# Thank you users! We ❤️ you! - Krrish & Ishaan
|
|
## This accepts a list of user id's for whom calls will be rejected
|
|
|
|
|
|
from typing import Optional, Literal
|
|
import litellm
|
|
from litellm.proxy.utils import PrismaClient
|
|
from litellm.caching.caching import DualCache
|
|
from litellm.proxy._types import UserAPIKeyAuth, LiteLLM_EndUserTable
|
|
from litellm.integrations.custom_logger import CustomLogger
|
|
from litellm._logging import verbose_proxy_logger
|
|
from fastapi import HTTPException
|
|
|
|
|
|
class _ENTERPRISE_BlockedUserList(CustomLogger):
|
|
enforces_request_content: bool = True
|
|
# Class variables or attributes
|
|
def __init__(self, prisma_client: Optional[PrismaClient]):
|
|
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
|
|
|
|
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")
|
|
except FileNotFoundError:
|
|
raise Exception(
|
|
f"File not found. blocked_user_list={blocked_user_list}"
|
|
)
|
|
except Exception as e:
|
|
raise Exception(
|
|
f"An error occurred: {str(e)}, blocked_user_list={blocked_user_list}"
|
|
)
|
|
|
|
def print_verbose(self, print_statement, level: Literal["INFO", "DEBUG"] = "DEBUG"):
|
|
if level == "INFO":
|
|
verbose_proxy_logger.info(print_statement)
|
|
elif level == "DEBUG":
|
|
verbose_proxy_logger.debug(print_statement)
|
|
|
|
if litellm.set_verbose is True:
|
|
print(print_statement) # noqa
|
|
|
|
async def async_pre_call_hook(
|
|
self,
|
|
user_api_key_dict: UserAPIKeyAuth,
|
|
cache: DualCache,
|
|
data: dict,
|
|
call_type: str,
|
|
):
|
|
try:
|
|
"""
|
|
- check if user id part of call
|
|
- check if user id part of blocked list
|
|
- if blocked list is none or user not in blocked list
|
|
- check if end-user in cache
|
|
- check if end-user in db
|
|
"""
|
|
self.print_verbose("Inside Blocked User List Pre-Call Hook")
|
|
if "user_id" in data or "user" in data:
|
|
user = data.get("user_id", data.get("user", ""))
|
|
if (
|
|
self.blocked_user_list is not None
|
|
and user in self.blocked_user_list
|
|
):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error": f"User blocked from making LLM API Calls. User={user}"
|
|
},
|
|
)
|
|
|
|
cache_key = f"litellm:end_user_id:{user}"
|
|
end_user_cache_obj: Optional[LiteLLM_EndUserTable] = cache.get_cache( # type: ignore
|
|
key=cache_key
|
|
)
|
|
if end_user_cache_obj is None and self.prisma_client is not None:
|
|
# check db
|
|
end_user_obj = (
|
|
await self.prisma_client.db.litellm_endusertable.find_unique(
|
|
where={"user_id": user}
|
|
)
|
|
)
|
|
if end_user_obj is None: # user not in db - assume not blocked
|
|
end_user_obj = LiteLLM_EndUserTable(user_id=user, blocked=False)
|
|
cache.set_cache(key=cache_key, value=end_user_obj, ttl=60)
|
|
if end_user_obj is not None and end_user_obj.blocked is True:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error": f"User blocked from making LLM API Calls. User={user}"
|
|
},
|
|
)
|
|
elif (
|
|
end_user_cache_obj is not None
|
|
and end_user_cache_obj.blocked is True
|
|
):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error": f"User blocked from making LLM API Calls. User={user}"
|
|
},
|
|
)
|
|
|
|
except HTTPException as e:
|
|
raise e
|
|
except Exception as e:
|
|
verbose_proxy_logger.exception(
|
|
"litellm.enterprise.enterprise_hooks.blocked_user_list::async_pre_call_hook - Exception occurred - {}".format(
|
|
str(e)
|
|
)
|
|
)
|