Skip to content

fix(agent): flatten gemini optional tool schema unions - #207

Merged
BegoniaHe merged 5 commits into
masterfrom
fix/issue-204-gemini-mcp-null-unions
Sep 16, 2026
Merged

BegoniaHe merged 5 commits into
masterfrom
fix/issue-204-gemini-mcp-null-unions

Conversation

@BegoniaHe

Copy link
Copy Markdown
Collaborator

Summary

Gemini rejected MCP tools whose optional fields were encoded as JSON Schema null unions (anyOf/oneOf/type lists). The whole tool request failed with 400 (schema didn't specify the schema type field) before any tool ran.

This change flattens those unions at send time to { "type": "<non-null>", "nullable": true } for native google_schema() and for OpenAI-compatible Gemini payloads ("gemini" in model). Stored MCP schemas and real OpenAI Chat Completions tool payloads stay JSON Schema.

Related issue

Fixes #204

Root cause

Pydantic/FastMCP emit legal JSON Schema for Optional[str]. Gemini FunctionDeclaration Schema is an OpenAPI 3.0 subset and requires a sibling type; type: "null" is not allowed.

AstrBot stored the schema correctly, then:

  • ToolSet.google_schema() returned anyOf unchanged and mapped oneOf to "type": "null"
  • the OpenAI-compatible Gemini path only omitted empty parameters, so the same anyOf went out as-is
  • google_gemini_openai is still type: openai_chat_completions, so switching the preset was not a workaround

Reproduction

Unit, no live MCP:

  1. Build a FunctionTool whose parameters.properties.category is anyOf: [{type: string}, {type: null}].
  2. Before the fix, ToolSet([tool]).google_schema() emitted anyOf with no type.
  3. openai_chat_completions_schema() forwarded the same shape even when the model name contained gemini.

Implementation notes

Add a send-time copy helper in astrbot/core/agent/tool.py that collapses a null union only when exactly one non-null branch remains. Native Gemini runs it before convert_schema. OpenAI-compatible Gemini opts in through flatten_null_unions=True next to the existing omit-empty heuristic. Remaining non-null unions (string | integer) stay anyOf; the helper does not invent type: "object". MCP ingest is unchanged.

Validation

uv run ruff format astrbot/core/agent/tool.py astrbot/core/tools/function_tool_manager.py astrbot/core/provider/sources/openai_chat_completions_source.py tests/unit/test_tool_google_schema.py tests/unit/test_mcp_client_schema.py tests/unit/provider/test_openai_chat_completions_source.py
uv run ruff check astrbot/core/agent/tool.py astrbot/core/tools/function_tool_manager.py astrbot/core/provider/sources/openai_chat_completions_source.py tests/unit/test_tool_google_schema.py tests/unit/test_mcp_client_schema.py tests/unit/provider/test_openai_chat_completions_source.py
uv run pytest tests/unit/test_tool_google_schema.py tests/unit/test_mcp_client_schema.py::TestMCPToolSchemaNormalization tests/unit/provider/test_openai_chat_completions_source.py -q

Result: ruff format/check passed; pytest 86 passed.

Did not run make check or make test-blocking (slice is tool-schema send paths only).

Compatibility and risk

Checklist

  • The change is focused and does not include unrelated refactoring.
  • I added or updated a regression test, or explained why a test is not practical.
  • I ran the relevant formatting, lint, build, and test commands.
  • User-visible behavior updates both docs/zh/ and docs/en/ when needed.
  • OpenAPI, generated client, docs/public/openapi.json, and tests change together when routes or schemas change.
  • No secrets committed. Runtime Python deps update pyproject.toml, requirements.txt, and uv.lock together.
  • I did not restore legacy shims, Python <3.14 fallbacks, or upstream publish/docs URLs as fork artifacts.
  • Breaking API or behavior changes use ! and a BREAKING CHANGE: footer.
  • I will not merge this PR myself. Merge needs a human maintainer review plus a separate AI-assisted review (AI_POLICY.md).
  • AI use follows AI_POLICY.md. Keep exactly one author note below. Do not fabricate the other.

Agent note

Goal: fix Gemini 400s on MCP optional fields by flattening null unions at send time only.

Paths: astrbot/core/agent/tool.py, astrbot/core/tools/function_tool_manager.py, astrbot/core/provider/sources/openai_chat_completions_source.py, tests/unit/test_tool_google_schema.py, tests/unit/test_mcp_client_schema.py, tests/unit/provider/test_openai_chat_completions_source.py.

Checks run: ruff format + ruff check on those files; uv run pytest of the three test files above (86 passed). Residual risk is leftover non-null unions and the existing Gemini model-name heuristic.

Tools: OpenCode / plan-issue skill for the plan, then this session for implementation.

Collapse anyOf/oneOf/type-list null unions at send time so Gemini
FunctionDeclaration nodes have a sibling type. Keep stored MCP JSON
Schema and real OpenAI Chat Completions payloads unchanged.

Fixes #204
AI-Generated: true
Generated-At: 2026-09-16T19:06:44Z
@BegoniaHe BegoniaHe self-assigned this Sep 16, 2026
Do not attach nullable to remaining anyOf/oneOf or mixed type lists.
Skip JSON Schema instance keys so default/enum values are not treated
as schemas. Coerce model names before the Gemini heuristic.

Fixes #204
AI-Generated: true
Generated-At: 2026-09-16T19:31:15Z
Comment thread astrbot/core/provider/sources/openai_chat_completions_source.py Fixed
Do not attach nullable to remaining anyOf/oneOf or untyped $ref
branches. Drop the unused model assignment that CodeQL flagged.

Fixes #204
AI-Generated: true
Generated-At: 2026-09-16T19:51:21Z
Refresh the tracked icon subset after Dashboard validation added
cpu-64-bit, monitor, shield-alert-outline, and shield-off-outline.

AI-Generated: true
Generated-At: 2026-09-16T19:55:19Z
Send album items immediately and keep debounce longer than max_wait so
macOS CI scheduling jitter cannot flush the album before the third
item arrives.

AI-Generated: true
Generated-At: 2026-09-16T20:12:00Z
@BegoniaHe
BegoniaHe merged commit d27c951 into master Sep 16, 2026
25 checks passed
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] Gemini rejects MCP tools that encode optional fields as anyOf null unions

2 participants