From 47af50f3dec601d76bd83f0fed49c41aa03a4578 Mon Sep 17 00:00:00 2001 From: yucheng Date: Thu, 8 Oct 2026 02:41:55 +0000 Subject: [PATCH] fix(proxy): skip plaintext complexity_router_config in the encryption scanner Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../credential_migration.py | 3 ++ .../test_credential_migration.py | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/litellm/proxy/management_endpoints/credential_migration.py b/litellm/proxy/management_endpoints/credential_migration.py index 2c27a888902..66a2f10f9b7 100644 --- a/litellm/proxy/management_endpoints/credential_migration.py +++ b/litellm/proxy/management_endpoints/credential_migration.py @@ -476,6 +476,7 @@ _COVERED_TABLE_SPECS: Final = [ ("sso_identity_assertion", "litellm_ssoidentityassertion", (), ("assertion_b64",)), ] _MCP_CREDENTIAL_TABLES: Final = frozenset({"litellm_mcpservertable", "litellm_mcpserveroauthclient"}) +_PLAINTEXT_LITELLM_PARAMS: Final = frozenset({"complexity_router_config"}) def _iter_encrypted_strings(obj: object): @@ -518,6 +519,8 @@ def _mcp_encrypted_leaves(col: str, raw: object) -> Iterator[str]: def _encrypted_leaves(db_attr: str, col: str, raw: object) -> Iterator[str]: if db_attr in _MCP_CREDENTIAL_TABLES and col in ("credentials", "env_vars"): return _mcp_encrypted_leaves(col, raw) + if db_attr == "litellm_proxymodeltable" and isinstance(raw, dict): + return _iter_encrypted_strings({k: v for k, v in raw.items() if k not in _PLAINTEXT_LITELLM_PARAMS}) return _iter_encrypted_strings(raw) diff --git a/tests/unit/proxy/management_endpoints/test_credential_migration.py b/tests/unit/proxy/management_endpoints/test_credential_migration.py index 4aa718f10dd..40de8d22b0d 100644 --- a/tests/unit/proxy/management_endpoints/test_credential_migration.py +++ b/tests/unit/proxy/management_endpoints/test_credential_migration.py @@ -159,6 +159,46 @@ async def test_migrate_and_check_refuse_without_pynacl_instead_of_miscounting_le client.db.litellm_config.update.assert_not_awaited() +@pytest.mark.asyncio +async def test_check_and_rotation_preflight_pass_without_pynacl_when_only_complexity_router_config_is_plaintext( + salt_key, monkeypatch +): + _enable_aes(monkeypatch) + monkeypatch.setitem(sys.modules, "nacl", None) + monkeypatch.setitem(sys.modules, "nacl.secret", None) + client: Final = MagicMock() + _empty_covered_tables(client) + client.db.litellm_teamtable.find_many = AsyncMock(return_value=[]) + client.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[]) + client.db.litellm_ssoconfig.find_unique = AsyncMock(return_value=None) + client.db.litellm_config.find_unique = AsyncMock(return_value=None) + client.db.litellm_proxymodeltable.find_many = AsyncMock( + return_value=[ + SimpleNamespace( + litellm_params={ + "api_key": encrypt_value_helper("migrated-secret"), + "complexity_router_config": { + "strong_model": "gpt-5.1", + "opensource_classifier_config": {"api_base": "http://classifier.internal"}, + }, + } + ) + ] + ) + + report: Final = await cm.check_encryption(prisma_client=client) + + assert report.residual_legacy == 0, report.to_dict() + assert report.as_dict()["locations"]["model_table"] == { + "scanned": 1, + "migrated": 0, + "already_v2": 1, + "plaintext": 0, + "undecryptable": 0, + "legacy": 0, + } + + @pytest.mark.parametrize( "db_attr, build_row", [