Skip to content

Commit 237ad99

Browse files
authored
Merge pull request #2459 from alexxa/rspeed-3209-vertex-429-workaround
RSPEED-3209: Detect Vertex AI RESOURCE_EXHAUSTED wrapped as 500 by llama-stack
2 parents fdf06f2 + c2f39c7 commit 237ad99

4 files changed

Lines changed: 80 additions & 0 deletions

File tree

src/utils/agents/error_handler.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from utils.query import (
2323
handle_known_apistatus_errors,
2424
is_context_length_error,
25+
is_resource_exhausted_error,
2526
)
2627

2728
type AgentInferenceError = (
@@ -90,6 +91,13 @@ def map_pydantic_agent_run_error( # pylint: disable=too-many-return-statements
9091
return PromptTooLongResponse(model=model_id)
9192
case ModelHTTPError(status_code=429):
9293
return QuotaExceededResponse.model(model_id)
94+
case ModelHTTPError() as http_exc if is_resource_exhausted_error(str(http_exc)):
95+
logger.warning(
96+
"Detected RESOURCE_EXHAUSTED in ModelHTTPError with status %d; "
97+
"treating as 429 (llama-stack wraps Vertex AI 429 as 500)",
98+
http_exc.status_code,
99+
)
100+
return QuotaExceededResponse.model(model_id)
93101
case ModelHTTPError():
94102
return InternalServerErrorResponse.generic()
95103
case ModelAPIError() as api_exc:

src/utils/query.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,23 @@ def normalize_vertex_ai_model_id(model_id: str) -> str:
539539
return model_id
540540

541541

542+
def is_resource_exhausted_error(error_message: str) -> bool:
543+
"""Detect Vertex AI RESOURCE_EXHAUSTED errors wrapped as 500 by llama-stack.
544+
545+
llama-stack's remote::vertexai provider translates Vertex AI's 429
546+
RESOURCE_EXHAUSTED into a generic 500 InternalServerError, losing the
547+
original status code. The original gRPC status name is preserved in
548+
the error message, so we match on that.
549+
550+
Args:
551+
error_message: The error message to inspect.
552+
553+
Returns:
554+
True if the message indicates a wrapped RESOURCE_EXHAUSTED error.
555+
"""
556+
return "resource_exhausted" in error_message.lower()
557+
558+
542559
def handle_known_apistatus_errors(
543560
error: LLSApiStatusError | OpenAIAPIStatusError, model_id: str
544561
) -> AbstractErrorResponse:
@@ -556,4 +573,11 @@ def handle_known_apistatus_errors(
556573
return PromptTooLongResponse(model=model_id)
557574
if error.status_code == 429:
558575
return QuotaExceededResponse.model(model_id)
576+
if is_resource_exhausted_error(error_message):
577+
logger.warning(
578+
"Detected RESOURCE_EXHAUSTED in error message with status %d; "
579+
"treating as 429 (llama-stack wraps Vertex AI 429 as 500)",
580+
error.status_code,
581+
)
582+
return QuotaExceededResponse.model(model_id)
559583
return InternalServerErrorResponse.generic()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Tests for agent inference error mapping."""
2+
3+
from pydantic_ai.exceptions import ModelHTTPError
4+
5+
from models.api.responses.error import (
6+
InternalServerErrorResponse,
7+
QuotaExceededResponse,
8+
)
9+
from utils.agents.error_handler import map_pydantic_agent_run_error
10+
11+
12+
class TestMapPydanticAgentRunError:
13+
"""Tests for map_pydantic_agent_run_error with RESOURCE_EXHAUSTED workaround."""
14+
15+
def test_vertex_429_wrapped_as_500_model_http_error(self) -> None:
16+
"""Test that ModelHTTPError 500 with RESOURCE_EXHAUSTED is treated as 429."""
17+
exc = ModelHTTPError(
18+
status_code=500,
19+
model_name="vertexai/gemini-2.5-flash",
20+
body="RESOURCE_EXHAUSTED: Quota exceeded for model",
21+
)
22+
result = map_pydantic_agent_run_error(exc, "vertexai/gemini-2.5-flash")
23+
assert isinstance(result, QuotaExceededResponse)
24+
25+
def test_generic_500_model_http_error(self) -> None:
26+
"""Test that a generic 500 without RESOURCE_EXHAUSTED stays as 500."""
27+
exc = ModelHTTPError(
28+
status_code=500,
29+
model_name="vertexai/gemini-2.5-flash",
30+
body="Internal server error",
31+
)
32+
result = map_pydantic_agent_run_error(exc, "vertexai/gemini-2.5-flash")
33+
assert isinstance(result, InternalServerErrorResponse)

tests/unit/utils/test_query.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,21 @@ def test_quota_exceeded(self) -> None:
358358
detail = result.model_dump()["detail"]
359359
assert "quota" in detail["response"].lower()
360360

361+
def test_vertex_429_wrapped_as_500(self) -> None:
362+
"""Test that Vertex AI RESOURCE_EXHAUSTED wrapped as 500 is treated as 429."""
363+
error = type(
364+
"APIStatusError",
365+
(),
366+
{
367+
"status_code": 500,
368+
"message": "RESOURCE_EXHAUSTED: Quota exceeded for model",
369+
},
370+
)()
371+
result = handle_known_apistatus_errors(error, "model1")
372+
assert isinstance(result, QuotaExceededResponse)
373+
detail = result.model_dump()["detail"]
374+
assert "quota" in detail["response"].lower()
375+
361376
def test_generic_error(self) -> None:
362377
"""Test handling generic error."""
363378
error = type(

0 commit comments

Comments
 (0)