mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-11 17:05:43 +00:00
3852fc96c1
* 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>
328 lines
12 KiB
Python
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}") |