Files
litellm/tests/test_litellm/google_genai/test_google_genai_handler.py
T
Ishaan Jaff 3852fc96c1 [Oct Staging Branch] (#15460)
* Implement fix for thinking_blocks and converse API calls

This fixes Claude's models via the Converse API, which should also fix
Claude Code.

* Add thinking literal

* Fix mypy issues

* Type fix for redacted thinking

* Add voyage model integration in sagemaker

* Add config file logic

* Use already exiting voyage transformation

* refactor code as per comments

* fix merge error

* refactor code as per comments

* refactor code as per comments

* UI new build

* [Fix] router - regression when adding/removing models  (#15451)

* fix(router): update model_name_to_deployment_indices on deployment removal

When a deployment is deleted, the model_name_to_deployment_indices map
was not being updated, causing stale index references. This could lead
to incorrect routing behavior when deployments with the same model_name
were dynamically removed.

Changes:
- Update _update_deployment_indices_after_removal to maintain
  model_name_to_deployment_indices mapping
- Remove deleted indices and decrement indices greater than removed index
- Clean up empty entries when no deployments remain for a model name
- Update test to verify proper index shifting and cleanup behavior

* fix(router): remove redundant index building during initialization

Remove duplicate index building operations that were causing unnecessary
work during router initialization:

1. Removed redundant `_build_model_id_to_deployment_index_map` call in
   __init__ - `set_model_list` already builds all indices from scratch

2. Removed redundant `_build_model_name_index` call at end of
   `set_model_list` - the index is already built incrementally via
   `_create_deployment` -> `_add_model_to_list_and_index_map`

Both indices (model_id_to_deployment_index_map and
model_name_to_deployment_indices) are properly maintained as lookup
indexes through existing helper methods. This change eliminates O(N)
duplicate work during initialization without any behavioral changes.

The indices continue to be correctly synchronized with model_list on
all operations (add/remove/upsert).

* fix(prometheus): Fix Prometheus metric collection in a multi-workers environment (#14929)

Co-authored-by: sotazhang <sotazhang@tencent.com>

* Add tiered pricing and cost calculation for xai

* Use generic cost calculator

* Resolve conflicts in generated HTML files

* Remove penalty params as supported params for gemini preview model (#15503)

* fix conversion of thinking block

* add application level encryption in SQS (#15512)

* docs: fix doc

* docs(index.md): bump rc

* [Fix] GEMINI - CLI -  add google_routes to llm_api_routes (#15500)

* fix: add google_routes to llm_api_routes

* test: test_virtual_key_llm_api_routes_allows_google_routes

* build: bump version

* bump: version 1.78.0 → 1.78.1

* add application level encryption in SQS

* add application level encryption in SQS

---------

Co-authored-by: Krrish Dholakia <krrishdholakia@gmail.com>
Co-authored-by: Ishaan Jaff <ishaanjaffer0324@gmail.com>
Co-authored-by: deepanshu <deepanshu.lulla@hq.bill.com>

* [Feat] Bedrock Knowledgebase - return search_response when using /chat/completions API with LiteLLM (#15509)

* docs: fix doc

* docs(index.md): bump rc

* [Fix] GEMINI - CLI -  add google_routes to llm_api_routes (#15500)

* fix: add google_routes to llm_api_routes

* test: test_virtual_key_llm_api_routes_allows_google_routes

* add AnthropicCitation

* fix async_post_call_success_deployment_hook

* fix add vector_store_custom_logger to global callbacks

* test_e2e_bedrock_knowledgebase_retrieval_with_llm_api_call

* async_post_call_success_deployment_hook

* add async_post_call_streaming_deployment_hook

* async def test_e2e_bedrock_knowledgebase_retrieval_with_llm_api_call_streaming(setup_vector_store_registry):

* fix _call_post_streaming_deployment_hook

* fix async_post_call_streaming_deployment_hook

* test update

* docs: Accessing Search Results

* docs KB

* fix chatUI

* fix searchResults

* fix onSearchResults

* fix kb

---------

Co-authored-by: Krrish Dholakia <krrishdholakia@gmail.com>

* [Feat] Add dynamic rate limits on LiteLLM Gateway  (#15518)

* docs: fix doc

* docs(index.md): bump rc

* [Fix] GEMINI - CLI -  add google_routes to llm_api_routes (#15500)

* fix: add google_routes to llm_api_routes

* test: test_virtual_key_llm_api_routes_allows_google_routes

* build: bump version

* bump: version 1.78.0 → 1.78.1

* fix: KeyRequestBase

* fix rpm_limit_type

* fix dynamic rate limits

* fix use dynamic limits here

* fix _should_enforce_rate_limit

* fix _should_enforce_rate_limit

* fix counter

* test_dynamic_rate_limiting_v3

* use _create_rate_limit_descriptors

---------

Co-authored-by: Krrish Dholakia <krrishdholakia@gmail.com>

* Add google rerank endpoint

* Add docs

* fix mypy error

* fix mypy and lint errors

* Add haiku 4.5 integration

* Add haiku 4.5 integration for other regions as well

* Handle citation field correctly

* Fix filtering headers for signature calcs

* Add haiku 4.5 integration (#15650)

---------

Co-authored-by: Leslie Cheng <leslie.cheng5@gmail.com>
Co-authored-by: Sameer Kankute <sameer@berri.ai>
Co-authored-by: Alexsander Hamir <alexsanderhamirgomesbaptista@gmail.com>
Co-authored-by: Lucas <10226902+LoadingZhang@users.noreply.github.com>
Co-authored-by: sotazhang <sotazhang@tencent.com>
Co-authored-by: Deepanshu Lulla <deepanshu.lulla@gmail.com>
Co-authored-by: Krrish Dholakia <krrishdholakia@gmail.com>
Co-authored-by: deepanshu <deepanshu.lulla@hq.bill.com>
2025-10-17 17:52:25 -07:00

328 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Test to verify the Google GenAI generate_content handler functionality
"""
import json
import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
sys.path.insert(
0, os.path.abspath("../../../..")
) # Adds the parent directory to the system path
import litellm
from litellm.google_genai.adapters.handler import GenerateContentToCompletionHandler
from litellm.google_genai.adapters.transformation import GoogleGenAIAdapter
from litellm.types.utils import ModelResponse
def test_non_stream_response_when_stream_requested_sync():
"""
Test that when a non-stream response is returned but streaming was requested,
the sync handler correctly transforms it to generate_content format.
"""
from litellm.types.utils import Choices
# Mock a non-stream response (ModelResponse with valid choices)
mock_response = ModelResponse(
id="test-123",
choices=[
Choices(
index=0,
message={
"role": "assistant",
"content": "Hello, world!"
},
finish_reason="stop"
)
],
created=1234567890,
model="gpt-3.5-turbo",
object="chat.completion"
)
# Create an instance of the adapter
adapter = GoogleGenAIAdapter()
# Test the adapter's translate_completion_to_generate_content method directly
result = adapter.translate_completion_to_generate_content(mock_response)
# Verify the result is a valid Google GenAI format response
assert "candidates" in result
assert isinstance(result["candidates"], list)
assert len(result["candidates"]) > 0
candidate = result["candidates"][0]
assert "content" in candidate
assert "parts" in candidate["content"]
assert isinstance(candidate["content"]["parts"], list)
assert len(candidate["content"]["parts"]) > 0
assert "text" in candidate["content"]["parts"][0]
assert candidate["content"]["parts"][0]["text"] == "Hello, world!"
@pytest.mark.asyncio
async def test_non_stream_response_when_stream_requested_async():
"""
Test that when a non-stream response is returned but streaming was requested,
the async handler correctly transforms it to generate_content format.
"""
from litellm.types.utils import Choices
# Mock a non-stream response (ModelResponse with valid choices)
mock_response = ModelResponse(
id="test-123",
choices=[
Choices(
index=0,
message={
"role": "assistant",
"content": "Hello, world!"
},
finish_reason="stop"
)
],
created=1234567890,
model="gpt-3.5-turbo",
object="chat.completion"
)
# Create an instance of the adapter
adapter = GoogleGenAIAdapter()
# Test the adapter's translate_completion_to_generate_content method directly
result = adapter.translate_completion_to_generate_content(mock_response)
# Verify the result is a valid Google GenAI format response
assert "candidates" in result
assert isinstance(result["candidates"], list)
assert len(result["candidates"]) > 0
candidate = result["candidates"][0]
assert "content" in candidate
assert "parts" in candidate["content"]
assert isinstance(candidate["content"]["parts"], list)
assert len(candidate["content"]["parts"]) > 0
assert "text" in candidate["content"]["parts"][0]
assert candidate["content"]["parts"][0]["text"] == "Hello, world!"
def test_stream_response_when_stream_requested_sync():
"""
Test that when a stream response is returned and streaming was requested,
the sync handler correctly transforms it to generate_content streaming format.
"""
# Mock a stream response
mock_stream = MagicMock()
mock_stream.__iter__ = MagicMock(return_value=iter([]))
# Mock the GoogleGenAIAdapter's translate_completion_output_params_streaming method
with patch.object(
GoogleGenAIAdapter,
"translate_completion_output_params_streaming",
return_value=mock_stream
) as mock_translate:
with patch("litellm.completion", return_value=mock_stream):
# Call the handler with stream=True
result = GenerateContentToCompletionHandler.generate_content_handler(
model="gemini-pro",
contents=[{"role": "user", "parts": [{"text": "Hello"}]}],
litellm_params={}, # Empty dict for params
stream=True
)
# Verify that translate_completion_output_params_streaming was called
mock_translate.assert_called_once_with(mock_stream)
# Verify the result is the transformed stream
assert result == mock_stream
@pytest.mark.asyncio
async def test_stream_response_when_stream_requested_async():
"""
Test that when a stream response is returned and streaming was requested,
the async handler correctly transforms it to generate_content streaming format.
"""
# Mock a stream response
mock_stream = MagicMock()
mock_stream.__aiter__ = AsyncMock(return_value=iter([])) # Return an empty async iterator
# Mock the GoogleGenAIAdapter's translate_completion_output_params_streaming method
with patch.object(
GoogleGenAIAdapter,
"translate_completion_output_params_streaming",
return_value=mock_stream
) as mock_translate:
with patch("litellm.acompletion", return_value=mock_stream):
# Call the handler with stream=True
result = await GenerateContentToCompletionHandler.async_generate_content_handler(
model="gemini-pro",
contents=[{"role": "user", "parts": [{"text": "Hello"}]}],
litellm_params={}, # Empty dict for params
stream=True
)
# Verify that translate_completion_output_params_streaming was called
mock_translate.assert_called_once_with(mock_stream)
# Verify the result is the transformed stream
assert result == mock_stream
def test_stream_transformation_error_sync():
"""
Test that when a stream transformation fails, the sync handler raises a ValueError.
"""
# Mock a stream response
mock_stream = MagicMock()
mock_stream.__iter__ = MagicMock(return_value=iter([]))
# Mock the GoogleGenAIAdapter's translate_completion_output_params_streaming method to return None
with patch.object(
GoogleGenAIAdapter,
"translate_completion_output_params_streaming",
return_value=None
):
with patch("litellm.completion", return_value=mock_stream):
# Call the handler with stream=True and expect a ValueError
with pytest.raises(ValueError, match="Failed to transform streaming response"):
GenerateContentToCompletionHandler.generate_content_handler(
model="gemini-pro",
contents=[{"role": "user", "parts": [{"text": "Hello"}]}],
litellm_params={}, # Empty dict for params
stream=True
)
@pytest.mark.asyncio
async def test_stream_transformation_error_async():
"""
Test that when a stream transformation fails, the async handler raises a ValueError.
"""
# Mock a stream response
mock_stream = MagicMock()
mock_stream.__aiter__ = AsyncMock(return_value=mock_stream)
# Mock the GoogleGenAIAdapter's translate_completion_output_params_streaming method to return None
with patch.object(
GoogleGenAIAdapter,
"translate_completion_output_params_streaming",
return_value=None
):
with patch("litellm.acompletion", return_value=mock_stream):
# Call the handler with stream=True and expect a ValueError
with pytest.raises(ValueError, match="Failed to transform streaming response"):
await GenerateContentToCompletionHandler.async_generate_content_handler(
model="gemini-pro",
contents=[{"role": "user", "parts": [{"text": "Hello"}]}],
litellm_params={}, # Empty dict for params
stream=True
)
def test_citation_metadata_transformation():
"""
Test that citationMetadata.citationSources is properly transformed to citationMetadata.citations
to avoid Pydantic validation errors.
"""
from litellm.llms.gemini.google_genai.transformation import GoogleGenAIConfig
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from unittest.mock import MagicMock
import httpx
# Create a mock response with citationMetadata.citationSources (the problematic format)
mock_response_data = {
"candidates": [
{
"content": {
"parts": [
{
"text": "This is a video analysis response with citation metadata."
}
],
"role": "model"
},
"finishReason": "STOP",
"index": 0,
"safetyRatings": [],
"citationMetadata": {
"citationSources": [
{
"startIndex": 5848,
"endIndex": 5900,
"uri": "https://example.com/video-source",
"license": "MIT",
"title": "Video Analysis Source",
"publicationDate": "2024-01-15"
},
{
"startIndex": 6200,
"endIndex": 6250,
"uri": "https://another-source.com/reference",
"license": "CC-BY",
"title": "Another Reference",
"publicationDate": "2024-02-01"
}
]
}
}
],
"usageMetadata": {
"promptTokenCount": 150,
"candidatesTokenCount": 200,
"totalTokenCount": 350
},
"responseId": "test-response-123"
}
# Create mock httpx response
mock_httpx_response = MagicMock(spec=httpx.Response)
mock_httpx_response.json.return_value = mock_response_data
mock_httpx_response.status_code = 200
mock_httpx_response.headers = {}
# Create logging object
logging_obj = LiteLLMLoggingObj(
model="gemini-2.5-flash",
messages=[],
stream=False,
call_type="generate_content",
start_time=1234567890,
litellm_call_id="test-call-123",
function_id="test-function-123"
)
# Create GoogleGenAI config
config = GoogleGenAIConfig()
# Test the transformation
try:
result = config.transform_generate_content_response(
model="gemini-2.5-flash",
raw_response=mock_httpx_response,
logging_obj=logging_obj
)
# Verify the transformation worked
assert result is not None
# Check that citationSources was transformed to citations
if hasattr(result, 'candidates') and result.candidates:
candidate = result.candidates[0]
if hasattr(candidate, 'citationMetadata') and candidate.citationMetadata:
# The citationMetadata should now have 'citations' instead of 'citationSources'
citation_metadata = candidate.citationMetadata
# Check that citations field exists
assert hasattr(citation_metadata, 'citations'), "citations field should exist after transformation"
# Verify the citations data is preserved
if hasattr(citation_metadata, 'citations') and citation_metadata.citations:
assert len(citation_metadata.citations) == 2, "Should have 2 citations"
assert citation_metadata.citations[0]['uri'] == "https://example.com/video-source"
assert citation_metadata.citations[1]['uri'] == "https://another-source.com/reference"
print("✅ Citation metadata transformation test passed!")
except Exception as e:
pytest.fail(f"Citation metadata transformation failed: {e}")