UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

144 lines (143 loc) • 5.15 kB
/** * Gemini function-calling schema sanitizer. * * Normalises an MCP tool's JSON Schema into the subset Gemini's function-calling * accepts. `@google/genai`'s processJsonSchema crashes on untyped nodes, * `$ref`/`$defs`, and some `anyOf`/`oneOf` shapes, so we rebuild a clean tree: * every node gets a concrete `type`, unions collapse to their first concrete * branch, and unsupported keywords are dropped. * * Pure, dependency-free, and assertion-free — values arrive as `unknown` and are * narrowed with the `isRecord` guard. Safe to reuse for any Gemini tool path. */ const GEMINI_TYPES = new Set([ "string", "number", "integer", "boolean", "array", "object", ]); function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } /** * The first concrete (non-`"null"`-typed) branch of an `anyOf`/`oneOf`/`allOf` * union, or `undefined` when there is no union to collapse. */ function firstConcreteUnionBranch(schema) { const union = schema.anyOf ?? schema.oneOf ?? schema.allOf; if (!Array.isArray(union)) { return undefined; } return union.find((branch) => isRecord(branch) && branch.type !== "null"); } function resolveSchemaType(schema) { if (typeof schema.type === "string") { return GEMINI_TYPES.has(schema.type) ? schema.type : "string"; } if (Array.isArray(schema.type)) { const named = schema.type.find((entry) => typeof entry === "string" && entry !== "null"); if (named !== undefined && GEMINI_TYPES.has(named)) { return named; } } if (isRecord(schema.properties)) { return "object"; } if (schema.items !== undefined) { return "array"; } return "string"; } function sanitizeObjectMembers(schema, out) { const properties = isRecord(schema.properties) ? schema.properties : {}; const sanitizedProperties = {}; for (const [key, value] of Object.entries(properties)) { sanitizedProperties[key] = sanitizeSchema(value); } out.properties = sanitizedProperties; if (Array.isArray(schema.required)) { out.required = schema.required.filter((name) => typeof name === "string" && name in sanitizedProperties); } if (Object.keys(sanitizedProperties).length === 0) { out.additionalProperties = true; } } /** * Rebuild a JSON Schema node into the Gemini-safe subset. Returns a fresh object * with a concrete `type` on every node. */ export function sanitizeSchema(node) { if (!isRecord(node)) { return { type: "string" }; } const out = {}; if (typeof node.description === "string") { out.description = node.description; } if (typeof node.type !== "string") { const branch = firstConcreteUnionBranch(node); if (branch !== undefined) { const merged = sanitizeSchema(branch); return out.description ? { ...merged, description: out.description } : merged; } } const type = resolveSchemaType(node); out.type = type; if (Array.isArray(node.enum)) { out.enum = node.enum; } if (type === "object") { sanitizeObjectMembers(node, out); } if (type === "array") { out.items = sanitizeSchema(node.items); } return out; } /** Tool parameters must be an object schema; force it and sanitize the tree. */ export function sanitizeToolParameters(schema) { const sanitized = sanitizeSchema(schema ?? {}); if (sanitized.type !== "object") { return { type: "object", properties: {}, additionalProperties: true }; } return sanitized; } /** * Walk a (sanitized) schema and return the first node the google plugin would * turn into `undefined` — which genai then crashes on. Returns a human-readable * path/reason, or `null` if the schema is safe. After `sanitizeSchema` this * should always be `null`; if not, it names the exact offending path. */ export function findSchemaIssue(node, pathPrefix = "$") { if (!isRecord(node)) { return `${pathPrefix}: not an object schema`; } if (typeof node.type !== "string") { return `${pathPrefix}: missing string "type"`; } if (node.type === "object") { const properties = isRecord(node.properties) ? node.properties : undefined; const isEmpty = properties === undefined || Object.keys(properties).length === 0; if (isEmpty && (node.additionalProperties === undefined || node.additionalProperties === null)) { return `${pathPrefix}: empty object schema without additionalProperties (plugin → undefined)`; } if (properties !== undefined) { for (const [key, value] of Object.entries(properties)) { const childIssue = findSchemaIssue(value, `${pathPrefix}.${key}`); if (childIssue) { return childIssue; } } } } if (node.type === "array") { return findSchemaIssue(node.items, `${pathPrefix}[]`); } return null; }