Skip to content

output_schema is silently dropped when Gemini uses the Interactions API (use_interactions_api=True) #7870

Description

@himanshu096

🔴 Required Information

Describe the Bug:
When an LlmAgent has output_schema and its model is Gemini(use_interactions_api=True), the schema never reaches the request. ADK puts it in llm_request.config.response_schema, but the Interactions request builder doesn't read that field. The model's output is unconstrained, and nothing raises an error or logs a warning.

This applies to:

  • Agents with output_schema and no tools, on Vertex AI and on the Gemini Developer API.
  • Agents with output_schema and tools, on Vertex AI. Gemini.capabilities.output_schema_and_tools is True on Vertex (_capabilities.py#L48-L51). That makes basic.py call set_output_schema() (basic.py#L138-L143), and the set_model_response fallback is skipped (_schema.py#L67-L72). The schema goes into config.response_schema, and the Interactions path drops it there too.

Only one combination keeps its schema: tools on the Gemini Developer API. There the capability is False, so set_model_response is added as a function tool.

Where the schema is dropped (src/google/adk/models/interactions_utils.py on main):

  • generate_content_via_interactions builds create_kwargs from only model, input, system_instruction, tools, generation_config and previous_interaction_id, plus service_tier/background (L1932-L1947).
  • build_generation_config copies only max_output_tokens, stop_sequences, seed and the sampling params (L1373-L1403).
  • config.response_schema, config.response_json_schema and config.response_mime_type are never read, and no response_format is sent. The schema also doesn't appear in the system instruction.

The Interactions API does support this. In google-genai 2.31.0, interactions.create() declares response_format (a ResponseFormatParam or a list of them) and response_mime_type. TextResponseFormatParam has the fields type: "text", mime_type and schema_ (schema on the wire). For example:

"response_format": {"type": "text", "mime_type": "application/json", "schema": {...}}

Steps to Reproduce:
The repro below runs offline. It swaps in a fake client that records the kwargs passed to aio.interactions.create() and then raises.

  1. Install: pip install google-adk==2.13.0 google-genai==2.31.0
  2. Save the script below as repro.py.
  3. Run GOOGLE_GENAI_USE_VERTEXAI=1 GOOGLE_CLOUD_PROJECT=p GOOGLE_CLOUD_LOCATION=global python repro.py (no tool), then the same command with a tool argument (adds a function tool).
  4. Run GOOGLE_GENAI_USE_VERTEXAI=0 GOOGLE_API_KEY=x python repro.py tool to compare with the Developer API.

Expected Behavior:
The Interactions API enforces an LlmAgent's output_schema the same way generateContent does. config.response_schema / response_json_schema (with response_mime_type="application/json") should be sent as response_format.

Observed Behavior:
create() receives no response_format, and the schema's field names appear nowhere in the request:

# Vertex, output_schema, no tools
keys sent to interactions.create(): ['extra_headers', 'input', 'model', 'stream', 'system_instruction']
generation_config: None
response_format sent: False
schema field 'reason' anywhere in request: False

# Vertex, output_schema + one function tool
keys sent to interactions.create(): ['extra_headers', 'input', 'model', 'stream', 'system_instruction', 'tools']
tools sent: ['lookup']
response_format sent: False
schema field 'reason' anywhere in request: False

# Developer API, output_schema + one function tool (for comparison: the fallback applies)
tools sent: ['set_model_response', 'lookup']
response_format sent: False
schema field 'reason' anywhere in request: True

We found this after turning on the Interactions API in production on Vertex. As a workaround, we moved every agent with output_schema or tools back to generateContent (use_interactions_api=False).

Environment Details:

  • ADK Library Version: google-adk 2.13.0 (latest on PyPI as of 2026-10-30). Code paths above are unchanged on main @ 73b6790.
  • google-genai: 2.31.0 (also checked 2.34.0: no change)
  • Desktop OS: macOS (repro); production on Cloud Run (Linux)
  • Python Version: 3.13

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: Gemini 3.x on Vertex AI / Gemini Enterprise Agent Platform, global endpoint, non-streaming. The repro uses gemini-3.6-flash, but the request builder doesn't depend on the model.

🟡 Optional Information

Regression:
No. As far as we can tell, the Interactions path has never forwarded the schema.

Additional Context:

  • We checked main. The latest commit to interactions_utils.py, a9cabe7 (2026-10-30), is about thought signatures. In src/, response_format appears only in the OpenAI, OCI, LiteLLM and Apigee connectors.
  • tests/unittests/models/test_interactions_utils.py has no coverage for output_schema or response_schema.
  • contributing/samples/models/interactions_api uses tools and no output_schema.
  • We found no existing issue (searched "use_interactions_api", "interactions output_schema", "interactions response_format", "interactions response_schema").

Suggested fix:
In generate_content_via_interactions, when config.response_schema or config.response_json_schema is set, send:

  • response_format={"type": "text", "mime_type": "application/json", "schema": <JSON schema>}
  • response_mime_type="application/json", which the google-genai docstring says is required when response_format is set

Things to watch for:

  • The API silently ignores some JSON Schema keywords (e.g. minLength, maxLength, uniqueItems) and returns 400 for overly complex schemas. Logging a warning, or documenting this, would help.
  • A unit test in test_interactions_utils.py should assert that response_format is present for an agent with output_schema.
  • Until this is fixed, either log a warning when response_schema is set on the Interactions path, or fall back to set_model_response, so the schema isn't dropped silently.

Related (separate from this bug, for docs):

  • On the Interactions API, a thinking model's reply can start with a thought part that carries only a signature and no text, so content.parts[0].text is None. That's expected behavior, but callers that read parts[0].text break when they switch to the Interactions API. A docs note would help.
  • The Interactions API returns 400 when a function result contains an empty list. Separately, Gemini(use_interactions_api=True) streaming does not coalesce consecutive text/thought delta Parts #7468 reported a streaming issue (closed as stale).

Minimal Reproduction Code:

import asyncio, json, sys
from types import SimpleNamespace
from pydantic import BaseModel
from google.adk.agents import LlmAgent
from google.adk.models import Gemini
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types

class Verdict(BaseModel):
    ok: bool
    reason: str

captured = {}

class _FakeInteractions:
    async def create(self, **kwargs):
        captured.update(kwargs)
        raise RuntimeError("captured; stop before network")

class _FakeClient:
    aio = SimpleNamespace(interactions=_FakeInteractions())
    vertexai = True

class RecordingGemini(Gemini):
    @property
    def api_client(self):
        return _FakeClient()

def lookup(q: str) -> str:
    """Looks something up."""
    return "x"

async def main(with_tool: bool):
    agent = LlmAgent(
        name="schema_probe",
        model=RecordingGemini(model="gemini-3.6-flash", use_interactions_api=True),
        instruction="Judge it.",
        output_schema=Verdict,
        tools=[lookup] if with_tool else [],
    )
    sessions = InMemorySessionService()
    runner = Runner(app_name="t", agent=agent, session_service=sessions)
    s = await sessions.create_session(app_name="t", user_id="u")
    msg = types.Content(role="user", parts=[types.Part.from_text(text="hi")])
    try:
        async for _ in runner.run_async(user_id="u", session_id=s.id, new_message=msg):
            pass
    except Exception:
        pass
    print("keys sent to interactions.create():", sorted(k for k, v in captured.items() if v is not None))
    print("tools sent:", [t.get("name") for t in (captured.get("tools") or [])])
    print("generation_config:", captured.get("generation_config"))
    print("response_format sent:", "response_format" in captured)
    print("schema field 'reason' anywhere in request:", "reason" in json.dumps(captured, default=str))

asyncio.run(main(with_tool=len(sys.argv) > 1))

How often has this issue occurred?:

  • Always (100%)

Activity

  1. himanshu096 commented on Oct 30, 2026

    @himanshu096
    Author

    I've sent a fix with unit tests and a live check on Vertex AI: #7871

  2. added 2 commits that reference this issue on Oct 30, 2026
    8a07937
    85fba8f
  3. llalitkumarrr commented on Nov 4, 2026

    @llalitkumarrr
    Collaborator

    Hello @himanshu096,

    Thank you for reporting this and for your contribution. Your PR is currently under review and will be merged once it is approved by the reviewer, in the meantime we appreciate your patience. We will reach out if we require any further information.

  4. added theissue type on Nov 4, 2026
  5. added
    core[Component] This issue is related to the core interface and implementation
    on Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

core[Component] This issue is related to the core interface and implementation

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions