fix(vertex): match standalone 403 status tokens instead of any substring (#47134)

* fix(vertex): match standalone 403 status tokens

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(vertex): keep text 403 fallback for wrapped 500 errors

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(vertex): keep BadRequestError for explicit 403 text matches

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(vertex): let structured body codes override the 403 text fallback

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-10-17 19:17:18 +00:00 • committed by GitHub
parent 4b635f18e4
commit 84d91bbe87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 138 additions and 3 deletions

View file

@ -1196,6 +1196,9 @@ def _map_sagemaker_exception(
)
_VERTEX_403_STATUS_CODE_PATTERN: Final = re.compile(r"(?<![0-9A-Za-z])403(?![0-9A-Za-z])")
def _map_vertex_exception(
*,
model: str,
@ -1206,6 +1209,8 @@ def _map_vertex_exception(
exception_provider: str,
extra_information: str,
) -> None:
status_code: Final[object] = getattr(original_exception, "status_code", None)
body_code: Final[int | None] = _get_body_error_code(error_str)
if "Vertex AI API has not been used in project" in error_str or "Unable to find your project" in error_str:
raise BadRequestError(
message=f"litellm.BadRequestError: {custom_llm_provider}Exception - {error_str}",
@ -1252,7 +1257,11 @@ def _map_vertex_exception(
llm_provider=custom_llm_provider,
litellm_debug_info=extra_information,
)
elif "403" in error_str:
elif (
(not isinstance(status_code, int) or status_code in (403, 500))
and body_code in (None, 403)
and _VERTEX_403_STATUS_CODE_PATTERN.search(error_str)
):
raise BadRequestError(
message=f"{custom_llm_provider.capitalize()}Exception BadRequestError - {error_str}",
model=model,
@ -1306,7 +1315,7 @@ def _map_vertex_exception(
elif (
isinstance(getattr(original_exception, "status_code", None), int)
and 500 <= original_exception.status_code < 600
and _get_body_error_code(error_str) == 429
and body_code == 429
):
# upstream gateway wraps a 429 inside a 5xx envelope
# e.g. HTTP 500/503 with {"error":{"code":429,...}}.

View file

@ -388,7 +388,7 @@ def test_adapter_route_upstream_error_reaches_the_caller_with_its_status(
gateway: Gateway, status: int, stream: bool
) -> None:
route: Final = _ROUTES["model_garden_publisher"]
message: Final = f"scripted vertex rejection {uuid.uuid4().hex}"
message: Final = f"scripted vertex rejection {uuid.uuid4().hex}f4039"
with wire_server(_vertex_peer(status=status, error=message)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(gateway, scenario, wire, route, num_retries=0)
key: Final = scenario.key(models=[model])

View file

@ -20,6 +20,7 @@ from litellm.litellm_core_utils.exception_mapping_utils import (
exception_type,
extract_and_raise_litellm_exception,
)
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.bedrock.common_utils import BedrockError
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.llms.openai.common_utils import OpenAIError
@ -578,6 +579,131 @@ def test_vertex_ai_rate_limit_error_mapping(error_message, should_raise_rate_lim
)
def test_vertex_status_code_429_ignores_403_token_in_message() -> None:
original_exception: Final = BaseLLMException(status_code=429, message="scripted vertex rejection f4039")
with pytest.raises(litellm.RateLimitError) as excinfo:
exception_type(
model="gemini/gemini-2.5-flash",
original_exception=original_exception,
custom_llm_provider="vertex_ai",
)
assert excinfo.value.status_code == 429
def test_vertex_status_code_403_maps_to_403() -> None:
original_exception: Final = BaseLLMException(status_code=403, message="permission denied")
with pytest.raises(litellm.PermissionDeniedError) as excinfo:
exception_type(
model="gemini/gemini-2.5-flash",
original_exception=original_exception,
custom_llm_provider="vertex_ai",
)
assert excinfo.value.status_code == 403
def test_vertex_status_code_403_with_text_token_maps_to_bad_request() -> None:
original_exception: Final = BaseLLMException(
status_code=403,
message='{"error":{"code":403,"message":"Permission denied"}}',
)
with pytest.raises(litellm.BadRequestError) as excinfo:
exception_type(
model="gemini/gemini-2.5-flash",
original_exception=original_exception,
custom_llm_provider="vertex_ai",
)
assert excinfo.value.status_code == 403
def test_vertex_text_403_without_status_code_still_maps_to_bad_request() -> None:
with pytest.raises(litellm.BadRequestError) as excinfo:
exception_type(
model="gemini/gemini-2.5-flash",
original_exception=Exception("403 Forbidden: permission denied"),
custom_llm_provider="vertex_ai",
)
assert excinfo.value.status_code == 403
def test_vertex_status_substring_without_status_code_does_not_map_to_403() -> None:
with pytest.raises(litellm.APIConnectionError) as excinfo:
exception_type(
model="gemini/gemini-2.5-flash",
original_exception=Exception("scripted vertex rejection f4039a"),
custom_llm_provider="vertex_ai",
)
assert excinfo.value.status_code != 403
def test_vertex_wrapped_500_with_403_body_maps_to_bad_request() -> None:
original_exception: Final = BaseLLMException(
status_code=500,
message='Error code: 403 - {"error": {"code": 403, "message": "Permission denied"}}',
)
with pytest.raises(litellm.BadRequestError) as excinfo:
exception_type(
model="gemini/gemini-2.5-flash",
original_exception=original_exception,
custom_llm_provider="vertex_ai",
)
assert excinfo.value.status_code == 403
def test_vertex_wrapped_500_with_embedded_403_text_does_not_map_to_403() -> None:
original_exception: Final = BaseLLMException(status_code=500, message="scripted vertex rejection f4039")
with pytest.raises(litellm.InternalServerError) as excinfo:
exception_type(
model="gemini/gemini-2.5-flash",
original_exception=original_exception,
custom_llm_provider="vertex_ai",
)
assert excinfo.value.status_code == 500
def test_vertex_wrapped_500_with_body_code_429_and_403_token_maps_to_rate_limit() -> None:
original_exception: Final = BaseLLMException(
status_code=500,
message='{"error":{"code":429,"message":"upstream unavailable (request id: 403-xyz)"}}',
)
with pytest.raises(litellm.RateLimitError) as excinfo:
exception_type(
model="gemini/gemini-2.5-flash",
original_exception=original_exception,
custom_llm_provider="vertex_ai",
)
assert excinfo.value.status_code == 429
def test_vertex_wrapped_500_with_body_code_403_maps_to_bad_request() -> None:
original_exception: Final = BaseLLMException(
status_code=500,
message='{"error":{"code":403,"message":"Permission denied"}}',
)
with pytest.raises(litellm.BadRequestError) as excinfo:
exception_type(
model="gemini/gemini-2.5-flash",
original_exception=original_exception,
custom_llm_provider="vertex_ai",
)
assert excinfo.value.status_code == 403
class TestGetBodyErrorCode:
"""Unit tests for _get_body_error_code helper."""