Skip to content

fix(google): drop unsupported JSON Schema keywords before validating - #7353

Open
feiiiiii5 wants to merge 1 commit into
livekit:mainfrom
feiiiiii5:fix-google-drop-unsupported-schema-keys
Open

feiiiiii5 wants to merge 1 commit into
livekit:mainfrom
feiiiiii5:fix-google-drop-unsupported-schema-keys

Conversation

@feiiiiii5

Copy link
Copy Markdown

Problem

Any MCP tool whose JSON Schema carries a legal annotation keyword β€” readOnly, deprecated, $comment, x-google-enum-descriptions, or any other vendor extension β€” makes the Gemini Realtime session fail to build its tool declarations. The worker joins the room, publishes its mic track, and then never speaks, because the schema was rejected before the session could start.

_GeminiJsonSchema._simplify removes a fixed list of keywords it knows Gemini does not accept (title, default, additionalProperties, $schema, discriminator, examples, and const, which is folded into enum). Everything else is passed through untouched and ends up in:

types.FunctionDeclaration.model_validate({"name": ..., "parameters": params})

types.Schema is declared with extra="forbid", so the first keyword outside that fixed list raises a ValidationError. The list is a deny-list against a model that validates by allow-list, so it cannot stay in sync.

Concretely, on main (e5024c7), a schema containing "readOnly": True produces:

pydantic_core._pydantic_core.ValidationError: 1 validation error for FunctionDeclaration
parameters.properties.attachments.items.properties.id.readOnly
  Extra inputs are not permitted [type=extra_forbidden, input_value=True, input_type=bool]

Fix

Derive the accepted keyword set from types.Schema.model_fields and its camelCase aliases, instead of maintaining a separate list that drifts, and prune at the top of _simplify so nested schemas and $defs/$ref targets are covered as well.

Three keys are added back explicitly because _simplify itself consumes them before the declaration is built: anyOf, $ref and prefixItems.

This keeps the existing behaviour for every keyword the transformer already understood, and only changes what happens to keywords it never handled.

Verification

livekit/agents at e5024c7, Python 3.13, macOS arm64, google-genai 2.24.0.

step result
pytest tests/test_schema_gemini.py --unit at base, with the two new tests added 2 failed, 9 passed
same command with the fix 11 passed
pytest tests/test_google_thought_signatures.py tests/test_google_credentials.py tests/test_tools.py --unit 186 passed
ruff check <both files> All checks passed
ruff format --check <both files> 2 files already formatted

The two added tests both fail on unmodified main and assert different halves of the fix: the first pins that an x-google-* extension and readOnly are gone from the output and that the declaration then validates; the second puts the unknown keyword inside a $defs entry reached through a $ref, which is the nested path the old deny-list never reached.

No network access and no API key is needed; the tests exercise _GeminiJsonSchema.simplify() directly and validate through types.FunctionDeclaration.

Scope note

There is a second, separate defect I did not fold in here: realtime_api.py:971 calls config = self._build_connect_config() outside the try: that starts on the next line, so any exception raised while building the config escapes the retry loop and the _emit_error handler below it. That is why this class of schema error is permanent rather than surfaced and retried. It is a distinct root cause in the session lifecycle, so I am leaving it to its own change rather than mixing a lifecycle fix into a schema fix.

AI assistance

AI-assisted development. An assistant helped locate the deny-list/extra="forbid" mismatch and draft the tests and this description. Every command result quoted above was run locally against e5024c7 before posting, including the failing-at-base run.

Refs #7349

_GeminiJsonSchema._simplify pops a fixed list of keywords it knows Gemini
rejects. Anything not on that list survives and is handed to
types.FunctionDeclaration.model_validate -- but types.Schema is declared
extra="forbid", so any other legal JSON Schema keyword fails validation.

MCP servers routinely emit annotation keywords (readOnly, deprecated, $comment,
x-google-enum-descriptions and other vendor extensions), so any tool whose
schema carries one of them aborted the Gemini Realtime session instead of being
usable, as reported in livekit#7349.

Derive the accepted set from types.Schema.model_fields plus its camelCase
aliases, so the filter tracks the SDK's own model instead of falling behind it,
and prune at the top of _simplify so nested schemas and $ref definitions are
covered too.
@feiiiiii5
feiiiiii5 requested a review from a team as a code owner September 19, 2026 15:50
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@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

_ALLOWED_KEYS: ClassVar[frozenset[str]] = frozenset(
set(types.Schema.model_fields)
| {to_camel(name) for name in types.Schema.model_fields}
| {"anyOf", "$ref", "prefixItems"}

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.

🟑 Const constraints are silently removed

Schemas containing const lose it before the existing enum conversion runs. Literal-valued tool parameters then accept values outside their declared constant.

Learn more

const is a JSON Schema keyword that permits exactly one value. The transformer already converts it into Gemini's supported single-value enum at lines 173–175. The new allow-list removes const first, so that conversion becomes unreachable and the value restriction disappears. This affects generated schemas for single-value literals and tagged variants.

Example: A parameter schema {"type": "string", "const": "only"} previously became {"type": STRING, "enum": ["only"]}. It now becomes only {"type": STRING}, allowing Gemini to emit any string.

Recommended fix: Add const to _ALLOWED_KEYS, alongside the other keywords consumed by _simplify, so the existing conversion can preserve the constraint.

Suggested change
| {"anyOf", "$ref", "prefixItems"}
| {"anyOf", "$ref", "prefixItems", "const"}

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.

2 participants