Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 34 additions & 6 deletions litellm/proxy/bug_report_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@
_OBJECT_MAP: Final[TypeAdapter[Mapping[str, object]]] = TypeAdapter(Mapping[str, object])
_OBJECT_LIST: Final[TypeAdapter[tuple[object, ...]]] = TypeAdapter(tuple[object, ...])
_JSON: Final[TypeAdapter[JsonValue]] = TypeAdapter(JsonValue)
CREDENTIAL_KEY_PARTS: Final = frozenset(
{
"key",
"keys",
"secret",
"secrets",
"token",
"password",
"passwd",
"credential",
"credentials",
"url",
"uri",
"dsn",
"host",
"hosts",
"base",
"endpoint",
"cert",
"pem",
"salt",
}
)
ENUM_KEYS_WITH_CREDENTIAL_PARTS: Final = frozenset({"key_management_system"})


def _object_map(value: object) -> Mapping[str, object]:
Expand Down Expand Up @@ -87,22 +111,26 @@
return frozenset(name for name in inspect.signature(Cache.__init__).parameters if name != "self") # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # untyped params, only names are read


def _render_json(value: JsonValue) -> str | None:
def _is_credential_key(key: str) -> bool:
return key not in ENUM_KEYS_WITH_CREDENTIAL_PARTS and not CREDENTIAL_KEY_PARTS.isdisjoint(key.lower().split("_"))


def _render_json(key: str, value: JsonValue) -> str | None:
match value:
case bool():
return str(value).lower()
case str():
return value if value in _known_values() else None
return value if value in _known_values() and not _is_credential_key(key) else None
case list():
known_items: Final = tuple(rendered for item in value if (rendered := _render_json(item)) is not None)
known_items: Final = tuple(rendered for item in value if (rendered := _render_json(key, item)) is not None)
return f"[{', '.join(known_items)}]" if known_items else None
case _:
return None


def _render(value: object) -> str | None:
def _render(key: str, value: object) -> str | None:
try:
return _render_json(_JSON.validate_python(value))
return _render_json(key, _JSON.validate_python(value))
except ValidationError:
return None

Expand All @@ -111,7 +139,7 @@
return tuple(
f"{section}.{key} = {rendered}"
for key, value in values.items()
if key in known_keys and (rendered := _render(value)) is not None
if key in known_keys and (rendered := _render(key, value)) is not None
)


Expand Down
44 changes: 44 additions & 0 deletions tests/test_litellm/proxy/test_bug_report_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,50 @@ def test_secrets_numbers_and_unknown_values_leave_no_line():
assert safe_config_lines({}, general_settings) == ("general_settings.background_health_checks = false",)


def test_credential_keys_never_render_even_when_the_secret_equals_a_litellm_token():
config: Mapping[str, object] = {
"litellm_settings": {
"openai_key": "openai",
"token": "langfuse",
"api_base": "azure",
"callbacks": ["langfuse"],
"cache_params": {"type": "redis", "password": "redis", "host": "openai", "qdrant_api_key": "qdrant"},
},
"router_settings": {
"routing_strategy": "simple-shuffle",
"redis_password": "simple-shuffle",
"redis_url": "redis",
},
"guardrails": [
{
"litellm_params": {
"guardrail": "presidio",
"api_key": "presidio",
"auth_token": "pre_call",
"client_secret": "openai",
"credentials": ["openai", "azure"],
}
}
],
}
general_settings: Mapping[str, object] = {
"master_key": "redis",
"database_url": "openai",
"alert_to_webhook_url": "langfuse",
"key_management_system": "aws_secret_manager",
"use_azure_key_vault": True,
}

assert safe_config_lines(config, general_settings) == (
"general_settings.key_management_system = aws_secret_manager",
"general_settings.use_azure_key_vault = true",
"litellm_settings.callbacks = [langfuse]",
"litellm_settings.cache_params.type = redis",
"router_settings.routing_strategy = simple-shuffle",
"guardrails[0].litellm_params.guardrail = presidio",
)


def test_malformed_sections_produce_no_lines():
config: Mapping[str, object] = {
"litellm_settings": "acme",
Expand Down
Loading