diff --git a/python/packages/gemini/agent_framework_gemini/_chat_client.py b/python/packages/gemini/agent_framework_gemini/_chat_client.py index 0bc33c56a5..35dc7682fe 100644 --- a/python/packages/gemini/agent_framework_gemini/_chat_client.py +++ b/python/packages/gemini/agent_framework_gemini/_chat_client.py @@ -751,6 +751,14 @@ def _prepare_config( kwargs["response_mime_type"] = "application/json" if schema := options.get("response_schema"): kwargs["response_schema"] = schema + elif (response_format := options.get("response_format")) is not None: + # Forward cross-client ``response_format`` to Gemini's native ``response_schema`` + # so structured-output requests carry the schema, not just JSON mode. + # See https://github.com/microsoft/agent-framework/issues/5888. + if isinstance(response_format, type) and issubclass(response_format, BaseModel): + kwargs["response_schema"] = response_format + elif isinstance(response_format, Mapping): + kwargs["response_schema"] = dict(response_format) if tools := self._prepare_tools(options): kwargs["tools"] = tools if tool_config := self._prepare_tool_config(options.get("tool_choice")): diff --git a/python/packages/gemini/tests/test_gemini_client.py b/python/packages/gemini/tests/test_gemini_client.py index ab6cde241e..edcb5a5496 100644 --- a/python/packages/gemini/tests/test_gemini_client.py +++ b/python/packages/gemini/tests/test_gemini_client.py @@ -931,6 +931,64 @@ async def test_response_schema_added_to_config() -> None: assert config.response_schema == schema +async def test_response_format_dict_forwarded_as_response_schema() -> None: + """Raw JSON schema in response_format should reach Gemini's response_schema, not just JSON mode. + + Regression test for https://github.com/microsoft/agent-framework/issues/5888 — the + declarative loader hands the chat client ``response_format`` as a plain dict; without + this forwarding the model gets ``response_mime_type=application/json`` but no schema + and returns arbitrary JSON. + """ + client, mock = _make_gemini_client() + mock.aio.models.generate_content = AsyncMock(return_value=_make_response([_make_part(text="{}")])) + schema = {"type": "object", "properties": {"answer": {"type": "string"}}} + + await client.get_response( + messages=[Message(role="user", contents=[Content.from_text("Hi")])], + options={"response_format": schema}, + ) + + config: types.GenerateContentConfig = mock.aio.models.generate_content.call_args.kwargs["config"] + assert config.response_mime_type == "application/json" + assert config.response_schema == schema + + +async def test_response_format_pydantic_forwarded_as_response_schema() -> None: + """Pydantic ``response_format`` should also surface on Gemini's ``response_schema``.""" + from pydantic import BaseModel + + class Reply(BaseModel): + text: str + + client, mock = _make_gemini_client() + mock.aio.models.generate_content = AsyncMock(return_value=_make_response([_make_part(text="{}")])) + + await client.get_response( + messages=[Message(role="user", contents=[Content.from_text("Hi")])], + options={"response_format": Reply}, + ) + + config: types.GenerateContentConfig = mock.aio.models.generate_content.call_args.kwargs["config"] + assert config.response_mime_type == "application/json" + assert config.response_schema is Reply + + +async def test_response_schema_takes_precedence_over_response_format() -> None: + """If both are set, the explicit ``response_schema`` wins over ``response_format``.""" + client, mock = _make_gemini_client() + mock.aio.models.generate_content = AsyncMock(return_value=_make_response([_make_part(text="{}")])) + explicit_schema = {"type": "object", "properties": {"score": {"type": "integer"}}} + fallback = {"type": "object", "properties": {"answer": {"type": "string"}}} + + await client.get_response( + messages=[Message(role="user", contents=[Content.from_text("Hi")])], + options={"response_schema": explicit_schema, "response_format": fallback}, + ) + + config: types.GenerateContentConfig = mock.aio.models.generate_content.call_args.kwargs["config"] + assert config.response_schema == explicit_schema + + async def test_streaming_response_format_passed_to_build_response_stream() -> None: """Verifies that response_format is forwarded to _build_response_stream when streaming so that structured output parsing works correctly on the final assembled response.