From 159389fad2b8c85013ebe55347f2bdcfbed523e4 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Wed, 13 May 2026 02:31:59 +0000 Subject: [PATCH] fix(mcp): sanitize for gemini when model is unresolved (#697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(mcp): sanitize for gemini when model is unresolved isGeminiRouted() previously required the effective model string to contain "gemini" — but when payload.model="auto" (or any unresolved slug) reaches addTools(), `effective` is the literal "auto", which doesn't match. opencode then auto-selects gemini *after* the MCP server has registered raw arktype schemas, and every tool turn dies on `function_declarations[*].properties[*].any_of[*].enum: only allowed for STRING type`. widen the gate: any unresolved specifier (undefined / "auto" / a slug without a `provider/` prefix) is treated as gemini-routed and sanitized. the transforms are universally compatible normalizations so the false-positive cost is negligible. tighten case 3 to preserve `description` so the only lossy path no longer drops operator-facing context. fixes #676. * revert case-3 description preservation per pullfrog review on #697: keeping `description` as a peer of `anyOf`/`oneOf` directly contradicts the file's own header (lines 19-21) and the upstream opencode #14659 rationale that gates this sanitizer — gemini requires anyOf to be the ONLY field on a schema node, sibling keywords trigger `anyOf must be the only field in a schema node`. the change was speculative scope creep with no evidence, and would silently re-introduce a different gemini failure for any future schema using `.describe().or(...)`. the bug fix for #676 doesn't need it (arktype doesn't emit non-collapsible anyOf for current tool schemas). --- mcp/geminiSanitizer.ts | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/mcp/geminiSanitizer.ts b/mcp/geminiSanitizer.ts index 6028c60..c4d092c 100644 --- a/mcp/geminiSanitizer.ts +++ b/mcp/geminiSanitizer.ts @@ -176,13 +176,32 @@ export function sanitizeToolForGemini>(tool: T): T { } /** - * true when the effective upstream model is served by google's generative - * language API — directly (`google/*`), via opencode (`opencode/gemini-*`), - * or via openrouter (`openrouter/google/gemini-*`). slug-substring match - * works because every gemini route's model id contains "gemini". + * true when the effective upstream model is — or might become — google + * generative language API traffic. matches: + * - direct `google/*`, opencode `opencode/gemini-*`, openrouter + * `openrouter/google/gemini-*` (slug substring "gemini" wins). + * - any unresolved specifier: `undefined`, `"auto"`, or a slug that + * didn't map through the alias registry (no `provider/` prefix). + * these flow through the agent's own auto-select, which may land + * on gemini *after* the MCP server has already registered tools — + * at which point sanitization is too late to apply. erring on the + * side of sanitizing is safe: cases 1 + 2 are universally + * compatible JSON-Schema normalizations (enum-only → typed string, + * collapsible const-unions → string enum); case 3 is gemini- + * specific but only fires on non-collapsible unions, which arktype + * does not emit for our current tool schemas. see issue #676 for + * the prod failure that motivated this widening. */ export function isGeminiRouted(ctx: ToolContext): boolean { const effective = ctx.payload.proxyModel ?? ctx.resolvedModel ?? ctx.payload.model; - if (!effective) return false; - return effective.toLowerCase().includes("gemini"); + if (!effective) return true; + const normalized = effective.toLowerCase(); + if (normalized.includes("gemini")) return true; + // every concrete model resolved through the registry carries a + // `provider/` prefix (e.g. "anthropic/claude-opus-4-7"). anything + // without a slash is either the literal `"auto"` alias or an + // unrecognized slug that resolveModel logged a warning for — both + // route through the agent's late auto-select, which may pick gemini. + if (!normalized.includes("/")) return true; + return false; }