Skip to content

fix(tools): sanitize anyOf schemas for Vertex AI function declarations#6381

Open
ShauryaaSharma wants to merge 1 commit into
google:mainfrom
ShauryaaSharma:fix/vertex-anyof-schema-sanitization
Open

fix(tools): sanitize anyOf schemas for Vertex AI function declarations#6381
ShauryaaSharma wants to merge 1 commit into
google:mainfrom
ShauryaaSharma:fix/vertex-anyof-schema-sanitization

Conversation

@ShauryaaSharma

@ShauryaaSharma ShauryaaSharma commented Jul 12, 2026

Copy link
Copy Markdown

Pydantic serializes Optional[X]/Union fields as anyOf, which Vertex AI rejects because the wrapping schema has no top-level type field (googleapis/python-genai#1807), while AI Studio accepts it unmodified. Flatten the common Optional[X] -> anyOf: [X, {type: null}] pattern into X + nullable: true for parameters_json_schema and response_json_schema when targeting Vertex AI, gated on GoogleLLMVariant.VERTEX_AI so AI Studio behavior is unchanged.

Fixes #6373

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

2. Or, if no issue exists, describe the change:

Problem:
Pydantic serializes Optional[X]/Union[X, None] fields as anyOf, e.g.:

{"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}], "default": null}

Vertex AI rejects this with functionDeclaration ... schema didn't specify the schema type field, because the wrapping schema itself has no top-level type (see googleapis/python-genai#1807). The Gemini Developer API (AI Studio) accepts the same schema unmodified — this is why the same agent definition works on AI Studio but fails on Vertex, as reported in #6373.

Solution:
Add a Vertex-only sanitization pass that flattens the common Optional[X] -> anyOf: [X, {type: null}] pattern into X merged with "nullable": true, which Vertex accepts:

{"type": "array", "items": {"type": "string"}, "default": null, "nullable": true}

This is applied in build_function_declaration_with_json_schema (src/google/adk/tools/_function_tool_declarations.py) to parameters_json_schema, response_json_schema, and the Pydantic-BaseModel-as-function path, gated strictly on get_google_llm_variant() == GoogleLLMVariant.VERTEX_AI so AI Studio's existing (working) behavior is unchanged. True multi-variant unions (e.g. Union[int, str], no None branch) can't be losslessly flattened this way and are intentionally left as-is.

Scope note: this PR covers tool/function declaration schemas only — the exact construct reported in the issue, and fully within ADK's control. The output_schema -> GenerateContentConfig.response_schema path hands a raw Pydantic class to the google-genai SDK, which converts it internally; that's better tracked against the upstream googleapis/python-genai#1807 issue rather than patched here. The second symptom in #6373 (dropped function-call results under SSE + output_schema + BuiltInPlanner thinking) was investigated separately — no bug was found in ADK's streaming aggregator (streaming_utils.py), so it's left out of this PR pending a live repro against Vertex.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Added TestVertexAiAnyOfSanitization to tests/unittests/tools/test_function_tool_declarations.py, covering:

  • Optional[list[str]] primitive field -> flattened to array + nullable: true
  • Optional[Address] (nested Pydantic model, $ref-based) -> flattened, $ref preserved
  • A BaseModel passed directly (mirrors output_schema-shaped classes) with an Optional[list[str]] field -> flattened
  • A true int | str union (no None branch) -> left untouched, confirming we don't overreach

All new tests are gated behind an autouse fixture setting GOOGLE_GENAI_USE_ENTERPRISE=true, so existing AI Studio-path tests (which assert raw anyOf is present) are unaffected.

$ pytest tests/unittests/tools/test_function_tool_declarations.py -q
44 passed, 4 warnings in 5.26s

Ran the full tests/unittests/tools/ directory:

$ pytest tests/unittests/tools/ -q
1607 passed, 1 skipped, 5 failed

The 5 failures (test_edit_file_tool.py, test_read_file_tool.py) are pre-existing Windows CRLF-handling issues, unrelated to this change — confirmed identical on main prior to this change.

Also ran the full tests/unittests suite (excluding one pre-existing, unrelated collection error in test_verify_snippets.py that expects a repo layout not present in this checkout — reproduces identically on main):

$ pytest tests/unittests --ignore=tests/unittests/test_verify_snippets.py -q
8385 passed, 79 failed, 69 skipped, 24 xfailed, 10 xpassed

Spot-checked two of the 79 failures (bigquery_agent_analytics_plugin, test_samples.py::test_sample[...parallel_functions...]) against main and confirmed both fail identically without this change — none of the 79 touch tool/schema declaration code.

Ran pre-commit (isort, pyink, addlicense, ruff) against the changed files — clean (isort auto-fixed import ordering; re-verified tests still pass after).

Manual End-to-End (E2E) Tests:

  • I have manually tested my changes end-to-end.

Not completed — I don't have a GCP project/Vertex AI credentials available in my dev environment to run the exact repro from #6373 (an LlmAgent with output_schema + a tool + BuiltInPlanner thinking, run with GOOGLE_GENAI_USE_VERTEXAI=1) against the live API. The unit tests assert the exact schema transformation Vertex requires per the linked upstream issue, but this hasn't been confirmed against a real Vertex API call yet. If a maintainer or the issue reporter can verify against their Vertex project, that would close the loop.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

tox multi-version testing (required by CONTRIBUTING.md before a PR) is blocked in my environment by the pre-existing test_verify_snippets.py collection error mentioned above, present in all 5 supported Python versions independent of this change. I ran the equivalent full-suite pytest invocation above as the closest substitute.

Pydantic serializes Optional[X]/Union fields as anyOf, which Vertex AI
rejects because the wrapping schema has no top-level type field
(googleapis/python-genai#1807), while AI Studio accepts it unmodified.
Flatten the common Optional[X] -> anyOf: [X, {type: null}] pattern into
X + nullable: true for parameters_json_schema and response_json_schema
when targeting Vertex AI, gated on GoogleLLMVariant.VERTEX_AI so AI
Studio behavior is unchanged.

Fixes google#6373
@adk-bot adk-bot added the tools [Component] This issue is related to tools label Jul 12, 2026
@adk-bot

adk-bot commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Response from ADK Triaging Agent

Hello @ShauryaaSharma, thank you for creating this PR!

We appreciate the comprehensive description, thorough explanation of the Vertex AI schema serialization issue, and the addition of solid unit tests.

However, looking at the checklist and details, there are a couple of points from our contribution guidelines that are still outstanding:

  • Manual End-to-End (E2E) Tests: As noted, manual E2E verification has not been performed due to lack of a Vertex AI environment. To maintain code quality and ensure this handles Vertex AI function declarations properly in real-world scenarios, we need this change to be verified end-to-end. If you are unable to verify this, perhaps you can ask the original issue reporter or a maintainer to assist in validating it in their Vertex environment.
  • Run multi-version unit tests (Tox): You mentioned that tox was blocked by a pre-existing collection error in test_verify_snippets.py. While running pytest directly is a great substitute, verifying compatibility across Python versions is required before merging.

This information and verification will help reviewers review your PR more efficiently. Thanks!

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

Labels

tools [Component] This issue is related to tools

Projects

None yet

3 participants