Repository navigation
Conversation
A field annotated `Optional["Model"]` makes `get_schema` recurse forever: the `anyOf` branch resolves the field's `$ref` back into the model being expanded, until the interpreter raises a bare RecursionError. Track the references on the current expansion path and render a `<name>` placeholder when a reference closes a cycle, the same way container-wrapped recursive fields (e.g. `Optional[list["Model"]]`) already render. Repeated non-recursive references keep expanding.
This branch has not been deployed
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
get_schemarenders a Pydantic model's schema for prompt examples by recursively following$refs. A field that refers back to an enclosing model (e.g.nxt: Optional["Node"]) has no base case: theanyOfbranch resolves the field's$ref, the$refbranch re-enters the same definition, and the walker spins until the interpreter raises a bareRecursionError.Track the references on the current expansion path and render the existing
<name>placeholder when a reference closes a cycle, the same way container-wrapped recursive fields (e.g.Optional[list["Node"]], whoseitemsthe walker never enters) already render. Repeated non-recursive references (two fields of the same type) still expand once per field, since only the current path is tracked, not the set of visited definitions.Why
Root cause:
src/outlines/templates.py:327(onmain) —parse_pydantic_schema_valueresolves every$refunconditionally:For
Optional["Node"], Pydantic emitsanyOf: [{"$ref": "#/$defs/Node"}, {"type": "null"}], which theanyOfbranch (src/outlines/templates.py:341) re-dispatches to the$refbranch, re-enteringNode's definition. Each pass adds anothersimple_schema[name] = parse_pydantic_schema_value(...)frame (line 316) untilRecursionError: maximum recursion depth exceeded— with nothing rendered at all.Recursive models inside containers never hit this because the walker does not descend into
items:children: Optional[list["Node"]]renders as"children": "<children>"on unpatchedmain(verified below). The fix makes the direct reference consistent with that.Changes
src/outlines/templates.py—parse_pydantic_schemaandparse_pydantic_schema_valuethread aseen_refstuple of the$refs on the current expansion path (default(), so existing callers are unaffected). When a$refis already on the path, the field renders the<name>placeholder instead of re-entering the definition. The tuple is accumulated per path, so sibling fields keep their own expansion history.tests/test_templates.py—test_get_schema_recursive_modelcovers a self-referentialOptional["PydanticRecursiveClass"]field (renders{"name": "<name>", "nxt": "<nxt>"}instead of crashing) and aPydanticRepeatedClasswith two fields of the same non-recursive type (both still expand), guarding against over-eager cycle detection.Testing
Python 3.12.14, pydantic 2.13.5. New test on unpatched
main(git checkout main -- src/outlines/templates.py):On this branch:
Full suite (minus seven
tests/models/*files that cannot even be collected in this environment — the anthropic/dottxt/gemini/lmstudio/mistral/ollama optional backends are not installed): identical results on unpatchedmainand this branch — 96 failed, 80 skipped, 5 errors, all in provider/cache/generator tests that need network access, model weights or missing optional dependencies. The only difference between the two runs is the new test (failed onmain, passes here). The placeholder rendering of container-wrapped recursive fields (Optional[list["Node"]]→"children": "<children>") was also verified unchanged on unpatchedmain.No open issue covers this exact path — the closed #330 tracks recursive schemas in the JSON-schema-to-regex converter, a different module. Happy to file a tracker issue if maintainers prefer one.
Checklist
maincommit (c52af84);pre-commitwas run before opening the PR; all hooks pass;get_schemais not mentioned in the docs.