From 02723a28a662851112dcfeaa8a5185bf7e96b1d0 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 4 Nov 2026 20:25:29 -0800 Subject: [PATCH] fix(vertex_ai): keep main's Gemini adapter and pass a missing Authorization as no key The Gemini adapter change for later systemInstruction parts goes back to main, since it is not the double-auth fix this PR is for. The Vertex auth dependency now hands a missing Authorization header to the auth check as no key, like main's first auth pass, so custom auth and the 401 text see what they see on main --- .../google_genai/adapters/transformation.py | 10 ++-- .../llm_passthrough_endpoints.py | 4 +- .../google_genai/test_google_genai_adapter.py | 56 ------------------- .../test_llm_pass_through_endpoints.py | 19 ++++++- 4 files changed, 24 insertions(+), 65 deletions(-) diff --git a/litellm/google_genai/adapters/transformation.py b/litellm/google_genai/adapters/transformation.py index 40ab377dd5e..c1895376675 100644 --- a/litellm/google_genai/adapters/transformation.py +++ b/litellm/google_genai/adapters/transformation.py @@ -249,10 +249,6 @@ def _genai_tool_call(part: object) -> ChatCompletionAssistantToolCall | None: ) -def _system_texts(parts: object) -> tuple[str, ...]: - return tuple(part["text"] for part in _genai_message_parts(_genai_content_parts(parts)) if part["type"] == "text") - - def _genai_content_messages(content: object) -> tuple[AllMessageValues, ...]: fields: Final = _validated(_CONFIG_FIELDS, content) if fields is None: @@ -636,9 +632,11 @@ class GoogleGenAIAdapter: system_instruction: _GenAISystemInstruction | None = None, ) -> list[AllMessageValues]: """Transform Google GenAI contents to OpenAI messages format""" - system_texts: Final = _system_texts(system_instruction.get("parts") if system_instruction else None) + system_parts: Final = system_instruction.get("parts", ()) if system_instruction else () system_messages: Final = ( - (ChatCompletionSystemMessage(role="system", content="\n".join(system_texts)),) if system_texts else () + (ChatCompletionSystemMessage(role="system", content=system_parts[0]["text"]),) + if system_parts and "text" in system_parts[0] + else () ) return [*system_messages, *chain.from_iterable(_genai_content_messages(content) for content in contents)] diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 9ffca3957b2..30b5c9965d8 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -3203,7 +3203,9 @@ async def _authenticate_vertex_caller( litellm_api_key: Annotated[str | None, Security(custom_litellm_key_header)], ) -> UserAPIKeyAuth: return await user_api_key_auth( - request=request, api_key=authorization or "", custom_litellm_key_header=litellm_api_key + request=request, + api_key=authorization, # pyright: ignore[reportArgumentType] # its own Security default is None when unset + custom_litellm_key_header=litellm_api_key, ) diff --git a/tests/unit/google_genai/test_google_genai_adapter.py b/tests/unit/google_genai/test_google_genai_adapter.py index d622a1b85b9..4ab642dc99d 100644 --- a/tests/unit/google_genai/test_google_genai_adapter.py +++ b/tests/unit/google_genai/test_google_genai_adapter.py @@ -194,62 +194,6 @@ def test_native_function_call_preserves_arguments_and_matching_response(args: ob ] -@pytest.mark.parametrize( - ("system_texts", "expected_content"), - [ - (("Answer tersely",), "Answer tersely"), - (("Answer tersely", "Use British spelling"), "Answer tersely\nUse British spelling"), - ], -) -def test_every_system_instruction_text_part_reaches_the_system_message( - system_texts: tuple[str, ...], expected_content: str -) -> None: - request: Final = GoogleGenAIAdapter().translate_generate_content_to_completion( - model="gemini/gemini-3.8-flash", - contents=[{"role": "user", "parts": [{"text": "hi"}]}], - systemInstruction={"parts": [{"text": text} for text in system_texts]}, - ) - assert request["messages"][0] == {"role": "system", "content": expected_content} - - -@pytest.mark.parametrize( - ("system_parts", "expected_messages"), - [ - pytest.param( - [ - {"text": "Answer tersely"}, - {"text": None}, - {"text": 3}, - {"text": ["x"]}, - {"text": "Use British spelling"}, - ], - [{"role": "system", "content": "Answer tersely\nUse British spelling"}, {"role": "user", "content": "hi"}], - id="malformed_parts_skipped_like_contents", - ), - pytest.param( - {"text": "Answer tersely"}, - [{"role": "system", "content": "Answer tersely"}, {"role": "user", "content": "hi"}], - id="single_object_parts", - ), - pytest.param( - ["Answer tersely", {"text": "Use British spelling"}], - [{"role": "system", "content": "Answer tersely\nUse British spelling"}, {"role": "user", "content": "hi"}], - id="bare_string_part_read_like_contents", - ), - pytest.param(None, [{"role": "user", "content": "hi"}], id="no_parts"), - ], -) -def test_system_instruction_parts_follow_the_contents_part_rules( - system_parts: object, expected_messages: list[dict[str, str]] -) -> None: - request: Final = GoogleGenAIAdapter().translate_generate_content_to_completion( - model="gemini/gemini-3.8-flash", - contents=[{"role": "user", "parts": [{"text": "hi"}]}], - systemInstruction={"parts": system_parts}, - ) - assert request["messages"] == expected_messages - - def test_adapter_import(): """Test that the adapter can be imported successfully""" from litellm.google_genai.adapters.handler import GenerateContentToCompletionHandler diff --git a/tests/unit/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/unit/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py index 2f7a87d54eb..2aa0ae118aa 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py +++ b/tests/unit/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py @@ -8490,9 +8490,9 @@ class TestAzureSpeechRawBodyThroughRealAuth: class _CountingCustomAuth: def __init__(self, only_accepts: str | None = None) -> None: self.only_accepts: Final = only_accepts - self.api_keys_seen: tuple[str, ...] = () + self.api_keys_seen: tuple[str | None, ...] = () - async def __call__(self, request: Request, api_key: str) -> UserAPIKeyAuth: + async def __call__(self, request: Request, api_key: str | None) -> UserAPIKeyAuth: self.api_keys_seen = (*self.api_keys_seen, api_key) if self.only_accepts is not None and api_key != self.only_accepts: raise ValueError("unknown caller") @@ -8694,6 +8694,21 @@ class TestVertexPassthroughAuthenticatesOncePerRequest: assert not google.called + @pytest.mark.parametrize("path", [f"/vertex_ai/{GENERATE_CONTENT}", f"/vertex_ai/discovery/{DISCOVERY_SEARCH}"]) + def test_request_without_a_key_hands_custom_auth_none( + self, monkeypatch: pytest.MonkeyPatch, path: str + ) -> None: + custom_auth: Final = _CountingCustomAuth(only_accepts=self.VIRTUAL_KEY) + with respx.mock(assert_all_called=False) as upstream: + google: Final = upstream.route().mock(return_value=httpx.Response(200)) + + response, recorder = self._post(monkeypatch, path, {}, custom_auth, ("us-central1", "global")) + + assert response.status_code == 401, response.text + assert custom_auth.api_keys_seen == (None,) + assert recorder.served_user_ids == () + assert not google.called + class TestTypeSafePassthroughRoute: @staticmethod def _request(body: object, query_params: Mapping[str, str] | None = None) -> MagicMock: