Skip to content

fix(google): reject tool calls when tool_choice="none" in realtime#6166

Open
longcw wants to merge 1 commit into
mainfrom
longc/gemini-realtime-reject-tool-when-none
Open

fix(google): reject tool calls when tool_choice="none" in realtime#6166
longcw wants to merge 1 commit into
mainfrom
longc/gemini-realtime-reject-tool-when-none

Conversation

@longcw

@longcw longcw commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Closes #6002

The Google Realtime API has no per-response tool_choice. When core requests tool_choice="none" (e.g. generate_reply() inside a tool, or the final post-tool reply), Gemini may still emit a tool call. With the default blocking tool behavior the turn then stalls waiting for a tool response that core drops (received a tool call with tool_choice set to 'none', ignoring), so the model never speaks its follow-up.

This handles the case inside the plugin: the requested tool_choice is stored on the session, and when it is "none" any tool call the model emits during that turn is answered with an error response. That unblocks the session and lets it reply to the user directly, instead of hanging.

It also unifies FunctionResponse construction into a single create_function_response, used by both get_tool_results_for_realtime and the rejection path, and honors is_error so error tool outputs are sent as {"error": ...} instead of {"output": ...}.

@longcw longcw requested a review from a team as a code owner June 19, 2026 12:42

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Open in Devin Review

Comment on lines +1326 to +1343
if self._opts.tool_choice == "none":
responses = [
create_function_response(
llm.FunctionCallOutput(
name=fnc_call.name or "",
call_id=fnc_call.id or "",
output="Tool calls are disabled for this turn, respond to the user directly.",
is_error=True,
),
vertexai=self._opts.vertexai,
tool_response_scheduling=self._opts.tool_response_scheduling,
)
for fnc_call in tool_call.function_calls or []
]
if responses:
self._send_client_event(types.LiveClientToolResponse(function_responses=responses))
self._mark_current_generation_done()
return

@devin-ai-integration devin-ai-integration Bot Jun 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 No infinite-loop guard when model repeatedly calls tools despite rejection

When tool_choice="none", each tool call is rejected and a new generation starts. If the model persistently calls tools after receiving error responses (unlikely but possible with certain prompts or model behaviors), this creates a loop: reject β†’ new generation β†’ tool call β†’ reject β†’ ... There's no max-retry or circuit-breaker mechanism. In practice, models stop after receiving error responses, but a pathological case could stall the session indefinitely. Consider adding a counter to break the loop after N rejections.

Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

The Google Realtime API has no per-response tool_choice. When core requests tool_choice="none" (e.g. generate_reply() inside a tool, or the final post-tool reply), Gemini may still emit a tool call, and with the default blocking tool behavior the turn stalls waiting for a response that core drops, so the model never speaks its follow-up.

Handle this in the plugin: store the requested tool_choice and, when it is "none", reject any tool call the model emits with an error response, without opening a generation. Keeping the pending generate_reply unresolved binds the model's eventual reply to it and keeps tools suppressed for the whole turn; the trailing server content / usage metadata of the rejected turn is dropped to debug instead of warning.

Also unify FunctionResponse construction into create_function_response, used by both get_tool_results_for_realtime and the rejection path, and honor is_error so error tool outputs are sent as {"error": ...} instead of {"output": ...}.
@longcw longcw force-pushed the longc/gemini-realtime-reject-tool-when-none branch from 7461128 to 8175882 Compare June 20, 2026 01:04

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Open in Devin Review

) -> types.FunctionResponse:
res = types.FunctionResponse(
name=output.name,
response={"error": output.output} if output.is_error else {"output": output.output},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Behavioral change in error response format for get_tool_results_for_realtime

The refactoring of create_function_response introduces a behavioral change: when is_error=True, the function response dict key changes from {"output": msg} (old behavior in get_tool_results_for_realtime) to {"error": msg} (new behavior). This affects all tool execution failures sent via update_chat_ctx β†’ get_tool_results_for_realtime. While likely intentional (and arguably more correct since it signals errors differently to the model), this is a semantic change to an existing code path that could subtly affect model behavior for error-case tool responses. The Gemini API's FunctionResponse.response field is a generic dict, so the key name is what the model "sees" β€” changing it from "output" to "error" may change how the model interprets failed tool calls.

Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gemini realtime does not support GPT-style tool control, which breaks data-capture tools and AgentTask flows

2 participants