From 78f53a1a5e65226939d38cb41cf69b58bcd2f8f0 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:02:13 +0000 Subject: [PATCH 1/3] fix(passthrough): skip team deployments when picking proxy-wide pass-through credentials on lts/1.108.x --- .../passthrough_endpoint_router.py | 10 +++++- .../test_passthrough_endpoint_router.py | 35 +++++++++++++++++++ .../test_watsonx_proxy_route.py | 22 ++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/pass_through_endpoints/passthrough_endpoint_router.py b/litellm/proxy/pass_through_endpoints/passthrough_endpoint_router.py index 018c1ce524c..39f1a1d2a71 100644 --- a/litellm/proxy/pass_through_endpoints/passthrough_endpoint_router.py +++ b/litellm/proxy/pass_through_endpoints/passthrough_endpoint_router.py @@ -1,5 +1,5 @@ import json -from collections.abc import Callable +from collections.abc import Callable, Mapping from dataclasses import dataclass from typing import TYPE_CHECKING, Final @@ -11,6 +11,7 @@ from litellm.integrations.vector_store_integrations.vector_store_pre_call_hook i LiteLLM_ManagedVectorStore, ) from litellm.litellm_core_utils.credential_accessor import CredentialAccessor +from litellm.llms.azure.ptu_capacity import is_str_keyed_mapping from litellm.secret_managers.main import get_secret_str from litellm.types.llms.vertex_ai import VERTEX_CREDENTIALS_TYPES from litellm.types.passthrough_endpoints.vertex_ai import VertexPassThroughCredentials @@ -26,6 +27,11 @@ def _get_proxy_llm_router() -> "Router | None": return llm_router +def _is_team_deployment(deployment: Mapping[str, object]) -> bool: + model_info: Final = deployment.get("model_info") + return is_str_keyed_mapping(model_info) and bool(model_info.get("team_id")) + + def _get_str_value(values: dict[str, object] | None, key: str) -> str | None: value: Final = values.get(key) if values is not None else None return value if isinstance(value, str) else None @@ -101,6 +107,8 @@ class PassthroughEndpointRouter: if llm_router is None: return None for deployment in llm_router.get_model_list() or (): + if _is_team_deployment(deployment): + continue litellm_params: LiteLLMParamsTypedDict = deployment["litellm_params"] api_key: str | None = self._resolve_matching_deployment_api_key( litellm_params=litellm_params, diff --git a/tests/unit/proxy/pass_through_endpoints/test_passthrough_endpoint_router.py b/tests/unit/proxy/pass_through_endpoints/test_passthrough_endpoint_router.py index 941accff314..de1350ab201 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_passthrough_endpoint_router.py +++ b/tests/unit/proxy/pass_through_endpoints/test_passthrough_endpoint_router.py @@ -60,6 +60,41 @@ def test_credential_loaded_after_deployment_registration_still_resolves(): ) +def _team_deployment(model: str, team_id: str, **litellm_params) -> dict: + return {**_flagged_deployment(model, **litellm_params), "model_info": {"team_id": team_id}} + + +def test_proxy_wide_deployment_credentials_skip_a_team_deployment_for_the_proxy_wide_one(): + llm_router = litellm.Router( + model_list=[ + _team_deployment( + "watsonx/ibm/granite-3-8b-instruct", + "team-a", + api_key="wx-team-a-key", + api_base="https://team-a.ml.cloud.ibm.com", + ), + _flagged_deployment( + "watsonx/ibm/granite-3-8b-instruct", + api_key="wx-proxy-wide-key", + api_base="https://eu-de.ml.cloud.ibm.com", + ), + ] + ) + + credentials = _passthrough_router(llm_router).get_proxy_wide_deployment_credentials(custom_llm_provider="watsonx") + + assert credentials is not None + assert (credentials.api_key, credentials.api_base) == ("wx-proxy-wide-key", "https://eu-de.ml.cloud.ibm.com") + + +def test_proxy_wide_deployment_credentials_are_none_when_only_a_team_deployment_is_flagged(): + llm_router = litellm.Router( + model_list=[_team_deployment("watsonx/ibm/granite-3-8b-instruct", "team-a", api_key="wx-team-a-key")] + ) + + assert _passthrough_router(llm_router).get_proxy_wide_deployment_credentials(custom_llm_provider="watsonx") is None + + def test_credential_rotation_is_reflected_without_deployment_update(): CredentialAccessor.upsert_credentials([_credential("cred_openai", "sk-before-rotation")]) llm_router = litellm.Router( diff --git a/tests/unit/proxy/pass_through_endpoints/test_watsonx_proxy_route.py b/tests/unit/proxy/pass_through_endpoints/test_watsonx_proxy_route.py index 8efd8a760f5..8b41f434201 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_watsonx_proxy_route.py +++ b/tests/unit/proxy/pass_through_endpoints/test_watsonx_proxy_route.py @@ -554,3 +554,25 @@ async def test_watsonx_route_falls_back_to_env_when_no_deployment_is_flagged(mon assert target == "https://us-south.ml.cloud.ibm.com/ml/v1/text/generation" assert authorization == "Bearer iam-token-1" assert iam_apikey == "wx-env-key" + + +@pytest.mark.asyncio +async def test_watsonx_route_falls_back_to_env_when_the_only_flagged_deployment_belongs_to_a_team(monkeypatch): + llm_router = litellm.Router( + model_list=[ + { + **_watsonx_deployment("wx-team-a-key", "https://team-a.ml.cloud.ibm.com", use_in_pass_through=True), + "model_info": {"team_id": "team-a"}, + } + ] + ) + + target, authorization, iam_apikey = await _route_target_and_iam_apikey( + monkeypatch, + llm_router, + env={"WX_API_KEY": "wx-env-key", "WATSONX_API_BASE": "https://us-south.ml.cloud.ibm.com"}, + ) + + assert target == "https://us-south.ml.cloud.ibm.com/ml/v1/text/generation" + assert authorization == "Bearer iam-token-1" + assert iam_apikey == "wx-env-key" From 689494ad5fa5d40e4f79230101d9f174794f6d1e Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:09:04 +0000 Subject: [PATCH 2/3] test(passthrough): type the team deployment helper and mark test locals Final --- .../test_passthrough_endpoint_router.py | 15 +++++++++------ .../test_watsonx_proxy_route.py | 7 +++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/unit/proxy/pass_through_endpoints/test_passthrough_endpoint_router.py b/tests/unit/proxy/pass_through_endpoints/test_passthrough_endpoint_router.py index de1350ab201..deb02b35e8b 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_passthrough_endpoint_router.py +++ b/tests/unit/proxy/pass_through_endpoints/test_passthrough_endpoint_router.py @@ -1,6 +1,7 @@ import asyncio import os import unittest +from typing import Final from unittest.mock import patch import pytest @@ -60,12 +61,12 @@ def test_credential_loaded_after_deployment_registration_still_resolves(): ) -def _team_deployment(model: str, team_id: str, **litellm_params) -> dict: +def _team_deployment(model: str, team_id: str, **litellm_params: str) -> dict[str, object]: return {**_flagged_deployment(model, **litellm_params), "model_info": {"team_id": team_id}} -def test_proxy_wide_deployment_credentials_skip_a_team_deployment_for_the_proxy_wide_one(): - llm_router = litellm.Router( +def test_proxy_wide_deployment_credentials_skip_a_team_deployment_for_the_proxy_wide_one() -> None: + llm_router: Final = litellm.Router( model_list=[ _team_deployment( "watsonx/ibm/granite-3-8b-instruct", @@ -81,14 +82,16 @@ def test_proxy_wide_deployment_credentials_skip_a_team_deployment_for_the_proxy_ ] ) - credentials = _passthrough_router(llm_router).get_proxy_wide_deployment_credentials(custom_llm_provider="watsonx") + credentials: Final = _passthrough_router(llm_router).get_proxy_wide_deployment_credentials( + custom_llm_provider="watsonx" + ) assert credentials is not None assert (credentials.api_key, credentials.api_base) == ("wx-proxy-wide-key", "https://eu-de.ml.cloud.ibm.com") -def test_proxy_wide_deployment_credentials_are_none_when_only_a_team_deployment_is_flagged(): - llm_router = litellm.Router( +def test_proxy_wide_deployment_credentials_are_none_when_only_a_team_deployment_is_flagged() -> None: + llm_router: Final = litellm.Router( model_list=[_team_deployment("watsonx/ibm/granite-3-8b-instruct", "team-a", api_key="wx-team-a-key")] ) diff --git a/tests/unit/proxy/pass_through_endpoints/test_watsonx_proxy_route.py b/tests/unit/proxy/pass_through_endpoints/test_watsonx_proxy_route.py index 8b41f434201..e19612da1ee 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_watsonx_proxy_route.py +++ b/tests/unit/proxy/pass_through_endpoints/test_watsonx_proxy_route.py @@ -6,6 +6,7 @@ and version parameter injection. """ import json +from typing import Final from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -557,8 +558,10 @@ async def test_watsonx_route_falls_back_to_env_when_no_deployment_is_flagged(mon @pytest.mark.asyncio -async def test_watsonx_route_falls_back_to_env_when_the_only_flagged_deployment_belongs_to_a_team(monkeypatch): - llm_router = litellm.Router( +async def test_watsonx_route_falls_back_to_env_when_the_only_flagged_deployment_belongs_to_a_team( + monkeypatch: pytest.MonkeyPatch, +) -> None: + llm_router: Final = litellm.Router( model_list=[ { **_watsonx_deployment("wx-team-a-key", "https://team-a.ml.cloud.ibm.com", use_in_pass_through=True), From 798025d2cd57eece3c585c2efc3d3861c5d197b2 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:20:53 +0000 Subject: [PATCH 3/3] chore: nudge the pull request head after a dropped sync Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>