Skip to content

fix(llm): apply tool defaults across schema shapes - #2150

Open
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
grimes-busiest-feasibly
Open

fix(llm): apply tool defaults across schema shapes#2150
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
grimes-busiest-feasibly

Conversation

@rosetta-livekit-bot

@rosetta-livekit-bot rosetta-livekit-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add strict tool-schema null sentinels for defaulted fields across plain, enum/literal, union, and nested object schema shapes
  • resolve null sentinels to Zod defaults before tool argument validation, including nested objects, records, and tuples
  • keep non-strict/schema-generation defaults sentinel-free and add a patch changeset for @livekit/agents

Source diff coverage

Coverage
  • livekit-agents/livekit/agents/llm/_strict.py: adapted to agents/src/llm/zod-utils.ts; target strict schema generation is Zod/JSON Schema based, so the source null-sentinel/default-removal behavior was ported into zodSchemaToJsonSchema(..., strict: true) and tested with target Zod schemas.
  • livekit-agents/livekit/agents/llm/utils.py: adapted to agents/src/llm/utils.ts; target tool execution validates through Zod, so the source recursive default injection and nullability checks were ported before parseZodSchema.
  • tests/test_response_format.py: not applicable as a standalone target file; agents-js has no equivalent structured response-format helper or validate_response_format decode path. The sentinel-free response-format intent is covered by non-strict schema generation tests in agents/src/llm/zod-utils.test.ts.
  • tests/test_tools.py: adapted to agents/src/llm/utils.test.ts and agents/src/llm/zod-utils.test.ts; source tool default/sentinel coverage was ported to target tool execution and strict Zod schema tests.

Target Gap

  • Python/Pydantic can force union variant selection by rejecting undeclared payload keys during validation. Zod unions in agents-js still use first-success parsing and strip extra object keys unless callers use strict or discriminated unions, so fully replacing runtime union selection would require replacing the target parser. This PR mirrors the stricter schema matcher for default injection, but runtime Zod union selection remains Zod-defined.

Tests

  • pnpm test agents
  • pnpm test agents/src/llm/zod-utils.test.ts agents/src/llm/utils.test.ts
  • pnpm --filter @livekit/agents build
  • pnpm --filter @livekit/agents typecheck
  • pnpm --filter @livekit/agents lint (passes with existing warnings)

No cue-cli run: this port touches LLM/tool schema handling, not the voice pipeline.


Ported from livekit/agents#6444

Original PR description

Summary

Strict JSON schemas require every property and don't support the default keyword. This PR settles how defaulted fields are handled on each of the two encode paths.

Tool schemas keep the null sentinel, and it now works for every schema shape. A defaulted field additionally accepts null, meaning "use the Python default" — the LLM never has to invent a value it can't know. This is safe because the framework controls the decode: _prepare_function_arguments resolves the sentinel before validation.

  • The sentinel is advertised for all shapes, not just bare types: Literal (const/enum), unions (anyOf), and $ref-typed defaults (nested models, enums) previously either contradicted themselves or never became nullable.
  • Resolution is now recursive: nested model arguments, dict[str, Model] values, and tuple items are decoded (previously a top-level-only signature walk).
  • Union variants are rejected when the payload carries keys outside their declared properties, matching the closed-object semantics of the strict wire schema — prevents a payload meant for variant B from silently matching variant A and dropping data.
  • Genuine nulls declared via enum membership (Literal["x", None]) are preserved instead of being replaced with the default.
  • When wrapping a defaulted $ref-typed field (e.g. a documented nested-model param), sibling keys like description stay on the wrapper — strict mode forbids siblings on $ref but allows them next to anyOf.

Response format schemas get no sentinel. to_openai_response_format output is decoded by users with plain Pydantic validation, so the wire schema must be exactly what validation accepts. Defaulted fields stay required and non-nullable; the model produces their values and the Python default never fires. to_strict_json_schema takes a null_sentinel_for_defaults flag (tools pass it; response formats don't).

Behavior changes

  • response_format: the model can no longer send null for a defaulted non-nullable field. Previously bare-type defaulted fields were widened to ["type", "null"], producing payloads that plain model_validate rejects.
  • Tool args: a null for a required parameter with no default now surfaces as a Pydantic ValidationError (still wrapped in ToolError) instead of a hand-written ValueError.

Known limitation

Field(default_factory=...) emits no schema default, so factory-defaulted tool fields get no sentinel and remain required on the wire. Left for a follow-up.

Testing

  • 134 tests across the response-format, tools, gemini-schema, tool-proxy, and placeholder suites (new regression tests written to fail first)
  • Ruff check/format clean; full repo type check (618 files) passes

@rosetta-livekit-bot
rosetta-livekit-bot Bot requested a review from a team as a code owner July 28, 2026 17:11
@changeset-bot

changeset-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a9d9d9f

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 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-google Patch
@livekit/agents-plugin-hedra 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-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

@rosetta-livekit-bot
rosetta-livekit-bot Bot requested a review from u9g July 28, 2026 17:11

@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: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

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