From 7a2e23cc92bc3770b43c48b4ee85135dd08c3226 Mon Sep 17 00:00:00 2001 From: Cabeza Date: Tue, 13 Oct 2026 15:57:35 +0800 Subject: [PATCH] fix(vertex_ai): strip a trailing slash from a custom Gemini api_base A custom `api_base` for `gemini/*` was joined straight into the request path, so the trailing slash that copying it out of Google's docs leaves behind produced `.../openai//models/...`. Google answers that with a 404, which made the model unreachable through the SDK. The other two branches of `_check_custom_proxy` already strip it; the Gemini branch was the one that did not. Fixes #43828 --- litellm/llms/vertex_ai/vertex_llm_base.py | 4 +- .../llms/vertex_ai/test_vertex_llm_base.py | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/litellm/llms/vertex_ai/vertex_llm_base.py b/litellm/llms/vertex_ai/vertex_llm_base.py index 1d5cbe09ba0..5c66db39e7c 100644 --- a/litellm/llms/vertex_ai/vertex_llm_base.py +++ b/litellm/llms/vertex_ai/vertex_llm_base.py @@ -667,7 +667,9 @@ class VertexBase: # For Gemini (Google AI Studio), construct the full path like other providers if model is None: raise ValueError("Model parameter is required for Gemini custom API base URLs") - url = f"{api_base}/models/{model}:{endpoint}" + # A trailing slash on api_base is a common copy-paste from the docs; without + # stripping it the joined path carries `//`, which Google answers with a 404. + url = f"{api_base.rstrip('/')}/models/{model}:{endpoint}" if gemini_api_key is None: raise ValueError( "Missing Gemini API key. Set the GEMINI_API_KEY or GOOGLE_API_KEY environment variable." diff --git a/tests/unit/llms/vertex_ai/test_vertex_llm_base.py b/tests/unit/llms/vertex_ai/test_vertex_llm_base.py index b5a24ab02d9..f8233e31537 100644 --- a/tests/unit/llms/vertex_ai/test_vertex_llm_base.py +++ b/tests/unit/llms/vertex_ai/test_vertex_llm_base.py @@ -875,6 +875,47 @@ class TestVertexBase: result_url == expected_url ), f"Expected {expected_url}, got {result_url} for model {model}" + def test_check_custom_proxy_gemini_url_strips_trailing_slash(self): + """A trailing slash on api_base must not double up in the joined path. + + Copy-pasting the api_base from Google's docs leaves the slash in place, + and the resulting `//` is answered with a 404. #43828 + """ + vertex_base = VertexBase() + + _, url = vertex_base._check_custom_proxy( + api_base="https://generativelanguage.googleapis.com/v1beta/openai/", + custom_llm_provider="gemini", + gemini_api_key="test-api-key", + endpoint="generateContent", + stream=False, + auth_header=None, + url="https://generativelanguage.googleapis.com/v1beta/models/gemini-3.8-flash:generateContent", + model="gemini-3.8-flash", + ) + + assert url == ( + "https://generativelanguage.googleapis.com/v1beta/openai/models/gemini-3.8-flash:generateContent" + ) + assert "//models" not in url + + def test_check_custom_proxy_gemini_url_without_trailing_slash_is_unchanged(self): + """The slash-free form keeps producing the same URL it always did.""" + vertex_base = VertexBase() + + _, url = vertex_base._check_custom_proxy( + api_base="https://proxy.example.com/v1beta", + custom_llm_provider="gemini", + gemini_api_key="test-api-key", + endpoint="generateContent", + stream=False, + auth_header=None, + url="https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent", + model="gemini-2.5-pro", + ) + + assert url == "https://proxy.example.com/v1beta/models/gemini-2.5-pro:generateContent" + def test_check_custom_proxy_streaming_parameter(self): """Test that streaming parameter correctly adds ?alt=sse to URLs""" vertex_base = VertexBase()