UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

556 lines (555 loc) 21.1 kB
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js"; import { o as isRecord } from "./record-coerce-DHZ4bFlT.js"; import { v as uniqueValues } from "./string-normalization-WNUDCpXX.js"; import { c as shouldOmitEmptyArrayItems, s as resolveUnsupportedToolSchemaKeywords } from "./provider-model-compat-DhKi-Qv5.js"; import { n as cleanSchemaForGemini, r as stripUnsupportedSchemaKeywords } from "./clean-for-gemini-B-ohtktB.js"; //#region src/agents/agent-tools-parameter-schema.ts /** * Normalizes model-facing tool parameter schemas across provider quirks. * Handles local JSON Schema refs, OpenAPI nullable syntax, top-level unions, * and provider-specific unsupported keyword stripping. */ const MAX_TOOL_PARAMETER_SCHEMA_CACHE_ENTRIES_PER_SCHEMA = 8; const toolParameterSchemaCache = /* @__PURE__ */ new WeakMap(); function resolveToolParameterSchemaCacheKey(options) { const normalizedProvider = normalizeLowercaseStringOrEmpty(options?.modelProvider); const normalizedModelId = normalizeLowercaseStringOrEmpty(options?.modelId); const unsupportedKeywords = Array.from(resolveUnsupportedToolSchemaKeywords(options?.modelCompat)).toSorted(); const omitEmptyArrayItems = shouldOmitEmptyArrayItems(options?.modelCompat); return JSON.stringify([ normalizedProvider, normalizedModelId, unsupportedKeywords, omitEmptyArrayItems ]); } function getCachedToolParameterSchema(schema, key) { return toolParameterSchemaCache.get(schema)?.find((entry) => entry.key === key)?.value; } function rememberCachedToolParameterSchema(schema, key, value) { const entries = toolParameterSchemaCache.get(schema) ?? []; toolParameterSchemaCache.set(schema, [{ key, value }, ...entries.filter((entry) => entry.key !== key)].slice(0, MAX_TOOL_PARAMETER_SCHEMA_CACHE_ENTRIES_PER_SCHEMA)); return value; } function extractEnumValues(schema) { if (!schema || typeof schema !== "object") return; const record = schema; if (Array.isArray(record.enum)) return record.enum; if ("const" in record) return [record.const]; const variants = Array.isArray(record.anyOf) ? record.anyOf : Array.isArray(record.oneOf) ? record.oneOf : null; if (variants) { const values = variants.flatMap((variant) => { return extractEnumValues(variant) ?? []; }); return values.length > 0 ? values : void 0; } } function mergePropertySchemas(existing, incoming) { if (!existing) return incoming; if (!incoming) return existing; const existingEnum = extractEnumValues(existing); const incomingEnum = extractEnumValues(incoming); if (existingEnum || incomingEnum) { const values = uniqueValues([...existingEnum ?? [], ...incomingEnum ?? []]); const merged = {}; for (const source of [existing, incoming]) { if (!source || typeof source !== "object") continue; const record = source; for (const key of [ "title", "description", "default" ]) if (!(key in merged) && key in record) merged[key] = record[key]; } const types = new Set(values.map((value) => typeof value)); if (types.size === 1) merged.type = Array.from(types)[0]; merged.enum = values; return merged; } return existing; } function setOwnSchemaProperty(target, key, value) { Object.defineProperty(target, key, { value, enumerable: true, configurable: true, writable: true }); } function hasTopLevelArrayKeyword(schemaRecord, key) { return Array.isArray(schemaRecord[key]); } function getFlattenableVariantKey(schemaRecord) { if (hasTopLevelArrayKeyword(schemaRecord, "anyOf")) return "anyOf"; if (hasTopLevelArrayKeyword(schemaRecord, "oneOf")) return "oneOf"; return null; } function getTopLevelConditionalKey(schemaRecord) { return getFlattenableVariantKey(schemaRecord) ?? (hasTopLevelArrayKeyword(schemaRecord, "allOf") ? "allOf" : null); } function hasTopLevelObjectSchema(schemaRecord, conditionalKey) { return schemaRecord.type === "object" && isRecord(schemaRecord.properties) && conditionalKey === null; } function isObjectLikeSchemaMissingType(schemaRecord, conditionalKey) { return !("type" in schemaRecord) && (isRecord(schemaRecord.properties) || Array.isArray(schemaRecord.required)) && conditionalKey === null; } function isTypedObjectSchemaMissingValidProperties(schemaRecord, conditionalKey) { return schemaRecord.type === "object" && !isRecord(schemaRecord.properties) && conditionalKey === null; } function isTrulyEmptySchema(schemaRecord) { return Object.keys(schemaRecord).length === 0; } function normalizeArraySchemasMissingItems(schema) { if (!isRecord(schema)) return schema; let changed = false; const nextSchema = { ...schema }; if (nextSchema.type === "array" && nextSchema.items === void 0) { nextSchema.items = {}; changed = true; } const normalizeSchemaValue = (key) => { if (!(key in nextSchema)) return; const value = nextSchema[key]; if (Array.isArray(value)) { const normalized = value.map(normalizeArraySchemasMissingItems); if (normalized.some((entry, index) => entry !== value[index])) { nextSchema[key] = normalized; changed = true; } return; } const normalized = normalizeArraySchemasMissingItems(value); if (normalized !== value) { nextSchema[key] = normalized; changed = true; } }; for (const key of [ "items", "contains", "additionalProperties", "propertyNames", "not", "if", "then", "else" ]) normalizeSchemaValue(key); for (const key of [ "anyOf", "oneOf", "allOf", "prefixItems" ]) normalizeSchemaValue(key); for (const key of [ "properties", "patternProperties", "dependentSchemas", "$defs", "definitions" ]) { const value = nextSchema[key]; if (!isRecord(value)) continue; let entriesChanged = false; const normalizedEntries = Object.entries(value).map(([entryKey, entryValue]) => { const normalizedEntryValue = normalizeArraySchemasMissingItems(entryValue); if (normalizedEntryValue !== entryValue) entriesChanged = true; return [entryKey, normalizedEntryValue]; }); if (entriesChanged) { nextSchema[key] = Object.fromEntries(normalizedEntries); changed = true; } } return changed ? nextSchema : schema; } function schemaAllowsArrayType(schema) { const type = schema.type; return type === "array" || Array.isArray(type) && type.includes("array"); } const ARRAY_ITEMS_SCHEMA_OBJECT_KEYS = new Set([ "additionalProperties", "contains", "else", "if", "items", "not", "propertyNames", "then" ]); const ARRAY_ITEMS_SCHEMA_ARRAY_KEYS = new Set([ "allOf", "anyOf", "oneOf", "prefixItems" ]); const ARRAY_ITEMS_SCHEMA_MAP_KEYS = new Set([ "$defs", "definitions", "dependentSchemas", "patternProperties", "properties" ]); function stripEmptyArrayItemsFromArraySchemas(schema) { if (Array.isArray(schema)) { let changed = false; const entries = schema.map((entry) => { const next = stripEmptyArrayItemsFromArraySchemas(entry); changed ||= next !== entry; return next; }); return changed ? entries : schema; } if (!isRecord(schema)) return schema; let changed = false; const entries = Object.entries(schema).flatMap(([key, value]) => { if (key === "items" && schemaAllowsArrayType(schema) && isRecord(value) && isTrulyEmptySchema(value)) { changed = true; return []; } if (ARRAY_ITEMS_SCHEMA_OBJECT_KEYS.has(key)) { const next = stripEmptyArrayItemsFromArraySchemas(value); changed ||= next !== value; return [[key, next]]; } if (ARRAY_ITEMS_SCHEMA_ARRAY_KEYS.has(key) && Array.isArray(value)) { const next = stripEmptyArrayItemsFromArraySchemas(value); changed ||= next !== value; return [[key, next]]; } if (ARRAY_ITEMS_SCHEMA_MAP_KEYS.has(key) && isRecord(value)) { let mapChanged = false; const next = Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => { const entryNext = stripEmptyArrayItemsFromArraySchemas(entryValue); mapChanged ||= entryNext !== entryValue; return [entryKey, entryNext]; })); changed ||= mapChanged; return [[key, mapChanged ? next : value]]; } return [[key, value]]; }); return changed ? Object.fromEntries(entries) : schema; } function copySchemaMeta(from, to) { for (const key of [ "title", "description", "default" ]) if (key in from && from[key] !== void 0) to[key] = from[key]; } 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 ? { $defs: new Map(defs.$defs), definitions: new Map(defs.definitions) } : { $defs: /* @__PURE__ */ new Map(), definitions: /* @__PURE__ */ new Map() }; if (defsEntry) for (const [key, value] of Object.entries(defsEntry)) next.$defs.set(key, value); if (legacyDefsEntry) for (const [key, value] of Object.entries(legacyDefsEntry)) next.definitions.set(key, value); return next; } function decodeJsonPointerSegment(segment) { return segment.replaceAll("~1", "/").replaceAll("~0", "~"); } function resolveJsonPointerPath(value, segments) { let current = value; for (const segment of segments) { if (!current || typeof current !== "object") return; const key = decodeJsonPointerSegment(segment); if (Array.isArray(current)) { const index = Number(key); if (!Number.isInteger(index) || index < 0 || index >= current.length) return; current = current[index]; continue; } const record = current; if (!Object.hasOwn(record, key)) return; current = record[key]; } return current; } function resolveLocalJsonPointer(rootDocument, ref) { if (!ref.startsWith("#/")) return; return resolveJsonPointerPath(rootDocument, ref.slice(2).split("/")); } const SCHEMA_MAP_KEYS = new Set([ "$defs", "definitions", "dependentSchemas", "patternProperties", "properties" ]); const SCHEMA_OBJECT_KEYS = new Set([ "additionalProperties", "contains", "else", "if", "items", "not", "propertyNames", "then" ]); const SCHEMA_ARRAY_KEYS = new Set([ "allOf", "anyOf", "items", "oneOf", "prefixItems" ]); const SCHEMA_LITERAL_KEYS = new Set([ "const", "default", "enum", "examples" ]); function tryResolveLocalRef(ref, defs, rootDocument) { const match = ref.match(/^#\/(\$defs|definitions)\/([^/]+)(?:\/(.*))?$/); if (match && defs) { const namespace = match[1] === "$defs" ? defs.$defs : defs.definitions; const name = decodeJsonPointerSegment(match[2] ?? ""); const resolved = name ? namespace.get(name) : void 0; if (resolved !== void 0) return resolveJsonPointerPath(resolved, match[3] ? match[3].split("/") : []); } return resolveLocalJsonPointer(rootDocument, ref); } function inlineLocalSchemaRefsWithDefs(schema, defs, refStack, state, rootDocument) { if (!schema || typeof schema !== "object") return schema; if (Array.isArray(schema)) return schema.map((entry) => inlineLocalSchemaRefsWithDefs(entry, defs, refStack, state, rootDocument)); 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, rootDocument); if (resolved === void 0) { if (refValue.startsWith("#/")) state.unresolvedLocalRefs = true; return { ...obj }; } const nextRefStack = refStack ? new Set(refStack) : /* @__PURE__ */ new Set(); nextRefStack.add(refValue); const inlined = inlineLocalSchemaRefsWithDefs(resolved, nextDefs, nextRefStack, state, rootDocument); if (!inlined || typeof inlined !== "object" || Array.isArray(inlined)) return inlined; const result = { ...inlined }; copySchemaMeta(obj, result); if (obj.nullable === true) result.nullable = true; return result; } const result = {}; for (const [key, value] of Object.entries(obj)) { if (key === "$defs" || key === "definitions" || key === "components") continue; if (SCHEMA_LITERAL_KEYS.has(key)) { setOwnSchemaProperty(result, key, value); continue; } if (SCHEMA_MAP_KEYS.has(key) && isRecord(value)) { setOwnSchemaProperty(result, key, Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, inlineLocalSchemaRefsWithDefs(entryValue, nextDefs, refStack, state, rootDocument)]))); continue; } if (SCHEMA_OBJECT_KEYS.has(key) && isRecord(value)) { setOwnSchemaProperty(result, key, inlineLocalSchemaRefsWithDefs(value, nextDefs, refStack, state, rootDocument)); continue; } if (SCHEMA_ARRAY_KEYS.has(key) && Array.isArray(value)) { setOwnSchemaProperty(result, key, value.map((entry) => inlineLocalSchemaRefsWithDefs(entry, nextDefs, refStack, state, rootDocument))); continue; } setOwnSchemaProperty(result, key, value); } if (state.unresolvedLocalRefs) { if ("$defs" in obj) result.$defs = obj.$defs; if ("definitions" in obj) result.definitions = obj.definitions; if ("components" in obj) result.components = obj.components; } return result; } /** Inline local $ref pointers so providers receive self-contained tool schemas. */ function inlineLocalToolSchemaRefs(schema) { if (!schema || typeof schema !== "object") return schema; return inlineLocalSchemaRefsWithDefs(schema, extendSchemaDefs(void 0, schema), void 0, { unresolvedLocalRefs: false }, schema); } const OPENAPI_SCHEMA_ANNOTATION_KEYS = new Set([ "discriminator", "externalDocs", "readOnly", "writeOnly", "xml", "example" ]); function appendNullSchemaType(type) { if (type === "null") return type; if (typeof type === "string") return [type, "null"]; if (Array.isArray(type)) return type.includes("null") ? type : [...type, "null"]; return type; } function isNullSchemaLike(schema) { if (!isRecord(schema)) return false; if (schema.type === "null") return true; if (Array.isArray(schema.type) && schema.type.includes("null")) return true; if ("const" in schema && schema.const === null) return true; return Array.isArray(schema.enum) && schema.enum.includes(null); } function hasOpenApiComposition(schema) { return [ "allOf", "anyOf", "oneOf" ].some((key) => Array.isArray(schema[key])); } function schemaCompositionAlreadyAllowsNull(schema) { return Array.isArray(schema.anyOf) && schema.anyOf.some(isNullSchemaLike) || Array.isArray(schema.oneOf) && schema.oneOf.some(isNullSchemaLike); } function wrapNullableComposedSchema(schema) { if (schemaCompositionAlreadyAllowsNull(schema)) return schema; const wrapped = { anyOf: [schema, { type: "null" }] }; copySchemaMeta(schema, wrapped); return wrapped; } function normalizeOpenApiSchemaKeywords(schema) { if (Array.isArray(schema)) { let changed = false; const normalized = schema.map((entry) => { const next = normalizeOpenApiSchemaKeywords(entry); changed ||= next !== entry; return next; }); return changed ? normalized : schema; } if (!isRecord(schema)) return schema; let changed = false; const nullable = schema.nullable === true; const normalized = {}; for (const [key, value] of Object.entries(schema)) { if (key === "nullable" || OPENAPI_SCHEMA_ANNOTATION_KEYS.has(key)) { changed = true; continue; } if (SCHEMA_LITERAL_KEYS.has(key)) { normalized[key] = value; continue; } if (SCHEMA_MAP_KEYS.has(key) && isRecord(value)) { let mapChanged = false; const next = Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => { const nextEntry = normalizeOpenApiSchemaKeywords(entryValue); mapChanged ||= nextEntry !== entryValue; return [entryKey, nextEntry]; })); normalized[key] = mapChanged ? next : value; changed ||= mapChanged; continue; } if (key === "components") { normalized[key] = value; continue; } if (SCHEMA_OBJECT_KEYS.has(key) && isRecord(value)) { const next = normalizeOpenApiSchemaKeywords(value); normalized[key] = next; changed ||= next !== value; continue; } if (SCHEMA_ARRAY_KEYS.has(key) && Array.isArray(value)) { const next = value.map(normalizeOpenApiSchemaKeywords); normalized[key] = next; changed ||= next.some((entry, index) => entry !== value[index]); continue; } normalized[key] = value; } if (nullable) { if (hasOpenApiComposition(normalized)) return wrapNullableComposedSchema(normalized); if ("type" in normalized) { const nextType = appendNullSchemaType(normalized.type); if (nextType !== normalized.type) normalized.type = nextType; } if (Array.isArray(normalized.enum) && !normalized.enum.includes(null)) normalized.enum = [...normalized.enum, null]; } return changed || nullable ? normalized : schema; } function normalizeToolParameterSchemaUncached(schema, options) { const inlinedSchema = normalizeOpenApiSchemaKeywords(inlineLocalToolSchemaRefs(schema)); const schemaRecord = inlinedSchema && typeof inlinedSchema === "object" ? inlinedSchema : void 0; if (!schemaRecord) return inlinedSchema; const normalizedProvider = normalizeLowercaseStringOrEmpty(options?.modelProvider); const isGeminiProvider = normalizedProvider.includes("google") || normalizedProvider.includes("gemini"); const isAnthropicProvider = normalizedProvider.includes("anthropic"); const unsupportedToolSchemaKeywords = resolveUnsupportedToolSchemaKeywords(options?.modelCompat); const omitEmptyArrayItems = shouldOmitEmptyArrayItems(options?.modelCompat); function applyProviderCleaning(s) { const normalizedSchema = normalizeArraySchemasMissingItems(s); const arrayItemsCompatibleSchema = omitEmptyArrayItems ? stripEmptyArrayItemsFromArraySchemas(normalizedSchema) : normalizedSchema; if (isGeminiProvider && !isAnthropicProvider) return cleanSchemaForGemini(arrayItemsCompatibleSchema); if (unsupportedToolSchemaKeywords.size > 0) return stripUnsupportedSchemaKeywords(arrayItemsCompatibleSchema, unsupportedToolSchemaKeywords); return arrayItemsCompatibleSchema; } const conditionalKey = getTopLevelConditionalKey(schemaRecord); const flattenableVariantKey = getFlattenableVariantKey(schemaRecord); if (hasTopLevelObjectSchema(schemaRecord, conditionalKey)) return applyProviderCleaning(schemaRecord); if (isObjectLikeSchemaMissingType(schemaRecord, conditionalKey)) return applyProviderCleaning({ ...schemaRecord, type: "object", properties: isRecord(schemaRecord.properties) ? schemaRecord.properties : {} }); if (isTypedObjectSchemaMissingValidProperties(schemaRecord, conditionalKey)) return applyProviderCleaning({ ...schemaRecord, properties: {} }); if (!flattenableVariantKey) { if (isTrulyEmptySchema(schemaRecord)) return applyProviderCleaning({ type: "object", properties: {} }); if (conditionalKey === "allOf") return applyProviderCleaning(inlinedSchema); return applyProviderCleaning(inlinedSchema); } const variants = schemaRecord[flattenableVariantKey]; const mergedProperties = {}; const requiredCounts = /* @__PURE__ */ new Map(); let objectVariants = 0; for (const entry of variants) { if (!entry || typeof entry !== "object") continue; const props = entry.properties; if (!props || typeof props !== "object") continue; objectVariants += 1; for (const [key, value] of Object.entries(props)) { if (!(key in mergedProperties)) { mergedProperties[key] = value; continue; } mergedProperties[key] = mergePropertySchemas(mergedProperties[key], value); } const required = Array.isArray(entry.required) ? entry.required : []; for (const key of required) { if (typeof key !== "string") continue; requiredCounts.set(key, (requiredCounts.get(key) ?? 0) + 1); } } const baseRequired = Array.isArray(schemaRecord.required) ? schemaRecord.required.filter((key) => typeof key === "string") : void 0; const mergedRequired = baseRequired && baseRequired.length > 0 ? baseRequired : objectVariants > 0 ? Array.from(requiredCounts.entries()).filter(([, count]) => count === objectVariants).map(([key]) => key) : void 0; const nextSchema = { ...schemaRecord }; return applyProviderCleaning({ type: "object", ...typeof nextSchema.title === "string" ? { title: nextSchema.title } : {}, ...typeof nextSchema.description === "string" ? { description: nextSchema.description } : {}, properties: Object.keys(mergedProperties).length > 0 ? mergedProperties : schemaRecord.properties ?? {}, ...mergedRequired && mergedRequired.length > 0 ? { required: mergedRequired } : {}, additionalProperties: "additionalProperties" in schemaRecord ? schemaRecord.additionalProperties : true }); } /** Return a provider-compatible JSON schema for a model-facing tool. */ function normalizeToolParameterSchema(schema, options) { if (!schema || typeof schema !== "object") return normalizeToolParameterSchemaUncached(schema, options); const cacheKey = resolveToolParameterSchemaCacheKey(options); const cached = getCachedToolParameterSchema(schema, cacheKey); if (cached) return cached; return rememberCachedToolParameterSchema(schema, cacheKey, normalizeToolParameterSchemaUncached(schema, options)); } //#endregion export { normalizeToolParameterSchema as t };