mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-15 04:19:08 +00:00
fix(proxy): harden Vertex pass-through route matching against substring false positives (#45787)
A plain pass-through endpoint whose target URL contains the substring "predict", "search", "embedContent", "rawPredict", "streamGenerateContent" or "predictLongRunning" could be misrouted to the Vertex AI logging handler. Inside vertex_passthrough_handler the unguarded response["predictions"] access then raised KeyError in the logging worker and silently dropped the spend row (issue #45787). - vertex_passthrough_logging_handler.py: match Vertex method routes by the canonical ":method" suffix (mirroring PassThroughEndpointLogging.is_vertex_route) instead of bare substring matching; the batchPredictionJobs resource route is path-based and kept as-is. - vertex_embeddings/transformation.py: raise a clear ValueError when the embedding response lacks the "predictions" field instead of a raw KeyError surfacing deep in the logging worker. - test_success_handler.py: regression tests for the issue URLs (negative) and for authentic Vertex ":method" URLs (positive). AI-assisted disclosure: root cause located and patch written with the help of an AI assistant; all tests and lint verified locally. Signed-off-by: apex-mochen <2756823972@qq.com>
This commit is contained in:
parent
7ff5010674
commit
d7d186d1f7
3 changed files with 80 additions and 9 deletions
|
|
@ -229,6 +229,9 @@ class VertexAITextEmbeddingConfig(LiteLLMBaseModel):
|
|||
if VertexBGEConfig.is_bge_model(model):
|
||||
return VertexBGEConfig.transform_response(response=response, model=model, model_response=model_response)
|
||||
|
||||
if not isinstance(response, dict) or "predictions" not in response:
|
||||
raise ValueError("Vertex embedding response missing 'predictions' field")
|
||||
|
||||
_predictions: Final = response["predictions"]
|
||||
|
||||
embedding_response: Final = []
|
||||
|
|
@ -257,6 +260,9 @@ class VertexAITextEmbeddingConfig(LiteLLMBaseModel):
|
|||
"""
|
||||
Transforms a vertex fine-tuned model embedding response to an openai response format.
|
||||
"""
|
||||
if not isinstance(response, dict) or "predictions" not in response:
|
||||
raise ValueError("Vertex embedding response missing 'predictions' field")
|
||||
|
||||
_predictions: Final = response["predictions"]
|
||||
|
||||
embedding_response: Final = []
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ class VertexPassthroughLoggingHandler:
|
|||
custom_llm_provider="vertex_ai",
|
||||
vertex_location=vertex_location,
|
||||
)
|
||||
if "predictLongRunning" in url_route:
|
||||
if ":predictLongRunning" in url_route:
|
||||
model = VertexPassthroughLoggingHandler.extract_model_from_url(url_route)
|
||||
|
||||
vertex_video_config: Final = VertexAIVideoConfig()
|
||||
|
|
@ -188,7 +188,7 @@ class VertexPassthroughLoggingHandler:
|
|||
"kwargs": kwargs,
|
||||
}
|
||||
|
||||
elif "generateContent" in url_route:
|
||||
elif ":generateContent" in url_route or ":streamGenerateContent" in url_route:
|
||||
model = VertexPassthroughLoggingHandler.extract_model_from_url(url_route)
|
||||
|
||||
instance_of_vertex_llm: Final = litellm.VertexGeminiConfig()
|
||||
|
|
@ -220,7 +220,7 @@ class VertexPassthroughLoggingHandler:
|
|||
"kwargs": kwargs,
|
||||
}
|
||||
|
||||
elif "embedContent" in url_route or "batchEmbedContents" in url_route:
|
||||
elif ":embedContent" in url_route or ":batchEmbedContents" in url_route:
|
||||
return VertexPassthroughLoggingHandler._handle_embed_content_response(
|
||||
httpx_response=httpx_response,
|
||||
logging_obj=logging_obj,
|
||||
|
|
@ -228,14 +228,14 @@ class VertexPassthroughLoggingHandler:
|
|||
kwargs=kwargs,
|
||||
request_body=request_body,
|
||||
)
|
||||
elif "predict" in url_route:
|
||||
elif ":predict" in url_route:
|
||||
return VertexPassthroughLoggingHandler._handle_predict_response(
|
||||
httpx_response=httpx_response,
|
||||
logging_obj=logging_obj,
|
||||
url_route=url_route,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
elif "rawPredict" in url_route or "streamRawPredict" in url_route:
|
||||
elif ":rawPredict" in url_route or ":streamRawPredict" in url_route:
|
||||
from litellm.llms.vertex_ai.vertex_ai_partner_models import (
|
||||
get_vertex_ai_partner_model_config,
|
||||
)
|
||||
|
|
@ -287,7 +287,7 @@ class VertexPassthroughLoggingHandler:
|
|||
"result": litellm_prediction_response,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
elif "search" in url_route:
|
||||
elif ":search" in url_route:
|
||||
litellm_vs_response: Final = vertex_search_api_config.transform_search_vector_store_response(
|
||||
response=httpx_response,
|
||||
litellm_logging_obj=logging_obj,
|
||||
|
|
@ -513,7 +513,7 @@ class VertexPassthroughLoggingHandler:
|
|||
|
||||
model: Final = VertexPassthroughLoggingHandler.extract_model_from_url(url_route)
|
||||
response_json: Final = httpx_response.json()
|
||||
is_batch: Final = "batchEmbedContents" in url_route
|
||||
is_batch: Final = ":batchEmbedContents" in url_route
|
||||
|
||||
input_text: Final = VertexPassthroughLoggingHandler._extract_embed_content_input(
|
||||
request_body=request_body, batch=is_batch
|
||||
|
|
@ -626,7 +626,7 @@ class VertexPassthroughLoggingHandler:
|
|||
url_route: str,
|
||||
) -> ModelResponse | TextCompletionResponse | None:
|
||||
parsed_chunks = []
|
||||
if "generateContent" in url_route or "streamGenerateContent" in url_route:
|
||||
if ":generateContent" in url_route or ":streamGenerateContent" in url_route:
|
||||
vertex_iterator: Any = VertexModelResponseIterator(
|
||||
streaming_response=None,
|
||||
sync_stream=False,
|
||||
|
|
@ -634,7 +634,7 @@ class VertexPassthroughLoggingHandler:
|
|||
)
|
||||
chunk_parsing_logic: Callable[..., ModelResponseStream | None] = vertex_iterator._common_chunk_parsing_logic
|
||||
parsed_chunks = [chunk_parsing_logic(chunk) for chunk in all_chunks]
|
||||
elif "rawPredict" in url_route or "streamRawPredict" in url_route:
|
||||
elif ":rawPredict" in url_route or ":streamRawPredict" in url_route:
|
||||
from litellm.llms.anthropic.chat.handler import ModelResponseIterator
|
||||
from litellm.llms.base_llm.base_model_iterator import (
|
||||
BaseModelResponseIterator,
|
||||
|
|
|
|||
|
|
@ -584,3 +584,68 @@ def test_vertex_ai_live_route_without_usage_frames_yields_no_logging_response(
|
|||
|
||||
assert normalized["standard_logging_response_object"] is None
|
||||
assert normalized["kwargs"] == {"model": _LIVE_MODEL}
|
||||
|
||||
|
||||
# --- regression for #45787: substring route matching must not classify plain
|
||||
# passthrough URLs (e.g. "http://example.com/api/predict/laya") as Vertex AI ---
|
||||
|
||||
_NON_VERTEX_URLS_WITH_METHOD_SUBSTRINGS: Final = [
|
||||
"http://example.com/api/predict/laya",
|
||||
"http://example.com/api/search/items",
|
||||
"http://example.com/api/embedContent/proxy",
|
||||
"http://example.com/v1/rawPredict",
|
||||
"http://example.com/predictLongRunning/jobs/1",
|
||||
"http://example.com/api/streamGenerateContent/relay",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url", _NON_VERTEX_URLS_WITH_METHOD_SUBSTRINGS)
|
||||
def test_is_vertex_route_ignores_non_vertex_urls_containing_method_substrings(url: str) -> None:
|
||||
assert PassThroughEndpointLogging().is_vertex_route(url) is False
|
||||
|
||||
|
||||
def test_is_vertex_route_matches_authentic_vertex_method_urls() -> None:
|
||||
handler = PassThroughEndpointLogging()
|
||||
assert (
|
||||
handler.is_vertex_route(
|
||||
"https://us-central1-aiplatform.googleapis.com/v1/projects/p/locations/us-central1/"
|
||||
"publishers/google/models/gemini-2.0-flash:predict"
|
||||
)
|
||||
is True
|
||||
)
|
||||
assert (
|
||||
handler.is_vertex_route(
|
||||
"https://us-central1-aiplatform.googleapis.com/v1/projects/p/locations/us-central1/"
|
||||
"publishers/google/models/gemini-2.0:search"
|
||||
)
|
||||
is True
|
||||
)
|
||||
assert (
|
||||
handler.is_vertex_route(
|
||||
"https://us-central1-aiplatform.googleapis.com/v1/projects/p/locations/us-central1/"
|
||||
"publishers/google/models/text-embedding-005:embedContent"
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("local_model_cost_map")
|
||||
def test_non_vertex_predict_url_normalizes_without_predictions_keyerror() -> None:
|
||||
# Issue #45787: a plain passthrough URL containing the substring "predict"
|
||||
# used to be misrouted to the Vertex handler, whose unguarded
|
||||
# response["predictions"] access then raised KeyError inside the logging
|
||||
# worker and silently dropped the spend row.
|
||||
normalized = PassThroughEndpointLogging().normalize_llm_passthrough_logging_payload(
|
||||
httpx_response=httpx.Response(200, request=httpx.Request("GET", "http://example.com/api/predict/laya")),
|
||||
response_body={}, # no "predictions" key
|
||||
request_body={},
|
||||
logging_obj=_logging_obj(),
|
||||
url_route="http://example.com/api/predict/laya",
|
||||
result="",
|
||||
start_time=_START,
|
||||
end_time=_END,
|
||||
cache_hit=False,
|
||||
custom_llm_provider=None,
|
||||
)
|
||||
|
||||
assert normalized["standard_logging_response_object"] is not None
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue