Skip to content

fix(google): report a bad tool schema instead of ending the session in silence - #2548

Open
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
grandad-crowning-readies
Open

rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
grandad-crowning-readies

Conversation

@rosetta-livekit-bot

@rosetta-livekit-bot rosetta-livekit-bot Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • validate Gemini Live function schemas and include the originating tool name in schema errors
  • build the realtime connect config inside the existing retry/error path
  • port the source regression test and add a patch changeset for @livekit/agents-plugin-google

Ports livekit/agents#7371.

Source diff coverage

Source diff classification
Source file Classification Target coverage
livekit-agents/livekit/agents/llm/_provider_format/google.py Adapted plugins/google/src/utils.ts. The JS target already has the source refactor's unified raw/generated conversion in toFunctionDeclarations; declaration conversion remains shared, and schema failures are now associated with the current tool.
livekit-plugins/livekit-plugins-google/livekit/plugins/google/realtime/realtime_api.py Ported plugins/google/src/realtime/realtime_api.ts. Connect-config construction now runs inside the existing try, so schema failures reach realtime error reporting and connection failure handling.
livekit-plugins/livekit-plugins-google/livekit/plugins/google/utils.py Adapted plugins/google/src/utils.ts. The JS SDK has TypeScript interfaces instead of Pydantic runtime validation, so the missing runtime validation is implemented for Gemini's numeric schema constraints and wrapped with the tool name.
tests/test_schema_gemini.py Ported plugins/google/src/utils.test.ts. The same invalid minLength: "many" raw schema asserts that the error names tool bad, adapted to Vitest and JS tool APIs.

No source files were classified as not applicable.

Validation

  • pnpm test plugins/google (71 passed, 2 skipped)
  • pnpm build
  • pnpm --filter @livekit/agents-plugin-google lint (passes; 3 pre-existing warnings)
  • pnpm format:check
  • Full pnpm lint was run but remains blocked by the pre-existing @typescript-eslint/no-misused-promises error in plugins/openai/src/ws/llm.ts:127.
  • cue-cli voice-mode validation was attempted with a temporary malformed-schema Gemini agent, but the Cue room connection failed before dispatch with 401 Unauthorized - invalid API key; no framework events were produced.

Ported from livekit/agents#7371

Original PR description

Problem: The Gemini realtime session built its connect config one line above the try that wraps the connection, so a tool schema the plugin cannot convert raised past the retry loop and past _emit_error. @utils.log_exceptions wrote one line to the worker log and swallowed the rest, so the agent joined the room, published its microphone track, and then never heard or spoke.

Fix: The config is now built inside the try, so the failure reaches _emit_error(recoverable=False) and APIConnectionError like any other connect failure. The error also names the tool, which until now pointed only at a JSON Schema keyword.

Follows #7353. Refs #7349.

Context for reviewing and coding agents

How to see it

tests/test_schema_gemini.py::test_rejected_tool_names_itself declares a raw tool with minLength: "many" and asserts create_tools_config raises ValueError: tool bad has a schema Gemini rejected. On main the declaration raises a bare pydantic.ValidationError that names the field path but not the tool. The test uses minLength rather than readOnly because minLength is a real types.Schema field alias, so it survives the keyword pruning added in #7353 and reaches validation either way.

Why the failure was invisible

_build_connect_config() sat at realtime_api.py:971, above the try on the next line. Every branch that reports a connect failure lives inside that try, so a schema error skipped all of them. The session was left with a live room connection and no model, which is why the symptom reported in #7349 is silence rather than an error.

Why to_fnc_ctx changed at all

to_fnc_ctx wrote the same conversion twice, once per tool kind, including the deferred _GeminiJsonSchema import and the behavior assignment. The two kinds differ only in where the name, description and JSON Schema come from, so the shared half is now written once. A FunctionTool with no arguments still sends no schema: build_legacy_openai_schema always writes an object, and an empty one means the tool takes no arguments.

Blast radius

to_fnc_ctx is reached from llm.py:357 for the text API and realtime_api.py:1265 for the Live API, both through create_tools_config. Output was compared before and after across eight tools on both paths. Every payload is byte-identical to main except one: a raw tool that declares parameters: {} now sends parameters_json_schema: null instead of {}, which is what the Live path already sent for that input. That tool is still declared and still called on gemini-3-flash-preview and on gemini-2.5-flash-native-audio-preview-12-2025.

What still fails, and is left out

_GeminiJsonSchema._simplify raises TypeError on a type: ["string", "null"] union, because it uses the list as a dictionary key, and ValueError on a recursive $ref. Both are legal JSON Schema that an MCP server can send, and neither is covered by #7353. The type union maps onto type: STRING, nullable: True, so it is a gap in the transformer rather than a limit of the API, and it belongs in that function rather than here.

@rosetta-livekit-bot
rosetta-livekit-bot Bot requested a review from a team as a code owner September 21, 2026 10:56
@changeset-bot

changeset-bot Bot commented Sep 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2cc4c63

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 39 packages
Name Type
@livekit/agents-plugin-google Patch
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-anthropic Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-azure Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-did Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-krisp Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-meta Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-protoface Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugin-soniox Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugins-test Patch
@livekit/agents-plugin-trugen Patch
@livekit/agents-plugin-xai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@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 potential issue.

Devin Review

Comment on lines +154 to +156
if (schema[field] !== undefined && typeof schema[field] !== 'number') {
throw new TypeError(`${field} must be a number`);
}

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.

🟡 Invalid numeric constraints evade attribution

Non-finite numbers and invalid count constraints pass validateGeminiSchema. Gemini rejects them later, so the connection error omits the originating tool name.

Learn more

JSON Schema count constraints such as minLength, maxItems, and minProperties accept only nonnegative integers. Numeric bounds such as minimum and maximum still require finite numbers. JavaScript reports NaN, Infinity, negative numbers, and fractions as number, so the current check accepts values Gemini rejects. The rejection then occurs after toFunctionDeclarations has returned, outside the per-tool wrapper.

Example: A raw tool schema with minLength: -1 passes this validator. Gemini rejects the Live connection, and the emitted error cannot name that tool. The same happens with minimum: NaN; serialization can also turn that value into null.

Recommended fix: Validate all numeric fields with Number.isFinite. Additionally require Number.isInteger(value) && value >= 0 for item, length, and property count constraints. Add regression cases for negative, fractional, NaN, and infinite values.

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.

0 participants