fix(proxy): never render credential-bearing config keys in the bug report (#42493)

Co-authored-by: ryan <ryan@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-09-22 10:17:19 -07:00 • committed by GitHub
parent f3c920a06b
commit f275be5fac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 78 additions and 6 deletions

View file

@ -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
)

View file

@ -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",