mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(proxy): restore admin key/team callback_vars.turn_off_message_logging override (LIT-3587) (#31905)
The security fix in34e9be1ba7removed turn_off_message_logging from _supported_callback_params to stop callers bypassing global redaction via the request body. That also killed the documented admin-only per-key or per-team override because both flows resolve through the same allowlist in initialize_standard_callback_dynamic_params. Put turn_off_message_logging back in _supported_callback_params so an admin-configured metadata.logging[].callback_vars.turn_off_message_logging survives into StandardCallbackDynamicParams and can override the global setting for that key or team, as documented at docs/proxy/team_logging#disableenable-message-redaction. Consolidate the metadata traversal so the extractor and the proxy strip walk the same set of client-controllable slots. iter_client_callback_metadata_dicts in litellm_core_utils/initialize_dynamic_callback_params.py is the single source of truth for metadata, litellm_metadata, and litellm_params.metadata; _strip_client_message_redaction_opt_out imports it so a future addition to one side automatically reaches the other. The extractor iterates the helper in reversed order so litellm_params.metadata keeps overriding metadata, matching the pre-refactor merge precedence. Client bypass stays blocked. Restoring the field re-enrolls it in the auth layer's _BANNED_REQUEST_BODY_PARAMS (derived from _supported_callback_params via _build_banned_observability_params), so client submissions at the top level, inside metadata, or inside a JSON-string litellm_metadata all 401 at ingress. is_request_body_safe also now descends into litellm_params.metadata for the same 401 defense against the nested-body attack vector, matching how the metadata and litellm_metadata slots are handled. _strip_client_message_redaction_opt_out runs after the litellm_metadata JSON parse and before the admin callback_vars unpack, so admin values survive while any leftover client-supplied opt-out is dropped when global redaction is on and the key or team lacks allow_client_message_redaction_opt_out. Flip the two dynamic-param e2e tests added by the security fix to reflect the restored override behavior, keeping the invariant that proxy client bypass is stopped by the auth layer 401 above. Co-authored-by: yucheng <yucheng@yuchengs-MBP.attlocal.net> Co-authored-by: Cursor Agent <cursoragent@cursor.com> (cherry picked from commit8e6098adc3)
This commit is contained in:
parent
4820053a81
commit
c5d2a3664d
7 changed files with 344 additions and 59 deletions
|
|
@ -1,7 +1,23 @@
|
|||
from typing import Dict, Optional
|
||||
from typing import Any, Dict, Iterator, Optional
|
||||
|
||||
from litellm.types.utils import StandardCallbackDynamicParams
|
||||
|
||||
_CLIENT_CALLBACK_METADATA_SLOTS: tuple[str, ...] = ("litellm_metadata", "metadata")
|
||||
|
||||
|
||||
def iter_client_callback_metadata_dicts(
|
||||
kwargs: dict[str, Any],
|
||||
) -> Iterator[tuple[str, dict[str, Any]]]:
|
||||
litellm_params = kwargs.get("litellm_params")
|
||||
if isinstance(litellm_params, dict):
|
||||
nested = litellm_params.get("metadata")
|
||||
if isinstance(nested, dict):
|
||||
yield "litellm_params.metadata", nested
|
||||
for key in _CLIENT_CALLBACK_METADATA_SLOTS:
|
||||
candidate = kwargs.get(key)
|
||||
if isinstance(candidate, dict):
|
||||
yield key, candidate
|
||||
|
||||
|
||||
def _is_env_reference(value: object) -> bool:
|
||||
return isinstance(value, str) and "os.environ/" in value
|
||||
|
|
@ -55,6 +71,7 @@ _supported_callback_params = [
|
|||
"dd_site",
|
||||
"dd_agent_host",
|
||||
"dd_agent_port",
|
||||
"turn_off_message_logging",
|
||||
]
|
||||
|
||||
_request_blocked_callback_params = {
|
||||
|
|
@ -87,19 +104,13 @@ def initialize_standard_callback_dynamic_params(
|
|||
validate_no_callback_env_reference(param, _param_value, source="request body")
|
||||
standard_callback_dynamic_params[param] = _param_value # type: ignore
|
||||
|
||||
# 2. Fallback: check "metadata" or "litellm_params" -> "metadata"
|
||||
metadata = (kwargs.get("metadata") or {}).copy()
|
||||
litellm_params = kwargs.get("litellm_params") or {}
|
||||
if isinstance(litellm_params, dict):
|
||||
metadata.update(litellm_params.get("metadata") or {})
|
||||
|
||||
if isinstance(metadata, dict):
|
||||
for slot_label, metadata in iter_client_callback_metadata_dicts(kwargs):
|
||||
for param in _supported_callback_params:
|
||||
if param in _request_blocked_callback_params:
|
||||
continue
|
||||
if param not in standard_callback_dynamic_params and param in metadata:
|
||||
_param_value = metadata.get(param)
|
||||
validate_no_callback_env_reference(param, _param_value, source="metadata")
|
||||
validate_no_callback_env_reference(param, _param_value, source=slot_label)
|
||||
standard_callback_dynamic_params[param] = _param_value # type: ignore
|
||||
|
||||
return standard_callback_dynamic_params
|
||||
|
|
|
|||
|
|
@ -369,6 +369,16 @@ def is_request_body_safe(request_body: dict, general_settings: dict, llm_router:
|
|||
metadata = _coerce_metadata_to_dict(request_body.get(metadata_key))
|
||||
if metadata is not None:
|
||||
_check_banned_params(metadata, general_settings, llm_router, model)
|
||||
litellm_params = _coerce_metadata_to_dict(request_body.get("litellm_params"))
|
||||
if litellm_params is not None:
|
||||
litellm_params_metadata = _coerce_metadata_to_dict(litellm_params.get("metadata"))
|
||||
if litellm_params_metadata is not None:
|
||||
_check_banned_params(
|
||||
litellm_params_metadata,
|
||||
general_settings,
|
||||
llm_router,
|
||||
model,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ from litellm._logging import verbose_logger, verbose_proxy_logger
|
|||
from litellm._service_logger import ServiceLogging
|
||||
from litellm.constants import PRE_CALL_EXECUTED_GUARDRAILS_KEY
|
||||
from litellm.litellm_core_utils.credential_accessor import CredentialAccessor
|
||||
from litellm.litellm_core_utils.initialize_dynamic_callback_params import (
|
||||
iter_client_callback_metadata_dicts,
|
||||
)
|
||||
from litellm.litellm_core_utils.safe_json_loads import safe_json_loads
|
||||
from litellm.litellm_core_utils.url_utils import is_url_destination_allowed_by_host
|
||||
from litellm.proxy._types import (
|
||||
|
|
@ -301,6 +304,24 @@ def _key_or_team_allows_client_pricing_override(
|
|||
)
|
||||
|
||||
|
||||
def _strip_client_message_redaction_opt_out(data: dict[str, Any]) -> None:
|
||||
stripped: list[str] = []
|
||||
if "turn_off_message_logging" in data and _is_false_like(data["turn_off_message_logging"]):
|
||||
stripped.append("turn_off_message_logging")
|
||||
data.pop("turn_off_message_logging", None)
|
||||
for slot_label, metadata in iter_client_callback_metadata_dicts(data):
|
||||
if "turn_off_message_logging" in metadata and _is_false_like(metadata["turn_off_message_logging"]):
|
||||
stripped.append(f"{slot_label}.turn_off_message_logging")
|
||||
metadata.pop("turn_off_message_logging", None)
|
||||
if stripped:
|
||||
verbose_proxy_logger.debug(
|
||||
"Stripped client-supplied message-redaction opt-out fields from request body: %s. "
|
||||
"Set `allow_client_message_redaction_opt_out: true` on the key or team metadata "
|
||||
"to keep these values.",
|
||||
", ".join(stripped),
|
||||
)
|
||||
|
||||
|
||||
def _strip_client_pricing_overrides(data: Dict[str, Any]) -> None:
|
||||
"""Drop pricing overrides from the request body and any metadata variant.
|
||||
|
||||
|
|
@ -1307,13 +1328,6 @@ async def add_litellm_data_to_request(
|
|||
_headers,
|
||||
allow_client_message_redaction_opt_out=_allow_client_message_redaction_opt_out,
|
||||
)
|
||||
if (
|
||||
not _allow_client_message_redaction_opt_out
|
||||
and litellm.turn_off_message_logging is True
|
||||
and "turn_off_message_logging" in data
|
||||
and _is_false_like(data["turn_off_message_logging"])
|
||||
):
|
||||
data.pop("turn_off_message_logging", None)
|
||||
verbose_proxy_logger.debug(f"Request Headers: {_headers}")
|
||||
verbose_proxy_logger.debug(f"Raw Headers: {_raw_headers}")
|
||||
|
||||
|
|
@ -1465,6 +1479,9 @@ async def add_litellm_data_to_request(
|
|||
if not _key_or_team_allows_client_pricing_override(user_api_key_dict):
|
||||
_strip_client_pricing_overrides(data)
|
||||
|
||||
if not _allow_client_message_redaction_opt_out and litellm.turn_off_message_logging is True:
|
||||
_strip_client_message_redaction_opt_out(data)
|
||||
|
||||
# Fill in the proxy_server_request body snapshot now that metadata has
|
||||
# been parsed. Consumers (standard_logging_payload, lago,
|
||||
# spend_tracking_utils, streaming_iterator) read `body` to audit the
|
||||
|
|
|
|||
|
|
@ -56,69 +56,56 @@ async def test_global_redaction_on():
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("turn_off_message_logging", [True, False])
|
||||
@pytest.mark.parametrize(
|
||||
"dynamic_turn_off, expect_redacted",
|
||||
[(True, True), (False, False)],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_global_redaction_ignores_dynamic_param(turn_off_message_logging):
|
||||
"""
|
||||
Request-body `turn_off_message_logging` is no longer honored as a dynamic
|
||||
callback param — global setting (or admin-configured key/team config) wins.
|
||||
With global redaction ON, the caller cannot disable redaction via the
|
||||
request body.
|
||||
"""
|
||||
async def test_dynamic_turn_off_message_logging_overrides_global_on(dynamic_turn_off, expect_redacted):
|
||||
litellm.turn_off_message_logging = True
|
||||
test_custom_logger = TestCustomLogger()
|
||||
litellm.callbacks = [test_custom_logger]
|
||||
response = await litellm.acompletion(
|
||||
await litellm.acompletion(
|
||||
model="gpt-5-mini",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
turn_off_message_logging=turn_off_message_logging,
|
||||
turn_off_message_logging=dynamic_turn_off,
|
||||
mock_response="hello",
|
||||
)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
standard_logging_payload = test_custom_logger.logged_standard_logging_payload
|
||||
assert standard_logging_payload is not None
|
||||
print(
|
||||
"logged standard logging payload",
|
||||
json.dumps(standard_logging_payload, indent=2),
|
||||
)
|
||||
|
||||
response = standard_logging_payload["response"]
|
||||
assert response["choices"][0]["message"]["content"] == "redacted-by-litellm"
|
||||
assert standard_logging_payload["messages"][0]["content"] == "redacted-by-litellm"
|
||||
expected_response_content = "redacted-by-litellm" if expect_redacted else "hello"
|
||||
expected_message_content = "redacted-by-litellm" if expect_redacted else "hi"
|
||||
assert standard_logging_payload["response"]["choices"][0]["message"]["content"] == expected_response_content
|
||||
assert standard_logging_payload["messages"][0]["content"] == expected_message_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize("turn_off_message_logging", [True, False])
|
||||
@pytest.mark.parametrize(
|
||||
"dynamic_turn_off, expect_redacted",
|
||||
[(True, True), (False, False)],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_global_redaction_off_ignores_dynamic_param(turn_off_message_logging):
|
||||
"""
|
||||
Request-body `turn_off_message_logging` is no longer honored as a dynamic
|
||||
callback param — global setting (or admin-configured key/team config) wins.
|
||||
With global redaction OFF, the caller cannot enable redaction via the
|
||||
request body.
|
||||
"""
|
||||
async def test_dynamic_turn_off_message_logging_overrides_global_off(dynamic_turn_off, expect_redacted):
|
||||
litellm.turn_off_message_logging = False
|
||||
test_custom_logger = TestCustomLogger()
|
||||
litellm.callbacks = [test_custom_logger]
|
||||
response = await litellm.acompletion(
|
||||
await litellm.acompletion(
|
||||
model="gpt-5-mini",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
turn_off_message_logging=turn_off_message_logging,
|
||||
turn_off_message_logging=dynamic_turn_off,
|
||||
mock_response="hello",
|
||||
)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
standard_logging_payload = test_custom_logger.logged_standard_logging_payload
|
||||
assert standard_logging_payload is not None
|
||||
print(
|
||||
"logged standard logging payload",
|
||||
json.dumps(standard_logging_payload, indent=2),
|
||||
)
|
||||
assert (
|
||||
standard_logging_payload["response"]["choices"][0]["message"]["content"]
|
||||
== "hello"
|
||||
)
|
||||
assert standard_logging_payload["messages"][0]["content"] == "hi"
|
||||
|
||||
expected_response_content = "redacted-by-litellm" if expect_redacted else "hello"
|
||||
expected_message_content = "redacted-by-litellm" if expect_redacted else "hi"
|
||||
assert standard_logging_payload["response"]["choices"][0]["message"]["content"] == expected_response_content
|
||||
assert standard_logging_payload["messages"][0]["content"] == expected_message_content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -7,9 +7,53 @@ sys.path.insert(0, os.path.abspath("../../.."))
|
|||
|
||||
from litellm.litellm_core_utils.initialize_dynamic_callback_params import (
|
||||
initialize_standard_callback_dynamic_params,
|
||||
iter_client_callback_metadata_dicts,
|
||||
)
|
||||
|
||||
|
||||
def test_iter_client_callback_metadata_dicts_covers_all_read_paths():
|
||||
md = {"m": 1}
|
||||
lm = {"lm": 1}
|
||||
lp_md = {"lp": 1}
|
||||
slots = dict(
|
||||
iter_client_callback_metadata_dicts(
|
||||
{
|
||||
"metadata": md,
|
||||
"litellm_metadata": lm,
|
||||
"litellm_params": {"metadata": lp_md},
|
||||
}
|
||||
)
|
||||
)
|
||||
assert slots == {
|
||||
"metadata": md,
|
||||
"litellm_metadata": lm,
|
||||
"litellm_params.metadata": lp_md,
|
||||
}
|
||||
|
||||
|
||||
def test_iter_client_callback_metadata_dicts_skips_non_dict_slots():
|
||||
slots = list(
|
||||
iter_client_callback_metadata_dicts(
|
||||
{
|
||||
"metadata": "not-a-dict",
|
||||
"litellm_metadata": None,
|
||||
"litellm_params": {"metadata": []},
|
||||
}
|
||||
)
|
||||
)
|
||||
assert slots == []
|
||||
|
||||
|
||||
def test_extractor_reads_turn_off_message_logging_from_every_slot():
|
||||
for kwargs in (
|
||||
{"metadata": {"turn_off_message_logging": True}},
|
||||
{"litellm_metadata": {"turn_off_message_logging": True}},
|
||||
{"litellm_params": {"metadata": {"turn_off_message_logging": True}}},
|
||||
):
|
||||
params = initialize_standard_callback_dynamic_params(kwargs)
|
||||
assert params.get("turn_off_message_logging") is True, kwargs
|
||||
|
||||
|
||||
def test_resolves_plain_values_at_top_level():
|
||||
kwargs = {
|
||||
"langfuse_public_key": "pk-test",
|
||||
|
|
@ -36,6 +80,33 @@ def test_resolves_plain_values_from_metadata():
|
|||
assert params.get("langfuse_host") == "https://test.langfuse.com"
|
||||
|
||||
|
||||
def test_litellm_params_metadata_overrides_metadata():
|
||||
kwargs = {
|
||||
"metadata": {
|
||||
"langfuse_public_key": "pk-meta",
|
||||
},
|
||||
"litellm_params": {
|
||||
"metadata": {
|
||||
"langfuse_public_key": "pk-litellm-params",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
params = initialize_standard_callback_dynamic_params(kwargs)
|
||||
|
||||
assert params.get("langfuse_public_key") == "pk-litellm-params"
|
||||
|
||||
|
||||
def test_top_level_kwargs_overrides_metadata_slots():
|
||||
kwargs = {
|
||||
"langfuse_public_key": "from-top-level",
|
||||
"metadata": {"langfuse_public_key": "from-metadata"},
|
||||
"litellm_params": {"metadata": {"langfuse_public_key": "from-litellm-params"}},
|
||||
}
|
||||
params = initialize_standard_callback_dynamic_params(kwargs)
|
||||
assert params.get("langfuse_public_key") == "from-top-level"
|
||||
|
||||
|
||||
def test_env_reference_at_top_level_raises_with_guidance():
|
||||
kwargs = {"langfuse_public_key": "os.environ/LANGFUSE_PUBLIC_KEY"}
|
||||
|
||||
|
|
@ -100,11 +171,17 @@ def test_non_string_values_are_not_flagged():
|
|||
assert params.get("langsmith_sampling_rate") == 0.5
|
||||
|
||||
|
||||
def test_turn_off_message_logging_not_extracted_from_request():
|
||||
"""turn_off_message_logging is admin-only — must not be settable via request."""
|
||||
kwargs = {"turn_off_message_logging": True}
|
||||
@pytest.mark.parametrize(
|
||||
"kwargs,expected",
|
||||
[
|
||||
({"turn_off_message_logging": False}, False),
|
||||
({"turn_off_message_logging": "False"}, "False"),
|
||||
({"metadata": {"turn_off_message_logging": True}}, True),
|
||||
],
|
||||
)
|
||||
def test_turn_off_message_logging_extracted_from_kwargs(kwargs, expected):
|
||||
params = initialize_standard_callback_dynamic_params(kwargs)
|
||||
assert params.get("turn_off_message_logging") is None
|
||||
assert params.get("turn_off_message_logging") == expected
|
||||
|
||||
|
||||
def test_empty_kwargs_returns_empty_params():
|
||||
|
|
|
|||
|
|
@ -1931,6 +1931,21 @@ class TestObservabilityCallbackBans:
|
|||
)
|
||||
assert field in str(exc.value)
|
||||
|
||||
def test_observability_field_in_litellm_params_metadata_is_rejected(self):
|
||||
with pytest.raises(ValueError) as exc:
|
||||
is_request_body_safe(
|
||||
request_body={
|
||||
"model": "gpt-4",
|
||||
"litellm_params": {
|
||||
"metadata": {"turn_off_message_logging": False}
|
||||
},
|
||||
},
|
||||
general_settings={},
|
||||
llm_router=None,
|
||||
model="gpt-4",
|
||||
)
|
||||
assert "turn_off_message_logging" in str(exc.value)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"metadata_key",
|
||||
["metadata", "litellm_metadata"],
|
||||
|
|
|
|||
|
|
@ -856,10 +856,19 @@ async def test_add_litellm_data_to_request_strips_client_redaction_bypass_contro
|
|||
"model": "gpt-3.5-turbo",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"turn_off_message_logging": False,
|
||||
"metadata": {"headers": {"litellm-disable-message-redaction": "true"}},
|
||||
"metadata": {
|
||||
"headers": {"litellm-disable-message-redaction": "true"},
|
||||
"turn_off_message_logging": False,
|
||||
},
|
||||
"litellm_metadata": json.dumps(
|
||||
{"headers": {"LiteLLM-Disable-Message-Redaction": "true"}}
|
||||
{
|
||||
"headers": {"LiteLLM-Disable-Message-Redaction": "true"},
|
||||
"turn_off_message_logging": "false",
|
||||
}
|
||||
),
|
||||
"litellm_params": {
|
||||
"metadata": {"turn_off_message_logging": False},
|
||||
},
|
||||
},
|
||||
request=request_mock,
|
||||
user_api_key_dict=UserAPIKeyAuth(api_key="hashed-key"),
|
||||
|
|
@ -871,6 +880,9 @@ async def test_add_litellm_data_to_request_strips_client_redaction_bypass_contro
|
|||
litellm.turn_off_message_logging = original_turn_off_message_logging
|
||||
|
||||
assert "turn_off_message_logging" not in updated
|
||||
assert "turn_off_message_logging" not in (updated.get("litellm_params") or {}).get("metadata", {})
|
||||
assert "turn_off_message_logging" not in updated["metadata"]
|
||||
assert "turn_off_message_logging" not in (updated.get("litellm_metadata") or {})
|
||||
assert "litellm-disable-message-redaction" not in {
|
||||
header.lower() for header in updated["metadata"]["headers"]
|
||||
}
|
||||
|
|
@ -891,6 +903,158 @@ async def test_add_litellm_data_to_request_strips_client_redaction_bypass_contro
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"admin_metadata_kwargs",
|
||||
[
|
||||
{
|
||||
"metadata": {
|
||||
"logging": [
|
||||
{
|
||||
"callback_name": "langfuse",
|
||||
"callback_type": "success_and_failure",
|
||||
"callback_vars": {"turn_off_message_logging": False},
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"team_metadata": {
|
||||
"logging": [
|
||||
{
|
||||
"callback_name": "langfuse",
|
||||
"callback_type": "success_and_failure",
|
||||
"callback_vars": {"turn_off_message_logging": False},
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_litellm_data_to_request_admin_callback_vars_turn_off_message_logging_overrides_global(
|
||||
admin_metadata_kwargs,
|
||||
):
|
||||
from litellm.litellm_core_utils.initialize_dynamic_callback_params import (
|
||||
initialize_standard_callback_dynamic_params,
|
||||
)
|
||||
from litellm.litellm_core_utils.redact_messages import should_redact_message_logging
|
||||
|
||||
request_mock = MagicMock(spec=Request)
|
||||
request_mock.url.path = "/v1/chat/completions"
|
||||
request_mock.url = MagicMock()
|
||||
request_mock.url.__str__.return_value = "http://localhost/v1/chat/completions"
|
||||
request_mock.method = "POST"
|
||||
request_mock.query_params = {}
|
||||
request_mock.headers = {"Content-Type": "application/json"}
|
||||
request_mock.client = MagicMock()
|
||||
request_mock.client.host = "127.0.0.1"
|
||||
|
||||
original_turn_off_message_logging = litellm.turn_off_message_logging
|
||||
litellm.turn_off_message_logging = True
|
||||
try:
|
||||
updated = await add_litellm_data_to_request(
|
||||
data={
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
},
|
||||
request=request_mock,
|
||||
user_api_key_dict=UserAPIKeyAuth(api_key="hashed-key", **admin_metadata_kwargs),
|
||||
proxy_config=MagicMock(),
|
||||
general_settings={},
|
||||
version="test-version",
|
||||
)
|
||||
|
||||
assert updated.get("turn_off_message_logging") == "False"
|
||||
|
||||
dynamic_params = initialize_standard_callback_dynamic_params(updated)
|
||||
assert dynamic_params.get("turn_off_message_logging") == "False"
|
||||
|
||||
assert (
|
||||
should_redact_message_logging(
|
||||
{"standard_callback_dynamic_params": dynamic_params}
|
||||
)
|
||||
is False
|
||||
)
|
||||
finally:
|
||||
litellm.turn_off_message_logging = original_turn_off_message_logging
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"admin_metadata_kwargs",
|
||||
[
|
||||
{
|
||||
"metadata": {
|
||||
"logging": [
|
||||
{
|
||||
"callback_name": "langfuse",
|
||||
"callback_type": "success_and_failure",
|
||||
"callback_vars": {"turn_off_message_logging": True},
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"team_metadata": {
|
||||
"logging": [
|
||||
{
|
||||
"callback_name": "langfuse",
|
||||
"callback_type": "success_and_failure",
|
||||
"callback_vars": {"turn_off_message_logging": True},
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_litellm_data_to_request_admin_callback_vars_turn_off_message_logging_enables_redaction_when_global_off(
|
||||
admin_metadata_kwargs,
|
||||
):
|
||||
from litellm.litellm_core_utils.initialize_dynamic_callback_params import (
|
||||
initialize_standard_callback_dynamic_params,
|
||||
)
|
||||
from litellm.litellm_core_utils.redact_messages import should_redact_message_logging
|
||||
|
||||
request_mock = MagicMock(spec=Request)
|
||||
request_mock.url.path = "/v1/chat/completions"
|
||||
request_mock.url = MagicMock()
|
||||
request_mock.url.__str__.return_value = "http://localhost/v1/chat/completions"
|
||||
request_mock.method = "POST"
|
||||
request_mock.query_params = {}
|
||||
request_mock.headers = {"Content-Type": "application/json"}
|
||||
request_mock.client = MagicMock()
|
||||
request_mock.client.host = "127.0.0.1"
|
||||
|
||||
original_turn_off_message_logging = litellm.turn_off_message_logging
|
||||
litellm.turn_off_message_logging = False
|
||||
try:
|
||||
updated = await add_litellm_data_to_request(
|
||||
data={
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
},
|
||||
request=request_mock,
|
||||
user_api_key_dict=UserAPIKeyAuth(api_key="hashed-key", **admin_metadata_kwargs),
|
||||
proxy_config=MagicMock(),
|
||||
general_settings={},
|
||||
version="test-version",
|
||||
)
|
||||
|
||||
assert updated.get("turn_off_message_logging") == "True"
|
||||
|
||||
dynamic_params = initialize_standard_callback_dynamic_params(updated)
|
||||
assert dynamic_params.get("turn_off_message_logging") == "True"
|
||||
|
||||
assert (
|
||||
should_redact_message_logging(
|
||||
{"standard_callback_dynamic_params": dynamic_params}
|
||||
)
|
||||
is True
|
||||
)
|
||||
finally:
|
||||
litellm.turn_off_message_logging = original_turn_off_message_logging
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"auth_kwargs",
|
||||
[
|
||||
|
|
@ -923,7 +1087,10 @@ async def test_add_litellm_data_to_request_allows_redaction_opt_out_with_admin_o
|
|||
"model": "gpt-3.5-turbo",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"turn_off_message_logging": False,
|
||||
"metadata": {"headers": {"litellm-disable-message-redaction": "true"}},
|
||||
"metadata": {
|
||||
"headers": {"litellm-disable-message-redaction": "true"},
|
||||
"turn_off_message_logging": False,
|
||||
},
|
||||
"litellm_metadata": json.dumps(
|
||||
{"headers": {"LiteLLM-Disable-Message-Redaction": "true"}}
|
||||
),
|
||||
|
|
@ -938,6 +1105,7 @@ async def test_add_litellm_data_to_request_allows_redaction_opt_out_with_admin_o
|
|||
litellm.turn_off_message_logging = original_turn_off_message_logging
|
||||
|
||||
assert updated["turn_off_message_logging"] is False
|
||||
assert updated["metadata"]["turn_off_message_logging"] is False
|
||||
assert "litellm-disable-message-redaction" in {
|
||||
header.lower() for header in updated["metadata"]["headers"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue