mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-06 07:48:06 +00:00
feat: add opt-in diagnostic message redaction
This commit is contained in:
parent
8c9a76859d
commit
8d9bd9045a
49 changed files with 942 additions and 99 deletions
18
README.md
18
README.md
|
|
@ -141,6 +141,24 @@ response = client.chat.completions.create(
|
|||
)
|
||||
```
|
||||
|
||||
#### Debug logs without message content
|
||||
|
||||
To troubleshoot with detailed logging, add these settings to your existing proxy configuration and restart with `litellm --config config.yaml --detailed_debug`
|
||||
|
||||
```yaml
|
||||
litellm_settings:
|
||||
redact_messages_in_debug_logs: true
|
||||
turn_off_message_logging: true
|
||||
```
|
||||
|
||||
`redact_messages_in_debug_logs` defaults to `false`. When enabled, LiteLLM replaces free-form diagnostic messages, payloads, and exception text with `redacted-by-litellm`, retaining selected operational events. This also applies to payload diagnostics emitted at warning or error level. Raw tracebacks and caller-supplied correlation identifiers are omitted because they can contain message content
|
||||
|
||||
The setting affects diagnostics only. Requests sent to providers and responses returned to clients stay intact. `turn_off_message_logging` separately controls message content sent to logging integrations. Request-level callback overrides do not disable diagnostic redaction. Custom application loggers and independently configured third-party wire logging need their own privacy controls
|
||||
|
||||
The built-in plain and JSON formatters support this mode. Custom formatters must use standard log fields or supply defaults for extra fields, which are removed to prevent content disclosure. This Python SDK/proxy setting does not configure the standalone Rust gateway
|
||||
|
||||
Verify with a synthetic prompt: the client should receive its normal response while LiteLLM diagnostics show `redacted-by-litellm` instead of the prompt or reply. Remove the new setting or set it to `false`, then restart the proxy to restore existing diagnostics
|
||||
|
||||
[**Docs: LLM Providers**](https://docs.litellm.ai/docs/providers)
|
||||
|
||||
</details>
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ _async_failure_callback: List[Union[str, Callable[..., object], "CustomLogger"]]
|
|||
pre_call_rules: List[Callable] = []
|
||||
post_call_rules: List[Callable[..., object]] = []
|
||||
turn_off_message_logging: Optional[bool] = False
|
||||
redact_messages_in_debug_logs: bool = False
|
||||
standard_logging_payload_excluded_fields: Optional[List[str]] = (
|
||||
None # Fields to exclude from StandardLoggingPayload before callbacks receive it
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ import logging
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
from collections.abc import Iterator, Mapping
|
||||
from datetime import datetime
|
||||
from logging import Formatter
|
||||
from typing import Final, TextIO
|
||||
from typing import Final, Literal, TextIO
|
||||
from urllib.parse import unquote
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
|
@ -21,6 +22,7 @@ from litellm.constants import (
|
|||
LITELLM_TRUNCATION_STDOUT_SAFEGUARD_NOTE,
|
||||
MAX_BASE64_LENGTH_STDOUT_LOG,
|
||||
MAX_STRING_LENGTH_STDOUT_LOG,
|
||||
REDACTED_BY_LITELLM,
|
||||
)
|
||||
from litellm.litellm_core_utils.env_utils import get_env_int
|
||||
from litellm.litellm_core_utils.safe_json_dumps import UNSERIALIZABLE_OBJECT, safe_dumps, safe_json_structure
|
||||
|
|
@ -87,6 +89,88 @@ _redact_string = redact_string
|
|||
|
||||
_REDACTED_RECORD_ATTR: Final = "litellm_redacted"
|
||||
_REDACTED_STAMP: Final = object()
|
||||
_SAFE_DIAGNOSTIC_ATTR: Final = "litellm_safe_diagnostic"
|
||||
_SAFE_DIAGNOSTIC_STAMP: Final = object()
|
||||
|
||||
|
||||
def diagnostic_message_logging_disabled() -> bool:
|
||||
return getattr(litellm, "redact_messages_in_debug_logs", False) is True
|
||||
|
||||
|
||||
def _litellm_exception_record(record: logging.LogRecord) -> bool:
|
||||
if not isinstance(record.exc_info, tuple) or record.exc_info[2] is None:
|
||||
return False
|
||||
return any(
|
||||
isinstance(module := frame.f_globals.get("__name__"), str)
|
||||
and (module == "litellm" or module.startswith("litellm."))
|
||||
for frame, _ in traceback.walk_tb(record.exc_info[2])
|
||||
)
|
||||
|
||||
|
||||
def _redact_diagnostic_record(record: logging.LogRecord) -> None:
|
||||
if (
|
||||
not diagnostic_message_logging_disabled()
|
||||
or getattr(record, _SAFE_DIAGNOSTIC_ATTR, None) is _SAFE_DIAGNOSTIC_STAMP
|
||||
):
|
||||
return
|
||||
owned: Final = record.name in (
|
||||
"LiteLLM",
|
||||
"LiteLLM Router",
|
||||
"LiteLLM Proxy",
|
||||
"uvicorn.error",
|
||||
) or record.name.startswith(("LiteLLM.", "LiteLLM Router.", "LiteLLM Proxy.", "litellm."))
|
||||
if not owned and not (record.name == "asyncio" and _litellm_exception_record(record)):
|
||||
return
|
||||
record.msg = REDACTED_BY_LITELLM # rebind-ok: logging filters sanitize the diagnostic copy
|
||||
record.args = () # rebind-ok: never render payload arguments
|
||||
record.exc_info = None # rebind-ok: exception messages may echo request content
|
||||
record.exc_text = None # rebind-ok: remove previously rendered exception content
|
||||
record.stack_info = None # rebind-ok: stack text may contain payloads
|
||||
for key in tuple(record.__dict__):
|
||||
if key not in _STANDARD_RECORD_ATTRS:
|
||||
delattr(record, key)
|
||||
setattr(record, _REDACTED_RECORD_ATTR, _REDACTED_STAMP)
|
||||
|
||||
|
||||
class MessageContentFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
_redact_diagnostic_record(record)
|
||||
return True
|
||||
|
||||
|
||||
def log_safe_diagnostic(
|
||||
logger: logging.Logger,
|
||||
event: Literal[
|
||||
"request_sent",
|
||||
"response_received",
|
||||
"response_ready",
|
||||
"request_completed",
|
||||
"request_failed",
|
||||
"stream_completed",
|
||||
"retry",
|
||||
"fallback",
|
||||
],
|
||||
*,
|
||||
status_code: int | None = None,
|
||||
elapsed_seconds: float | None = None,
|
||||
attempt: int | None = None,
|
||||
level: int = logging.DEBUG,
|
||||
) -> None:
|
||||
if not diagnostic_message_logging_disabled() or not logger.isEnabledFor(level):
|
||||
return
|
||||
logger.log(
|
||||
level,
|
||||
"event=%s status=%s elapsed_seconds=%s attempt=%s content=%s",
|
||||
event,
|
||||
status_code,
|
||||
elapsed_seconds,
|
||||
attempt,
|
||||
REDACTED_BY_LITELLM,
|
||||
extra={_SAFE_DIAGNOSTIC_ATTR: _SAFE_DIAGNOSTIC_STAMP},
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
_DECODED_LOG_PAYLOAD: Final = TypeAdapter(object)
|
||||
_UNREDACTED_SCALAR_TYPES: Final = (bool, int, float, type(None))
|
||||
|
||||
|
|
@ -162,6 +246,7 @@ class SecretRedactionFilter(logging.Filter):
|
|||
_formatter = logging.Formatter()
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
_redact_diagnostic_record(record)
|
||||
if not _ENABLE_SECRET_REDACTION or _is_redacted(record):
|
||||
return True
|
||||
return _process_record(record, base64_limit=0, text_limit=0, redact=True)
|
||||
|
|
@ -222,6 +307,23 @@ class AccessLogRedactionFilter(logging.Filter):
|
|||
"""
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
if diagnostic_message_logging_disabled():
|
||||
safe_args: Final = (
|
||||
tuple(value if isinstance(value, (int, float, bool)) else REDACTED_BY_LITELLM for value in record.args)
|
||||
if isinstance(record.args, tuple) and len(record.args) == 5
|
||||
else ()
|
||||
)
|
||||
record.msg = ( # rebind-ok: access diagnostic copy
|
||||
'%s - "%s %s HTTP/%s" %s' if safe_args else REDACTED_BY_LITELLM
|
||||
)
|
||||
record.args = safe_args # rebind-ok: preserve the access formatter's tuple contract
|
||||
record.exc_info = None # rebind-ok: exceptions can echo request content
|
||||
record.exc_text = None # rebind-ok: discard cached exception content
|
||||
record.stack_info = None # rebind-ok: discard stack content
|
||||
for key in tuple(record.__dict__):
|
||||
if key not in _STANDARD_RECORD_ATTRS:
|
||||
delattr(record, key)
|
||||
return True
|
||||
if not _ENABLE_SECRET_REDACTION:
|
||||
return True
|
||||
if isinstance(record.args, tuple) and record.args:
|
||||
|
|
@ -431,6 +533,7 @@ def _render_exception(record: logging.LogRecord) -> str | None:
|
|||
|
||||
|
||||
def _process_record(record: logging.LogRecord, *, base64_limit: int, text_limit: int, redact: bool) -> bool:
|
||||
_redact_diagnostic_record(record)
|
||||
if _is_redacted(record):
|
||||
return True
|
||||
message: Final = _render_message(record)
|
||||
|
|
@ -448,7 +551,7 @@ def _process_record(record: logging.LogRecord, *, base64_limit: int, text_limit:
|
|||
)
|
||||
for key, value in _record_attributes(record).items()
|
||||
if key not in _STANDARD_RECORD_ATTRS
|
||||
and key != _REDACTED_RECORD_ATTR
|
||||
and key not in (_REDACTED_RECORD_ATTR, _SAFE_DIAGNOSTIC_ATTR)
|
||||
and not isinstance(value, _UNREDACTED_SCALAR_TYPES)
|
||||
)
|
||||
if redact
|
||||
|
|
@ -532,6 +635,7 @@ class StdoutLogTruncationFilter(logging.Filter):
|
|||
_formatter = logging.Formatter()
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
_redact_diagnostic_record(record)
|
||||
try:
|
||||
message: Final = record.getMessage()
|
||||
except (TypeError, ValueError):
|
||||
|
|
@ -580,7 +684,7 @@ class CorrelationContextFilter(logging.Filter):
|
|||
"""
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
if not litellm.request_correlation_in_logs:
|
||||
if diagnostic_message_logging_disabled() or not litellm.request_correlation_in_logs:
|
||||
return True
|
||||
trace_id: Final = trace_id_var.get()
|
||||
if trace_id:
|
||||
|
|
@ -719,13 +823,13 @@ def _try_parse_embedded_python_dict(message: str) -> dict[str, object] | None:
|
|||
|
||||
# Standard LogRecord attribute names - used to identify 'extra' fields.
|
||||
# Derived at runtime so we automatically include version-specific attrs (e.g. taskName).
|
||||
def _get_standard_record_attrs() -> frozenset:
|
||||
def _get_standard_record_attrs() -> frozenset[str]:
|
||||
"""Standard LogRecord attribute names - excludes extra keys from logger.debug(..., extra={...})."""
|
||||
return frozenset(logging.LogRecord("", 0, "", 0, "", (), None).__dict__.keys())
|
||||
|
||||
|
||||
_STANDARD_RECORD_ATTRS: Final = _get_standard_record_attrs()
|
||||
_NON_EXTRA_RECORD_ATTRS: Final = _STANDARD_RECORD_ATTRS | {_REDACTED_RECORD_ATTR}
|
||||
_NON_EXTRA_RECORD_ATTRS: Final = _STANDARD_RECORD_ATTRS | {_REDACTED_RECORD_ATTR, _SAFE_DIAGNOSTIC_ATTR}
|
||||
|
||||
# CorrelationContextFilter is the only legitimate source for these two JSON fields;
|
||||
# see JsonFormatter.format() for why they're excluded from the generic message-content
|
||||
|
|
@ -743,6 +847,7 @@ class JsonFormatter(Formatter):
|
|||
return dt.isoformat()
|
||||
|
||||
def format(self, record):
|
||||
_redact_diagnostic_record(record)
|
||||
message_str: Final = record.getMessage()
|
||||
json_record: Final[dict[str, object]] = {
|
||||
"message": message_str,
|
||||
|
|
@ -833,7 +938,7 @@ def _setup_json_exception_handlers(formatter):
|
|||
level=logging.ERROR,
|
||||
pathname="",
|
||||
lineno=0,
|
||||
msg=str(exc_value),
|
||||
msg=REDACTED_BY_LITELLM if diagnostic_message_logging_disabled() else str(exc_value),
|
||||
args=(),
|
||||
exc_info=(exc_type, exc_value, exc_traceback),
|
||||
)
|
||||
|
|
@ -854,7 +959,7 @@ def _setup_json_exception_handlers(formatter):
|
|||
level=logging.ERROR,
|
||||
pathname="",
|
||||
lineno=0,
|
||||
msg=str(exception),
|
||||
msg=REDACTED_BY_LITELLM if diagnostic_message_logging_disabled() else str(exception),
|
||||
args=(),
|
||||
exc_info=(exc_type, exception, exception.__traceback__),
|
||||
)
|
||||
|
|
@ -1153,7 +1258,13 @@ def print_verbose(print_statement: object, *args: object) -> None:
|
|||
try:
|
||||
if not set_verbose:
|
||||
return
|
||||
message: Final = str(print_statement) % args if args else str(print_statement)
|
||||
message: Final = (
|
||||
REDACTED_BY_LITELLM
|
||||
if diagnostic_message_logging_disabled()
|
||||
else str(print_statement) % args
|
||||
if args
|
||||
else str(print_statement)
|
||||
)
|
||||
print(redact_secrets(message)) # noqa: T201
|
||||
except Exception:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class BudgetManager:
|
|||
if litellm.set_verbose:
|
||||
import logging
|
||||
|
||||
logging.info(print_statement)
|
||||
logging.info("redacted-by-litellm" if litellm.redact_messages_in_debug_logs else print_statement)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ def print_verbose(print_statement):
|
|||
try:
|
||||
verbose_logger.debug(print_statement)
|
||||
if litellm.set_verbose:
|
||||
print(print_statement) # noqa: T201
|
||||
print("redacted-by-litellm" if litellm.redact_messages_in_debug_logs else print_statement) # noqa: T201
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -198,7 +198,9 @@ class DualCache(BaseCache):
|
|||
|
||||
result = redis_result
|
||||
|
||||
print_verbose(f"get cache: cache result: {result}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm" if litellm.redact_messages_in_debug_logs else f"get cache: cache result: {result}"
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
log_redis_failure(
|
||||
|
|
@ -262,7 +264,11 @@ class DualCache(BaseCache):
|
|||
if self.in_memory_cache is not None:
|
||||
in_memory_result: Final = await self.in_memory_cache.async_get_cache(key, **kwargs)
|
||||
|
||||
print_verbose(f"in_memory_result: {in_memory_result}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"in_memory_result: {in_memory_result}"
|
||||
)
|
||||
if in_memory_result is not None:
|
||||
result = in_memory_result
|
||||
|
||||
|
|
@ -279,7 +285,9 @@ class DualCache(BaseCache):
|
|||
|
||||
result = redis_result
|
||||
|
||||
print_verbose(f"get cache: cache result: {result}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm" if litellm.redact_messages_in_debug_logs else f"get cache: cache result: {result}"
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
log_redis_failure(
|
||||
|
|
|
|||
|
|
@ -208,7 +208,11 @@ class QdrantSemanticCache(BaseCache):
|
|||
},
|
||||
)
|
||||
if response.status_code not in (200, 201):
|
||||
print_verbose(f"Qdrant semantic-cache could not create cache-key payload index: {response.text}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"Qdrant semantic-cache could not create cache-key payload index: {response.text}"
|
||||
)
|
||||
except Exception as exc:
|
||||
print_verbose(f"Qdrant semantic-cache could not create cache-key payload index: {exc}")
|
||||
|
||||
|
|
@ -284,7 +288,11 @@ class QdrantSemanticCache(BaseCache):
|
|||
return await asyncio.wait_for(embedding_call, self.embedding_timeout)
|
||||
|
||||
def set_cache(self, key, value, **kwargs):
|
||||
print_verbose(f"qdrant semantic-cache set_cache, kwargs: {kwargs}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"qdrant semantic-cache set_cache, kwargs: {kwargs}"
|
||||
)
|
||||
from litellm._uuid import uuid
|
||||
|
||||
# get the prompt
|
||||
|
|
@ -324,7 +332,11 @@ class QdrantSemanticCache(BaseCache):
|
|||
)
|
||||
|
||||
def get_cache(self, key, **kwargs):
|
||||
print_verbose(f"sync qdrant semantic-cache get_cache, kwargs: {kwargs}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"sync qdrant semantic-cache get_cache, kwargs: {kwargs}"
|
||||
)
|
||||
|
||||
# get the messages
|
||||
messages: Final = kwargs["messages"]
|
||||
|
|
@ -399,7 +411,11 @@ class QdrantSemanticCache(BaseCache):
|
|||
async def async_set_cache(self, key, value, **kwargs):
|
||||
from litellm._uuid import uuid
|
||||
|
||||
print_verbose(f"async qdrant semantic-cache set_cache, kwargs: {kwargs}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"async qdrant semantic-cache set_cache, kwargs: {kwargs}"
|
||||
)
|
||||
|
||||
# get the prompt
|
||||
messages: Final = kwargs["messages"]
|
||||
|
|
@ -434,7 +450,11 @@ class QdrantSemanticCache(BaseCache):
|
|||
)
|
||||
|
||||
async def async_get_cache(self, key, **kwargs):
|
||||
print_verbose(f"async qdrant semantic-cache get_cache, kwargs: {kwargs}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"async qdrant semantic-cache get_cache, kwargs: {kwargs}"
|
||||
)
|
||||
|
||||
# get the messages
|
||||
messages: Final = kwargs["messages"]
|
||||
|
|
|
|||
|
|
@ -1266,7 +1266,9 @@ class RedisCache(BaseCache):
|
|||
async with _redis_client.pipeline(transaction=False) as pipe:
|
||||
results: Final = await self._pipeline_helper(pipe, cache_list, ttl)
|
||||
|
||||
print_verbose(f"pipeline results: {results}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm" if litellm.redact_messages_in_debug_logs else f"pipeline results: {results}"
|
||||
)
|
||||
# Optionally, you could process 'results' to make sure that all set operations were successful.
|
||||
## LOGGING ##
|
||||
end_time = time.time()
|
||||
|
|
@ -1870,7 +1872,9 @@ class RedisCache(BaseCache):
|
|||
start_time: Final = time.time()
|
||||
try:
|
||||
response: Final[bool] = self.redis_client.ping()
|
||||
print_verbose(f"Redis Cache PING: {response}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm" if litellm.redact_messages_in_debug_logs else f"Redis Cache PING: {response}"
|
||||
)
|
||||
## LOGGING ##
|
||||
end_time = time.time()
|
||||
_duration = end_time - start_time
|
||||
|
|
|
|||
|
|
@ -419,7 +419,11 @@ class RedisSemanticCache(BaseCache):
|
|||
**kwargs: Additional arguments including 'messages' for the prompt
|
||||
and optional 'ttl' for time-to-live
|
||||
"""
|
||||
print_verbose(f"Redis semantic-cache set_cache, kwargs: {kwargs}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"Redis semantic-cache set_cache, kwargs: {kwargs}"
|
||||
)
|
||||
|
||||
value_str: str | None = None
|
||||
try:
|
||||
|
|
@ -456,7 +460,11 @@ class RedisSemanticCache(BaseCache):
|
|||
Returns:
|
||||
The cached response if a semantically similar prompt is found, else None
|
||||
"""
|
||||
print_verbose(f"Redis semantic-cache get_cache, kwargs: {kwargs}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"Redis semantic-cache get_cache, kwargs: {kwargs}"
|
||||
)
|
||||
|
||||
try:
|
||||
prompt: Final = self._get_prompt_from_kwargs(**kwargs)
|
||||
|
|
@ -565,7 +573,11 @@ class RedisSemanticCache(BaseCache):
|
|||
**kwargs: Additional arguments including 'messages' for the prompt
|
||||
and optional 'ttl' for time-to-live
|
||||
"""
|
||||
print_verbose(f"Async Redis semantic-cache set_cache, kwargs: {kwargs}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"Async Redis semantic-cache set_cache, kwargs: {kwargs}"
|
||||
)
|
||||
|
||||
try:
|
||||
prompt: Final = self._get_prompt_from_kwargs(**kwargs)
|
||||
|
|
@ -606,7 +618,11 @@ class RedisSemanticCache(BaseCache):
|
|||
Returns:
|
||||
The cached response if a semantically similar prompt is found, else None
|
||||
"""
|
||||
print_verbose(f"Async Redis semantic-cache get_cache, kwargs: {kwargs}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"Async Redis semantic-cache get_cache, kwargs: {kwargs}"
|
||||
)
|
||||
|
||||
try:
|
||||
prompt: Final = self._get_prompt_from_kwargs(**kwargs)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from redis.commands.search.field import TagField, VectorField
|
|||
from redis.commands.search.indexDefinition import IndexDefinition, IndexType
|
||||
from redis.commands.search.query import Query
|
||||
|
||||
import litellm
|
||||
from litellm._logging import print_verbose
|
||||
from litellm._uuid import uuid
|
||||
from litellm.llms.valkey.common_utils import build_valkey_url, pack_vector
|
||||
|
|
@ -259,7 +260,11 @@ class ValkeySemanticCache(RedisSemanticCache):
|
|||
return self._get_cache_logic(cached_response=hit.response)
|
||||
|
||||
def set_cache(self, key: str, value: object, **kwargs: object) -> None:
|
||||
print_verbose(f"Valkey semantic-cache set_cache, kwargs: {kwargs}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"Valkey semantic-cache set_cache, kwargs: {kwargs}"
|
||||
)
|
||||
try:
|
||||
prompt: Final = self._get_prompt_from_kwargs(**kwargs)
|
||||
if prompt is None:
|
||||
|
|
@ -278,7 +283,11 @@ class ValkeySemanticCache(RedisSemanticCache):
|
|||
print_verbose(f"Error in Valkey semantic-cache set_cache: {e}")
|
||||
|
||||
def get_cache(self, key: str, **kwargs: object) -> object:
|
||||
print_verbose(f"Valkey semantic-cache get_cache, kwargs: {kwargs}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"Valkey semantic-cache get_cache, kwargs: {kwargs}"
|
||||
)
|
||||
try:
|
||||
prompt: Final = self._get_prompt_from_kwargs(**kwargs)
|
||||
if prompt is None:
|
||||
|
|
@ -298,7 +307,11 @@ class ValkeySemanticCache(RedisSemanticCache):
|
|||
self._record_similarity(kwargs, 0.0)
|
||||
|
||||
async def async_set_cache(self, key: str, value: object, **kwargs: object) -> None:
|
||||
print_verbose(f"Async Valkey semantic-cache set_cache, kwargs: {kwargs}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"Async Valkey semantic-cache set_cache, kwargs: {kwargs}"
|
||||
)
|
||||
try:
|
||||
prompt: Final = self._get_prompt_from_kwargs(**kwargs)
|
||||
if prompt is None:
|
||||
|
|
@ -317,7 +330,11 @@ class ValkeySemanticCache(RedisSemanticCache):
|
|||
print_verbose(f"Error in async Valkey semantic-cache set_cache: {e}")
|
||||
|
||||
async def async_get_cache(self, key: str, **kwargs: object) -> object:
|
||||
print_verbose(f"Async Valkey semantic-cache get_cache, kwargs: {kwargs}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"Async Valkey semantic-cache get_cache, kwargs: {kwargs}"
|
||||
)
|
||||
try:
|
||||
prompt: Final = self._get_prompt_from_kwargs(**kwargs)
|
||||
if prompt is None:
|
||||
|
|
|
|||
|
|
@ -1221,7 +1221,10 @@ Model Info:
|
|||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
print("Error sending webhook alert. Error=", response.text) # noqa: T201
|
||||
print( # noqa: T201 # legacy stdout diagnostic
|
||||
"Error sending webhook alert. Error=",
|
||||
"redacted-by-litellm" if litellm.redact_messages_in_debug_logs else response.text,
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import httpx
|
|||
import polars as pl
|
||||
from rich.console import Console
|
||||
|
||||
import litellm
|
||||
|
||||
|
||||
class CloudZeroStreamer:
|
||||
"""Stream CBF data to CloudZero AnyCost API with proper batching and timezone handling."""
|
||||
|
|
@ -44,7 +46,11 @@ class CloudZeroStreamer:
|
|||
try:
|
||||
self.user_timezone = zoneinfo.ZoneInfo(user_timezone)
|
||||
except zoneinfo.ZoneInfoNotFoundError:
|
||||
self.console.print(f"[yellow]Warning: Unknown timezone '{user_timezone}', using UTC[/yellow]")
|
||||
self.console.print(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"[yellow]Warning: Unknown timezone '{user_timezone}', using UTC[/yellow]"
|
||||
)
|
||||
self.user_timezone = timezone.utc
|
||||
else:
|
||||
self.user_timezone = timezone.utc
|
||||
|
|
@ -62,7 +68,11 @@ class CloudZeroStreamer:
|
|||
self.console.print("[yellow]No valid daily batches to send[/yellow]")
|
||||
return
|
||||
|
||||
self.console.print(f"[blue]Sending {len(daily_batches)} daily batch(es) with operation '{operation}'[/blue]")
|
||||
self.console.print(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"[blue]Sending {len(daily_batches)} daily batch(es) with operation '{operation}'[/blue]"
|
||||
)
|
||||
|
||||
for batch_date, batch_data in daily_batches.items():
|
||||
self._send_daily_batch(batch_date, batch_data, operation)
|
||||
|
|
@ -94,7 +104,11 @@ class CloudZeroStreamer:
|
|||
daily_batches[batch_date].append(row)
|
||||
|
||||
except Exception as e:
|
||||
self.console.print(f"[yellow]Warning: Could not process timestamp '{timestamp_str}': {e}[/yellow]")
|
||||
self.console.print(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"[yellow]Warning: Could not process timestamp '{timestamp_str}': {e}[/yellow]"
|
||||
)
|
||||
continue
|
||||
|
||||
# Convert lists back to DataFrames
|
||||
|
|
@ -171,21 +185,33 @@ class CloudZeroStreamer:
|
|||
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
self.console.print(f"[blue]Sending batch for {batch_date} ({len(batch_data)} records)[/blue]")
|
||||
self.console.print(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"[blue]Sending batch for {batch_date} ({len(batch_data)} records)[/blue]"
|
||||
)
|
||||
|
||||
response: Final = client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
|
||||
self.console.print(
|
||||
f"[green]✓ Successfully sent batch for {batch_date} ({len(batch_data)} records)[/green]"
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"[green]✓ Successfully sent batch for {batch_date} ({len(batch_data)} records)[/green]"
|
||||
)
|
||||
|
||||
except httpx.RequestError as e:
|
||||
self.console.print(f"[red]✗ Network error sending batch for {batch_date}: {e}[/red]")
|
||||
self.console.print(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"[red]✗ Network error sending batch for {batch_date}: {e}[/red]"
|
||||
)
|
||||
raise
|
||||
except httpx.HTTPStatusError as e:
|
||||
self.console.print(
|
||||
f"[red]✗ HTTP error sending batch for {batch_date}: {e.response.status_code} {e.response.text}[/red]"
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"[red]✗ HTTP error sending batch for {batch_date}: {e.response.status_code} {e.response.text}[/red]"
|
||||
)
|
||||
raise
|
||||
|
||||
|
|
@ -237,7 +263,11 @@ class CloudZeroStreamer:
|
|||
return api_record
|
||||
|
||||
except Exception as e:
|
||||
self.console.print(f"[yellow]Warning: Could not convert record to API format: {e}[/yellow]")
|
||||
self.console.print(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"[yellow]Warning: Could not convert record to API format: {e}[/yellow]"
|
||||
)
|
||||
return None
|
||||
|
||||
def _ensure_utc_timestamp(self, timestamp_str: str) -> str:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from typing import Final
|
|||
|
||||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
|
||||
DEEPEVAL_BASE_URL: Final = "https://deepeval.confident-ai.com"
|
||||
|
|
@ -21,8 +22,8 @@ from litellm.llms.custom_httpx.http_handler import (
|
|||
|
||||
|
||||
def log_retry_error(details):
|
||||
exception: Final = details.get("exception")
|
||||
tries: Final = details.get("tries")
|
||||
exception: Final = "redacted-by-litellm" if litellm.redact_messages_in_debug_logs else details.get("exception")
|
||||
tries: Final = "redacted-by-litellm" if litellm.redact_messages_in_debug_logs else details.get("tries")
|
||||
if exception:
|
||||
logging.error("Confident AI Error: %s. Retrying: %s time(s)...", exception, tries)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ from opentelemetry.sdk.trace import ReadableSpan
|
|||
from opentelemetry.sdk.trace.export import SpanExportResult
|
||||
from opentelemetry.semconv.attributes.http_attributes import HTTP_RESPONSE_STATUS_CODE
|
||||
|
||||
from litellm._logging import MessageContentFilter
|
||||
|
||||
_logger: Final = logging.getLogger(__name__)
|
||||
_logger.addFilter(MessageContentFilter())
|
||||
JSON_CONTENT_TYPE: Final = "application/json"
|
||||
_HEX_ID_KEYS: Final = frozenset({"traceId", "spanId", "parentSpanId"})
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ try:
|
|||
|
||||
from wandb.sdk.data_types import trace_tree
|
||||
|
||||
from litellm._logging import MessageContentFilter
|
||||
|
||||
logger: Final = logging.getLogger(__name__)
|
||||
logger.addFilter(MessageContentFilter())
|
||||
|
||||
K = TypeVar("K", bound=str)
|
||||
V = TypeVar("V")
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import copy
|
|||
import datetime
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
|
|
@ -25,7 +26,9 @@ from pydantic import BaseModel, JsonValue, TypeAdapter
|
|||
import litellm
|
||||
from litellm._internal_context import post_response_phase
|
||||
from litellm._logging import (
|
||||
diagnostic_message_logging_disabled,
|
||||
is_debugging_on,
|
||||
log_safe_diagnostic,
|
||||
redact_string,
|
||||
session_id_var,
|
||||
set_session_id,
|
||||
|
|
@ -1708,6 +1711,13 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
|
||||
Prints the RAW curl command sent from LiteLLM
|
||||
"""
|
||||
if diagnostic_message_logging_disabled():
|
||||
log_safe_diagnostic(
|
||||
verbose_logger,
|
||||
"request_sent",
|
||||
level=logging.WARNING if self.litellm_request_debug else logging.DEBUG,
|
||||
)
|
||||
return
|
||||
if is_debugging_on() or self.litellm_request_debug:
|
||||
if litellm.json_logs:
|
||||
masked_headers: Final = self._get_masked_headers(headers or {})
|
||||
|
|
@ -1788,11 +1798,18 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
attr = "debug"
|
||||
|
||||
callattr: Final = verbose_logger.warning if attr == "warning" else verbose_logger.debug
|
||||
callattr(
|
||||
"RAW RESPONSE:\n{}\n\n".format(
|
||||
self._model_call_details_view.get("original_response", self._model_call_details_view)
|
||||
if diagnostic_message_logging_disabled():
|
||||
log_safe_diagnostic(
|
||||
verbose_logger,
|
||||
"response_received",
|
||||
level=logging.WARNING if self.litellm_request_debug else logging.DEBUG,
|
||||
)
|
||||
else:
|
||||
callattr(
|
||||
"RAW RESPONSE:\n{}\n\n".format(
|
||||
self._model_call_details_view.get("original_response", self._model_call_details_view)
|
||||
)
|
||||
)
|
||||
)
|
||||
if getattr(self, "logger_fn", None) and callable(self.logger_fn):
|
||||
try:
|
||||
self.logger_fn(
|
||||
|
|
@ -7648,7 +7665,12 @@ def get_standard_logging_object_payload(
|
|||
def emit_standard_logging_payload(payload: StandardLoggingPayload):
|
||||
if os.getenv("LITELLM_PRINT_STANDARD_LOGGING_PAYLOAD"):
|
||||
try:
|
||||
print(json.dumps(payload, indent=4, default=str), flush=True) # noqa: T201
|
||||
print( # noqa: T201 # explicitly enabled diagnostic stdout
|
||||
REDACTED_BY_LITELLM
|
||||
if diagnostic_message_logging_disabled()
|
||||
else json.dumps(payload, indent=4, default=str),
|
||||
flush=True,
|
||||
)
|
||||
except Exception as e: # noqa: BLE001 # Safe catch-all for verbose logging
|
||||
verbose_logger.exception("Error serializing standard logging payload for debug output: %s", e)
|
||||
|
||||
|
|
|
|||
|
|
@ -151,10 +151,16 @@ def is_async_iterable(obj: object) -> bool:
|
|||
return isinstance(obj, collections.abc.AsyncIterable)
|
||||
|
||||
|
||||
def print_verbose(print_statement: object):
|
||||
def print_verbose(print_statement: object, *args: object):
|
||||
try:
|
||||
if litellm.set_verbose:
|
||||
print(print_statement) # noqa: T201
|
||||
print( # noqa: T201
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else str(print_statement) % args
|
||||
if args
|
||||
else print_statement
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -654,10 +660,10 @@ class CustomStreamWrapper:
|
|||
text = ""
|
||||
is_finished = False
|
||||
finish_reason = ""
|
||||
print_verbose(f"chunk: {chunk}")
|
||||
print_verbose("chunk: %s", cast(object, chunk)) # cast-ok: logging accepts any object
|
||||
if chunk.startswith("data:"):
|
||||
data_json: Final[_PredibaseStreamData] = json.loads(chunk[5:])
|
||||
print_verbose(f"data json: {data_json}")
|
||||
print_verbose("data json: %s", data_json)
|
||||
if "token" in data_json and "text" in data_json["token"]:
|
||||
text = data_json["token"]["text"]
|
||||
if data_json.get("details", False) and data_json["details"].get("finish_reason", False):
|
||||
|
|
@ -847,7 +853,8 @@ class CustomStreamWrapper:
|
|||
"completion_tokens": 0,
|
||||
}
|
||||
else:
|
||||
print_verbose(f"chunk: {chunk} (Type: {type(chunk)})")
|
||||
if not litellm.redact_messages_in_debug_logs:
|
||||
print_verbose(f"chunk: {chunk} (Type: {type(chunk)})")
|
||||
raise ValueError(f"Unable to parse response. Original response: {chunk}")
|
||||
text: Final = parsed_response.get("text_output", "")
|
||||
finish_reason: Final = parsed_response.get("stop_reason")
|
||||
|
|
@ -1483,13 +1490,21 @@ class CustomStreamWrapper:
|
|||
elif self.custom_llm_provider == "triton":
|
||||
response_obj = self.handle_triton_stream(chunk)
|
||||
completion_obj["content"] = response_obj["text"]
|
||||
print_verbose(f"completion obj content: {completion_obj['content']}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"completion obj content: {completion_obj['content']}"
|
||||
)
|
||||
if response_obj["is_finished"]:
|
||||
self.received_finish_reason = response_obj["finish_reason"]
|
||||
elif self.custom_llm_provider == "text-completion-openai":
|
||||
response_obj = self.handle_openai_text_completion_chunk(chunk)
|
||||
completion_obj["content"] = response_obj["text"]
|
||||
print_verbose(f"completion obj content: {completion_obj['content']}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"completion obj content: {completion_obj['content']}"
|
||||
)
|
||||
if response_obj["is_finished"]:
|
||||
self.received_finish_reason = response_obj["finish_reason"]
|
||||
if response_obj["usage"] is not None:
|
||||
|
|
@ -1511,7 +1526,11 @@ class CustomStreamWrapper:
|
|||
litellm.CodestralTextCompletionConfig().chunk_parser(chunk),
|
||||
)
|
||||
completion_obj["content"] = response_obj["text"]
|
||||
print_verbose(f"completion obj content: {completion_obj['content']}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"completion obj content: {completion_obj['content']}"
|
||||
)
|
||||
if response_obj["is_finished"]:
|
||||
self.received_finish_reason = response_obj["finish_reason"]
|
||||
if "usage" in response_obj is not None:
|
||||
|
|
@ -1528,7 +1547,11 @@ class CustomStreamWrapper:
|
|||
elif self.custom_llm_provider == "azure_text":
|
||||
response_obj = self.handle_azure_text_completion_chunk(chunk)
|
||||
completion_obj["content"] = response_obj["text"]
|
||||
print_verbose(f"completion obj content: {completion_obj['content']}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"completion obj content: {completion_obj['content']}"
|
||||
)
|
||||
if response_obj["is_finished"]:
|
||||
self.received_finish_reason = response_obj["finish_reason"]
|
||||
elif self.custom_llm_provider == "cached_response":
|
||||
|
|
@ -1546,7 +1569,11 @@ class CustomStreamWrapper:
|
|||
completion_obj["content"] = response_obj["text"]
|
||||
if response_obj["tool_calls"] is not None:
|
||||
completion_obj["tool_calls"] = response_obj["tool_calls"]
|
||||
print_verbose(f"completion obj content: {completion_obj['content']}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"completion obj content: {completion_obj['content']}"
|
||||
)
|
||||
if hasattr(cached_chunk, "id"):
|
||||
model_response.id = cached_chunk.id
|
||||
self.response_id = cached_chunk.id
|
||||
|
|
@ -2038,11 +2065,13 @@ class CustomStreamWrapper:
|
|||
else:
|
||||
chunk = next(self.completion_stream)
|
||||
if chunk is not None and chunk != b"":
|
||||
print_verbose(
|
||||
f"PROCESSED CHUNK PRE CHUNK CREATOR: {chunk.decode('utf-8', errors='replace') if isinstance(chunk, bytes) else chunk}; custom_llm_provider: {self.custom_llm_provider}"
|
||||
)
|
||||
if not litellm.redact_messages_in_debug_logs:
|
||||
print_verbose(
|
||||
f"PROCESSED CHUNK PRE CHUNK CREATOR: {chunk.decode('utf-8', errors='replace') if isinstance(chunk, bytes) else chunk}; custom_llm_provider: {self.custom_llm_provider}"
|
||||
)
|
||||
response: ModelResponseStream | None = self.chunk_creator(chunk=chunk)
|
||||
print_verbose(f"PROCESSED CHUNK POST CHUNK CREATOR: {response}")
|
||||
if not litellm.redact_messages_in_debug_logs:
|
||||
print_verbose(f"PROCESSED CHUNK POST CHUNK CREATOR: {response}")
|
||||
|
||||
if response is None:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -181,7 +181,11 @@ class CodestralTextCompletion:
|
|||
original_response=response.text,
|
||||
additional_args={"complete_input_dict": data},
|
||||
)
|
||||
print_verbose(f"codestral api: raw model_response: {response.text}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"codestral api: raw model_response: {response.text}"
|
||||
)
|
||||
## RESPONSE OBJECT
|
||||
if response.status_code != 200:
|
||||
raise TextCompletionCodestralError(
|
||||
|
|
|
|||
|
|
@ -247,7 +247,9 @@ def completion(
|
|||
original_response=response.text,
|
||||
additional_args={"complete_input_dict": data},
|
||||
)
|
||||
print_verbose(f"raw model_response: {response.text}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm" if litellm.redact_messages_in_debug_logs else f"raw model_response: {response.text}"
|
||||
)
|
||||
## RESPONSE OBJECT
|
||||
completion_response: Final = response.json()
|
||||
if "error" in completion_response:
|
||||
|
|
|
|||
|
|
@ -14,12 +14,14 @@ if TYPE_CHECKING:
|
|||
else:
|
||||
LoggingClass = Any
|
||||
|
||||
from litellm._logging import MessageContentFilter
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
|
||||
from ...openai.chat.gpt_transformation import OpenAIGPTConfig
|
||||
from ..common_utils import HuggingFaceError, fetch_inference_provider_mapping
|
||||
|
||||
logger: Final = logging.getLogger(__name__)
|
||||
logger.addFilter(MessageContentFilter())
|
||||
|
||||
BASE_URL: Final = "https://router.huggingface.co"
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,9 @@ def handle_prediction_response_streaming(
|
|||
headers=response.headers,
|
||||
)
|
||||
new_output = output_string[len(previous_output) :]
|
||||
print_verbose(f"New chunk: {new_output}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm" if litellm.redact_messages_in_debug_logs else f"New chunk: {new_output}"
|
||||
)
|
||||
yield {"output": new_output, "status": status}
|
||||
previous_output = output_string
|
||||
status = response_data["status"]
|
||||
|
|
@ -66,7 +68,9 @@ def handle_prediction_response_streaming(
|
|||
else:
|
||||
# this can fail temporarily but it does not mean the replicate request failed, replicate request fails when status=="failed"
|
||||
print_verbose(
|
||||
f"Replicate: Failed to fetch prediction status and output.{response.status_code}{response.text}"
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"Replicate: Failed to fetch prediction status and output.{response.status_code}{response.text}"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -107,7 +111,9 @@ async def async_handle_prediction_response_streaming(
|
|||
headers=response.headers,
|
||||
)
|
||||
new_output = output_string[len(previous_output) :]
|
||||
print_verbose(f"New chunk: {new_output}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm" if litellm.redact_messages_in_debug_logs else f"New chunk: {new_output}"
|
||||
)
|
||||
yield {"output": new_output, "status": status}
|
||||
previous_output = output_string
|
||||
status = response_data.get("status", "")
|
||||
|
|
@ -121,7 +127,9 @@ async def async_handle_prediction_response_streaming(
|
|||
else:
|
||||
# this can fail temporarily but it does not mean the replicate request failed, replicate request fails when status=="failed"
|
||||
print_verbose(
|
||||
f"Replicate: Failed to fetch prediction status and output.{response.status_code}{response.text}"
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"Replicate: Failed to fetch prediction status and output.{response.status_code}{response.text}"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -635,7 +635,9 @@ class SagemakerLLM(BaseAWSLLM):
|
|||
additional_args={"complete_input_dict": data},
|
||||
)
|
||||
|
||||
print_verbose(f"raw model_response: {response}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm" if litellm.redact_messages_in_debug_logs else f"raw model_response: {response}"
|
||||
)
|
||||
|
||||
# Transform response based on model type
|
||||
from httpx import Response as HttpxResponse
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from typing import Final
|
|||
|
||||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
prompt_factory,
|
||||
render_custom_prompt_template,
|
||||
|
|
@ -85,7 +86,9 @@ def completion(
|
|||
original_response=outputs,
|
||||
additional_args={"complete_input_dict": sampling_params},
|
||||
)
|
||||
print_verbose(f"raw model_response: {outputs}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm" if litellm.redact_messages_in_debug_logs else f"raw model_response: {outputs}"
|
||||
)
|
||||
## RESPONSE OBJECT
|
||||
model_response.choices[0].message.content = outputs[0].outputs[0].text
|
||||
|
||||
|
|
|
|||
|
|
@ -819,7 +819,11 @@ def _text_completion_response_from_cached_dict(cached_response_dict: Mapping[str
|
|||
|
||||
async def _async_streaming(response, model, custom_llm_provider, args):
|
||||
try:
|
||||
print_verbose(f"received response in _async_streaming: {response}")
|
||||
print_verbose(
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"received response in _async_streaming: {response}"
|
||||
)
|
||||
if asyncio.iscoroutine(response):
|
||||
response = await response
|
||||
async for line in response:
|
||||
|
|
@ -9219,7 +9223,7 @@ def print_verbose(print_statement: object):
|
|||
try:
|
||||
verbose_logger.debug(print_statement)
|
||||
if litellm.set_verbose:
|
||||
print(print_statement) # noqa: T201
|
||||
print("redacted-by-litellm" if litellm.redact_messages_in_debug_logs else print_statement) # noqa: T201
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable,
|
|||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
from itertools import chain
|
||||
from time import perf_counter
|
||||
from types import MappingProxyType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
|
|
@ -33,7 +34,7 @@ from starlette.types import Receive, Scope, Send
|
|||
from typing_extensions import Never
|
||||
|
||||
import litellm
|
||||
from litellm._logging import redact_internal_details_from_client_message, verbose_proxy_logger
|
||||
from litellm._logging import log_safe_diagnostic, redact_internal_details_from_client_message, verbose_proxy_logger
|
||||
from litellm._uuid import uuid
|
||||
from litellm.anthropic_interface.exceptions import AnthropicErrorSseFrame, anthropic_error_sse_frame
|
||||
from litellm.constants import (
|
||||
|
|
@ -2775,7 +2776,7 @@ class ProxyBaseLLMRequestProcessing:
|
|||
|
||||
def _debug_log_request_payload(self) -> None:
|
||||
"""Log request payload at DEBUG level, truncating if too large."""
|
||||
if not verbose_proxy_logger.isEnabledFor(logging.DEBUG):
|
||||
if litellm.redact_messages_in_debug_logs or not verbose_proxy_logger.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
_payload_str: Final = json.dumps(self.data, default=str)
|
||||
if len(_payload_str) > MAX_PAYLOAD_SIZE_FOR_DEBUG_LOG:
|
||||
|
|
@ -2831,7 +2832,12 @@ class ProxyBaseLLMRequestProcessing:
|
|||
dispatched_call_type=self.route_type,
|
||||
)
|
||||
|
||||
return await open_sse_before_first_byte(
|
||||
diagnostic_started: Final = (
|
||||
perf_counter()
|
||||
if litellm.redact_messages_in_debug_logs and verbose_proxy_logger.isEnabledFor(logging.DEBUG)
|
||||
else None
|
||||
)
|
||||
response: Final = await open_sse_before_first_byte(
|
||||
self._process_llm_request(
|
||||
request=request,
|
||||
fastapi_response=fastapi_response,
|
||||
|
|
@ -2856,6 +2862,18 @@ class ProxyBaseLLMRequestProcessing:
|
|||
ping_interval_seconds=ttft_keepalive_interval(self.data, llm_router),
|
||||
on_late_failure=_audit_late_failure,
|
||||
)
|
||||
if diagnostic_started is not None:
|
||||
log_safe_diagnostic(
|
||||
verbose_proxy_logger,
|
||||
"response_ready",
|
||||
status_code=(
|
||||
response.status_code
|
||||
if isinstance(response, Response)
|
||||
else fastapi_response.status_code or status.HTTP_200_OK
|
||||
),
|
||||
elapsed_seconds=perf_counter() - diagnostic_started,
|
||||
)
|
||||
return response
|
||||
|
||||
async def _process_llm_request(
|
||||
self,
|
||||
|
|
@ -4167,6 +4185,13 @@ class ProxyBaseLLMRequestProcessing:
|
|||
version: str | None = None,
|
||||
) -> Never:
|
||||
"""Raises ProxyException (OpenAI API compatible) if an exception is raised"""
|
||||
if litellm.redact_messages_in_debug_logs:
|
||||
log_safe_diagnostic(
|
||||
verbose_proxy_logger,
|
||||
"request_failed",
|
||||
status_code=error_status_code(e, status.HTTP_500_INTERNAL_SERVER_ERROR),
|
||||
level=logging.ERROR,
|
||||
)
|
||||
log_llm_api_exception(e, self.litellm_call_id)
|
||||
# Allow callbacks to transform the error response
|
||||
transformed_exception: Final = await proxy_logging_obj.post_call_failure_hook(
|
||||
|
|
@ -4444,6 +4469,7 @@ class ProxyBaseLLMRequestProcessing:
|
|||
cost_injection_enabled: Final = bool(getattr(litellm, "include_cost_in_streaming_usage", False))
|
||||
fast_path = not caps.has_streaming_chunk_override and not caps.has_guardrail and not cost_injection_enabled
|
||||
debug_enabled: Final = verbose_proxy_logger.isEnabledFor(logging.DEBUG)
|
||||
diagnostic_started: Final = perf_counter() if litellm.redact_messages_in_debug_logs and debug_enabled else None
|
||||
stream_completed = False
|
||||
client_disconnected = False
|
||||
delivered_chunk = False
|
||||
|
|
@ -4458,7 +4484,7 @@ class ProxyBaseLLMRequestProcessing:
|
|||
async for chunk in guarded_stream:
|
||||
# ``.format(chunk)`` was previously evaluated for every chunk
|
||||
# regardless of log level; gate it behind the level check.
|
||||
if debug_enabled:
|
||||
if debug_enabled and not litellm.redact_messages_in_debug_logs:
|
||||
verbose_proxy_logger.debug("async_data_generator: received streaming chunk - %s", chunk)
|
||||
|
||||
if not fast_path:
|
||||
|
|
@ -4501,6 +4527,11 @@ class ProxyBaseLLMRequestProcessing:
|
|||
if held_tail:
|
||||
yield serialize_chunk(held_tail)
|
||||
stream_completed = True
|
||||
log_safe_diagnostic(
|
||||
verbose_proxy_logger,
|
||||
"stream_completed",
|
||||
elapsed_seconds=perf_counter() - diagnostic_started if diagnostic_started is not None else None,
|
||||
)
|
||||
except (asyncio.CancelledError, GeneratorExit):
|
||||
# Client disconnected mid-stream. CancelledError / GeneratorExit
|
||||
# are BaseException and bypass the success/failure logging
|
||||
|
|
@ -4538,6 +4569,7 @@ class ProxyBaseLLMRequestProcessing:
|
|||
if isinstance(e, HTTPException):
|
||||
raise e
|
||||
stream_error_status: Final = error_status_code(e, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
log_safe_diagnostic(verbose_proxy_logger, "request_failed", status_code=stream_error_status)
|
||||
proxy_exception: Final = ProxyException(
|
||||
message=redact_internal_details_from_client_message(getattr(e, "message", str(e))),
|
||||
type=openai_error_type(e, stream_error_status),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
|
|||
from pydantic import TypeAdapter
|
||||
from typing_extensions import ReadOnly
|
||||
|
||||
import litellm
|
||||
from litellm import get_secret_str
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.constants import PYTHON_GC_THRESHOLD
|
||||
|
|
@ -853,7 +854,7 @@ async def get_otel_spans():
|
|||
else:
|
||||
recorded_spans = []
|
||||
|
||||
print("Spans: ", recorded_spans) # noqa: T201
|
||||
print("Spans: ", "redacted-by-litellm" if litellm.redact_messages_in_debug_logs else recorded_spans) # noqa: T201
|
||||
|
||||
most_recent_parent = None
|
||||
most_recent_start_time = 1000000
|
||||
|
|
@ -948,4 +949,7 @@ def init_verbose_loggers():
|
|||
except Exception as e:
|
||||
import logging
|
||||
|
||||
logging.warning("Failed to init verbose loggers: %s", e)
|
||||
logging.warning(
|
||||
"Failed to init verbose loggers: %s",
|
||||
"redacted-by-litellm" if litellm.redact_messages_in_debug_logs else e,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1770,7 +1770,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail):
|
|||
try:
|
||||
verbose_proxy_logger.debug(print_statement)
|
||||
if litellm.set_verbose:
|
||||
print(print_statement) # noqa: T201
|
||||
print("redacted-by-litellm" if litellm.redact_messages_in_debug_logs else print_statement) # noqa: T201
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ import litellm
|
|||
if TYPE_CHECKING:
|
||||
from litellm.router import Router
|
||||
|
||||
from litellm._logging import MessageContentFilter
|
||||
|
||||
logger: Final = logging.getLogger(__name__)
|
||||
logger.addFilter(MessageContentFilter())
|
||||
_DeploymentT: Final = TypeVar("_DeploymentT", bound=Mapping[str, object])
|
||||
from litellm.constants import (
|
||||
BACKGROUND_HEALTH_CHECK_MAX_TOKENS,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class _PROXY_BatchRedisRequests(CustomLogger):
|
|||
if debug_level == "DEBUG" or debug_level == "INFO":
|
||||
verbose_proxy_logger.debug(print_statement)
|
||||
if litellm.set_verbose is True:
|
||||
print(print_statement) # noqa: T201
|
||||
print("redacted-by-litellm" if litellm.redact_messages_in_debug_logs else print_statement) # noqa: T201
|
||||
|
||||
async def async_pre_call_hook(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class _PROXY_MaxParallelRequestsHandler(CustomLogger):
|
|||
try:
|
||||
verbose_proxy_logger.debug(print_statement)
|
||||
if litellm.set_verbose:
|
||||
print(print_statement) # noqa: T201
|
||||
print("redacted-by-litellm" if litellm.redact_messages_in_debug_logs else print_statement) # noqa: T201
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class _OPTIONAL_PromptInjectionDetection(CustomLogger):
|
|||
verbose_proxy_logger.debug(print_statement)
|
||||
|
||||
if litellm.set_verbose is True:
|
||||
print(print_statement) # noqa: T201
|
||||
print("redacted-by-litellm" if litellm.redact_messages_in_debug_logs else print_statement) # noqa: T201
|
||||
|
||||
def update_environment(self, router: Router | None = None) -> None:
|
||||
self.llm_router = router
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
def post_response_rule(input): # receives the model response
|
||||
print(f"post_response_rule:input={input}") # noqa: T201
|
||||
from collections.abc import Mapping
|
||||
|
||||
import litellm
|
||||
|
||||
|
||||
def post_response_rule(input: str) -> Mapping[str, bool | str]:
|
||||
print( # noqa: T201 # legacy stdout diagnostic
|
||||
"post_response_rule:input=redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"post_response_rule:input={input}"
|
||||
)
|
||||
if len(input) < 200:
|
||||
return {
|
||||
"decision": False,
|
||||
|
|
|
|||
|
|
@ -1147,13 +1147,23 @@ def _redact_worker_config_for_logging(worker_config: str | dict[str, JsonValue]
|
|||
(`LITELLM_DISABLE_REDACT_SECRETS=true`, an older log sink, a downstream
|
||||
handler that captures records pre-filter). Redact at the source.
|
||||
"""
|
||||
if litellm.redact_messages_in_debug_logs:
|
||||
return "redacted-by-litellm"
|
||||
if worker_config is None:
|
||||
return None
|
||||
if isinstance(worker_config, dict):
|
||||
return _redact_secret_values_in_obj(worker_config)
|
||||
parsed: Final[JsonValue] = safe_json_loads(worker_config, default=None)
|
||||
parsed: Final[JsonValue] = (
|
||||
worker_config if isinstance(worker_config, dict) else safe_json_loads(worker_config, default=None)
|
||||
)
|
||||
if isinstance(parsed, dict):
|
||||
return safe_dumps(_redact_secret_values_in_obj(parsed))
|
||||
settings: Final = parsed.get("litellm_settings")
|
||||
if isinstance(settings, dict) and settings.get("redact_messages_in_debug_logs") is not None:
|
||||
try:
|
||||
if TypeAdapter(bool).validate_python(settings["redact_messages_in_debug_logs"]):
|
||||
return "redacted-by-litellm"
|
||||
except ValidationError:
|
||||
return "redacted-by-litellm"
|
||||
redacted: Final = _redact_secret_values_in_obj(parsed)
|
||||
return redacted if isinstance(worker_config, dict) else safe_dumps(redacted)
|
||||
return worker_config
|
||||
|
||||
|
||||
|
|
@ -5739,6 +5749,11 @@ class ProxyConfig:
|
|||
}
|
||||
|
||||
def _apply_resolved_runtime_settings(self, config: Mapping[str, object]) -> None:
|
||||
diagnostic_settings: Final = config.get("litellm_settings")
|
||||
if isinstance(diagnostic_settings, Mapping) and "redact_messages_in_debug_logs" in diagnostic_settings:
|
||||
litellm.redact_messages_in_debug_logs = TypeAdapter(bool).validate_python(
|
||||
diagnostic_settings["redact_messages_in_debug_logs"]
|
||||
)
|
||||
for section, store in self._settings_stores.items():
|
||||
if isinstance(config.get(section), Mapping):
|
||||
store.apply_runtime_values(_as_settings_mapping(config[section]))
|
||||
|
|
@ -6915,6 +6930,8 @@ class ProxyConfig:
|
|||
|
||||
parse_budget_reset_time(value)
|
||||
setattr(litellm, key, value)
|
||||
elif key == "redact_messages_in_debug_logs":
|
||||
litellm.redact_messages_in_debug_logs = TypeAdapter(bool).validate_python(value)
|
||||
elif key == "drop_params":
|
||||
litellm.drop_params = drop_params_flag(value, "litellm_settings.drop_params", verbose_proxy_logger)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -435,9 +435,15 @@ def print_verbose(print_statement: object):
|
|||
"""
|
||||
import traceback
|
||||
|
||||
verbose_proxy_logger.debug("%s\n%s", print_statement, traceback.format_exc())
|
||||
verbose_proxy_logger.debug(
|
||||
"%s\n%s", print_statement, "" if litellm.redact_messages_in_debug_logs else traceback.format_exc()
|
||||
)
|
||||
if litellm.set_verbose:
|
||||
print(f"LiteLLM Proxy: {redact_string(str(print_statement))}") # noqa: T201
|
||||
print( # noqa: T201 # legacy stdout diagnostic
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"LiteLLM Proxy: {redact_string(str(print_statement))}"
|
||||
)
|
||||
|
||||
|
||||
def _get_email_logger_class():
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from collections.abc import Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm._uuid import uuid
|
||||
from litellm.litellm_core_utils.agentic_loop_settings import validated_max_agentic_loops
|
||||
|
|
@ -150,7 +151,8 @@ async def create_mcp_list_tools_events(
|
|||
verbose_logger.error("Error creating MCP list tools events: %s", e)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
if not litellm.redact_messages_in_debug_logs:
|
||||
traceback.print_exc()
|
||||
|
||||
# Emit failed event on error
|
||||
failed_event: Final = MCPListToolsFailedEvent(
|
||||
|
|
@ -816,7 +818,8 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator):
|
|||
verbose_logger.error("Error creating initial response iterator: %s", e)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
if not litellm.redact_messages_in_debug_logs:
|
||||
traceback.print_exc()
|
||||
self.base_iterator = None
|
||||
self._initial_creation_error = e
|
||||
self._stream_error = e
|
||||
|
|
@ -976,7 +979,8 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator):
|
|||
verbose_logger.error("Error in tool execution: %s", e)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
if not litellm.redact_messages_in_debug_logs:
|
||||
traceback.print_exc()
|
||||
self.tool_results = []
|
||||
self._tool_results_for_response = self.collected_response
|
||||
|
||||
|
|
@ -1052,7 +1056,8 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator):
|
|||
verbose_logger.error("Error creating follow-up iterator: %s", e)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
if not litellm.redact_messages_in_debug_logs:
|
||||
traceback.print_exc()
|
||||
self.base_iterator = None
|
||||
self._stream_error = e
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ import litellm
|
|||
import litellm.litellm_core_utils.exception_mapping_utils
|
||||
from litellm import get_secret_str
|
||||
from litellm._internal_context import service_target, with_service_target
|
||||
from litellm._logging import verbose_router_logger
|
||||
from litellm._logging import log_safe_diagnostic, verbose_router_logger
|
||||
from litellm._uuid import uuid
|
||||
from litellm.caching.caching import (
|
||||
DualCache,
|
||||
|
|
@ -6774,6 +6774,8 @@ class Router:
|
|||
trigger, healthy_deployments, all_deployments, initial_kwargs
|
||||
):
|
||||
return _StreamRetriesExhausted(last_error)
|
||||
if litellm.redact_messages_in_debug_logs:
|
||||
log_safe_diagnostic(verbose_router_logger, "retry", attempt=attempt + 1)
|
||||
if get_metadata_variable_name_from_kwargs(retry_kwargs) in retry_kwargs:
|
||||
self.log_retry(kwargs=retry_kwargs, e=trigger)
|
||||
await asyncio.sleep(
|
||||
|
|
@ -7627,7 +7629,8 @@ class Router:
|
|||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
if not litellm.redact_messages_in_debug_logs:
|
||||
traceback.print_exc()
|
||||
receieved_exceptions.append(e)
|
||||
return None
|
||||
|
||||
|
|
@ -9064,6 +9067,8 @@ class Router:
|
|||
await asyncio.sleep(retry_after)
|
||||
|
||||
for current_attempt in range(num_retries):
|
||||
if litellm.redact_messages_in_debug_logs:
|
||||
log_safe_diagnostic(verbose_router_logger, "retry", attempt=current_attempt + 1)
|
||||
try:
|
||||
# Update retry tracking metadata before each retry attempt
|
||||
_metadata["attempted_retries"] = current_attempt + 1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
|
@ -11,7 +12,7 @@ import httpx
|
|||
from pydantic import TypeAdapter
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_router_logger
|
||||
from litellm._logging import log_safe_diagnostic, verbose_router_logger
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.litellm_core_utils.core_helpers import get_metadata_variable_name_from_kwargs, safe_deep_copy
|
||||
from litellm.litellm_core_utils.credential_accessor import reject_api_key_secret_reference
|
||||
|
|
@ -870,7 +871,10 @@ async def run_async_fallback(
|
|||
try:
|
||||
# LOGGING
|
||||
kwargs = litellm_router.log_retry(kwargs=kwargs, e=original_exception)
|
||||
verbose_router_logger.info("Falling back to model_group = %s", mask_sensitive_structure(mg))
|
||||
if litellm.redact_messages_in_debug_logs:
|
||||
log_safe_diagnostic(verbose_router_logger, "fallback", attempt=fallback_depth + 1, level=logging.INFO)
|
||||
else:
|
||||
verbose_router_logger.info("Falling back to model_group = %s", mask_sensitive_structure(mg))
|
||||
kwargs.pop("_target_order", None)
|
||||
if isinstance(mg, str):
|
||||
kwargs["model"] = mg
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from typing import Final
|
|||
|
||||
from pydantic import JsonValue
|
||||
|
||||
import litellm
|
||||
from litellm._logging import (
|
||||
CorrelationContextFilter,
|
||||
DiagnosticProcessingFilter,
|
||||
|
|
@ -24,7 +25,7 @@ def context() -> tuple[str, str]:
|
|||
|
||||
|
||||
def enabled(level: int) -> bool:
|
||||
return verbose_logger.isEnabledFor(level)
|
||||
return not getattr(litellm, "redact_messages_in_debug_logs", False) and verbose_logger.isEnabledFor(level)
|
||||
|
||||
|
||||
def emit(
|
||||
|
|
|
|||
|
|
@ -582,7 +582,7 @@ def print_verbose(
|
|||
elif log_level == "ERROR":
|
||||
verbose_logger.error(print_statement)
|
||||
if litellm.set_verbose is True and logger_only is False:
|
||||
print(print_statement) # noqa: T201
|
||||
print("redacted-by-litellm" if litellm.redact_messages_in_debug_logs else print_statement) # noqa: T201
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -1948,7 +1948,9 @@ def client(original_function):
|
|||
|
||||
# [OPTIONAL] CHECK CACHE
|
||||
print_verbose(
|
||||
f"SYNC kwargs[caching]: {kwargs.get('caching', False)}; litellm.cache: {litellm.cache}; kwargs.get('cache')['no-cache']: {kwargs.get('cache', {}).get('no-cache', False)}"
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"SYNC kwargs[caching]: {kwargs.get('caching', False)}; litellm.cache: {litellm.cache}; kwargs.get('cache')['no-cache']: {kwargs.get('cache', {}).get('no-cache', False)}"
|
||||
)
|
||||
# if caching is false or cache["no-cache"]==True, don't run this
|
||||
if (
|
||||
|
|
@ -2232,7 +2234,9 @@ def client(original_function):
|
|||
# [OPTIONAL] CHECK CACHE
|
||||
if is_debugging_on():
|
||||
print_verbose(
|
||||
f"ASYNC kwargs[caching]: {kwargs.get('caching', False)}; litellm.cache: {litellm.cache}; kwargs.get('cache'): {kwargs.get('cache', None)}"
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"ASYNC kwargs[caching]: {kwargs.get('caching', False)}; litellm.cache: {litellm.cache}; kwargs.get('cache'): {kwargs.get('cache', None)}"
|
||||
)
|
||||
_caching_handler_response: CachingHandlerResponse | None = await _llm_caching_handler.async_get_cache(
|
||||
model=model or "",
|
||||
|
|
@ -8272,7 +8276,9 @@ def trim_messages(
|
|||
|
||||
#### Trimming messages if current_tokens > max_tokens
|
||||
print_verbose(
|
||||
f"Need to trim input messages: {messages}, current_tokens{current_tokens}, max_tokens: {max_tokens}"
|
||||
"redacted-by-litellm"
|
||||
if litellm.redact_messages_in_debug_logs
|
||||
else f"Need to trim input messages: {messages}, current_tokens{current_tokens}, max_tokens: {max_tokens}"
|
||||
)
|
||||
system_message_event: dict | None = None
|
||||
if system_message:
|
||||
|
|
@ -8535,6 +8541,9 @@ def get_valid_models(
|
|||
def print_args_passed_to_litellm(original_function, args, kwargs):
|
||||
if not is_debugging_on():
|
||||
return
|
||||
if litellm.redact_messages_in_debug_logs:
|
||||
print_verbose("redacted-by-litellm")
|
||||
return
|
||||
try:
|
||||
# we've already printed this for acompletion, don't print for completion
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import zoneinfo
|
||||
from datetime import datetime, timezone
|
||||
from typing import Final
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import httpx
|
||||
import polars as pl
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
import litellm
|
||||
|
||||
|
||||
from litellm.integrations.cloudzero.cz_stream_api import CloudZeroStreamer
|
||||
|
|
@ -177,3 +181,20 @@ class TestCloudZeroStreamer:
|
|||
# Should avoid scientific notation
|
||||
assert "e" not in result["cost"].lower()
|
||||
assert "e" not in result["large_float"].lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("redact", (False, True))
|
||||
def test_cloudzero_http_error_diagnostics_respect_privacy_without_changing_exception(
|
||||
redact: bool, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", redact)
|
||||
marker: Final = "synthetic-private-upstream-content"
|
||||
streamer: Final = CloudZeroStreamer("test-key", "test-connection")
|
||||
with respx.mock:
|
||||
respx.post(f"{streamer.base_url}/v2/connections/billing/anycost/test-connection/billing_drops").respond(
|
||||
status_code=400, text=marker
|
||||
)
|
||||
with pytest.raises(httpx.HTTPStatusError) as captured:
|
||||
streamer._send_daily_batch("2026-01-01", pl.DataFrame({"cost/cost": [1.0]}), "replace_hourly")
|
||||
assert captured.value.response.text == marker
|
||||
assert (marker in capsys.readouterr().out) is not redact
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import os
|
||||
import unittest
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm._uuid import uuid
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
|
@ -118,3 +123,19 @@ class TestDeepEvalLogger(unittest.TestCase):
|
|||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("redact", (False, True))
|
||||
def test_retry_diagnostic_hides_exception_and_dynamic_details_only_when_opted_in(
|
||||
redact: bool, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
from litellm.integrations.deepeval.api import log_retry_error
|
||||
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", redact)
|
||||
marker: Final = "synthetic-private-retry-content"
|
||||
failure: Final = ValueError(marker)
|
||||
details: Final = {"exception": failure, "tries": 2}
|
||||
log_retry_error(details)
|
||||
assert (marker in caplog.text) is not redact
|
||||
assert ("redacted-by-litellm" in caplog.text) is redact
|
||||
assert details["exception"] is failure
|
||||
|
|
|
|||
|
|
@ -13775,3 +13775,56 @@ def test_record_native_bedrock_guardrail_bills_nothing_for_a_response_served_fro
|
|||
logging_obj.record_native_bedrock_guardrail({"trace": trace})
|
||||
|
||||
assert "standard_logging_guardrail_information" not in logging_obj.model_call_details["litellm_params"]["metadata"]
|
||||
|
||||
|
||||
class _UnrenderableRequestBody:
|
||||
def __repr__(self) -> str:
|
||||
raise AssertionError("request body was formatted for diagnostics")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("json_logs", [False, True])
|
||||
def test_private_debug_request_skips_body_formatting_and_keeps_transform_data(
|
||||
logging_obj: LitellmLogging, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, json_logs: bool
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
monkeypatch.setattr(litellm, "json_logs", json_logs)
|
||||
logging_obj.litellm_request_debug = True
|
||||
body: Final = {"messages": _UnrenderableRequestBody()}
|
||||
details: Final = {"api_base": "https://example.com", "complete_input_dict": body, "request_str": "PRIVATE_REQUEST"}
|
||||
logging_obj._print_llm_call_debugging_log(api_base="https://example.com", headers={}, additional_args=details)
|
||||
assert "event=request_sent" in caplog.text
|
||||
assert "PRIVATE_REQUEST" not in caplog.text
|
||||
assert details["complete_input_dict"] is body
|
||||
|
||||
|
||||
@pytest.mark.parametrize("diagnostic_privacy", [False, True])
|
||||
@pytest.mark.parametrize("callback_privacy", [False, True])
|
||||
def test_debug_privacy_preserves_raw_transform_request_body(
|
||||
logging_obj: LitellmLogging, monkeypatch: pytest.MonkeyPatch, diagnostic_privacy: bool, callback_privacy: bool
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", diagnostic_privacy)
|
||||
monkeypatch.setattr(litellm, "turn_off_message_logging", callback_privacy)
|
||||
metadata: Final = _pre_call_with_raw_request_logging(logging_obj)
|
||||
if callback_privacy:
|
||||
assert metadata["raw_request"] == REDACTED_BY_LITELLM
|
||||
else:
|
||||
assert "PRIVATE" in metadata["raw_request"]
|
||||
assert logging_obj.model_call_details["raw_request_typed_dict"]["raw_request_body"] == _PRIVATE_RAW_REQUEST_ARGS["complete_input_dict"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", [False, True])
|
||||
def test_standard_payload_debug_stdout_obeys_diagnostic_privacy(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], enabled: bool
|
||||
) -> None:
|
||||
from litellm.litellm_core_utils.litellm_logging import emit_standard_logging_payload
|
||||
|
||||
monkeypatch.setenv("LITELLM_PRINT_STANDARD_LOGGING_PAYLOAD", "1")
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", enabled)
|
||||
payload: Final = StandardLoggingPayload(messages=[{"role": "user", "content": "PRIVATE_PAYLOAD"}])
|
||||
emit_standard_logging_payload(payload)
|
||||
output: Final = capsys.readouterr().out
|
||||
if enabled:
|
||||
assert output.strip() == REDACTED_BY_LITELLM
|
||||
else:
|
||||
assert json.loads(output)["messages"] == payload["messages"]
|
||||
assert payload["messages"] == [{"role": "user", "content": "PRIVATE_PAYLOAD"}]
|
||||
|
|
|
|||
|
|
@ -7943,3 +7943,13 @@ def test_chunk_creator_usage_only_chunk_with_dict_usage_keeps_details(
|
|||
assert usage.prompt_tokens_details.cached_tokens == 80
|
||||
assert usage.completion_tokens_details is not None
|
||||
assert usage.completion_tokens_details.reasoning_tokens == 5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", [False, True])
|
||||
def test_legacy_stream_verbose_output_obeys_diagnostic_privacy(monkeypatch, capsys, enabled):
|
||||
from litellm.litellm_core_utils.streaming_handler import print_verbose
|
||||
|
||||
monkeypatch.setattr(litellm, "set_verbose", True)
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", enabled)
|
||||
print_verbose("chunk: %s", "PRIVATE_STREAM_CONTENT")
|
||||
assert capsys.readouterr().out.strip() == ("redacted-by-litellm" if enabled else "chunk: PRIVATE_STREAM_CONTENT")
|
||||
|
|
|
|||
|
|
@ -198,3 +198,35 @@ def test_memory_routes_count_prompt_caching_pins(
|
|||
|
||||
assert response.status_code == 200
|
||||
assert pin_count(response.json()) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_otel_span_debug_privacy_preserves_api_result(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
from typing import Final
|
||||
|
||||
import litellm
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
||||
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
|
||||
|
||||
from litellm.integrations.opentelemetry import OpenTelemetry, OpenTelemetryConfig
|
||||
from litellm.proxy.common_utils.debug_utils import get_otel_spans
|
||||
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
exporter: Final = InMemorySpanExporter()
|
||||
provider: Final = TracerProvider()
|
||||
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
||||
logger: Final = OpenTelemetry(config=OpenTelemetryConfig(exporter=exporter), tracer_provider=provider)
|
||||
monkeypatch.setattr(proxy_server, "open_telemetry_logger", logger)
|
||||
try:
|
||||
with provider.get_tracer("privacy-test").start_as_current_span("PRIVATE_SPAN_42896") as span:
|
||||
span.set_attribute("gen_ai.input.messages", "PRIVATE_MESSAGE_42896")
|
||||
result: Final = await get_otel_spans()
|
||||
output: Final = capsys.readouterr().out
|
||||
assert result["otel_spans"] == ["PRIVATE_SPAN_42896"]
|
||||
assert "redacted-by-litellm" in output
|
||||
assert "PRIVATE" not in output
|
||||
finally:
|
||||
provider.shutdown()
|
||||
|
|
|
|||
|
|
@ -12191,3 +12191,61 @@ def test_maybe_get_model_id_prefers_litellm_metadata_over_caller_metadata_withou
|
|||
|
||||
def test_maybe_get_model_id_returns_none_before_the_router_picked_a_deployment():
|
||||
assert ProxyBaseLLMRequestProcessing(data={"metadata": {}}).maybe_get_model_id(None) is None
|
||||
|
||||
|
||||
def test_debug_message_privacy_does_not_serialize_proxy_payload(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
class Unrenderable:
|
||||
def __str__(self) -> str:
|
||||
raise AssertionError("diagnostics must not serialize request content")
|
||||
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
content: Final = Unrenderable()
|
||||
request: Final = ProxyBaseLLMRequestProcessing(data={"messages": [{"role": "user", "content": content}]})
|
||||
request._debug_log_request_payload()
|
||||
assert request.data["messages"][0]["content"] is content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("privacy", [False, True])
|
||||
async def test_debug_message_privacy_preserves_stream_content(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, privacy: bool
|
||||
) -> None:
|
||||
import logging
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", privacy)
|
||||
monkeypatch.setattr(litellm, "callbacks", [])
|
||||
monkeypatch.setattr(litellm, "success_callback", [])
|
||||
monkeypatch.setattr(litellm, "_async_success_callback", [])
|
||||
monkeypatch.setattr(litellm, "include_cost_in_streaming_usage", False)
|
||||
monkeypatch.setattr(verbose_proxy_logger, "level", logging.DEBUG)
|
||||
ProxyLogging._callback_capabilities_cache.clear()
|
||||
chunks: Final = ("data: PRIVATE_STREAM_INPUT_42896\n\n", "data: [DONE]\n\n")
|
||||
|
||||
async def upstream() -> AsyncGenerator[str, None]:
|
||||
for chunk in chunks:
|
||||
yield chunk
|
||||
|
||||
def serialize(chunk: object) -> str:
|
||||
assert isinstance(chunk, str)
|
||||
return chunk
|
||||
|
||||
stream: Final = ProxyBaseLLMRequestProcessing.async_streaming_data_generator(
|
||||
response=upstream(),
|
||||
user_api_key_dict=ProxyUserAPIKeyAuth(),
|
||||
request_data={"model": "example-model"},
|
||||
proxy_logging_obj=ProxyLogging(user_api_key_cache=DualCache()),
|
||||
serialize_chunk=serialize,
|
||||
serialize_error=lambda error: str(error),
|
||||
)
|
||||
observed: Final = tuple([chunk async for chunk in stream])
|
||||
assert observed == chunks
|
||||
messages: Final = tuple(
|
||||
record.getMessage() for record in caplog.records if record.name == verbose_proxy_logger.name
|
||||
)
|
||||
if privacy:
|
||||
assert any("event=stream_completed" in message for message in messages)
|
||||
assert all("PRIVATE_STREAM_INPUT_42896" not in message for message in messages)
|
||||
else:
|
||||
assert any("PRIVATE_STREAM_INPUT_42896" in message for message in messages)
|
||||
|
|
|
|||
|
|
@ -3857,3 +3857,36 @@ async def test_model_list_return_wildcard_routes_defaults_to_general_setting(
|
|||
|
||||
assert "gpt-6.1-sol" in listed, listed
|
||||
assert ("openai/*" in listed) is wildcard_listed, listed
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", [True, False])
|
||||
async def test_yaml_config_applies_diagnostic_privacy_before_model_initialization(
|
||||
tmp_path, monkeypatch: pytest.MonkeyPatch, enabled: bool
|
||||
) -> None:
|
||||
from litellm.proxy.proxy_server import ProxyConfig
|
||||
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", not enabled)
|
||||
config_file: Final = tmp_path / "config.yaml"
|
||||
config_file.write_text("litellm_settings:\n redact_messages_in_debug_logs: " + str(enabled).lower() + "\n")
|
||||
config: Final = await ProxyConfig().get_config(str(config_file))
|
||||
assert litellm.redact_messages_in_debug_logs is enabled
|
||||
assert config["litellm_settings"]["redact_messages_in_debug_logs"] is enabled
|
||||
|
||||
|
||||
@pytest.mark.parametrize("as_json", [False, True])
|
||||
@pytest.mark.parametrize("enabled", [False, True])
|
||||
def test_worker_config_privacy_applies_before_global_setting(
|
||||
monkeypatch: pytest.MonkeyPatch, as_json: bool, enabled: bool
|
||||
) -> None:
|
||||
from litellm.proxy.proxy_server import _redact_worker_config_for_logging
|
||||
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", False)
|
||||
config: Final = {
|
||||
"litellm_settings": {"redact_messages_in_debug_logs": enabled},
|
||||
"model_list": [{"litellm_params": {"messages": [{"content": "PRIVATE_SYSTEM_PROMPT"}]}}],
|
||||
}
|
||||
result: Final = _redact_worker_config_for_logging(json.dumps(config) if as_json else config)
|
||||
if enabled:
|
||||
assert result == "redacted-by-litellm"
|
||||
else:
|
||||
assert "PRIVATE_SYSTEM_PROMPT" in str(result)
|
||||
|
|
|
|||
|
|
@ -135,3 +135,14 @@ def test_missing_native_diagnostic_processor_falls_back_before_record_mutation(
|
|||
def test_unsupported_unicode_uses_safe_python_redaction(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("LITELLM_RUST", "1")
|
||||
assert redact_secrets("broken\ud800 api_key=secret123") == "broken\ud800 REDACTED"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("level", (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR))
|
||||
def test_native_diagnostics_do_not_format_or_emit_content_when_privacy_enabled(
|
||||
level: int, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
with caplog.at_level(logging.DEBUG, logger="LiteLLM"):
|
||||
assert not logger.enabled(level)
|
||||
logger.emit(level, "private response", "native.rs", 1, "litellm_http", {"body": "private prompt"}, ("", ""))
|
||||
assert caplog.records == []
|
||||
|
|
|
|||
|
|
@ -2044,3 +2044,168 @@ def test_print_startup_message_blank_spacer_emits_no_json_record(
|
|||
print_startup_message("\n\033[0m\n")
|
||||
|
||||
assert capfd.readouterr() == ("", "")
|
||||
|
||||
|
||||
class _UnrenderableDiagnosticPayload:
|
||||
def __str__(self) -> str:
|
||||
raise AssertionError("diagnostic payload was rendered")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
raise AssertionError("diagnostic payload was rendered")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("level", [logging.DEBUG, logging.WARNING, logging.ERROR])
|
||||
@pytest.mark.parametrize("credential_redaction", [False, True])
|
||||
def test_message_privacy_removes_payload_before_rendering(
|
||||
monkeypatch: pytest.MonkeyPatch, level: int, credential_redaction: bool
|
||||
) -> None:
|
||||
import litellm._logging as logging_module
|
||||
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
monkeypatch.setattr(logging_module, "_ENABLE_SECRET_REDACTION", credential_redaction)
|
||||
payload: Final = _UnrenderableDiagnosticPayload()
|
||||
record: Final = logging.LogRecord("LiteLLM", level, __file__, 1, "payload=%s", (payload,), None)
|
||||
record.exc_info = (ValueError, ValueError("PRIVATE_EXCEPTION"), None)
|
||||
record.exc_text = "PRIVATE_CACHED_TRACEBACK"
|
||||
record.stack_info = "PRIVATE_STACK"
|
||||
record.payload = payload
|
||||
record.color_message = "PRIVATE_COLOR"
|
||||
record.message = "PRIVATE_CACHED_MESSAGE"
|
||||
assert DiagnosticProcessingFilter().filter(record)
|
||||
rendered: Final = JsonFormatter().format(record)
|
||||
assert json.loads(rendered)["message"] == "redacted-by-litellm"
|
||||
assert "PRIVATE" not in rendered
|
||||
assert record.exc_info is None
|
||||
assert record.args == ()
|
||||
assert not hasattr(record, "payload")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("spoof", [True, "safe", "redacted-by-litellm"])
|
||||
def test_message_privacy_cannot_be_disabled_by_log_extras(monkeypatch: pytest.MonkeyPatch, spoof: object) -> None:
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
record: Final = logging.LogRecord("LiteLLM", logging.DEBUG, __file__, 1, "PRIVATE_PROMPT", (), None)
|
||||
record.litellm_safe_diagnostic = spoof
|
||||
assert DiagnosticProcessingFilter().filter(record)
|
||||
assert record.getMessage() == "redacted-by-litellm"
|
||||
|
||||
|
||||
def test_message_privacy_disabled_preserves_record(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from litellm._logging import MessageContentFilter
|
||||
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", False)
|
||||
payload: Final = {"messages": [{"content": "PRIVATE_PROMPT"}]}
|
||||
record: Final = logging.LogRecord("LiteLLM", logging.DEBUG, __file__, 1, "payload=%s", (payload,), None)
|
||||
before: Final = dict(record.__dict__)
|
||||
assert MessageContentFilter().filter(record)
|
||||
assert record.__dict__ == before
|
||||
assert record.args is before["args"]
|
||||
assert "PRIVATE_PROMPT" in record.getMessage()
|
||||
|
||||
|
||||
def test_message_privacy_keeps_safe_operational_events(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from litellm._logging import MessageContentFilter, log_safe_diagnostic
|
||||
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
stream: Final = StringIO()
|
||||
handler: Final = logging.StreamHandler(stream)
|
||||
handler.setFormatter(JsonFormatter())
|
||||
handler.addFilter(MessageContentFilter())
|
||||
logger: Final = logging.Logger("privacy-test", level=logging.DEBUG)
|
||||
logger.addFilter(DiagnosticProcessingFilter())
|
||||
logger.addHandler(handler)
|
||||
log_safe_diagnostic(logger, "retry", status_code=429, elapsed_seconds=0.25, attempt=2)
|
||||
result: Final = json.loads(stream.getvalue())
|
||||
assert result["message"] == "event=retry status=429 elapsed_seconds=0.25 attempt=2 content=redacted-by-litellm"
|
||||
assert "litellm_safe_diagnostic" not in result
|
||||
|
||||
|
||||
def test_message_privacy_suppresses_caller_correlation_fields(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
monkeypatch.setattr(litellm, "request_correlation_in_logs", True)
|
||||
trace_token: Final = set_trace_id("PRIVATE_TRACE")
|
||||
session_token: Final = set_session_id("PRIVATE_SESSION")
|
||||
try:
|
||||
record: Final = logging.LogRecord("LiteLLM", logging.DEBUG, __file__, 1, "PRIVATE_PROMPT", (), None)
|
||||
assert DiagnosticProcessingFilter().filter(record)
|
||||
assert CorrelationContextFilter().filter(record)
|
||||
assert "PRIVATE" not in JsonFormatter().format(record)
|
||||
finally:
|
||||
trace_id_var.reset(trace_token)
|
||||
session_id_var.reset(session_token)
|
||||
|
||||
|
||||
def test_json_formatter_alone_obeys_message_privacy(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
record: Final = logging.LogRecord(
|
||||
"LiteLLM.child", logging.ERROR, __file__, 1, _UnrenderableDiagnosticPayload(), (), None
|
||||
)
|
||||
assert json.loads(JsonFormatter().format(record))["message"] == "redacted-by-litellm"
|
||||
|
||||
|
||||
def test_message_privacy_precedes_custom_handler(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
stream: Final = StringIO()
|
||||
handler: Final = logging.StreamHandler(stream)
|
||||
verbose_logger.addHandler(handler)
|
||||
try:
|
||||
verbose_logger.warning("PRIVATE_PROMPT %s", _UnrenderableDiagnosticPayload())
|
||||
assert stream.getvalue().strip() == "redacted-by-litellm"
|
||||
finally:
|
||||
verbose_logger.removeHandler(handler)
|
||||
|
||||
|
||||
def test_message_privacy_does_not_change_application_records(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
record: Final = logging.LogRecord("application", logging.INFO, __file__, 1, "application says %s", ("hello",), None)
|
||||
assert SecretRedactionFilter().filter(record)
|
||||
assert json.loads(JsonFormatter().format(record))["message"] == "application says hello"
|
||||
|
||||
|
||||
def _raise_private_diagnostic_exception() -> None:
|
||||
raise ValueError("PRIVATE_ASYNC_EXCEPTION")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("module", ["litellm.background", "application.background"])
|
||||
def test_asyncio_message_privacy_is_scoped_to_litellm_tracebacks(monkeypatch: pytest.MonkeyPatch, module: str) -> None:
|
||||
from types import FunctionType
|
||||
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
fail: Final = FunctionType(_raise_private_diagnostic_exception.__code__, {"__name__": module})
|
||||
try:
|
||||
fail()
|
||||
except ValueError:
|
||||
record: Final = logging.LogRecord("asyncio", logging.ERROR, __file__, 1, "PRIVATE_TASK", (), sys.exc_info())
|
||||
assert SecretRedactionFilter().filter(record)
|
||||
rendered: Final = JsonFormatter().format(record)
|
||||
if module.startswith("litellm."):
|
||||
assert "PRIVATE" not in rendered
|
||||
else:
|
||||
assert "PRIVATE_TASK" in rendered
|
||||
assert "PRIVATE_ASYNC_EXCEPTION" in rendered
|
||||
|
||||
|
||||
def test_private_diagnostics_support_custom_formatter_defaults(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from litellm._logging import MessageContentFilter
|
||||
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
record: Final = logging.LogRecord("LiteLLM", logging.ERROR, __file__, 1, "PRIVATE_PROMPT", (), None)
|
||||
record.customer_metadata = "PRIVATE_METADATA"
|
||||
assert MessageContentFilter().filter(record)
|
||||
formatter: Final = logging.Formatter("%(levelname)s %(customer_metadata)s %(message)s", defaults={"customer_metadata": "-"})
|
||||
assert formatter.format(record) == "ERROR - redacted-by-litellm"
|
||||
|
||||
|
||||
def test_private_access_logs_keep_formatter_contract_without_request_content(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from uvicorn.logging import AccessFormatter
|
||||
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
record: Final = logging.LogRecord(
|
||||
"uvicorn.access", logging.INFO, __file__, 1, "PRIVATE_TEMPLATE",
|
||||
("PRIVATE_CLIENT", "GET", "/v1/chat/completions?prompt=PRIVATE_PROMPT", "1.1", 200), None,
|
||||
)
|
||||
record.metadata = "PRIVATE_METADATA"
|
||||
assert AccessLogRedactionFilter().filter(record)
|
||||
formatted: Final = AccessFormatter('%(client_addr)s - "%(request_line)s" %(status_code)s', use_colors=False).format(record)
|
||||
assert "PRIVATE" not in formatted
|
||||
assert "200" in formatted
|
||||
assert "PRIVATE" not in JsonFormatter().format(record)
|
||||
|
|
|
|||
|
|
@ -10880,3 +10880,36 @@ def test_get_optional_params_forwards_a_tool_config_cache_point_to_a_downstream_
|
|||
)
|
||||
|
||||
assert optional_params["extra_body"] == {"kept": True, "cache_control_injection_points": [point]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("redact", (False, True))
|
||||
@pytest.mark.parametrize("module_name", ("litellm.utils", "litellm.main", "litellm.caching.caching", "litellm.proxy.utils"))
|
||||
def test_legacy_verbose_stdout_obeys_diagnostic_privacy(
|
||||
redact: bool,
|
||||
module_name: str,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", redact)
|
||||
monkeypatch.setattr(litellm, "set_verbose", True)
|
||||
module: Final = importlib.import_module(module_name)
|
||||
module.print_verbose("synthetic-private-prompt-42896")
|
||||
stdout: Final = capsys.readouterr().out
|
||||
assert ("synthetic-private-prompt-42896" in stdout) is not redact
|
||||
assert ("redacted-by-litellm" in stdout) is redact
|
||||
|
||||
|
||||
def test_private_argument_diagnostic_does_not_render_request_objects(
|
||||
capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
from litellm.utils import print_args_passed_to_litellm
|
||||
|
||||
class UnrenderableRequest:
|
||||
def __repr__(self) -> str:
|
||||
raise AssertionError("diagnostic attempted to render a private request")
|
||||
|
||||
monkeypatch.setattr(litellm, "redact_messages_in_debug_logs", True)
|
||||
monkeypatch.setattr(litellm, "set_verbose", True)
|
||||
with caplog.at_level(logging.DEBUG, logger="LiteLLM"):
|
||||
print_args_passed_to_litellm(litellm.completion, (UnrenderableRequest(),), {"messages": UnrenderableRequest()})
|
||||
assert capsys.readouterr().out.endswith("redacted-by-litellm\n")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue