Skip to content

fix: unwrap single-variant anyOf to preserve sibling fields in Vertex AI schema - #19270

Closed
VedantMadane wants to merge 1 commit into
BerriAI:mainfrom
VedantMadane:fix/vertex-ai-anyof-sibling-fields
Closed

fix: unwrap single-variant anyOf to preserve sibling fields in Vertex AI schema#19270
VedantMadane wants to merge 1 commit into
BerriAI:mainfrom
VedantMadane:fix/vertex-ai-anyof-sibling-fields

Conversation

@VedantMadane

Copy link
Copy Markdown
Contributor

Relevant issues

Fixes #19255

Summary

When Pydantic generates Optional[X], it creates:

{
  "anyOf": [{"type": "integer", "minimum": 0}, {"type": "null"}],
  "default": null,
  "examples": [7, 30, 90],
  "description": "Filter by number of days"
}

The constraints (default, examples, pattern) are siblings of anyOf.

After convert_anyof_null_to_nullable removes the null type, if only ONE variant remains in anyOf, we now unwrap it and merge the sibling fields. This prevents _filter_anyof_fields from stripping them.

Before (sibling fields lost):

{"anyOf": [{"type": "integer", "minimum": 0}], "default": null, "examples": [...]}
-> {"anyOf": [{"type": "integer", "minimum": 0, "nullable": true}]}
// default and examples are LOST!

After (sibling fields preserved):

{"anyOf": [{"type": "integer", "minimum": 0}], "default": null, "examples": [...]}
-> {"type": "integer", "minimum": 0, "nullable": true, "default": null, "examples": [...]}

Pre-Submission checklist

  • I have Added testing in the tests/litellm/ directory - Added test_anyof_conversion_preserves_sibling_fields
  • All existing tests pass (schema-related tests all pass)

Changes

  1. litellm/llms/vertex_ai/common_utils.py:

    • Modified convert_anyof_null_to_nullable to unwrap single-variant anyOf arrays and merge sibling fields
  2. tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py:

    • Added test_anyof_conversion_preserves_sibling_fields to verify the fix
    • Updated test_basic_anyof_conversion to expect unwrapped single-variant anyOf
    • Updated test_build_vertex_schema and test_build_vertex_schema_empty_properties to reflect new behavior

@CLAassistant

CLAassistant commented Jan 17, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@vercel

vercel Bot commented Jan 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Error Error Mar 5, 2026 7:14pm

Request Review

@greptile-apps

greptile-apps Bot commented Mar 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a schema field-loss bug in Vertex AI schema transformation where Pydantic-generated Optional[X] fields had their sibling attributes (default, examples, description, pattern) silently stripped. The root cause was that after convert_anyof_null_to_nullable reduced a two-variant anyOf (one real type + null) to a single variant, the downstream _filter_anyof_fields function would see the remaining anyOf key and discard all siblings. The fix unwraps single-element anyOf arrays directly into the parent schema and merges sibling fields, so no anyOf key is left for _filter_anyof_fields to trigger on.

Key changes:

  • litellm/llms/vertex_ai/common_utils.py: After null-removal leaves a single variant in anyOf, the variant is unwrapped into the parent dict with siblings merged in (variant fields take precedence on conflict).
  • tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py: New test_anyof_conversion_preserves_sibling_fields and test_anyof_conversion_precedence_on_conflict tests; existing tests updated to match the new unwrapped output.
  • litellm/completion_extras/litellm_responses_transformation/transformation.py: Moves # noqa: PLR0915 onto the def line so ruff honours it (trivial fix bundled in the same PR).

Confidence Score: 4/5

  • Safe to merge — the core fix is correct, well-tested, and confined to the Vertex AI schema transformation path.
  • The unwrapping logic is logically sound for the target scenario. The only minor concern is that the if len(anyof) == 1: unwrap fires unconditionally (not gated on contains_null), making the change slightly broader than the PR description implies. For Vertex AI schemas this is harmless since a single-variant anyOf is semantically equivalent to the inline type, but a missing test or if contains_null guard would make the intent clearer. No critical regressions expected.
  • litellm/llms/vertex_ai/common_utils.py — review the scope of the single-variant unwrap (with vs. without contains_null guard).

Important Files Changed

