diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 9a3bc82c6fa..dbf587241ac 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Annotated, 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 @@ -1924,23 +1925,37 @@ 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 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}. " + "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 961479c0393..5024ed28b79 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,12 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from google.auth.exceptions import DefaultCredentialsError, TransportError from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import ( _base_vertex_proxy_route, ) -from litellm.types.router import DeploymentTypedDict @pytest.mark.asyncio @@ -348,6 +348,95 @@ 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_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(): """