refactor: tighten tool schema types

This commit is contained in:
Peter Steinberger
2026-04-23 05:06:45 +01:00
parent 675cf823fd
commit 33aea44fe5
11 changed files with 36 additions and 37 deletions

View File

@@ -231,7 +231,7 @@ export function createOllamaWebSearchProvider(): WebSearchProviderPlugin {
createTool: (ctx) => ({
description:
"Search the web using Ollama's experimental web search API. Returns titles, URLs, and snippets from the configured Ollama host.",
parameters: OLLAMA_WEB_SEARCH_SCHEMA as unknown as Record<string, unknown>,
parameters: OLLAMA_WEB_SEARCH_SCHEMA,
execute: async (args) =>
await runOllamaWebSearch({
config: ctx.config,

View File

@@ -11,7 +11,7 @@ import {
TOOL_NAME_SEPARATOR,
} from "./pi-bundle-mcp-names.js";
import type { BundleMcpToolRuntime, SessionMcpRuntime } from "./pi-bundle-mcp-types.js";
import { asToolParameterSchema, type AnyAgentTool } from "./tools/common.js";
import type { AnyAgentTool } from "./tools/common.js";
function toAgentToolResult(params: {
serverName: string;
@@ -102,7 +102,7 @@ export async function materializeBundleMcpToolsForRun(params: {
name: safeToolName,
label: tool.title ?? tool.toolName,
description: tool.description || tool.fallbackDescription,
parameters: asToolParameterSchema(tool.inputSchema),
parameters: tool.inputSchema,
execute: async (_toolCallId: string, input: unknown) => {
const result = await params.runtime.callTool(tool.serverName, tool.toolName, input);
return toAgentToolResult({

View File

@@ -1,4 +1,5 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { TSchema } from "typebox";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { AnyAgentTool } from "./tools/common.js";
@@ -19,7 +20,7 @@ export type McpCatalogTool = {
toolName: string;
title?: string;
description?: string;
inputSchema: unknown;
inputSchema: TSchema;
fallbackDescription: string;
};

View File

@@ -1,3 +1,4 @@
import type { TSchema } from "typebox";
import type { ModelCompatConfig } from "../config/types.models.js";
import { stripUnsupportedSchemaKeywords } from "../plugin-sdk/provider-tools.js";
import { resolveUnsupportedToolSchemaKeywords } from "../plugins/provider-model-compat.js";
@@ -134,11 +135,11 @@ function isTrulyEmptySchema(schemaRecord: Record<string, unknown>): boolean {
export function normalizeToolParameterSchema(
schema: unknown,
options?: { modelProvider?: string; modelId?: string; modelCompat?: ModelCompatConfig },
): unknown {
): TSchema {
const schemaRecord =
schema && typeof schema === "object" ? (schema as Record<string, unknown>) : undefined;
if (!schemaRecord) {
return schema;
return schema as TSchema;
}
// Provider quirks:
@@ -155,14 +156,14 @@ export function normalizeToolParameterSchema(
const isAnthropicProvider = normalizedProvider.includes("anthropic");
const unsupportedToolSchemaKeywords = resolveUnsupportedToolSchemaKeywords(options?.modelCompat);
function applyProviderCleaning(s: unknown): unknown {
function applyProviderCleaning(s: unknown): TSchema {
if (isGeminiProvider && !isAnthropicProvider) {
return cleanSchemaForGemini(s);
}
if (unsupportedToolSchemaKeywords.size > 0) {
return stripUnsupportedSchemaKeywords(s, unsupportedToolSchemaKeywords);
return stripUnsupportedSchemaKeywords(s, unsupportedToolSchemaKeywords) as TSchema;
}
return s;
return s as TSchema;
}
const conditionalKey = getTopLevelConditionalKey(schemaRecord);
@@ -188,9 +189,9 @@ export function normalizeToolParameterSchema(
if (conditionalKey === "allOf") {
// Top-level `allOf` is not safely flattenable with the same heuristics we
// use for unions. Keep it explicit rather than silently rewriting it.
return schema;
return applyProviderCleaning(schema);
}
return schema;
return applyProviderCleaning(schema);
}
const variants = schemaRecord[flattenableVariantKey] as unknown[];
const mergedProperties: Record<string, unknown> = {};

View File

@@ -1,4 +1,4 @@
import { Type } from "typebox";
import { Type, type TSchema } from "typebox";
import { describe, expect, it, vi } from "vitest";
import {
cleanToolSchemaForGemini,
@@ -6,7 +6,6 @@ import {
normalizeToolParameters,
} from "./pi-tools.schema.js";
import type { AnyAgentTool } from "./pi-tools.types.js";
import { asToolParameterSchema } from "./tools/common.js";
describe("normalizeToolParameterSchema", () => {
it("normalizes truly empty schemas to type:object with properties:{}", () => {
@@ -108,12 +107,12 @@ describe("normalizeToolParameterSchema", () => {
});
});
function makeTool(parameters: unknown): AnyAgentTool {
function makeTool(parameters: TSchema): AnyAgentTool {
return {
name: "test_tool",
label: "Test Tool",
description: "test",
parameters: asToolParameterSchema(parameters),
parameters,
execute: vi.fn(),
};
}

View File

@@ -5,7 +5,6 @@ import {
type ToolParameterSchemaOptions,
} from "./pi-tools-parameter-schema.js";
import type { AnyAgentTool } from "./pi-tools.types.js";
import { asToolParameterSchema } from "./tools/common.js";
export { normalizeToolParameterSchema };
@@ -27,7 +26,7 @@ export function normalizeToolParameters(
}
return preserveToolMeta({
...tool,
parameters: asToolParameterSchema(normalizeToolParameterSchema(schema, options)),
parameters: normalizeToolParameterSchema(schema, options),
});
}

View File

@@ -1,6 +1,8 @@
// Cloud Code Assist API rejects a subset of JSON Schema keywords.
// This module scrubs/normalizes tool schemas to keep Gemini happy.
import type { TSchema } from "typebox";
// Keywords that Cloud Code Assist API rejects (not compliant with their JSON Schema subset)
export const GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS = new Set([
"patternProperties",
@@ -443,14 +445,14 @@ function flattenUnionFallback(
return merged;
}
export function cleanSchemaForGemini(schema: unknown): unknown {
export function cleanSchemaForGemini(schema: unknown): TSchema {
if (!schema || typeof schema !== "object") {
return schema;
return schema as TSchema;
}
if (Array.isArray(schema)) {
return schema.map(cleanSchemaForGemini);
return schema.map(cleanSchemaForGemini) as TSchema;
}
const defs = extendSchemaDefs(undefined, schema as Record<string, unknown>);
return cleanSchemaForGeminiWithDefs(schema, defs, undefined);
return cleanSchemaForGeminiWithDefs(schema, defs, undefined) as TSchema;
}

View File

@@ -34,10 +34,6 @@ export type AnyAgentTool = Omit<AgentTool<TSchema, unknown>, "execute"> &
displaySummary?: string;
};
export function asToolParameterSchema(schema: unknown): TSchema {
return schema as TSchema;
}
export function asToolParamsRecord(params: unknown): Record<string, unknown> {
return params && typeof params === "object" && !Array.isArray(params)
? (params as Record<string, unknown>)

View File

@@ -7,7 +7,7 @@ import {
runWebSearch,
} from "../../web-search/runtime.js";
import type { AnyAgentTool } from "./common.js";
import { asToolParameterSchema, asToolParamsRecord, jsonResult } from "./common.js";
import { asToolParamsRecord, jsonResult } from "./common.js";
import { SEARCH_CACHE } from "./web-search-provider-common.js";
export function createWebSearchTool(options?: {
@@ -37,7 +37,7 @@ export function createWebSearchTool(options?: {
label: "Web Search",
name: "web_search",
description: resolved.definition.description,
parameters: asToolParameterSchema(resolved.definition.parameters),
parameters: resolved.definition.parameters,
execute: async (_toolCallId, args) => {
const result = await runWebSearch({
config: options?.config,

View File

@@ -1,8 +1,8 @@
import type { TSchema } from "typebox";
import {
cleanSchemaForGemini,
GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS,
} from "../agents/schema/clean-for-gemini.js";
import { asToolParameterSchema } from "../agents/tools/common.js";
import type { ModelCompatConfig } from "../config/types.models.js";
import { applyModelCompatPatch } from "../plugins/provider-model-compat.js";
import type {
@@ -139,9 +139,7 @@ export function normalizeGeminiToolSchemas(
}
return {
...tool,
parameters: asToolParameterSchema(
cleanSchemaForGemini(tool.parameters as Record<string, unknown>),
),
parameters: cleanSchemaForGemini(tool.parameters),
};
});
}
@@ -172,7 +170,7 @@ export function normalizeOpenAIToolSchemas(
if (tool.parameters == null) {
return {
...tool,
parameters: asToolParameterSchema(normalizeOpenAIStrictCompatSchema({})),
parameters: normalizeOpenAIStrictCompatSchema({}),
};
}
if (typeof tool.parameters !== "object") {
@@ -180,13 +178,15 @@ export function normalizeOpenAIToolSchemas(
}
return {
...tool,
parameters: asToolParameterSchema(normalizeOpenAIStrictCompatSchema(tool.parameters)),
parameters: normalizeOpenAIStrictCompatSchema(tool.parameters),
};
});
}
function normalizeOpenAIStrictCompatSchema(schema: unknown): unknown {
return normalizeOpenAIStrictCompatSchemaRecursive(schema, { promoteEmptyObject: true });
function normalizeOpenAIStrictCompatSchema(schema: unknown): TSchema {
return normalizeOpenAIStrictCompatSchemaRecursive(schema, {
promoteEmptyObject: true,
}) as TSchema;
}
function shouldApplyOpenAIToolCompat(ctx: ProviderNormalizeToolSchemasContext): boolean {

View File

@@ -1,3 +1,4 @@
import type { TSchema } from "typebox";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { RuntimeEnv } from "../runtime.js";
import type {
@@ -12,13 +13,13 @@ export type WebFetchProviderId = string;
export type WebSearchProviderToolDefinition = {
description: string;
parameters: Record<string, unknown>;
parameters: TSchema;
execute: (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
};
export type WebFetchProviderToolDefinition = {
description: string;
parameters: Record<string, unknown>;
parameters: TSchema;
execute: (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
};