Filename Overview
litellm/llms/vertex_ai/common_utils.py Adds single-variant anyOf unwrapping to preserve sibling fields (default, examples, description). Logic is correct for the Optional[X] use-case, but the unwrap is also applied to single-variant anyOf without null — a broader change than the PR description implies. No critical bugs.
tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py New tests cover the sibling-field preservation case, the variant-wins-on-conflict case, and updated existing tests to reflect the new unwrapping behaviour. No network calls; all mocked. Good coverage of the happy paths.
litellm/completion_extras/litellm_responses_transformation/transformation.py Single-line change: moves the # noqa: PLR0915 directive from a standalone comment (where ruff ignores it) onto the def line where it is effective. No functional change.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["convert_anyof_null_to_nullable(schema)"] --> B{"anyOf present?"}
    B -- No --> G["Recurse into properties / items"]
    B -- Yes --> C["Scan variants: remove null, set contains_null=True"]
    C --> D{"len(anyof) == 0?"}
    D -- Yes --> E["raise ValueError (only-null schema)"]
    D -- No --> F{"contains_null?"}
    F -- Yes --> H["Set nullable=True on every remaining variant"]
    F -- No --> I["(no-op)"]
    H --> J{"len(anyof) == 1?"}
    I --> J
    J -- No --> G
    J -- Yes --> K["Collect siblings (all keys except anyOf)"]
    K --> L["schema.clear()"]
    L --> M["schema.update(siblings)"]
    M --> N["schema.update(single_variant)\n(variant fields win on conflict)"]
    N --> G
    G --> O["Return (schema mutated in-place)"]
    O --> P["filter_schema_fields calls _filter_anyof_fields"]
    P --> Q{"anyOf key present?"}
    Q -- Yes --> R["Strip sibling fields (old behaviour)"]
    Q -- No --> S["No stripping — siblings preserved ✓"]
Loading

Last reviewed commit: 88c7c3f

@greptile-apps greptile-apps 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.

5 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +1568 to +1573
previous_response_id=getattr(
chat_completion_response, "previous_response_id", None
),
previous_response_id=getattr(
chat_completion_response, "previous_response_id", None
),

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.

Duplicate keyword argument causes SyntaxError

The previous_response_id keyword argument is passed twice to the ResponsesAPIResponse constructor. This is a Python syntax error introduced by the merge conflict resolution in commit 6e7252aad ("Resolve merge conflict in responses transformation"). Python will raise SyntaxError: keyword argument repeated at parse time, meaning this file cannot be imported at all.

Remove the duplicate:

Suggested change
previous_response_id=getattr(
chat_completion_response, "previous_response_id", None
),
previous_response_id=getattr(
chat_completion_response, "previous_response_id", None
),
previous_response_id=getattr(
chat_completion_response, "previous_response_id", None
),

Comment thread pr_body.txt Outdated
Comment thread litellm/llms/vertex_ai/common_utils.py Outdated
Comment on lines +718 to +727
if len(anyof) == 1:
single_variant = anyof[0]
# Collect sibling fields (everything except 'anyOf')
siblings = {k: v for k, v in schema.items() if k != "anyOf"}
# Clear the schema and replace with merged content
schema.clear()
# Add sibling fields first (can be overwritten by variant fields if there's a conflict)
schema.update(siblings)
# Merge the single variant's fields (these take precedence)
schema.update(single_variant)

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.

Sibling fields may silently override variant fields

The merge order gives the single variant precedence over siblings (schema.update(single_variant) last). However, if a sibling field has the same key as a field inside the variant (e.g., both have "description"), the variant's value silently wins with no warning. This is the correct priority for most cases, but consider the scenario where the user sets a "description" as a sibling of anyOf and the inner variant also has a "description" — the sibling's description would be lost.

This is a minor concern and may be acceptable since it's an uncommon edge case, but it would be worth documenting this merge precedence in a brief inline comment so future maintainers understand the tradeoff.

