Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions python/packages/gemini/agent_framework_gemini/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")):
Expand Down
58 changes: 58 additions & 0 deletions python/packages/gemini/tests/test_gemini_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading