Conversation
_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.
|
|
| _ALLOWED_KEYS: ClassVar[frozenset[str]] = frozenset( | ||
| set(types.Schema.model_fields) | ||
| | {to_camel(name) for name in types.Schema.model_fields} | ||
| | {"anyOf", "$ref", "prefixItems"} |
There was a problem hiding this comment.
π‘ 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.
| | {"anyOf", "$ref", "prefixItems"} | |
| | {"anyOf", "$ref", "prefixItems", "const"} |
Was this helpful? React with π or π to provide feedback.
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._simplifyremoves a fixed list of keywords it knows Gemini does not accept (title,default,additionalProperties,$schema,discriminator,examples, andconst, which is folded intoenum). Everything else is passed through untouched and ends up in:types.Schemais declared withextra="forbid", so the first keyword outside that fixed list raises aValidationError. 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": Trueproduces:Fix
Derive the accepted keyword set from
types.Schema.model_fieldsand its camelCase aliases, instead of maintaining a separate list that drifts, and prune at the top of_simplifyso nested schemas and$defs/$reftargets are covered as well.Three keys are added back explicitly because
_simplifyitself consumes them before the declaration is built:anyOf,$refandprefixItems.This keeps the existing behaviour for every keyword the transformer already understood, and only changes what happens to keywords it never handled.
Verification
livekit/agentsate5024c7, Python 3.13, macOS arm64,google-genai2.24.0.pytest tests/test_schema_gemini.py --unitat base, with the two new tests addedpytest tests/test_google_thought_signatures.py tests/test_google_credentials.py tests/test_tools.py --unitruff check <both files>ruff format --check <both files>The two added tests both fail on unmodified
mainand assert different halves of the fix: the first pins that anx-google-*extension andreadOnlyare gone from the output and that the declaration then validates; the second puts the unknown keyword inside a$defsentry 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 throughtypes.FunctionDeclaration.Scope note
There is a second, separate defect I did not fold in here:
realtime_api.py:971callsconfig = self._build_connect_config()outside thetry:that starts on the next line, so any exception raised while building the config escapes the retry loop and the_emit_errorhandler 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 againste5024c7before posting, including the failing-at-base run.Refs #7349