fix(agent): flatten gemini optional tool schema unions - #207
Merged
Merged
Conversation
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
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
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
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
Gemini rejected MCP tools whose optional fields were encoded as JSON Schema null unions (
anyOf/oneOf/typelists). 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 nativegoogle_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 siblingtype;type: "null"is not allowed.AstrBot stored the schema correctly, then:
ToolSet.google_schema()returnedanyOfunchanged and mappedoneOfto"type": "null"parameters, so the sameanyOfwent out as-isgoogle_gemini_openaiis stilltype: openai_chat_completions, so switching the preset was not a workaroundReproduction
Unit, no live MCP:
FunctionToolwhoseparameters.properties.categoryisanyOf: [{type: string}, {type: null}].ToolSet([tool]).google_schema()emittedanyOfwith notype.openai_chat_completions_schema()forwarded the same shape even when the model name containedgemini.Implementation notes
Add a send-time copy helper in
astrbot/core/agent/tool.pythat collapses a null union only when exactly one non-null branch remains. Native Gemini runs it beforeconvert_schema. OpenAI-compatible Gemini opts in throughflatten_null_unions=Truenext to the existing omit-empty heuristic. Remaining non-null unions (string | integer) stayanyOf; the helper does not inventtype: "object". MCP ingest is unchanged.Validation
Result: ruff format/check passed; pytest 86 passed.
Did not run
make checkormake test-blocking(slice is tool-schema send paths only).Compatibility and risk
openai_chat_completions_schema(..., *, flatten_null_unions=False)is additive; default keeps JSON Schema."gemini" in modelis the same heuristic already used to omit emptyparameters.Checklist
docs/zh/anddocs/en/when needed.docs/public/openapi.json, and tests change together when routes or schemas change.pyproject.toml,requirements.txt, anduv.locktogether.!and aBREAKING CHANGE:footer.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 pytestof 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.