Skip to content

Fix schema constraint loss in json_schema_updater.py recursive_update() #3095

Open

Description

@fullsend-ai-retro

What happened

PR #3093 regenerated developer documentation. CodeRabbit's review found that the generated docs/user_doc/config.json schema diverges from runtime Pydantic validators in two ways: (1) streaming_output_check_interval_tokens (a PositiveInt field, valid values ≥ 1) was emitted as minimum: 0, allowing zero; (2) AllowedModel.model_type (typed as Optional[Literal["llm", "embedding", "rerank"]]) was emitted as bare type: string with no enum constraint. Both inaccuracies are pre-existing, introduced by the recursive_update() function in src/utils/json_schema_updater.py which transforms Pydantic-generated JSON Schema into OpenAPI 3.0.

What could go better

The recursive_update() function has two bugs that silently drop schema constraints:

  1. exclusiveMinimum conversion (line 52-53): Pydantic v2 emits {"exclusiveMinimum": 0} for PositiveInt (meaning value > 0). The function rewrites this to {"minimum": 0} (meaning value ≥ 0), losing the exclusive semantics. For OpenAPI 3.0, the correct output is {"minimum": 0, "exclusiveMinimum": true} or equivalently {"minimum": 1} for integer types.

  2. anyOf nullable handling (lines 36-49): When converting anyOf: [{type, enum, format, ...}, {type: null}] to a nullable type, the function extracts only value[0]["type"] and discards all other constraints (enum, format, maxLength, items, etc.). This is why Literal enums and array item types are lost in the generated schema.

The existing unit tests in tests/unit/utils/test_json_schema_updater.py encode the incorrect behavior as expected — e.g., test_exclusive_minimum_handling_zero_value asserts that exclusiveMinimum: 0 becomes minimum: 0, and test_anyof_with_additional_fields_on_first_item asserts that format and maxLength are dropped.

Confidence: High. The bugs are visible in the source code and confirmed by the test expectations matching the observed schema output.

Proposed change

Fix both bugs in src/utils/json_schema_updater.py::recursive_update():

  1. exclusiveMinimum: Change the conversion from new["minimum"] = value to new["minimum"] = value; new["exclusiveMinimum"] = True (OpenAPI 3.0 style), which correctly encodes "strictly greater than value".

  2. anyOf nullable: After extracting val = value[0]["type"], copy all remaining keys from value[0] into new (e.g., enum, format, maxLength, items, $ref). Something like:

for k, v in value[0].items():
    if k != "type":
        new[k] = recursive_update(v) if isinstance(v, dict) else v
  1. Update unit tests to assert the corrected behavior: exclusiveMinimum: 0 → {"minimum": 0, "exclusiveMinimum": true}; nullable types preserve enum, format, maxLength, etc.

  2. Regenerate all doc artifacts (uv run make generate-documentation) to propagate the fixes to docs/user_doc/config.json, docs/models/requests.json, etc.

Validation criteria

After the fix: (1) docs/user_doc/config.json should show streaming_output_check_interval_tokens with "minimum": 0, "exclusiveMinimum": true or "minimum": 1. (2) AllowedModel.model_type schema should include "enum": ["llm", "embedding", "rerank"]. (3) All existing unit tests pass after updating expectations. (4) uv run make verify passes.

Related issues checked

Searched: PositiveInt schema minimum; config schema generation accuracy; openapi schema accuracy validation; json_schema_updater exclusiveMinimum anyOf constraint loss
No overlapping issues found.


Generated by retro agent from #3093

Activity

  1. coderabbitai commented on Nov 1, 2026

    @coderabbitai
    Contributor
    🔗 Related PRs

    #3093 - LCORE-4923: Updated devel documentation [closed]


    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. fullsend-ai-triage commented on Nov 1, 2026

    @fullsend-ai-triage

    🤖 Finished Triage · ✅ Success · Started 12:22 PM UTC · Completed 12:27 PM UTC

    Commit: 7850c3c · View workflow run →

    Runtime: pi · Model: opus → claude-opus-4-6 · Effort: high · Cost: $1.01

  3. fullsend-ai-triage commented on Nov 1, 2026

    @fullsend-ai-triage

    Proposed test case

    # Bug 1: exclusiveMinimum must preserve exclusive semantics
    def test_exclusive_minimum_produces_openapi30_boolean_form():
        original = {"type": "integer", "exclusiveMinimum": 0}
        result = recursive_update(original)
        assert result == {"type": "integer", "minimum": 0, "exclusiveMinimum": True}
    
    # Bug 2: anyOf nullable must preserve enum, format, maxLength, items
    def test_anyof_nullable_preserves_enum():
        original = {"anyOf": [{"type": "string", "enum": ["llm", "embedding", "rerank"]}, {"type": "null"}]}
        result = recursive_update(original)
        assert result == {"type": "string", "nullable": True, "enum": ["llm", "embedding", "rerank"]}
    
    def test_anyof_nullable_preserves_format_and_maxlength():
        original = {"anyOf": [{"type": "string", "format": "email", "maxLength": 50}, {"type": "null"}]}
        result = recursive_update(original)
        assert result == {"type": "string", "nullable": True, "format": "email", "maxLength": 50}
    
    def test_anyof_nullable_preserves_items():
        original = {"anyOf": [{"type": "array", "items": {"type": "integer"}}, {"type": "null"}]}
        result = recursive_update(original)
        assert result == {"type": "array", "nullable": True, "items": {"type": "integer"}}

    After fixing, regenerate doc artifacts with uv run make generate-documentation and verify the generated schemas reflect the corrected constraints.


    Next steps:

    • /fs-code — agent creates a PR to implement this issue
    • /fs-code <your instruction> — agent implements with your specific guidance
  4. fullsend-ai-coder commented on Nov 1, 2026

    @fullsend-ai-coder
    Contributor

    🤖 Finished Code · ✅ Success · Started 12:28 PM UTC · Completed 12:37 PM UTC

    Commit: 7850c3c · View workflow run →

    Runtime: pi · Model: opus → claude-opus-4-6 · Effort: high · Cost: $1.92

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingready-for-triageTriggers triage agent dispatchready-to-codeTriggers code agent dispatch

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions