From fe94e83f6b6c701d5a125b8723f0c6e8a590d0f8 Mon Sep 17 00:00:00 2001 From: Sean McLellan Date: Mon, 16 Feb 2026 14:25:44 -0500 Subject: [PATCH] fix: make tool schema normalization provider-aware The cleanSchemaForGemini function was being applied universally to all tools for all providers, stripping out valid JSON Schema keywords like minimum/maximum that are required by Anthropic's draft 2020-12 validation. This caused the 21st tool (web_search) to fail with google-antigravity because its count parameter's constraints were being removed. Changes: - Modified normalizeToolParameters to accept modelProvider option - Only apply Gemini-specific cleaning when provider is Gemini/Google - Skip aggressive cleaning for Anthropic/google-antigravity providers - Updated call site in createOpenClawCodingTools to pass modelProvider Fixes schema validation errors for Anthropic models served via google-antigravity. Co-Authored-By: Claude Opus 4.6 --- src/agents/pi-tools.schema.ts | 53 +++++++++++++++++++++++++---------- src/agents/pi-tools.ts | 5 +++- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/agents/pi-tools.schema.ts b/src/agents/pi-tools.schema.ts index ca8e64e08c1..41fdefb766e 100644 --- a/src/agents/pi-tools.schema.ts +++ b/src/agents/pi-tools.schema.ts @@ -62,7 +62,10 @@ function mergePropertySchemas(existing: unknown, incoming: unknown): unknown { return existing; } -export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { +export function normalizeToolParameters( + tool: AnyAgentTool, + options?: { modelProvider?: string }, +): AnyAgentTool { const schema = tool.parameters && typeof tool.parameters === "object" ? (tool.parameters as Record) @@ -75,15 +78,23 @@ export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { // - Gemini rejects several JSON Schema keywords, so we scrub those. // - OpenAI rejects function tool schemas unless the *top-level* is `type: "object"`. // (TypeBox root unions compile to `{ anyOf: [...] }` without `type`). + // - Anthropic (google-antigravity) expects full JSON Schema draft 2020-12 compliance. // // Normalize once here so callers can always pass `tools` through unchanged. + const isGeminiProvider = + options?.modelProvider?.toLowerCase().includes("google") || + options?.modelProvider?.toLowerCase().includes("gemini"); + const isAnthropicProvider = + options?.modelProvider?.toLowerCase().includes("anthropic") || + options?.modelProvider?.toLowerCase().includes("google-antigravity"); + // If schema already has type + properties (no top-level anyOf to merge), - // still clean it for Gemini compatibility + // clean it for Gemini compatibility (but only if using Gemini, not Anthropic) if ("type" in schema && "properties" in schema && !Array.isArray(schema.anyOf)) { return { ...tool, - parameters: cleanSchemaForGemini(schema), + parameters: isGeminiProvider && !isAnthropicProvider ? cleanSchemaForGemini(schema) : schema, }; } @@ -95,9 +106,13 @@ export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { !Array.isArray(schema.anyOf) && !Array.isArray(schema.oneOf) ) { + const schemaWithType = { ...schema, type: "object" }; return { ...tool, - parameters: cleanSchemaForGemini({ ...schema, type: "object" }), + parameters: + isGeminiProvider && !isAnthropicProvider + ? cleanSchemaForGemini(schemaWithType) + : schemaWithType, }; } @@ -154,26 +169,34 @@ export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { : undefined; const nextSchema: Record = { ...schema }; + const flattenedSchema = { + type: "object", + ...(typeof nextSchema.title === "string" ? { title: nextSchema.title } : {}), + ...(typeof nextSchema.description === "string" ? { description: nextSchema.description } : {}), + properties: + Object.keys(mergedProperties).length > 0 ? mergedProperties : (schema.properties ?? {}), + ...(mergedRequired && mergedRequired.length > 0 ? { required: mergedRequired } : {}), + additionalProperties: "additionalProperties" in schema ? schema.additionalProperties : true, + }; + return { ...tool, // Flatten union schemas into a single object schema: // - Gemini doesn't allow top-level `type` together with `anyOf`. // - OpenAI rejects schemas without top-level `type: "object"`. + // - Anthropic accepts proper JSON Schema with constraints. // Merging properties preserves useful enums like `action` while keeping schemas portable. - parameters: cleanSchemaForGemini({ - type: "object", - ...(typeof nextSchema.title === "string" ? { title: nextSchema.title } : {}), - ...(typeof nextSchema.description === "string" - ? { description: nextSchema.description } - : {}), - properties: - Object.keys(mergedProperties).length > 0 ? mergedProperties : (schema.properties ?? {}), - ...(mergedRequired && mergedRequired.length > 0 ? { required: mergedRequired } : {}), - additionalProperties: "additionalProperties" in schema ? schema.additionalProperties : true, - }), + parameters: + isGeminiProvider && !isAnthropicProvider + ? cleanSchemaForGemini(flattenedSchema) + : flattenedSchema, }; } +/** + * @deprecated Use normalizeToolParameters with modelProvider instead. + * This function should only be used for Gemini providers. + */ export function cleanToolSchemaForGemini(schema: Record): unknown { return cleanSchemaForGemini(schema); } diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 7ba93ab3810..21ca158a0d6 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -446,7 +446,10 @@ export function createOpenClawCodingTools(options?: { }); // Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai. // Without this, some providers (notably OpenAI) will reject root-level union schemas. - const normalized = subagentFiltered.map(normalizeToolParameters); + // Provider-specific cleaning: Gemini needs constraint keywords stripped, but Anthropic expects them. + const normalized = subagentFiltered.map((tool) => + normalizeToolParameters(tool, { modelProvider: options?.modelProvider }), + ); const withHooks = normalized.map((tool) => wrapToolWithBeforeToolCallHook(tool, { agentId,