From 311039d2cbc3d472ecd8aa1632e59d388a4021dc Mon Sep 17 00:00:00 2001 From: Charles Cheng Date: Thu, 18 Jun 2026 16:06:38 +0800 Subject: [PATCH] fix(logging): fall back to adapter dict in generate_content logging The google_genai generate_content adapter path (generate_content_provider_config is None) routes through litellm.completion and hands the success-logging handler an already transformed generate_content dict, with no raw httpx_response recorded. The handler assumed a raw httpx.Response was always present and raised ValueError('Google GenAI Generate Content: httpx_response is None'), crashing the background logging worker even though the call itself returned HTTP 200. Fall back to the transformed dict when no httpx_response is available, mirroring the existing anthropic_messages logging handler. The native path still uses the raw httpx_response as before. Adds regression tests covering both the adapter (no httpx_response) and native (httpx_response present) logging paths. --- litellm/litellm_core_utils/litellm_logging.py | 12 ++- .../test_generate_content_logging.py | 84 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 tests/test_litellm/google_genai/test_generate_content_logging.py diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 36d17596873..e09e1bfa275 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -3294,10 +3294,18 @@ class Logging(LiteLLMLoggingBaseClass): """ import httpx + # The adapter path (generate_content_provider_config is None) routes + # through litellm.completion and hands us an already transformed + # generate_content dict with no raw httpx_response recorded. Fall back to + # that dict instead of crashing the logging worker (#30707); the native + # path still uses the raw httpx_response as before. httpx_response = self.model_call_details.get("httpx_response", None) - if httpx_response is None: + if httpx_response is not None and isinstance(httpx_response, httpx.Response): + dict_result = httpx_response.json() + elif isinstance(result, dict): + dict_result = result + else: raise ValueError("Google GenAI Generate Content: httpx_response is None") - dict_result = httpx_response.json() result = litellm.VertexGeminiConfig()._transform_google_generate_content_to_openai_model_response( completion_response=dict_result, model_response=litellm.ModelResponse(), diff --git a/tests/test_litellm/google_genai/test_generate_content_logging.py b/tests/test_litellm/google_genai/test_generate_content_logging.py new file mode 100644 index 00000000000..81bb1a32bc3 --- /dev/null +++ b/tests/test_litellm/google_genai/test_generate_content_logging.py @@ -0,0 +1,84 @@ +"""Logging-path tests for the google_genai generate_content adapter (#30707). + +The adapter path (``generate_content_provider_config is None``) routes through +``litellm.completion`` and hands the success-logging handler an already +transformed generate_content dict, with no raw ``httpx.Response`` recorded. The +handler must fall back to that dict instead of crashing the logging worker. +""" + +import os +import sys +import time + +import httpx +import pytest + +sys.path.insert(0, os.path.abspath("../../..")) + +from litellm.litellm_core_utils.litellm_logging import Logging as LitellmLogging +from litellm.types.utils import ModelResponse + + +def _make_logging_obj() -> LitellmLogging: + logging_obj = LitellmLogging( + model="gemini-3.1-flash-lite", + messages=[{"role": "user", "content": "Hello"}], + stream=False, + call_type="generate_content", + start_time=time.time(), + litellm_call_id="30707-test", + function_id="30707-test", + ) + # set in update_environment_variables() during a real call; the response + # transform reads it, so provide it for the isolated handler test. + logging_obj.optional_params = {} + return logging_obj + + +def _generate_content_dict() -> dict: + return { + "candidates": [ + { + "content": {"parts": [{"text": "Hello there"}], "role": "model"}, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [], + } + ], + "usageMetadata": { + "promptTokenCount": 5, + "candidatesTokenCount": 2, + "totalTokenCount": 7, + }, + # the adapter adds a convenience top-level text field + "text": "Hello there", + } + + +def test_adapter_path_logs_without_httpx_response(): + """No httpx_response (adapter path) must not raise; the transformed dict is used.""" + logging_obj = _make_logging_obj() + logging_obj.model_call_details["httpx_response"] = None + + result = logging_obj._handle_non_streaming_google_genai_generate_content_response_logging( + result=_generate_content_dict() + ) + + assert isinstance(result, ModelResponse) + assert result.choices[0].message.content == "Hello there" + assert result.usage.total_tokens == 7 + + +def test_httpx_response_path_still_used_when_present(): + """When a raw httpx_response exists it is still the source of truth.""" + logging_obj = _make_logging_obj() + logging_obj.model_call_details["httpx_response"] = httpx.Response( + status_code=200, json=_generate_content_dict() + ) + + result = logging_obj._handle_non_streaming_google_genai_generate_content_response_logging( + result=None + ) + + assert isinstance(result, ModelResponse) + assert result.choices[0].message.content == "Hello there"