Files
openclaw/extensions/clawrouter/tool-schemas.ts
Peter Steinberger 964c8c84c1 refactor: consolidate coercion ownership (#122299)
* refactor: consolidate coercion ownership

Centralize four canonical coercion helpers, migrate exact core and plugin duplicates through narrow Plugin SDK facades, and enforce declaration and plugin-normalization ownership boundaries.

The sweep adds eight focused SDK exports while deleting more production and tooling code than it adds. User-visible behavior is unchanged except for safer equivalent object and UI parsing at existing boundaries.

* fix: guard integer option ownership

Register resolveIntegerOption with the canonical function owner and extend the declaration-guard fixture so future local duplicates fail validation.

* fix: keep integer helpers on numeric facade

Remove the unshipped duplicate string-coerce exports and route every affected plugin consumer through the existing number-runtime contract.

* fix: point numeric coercion to number runtime

Make boundary and declaration diagnostics recommend the canonical numeric facade, with failing-before coverage for both guidance paths.
2026-08-11 17:14:53 -07:00

142 lines
4.0 KiB
TypeScript

import type {
AnyAgentTool,
ProviderNormalizeToolSchemasContext,
ProviderToolSchemaDiagnostic,
} from "openclaw/plugin-sdk/plugin-entry";
import { findUnsupportedSchemaKeywords } from "openclaw/plugin-sdk/provider-tools";
import { asOptionalRecord as readRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
const PERPLEXITY_UNSUPPORTED_SCHEMA_KEYWORDS = new Set([
"patternProperties",
"additionalProperties",
]);
const SCHEMA_MAP_KEYS = new Set([
"properties",
"$defs",
"definitions",
"dependentSchemas",
// Legacy `dependencies` mixes schema values with property-name arrays; the
// walker leaves non-record values untouched, so both forms stay valid.
"dependencies",
]);
const SCHEMA_VALUE_KEYS = new Set([
"items",
"additionalItems",
"prefixItems",
"anyOf",
"oneOf",
"allOf",
"then",
"else",
"if",
"not",
"contains",
"propertyNames",
"unevaluatedItems",
"unevaluatedProperties",
"contentSchema",
]);
// JSON Schema allows `type` to be an array; a union containing "object" still
// admits objects, so it needs `properties` for Perplexity too.
function isObjectType(type: unknown): boolean {
return type === "object" || (Array.isArray(type) && type.includes("object"));
}
function normalizeSchemaMap(value: unknown): unknown {
const record = readRecord(value);
if (!record) {
return value;
}
return Object.fromEntries(
Object.entries(record).map(([key, schema]) => [key, normalizePerplexitySchema(schema)]),
);
}
function normalizePerplexitySchema(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(normalizePerplexitySchema);
}
const record = readRecord(value);
if (!record) {
return value;
}
const normalized: Record<string, unknown> = {};
for (const [key, child] of Object.entries(record)) {
if (PERPLEXITY_UNSUPPORTED_SCHEMA_KEYWORDS.has(key)) {
continue;
}
normalized[key] = SCHEMA_MAP_KEYS.has(key)
? normalizeSchemaMap(child)
: SCHEMA_VALUE_KEYS.has(key)
? normalizePerplexitySchema(child)
: child;
}
if (isObjectType(normalized.type) && !("properties" in normalized)) {
normalized.properties = {};
}
return normalized;
}
function findObjectSchemasMissingProperties(schema: unknown, path: string): string[] {
if (Array.isArray(schema)) {
return schema.flatMap((child, index) =>
findObjectSchemasMissingProperties(child, `${path}[${index}]`),
);
}
const record = readRecord(schema);
if (!record) {
return [];
}
const violations =
isObjectType(record.type) && !("properties" in record) ? [`${path}.properties`] : [];
for (const [key, child] of Object.entries(record)) {
if (SCHEMA_MAP_KEYS.has(key)) {
const schemas = readRecord(child);
if (schemas) {
for (const [name, nestedSchema] of Object.entries(schemas)) {
violations.push(
...findObjectSchemasMissingProperties(nestedSchema, `${path}.${key}.${name}`),
);
}
}
continue;
}
if (SCHEMA_VALUE_KEYS.has(key)) {
violations.push(...findObjectSchemasMissingProperties(child, `${path}.${key}`));
}
}
return violations;
}
export function normalizePerplexityToolSchemas(
ctx: ProviderNormalizeToolSchemasContext,
): AnyAgentTool[] {
return ctx.tools.map((tool) => {
if (!tool.parameters || typeof tool.parameters !== "object") {
return tool;
}
return {
...tool,
parameters: normalizePerplexitySchema(tool.parameters) as AnyAgentTool["parameters"],
};
});
}
export function inspectPerplexityToolSchemas(
ctx: ProviderNormalizeToolSchemasContext,
): ProviderToolSchemaDiagnostic[] {
return ctx.tools.flatMap((tool, toolIndex) => {
const path = `${tool.name}.parameters`;
const violations = [
...findUnsupportedSchemaKeywords(
tool.parameters,
path,
PERPLEXITY_UNSUPPORTED_SCHEMA_KEYWORDS,
),
...findObjectSchemasMissingProperties(tool.parameters, path),
];
return violations.length > 0 ? [{ toolName: tool.name, toolIndex, violations }] : [];
});
}