Skip to content

Let the model choose the variant of an anyOf - #320

Merged
mattt merged 7 commits into
mainfrom
mattt/pr-309-fixes
Oct 13, 2026
Merged

mattt merged 7 commits into
mainfrom
mattt/pr-309-fixes

Conversation

@mattt

@mattt mattt commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

This PR lands #309 by @2570165831, with one fix on top. When a nested object in a union closes, generation now keeps only the variants that the object completes. Before, a nested union could produce JSON that matched none of its variants.

Fixes #289

2570165831 and others added 7 commits October 8, 2026 11:31
Constrained generation always took the first variant of an anyOf, so a
nullable union could never produce null and a union of object types
always produced the first type (#289).

The generator now resolves references and flattens nested unions, then
samples once among the tokens that can start each variant and continues
with the variants that can start with the sample. Object variants all
start with `{`, so the key mask offers the keys of every live variant and
each emitted key keeps the variants that declare it; a shared key whose
values are string choices (enum case tags, constants) narrows by the
chosen value. Variants that start alike but aren't all objects still fall
back to the first of them, noted where it happens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node equality includes descriptions, so two object variants whose schemas
for a shared key differ only in their descriptions were treated as
different: the fallback chose the first and dropped the other, letting
declaration order decide the union again. Compare the schemas with
descriptions removed, both for the fast path and for narrowing after the
sampled value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a shared key's values are all objects, generate the value with the
multi-variant object generator and keep the variants whose nested object
still matches, instead of always keeping the first.

Resolve references when comparing a shared key's schemas, stopping at a
reference already being resolved so recursive definitions terminate.

Keep the live variants when a generated choice matches none of the
choices, as when a token decodes with a leading space, so the object
does not close before the remaining required keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the tokens sampled for a string choice decode to text that is not
exactly one of the choices (a piece with a leading space, say), the live
variants used to stay as they were. A variant that the choice ruled out
could then let the object close before the chosen case's required keys.

generateChoice now reports which candidate the sampled tokens spell, and
the value path narrows by that candidate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A union that refers to the same definition twice, at each level, made
`flattenedVariants` double its result per level: the depth guard capped
the chain length, not the size. Each definition is now expanded once per
flattening, which also drops the depth guard.

`withoutDescriptions` had the same shape for definitions that refer to
another one twice. It now resolves at most 256 references per call and
leaves the rest as references, which can only make two schemas compare
unequal sooner. Properties are visited in key order, so the cut-off is
the same for equal schemas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mattt
mattt merged commit 2f4e53f into main Oct 13, 2026
11 checks passed
@mattt
mattt deleted the mattt/pr-309-fixes branch October 13, 2026 12:42
mattt added a commit that referenced this pull request Oct 13, 2026
Resolve the conflict in StructuredGeneration.swift by keeping #320's variant selection, where the keys the model writes choose among the objects of a union. When those objects are the cases of an enumeration with associated values, each with a different constant type, the object now starts with type, and #320's string choice narrowing picks the case.
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.

Constrained generation always picks the first anyOf variant

2 participants