Comment thread litellm/completion_extras/litellm_responses_transformation/transformation.py Outdated
Comment on lines 223 to 224
def _map_optional_params_to_responses_api_request( # noqa: PLR0915
def _map_optional_params_to_responses_api_request(

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.

Duplicate def line causes SyntaxError

The fix for the ruff: noqa placement (commit 57dd237d1) converted the comment into a full def line but did not remove the original def on line 224. This results in two consecutive function definition lines — Python will raise a SyntaxError because line 223 is a def with no body. The file cannot be imported at all.

Suggested change
def _map_optional_params_to_responses_api_request( # noqa: PLR0915
def _map_optional_params_to_responses_api_request(
def _map_optional_params_to_responses_api_request( # noqa: PLR0915

@giulio-leone

Copy link
Copy Markdown
Contributor

Automated patch bundle from next-100 unresolved backlog expansion.\nGenerated due limited direct branch-write access; please apply/cherry-pick minimal edits below.\n\n## PR #19270fix/vertex-ai-anyof-sibling-fields (3 unresolved)

Unresolved thread summary

  • T1 litellm/responses/litellm_completion_transformation/transformation.py:1573 — Duplicate keyword argument causes SyntaxError
  • T2 litellm/llms/vertex_ai/common_utils.py:725 — Sibling fields may silently override variant fields
  • T3 litellm/completion_extras/litellm_responses_transformation/transformation.py:224 — Duplicate def line causes SyntaxError

Minimal patch proposals

  • T1 litellm/responses/litellm_completion_transformation/transformation.py:1573
    • Edit steps:
      1. Implement the minimal targeted code change at this location to resolve the reviewer concern.
      2. Add or update one focused regression test near this module for the corrected behavior.
  • T2 litellm/llms/vertex_ai/common_utils.py:725
    • Edit steps:
      1. Replace silent fallback with explicit validation error for unsupported/invalid values.
      2. Add or update one focused regression test near this module for the corrected behavior.
  • T3 litellm/completion_extras/litellm_responses_transformation/transformation.py:224
    • Edit steps:
      1. Implement the minimal targeted code change at this location to resolve the reviewer concern.
      2. Add or update one focused regression test near this module for the corrected behavior.

Comment thread litellm/llms/vertex_ai/common_utils.py Outdated
Comment on lines +718 to +728
if len(anyof) == 1:
single_variant = anyof[0]
# Collect sibling fields (everything except 'anyOf')
siblings = {k: v for k, v in schema.items() if k != "anyOf"}
# Clear the schema and replace with merged content
schema.clear()
# Start with sibling metadata, then let variant fields override
# so core schema-defining fields (type, enum, etc.) from the
# variant are never silently clobbered by siblings.
schema.update(siblings)
schema.update(single_variant)

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.

Unwrap fires for all single-variant anyOf, not only post-null-removal

The if len(anyof) == 1: check is not gated on contains_null, so it triggers for any schema that already has exactly one variant (without a null peer), e.g. {"anyOf": [{"type": "string"}], "default": "hello"}. The PR description says "after convert_anyof_null_to_nullable removes the null type", implying this is only for Optional patterns, but the implementation is broader.

For Vertex AI this is generally safe (single-variant anyOf is semantically identical to the unwrapped form), and it's arguably better behaviour because it also preserves siblings in the non-null single-variant case. However, it is a wider behaviour change than the PR description suggests and there's no test covering this path. Consider either:

  1. Guarding the unwrap with if contains_null: to match the documented intent, or
  2. Adding a test for the non-null single-variant case to document the intended broader scope.
# Option 1 – narrower, matches PR description:
        if contains_null and len(anyof) == 1:

@VedantMadane
VedantMadane force-pushed the fix/vertex-ai-anyof-sibling-fields branch from 88c7c3f to 14429dd Compare May 2, 2026 17:00
- Fix duplicate def line in transformation.py (SyntaxError)
- Ensure correct sibling field merging in Vertex AI schema transformation
- Rebase onto latest upstream/main
@VedantMadane
VedantMadane force-pushed the fix/vertex-ai-anyof-sibling-fields branch from 14429dd to f63fbf4 Compare May 19, 2026 20:38
@VedantMadane

Copy link
Copy Markdown
Contributor Author

Addressed review feedback from #pullrequestreview-3874764358.

  • Fixed duplicate \def\ line in \litellm/completion_extras/litellm_responses_transformation/transformation.py\ that was causing a \SyntaxError.
  • Rebased branch onto latest \upstream/main.
  • Verified file integrity and encoding.

Please take another look. Thank you!

@Sameerlite Sameerlite closed this Jun 4, 2026
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.

[Bug]: Vertex AI schema transformation strips sibling fields from Optional types - single-variant anyOf should be unwrapped

4 participants