From ca6f731d800c4ca31bb033c0748298a58c93c732 Mon Sep 17 00:00:00 2001 From: milan Date: Thu, 13 Aug 2026 22:30:26 +0000 Subject: [PATCH 1/2] fix(vertex passthrough): return an actionable auth error when Google token minting fails Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../llm_passthrough_endpoints.py | 44 ++++++++++------ .../test_vertex_passthrough_load_balancing.py | 50 +++++++++++++++++++ 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index f84cdd0c222..69126b5fd6f 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -1598,23 +1598,35 @@ async def _prepare_vertex_auth_headers( else: raise ValueError("No vertex credentials found") - _auth_header, vertex_project = await vertex_llm_base._ensure_access_token_async( - credentials=vertex_credentials_str, - project_id=vertex_project, - custom_llm_provider="vertex_ai_beta", - ) + try: + _auth_header, vertex_project = await vertex_llm_base._ensure_access_token_async( + credentials=vertex_credentials_str, + project_id=vertex_project, + custom_llm_provider="vertex_ai_beta", + ) - auth_header, _ = vertex_llm_base._get_token_and_url( - model="", - auth_header=_auth_header, - gemini_api_key=None, - vertex_credentials=vertex_credentials_str, - vertex_project=vertex_project, - vertex_location=vertex_location, - stream=False, - custom_llm_provider="vertex_ai_beta", - api_base="", - ) + auth_header, _ = vertex_llm_base._get_token_and_url( + model="", + auth_header=_auth_header, + gemini_api_key=None, + vertex_credentials=vertex_credentials_str, + vertex_project=vertex_project, + vertex_location=vertex_location, + stream=False, + custom_llm_provider="vertex_ai_beta", + api_base="", + ) + except Exception as e: + raise ProxyException( + message=( + f"Failed to get a Google access token for project={vertex_project} + location={vertex_location}: {e}. " + "Set `vertex_credentials` on the deployment marked `use_in_pass_through: true`, or on " + "`default_vertex_config` / `DEFAULT_GOOGLE_APPLICATION_CREDENTIALS`." + ), + type=ProxyErrorTypes.auth_error, + param="vertex_credentials", + code=401, + ) from e # Use allowlist approach - only forward specific safe headers headers = get_vertex_ai_allowed_incoming_headers(request) diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py b/tests/test_litellm/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py index aaf1dad4910..8e16fcf66f6 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py @@ -1,6 +1,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from google.auth.exceptions import DefaultCredentialsError from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import ( _base_vertex_proxy_route, @@ -346,6 +347,55 @@ async def test_vertex_passthrough_forwards_anthropic_beta_header(): assert headers_passed_through is False +@pytest.mark.asyncio +async def test_vertex_passthrough_credential_failure_raises_auth_error(): + """ + A project/location configured for pass-through without usable Google credentials used to + bubble google.auth errors out of the route, so callers only saw an opaque 500 + {"error": {"message": "Internal server error"}} with no hint about what to configure. + """ + from starlette.datastructures import Headers + + from litellm.llms.vertex_ai.vertex_llm_base import VertexBase + from litellm.proxy._types import ProxyErrorTypes, ProxyException + from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import ( + _prepare_vertex_auth_headers, + ) + + mock_request = MagicMock() + mock_request.headers = Headers({"authorization": "Bearer sk-litellm-key"}) + mock_request.state._cached_headers = None + + mock_vertex_credentials = MagicMock() + mock_vertex_credentials.vertex_project = "test-project" + mock_vertex_credentials.vertex_location = "us-central1" + mock_vertex_credentials.vertex_credentials = None + + with patch.object( + VertexBase, + "_ensure_access_token_async", + new_callable=AsyncMock, + side_effect=DefaultCredentialsError("Your default credentials were not found"), + ): + with pytest.raises(ProxyException) as exc_info: + await _prepare_vertex_auth_headers( + request=mock_request, + vertex_credentials=mock_vertex_credentials, + router_credentials=None, + vertex_project="test-project", + vertex_location="us-central1", + base_target_url="https://us-central1-aiplatform.googleapis.com", + get_vertex_pass_through_handler=MagicMock(), + ) + + assert exc_info.value.code == "401" + assert exc_info.value.type == ProxyErrorTypes.auth_error.value + assert "test-project" in exc_info.value.message + assert "us-central1" in exc_info.value.message + assert "vertex_credentials" in exc_info.value.message + assert "Your default credentials were not found" in exc_info.value.message + + @pytest.mark.asyncio async def test_vertex_passthrough_does_not_forward_litellm_auth_token(): """ From 8a23564d6e5ab1fda298bc661b05ab9b733f0a0e Mon Sep 17 00:00:00 2001 From: milan Date: Thu, 13 Aug 2026 22:42:57 +0000 Subject: [PATCH 2/2] fix(vertex passthrough): keep transport failures out of the auth error mapping Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../llm_passthrough_endpoints.py | 5 ++- .../test_vertex_passthrough_load_balancing.py | 43 ++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 69126b5fd6f..061e60a2e32 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -14,6 +14,7 @@ from typing import Any, Final, cast import httpx from fastapi import APIRouter, Depends, HTTPException, Request, Response, WebSocket from fastapi.responses import StreamingResponse +from google.auth.exceptions import GoogleAuthError, TransportError from starlette.websockets import WebSocketState import litellm @@ -1616,7 +1617,9 @@ async def _prepare_vertex_auth_headers( custom_llm_provider="vertex_ai_beta", api_base="", ) - except Exception as e: + except TransportError: + raise + except (GoogleAuthError, ValueError) as e: raise ProxyException( message=( f"Failed to get a Google access token for project={vertex_project} + location={vertex_location}: {e}. " diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py b/tests/test_litellm/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py index 8e16fcf66f6..8b3c6bfd292 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py @@ -1,12 +1,11 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from google.auth.exceptions import DefaultCredentialsError +from google.auth.exceptions import DefaultCredentialsError, TransportError from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import ( _base_vertex_proxy_route, ) -from litellm.types.router import DeploymentTypedDict @pytest.mark.asyncio @@ -396,6 +395,46 @@ async def test_vertex_passthrough_credential_failure_raises_auth_error(): assert "Your default credentials were not found" in exc_info.value.message +@pytest.mark.asyncio +async def test_vertex_passthrough_transport_failure_is_not_reported_as_auth_error(): + """ + Reaching Google's token endpoint can fail for reasons the operator cannot fix with + credentials, so those must not be relabelled as authentication errors. + """ + from starlette.datastructures import Headers + + from litellm.llms.vertex_ai.vertex_llm_base import VertexBase + from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import ( + _prepare_vertex_auth_headers, + ) + + mock_request = MagicMock() + mock_request.headers = Headers({"authorization": "Bearer sk-litellm-key"}) + mock_request.state._cached_headers = None + + mock_vertex_credentials = MagicMock() + mock_vertex_credentials.vertex_project = "test-project" + mock_vertex_credentials.vertex_location = "us-central1" + mock_vertex_credentials.vertex_credentials = None + + with patch.object( + VertexBase, + "_ensure_access_token_async", + new_callable=AsyncMock, + side_effect=TransportError("connection reset by peer"), + ): + with pytest.raises(TransportError): + await _prepare_vertex_auth_headers( + request=mock_request, + vertex_credentials=mock_vertex_credentials, + router_credentials=None, + vertex_project="test-project", + vertex_location="us-central1", + base_target_url="https://us-central1-aiplatform.googleapis.com", + get_vertex_pass_through_handler=MagicMock(), + ) + + @pytest.mark.asyncio async def test_vertex_passthrough_does_not_forward_litellm_auth_token(): """