mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
fix(router): accept Pydantic LiteLLM_Params in encryption-boundary key lookup
Greptile flagged that the strict isinstance(dict) guard in
_encryption_boundary_key would silently return None for any non-dict input,
including a LiteLLM_Params Pydantic instance, which exposes a custom .get()
method and is intended to be used dict-style in some router paths. If such
an instance ever flowed into healthy_deployments, the guard would drop every
candidate from boundary matching and fall through to the full deployment
pool, i.e. trigger the exact invalid_encrypted_content failure this check
exists to prevent.
Loosen the guard to accept any object exposing a callable .get(): plain
dicts (the common case) and LiteLLM_Params-style Pydantic instances. The
function still returns None for non-dict-like values (None, lists, strings,
ints, bare objects).
Adds regression tests covering:
- LiteLLM_Params Pydantic instance resolves to the same boundary tuple as
an equivalent plain dict
- non-dict-like values and dicts missing required fields still return None
This commit is contained in:
parent
f3b8aad883
commit
40db114a23
2 changed files with 103 additions and 18 deletions
|
|
@ -125,17 +125,26 @@ class EncryptedContentAffinityCheck(CustomLogger):
|
|||
|
||||
@staticmethod
|
||||
def _encryption_boundary_key(
|
||||
litellm_params: dict,
|
||||
litellm_params: Any,
|
||||
) -> Optional[tuple]:
|
||||
"""
|
||||
``(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.
|
||||
|
||||
Accepts any object exposing dict-style ``.get(key, default)``: plain
|
||||
dicts (the common case in ``healthy_deployments``) as well as
|
||||
``LiteLLM_Params``-style Pydantic instances, which define a custom
|
||||
``.get()``. A stricter ``isinstance(dict)`` guard would silently drop
|
||||
the latter from boundary matching and fall back to the full pool —
|
||||
i.e. trigger the exact ``invalid_encrypted_content`` failure this
|
||||
check exists to prevent.
|
||||
"""
|
||||
if not isinstance(litellm_params, dict):
|
||||
getter = getattr(litellm_params, "get", None)
|
||||
if not callable(getter):
|
||||
return None
|
||||
api_base = litellm_params.get("api_base")
|
||||
api_key = litellm_params.get("api_key")
|
||||
api_base = getter("api_base")
|
||||
api_key = getter("api_key")
|
||||
if not api_base or not api_key:
|
||||
return None
|
||||
return (api_base, api_key)
|
||||
|
|
|
|||
|
|
@ -888,13 +888,16 @@ async def test_affinity_falls_back_to_same_encryption_boundary_on_model_group_sw
|
|||
return d
|
||||
return seq[0]
|
||||
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.llm_http_handler.BaseLLMHTTPHandler.async_response_api_handler",
|
||||
new_callable=AsyncMock,
|
||||
return_value=first_resp,
|
||||
), patch(
|
||||
"litellm.router_strategy.simple_shuffle.random.choice",
|
||||
side_effect=first_call_picks_account_a,
|
||||
with (
|
||||
patch(
|
||||
"litellm.llms.custom_httpx.llm_http_handler.BaseLLMHTTPHandler.async_response_api_handler",
|
||||
new_callable=AsyncMock,
|
||||
return_value=first_resp,
|
||||
),
|
||||
patch(
|
||||
"litellm.router_strategy.simple_shuffle.random.choice",
|
||||
side_effect=first_call_picks_account_a,
|
||||
),
|
||||
):
|
||||
r1 = await router.aresponses(model="gpt-5.3-codex", input="hi")
|
||||
|
||||
|
|
@ -1013,13 +1016,16 @@ async def test_affinity_falls_back_to_same_boundary_on_alias_switch():
|
|||
return d
|
||||
return seq[0]
|
||||
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.llm_http_handler.BaseLLMHTTPHandler.async_response_api_handler",
|
||||
new_callable=AsyncMock,
|
||||
return_value=first_resp,
|
||||
), patch(
|
||||
"litellm.router_strategy.simple_shuffle.random.choice",
|
||||
side_effect=pick_account_a,
|
||||
with (
|
||||
patch(
|
||||
"litellm.llms.custom_httpx.llm_http_handler.BaseLLMHTTPHandler.async_response_api_handler",
|
||||
new_callable=AsyncMock,
|
||||
return_value=first_resp,
|
||||
),
|
||||
patch(
|
||||
"litellm.router_strategy.simple_shuffle.random.choice",
|
||||
side_effect=pick_account_a,
|
||||
),
|
||||
):
|
||||
r1 = await router.aresponses(model="gpt-5.3-codex", input="hi")
|
||||
|
||||
|
|
@ -1096,3 +1102,73 @@ def test_boundary_fallback_originating_deployment_removed_returns_empty():
|
|||
)
|
||||
assert matches == []
|
||||
mock_router.get_deployment.assert_called_once_with(model_id="dep-removed")
|
||||
|
||||
|
||||
def test_boundary_key_accepts_pydantic_litellm_params_instance():
|
||||
"""
|
||||
Regression: ``_encryption_boundary_key`` must accept any object exposing
|
||||
dict-style ``.get()`` (incl. ``LiteLLM_Params`` Pydantic instances) — not
|
||||
just plain dicts.
|
||||
|
||||
A stricter ``isinstance(dict)`` guard would silently return ``None`` for a
|
||||
``LiteLLM_Params`` value, drop the deployment from boundary matching, and
|
||||
fall back to the full pool — which is the exact ``invalid_encrypted_content``
|
||||
failure this check exists to prevent.
|
||||
"""
|
||||
from litellm.router_utils.pre_call_checks.encrypted_content_affinity_check import (
|
||||
EncryptedContentAffinityCheck,
|
||||
)
|
||||
from litellm.types.router import LiteLLM_Params
|
||||
|
||||
pydantic_params = LiteLLM_Params(
|
||||
model="azure/gpt-5.3-codex",
|
||||
api_base="https://mateo-resource.openai.azure.com",
|
||||
api_key="fake-azure-resource-key-a",
|
||||
)
|
||||
plain_params = {
|
||||
"model": "azure/gpt-5.3-codex",
|
||||
"api_base": "https://mateo-resource.openai.azure.com",
|
||||
"api_key": "fake-azure-resource-key-a",
|
||||
}
|
||||
|
||||
pydantic_key = EncryptedContentAffinityCheck._encryption_boundary_key(
|
||||
pydantic_params
|
||||
)
|
||||
plain_key = EncryptedContentAffinityCheck._encryption_boundary_key(plain_params)
|
||||
|
||||
assert pydantic_key is not None
|
||||
assert (
|
||||
pydantic_key
|
||||
== plain_key
|
||||
== (
|
||||
"https://mateo-resource.openai.azure.com",
|
||||
"fake-azure-resource-key-a",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_boundary_key_rejects_non_dict_like_inputs():
|
||||
"""
|
||||
Inputs that don't expose ``.get()`` (None, lists, strings, ints) -> None.
|
||||
Guards against accidentally treating a stray non-dict-like value as a
|
||||
valid boundary.
|
||||
"""
|
||||
from litellm.router_utils.pre_call_checks.encrypted_content_affinity_check import (
|
||||
EncryptedContentAffinityCheck,
|
||||
)
|
||||
|
||||
for bad in (None, [], "not a dict", 42, object()):
|
||||
assert EncryptedContentAffinityCheck._encryption_boundary_key(bad) is None
|
||||
|
||||
assert (
|
||||
EncryptedContentAffinityCheck._encryption_boundary_key(
|
||||
{"api_base": "", "api_key": "k"}
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
EncryptedContentAffinityCheck._encryption_boundary_key(
|
||||
{"api_base": "https://x"}
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue