Merge pull request #25791 from J-Byron/fix/langfuse-credential-leak

Langfuse credential leak
This commit is contained in:
ishaan-berri 2026-04-18 10:12:20 -07:00 committed by GitHub
commit 237b18ad85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 595 additions and 9 deletions

View file

@ -1,5 +1,11 @@
import copy
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Literal, Optional
from litellm.proxy.common_utils.encrypt_decrypt_utils import (
decrypt_value_helper,
encrypt_value_helper,
)
import litellm
from litellm import get_secret
from litellm._logging import verbose_proxy_logger
@ -524,3 +530,89 @@ def normalize_callback_names(callbacks: Iterable[Any]) -> List[Any]:
if callbacks is None:
return []
return [c.lower() if isinstance(c, str) else c for c in callbacks]
def encrypt_logging_callback_vars(metadata: Optional[Dict]) -> Optional[Dict]:
"""
Encrypt credential values inside ``metadata["logging"][*]["callback_vars"]``
before persisting to the database.
Values that are environment-variable references (``os.environ/``) are
left untouched they are pointers, not secrets, and do not need
to be stored encrypted.
Returns the *same* dict (mutated in-place) so the caller can assign the
result back to ``metadata`` if convenient.
"""
if not metadata:
return metadata
logging_configs = metadata.get("logging")
if not isinstance(logging_configs, list):
return metadata
for entry in logging_configs:
if not isinstance(entry, dict):
continue
callback_vars = entry.get("callback_vars")
if not isinstance(callback_vars, dict):
continue
for key, value in callback_vars.items():
if not isinstance(value, str):
continue
# Leave env-var pointers as-is; encrypt everything else
if value.startswith("os.environ/"):
continue
callback_vars[key] = encrypt_value_helper(value)
return metadata
def redact_sensitive_logging_metadata(metadata: Optional[Dict]) -> Optional[Dict]:
"""
Return a copy of `metadata` with credential values inside
`metadata["logging"][*]["callback_vars"]` partially masked.
Each value is replaced with "...XYZ" where XYZ is the last 3 characters
of the plaintext (decrypting first if the value is encrypted), giving
admins a visual hint without exposing the full secret.
Values that are environment-variable references
(e.g. "os.environ/LANGFUSE_SECRET_KEY") are left as-is.
"""
if not metadata:
return metadata
metadata = copy.deepcopy(metadata)
logging_configs = metadata.get("logging")
if not isinstance(logging_configs, list):
return metadata
for entry in logging_configs:
if not isinstance(entry, dict):
continue
callback_vars = entry.get("callback_vars")
if not isinstance(callback_vars, dict):
continue
for key, value in callback_vars.items():
if not isinstance(value, str):
callback_vars[key] = "***"
continue
# Keep env-var pointers as-is
if value.startswith("os.environ/"):
continue
# Decrypt to get plaintext (no-op for already-plaintext rows)
plaintext = str(
decrypt_value_helper(
value=value,
key=key,
exception_type="debug",
return_original_value=True,
)
or value
)
suffix = plaintext[-3:] if len(plaintext) >= 3 else plaintext
callback_vars[key] = f"...{suffix}"
return metadata

View file

