From ed6c66c20c3d2603e19b8f42dfd4c93b3645222a Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Thu, 11 Dec 2025 21:29:23 +0530 Subject: [PATCH] Add support for structured output thinkingConfig param --- .../gemini/google_genai/transformation.py | 26 ++- .../test_google_genai_transformation.py | 171 ++++++++++++++++++ 2 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 tests/test_litellm/google_genai/test_google_genai_transformation.py diff --git a/litellm/llms/gemini/google_genai/transformation.py b/litellm/llms/gemini/google_genai/transformation.py index bc32aca655..335f4d5b0a 100644 --- a/litellm/llms/gemini/google_genai/transformation.py +++ b/litellm/llms/gemini/google_genai/transformation.py @@ -75,6 +75,7 @@ class GoogleGenAIConfig(BaseGoogleGenAIGenerateContentConfig, VertexLLM): "seed", "response_mime_type", "response_schema", + "response_json_schema", "routing_config", "model_selection_config", "safety_settings", @@ -105,13 +106,34 @@ class GoogleGenAIConfig(BaseGoogleGenAIGenerateContentConfig, VertexLLM): Returns: Mapped parameters for the provider """ + from litellm.llms.vertex_ai.gemini.transformation import _snake_to_camel, _camel_to_snake + _generate_content_config_dict: Dict[str, Any] = {} supported_google_genai_params = ( self.get_supported_generate_content_optional_params(model) ) + # Create a set with both camelCase and snake_case versions for faster lookup + supported_params_set = set(supported_google_genai_params) + supported_params_set.update(_snake_to_camel(p) for p in supported_google_genai_params) + supported_params_set.update(_camel_to_snake(p) for p in supported_google_genai_params if "_" not in p) + for param, value in generate_content_config_dict.items(): - if param in supported_google_genai_params: - _generate_content_config_dict[param] = value + # Google GenAI API expects camelCase, so we'll always output in camelCase + # Check if param (or its variants) is supported + param_snake = _camel_to_snake(param) + param_camel = _snake_to_camel(param) + + # Check if param is supported in any format + is_supported = ( + param in supported_google_genai_params or + param_snake in supported_google_genai_params or + param_camel in supported_google_genai_params + ) + + if is_supported: + # Always output in camelCase for Google GenAI API + output_key = param_camel if param != param_camel else param + _generate_content_config_dict[output_key] = value return _generate_content_config_dict def validate_environment( diff --git a/tests/test_litellm/google_genai/test_google_genai_transformation.py b/tests/test_litellm/google_genai/test_google_genai_transformation.py new file mode 100644 index 0000000000..3c71ac45c5 --- /dev/null +++ b/tests/test_litellm/google_genai/test_google_genai_transformation.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +Test to verify the Google GenAI transformation logic for generateContent parameters +""" +import os +import sys + +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system path + +import pytest + +from litellm.llms.gemini.google_genai.transformation import GoogleGenAIConfig + + +def test_map_generate_content_optional_params_response_json_schema_camelcase(): + """Test that responseJsonSchema (camelCase) is passed through correctly""" + config = GoogleGenAIConfig() + + generate_content_config_dict = { + "responseJsonSchema": { + "type": "object", + "properties": { + "recipe_name": {"type": "string"} + } + }, + "temperature": 1.0 + } + + result = config.map_generate_content_optional_params( + generate_content_config_dict=generate_content_config_dict, + model="gemini/skyhawk" + ) + + # responseJsonSchema should be in the result (camelCase format for Google GenAI API) + assert "responseJsonSchema" in result + assert result["responseJsonSchema"] == generate_content_config_dict["responseJsonSchema"] + assert "temperature" in result + assert result["temperature"] == 1.0 + + +def test_map_generate_content_optional_params_response_schema_snakecase(): + """Test that response_schema (snake_case) is converted to responseJsonSchema (camelCase)""" + config = GoogleGenAIConfig() + + generate_content_config_dict = { + "response_json_schema": { + "type": "object", + "properties": { + "recipe_name": {"type": "string"} + } + }, + "temperature": 1.0 + } + + result = config.map_generate_content_optional_params( + generate_content_config_dict=generate_content_config_dict, + model="gemini/skyhawk" + ) + + # response_schema should be converted to responseJsonSchema (camelCase) + assert "responseJsonSchema" in result + assert result["responseJsonSchema"] == generate_content_config_dict["response_json_schema"] + assert "temperature" in result + + +def test_map_generate_content_optional_params_thinking_config_camelcase(): + """Test that thinkingConfig (camelCase) is passed through correctly""" + config = GoogleGenAIConfig() + + generate_content_config_dict = { + "thinkingConfig": { + "thinkingLevel": "minimal", + "includeThoughts": True + }, + "temperature": 1.0 + } + + result = config.map_generate_content_optional_params( + generate_content_config_dict=generate_content_config_dict, + model="gemini/skyhawk" + ) + + # thinkingConfig should be in the result (camelCase format for Google GenAI API) + assert "thinkingConfig" in result + assert result["thinkingConfig"]["thinkingLevel"] == "minimal" + assert result["thinkingConfig"]["includeThoughts"] is True + assert "temperature" in result + + +def test_map_generate_content_optional_params_thinking_config_snakecase(): + """Test that thinking_config (snake_case) is converted to thinkingConfig (camelCase)""" + config = GoogleGenAIConfig() + + generate_content_config_dict = { + "thinking_config": { + "thinkingLevel": "medium", + "includeThoughts": True + }, + "temperature": 1.0 + } + + result = config.map_generate_content_optional_params( + generate_content_config_dict=generate_content_config_dict, + model="gemini/skyhawk" + ) + + # thinking_config should be converted to thinkingConfig (camelCase) + assert "thinkingConfig" in result + assert result["thinkingConfig"]["thinkingLevel"] == "medium" + assert result["thinkingConfig"]["includeThoughts"] is True + assert "thinking_config" not in result # Should not be in snake_case format + assert "temperature" in result + + +def test_map_generate_content_optional_params_mixed_formats(): + """Test that both camelCase and snake_case parameters work together""" + config = GoogleGenAIConfig() + + generate_content_config_dict = { + "responseJsonSchema": { + "type": "object", + "properties": { + "recipe_name": {"type": "string"} + } + }, + "thinking_config": { + "thinkingLevel": "low", + "includeThoughts": True + }, + "temperature": 1.0, + "max_output_tokens": 100 + } + + result = config.map_generate_content_optional_params( + generate_content_config_dict=generate_content_config_dict, + model="gemini/skyhawk" + ) + + # All parameters should be converted to camelCase + assert "responseJsonSchema" in result + assert "thinkingConfig" in result + assert result["thinkingConfig"]["thinkingLevel"] == "low" + assert "temperature" in result + assert "maxOutputTokens" in result # This one stays as-is if it's in supported list + + +def test_map_generate_content_optional_params_response_mime_type(): + """Test that responseMimeType is handled correctly""" + config = GoogleGenAIConfig() + + generate_content_config_dict = { + "responseMimeType": "application/json", + "responseJsonSchema": { + "type": "object", + "properties": { + "recipe_name": {"type": "string"} + } + } + } + + result = config.map_generate_content_optional_params( + generate_content_config_dict=generate_content_config_dict, + model="gemini/skyhawk" + ) + + # responseMimeType should be passed through (it's already camelCase) + assert "responseMimeType" in result or "response_mime_type" in result + assert "responseJsonSchema" in result +