@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
JavaScript
/**
* 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;
}