@ -21,6 +21,7 @@ from litellm.proxy._types import (
TeamCallbackMetadata,
UserAPIKeyAuth,
)
from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper
from litellm.proxy.common_utils.http_parsing_utils import _safe_get_request_headers
# Cache special headers as a frozenset for O(1) lookup performance
@ -187,9 +188,18 @@ def convert_key_logging_metadata_to_callback(
for var, value in data.callback_vars.items():
if team_callback_settings_obj.callback_vars is None:
team_callback_settings_obj.callback_vars = {}
team_callback_settings_obj.callback_vars[var] = str(
litellm.utils.get_secret(value, default_value=value) or value
)
resolved = str(litellm.utils.get_secret(value, default_value=value) or value)
# If not an env-var pointer the stored value may be encrypted — decrypt it
if not value.startswith("os.environ/"):
resolved = str(
decrypt_value_helper(
value=resolved,
key=var,
exception_type="debug",
return_original_value=True,
)
)
team_callback_settings_obj.callback_vars[var] = resolved
return team_callback_settings_obj

View file

@ -49,6 +49,10 @@ from litellm.proxy.auth.auth_checks import (
)
from litellm.proxy.auth.auth_utils import abbreviate_api_key
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.common_utils.callback_utils import (
encrypt_logging_callback_vars,
redact_sensitive_logging_metadata,
)
from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time
from litellm.proxy.hooks.key_management_event_hooks import KeyManagementEventHooks
from litellm.proxy.management_endpoints.common_utils import (
@ -1513,6 +1517,7 @@ def prepare_metadata_fields(
"""
Check LiteLLM_ManagementEndpoint_MetadataFields (proxy/_types.py) for fields that are allowed to be updated
"""
metadata_provided_in_update = "metadata" in non_default_values
if "metadata" not in non_default_values: # allow user to set metadata to none
non_default_values["metadata"] = existing_metadata.copy()
@ -1541,6 +1546,8 @@ def prepare_metadata_fields(
)
non_default_values["metadata"] = casted_metadata
if metadata_provided_in_update and "logging" in casted_metadata:
encrypt_logging_callback_vars(non_default_values["metadata"])
return non_default_values
@ -2723,6 +2730,9 @@ async def info_key_fn_v2(
except Exception:
k_dict = k.dict()
k_dict.pop("token", None)
k_dict["metadata"] = redact_sensitive_logging_metadata(
k_dict.get("metadata")
)
filtered_key_info.append(k_dict)
return {"key": data.keys, "info": filtered_key_info}
@ -2806,6 +2816,12 @@ async def info_key_fn(
key_info = key_info.dict()
key_info.pop("token")
# Scrub credentials stored in metadata.logging[].callback_vars so
# Langfuse / other integration secrets are never returned in plaintext.
key_info["metadata"] = redact_sensitive_logging_metadata(
key_info.get("metadata")
)
# Attach object_permission if object_permission_id is set
key_info = await attach_object_permission_to_dict(key_info, prisma_client)
@ -2970,6 +2986,7 @@ async def generate_key_helper_fn( # noqa: PLR0915
metadata = metadata or {}
metadata["prompts"] = prompts
encrypt_logging_callback_vars(metadata)
metadata_json = json.dumps(metadata)
validate_model_max_budget(model_max_budget)
model_max_budget_json = json.dumps(model_max_budget)
@ -4964,6 +4981,9 @@ async def _list_key_helper(
}
if return_full_object is True or (expand and "user" in expand):
key_dict["metadata"] = redact_sensitive_logging_metadata(
key_dict.get("metadata")
)
if use_deleted_table:
# Use deleted key type to preserve deleted_at, deleted_by, etc.
key_list.append(LiteLLM_DeletedVerificationToken(**key_dict))

View file

@ -19,6 +19,10 @@ from litellm.proxy._types import (
UserAPIKeyAuth,
)
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.common_utils.callback_utils import (
encrypt_logging_callback_vars,
redact_sensitive_logging_metadata,
)
from litellm.proxy.management_helpers.utils import management_endpoint_wrapper
router = APIRouter()
@ -123,7 +127,9 @@ async def add_team_callbacks(
param="callback_name",
)
team_callback_settings.append(data.model_dump())
new_entry = data.model_dump()
encrypt_logging_callback_vars({"logging": [new_entry]})
team_callback_settings.append(new_entry)
team_metadata["logging"] = team_callback_settings
team_metadata_json = json.dumps(team_metadata) # update team_metadata
@ -312,13 +318,22 @@ async def get_team_callbacks(
# Convert to TeamCallbackMetadata object for consistent structure
team_callback_settings_obj = TeamCallbackMetadata(**team_callback_settings)
redacted_metadata = redact_sensitive_logging_metadata(
{"logging": [{"callback_vars": team_callback_settings_obj.callback_vars}]}
)
redacted_vars = (
redacted_metadata["logging"][0]["callback_vars"]
if redacted_metadata
else team_callback_settings_obj.callback_vars
)
return {
"status": "success",
"data": {
"team_id": team_id,
"success_callbacks": team_callback_settings_obj.success_callback,
"failure_callbacks": team_callback_settings_obj.failure_callback,
"callback_vars": team_callback_settings_obj.callback_vars,
"callback_vars": redacted_vars,
},
}

View file

@ -69,6 +69,10 @@ from litellm.proxy.auth.auth_checks import (
get_user_object,
)
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.common_utils.callback_utils import (
encrypt_logging_callback_vars,
redact_sensitive_logging_metadata,
)
from litellm.proxy.management_endpoints.common_utils import (
_is_user_org_admin_for_team,
_is_user_team_admin,
@ -1095,6 +1099,7 @@ async def new_team( # noqa: PLR0915
complete_team_data.members_with_roles = []
complete_team_data_dict = complete_team_data.model_dump(exclude_none=True)
encrypt_logging_callback_vars(complete_team_data_dict.get("metadata"))
# Serialize router_settings to JSON (matching key creation pattern)
router_settings_value = getattr(data, "router_settings", None)
@ -1654,6 +1659,7 @@ async def update_team( # noqa: PLR0915
# update team metadata fields
_update_metadata_fields(updated_kv=updated_kv)
encrypt_logging_callback_vars(updated_kv.get("metadata"))
if "model_aliases" in updated_kv:
updated_kv.pop("model_aliases")
@ -3197,6 +3203,7 @@ async def team_info(
# if using pydantic v1
key = key.dict()
key.pop("token", None)
key["metadata"] = redact_sensitive_logging_metadata(key.get("metadata"))
## GET ALL MEMBERSHIPS ##
returned_tm = await get_all_team_memberships(
@ -3226,6 +3233,10 @@ async def team_info(
# Resolve resources inherited from access groups
await _resolve_team_access_group_resources(_team_info)
# Scrub credentials from team-level logging config before returning
if _team_info.metadata is not None:
_team_info.metadata = redact_sensitive_logging_metadata(_team_info.metadata)
response_object = TeamInfoResponseObject(
team_id=team_id,
team_info=_team_info,
@ -3578,6 +3589,9 @@ def _convert_teams_to_response_models(
except Exception:
team_dict = team.dict()
team_dict["metadata"] = redact_sensitive_logging_metadata(
team_dict.get("metadata")
)
if use_deleted_table:
team_list.append(LiteLLM_DeletedTeamTable(**team_dict))
else:
@ -4002,9 +4016,13 @@ async def list_team(
)
try:
_team_dict = team.model_dump()
_team_dict["metadata"] = redact_sensitive_logging_metadata(
_team_dict.get("metadata")
)
returned_responses.append(
TeamListResponseObject(
**team.model_dump(),
**_team_dict,
team_memberships=_team_memberships,
keys=keys,
)
@ -4012,9 +4030,7 @@ async def list_team(
except Exception as e:
team_exception = """Invalid team object for team_id: {}. team_object={}.
Error: {}
""".format(
team.team_id, team.model_dump(), str(e)
)
""".format(team.team_id, team.model_dump(), str(e))
verbose_proxy_logger.exception(team_exception)
continue
# Sort the responses by team_alias

View file

@ -6,8 +6,10 @@ sys.path.insert(
) # Adds the parent directory to the system path
from litellm.proxy.common_utils.callback_utils import (
encrypt_logging_callback_vars,
get_remaining_tokens_and_requests_from_request_data,
normalize_callback_names,
redact_sensitive_logging_metadata,
)
from unittest.mock import patch
@ -84,3 +86,171 @@ def test_normalize_callback_names_lowercases_strings():
"s3",
"custom_callback",
]
# ---------------------------------------------------------------------------
# redact_sensitive_logging_metadata tests
# ---------------------------------------------------------------------------
def test_redact_scrubs_real_secret_values():
"""Real credential values must be partially masked showing last 3 chars."""
metadata = {
"logging": [
{
"callback_name": "langfuse",
"callback_type": "success_and_failure",
"callback_vars": {
"langfuse_public_key": "pk-lf-abc123",
"langfuse_secret_key": "sk-lf-supersecret",
"langfuse_host": "https://us.cloud.langfuse.com",
},
}
]
}
result = redact_sensitive_logging_metadata(metadata)
vars_ = result["logging"][0]["callback_vars"]
assert vars_["langfuse_public_key"] == "...123"
assert vars_["langfuse_secret_key"] == "...ret"
assert vars_["langfuse_host"] == "...com"
def test_redact_keeps_env_var_references():
"""os.environ/ pointers are not secrets — they must be preserved."""
metadata = {
"logging": [
{
"callback_name": "langfuse",
"callback_vars": {
"langfuse_public_key": "os.environ/LANGFUSE_PUBLIC_KEY",
"langfuse_secret_key": "os.environ/LANGFUSE_SECRET_KEY",
},
}
]
}
result = redact_sensitive_logging_metadata(metadata)
vars_ = result["logging"][0]["callback_vars"]
assert vars_["langfuse_public_key"] == "os.environ/LANGFUSE_PUBLIC_KEY"
assert vars_["langfuse_secret_key"] == "os.environ/LANGFUSE_SECRET_KEY"
def test_redact_does_not_mutate_original():
"""The original metadata dict must not be modified."""
metadata = {
"logging": [
{
"callback_name": "langfuse",
"callback_vars": {"langfuse_secret_key": "sk-real-secret"},
}
]
}
original_value = metadata["logging"][0]["callback_vars"]["langfuse_secret_key"]
redact_sensitive_logging_metadata(metadata)
assert (
metadata["logging"][0]["callback_vars"]["langfuse_secret_key"] == original_value
)
def test_redact_returns_none_for_none_input():
assert redact_sensitive_logging_metadata(None) is None
def test_redact_returns_unchanged_when_no_logging_key():
"""Metadata without a 'logging' key passes through untouched."""
metadata = {"some_other_key": "value"}
result = redact_sensitive_logging_metadata(metadata)
assert result == {"some_other_key": "value"}
def test_redact_mixed_env_and_real_values():
"""Only real values are scrubbed; env-var pointers in the same dict survive."""
metadata = {
"logging": [
{
"callback_name": "langfuse",
"callback_vars": {
"langfuse_public_key": "os.environ/LANGFUSE_PUBLIC_KEY",
"langfuse_secret_key": "sk-real-secret",
},
}
]
}
result = redact_sensitive_logging_metadata(metadata)
vars_ = result["logging"][0]["callback_vars"]
assert vars_["langfuse_public_key"] == "os.environ/LANGFUSE_PUBLIC_KEY"
assert vars_["langfuse_secret_key"] == "...ret"
# ---------------------------------------------------------------------------
# encrypt_logging_callback_vars tests
# ---------------------------------------------------------------------------
def _make_metadata(callback_vars: dict) -> dict:
return {
"logging": [
{
"callback_name": "langfuse",
"callback_type": "success_and_failure",
"callback_vars": callback_vars,
}
]
}
def test_encrypt_returns_none_for_none():
assert encrypt_logging_callback_vars(None) is None
def test_encrypt_returns_unchanged_when_no_logging_key():
metadata = {"other_key": "value"}
result = encrypt_logging_callback_vars(metadata)
assert result == {"other_key": "value"}
def test_encrypt_leaves_env_var_pointers_unchanged(monkeypatch):
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-key-1234567890123456")
metadata = _make_metadata({"langfuse_secret_key": "os.environ/LANGFUSE_SECRET_KEY"})
result = encrypt_logging_callback_vars(metadata)
assert (
result["logging"][0]["callback_vars"]["langfuse_secret_key"]
== "os.environ/LANGFUSE_SECRET_KEY"
)
def test_encrypt_real_values_are_changed(monkeypatch):
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-key-1234567890123456")
plaintext = "sk-lf-supersecret"
metadata = _make_metadata({"langfuse_secret_key": plaintext})
result = encrypt_logging_callback_vars(metadata)
encrypted = result["logging"][0]["callback_vars"]["langfuse_secret_key"]
assert encrypted != plaintext
assert isinstance(encrypted, str)
def test_encrypt_then_decrypt_roundtrip(monkeypatch):
"""Encrypt a value and confirm decrypt_value_helper returns the original."""
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-key-1234567890123456")
from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper
plaintext = "pk-lf-abc123"
metadata = _make_metadata({"langfuse_public_key": plaintext})
encrypt_logging_callback_vars(metadata)
encrypted = metadata["logging"][0]["callback_vars"]["langfuse_public_key"]
assert encrypted != plaintext
recovered = decrypt_value_helper(
value=encrypted, key="langfuse_public_key", return_original_value=True
)
assert recovered == plaintext
def test_encrypt_mutates_in_place(monkeypatch):
"""encrypt_logging_callback_vars modifies the dict in-place."""
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-key-1234567890123456")
metadata = _make_metadata({"langfuse_secret_key": "sk-lf-real"})
returned = encrypt_logging_callback_vars(metadata)
assert returned is metadata
assert (
metadata["logging"][0]["callback_vars"]["langfuse_secret_key"] != "sk-lf-real"
)

View file

@ -9007,3 +9007,80 @@ async def test_execute_virtual_key_regeneration_cache_invalidation_with_token_ha
call_kwargs = mock_delete_cache.call_args.kwargs
# The token hash should be passed as-is, NOT double-hashed
assert call_kwargs["hashed_token"] == token_hash
# ---------------------------------------------------------------------------
# prepare_metadata_fields double-encryption guard tests
# ---------------------------------------------------------------------------
def test_prepare_metadata_fields_no_double_encrypt_when_metadata_not_in_update(
monkeypatch,
):
"""
When metadata is NOT in the update request, existing_metadata (which may
already have encrypted callback_vars) must be copied as-is.
encrypt_logging_callback_vars must NOT be called.
"""
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-key-1234567890123456")
from litellm.proxy.management_endpoints.key_management_endpoints import (
prepare_metadata_fields,
)
already_encrypted = "someencryptedblob=="
existing_metadata = {
"logging": [
{
"callback_name": "langfuse",
"callback_vars": {"langfuse_secret_key": already_encrypted},
}
]
}
data = UpdateKeyRequest(key="sk-test")
non_default_values: dict = {} # metadata NOT provided in update
result = prepare_metadata_fields(
data=data,
non_default_values=non_default_values,
existing_metadata=existing_metadata,
)
# Value must be unchanged — no second encryption pass
assert (
result["metadata"]["logging"][0]["callback_vars"]["langfuse_secret_key"]
== already_encrypted
)
def test_prepare_metadata_fields_encrypts_when_metadata_in_update(monkeypatch):
"""
When metadata IS in the update request with fresh callback_vars, they must
be encrypted.
"""
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-key-1234567890123456")
from litellm.proxy.management_endpoints.key_management_endpoints import (
prepare_metadata_fields,
)
plaintext = "sk-lf-supersecret"
fresh_metadata = {
"logging": [
{
"callback_name": "langfuse",
"callback_vars": {"langfuse_secret_key": plaintext},
}
]
}
data = UpdateKeyRequest(key="sk-test", metadata=fresh_metadata)
non_default_values: dict = {"metadata": fresh_metadata} # metadata WAS provided
result = prepare_metadata_fields(
data=data,
non_default_values=non_default_values,
existing_metadata={},
)
encrypted = result["metadata"]["logging"][0]["callback_vars"]["langfuse_secret_key"]
assert encrypted != plaintext, "plaintext credential must be encrypted on write"

View file

@ -0,0 +1,186 @@
import json
import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
sys.path.insert(0, os.path.abspath("../../../.."))
from litellm.proxy.management_endpoints.team_callback_endpoints import (
add_team_callbacks,
get_team_callbacks,
)
from litellm.proxy._types import AddTeamCallback, UserAPIKeyAuth
def _make_team(metadata: dict) -> MagicMock:
team = MagicMock()
team.metadata = metadata
return team
def _make_request() -> MagicMock:
req = MagicMock()
req.headers = {}
return req
# ---------------------------------------------------------------------------
# POST /team/{team_id}/callback — encrypt on write
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@patch("litellm.proxy.proxy_server.prisma_client")
async def test_add_team_callback_encrypts_callback_vars(mock_prisma, monkeypatch):
"""callback_vars must be encrypted before writing to DB."""
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-key-1234567890123456")
plaintext_secret = "sk-lf-supersecret99"
existing_team = _make_team({"logging": []})
mock_prisma.get_data = AsyncMock(return_value=existing_team)
captured = {}
async def fake_update(where, data):
captured["data"] = data
row = MagicMock()
row.metadata = data.get("metadata", "{}")
return row
mock_prisma.db = MagicMock()
mock_prisma.db.litellm_teamtable = MagicMock()
mock_prisma.db.litellm_teamtable.update = fake_update
data = AddTeamCallback(
callback_name="langfuse",
callback_type="success",
callback_vars={"langfuse_secret_key": plaintext_secret},
)
await add_team_callbacks(
data=data,
http_request=_make_request(),
team_id="team-123",
user_api_key_dict=UserAPIKeyAuth(),
)
written_metadata = json.loads(captured["data"]["metadata"])
stored_secret = written_metadata["logging"][0]["callback_vars"]["langfuse_secret_key"]
assert stored_secret != plaintext_secret, "plaintext secret must be encrypted before DB write"
@pytest.mark.asyncio
@patch("litellm.proxy.proxy_server.prisma_client")
async def test_add_second_callback_does_not_double_encrypt_first(mock_prisma, monkeypatch):
"""Adding a second callback must not re-encrypt already-encrypted entries."""
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-key-1234567890123456")
from litellm.proxy.common_utils.callback_utils import encrypt_logging_callback_vars
from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper
# Simulate a team whose first callback was already written (and encrypted)
first_entry = {
"callback_name": "langfuse",
"callback_type": "success",
"callback_vars": {"langfuse_secret_key": "sk-lf-firstsecret"},
}
encrypt_logging_callback_vars({"logging": [first_entry]})
encrypted_first = first_entry["callback_vars"]["langfuse_secret_key"]
existing_team = _make_team({"logging": [first_entry]})
mock_prisma.get_data = AsyncMock(return_value=existing_team)
captured = {}
async def fake_update(where, data):
captured["data"] = data
row = MagicMock()
row.metadata = data.get("metadata", "{}")
return row
mock_prisma.db = MagicMock()
mock_prisma.db.litellm_teamtable = MagicMock()
mock_prisma.db.litellm_teamtable.update = fake_update
data = AddTeamCallback(
callback_name="langsmith",
callback_type="success",
callback_vars={"langsmith_api_key": "ls-secondsecret"},
)
await add_team_callbacks(
data=data,
http_request=_make_request(),
team_id="team-123",
user_api_key_dict=UserAPIKeyAuth(),
)
written_metadata = json.loads(captured["data"]["metadata"])
# First entry must be unchanged (not re-encrypted)
assert written_metadata["logging"][0]["callback_vars"]["langfuse_secret_key"] == encrypted_first
# First entry must still decrypt correctly
recovered = decrypt_value_helper(value=encrypted_first, key="langfuse_secret_key", return_original_value=True)
assert recovered == "sk-lf-firstsecret"
# Second entry must be encrypted
stored_second = written_metadata["logging"][1]["callback_vars"]["langsmith_api_key"]
assert stored_second != "ls-secondsecret"
# ---------------------------------------------------------------------------
# GET /team/{team_id}/callback — redact on read
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@patch("litellm.proxy.proxy_server.prisma_client")
async def test_get_team_callbacks_redacts_callback_vars(mock_prisma, monkeypatch):
"""callback_vars must be redacted (last-3-chars) in the GET response."""
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-key-1234567890123456")
existing_team = _make_team(
{
"callback_settings": {
"success_callback": ["langfuse"],
"callback_vars": {"langfuse_secret_key": "sk-lf-supersecret99"},
}
}
)
mock_prisma.get_data = AsyncMock(return_value=existing_team)
result = await get_team_callbacks(
http_request=_make_request(),
team_id="team-123",
user_api_key_dict=UserAPIKeyAuth(),
)
secret = result["data"]["callback_vars"]["langfuse_secret_key"]
assert secret != "sk-lf-supersecret99", "plaintext secret must not be returned"
assert secret.startswith("..."), "redacted value must use ...XYZ format"
@pytest.mark.asyncio
@patch("litellm.proxy.proxy_server.prisma_client")
async def test_get_team_callbacks_keeps_env_var_pointers(mock_prisma):
"""os.environ/ references must pass through unredacted."""
existing_team = _make_team(
{
"callback_settings": {
"callback_vars": {
"langfuse_secret_key": "os.environ/LANGFUSE_SECRET_KEY"
},
}
}
)
mock_prisma.get_data = AsyncMock(return_value=existing_team)
result = await get_team_callbacks(
http_request=_make_request(),
team_id="team-123",
user_api_key_dict=UserAPIKeyAuth(),
)
assert (
result["data"]["callback_vars"]["langfuse_secret_key"]
== "os.environ/LANGFUSE_SECRET_KEY"
)