@@ -2429,15 +2429,16 @@ def __init__(self):
24292429 # thinking_config must NOT be in generation_config
24302430 assert "thinking_config" not in gen_config
24312431
2432- def test_structured_chat_uses_default_genai_tools_mode (self ):
2433- """Instructor client uses default GENAI_TOOLS mode, not GENAI_STRUCTURED_OUTPUTS .
2432+ def test_structured_chat_uses_genai_structured_outputs_mode (self ):
2433+ """Instructor client uses GENAI_STRUCTURED_OUTPUTS mode.
24342434
2435- GENAI_STRUCTURED_OUTPUTS passes Pydantic schemas directly to Gemini's native
2436- JSON schema output, which rejects additionalProperties (emitted by Pydantic v2 ).
2437- GENAI_TOOLS uses map_to_genai_schema() which strips unsupported fields .
2435+ GENAI_TOOLS has upstream bugs with thinking mode (AttributeError on
2436+ MALFORMED_FUNCTION_CALL, AssertionError on duplicate parts ).
2437+ GENAI_STRUCTURED_OUTPUTS parses via completion.text, avoiding both .
24382438 """
24392439 pytest .importorskip ("google.genai" )
24402440 pytest .importorskip ("instructor" )
2441+ import instructor
24412442 from maseval .interface .inference .google_genai import GoogleGenAIModelAdapter
24422443
24432444 class MockClient :
@@ -2465,20 +2466,62 @@ def __init__(self):
24652466 )
24662467
24672468 mock_from_genai .assert_called_once ()
2468- # Must NOT pass mode=GENAI_STRUCTURED_OUTPUTS — only GENAI_TOOLS
2469- # handles additionalProperties in Pydantic schemas correctly.
2470- _ , call_kwargs = mock_from_genai .call_args
2471- assert "mode" not in call_kwargs
2469+ call_kwargs = mock_from_genai .call_args
2470+ assert call_kwargs .kwargs .get ("mode" ) == instructor .Mode .GENAI_STRUCTURED_OUTPUTS
24722471
2473- def test_structured_chat_with_dict_field_model (self ):
2474- """Response models with Dict[str, Any] fields (like ToolSimulatorResponse) work .
2472+ def test_clean_schema_model_strips_additional_properties (self ):
2473+ """_clean_schema_model strips additionalProperties from JSON schema .
24752474
2476- Dict fields cause Pydantic v2 to emit additionalProperties in the JSON schema.
2477- This must not be passed raw to the Gemini API.
2475+ Pydantic v2 emits additionalProperties for Dict fields and nested models.
2476+ Gemini's structured output API rejects it. The clean model must strip it
2477+ while preserving parsing and isinstance() behavior.
24782478 """
24792479 pytest .importorskip ("google.genai" )
24802480 from maseval .interface .inference .google_genai import GoogleGenAIModelAdapter
24812481
2482+ from typing import Any , Dict
2483+
2484+ from pydantic import BaseModel , Field
2485+
2486+ class Inner (BaseModel ):
2487+ name : str
2488+
2489+ class Outer (BaseModel ):
2490+ text : str = Field (default = "" )
2491+ details : Dict [str , Any ] = Field (default_factory = dict )
2492+ inner : Inner = Field (default_factory = lambda : Inner (name = "" ))
2493+
2494+ # Original schema has additionalProperties
2495+ original_schema = Outer .model_json_schema ()
2496+ assert any ("additionalProperties" in str (v ) for v in original_schema .values ()) or "additionalProperties" in str (
2497+ original_schema .get ("$defs" , {})
2498+ )
2499+
2500+ # Cleaned model's schema does not
2501+ CleanOuter = GoogleGenAIModelAdapter ._clean_schema_model (Outer )
2502+ clean_schema = CleanOuter .model_json_schema () # ty: ignore[unresolved-attribute]
2503+ assert "additionalProperties" not in str (clean_schema )
2504+
2505+ # Parsing still works and returns instances of the original class
2506+ instance = CleanOuter .model_validate ({"text" : "hi" , "details" : {"k" : "v" }, "inner" : {"name" : "test" }}) # ty: ignore[unresolved-attribute]
2507+ assert isinstance (instance , Outer )
2508+ assert instance .text == "hi"
2509+ assert instance .details == {"k" : "v" }
2510+ assert instance .inner .name == "test"
2511+
2512+ def test_structured_chat_passes_clean_model_to_instructor (self ):
2513+ """_structured_chat wraps the response model to strip additionalProperties."""
2514+ pytest .importorskip ("google.genai" )
2515+ from maseval .interface .inference .google_genai import GoogleGenAIModelAdapter
2516+
2517+ from typing import Any , Dict
2518+
2519+ from pydantic import BaseModel , Field
2520+
2521+ class ToolOutput (BaseModel ):
2522+ text : str = Field (default = "" , description = "Description" )
2523+ details : Dict [str , Any ] = Field (default_factory = dict , description = "Structured data" )
2524+
24822525 class MockClient :
24832526 class Models :
24842527 def generate_content (self , model , contents , config = None ):
@@ -2497,27 +2540,21 @@ def __init__(self):
24972540 mock_instructor .chat .completions .create .return_value = mock_result
24982541 adapter ._instructor_client = mock_instructor
24992542
2500- # Use a model with Dict[str, Any] — same pattern as ToolSimulatorResponse
2501- from typing import Any , Dict
2502-
2503- from pydantic import BaseModel , Field
2504-
2505- class ToolOutput (BaseModel ):
2506- text : str = Field (default = "" , description = "Description" )
2507- details : Dict [str , Any ] = Field (default_factory = dict , description = "Structured data" )
2508-
2509- response = adapter ._structured_chat (
2543+ adapter ._structured_chat (
25102544 messages = [{"role" : "user" , "content" : "Hi" }],
25112545 response_model = ToolOutput ,
25122546 generation_params = {"temperature" : 0.5 },
25132547 )
25142548
2515- # Verify the call went through with correct kwargs structure
25162549 call_kwargs = mock_instructor .chat .completions .create .call_args .kwargs
2517- assert call_kwargs ["response_model" ] is ToolOutput
2550+ passed_model = call_kwargs ["response_model" ]
2551+ # Must be a subclass (not the original) with clean schema
2552+ assert passed_model is not ToolOutput
2553+ assert issubclass (passed_model , ToolOutput )
2554+ assert "additionalProperties" not in str (passed_model .model_json_schema ())
2555+ # Generation params still wrapped correctly
25182556 gen_config = call_kwargs .get ("generation_config" , {})
25192557 assert gen_config .get ("temperature" ) == 0.5
2520- assert response .structured_response is mock_result
25212558
25222559
25232560@pytest .mark .interface
0 commit comments