fix(llm): never give a const field the null default sentinel - #6688
Merged
Conversation
A defaulted field gets a null branch in strict schemas so the model can decline a
value that _prepare_function_arguments then swaps back for the Python default. A
const field has exactly one legal value, so nulling it gains nothing, and the
emitted schema contradicts itself: {"const": "celsius", "type": ["string", "null"]}.
It also loses information. Strict mode has no `discriminator` keyword, so the const
each variant of a discriminated union pins on its tag field is the only thing that
resolves the variant on the way back in. Tags are normally written with a default
(`unit: Literal["celsius"] = "celsius"`), so they were taking the sentinel — and a
returned null leaves variants that differ only in their tag indistinguishable, with
the first one silently winning.
Covers callable discriminators too, which emit no `discriminator` keyword at all.
u9g
force-pushed
the
fix/strict-const-null-sentinel
branch
from
August 9, 2026 12:24
ca957e2 to
5dd87db
Compare
There was a problem hiding this comment.
Pull request overview
Fixes strict tool-schema generation so defaulted const fields (notably discriminated-union tags) are never made nullable via the “null default sentinel”, preventing contradictory schemas and incorrect variant resolution during tool argument parsing.
Changes:
- Update strict-schema default handling to skip adding a null sentinel for
const-constrained fields. - Add targeted tests ensuring defaulted
Literal[...]tags remain pinned ({"const": ..., "type": "string"}) and discriminated unions round-trip into the correct variant.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
tests/test_tools.py |
Adds regression tests for defaulted const fields and discriminated-union tag stability under strict schemas. |
livekit-agents/livekit/agents/llm/_strict.py |
Adjusts null-sentinel injection to explicitly exempt const schemas while preserving existing handling for enums/unions/refs. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
longcw
approved these changes
Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A defaulted field gets a null branch in strict schemas so the model can decline a value, which
_prepare_function_argumentsswaps back for the Python default. Aconstfield has exactly one legal value, so nulling it gains nothing and the emitted schema contradicts itself:{"const": "celsius", "type": ["string", "null"]}It also loses information. Strict mode has no
discriminatorkeyword, so theconsteach variant of a discriminated union pins on its tag is the only thing that resolves the variant on the way back in. Tags are normally written with a default (unit: Literal["celsius"] = "celsius"), so they were taking the sentinel — and a returned null leaves variants that differ only in their tag indistinguishable:{"temp": {"unit": None, "value": 70.0}} -> Celsius(unit='celsius', value=70.0)The model said Fahrenheit; the tool got Celsius. Covers callable discriminators (
Discriminator(fn)+Tag(...)) too, which emit nodiscriminatorkeyword to key off.Testing
pytest tests/test_tools.py(105 passed), plus-k "strict or schema or tool"acrosstests/(211 passed){"const": ..., "type": "string"}, required and non-nullable, and the named variant round-trips, for: bare arg, optional-only arg, list element, nested model field,str-Enum-valued tag, callable discriminator, andto_openai_response_formatopenai/gpt-4.1-miniandgoogle/gemini-2.5-flashvia the inference gateway