-
-
Notifications
You must be signed in to change notification settings - Fork 9.9k
litellm ryan march 20 #24323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
litellm ryan march 20 #24323
Changes from 15 commits
c1b3640
364f5c6
1d7cff2
1da02b6
9c47d50
ae21527
4e32db2
8766004
e1d2ab3
c68a8aa
031b3d8
41b1c44
b21948f
7738710
d2a0f79
9b90e80
f494ab5
3e27ff1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,19 +2,111 @@ | |
| Functions to create audit logs for LiteLLM Proxy | ||
| """ | ||
|
|
||
| import asyncio | ||
| import json | ||
| from litellm._uuid import uuid | ||
| from datetime import datetime, timezone | ||
| from typing import Dict | ||
|
|
||
| import litellm | ||
| from litellm._logging import verbose_proxy_logger | ||
| from litellm._uuid import uuid | ||
| from litellm.integrations.custom_logger import CustomLogger | ||
| from litellm.proxy._types import ( | ||
| AUDIT_ACTIONS, | ||
| LiteLLM_AuditLogs, | ||
| LitellmTableNames, | ||
| Optional, | ||
| UserAPIKeyAuth, | ||
| ) | ||
| from litellm.types.utils import StandardAuditLogPayload | ||
|
|
||
| _audit_log_callback_cache: Dict[str, CustomLogger] = {} | ||
|
|
||
|
|
||
| def _resolve_audit_log_callback(name: str) -> Optional[CustomLogger]: | ||
| """Resolve a string callback name to a CustomLogger instance, with caching.""" | ||
| if name in _audit_log_callback_cache: | ||
| return _audit_log_callback_cache[name] | ||
|
|
||
| from litellm.litellm_core_utils.litellm_logging import ( | ||
| _init_custom_logger_compatible_class, | ||
| ) | ||
|
|
||
| instance = _init_custom_logger_compatible_class( | ||
| logging_integration=name, # type: ignore | ||
| internal_usage_cache=None, | ||
| llm_router=None, | ||
| ) | ||
|
|
||
| if instance is not None: | ||
| _audit_log_callback_cache[name] = instance | ||
| return instance | ||
|
Comment on lines
+23
to
+43
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
To fix, add a cache-clearing step whenever def _clear_audit_log_callback_cache() -> None:
"""Invalidate the resolved-callback cache (call on config reload)."""
_audit_log_callback_cache.clear()And in elif key == "audit_log_callbacks":
from litellm.proxy.management_helpers.audit_logs import _clear_audit_log_callback_cache
_clear_audit_log_callback_cache()
litellm.audit_log_callbacks = []
... |
||
|
|
||
|
|
||
| def _build_audit_log_payload( | ||
| request_data: LiteLLM_AuditLogs, | ||
| ) -> StandardAuditLogPayload: | ||
| """Convert LiteLLM_AuditLogs to StandardAuditLogPayload for callback dispatch.""" | ||
| updated_at = "" | ||
| if request_data.updated_at is not None: | ||
| updated_at = request_data.updated_at.isoformat() | ||
|
|
||
| table_name = request_data.table_name | ||
| if isinstance(table_name, LitellmTableNames): | ||
| table_name = table_name.value | ||
|
|
||
| return StandardAuditLogPayload( | ||
| id=request_data.id, | ||
| updated_at=updated_at, | ||
| changed_by=request_data.changed_by or "", | ||
| changed_by_api_key=request_data.changed_by_api_key or "", | ||
| action=request_data.action, | ||
| table_name=str(table_name), | ||
| object_id=request_data.object_id, | ||
| before_value=request_data.before_value, | ||
| updated_values=request_data.updated_values, | ||
| ) | ||
|
|
||
|
|
||
| def _audit_log_task_done_callback(task: asyncio.Task) -> None: | ||
| """Log exceptions from audit log callback tasks so they don't slip through silently.""" | ||
| try: | ||
| exc = task.exception() | ||
| except asyncio.CancelledError: | ||
| return | ||
| if exc is not None: | ||
| verbose_proxy_logger.error( | ||
| "Audit log callback task failed: %s", exc, exc_info=exc | ||
| ) | ||
|
|
||
|
|
||
| async def _dispatch_audit_log_to_callbacks( | ||
| request_data: LiteLLM_AuditLogs, | ||
| ) -> None: | ||
| """Dispatch audit log to all registered audit_log_callbacks.""" | ||
| if not litellm.audit_log_callbacks: | ||
| return | ||
|
|
||
| payload = _build_audit_log_payload(request_data) | ||
|
|
||
| for callback in litellm.audit_log_callbacks: | ||
| try: | ||
| resolved = callback | ||
| if isinstance(callback, str): | ||
| resolved = _resolve_audit_log_callback(callback) | ||
| if resolved is None: | ||
| verbose_proxy_logger.warning( | ||
| "Could not resolve audit log callback: %s", callback | ||
| ) | ||
| continue | ||
|
|
||
| if isinstance(resolved, CustomLogger): | ||
| task = asyncio.create_task(resolved.async_log_audit_log_event(payload)) | ||
| task.add_done_callback(_audit_log_task_done_callback) | ||
| except Exception as e: | ||
| verbose_proxy_logger.error( | ||
| "Failed dispatching audit log to callback: %s", e | ||
| ) | ||
|
Comment on lines
+90
to
+107
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is a silent failure: a user who registers a callable expecting it to be invoked will see no error and no invocation. At minimum, an for callback in litellm.audit_log_callbacks:
try:
resolved: Optional[CustomLogger] = callback if isinstance(callback, CustomLogger) else None
if isinstance(callback, str):
resolved = _resolve_audit_log_callback(callback)
if resolved is None:
verbose_proxy_logger.warning(
"Could not resolve audit log callback: %s", callback
)
continue
elif not isinstance(callback, CustomLogger):
verbose_proxy_logger.warning(
"Unsupported audit_log_callback type %s (%r) — only CustomLogger instances "
"and string names are supported.",
type(callback).__name__,
callback,
)
continue
if isinstance(resolved, CustomLogger):
task = asyncio.create_task(resolved.async_log_audit_log_event(payload))
task.add_done_callback(_audit_log_task_done_callback)
except Exception as e:
... |
||
|
|
||
|
|
||
| async def create_object_audit_log( | ||
|
|
@@ -81,9 +173,6 @@ async def create_audit_log_for_update(request_data: LiteLLM_AuditLogs): | |
| if premium_user is not True: | ||
| return | ||
|
|
||
| if prisma_client is None: | ||
| raise Exception("prisma_client is None, no DB connected") | ||
|
|
||
| verbose_proxy_logger.debug("creating audit log for %s", request_data) | ||
|
|
||
| if isinstance(request_data.updated_values, dict): | ||
|
|
@@ -92,6 +181,15 @@ async def create_audit_log_for_update(request_data: LiteLLM_AuditLogs): | |
| if isinstance(request_data.before_value, dict): | ||
| request_data.before_value = json.dumps(request_data.before_value) | ||
|
|
||
| # Dispatch to external audit log callbacks regardless of DB availability | ||
| await _dispatch_audit_log_to_callbacks(request_data) | ||
|
|
||
| if prisma_client is None: | ||
| verbose_proxy_logger.error( | ||
| "prisma_client is None, cannot write audit log to DB" | ||
| ) | ||
| return | ||
|
|
||
| _request_data = request_data.model_dump(exclude_none=True) | ||
|
|
||
| try: | ||
|
|
@@ -103,5 +201,3 @@ async def create_audit_log_for_update(request_data: LiteLLM_AuditLogs): | |
| except Exception as e: | ||
| # [Non-Blocking Exception. Do not allow blocking LLM API call] | ||
| verbose_proxy_logger.error(f"Failed Creating audit log {e}") | ||
|
|
||
| return | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2961,6 +2961,29 @@ async def load_config( # noqa: PLR0915 | |||||||||||||||||||||
| print( # noqa | ||||||||||||||||||||||
| f"{blue_color_code} Initialized Failure Callbacks - {litellm.failure_callback} {reset_color_code}" | ||||||||||||||||||||||
| ) # noqa | ||||||||||||||||||||||
| elif key == "audit_log_callbacks": | ||||||||||||||||||||||
| litellm.audit_log_callbacks = [] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| for callback in value: | ||||||||||||||||||||||
| if "." in callback: | ||||||||||||||||||||||
| litellm.audit_log_callbacks.append( | ||||||||||||||||||||||
| get_instance_fn(value=callback) | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| else: | ||||||||||||||||||||||
| litellm.audit_log_callbacks.append(callback) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| _store_audit_logs = litellm_settings.get( | ||||||||||||||||||||||
| "store_audit_logs", litellm.store_audit_logs | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| if _store_audit_logs: | ||||||||||||||||||||||
| print( # noqa | ||||||||||||||||||||||
| f"{blue_color_code} Initialized Audit Log Callbacks - {litellm.audit_log_callbacks} {reset_color_code}" | ||||||||||||||||||||||
| ) # noqa | ||||||||||||||||||||||
| else: | ||||||||||||||||||||||
| verbose_proxy_logger.warning( | ||||||||||||||||||||||
| "'audit_log_callbacks' is configured but 'store_audit_logs' is not enabled. " | ||||||||||||||||||||||
| "Audit log callbacks will not fire until 'store_audit_logs: true' is added to litellm_settings." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
Comment on lines
+2982
to
+2986
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
| elif key == "cache_params": | ||||||||||||||||||||||
| # this is set in the cache branch | ||||||||||||||||||||||
| # see usage here: https://docs.litellm.ai/docs/proxy/caching | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The key-name patterns (e.g.
access_token,refresh_token,webhook_url) are not anchored with word boundaries or negative lookbehinds. This means a key such asoauth_access_token,last_access_token, ormy_webhook_urlwould produce a partial match, e.g.:The regex matches
access_token"(consuming the trailing"of the key name), then: ", thenmy-value— leaving a dangling"last_prefix and a broken closing"in the log line.Consider adding a word-boundary or negative lookbehind to anchor each key name:
Note: the existing
api[_-]?keypattern already sidesteps this by requiring{8,}characters for the value. The new patterns have no such minimum length guard either.