From 19c0adde5482231c666da5a329cb1a601225d763 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 31 Oct 2026 03:15:20 -0700 Subject: [PATCH] fix(proxy): merge opt-in fallback groups only when their deployments share provider and opt-in list --- litellm/proxy/auth/auth_utils.py | 27 +++++++++++++----------- tests/unit/proxy/auth/test_auth_utils.py | 20 ++++++++++++++++++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index f87400371b3..50b96f6eeb9 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -556,27 +556,30 @@ class _OptInGroups: vertex_target: tuple[str, ...] -def _deployment_id(deployment: Mapping[str, object]) -> str | None: - match deployment.get("model_info"): - case {"id": str(deployment_id)} if deployment_id: - return deployment_id - case _: - return None +_OptInKey = tuple[str | tuple[str, str | None], ...] -def _same_deployments_key(model_group: str, llm_router: Router) -> frozenset[tuple[str | None, str | None]] | str: - deployments: Final = _model_group_deployments(model_group, llm_router) - key: Final = tuple( - (_deployment_id(deployment), _deployment_provider(deployment["litellm_params"])) for deployment in deployments +def _opt_in_key(litellm_params: LiteLLMParamsTypedDict) -> _OptInKey: + return tuple( + item if isinstance(item, str) else ("api_base", item.get("api_base")) + for item in deployment_clientside_auth_params(litellm_params) or () ) - return frozenset(key) if key and all(deployment_id for deployment_id, _ in key) else model_group + + +def _same_answer_key(model_group: str, llm_router: Router) -> frozenset[tuple[str | None, _OptInKey]] | str: + deployments: Final = _model_group_deployments(model_group, llm_router) + key: Final = frozenset( + (_deployment_provider(deployment["litellm_params"]), _opt_in_key(deployment["litellm_params"])) + for deployment in deployments + ) + return key or model_group def _distinct_opt_in_groups(model_groups: frozenset[str], llm_router: Router | None) -> tuple[str, ...]: if llm_router is None: return tuple(sorted(model_groups)) return tuple( - {_same_deployments_key(model_group, llm_router): model_group for model_group in sorted(model_groups)}.values() + {_same_answer_key(model_group, llm_router): model_group for model_group in sorted(model_groups)}.values() ) diff --git a/tests/unit/proxy/auth/test_auth_utils.py b/tests/unit/proxy/auth/test_auth_utils.py index 4db586645f1..7310b820472 100644 --- a/tests/unit/proxy/auth/test_auth_utils.py +++ b/tests/unit/proxy/auth/test_auth_utils.py @@ -5299,6 +5299,26 @@ class TestIsRequestBodySafeBlocksVertexTargetOverrides: with pytest.raises(ValueError, match="vertex_location"): is_request_body_safe(request_body, {}, router, "vertex_ai/gemini-2.5-flash", route="/v1/chat/completions") + def test_a_pinned_group_sharing_a_deployment_id_with_an_opted_in_group_is_still_checked(self) -> None: + router: Final = Router( + model_list=[ + { + "model_name": name, + "litellm_params": { + "model": "vertex_ai/gemini-2.5-flash", + "vertex_location": "europe-west4", + "configurable_clientside_auth_params": opt_in, + }, + "model_info": {"id": "shared-id"}, + } + for name, opt_in in (("a-pinned", []), ("z-optin", ["vertex_location"])) + ] + ) + request_body: Final = {"model": "a-pinned", "vertex_location": "us-central1", "fallbacks": ["z-optin"]} + + with pytest.raises(ValueError, match="vertex_location"): + is_request_body_safe(request_body, {}, router, "a-pinned", route="/v1/chat/completions") + @pytest.mark.parametrize( "model, is_allowed", [("public-alias", True), ("gemini-optin", False)], ids=["alias", "aliased_target"] )