From d5cf5640b0d2f5451af553024c135e25a9b05c8d Mon Sep 17 00:00:00 2001 From: AaronHowell <111272993+AaronHowell@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:30:29 +0800 Subject: [PATCH 1/2] fix(responses): preserve provider affinity Co-authored-by: Bytechoreographer --- .../encrypted_content_affinity_check.py | 30 +++- .../test_encrypted_content_affinity_check.py | 136 +++++++++++++++++- 2 files changed, 162 insertions(+), 4 deletions(-) diff --git a/litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py b/litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py index b623e31ce06..eba2f36b675 100644 --- a/litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py +++ b/litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py @@ -37,6 +37,7 @@ Safe to enable globally: """ import time +from collections.abc import Mapping from typing import TYPE_CHECKING, Final, Optional, Protocol, cast import httpx @@ -48,6 +49,7 @@ from litellm.exceptions import ( ServiceUnavailableError, ) from litellm.integrations.custom_logger import CustomLogger, Span +from litellm.litellm_core_utils.credential_accessor import CredentialAccessor from litellm.responses.utils import ResponsesAPIRequestUtils from litellm.router_utils.cooldown_cache import CooldownCacheValue from litellm.types.llms.openai import AllMessageValues @@ -161,11 +163,13 @@ class EncryptedContentAffinityCheck(CustomLogger): @staticmethod def _encryption_boundary_key( litellm_params: object, - ) -> tuple | None: + ) -> tuple[object, object] | None: """ ``(api_base, api_key)`` pair identifying an Azure resource. Two deployments sharing both are interchangeable for ``encrypted_content`` follow-ups; Azure rejects content produced by any other resource. + Missing values are resolved from ``litellm_credential_name`` without + modifying the deployment, and explicit deployment values take precedence. Accepts any object exposing dict-style ``.get(key, default)``: plain dicts (the common case in ``healthy_deployments``) as well as @@ -180,9 +184,29 @@ class EncryptedContentAffinityCheck(CustomLogger): return None api_base: Final = getter("api_base") api_key: Final = getter("api_key") - if not api_base or not api_key: + credential_name: Final = getter("litellm_credential_name") + credential_values: Final[Mapping[str, object] | None] = ( + CredentialAccessor.get_credential_values(credential_name) + if isinstance(credential_name, str) and credential_name and (api_base is None or api_key is None) + else None + ) + effective_api_base: Final = ( + api_base + if api_base is not None + else credential_values.get("api_base") + if credential_values is not None + else None + ) + effective_api_key: Final = ( + api_key + if api_key is not None + else credential_values.get("api_key") + if credential_values is not None + else None + ) + if not effective_api_base or not effective_api_key: return None - return (api_base, api_key) + return (effective_api_base, effective_api_key) def _find_deployments_on_same_encryption_boundary( self, diff --git a/tests/test_litellm/router_utils/pre_call_checks/test_encrypted_content_affinity_check.py b/tests/test_litellm/router_utils/pre_call_checks/test_encrypted_content_affinity_check.py index dac991a41c4..46e67983c33 100644 --- a/tests/test_litellm/router_utils/pre_call_checks/test_encrypted_content_affinity_check.py +++ b/tests/test_litellm/router_utils/pre_call_checks/test_encrypted_content_affinity_check.py @@ -21,8 +21,8 @@ from unittest.mock import AsyncMock, patch import pytest - import litellm +from litellm.models.credentials import CredentialItem from litellm.responses.utils import ResponsesAPIRequestUtils from litellm.types.llms.openai import ResponsesAPIResponse @@ -1148,6 +1148,140 @@ def test_boundary_key_accepts_pydantic_litellm_params_instance(): ) +def test_boundary_key_resolves_missing_values_from_named_credential(): + from litellm.router_utils.pre_call_checks.encrypted_content_affinity_check import ( + EncryptedContentAffinityCheck, + ) + + with ( + patch.object( # test-quality-ok: credential registry is the direct dependency under test + litellm, + "credential_list", + [ + CredentialItem( + credential_name="account-a", + credential_values={ + "api_base": "https://account-a.example.com", + "api_key": "credential-key-a", + }, + credential_info={}, + ) + ], + ) + ): + boundary = EncryptedContentAffinityCheck._encryption_boundary_key({"litellm_credential_name": "account-a"}) + + assert boundary == ("https://account-a.example.com", "credential-key-a") + + +def test_boundary_key_prefers_explicit_values_over_named_credential(): + from litellm.router_utils.pre_call_checks.encrypted_content_affinity_check import ( + EncryptedContentAffinityCheck, + ) + + with ( + patch.object( # test-quality-ok: credential registry is the direct dependency under test + litellm, + "credential_list", + [ + CredentialItem( + credential_name="account-a", + credential_values={ + "api_base": "https://credential.example.com", + "api_key": "credential-key-a", + }, + credential_info={}, + ) + ], + ) + ): + boundary = EncryptedContentAffinityCheck._encryption_boundary_key( + { + "api_base": "https://deployment.example.com", + "litellm_credential_name": "account-a", + } + ) + + assert boundary == ("https://deployment.example.com", "credential-key-a") + + +def test_boundary_fallback_matches_deployments_with_same_named_credential_values(): + from litellm.router_utils.pre_call_checks.encrypted_content_affinity_check import ( + EncryptedContentAffinityCheck, + ) + + with ( + patch.object( # test-quality-ok: credential registry is the direct dependency under test + litellm, + "credential_list", + [ + CredentialItem( + credential_name="account-a", + credential_values={ + "api_base": "https://account-a.example.com", + "api_key": "credential-key-a", + }, + credential_info={}, + ), + CredentialItem( + credential_name="account-a-peer", + credential_values={ + "api_base": "https://account-a.example.com", + "api_key": "credential-key-a", + }, + credential_info={}, + ), + CredentialItem( + credential_name="account-b", + credential_values={ + "api_base": "https://account-b.example.com", + "api_key": "credential-key-b", + }, + credential_info={}, + ), + ], + ) + ): + router = litellm.Router( + model_list=[ + { + "model_name": "gpt-5.3-codex", + "litellm_params": { + "model": "azure/gpt-5.3-codex", + "litellm_credential_name": "account-a", + }, + "model_info": {"id": "origin"}, + } + ], + num_retries=0, + ) + check = EncryptedContentAffinityCheck(router=router) + healthy_deployments = [ + { + "model_info": {"id": "peer-same-boundary"}, + "litellm_params": { + "model": "azure/gpt-5.4", + "litellm_credential_name": "account-a-peer", + }, + }, + { + "model_info": {"id": "peer-different-boundary"}, + "litellm_params": { + "model": "azure/gpt-5.4", + "litellm_credential_name": "account-b", + }, + }, + ] + + matches, originating = check._find_deployments_on_same_encryption_boundary( + healthy_deployments=healthy_deployments, + model_id="origin", + ) + + assert originating is not None + assert [deployment["model_info"]["id"] for deployment in matches] == ["peer-same-boundary"] + + def test_boundary_key_rejects_non_dict_like_inputs(): """ Inputs that don't expose ``.get()`` (None, lists, strings, ints) -> None. From 2cd28b97f1793ef6032526a0119cb892dc3e9b66 Mon Sep 17 00:00:00 2001 From: AaronHowell <111272993+AaronHowell@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:02:31 +0800 Subject: [PATCH 2/2] fix(responses): align credential boundary resolution --- .../encrypted_content_affinity_check.py | 26 +++++-------- .../test_encrypted_content_affinity_check.py | 37 ++++++++++++++++++- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py b/litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py index eba2f36b675..db3c865e183 100644 --- a/litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py +++ b/litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py @@ -165,11 +165,9 @@ class EncryptedContentAffinityCheck(CustomLogger): litellm_params: object, ) -> tuple[object, object] | None: """ - ``(api_base, api_key)`` pair identifying an Azure resource. Two - deployments sharing both are interchangeable for ``encrypted_content`` - follow-ups; Azure rejects content produced by any other resource. - Missing values are resolved from ``litellm_credential_name`` without - modifying the deployment, and explicit deployment values take precedence. + ``(api_base, api_key)`` identifies an upstream encryption boundary. + The values are resolved from the deployment and its named credential + without modifying the deployment. Accepts any object exposing dict-style ``.get(key, default)``: plain dicts (the common case in ``healthy_deployments``) as well as @@ -187,22 +185,18 @@ class EncryptedContentAffinityCheck(CustomLogger): credential_name: Final = getter("litellm_credential_name") credential_values: Final[Mapping[str, object] | None] = ( CredentialAccessor.get_credential_values(credential_name) - if isinstance(credential_name, str) and credential_name and (api_base is None or api_key is None) + if isinstance(credential_name, str) and credential_name else None ) effective_api_base: Final = ( - api_base - if api_base is not None - else credential_values.get("api_base") - if credential_values is not None - else None + credential_values.get("api_base") + if credential_values is not None and "api_base" in credential_values + else api_base ) effective_api_key: Final = ( - api_key - if api_key is not None - else credential_values.get("api_key") - if credential_values is not None - else None + credential_values.get("api_key") + if credential_values is not None and "api_key" in credential_values + else api_key ) if not effective_api_base or not effective_api_key: return None diff --git a/tests/test_litellm/router_utils/pre_call_checks/test_encrypted_content_affinity_check.py b/tests/test_litellm/router_utils/pre_call_checks/test_encrypted_content_affinity_check.py index 46e67983c33..7b7a4969d41 100644 --- a/tests/test_litellm/router_utils/pre_call_checks/test_encrypted_content_affinity_check.py +++ b/tests/test_litellm/router_utils/pre_call_checks/test_encrypted_content_affinity_check.py @@ -1174,7 +1174,7 @@ def test_boundary_key_resolves_missing_values_from_named_credential(): assert boundary == ("https://account-a.example.com", "credential-key-a") -def test_boundary_key_prefers_explicit_values_over_named_credential(): +def test_boundary_key_matches_named_credential_precedence(): from litellm.router_utils.pre_call_checks.encrypted_content_affinity_check import ( EncryptedContentAffinityCheck, ) @@ -1198,11 +1198,44 @@ def test_boundary_key_prefers_explicit_values_over_named_credential(): boundary = EncryptedContentAffinityCheck._encryption_boundary_key( { "api_base": "https://deployment.example.com", + "api_key": "deployment-key", "litellm_credential_name": "account-a", } ) - assert boundary == ("https://deployment.example.com", "credential-key-a") + assert boundary == ("https://credential.example.com", "credential-key-a") + + +def test_boundary_key_resolves_credential_when_explicit_values_are_empty(): + from litellm.router_utils.pre_call_checks.encrypted_content_affinity_check import ( + EncryptedContentAffinityCheck, + ) + + with ( + patch.object( # test-quality-ok: credential registry is the direct dependency under test + litellm, + "credential_list", + [ + CredentialItem( + credential_name="account-a", + credential_values={ + "api_base": "https://credential.example.com", + "api_key": "credential-key-a", + }, + credential_info={}, + ) + ], + ) + ): + boundary = EncryptedContentAffinityCheck._encryption_boundary_key( + { + "api_base": "", + "api_key": "", + "litellm_credential_name": "account-a", + } + ) + + assert boundary == ("https://credential.example.com", "credential-key-a") def test_boundary_fallback_matches_deployments_with_same_named_credential_values():