UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

279 lines (278 loc) 10.9 kB
//#region src/shared/schema-keyword-strip.ts /** Recursively remove schema keywords unsupported by a target provider/tool surface. */ function stripUnsupportedSchemaKeywords(schema, unsupportedKeywords) { if (!schema || typeof schema !== "object") return schema; if (Array.isArray(schema)) return schema.map((entry) => stripUnsupportedSchemaKeywords(entry, unsupportedKeywords)); const obj = schema; const cleaned = {}; for (const [key, value] of Object.entries(obj)) { if (unsupportedKeywords.has(key)) continue; if (key === "properties" && value && typeof value === "object" && !Array.isArray(value)) { cleaned[key] = Object.fromEntries(Object.entries(value).map(([childKey, childValue]) => [childKey, stripUnsupportedSchemaKeywords(childValue, unsupportedKeywords)])); continue; } if (key === "items" && value && typeof value === "object") { cleaned[key] = Array.isArray(value) ? value.map((entry) => stripUnsupportedSchemaKeywords(entry, unsupportedKeywords)) : stripUnsupportedSchemaKeywords(value, unsupportedKeywords); continue; } if ((key === "anyOf" || key === "oneOf" || key === "allOf") && Array.isArray(value)) { cleaned[key] = value.map((entry) => stripUnsupportedSchemaKeywords(entry, unsupportedKeywords)); continue; } cleaned[key] = value; } return cleaned; } //#endregion //#region src/agents/schema/clean-for-gemini.ts const GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS = new Set([ "patternProperties", "additionalProperties", "$schema", "$id", "$ref", "$defs", "definitions", "examples", "minLength", "maxLength", "minimum", "maximum", "multipleOf", "pattern", "format", "minItems", "maxItems", "uniqueItems", "minProperties", "maxProperties", "not" ]); const SCHEMA_META_KEYS = [ "description", "title", "default" ]; function copySchemaMeta(from, to) { for (const key of SCHEMA_META_KEYS) if (key in from && from[key] !== void 0) to[key] = from[key]; } function tryFlattenLiteralAnyOf(variants) { if (variants.length === 0) return null; const allValues = []; let commonType = null; for (const variant of variants) { if (!variant || typeof variant !== "object") return null; const v = variant; let literalValue; if ("const" in v) literalValue = v.const; else if (Array.isArray(v.enum) && v.enum.length === 1) literalValue = v.enum[0]; else return null; const variantType = typeof v.type === "string" ? v.type : null; if (!variantType) return null; if (commonType === null) commonType = variantType; else if (commonType !== variantType) return null; allValues.push(literalValue); } if (commonType && allValues.length > 0) return { type: commonType, enum: allValues }; return null; } function isNullSchema(variant) { if (!variant || typeof variant !== "object" || Array.isArray(variant)) return false; const record = variant; if ("const" in record && record.const === null) return true; if (Array.isArray(record.enum) && record.enum.length === 1) return record.enum[0] === null; const typeValue = record.type; if (typeValue === "null") return true; if (Array.isArray(typeValue) && typeValue.length === 1 && typeValue[0] === "null") return true; return false; } function stripNullVariants(variants) { if (variants.length === 0) return { variants, stripped: false }; const nonNull = variants.filter((variant) => !isNullSchema(variant)); return { variants: nonNull, stripped: nonNull.length !== variants.length }; } function extendSchemaDefs(defs, schema) { const defsEntry = schema.$defs && typeof schema.$defs === "object" && !Array.isArray(schema.$defs) ? schema.$defs : void 0; const legacyDefsEntry = schema.definitions && typeof schema.definitions === "object" && !Array.isArray(schema.definitions) ? schema.definitions : void 0; if (!defsEntry && !legacyDefsEntry) return defs; const next = defs ? new Map(defs) : /* @__PURE__ */ new Map(); if (defsEntry) for (const [key, value] of Object.entries(defsEntry)) next.set(key, value); if (legacyDefsEntry) for (const [key, value] of Object.entries(legacyDefsEntry)) next.set(key, value); return next; } function decodeJsonPointerSegment(segment) { return segment.replaceAll("~1", "/").replaceAll("~0", "~"); } function tryResolveLocalRef(ref, defs) { if (!defs) return; const match = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/); if (!match) return; const name = decodeJsonPointerSegment(match[1] ?? ""); if (!name) return; return defs.get(name); } function simplifyUnionVariants(params) { const { obj, variants } = params; const { variants: nonNullVariants, stripped } = stripNullVariants(variants); const flattened = tryFlattenLiteralAnyOf(nonNullVariants); if (flattened) { const result = { type: flattened.type, enum: flattened.enum }; copySchemaMeta(obj, result); return { variants: nonNullVariants, simplified: result }; } if (stripped && nonNullVariants.length === 1) { const lone = nonNullVariants[0]; if (lone && typeof lone === "object" && !Array.isArray(lone)) { const result = { ...lone }; copySchemaMeta(obj, result); return { variants: nonNullVariants, simplified: result }; } return { variants: nonNullVariants, simplified: lone }; } return { variants: stripped ? nonNullVariants : variants }; } function sanitizeRequiredFields(schema) { if (!Array.isArray(schema.required)) return schema; if (!schema.properties || typeof schema.properties !== "object" || Array.isArray(schema.properties)) { if (schema.type === "object") delete schema.required; return schema; } const properties = schema.properties; const required = schema.required.filter((key) => typeof key === "string" && Object.hasOwn(properties, key)); if (required.length > 0) schema.required = required; else delete schema.required; return schema; } function cleanSchemaForGeminiWithDefs(schema, defs, refStack) { if (!schema || typeof schema !== "object") return schema; if (Array.isArray(schema)) return schema.map((item) => cleanSchemaForGeminiWithDefs(item, defs, refStack)); const obj = schema; const nextDefs = extendSchemaDefs(defs, obj); const refValue = typeof obj.$ref === "string" ? obj.$ref : void 0; if (refValue) { if (refStack?.has(refValue)) return {}; const resolved = tryResolveLocalRef(refValue, nextDefs); if (resolved) { const nextRefStack = refStack ? new Set(refStack) : /* @__PURE__ */ new Set(); nextRefStack.add(refValue); const cleaned = cleanSchemaForGeminiWithDefs(resolved, nextDefs, nextRefStack); if (!cleaned || typeof cleaned !== "object" || Array.isArray(cleaned)) return cleaned; const result = { ...cleaned }; copySchemaMeta(obj, result); return result; } const result = {}; copySchemaMeta(obj, result); return result; } const hasAnyOf = "anyOf" in obj && Array.isArray(obj.anyOf); const hasOneOf = "oneOf" in obj && Array.isArray(obj.oneOf); let cleanedAnyOf = hasAnyOf ? obj.anyOf.map((variant) => cleanSchemaForGeminiWithDefs(variant, nextDefs, refStack)) : void 0; let cleanedOneOf = hasOneOf ? obj.oneOf.map((variant) => cleanSchemaForGeminiWithDefs(variant, nextDefs, refStack)) : void 0; if (hasAnyOf) { const simplified = simplifyUnionVariants({ obj, variants: cleanedAnyOf ?? [] }); cleanedAnyOf = simplified.variants; if ("simplified" in simplified) return simplified.simplified; } if (hasOneOf) { const simplified = simplifyUnionVariants({ obj, variants: cleanedOneOf ?? [] }); cleanedOneOf = simplified.variants; if ("simplified" in simplified) return simplified.simplified; } const cleaned = {}; for (const [key, value] of Object.entries(obj)) { if (GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS.has(key)) continue; if (key === "const") { cleaned.enum = [value]; continue; } if (key === "required" && Array.isArray(value) && value.length === 0) continue; if (key === "type" && (hasAnyOf || hasOneOf)) continue; if (key === "type" && Array.isArray(value) && value.every((entry) => typeof entry === "string")) { const types = value.filter((entry) => entry !== "null"); cleaned.type = types.length === 1 ? types[0] : types; continue; } if (key === "properties") if (value && typeof value === "object" && !Array.isArray(value)) cleaned[key] = Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cleanSchemaForGeminiWithDefs(v, nextDefs, refStack)])); else cleaned[key] = {}; else if (key === "items" && value) if (Array.isArray(value)) cleaned[key] = value.map((entry) => cleanSchemaForGeminiWithDefs(entry, nextDefs, refStack)); else if (typeof value === "object") cleaned[key] = cleanSchemaForGeminiWithDefs(value, nextDefs, refStack); else cleaned[key] = value; else if (key === "anyOf" && Array.isArray(value)) cleaned[key] = cleanedAnyOf ?? value.map((variant) => cleanSchemaForGeminiWithDefs(variant, nextDefs, refStack)); else if (key === "oneOf" && Array.isArray(value)) cleaned[key] = cleanedOneOf ?? value.map((variant) => cleanSchemaForGeminiWithDefs(variant, nextDefs, refStack)); else if (key === "allOf" && Array.isArray(value)) cleaned[key] = value.map((variant) => cleanSchemaForGeminiWithDefs(variant, nextDefs, refStack)); else cleaned[key] = value; } if (cleaned.anyOf && Array.isArray(cleaned.anyOf)) { const flattened = flattenUnionFallback(cleaned, cleaned.anyOf); if (flattened) return sanitizeRequiredFields(flattened); } if (cleaned.oneOf && Array.isArray(cleaned.oneOf)) { const flattened = flattenUnionFallback(cleaned, cleaned.oneOf); if (flattened) return sanitizeRequiredFields(flattened); } return sanitizeRequiredFields(cleaned); } /** * Last-resort flattening for anyOf/oneOf arrays that could not be simplified * by `simplifyUnionVariants`. Picks a representative type so the schema is * accepted by Google's restricted JSON Schema validation. */ function flattenUnionFallback(obj, variants) { const objects = variants.filter((v) => Boolean(v) && typeof v === "object"); if (objects.length === 0) return; const types = new Set(objects.map((v) => v.type).filter(Boolean)); if (objects.length === 1) { const merged = { ...objects[0] }; copySchemaMeta(obj, merged); return merged; } if (types.size === 1) { const merged = { type: Array.from(types)[0] }; copySchemaMeta(obj, merged); return merged; } const first = objects[0]; if (first?.type) { const merged = { type: first.type }; copySchemaMeta(obj, merged); return merged; } const merged = {}; copySchemaMeta(obj, merged); return merged; } function cleanSchemaForGemini(schema) { if (!schema || typeof schema !== "object") return schema; if (Array.isArray(schema)) return schema.map(cleanSchemaForGemini); return cleanSchemaForGeminiWithDefs(schema, extendSchemaDefs(void 0, schema), void 0); } //#endregion export { cleanSchemaForGemini as n, stripUnsupportedSchemaKeywords as r, GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS as t };