From 968600031a7e84b64714bf3e725adf97be5923f7 Mon Sep 17 00:00:00 2001 From: mateo Date: Mon, 2 Nov 2026 19:54:32 +0000 Subject: [PATCH] fix(responses): enable chat logprobs when include names message.output_text.logprobs Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../transformation.py | 13 ++++++++++++- .../test_responses_sampling_vertex_ai.py | 17 ++++++++++++++++- .../test_litellm_completion_responses.py | 11 +++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 2a2f9fb0f41..fe24d45123c 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -924,11 +924,22 @@ class ChatCompletionSession(TypedDict, total=False): ########### End of Initialize Classes used for Responses API ########### +OUTPUT_TEXT_LOGPROBS_INCLUDE: Final = "message.output_text.logprobs" + + def _requested_top_logprobs(request_fields: Mapping[str, object]) -> int | None: value: Final = request_fields.get("top_logprobs") return value if isinstance(value, int) else None +def _requests_output_text_logprobs(request_fields: Mapping[str, object]) -> bool: + """``top_logprobs`` or ``include: [message.output_text.logprobs]`` each ask the chat route for logprobs.""" + include: Final = request_fields.get("include") + return _requested_top_logprobs(request_fields) is not None or ( + isinstance(include, (list, tuple)) and OUTPUT_TEXT_LOGPROBS_INCLUDE in include + ) + + def _chain_session_metadata(chain_session: str, request_kwargs: Mapping[str, object]) -> Mapping[str, object]: key: Final = "litellm_metadata" if request_kwargs.get("litellm_metadata") else "metadata" metadata: Final = request_kwargs.get(key) @@ -1299,7 +1310,7 @@ class LiteLLMCompletionResponsesConfig: "tools": tools, "top_p": responses_api_request.get("top_p"), "top_logprobs": top_logprobs, - "logprobs": True if top_logprobs is not None else None, + "logprobs": True if _requests_output_text_logprobs(responses_api_request) else None, "user": responses_api_request.get("user"), "temperature": responses_api_request.get("temperature"), "parallel_tool_calls": responses_api_request.get("parallel_tool_calls"), diff --git a/tests/integration/translation/responses/sampling/test_responses_sampling_vertex_ai.py b/tests/integration/translation/responses/sampling/test_responses_sampling_vertex_ai.py index a89e8a867a7..6fadebbe438 100644 --- a/tests/integration/translation/responses/sampling/test_responses_sampling_vertex_ai.py +++ b/tests/integration/translation/responses/sampling/test_responses_sampling_vertex_ai.py @@ -108,6 +108,21 @@ GEMINI_2_5_FLASH_TOP_LOGPROBS_TEST_CASE: Final = replace( ) -@pytest.mark.parametrize("case", [GEMINI_2_5_FLASH_TOP_LOGPROBS_TEST_CASE], ids=lambda case: case.id) +GEMINI_2_5_FLASH_INCLUDE_LOGPROBS_TEST_CASE: Final = replace( + GEMINI_2_5_FLASH_TOP_LOGPROBS_TEST_CASE, + scenario="include_output_text_logprobs", + litellm_request={**GEMINI_2_5_FLASH_TEST_CASE.litellm_request, "include": ["message.output_text.logprobs"]}, + expected_provider_request={ + **GEMINI_2_5_FLASH_TEST_CASE.expected_provider_request, + "generationConfig": {**GENERATION_CONFIG, "responseLogprobs": True}, + }, +) + + +@pytest.mark.parametrize( + "case", + [GEMINI_2_5_FLASH_TOP_LOGPROBS_TEST_CASE, GEMINI_2_5_FLASH_INCLUDE_LOGPROBS_TEST_CASE], + ids=lambda case: case.id, +) def test_responses_sampling_vertex_ai(case: TranslationTestCase, gateway: Gateway, provider: SharedProvider) -> None: assert_translation(case, gateway, provider) diff --git a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py index f9dba9bf994..1a1b89f5b41 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -7489,6 +7489,17 @@ def test_responses_top_logprobs_bridges_to_chat_logprobs() -> None: assert (request["top_logprobs"], request["logprobs"]) == (2, True) +def test_responses_include_output_text_logprobs_bridges_to_chat_logprobs() -> None: + request: Final = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="gemini-3.5-flash", + input="Say hello.", + responses_api_request={"include": ["message.output_text.logprobs"]}, + custom_llm_provider="vertex_ai", + ) + + assert request["logprobs"] is True and "top_logprobs" not in request + + def test_responses_without_top_logprobs_bridges_without_chat_logprobs() -> None: request: Final = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( model="gemini-3.5-flash", input="Say hello.", responses_api_request={}, custom_llm_provider="vertex_ai"