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.
This commit is contained in:
Charles Cheng 2026-06-18 16:06:38 +08:00 • committed by Charles Cheng
parent 4d33964898
commit 311039d2cb
2 changed files with 94 additions and 2 deletions

View file

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

View file

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