diff --git a/litellm/llms/gemini/chat/transformation.py b/litellm/llms/gemini/chat/transformation.py index 1539f13455f..c15bfc88454 100644 --- a/litellm/llms/gemini/chat/transformation.py +++ b/litellm/llms/gemini/chat/transformation.py @@ -97,6 +97,7 @@ class GoogleAIStudioGeminiConfig(VertexGeminiConfig): "n", "stop", "logprobs", + "top_logprobs", "frequency_penalty", "presence_penalty", "seed", diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index 80244cfc3eb..c553829f862 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -1888,6 +1888,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): optional_params["responseLogprobs"] = value elif param == "top_logprobs": optional_params["logprobs"] = value + optional_params["responseLogprobs"] = True # rebind-ok: this mapping fills optional_params in place elif (param == "tools" or param == "functions") and isinstance(value, list) and value: # Pass optional_params so _map_function can add toolConfig if needed mapped_tools = with_integer_enums_that_fit( diff --git a/tests/integration/translation/chat_completions/params/__init__.py b/tests/integration/translation/chat_completions/params/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/integration/translation/chat_completions/params/test_chat_completions_params_gemini_logprobs.py b/tests/integration/translation/chat_completions/params/test_chat_completions_params_gemini_logprobs.py new file mode 100644 index 00000000000..e83edc00ac5 --- /dev/null +++ b/tests/integration/translation/chat_completions/params/test_chat_completions_params_gemini_logprobs.py @@ -0,0 +1,30 @@ +from dataclasses import replace +from typing import Final + +import pytest +from integration._support.client import Gateway +from integration._support.provider import SharedProvider +from integration.translation.case import TranslationTestCase +from integration.translation.chat_completions.bases.gemini import GEMINI_3_5_FLASH_TEST_CASE +from integration.translation.params import with_field +from integration.translation.runner import assert_translation + +GEMINI_3_5_FLASH_TOP_LOGPROBS_TEST_CASE: Final = replace( + GEMINI_3_5_FLASH_TEST_CASE, + scenario="logprobs_top_logprobs", + litellm_request={**GEMINI_3_5_FLASH_TEST_CASE.litellm_request, "logprobs": True, "top_logprobs": 3}, + expected_provider_request=with_field( + with_field(GEMINI_3_5_FLASH_TEST_CASE.expected_provider_request, ("generationConfig", "responseLogprobs"), True), + ("generationConfig", "logprobs"), + 3, + ), +) + +CASES: Final = (GEMINI_3_5_FLASH_TOP_LOGPROBS_TEST_CASE,) + + +@pytest.mark.parametrize("case", CASES, ids=lambda case: case.id) +def test_chat_completions_params_gemini_logprobs( + case: TranslationTestCase, gateway: Gateway, provider: SharedProvider +) -> None: + assert_translation(case, gateway, provider) diff --git a/tests/integration/translation/params.py b/tests/integration/translation/params.py new file mode 100644 index 00000000000..30681d3aa71 --- /dev/null +++ b/tests/integration/translation/params.py @@ -0,0 +1,13 @@ +from collections.abc import Mapping, Sequence +from typing import Final + +from pydantic import JsonValue + + +def with_field(mapping: Mapping[str, JsonValue], path: Sequence[str], value: JsonValue) -> Mapping[str, JsonValue]: + """Return a copy of `mapping` with the nested key `path` set to `value`, creating intermediate objects.""" + head: Final = path[0] + if len(path) == 1: + return {**mapping, head: value} + inner: Final = mapping.get(head) + return {**mapping, head: with_field(inner if isinstance(inner, dict) else {}, path[1:], value)} diff --git a/tests/integration/translation/responses/params/__init__.py b/tests/integration/translation/responses/params/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/integration/translation/responses/params/test_responses_params_gemini_logprobs.py b/tests/integration/translation/responses/params/test_responses_params_gemini_logprobs.py new file mode 100644 index 00000000000..fa51b99037f --- /dev/null +++ b/tests/integration/translation/responses/params/test_responses_params_gemini_logprobs.py @@ -0,0 +1,30 @@ +from dataclasses import replace +from typing import Final + +import pytest +from integration._support.client import Gateway +from integration._support.provider import SharedProvider +from integration.translation.case import TranslationTestCase +from integration.translation.params import with_field +from integration.translation.responses.bases.gemini import GEMINI_3_5_FLASH_TEST_CASE +from integration.translation.runner import assert_translation + +GEMINI_3_5_FLASH_TOP_LOGPROBS_TEST_CASE: Final = replace( + GEMINI_3_5_FLASH_TEST_CASE, + scenario="top_logprobs", + litellm_request={**GEMINI_3_5_FLASH_TEST_CASE.litellm_request, "top_logprobs": 3}, + expected_provider_request=with_field( + with_field(GEMINI_3_5_FLASH_TEST_CASE.expected_provider_request, ("generationConfig", "logprobs"), 3), + ("generationConfig", "responseLogprobs"), + True, + ), +) + +CASES: Final = (GEMINI_3_5_FLASH_TOP_LOGPROBS_TEST_CASE,) + + +@pytest.mark.parametrize("case", CASES, ids=lambda case: case.id) +def test_responses_params_gemini_logprobs( + case: TranslationTestCase, gateway: Gateway, provider: SharedProvider +) -> None: + assert_translation(case, gateway, provider) diff --git a/tests/unit/llms/gemini/chat/__init__.py b/tests/unit/llms/gemini/chat/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/llms/gemini/chat/test_transformation.py b/tests/unit/llms/gemini/chat/test_transformation.py new file mode 100644 index 00000000000..31bb8cc6e0a --- /dev/null +++ b/tests/unit/llms/gemini/chat/test_transformation.py @@ -0,0 +1,31 @@ +from typing import Final + +from litellm.llms.gemini.chat.transformation import GoogleAIStudioGeminiConfig + + +def test_top_logprobs_maps_to_generation_config_logprobs(): + config: Final = GoogleAIStudioGeminiConfig() + + assert "top_logprobs" in config.get_supported_openai_params("gemini-3.5-flash") + + optional_params: Final = config.map_openai_params( + non_default_params={"logprobs": True, "top_logprobs": 3}, + optional_params={}, + model="gemini-3.5-flash", + drop_params=False, + ) + + assert optional_params["responseLogprobs"] is True + assert optional_params["logprobs"] == 3 + + +def test_top_logprobs_alone_enables_response_logprobs(): + optional_params: Final = GoogleAIStudioGeminiConfig().map_openai_params( + non_default_params={"top_logprobs": 3}, + optional_params={}, + model="gemini-3.5-flash", + drop_params=False, + ) + + assert optional_params["responseLogprobs"] is True + assert optional_params["logprobs"] == 3