diff --git a/litellm/proxy/bug_report_config.py b/litellm/proxy/bug_report_config.py index fa4a2604209..527be65c9a7 100644 --- a/litellm/proxy/bug_report_config.py +++ b/litellm/proxy/bug_report_config.py @@ -21,6 +21,30 @@ from litellm.types.secret_managers.main import KeyManagementSystem _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]: @@ -87,22 +111,26 @@ def _cache_params_keys() -> frozenset[str]: 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 @@ -111,7 +139,7 @@ def _section_lines(section: str, values: Mapping[str, object], known_keys: froze 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 ) diff --git a/tests/test_litellm/proxy/test_bug_report_config.py b/tests/test_litellm/proxy/test_bug_report_config.py index ce4baa946f9..06bca8c66fb 100644 --- a/tests/test_litellm/proxy/test_bug_report_config.py +++ b/tests/test_litellm/proxy/test_bug_report_config.py @@ -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",