From ceff510c7bc741763a665f33d5fc00cc604e983c Mon Sep 17 00:00:00 2001 From: RohanDisa <105740583+RohanDisa@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:47:39 -0800 Subject: [PATCH 1/2] fix(genai): Add ProviderStrategy response_format support - Add handling for response_format parameter from ProviderStrategy --- .../langchain_google_genai/chat_models.py | 24 +++- .../tests/unit_tests/test_chat_models.py | 105 ++++++++++++++++++ 2 files changed, 126 insertions(+), 3 deletions(-) diff --git a/libs/genai/langchain_google_genai/chat_models.py b/libs/genai/langchain_google_genai/chat_models.py index 7917b5216..765ed82ba 100644 --- a/libs/genai/langchain_google_genai/chat_models.py +++ b/libs/genai/langchain_google_genai/chat_models.py @@ -2220,14 +2220,23 @@ def _prepare_params( gen_config = {**gen_config, **generation_config} response_mime_type = kwargs.get("response_mime_type", self.response_mime_type) - if response_mime_type is not None: - gen_config["response_mime_type"] = response_mime_type - response_schema = kwargs.get("response_schema", self.response_schema) # In case passed in as a direct kwarg response_json_schema = kwargs.get("response_json_schema") + # Handle response_format from ProviderStrategy (OpenAI-style format) + # Format: {'type': 'json_schema', 'json_schema': {'schema': {...}}} + # Only extract from response_format if response_json_schema is not already set + if response_json_schema is None: + response_format = kwargs.get("response_format") + if response_format is not None and isinstance(response_format, dict): + if response_format.get("type") == "json_schema": + json_schema_obj = response_format.get("json_schema", {}) + if isinstance(json_schema_obj, dict) and "schema" in json_schema_obj: + # Extract the actual schema from the nested structure + response_json_schema = json_schema_obj["schema"] + # Handle both response_schema and response_json_schema # (Regardless, we use `response_json_schema` in the request) schema_to_use = ( @@ -2236,6 +2245,15 @@ def _prepare_params( else response_schema ) + # Automatically set response_mime_type to "application/json" when response_schema + # is provided but response_mime_type is not set. This enables seamless support + # for structured output strategies like ProviderStrategy. + if schema_to_use is not None and response_mime_type is None: + response_mime_type = "application/json" + + if response_mime_type is not None: + gen_config["response_mime_type"] = response_mime_type + if schema_to_use is not None: if response_mime_type != "application/json": param_name = ( diff --git a/libs/genai/tests/unit_tests/test_chat_models.py b/libs/genai/tests/unit_tests/test_chat_models.py index 46945d946..405692d12 100644 --- a/libs/genai/tests/unit_tests/test_chat_models.py +++ b/libs/genai/tests/unit_tests/test_chat_models.py @@ -10,6 +10,7 @@ from unittest.mock import ANY, AsyncMock, Mock, patch import google.ai.generativelanguage as glm +import proto # type: ignore[import-untyped] import pytest from google.ai.generativelanguage_v1beta.types import ( Candidate, @@ -2957,6 +2958,110 @@ def test_response_schema_mime_type_validation() -> None: assert llm_with_json_schema is not None +def _convert_proto_to_dict(obj: Any) -> Any: + """Recursively convert proto objects to dicts for comparison.""" + if isinstance(obj, dict): + return {k: _convert_proto_to_dict(v) for k, v in obj.items()} + elif isinstance(obj, (list, tuple)): + return [_convert_proto_to_dict(item) for item in obj] + elif hasattr(obj, "__class__") and "proto" in str(type(obj)): + # Try to convert proto object to dict + try: + if hasattr(obj, "__iter__") and not isinstance(obj, str): + converted = dict(obj) + # Recursively convert nested proto objects + return {k: _convert_proto_to_dict(v) for k, v in converted.items()} + else: + return obj + except (TypeError, ValueError): + return obj + return obj + + +def test_response_format_provider_strategy() -> None: + """Test that `response_format` from ProviderStrategy is correctly handled.""" + llm = ChatGoogleGenerativeAI( + model=MODEL_NAME, google_api_key=SecretStr(FAKE_API_KEY) + ) + + schema_dict = { + "type": "object", + "properties": { + "sentiment": { + "type": "string", + "enum": ["positive", "negative", "neutral"], + }, + "confidence": {"type": "number", "minimum": 0, "maximum": 1}, + }, + "required": ["sentiment", "confidence"], + "additionalProperties": False, + } + + # Test response_format with ProviderStrategy format (OpenAI-style) + response_format = { + "type": "json_schema", + "json_schema": { + "name": "response_format_test", + "schema": schema_dict, + }, + } + + gen_config = llm._prepare_params(stop=None, response_format=response_format) + + # response_json_schema may be converted to proto object, so convert to dict for comparison + schema = _convert_proto_to_dict(gen_config.response_json_schema) + assert schema == schema_dict + assert gen_config.response_mime_type == "application/json" + + # Test that response_json_schema takes precedence over response_format + different_schema = { + "type": "object", + "properties": {"age": {"type": "integer"}}, + "required": ["age"], + } + + gen_config_2 = llm._prepare_params( + stop=None, + response_format=response_format, + response_json_schema=different_schema, + ) + + # response_json_schema may be converted to proto object, so convert to dict for comparison + schema_2 = _convert_proto_to_dict(gen_config_2.response_json_schema) + assert schema_2 == different_schema + assert gen_config_2.response_mime_type == "application/json" + + + old_schema = { + "type": "object", + "properties": {"old_field": {"type": "string"}}, + "required": ["old_field"], + } + + gen_config_3 = llm._prepare_params( + stop=None, + response_schema=old_schema, + response_format=response_format, + ) + + # response_json_schema may be converted to proto object, so convert to dict for comparison + schema_3 = _convert_proto_to_dict(gen_config_3.response_json_schema) + assert schema_3 == schema_dict + assert gen_config_3.response_mime_type == "application/json" + + invalid_response_format = {"type": "invalid_type"} + gen_config_4 = llm._prepare_params( + stop=None, + response_format=invalid_response_format, + response_schema=schema_dict, + ) + # Should fall back to response_schema + # response_json_schema may be converted to proto object, so convert to dict for comparison + schema_4 = _convert_proto_to_dict(gen_config_4.response_json_schema) + assert schema_4 == schema_dict + assert gen_config_4.response_mime_type == "application/json" + + def test_is_new_gemini_model() -> None: assert _is_gemini_3_or_later("gemini-3.0-pro") is True assert _is_gemini_3_or_later("gemini-2.5-pro") is False From 71a723932646585c3c00bf1b457b1f071a4cc319 Mon Sep 17 00:00:00 2001 From: RohanDisa <105740583+RohanDisa@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:47:39 -0800 Subject: [PATCH 2/2] fix(genai): Add ProviderStrategy response_format support - Add handling for response_format parameter from ProviderStrategy --- .../langchain_google_genai/chat_models.py | 28 ++++- .../tests/unit_tests/test_chat_models.py | 105 ++++++++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/libs/genai/langchain_google_genai/chat_models.py b/libs/genai/langchain_google_genai/chat_models.py index 7917b5216..0f6e94d50 100644 --- a/libs/genai/langchain_google_genai/chat_models.py +++ b/libs/genai/langchain_google_genai/chat_models.py @@ -2220,14 +2220,26 @@ def _prepare_params( gen_config = {**gen_config, **generation_config} response_mime_type = kwargs.get("response_mime_type", self.response_mime_type) - if response_mime_type is not None: - gen_config["response_mime_type"] = response_mime_type - response_schema = kwargs.get("response_schema", self.response_schema) # In case passed in as a direct kwarg response_json_schema = kwargs.get("response_json_schema") + # Handle response_format from ProviderStrategy (OpenAI-style format) + # Format: {'type': 'json_schema', 'json_schema': {'schema': {...}}} + # Only extract from response_format if response_json_schema is not already set + if response_json_schema is None: + response_format = kwargs.get("response_format") + if response_format is not None and isinstance(response_format, dict): + if response_format.get("type") == "json_schema": + json_schema_obj = response_format.get("json_schema", {}) + if ( + isinstance(json_schema_obj, dict) + and "schema" in json_schema_obj + ): + # Extract the actual schema from the nested structure + response_json_schema = json_schema_obj["schema"] + # Handle both response_schema and response_json_schema # (Regardless, we use `response_json_schema` in the request) schema_to_use = ( @@ -2236,6 +2248,16 @@ def _prepare_params( else response_schema ) + # Automatically set response_mime_type to "application/json" when + # response_schema is provided but response_mime_type is not set. This + # enables seamless support for structured output strategies like + # ProviderStrategy. + if schema_to_use is not None and response_mime_type is None: + response_mime_type = "application/json" + + if response_mime_type is not None: + gen_config["response_mime_type"] = response_mime_type + if schema_to_use is not None: if response_mime_type != "application/json": param_name = ( diff --git a/libs/genai/tests/unit_tests/test_chat_models.py b/libs/genai/tests/unit_tests/test_chat_models.py index 46945d946..405692d12 100644 --- a/libs/genai/tests/unit_tests/test_chat_models.py +++ b/libs/genai/tests/unit_tests/test_chat_models.py @@ -10,6 +10,7 @@ from unittest.mock import ANY, AsyncMock, Mock, patch import google.ai.generativelanguage as glm +import proto # type: ignore[import-untyped] import pytest from google.ai.generativelanguage_v1beta.types import ( Candidate, @@ -2957,6 +2958,110 @@ def test_response_schema_mime_type_validation() -> None: assert llm_with_json_schema is not None +def _convert_proto_to_dict(obj: Any) -> Any: + """Recursively convert proto objects to dicts for comparison.""" + if isinstance(obj, dict): + return {k: _convert_proto_to_dict(v) for k, v in obj.items()} + elif isinstance(obj, (list, tuple)): + return [_convert_proto_to_dict(item) for item in obj] + elif hasattr(obj, "__class__") and "proto" in str(type(obj)): + # Try to convert proto object to dict + try: + if hasattr(obj, "__iter__") and not isinstance(obj, str): + converted = dict(obj) + # Recursively convert nested proto objects + return {k: _convert_proto_to_dict(v) for k, v in converted.items()} + else: + return obj + except (TypeError, ValueError): + return obj + return obj + + +def test_response_format_provider_strategy() -> None: + """Test that `response_format` from ProviderStrategy is correctly handled.""" + llm = ChatGoogleGenerativeAI( + model=MODEL_NAME, google_api_key=SecretStr(FAKE_API_KEY) + ) + + schema_dict = { + "type": "object", + "properties": { + "sentiment": { + "type": "string", + "enum": ["positive", "negative", "neutral"], + }, + "confidence": {"type": "number", "minimum": 0, "maximum": 1}, + }, + "required": ["sentiment", "confidence"], + "additionalProperties": False, + } + + # Test response_format with ProviderStrategy format (OpenAI-style) + response_format = { + "type": "json_schema", + "json_schema": { + "name": "response_format_test", + "schema": schema_dict, + }, + } + + gen_config = llm._prepare_params(stop=None, response_format=response_format) + + # response_json_schema may be converted to proto object, so convert to dict for comparison + schema = _convert_proto_to_dict(gen_config.response_json_schema) + assert schema == schema_dict + assert gen_config.response_mime_type == "application/json" + + # Test that response_json_schema takes precedence over response_format + different_schema = { + "type": "object", + "properties": {"age": {"type": "integer"}}, + "required": ["age"], + } + + gen_config_2 = llm._prepare_params( + stop=None, + response_format=response_format, + response_json_schema=different_schema, + ) + + # response_json_schema may be converted to proto object, so convert to dict for comparison + schema_2 = _convert_proto_to_dict(gen_config_2.response_json_schema) + assert schema_2 == different_schema + assert gen_config_2.response_mime_type == "application/json" + + + old_schema = { + "type": "object", + "properties": {"old_field": {"type": "string"}}, + "required": ["old_field"], + } + + gen_config_3 = llm._prepare_params( + stop=None, + response_schema=old_schema, + response_format=response_format, + ) + + # response_json_schema may be converted to proto object, so convert to dict for comparison + schema_3 = _convert_proto_to_dict(gen_config_3.response_json_schema) + assert schema_3 == schema_dict + assert gen_config_3.response_mime_type == "application/json" + + invalid_response_format = {"type": "invalid_type"} + gen_config_4 = llm._prepare_params( + stop=None, + response_format=invalid_response_format, + response_schema=schema_dict, + ) + # Should fall back to response_schema + # response_json_schema may be converted to proto object, so convert to dict for comparison + schema_4 = _convert_proto_to_dict(gen_config_4.response_json_schema) + assert schema_4 == schema_dict + assert gen_config_4.response_mime_type == "application/json" + + def test_is_new_gemini_model() -> None: assert _is_gemini_3_or_later("gemini-3.0-pro") is True assert _is_gemini_3_or_later("gemini-2.5-pro") is False