From a6a968b9fb6021ff133d75892ee227bd14375c87 Mon Sep 17 00:00:00 2001 From: yucheng Date: Thu, 8 Oct 2026 01:51:51 +0000 Subject: [PATCH] fix(proxy): keep long first-write config values without PyNaCl instead of storing null Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/common_utils/encrypt_decrypt_utils.py | 9 +++++++++ litellm/proxy/proxy_server.py | 3 ++- tests/unit/proxy/proxy_server/test_proxy_config.py | 6 ++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/common_utils/encrypt_decrypt_utils.py b/litellm/proxy/common_utils/encrypt_decrypt_utils.py index cb2be0e672d..66e09159de4 100644 --- a/litellm/proxy/common_utils/encrypt_decrypt_utils.py +++ b/litellm/proxy/common_utils/encrypt_decrypt_utils.py @@ -269,6 +269,15 @@ def legacy_unreadable(value: object) -> bool: return needs_legacy_reader(value) and not legacy_encryption_available() +def decrypt_for_rewrite(value: str, key: str) -> str | None: + """Decrypt a value a config writer submitted for storage: ciphertext read back from the store or a first-write + plaintext. Without PyNaCl an unprefixed value cannot be told apart from plaintext, so it is kept as submitted + instead of being dropped.""" + if legacy_unreadable(value): + return value + return decrypt_value_helper(value=value, key=key, return_original_value=True) + + def require_legacy_reader_for(values: Iterable[object], purpose: str) -> None: """Refuse a decrypt-then-rewrite pass when PyNaCl is missing and one of the values is unprefixed: it would read as unreadable and be dropped, double wrapped or miscounted as plaintext. Versioned gcm and non string diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 31e436d1075..56200e037fc 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -419,6 +419,7 @@ from litellm.proxy.common_utils.debug_utils import init_verbose_loggers from litellm.proxy.common_utils.debug_utils import router as debugging_endpoints_router from litellm.proxy.common_utils.discoverable_model_filter import discoverable_rows, undiscoverable_model_names from litellm.proxy.common_utils.encrypt_decrypt_utils import ( + decrypt_for_rewrite, decrypt_value_helper, encrypt_value_helper, ) @@ -7794,7 +7795,7 @@ class ProxyConfig: _decrypt_and_set_db_env_variables): this is a write path, and loading values into os.environ is the read path's responsibility. """ - decrypted_env_vars: Final = self._decrypt_db_variables(environment_variables) + decrypted_env_vars: Final = {k: decrypt_for_rewrite(value=v, key=k) for k, v in environment_variables.items()} return self._encrypt_env_variables( environment_variables=decrypted_env_vars, new_encryption_key=new_encryption_key, diff --git a/tests/unit/proxy/proxy_server/test_proxy_config.py b/tests/unit/proxy/proxy_server/test_proxy_config.py index 4f149ad54f0..3874ad8402f 100644 --- a/tests/unit/proxy/proxy_server/test_proxy_config.py +++ b/tests/unit/proxy/proxy_server/test_proxy_config.py @@ -3755,11 +3755,13 @@ def test_ProxyConfig__encrypt_env_variables_for_db_without_pynacl_encrypts_new_p monkeypatch.setitem(sys.modules, "nacl.secret", None) pc = ProxyConfig() - saved = pc._encrypt_env_variables_for_db({"A": v3, "B": "first-write-plaintext"}) - assert saved["B"].startswith("v3:gcm:") and saved["B"] != "first-write-plaintext" + long_plaintext = "A" * 56 + saved = pc._encrypt_env_variables_for_db({"A": v3, "B": "first-write-plaintext", "C": long_plaintext}) + assert all(v.startswith("v3:gcm:") for v in saved.values()) assert {k: decrypt_value_helper(v, key=k) for k, v in saved.items()} == { "A": "already-migrated", "B": "first-write-plaintext", + "C": long_plaintext, }