Repository navigation
[Bug]: Gemini models fail every turn (HTTP 400) because nullable tool parameters lack a top-level schema type #544
Description
Product or interface
CLI - interactive TUI
Version
0.6.13 (also present on main @ da22f11)
Platform
macOS
OS version and architecture
macOS 26.4, arm64
Issue area
Tool call / permission / sandbox
Steps to reproduce
- Configure an OpenAI-compatible BYOK
custom_providerthat serves a Gemini model (e.g.gemini-4-argon,api: openai-completions). - Select it as the model and send any prompt (e.g. "hi").
- The turn fails immediately;
/retryfails the same way.
Expected and actual behavior
Expected: Gemini answers like other models.
Actual: every turn fails with Error The model provider is temporarily unavailable. Code: 50113. The upstream response is HTTP 400. The same key, endpoint and model work with plain curl.
Redacted error summary
400 Unable to submit request because `update_goal` functionDeclaration `parameters.token_budget` schema didn't specify the schema type field.
400 Unable to submit request because `mavis` functionDeclaration `***.args.timeout_ms` schema didn't specify the schema type field.
Summary
With a Gemini model (gemini-4-argon via an OpenAI-compatible BYOK custom_provider), every turn fails with 50113 The model provider is temporarily unavailable. The real upstream response is HTTP 400 because two built-in tool schemas contain a nullable union without a top-level type, which Gemini function declarations reject. The same key/endpoint/model work with plain curl and other clients.
Root cause
| Tool | Parameter | Source |
|---|---|---|
update_goal |
token_budget |
packages/agent-modules/goal/src/tool-defs.ts — Type.Union([Type.Integer(...), Type.Null(...)]) |
mavis |
args.timeout_ms |
packages/agent-tools/src/desktop/local-mavis.ts — z.union([z.number().int().positive(), z.null()]) |
Both serialize to {"anyOf":[{"type":"integer","minimum":1},{"type":"null"}]}. Gemini rejects this (also when a top-level type is added next to anyOf), but accepts the equivalent {"type":["integer","null"],"minimum":1} or {"type":"integer","nullable":true}. Gemini reports only one offending property per request and not always the same one, so fixing a single definition is insufficient.
Changing the definitions to Type.Integer({ nullable: true }) would break runtime validation (Value.Check rejects null, so update_goal could no longer clear a budget), and Type.Unsafe throws Unknown type in Value.Check.
Proposed fix (patch ready, verified)
Normalize only the provider-facing schema: add toProviderToolParameters() in packages/agent-core/src/pi-turn-runner/tools.ts, which recursively rewrites anyOf: [<single-typed schema>, {type:"null"}] → {...schema, type:[<type>,"null"]} (other anyOf shapes untouched). Apply it in newTool() (main agent-loop path → pi-ai convertTools) and the two auxiliary request builders in llm.ts / turn.ts. Hook-input revalidation keeps the original TypeBox schema. Type arrays are standard JSON Schema, so other providers get an equivalent schema.
Validation done on main @ da22f11:
- New unit test
provider-tool-parameters.test.ts(5 cases; incl.UpdateGoalToolDef.schemanot mutated andValue.Checkstill acceptingnull). - pi-ai
validateToolArgumentson the normalized schema:nullaccepted,"5"coerced,0rejected. - agent-core + goal vitest: 11 files / 259 tests pass;
tsc,eslint --quiet,check:sourcepass. - Live replay of a real 47 KB MiniMax Code tool list against
gemini-4-argon: original → HTTP 400, normalized → HTTP 200. - Not run: full
pnpm verify, native Google/Vertex providers, Windows.
Since external PRs are not accepted, I can share the patch (135 lines incl. test). Happy to adjust if you prefer fixing this elsewhere (e.g. in pi-ai convertTools for Gemini-compatible endpoints).
Side note
The 400 surfaces in the TUI as a generic, retryable 50113 provider temporarily unavailable, hiding a non-retryable schema error. Surfacing the upstream message for 4xx would make this much easier to diagnose.
Activity
qiurungeng commentedon Oct 26, 2026
Proposed patch against
main@da22f11(release/public-source.jsoninventory line omitted; regenerate withnode scripts/source-inventory.mjs --write).Diff (agent-core + test registration)