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
This commit is contained in:
mateo-berri 2026-11-04 20:25:29 -08:00
parent f418a83f49
commit 02723a28a6
4 changed files with 24 additions and 65 deletions

View file

@ -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)]

View file

@ -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,
)

View file

@ -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

View file

@ -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: