From 81f4a7cdb70dc2c8b59944e207ede4a58e69d219 Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 3 Jan 2026 20:43:32 +1300 Subject: [PATCH] Agents: Fix Gemini schema compatibility and robust model discovery --- src/agents/model-catalog.ts | 8 +++- src/agents/pi-tools.ts | 76 +++++++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/src/agents/model-catalog.ts b/src/agents/model-catalog.ts index 6964fbbb8..402fd7a05 100644 --- a/src/agents/model-catalog.ts +++ b/src/agents/model-catalog.ts @@ -62,8 +62,14 @@ export async function loadModelCatalog(params?: { typeof entry?.reasoning === "boolean" ? entry.reasoning : undefined; models.push({ id, name, provider, contextWindow, reasoning }); } + + if (models.length === 0) { + // If we found nothing, don't cache this result so we can try again. + modelCatalogPromise = null; + } } catch { - // Leave models empty on discovery errors. + // Leave models empty on discovery errors and don't cache. + modelCatalogPromise = null; } return models.sort((a, b) => { diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index f69e53f2e..5de878a9c 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -141,13 +141,80 @@ function mergePropertySchemas(existing: unknown, incoming: unknown): unknown { return existing; } +function cleanSchemaForGemini(schema: unknown): unknown { + if (!schema || typeof schema !== "object") return schema; + if (Array.isArray(schema)) return schema.map(cleanSchemaForGemini); + + const obj = schema as Record; + const hasAnyOf = "anyOf" in obj && Array.isArray(obj.anyOf); + const hasConst = "const" in obj; + const cleaned: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + // Skip unsupported schema features for Gemini: + // - patternProperties: not in OpenAPI 3.0 subset + // - const: convert to enum with single value instead + if (key === "patternProperties") { + // Gemini doesn't support patternProperties - skip it + continue; + } + + // Convert const to enum (Gemini doesn't support const) + if (key === "const") { + cleaned.enum = [value]; + continue; + } + + // Skip 'type' if we have 'anyOf' — Gemini doesn't allow both + if (key === "type" && hasAnyOf) { + continue; + } + + if (key === "properties" && value && typeof value === "object") { + // Recursively clean nested properties + const props = value as Record; + cleaned[key] = Object.fromEntries( + Object.entries(props).map(([k, v]) => [k, cleanSchemaForGemini(v)]) + ); + } else if (key === "items" && value && typeof value === "object") { + // Recursively clean array items schema + cleaned[key] = cleanSchemaForGemini(value); + } else if (key === "anyOf" && Array.isArray(value)) { + // Clean each anyOf variant + cleaned[key] = value.map(v => cleanSchemaForGemini(v)); + } else if (key === "oneOf" && Array.isArray(value)) { + // Clean each oneOf variant + cleaned[key] = value.map(v => cleanSchemaForGemini(v)); + } else if (key === "allOf" && Array.isArray(value)) { + // Clean each allOf variant + cleaned[key] = value.map(v => cleanSchemaForGemini(v)); + } else if (key === "additionalProperties" && value && typeof value === "object") { + // Recursively clean additionalProperties schema + cleaned[key] = cleanSchemaForGemini(value); + } else { + cleaned[key] = value; + } + } + + return cleaned; +} + function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { const schema = tool.parameters && typeof tool.parameters === "object" ? (tool.parameters as Record) : undefined; if (!schema) return tool; - if ("type" in schema && "properties" in schema) return tool; + + // If schema already has type + properties (no top-level anyOf to merge), + // still clean it for Gemini compatibility + if ("type" in schema && "properties" in schema && !Array.isArray(schema.anyOf)) { + return { + ...tool, + parameters: cleanSchemaForGemini(schema), + }; + } + if (!Array.isArray(schema.anyOf)) return tool; const mergedProperties: Record = {}; const requiredCounts = new Map(); @@ -191,10 +258,11 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { .map(([key]) => key) : undefined; + const { anyOf: _unusedAnyOf, ...restSchema } = schema; return { ...tool, - parameters: { - ...schema, + parameters: cleanSchemaForGemini({ + ...restSchema, type: "object", properties: Object.keys(mergedProperties).length > 0 @@ -205,7 +273,7 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { : {}), additionalProperties: "additionalProperties" in schema ? schema.additionalProperties : true, - }, + }), }; }