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

737 lines (736 loc) • 32.7 kB
import { createRequire } from "node:module"; import * as zodModule from "zod"; import { z } from "zod"; import { logger } from "./logger.js"; /** * zod-to-json-schema and json-schema-to-zod are loaded synchronously via * require() on first actual use, not via a top-level import, so merely * importing this module no longer pulls in either package for the many * callers who never convert a schema. Both ship a CJS `require` export * condition (checked against their package.json), so require() works on * any Node >=22.0.0 — no experimental flag, unlike require(esm) of a local * ES module (which only works unflagged from Node >=22.12). Zod itself * stays a normal static import: its runtime schemas (`z`, `zodModule`) are * public exports used well beyond schema conversion. Follows the pattern in * src/lib/proxy/otelLogSink.ts — construction stays one synchronous call, * exactly as before; only the *import* is deferred. * * A require() made through createRequire() is invisible to bundlers, so a * consumer that bundles the SDK without these packages next to the output can * no longer resolve them, and listing them as external changes nothing for the * same reason: the bundler never sees the call. That must not pass for a * schema that simply had no properties: the failure is reported once per * package at error level (the only level shown without NEUROLINK_DEBUG), * remembered so later calls neither retry the lookup nor repeat the report, * and the caller degrades exactly as it does when a conversion throws. The * browser artifact is the one bundle we build ourselves; * scripts/build-browser.mjs bundles both packages into it and serves them from * its `module` stub. */ const require = createRequire(import.meta.url); function lazyPackageFunction(packageName, exportName) { let attempted = false; let resolved; return () => { if (attempted) { return resolved; } attempted = true; let failure; try { const loadedModule = require(packageName); const candidate = typeof loadedModule === "object" && loadedModule !== null ? Reflect.get(loadedModule, exportName) : undefined; if (typeof candidate === "function") { resolved = candidate; } else { failure = `it resolved without a "${exportName}" function`; } } catch (error) { failure = (error instanceof Error ? error.message : String(error)).split("\n")[0]; } if (failure !== undefined) { logger.error(`[SCHEMA-CONVERSION] Cannot load "${packageName}" (${failure}). Any schema ` + `that needs it is replaced with an empty object schema, so the model ` + `sees no parameters for it. The SDK loads this package at run time ` + `and a bundler cannot see that load, so bundling the SDK does not ` + `provide it: install "${packageName}" where @juspay/neurolink can ` + `resolve it, next to your bundle if you bundle the SDK.`); } return resolved; }; } const loadZodToJsonSchema = lazyPackageFunction("zod-to-json-schema", "zodToJsonSchema"); const loadJsonSchemaToZod = lazyPackageFunction("json-schema-to-zod", "jsonSchemaToZod"); // Zod 4 ships a built-in `z.toJSONSchema(...)`. Zod 3 does not — it returned // nothing of the sort and we relied entirely on `zod-to-json-schema`. The // `zod-to-json-schema` package only understands Zod 3's internal `_def` // shape, so feeding it a Zod 4 schema yields an empty `{}` (it silently // produces `definitions: { ToolParameters: {} }`). When this happens // downstream callers send an empty `responseSchema` to the model and get // back arbitrary JSON, which is exactly how the Vertex Structured-Output // regressions surfaced. Detect the Zod 4 helper at module load and prefer // it for actual Zod schemas. // Zod 4 spells the OpenAPI target as "openapi-3.0" (with a dot) while the // third-party zod-to-json-schema package uses "openApi3". Internally we use // the latter for backwards compatibility with existing call sites; this map // translates to the dialect Zod 4 actually accepts. The Zod4Native* types // live in src/lib/types/aliases.ts per project rule 2. const zodToJsonSchemaV4 = typeof zodModule.toJSONSchema === "function" ? zodModule.toJSONSchema : undefined; /** * Resolve a deep JSON pointer path within a schema. * Handles paths like "#/definitions/ToolParameters/properties/foo/properties/bar" * * Implements RFC 6901 token decoding so property names containing the literal * characters "/" or "~" can still be resolved (their escaped forms are "~1" * and "~0" respectively). Because "#/..." is the URI-fragment form of a JSON * Pointer (RFC 6901 §6), each segment may also be percent-encoded; we decode * that first. * * Order matters: percent-decode → "~1" → "/" → "~0" → "~". Reversing the * tilde steps would let "~01" round-trip to "/" instead of the intended "~1". */ function resolveDeepRef(rootSchema, refPath) { // Strip the leading "#/" then split + decode each segment per RFC 6901 const pathParts = refPath .replace(/^#\//, "") .split("/") .map((seg) => safePercentDecode(seg).replace(/~1/g, "/").replace(/~0/g, "~")); let current = rootSchema; for (const part of pathParts) { if (current && typeof current === "object" && part in current) { current = current[part]; } else { return undefined; } } if (current && typeof current === "object") { return current; } return undefined; } /** * Percent-decode a JSON Pointer segment defensively. Falls back to the raw * segment if the input contains a malformed escape sequence (decodeURIComponent * throws URIError on those) — better to attempt a literal match than fail the * whole resolution. */ function safePercentDecode(segment) { try { return decodeURIComponent(segment); } catch { return segment; } } /** * Inline a JSON Schema by recursively resolving all $ref references. * zodToJsonSchema with 'name' option produces schemas with $ref pointing to definitions. * Some SDKs (like @google/genai) expect flat schemas without $ref. * * This function handles: * - Top-level $ref resolution * - Nested $ref within properties, items, additionalProperties * - $ref within allOf, anyOf, oneOf arrays * - Deep $ref paths like "#/definitions/Foo/properties/bar" * - Circular reference detection to prevent infinite loops */ export function inlineJsonSchema(schema, definitions, visited = new Set(), rootSchema) { // Use definitions from schema if not provided. Modern MCP servers and // zod v4's native toJSONSchema emit the 2020-12 `$defs` keyword; older // emitters use draft-07 `definitions`. Support both. const defs = definitions || (schema.definitions ?? schema.$defs); // Keep track of the root schema for deep ref resolution const root = rootSchema || schema; // Handle $ref at current level if (typeof schema.$ref === "string" && schema.$ref.startsWith("#/")) { const refPath = schema.$ref; // Prevent circular reference infinite loops if (visited.has(refPath)) { logger.debug(`[SCHEMA-INLINE] Circular reference detected for: ${refPath}`); // Return a simple object placeholder for circular refs return { type: "object" }; } // Try simple definition lookup first (#/definitions/SomeName or the // 2020-12 #/$defs/SomeName form) if (refPath.startsWith("#/definitions/") || refPath.startsWith("#/$defs/")) { const defName = refPath .replace("#/definitions/", "") .replace("#/$defs/", ""); // Check if it's a simple definition name (no slashes after the prefix) if (!defName.includes("/") && defs && defs[defName]) { visited.add(refPath); const resolved = inlineJsonSchema({ ...defs[defName] }, defs, visited, root); visited.delete(refPath); return resolved; } } // Try deep path resolution for complex paths like // #/definitions/ToolParameters/properties/accountPerformance/properties/roas const resolved = resolveDeepRef(root, refPath); if (resolved) { visited.add(refPath); const inlined = inlineJsonSchema({ ...resolved }, defs, visited, root); visited.delete(refPath); return inlined; } // Unresolved $ref: warn and preserve the original node verbatim. Falling // through to the copy loop below would strip the $ref key and silently // turn a ref-only node into an empty {}, which broadens validation // instead of failing closed. logger.warn(`[SCHEMA-INLINE] Could not resolve $ref: ${refPath}`); return { ...schema }; } // Create result without $ref and definitions const result = {}; for (const [key, value] of Object.entries(schema)) { // Skip $ref and definition-container keys (both draft-07 and 2020-12) if (key === "$ref" || key === "definitions" || key === "$defs") { continue; } // Recursively process nested schemas if (key === "properties" && value && typeof value === "object") { const properties = {}; for (const [propName, propSchema] of Object.entries(value)) { if (propSchema && typeof propSchema === "object") { properties[propName] = inlineJsonSchema(propSchema, defs, visited, root); } else { properties[propName] = propSchema; } } result[key] = properties; } else if (key === "items" && value && typeof value === "object") { // Handle array items schema if (Array.isArray(value)) { result[key] = value.map((item) => item && typeof item === "object" ? inlineJsonSchema(item, defs, visited, root) : item); } else { result[key] = inlineJsonSchema(value, defs, visited, root); } } else if (key === "additionalProperties" && value && typeof value === "object") { result[key] = inlineJsonSchema(value, defs, visited, root); } else if ((key === "allOf" || key === "anyOf" || key === "oneOf") && Array.isArray(value)) { // Handle composition schemas result[key] = value.map((item) => item && typeof item === "object" ? inlineJsonSchema(item, defs, visited, root) : item); } else if (key === "not" && value && typeof value === "object") { result[key] = inlineJsonSchema(value, defs, visited, root); } else if ((key === "if" || key === "then" || key === "else") && value && typeof value === "object") { result[key] = inlineJsonSchema(value, defs, visited, root); } else { result[key] = value; } } return result; } /** * Recursively ensure all nested schemas have a type field. * Google Vertex AI requires ALL schema objects (including nested properties) to have a type field. * This function walks through the schema tree and adds type:"object" to any object-like schema * that's missing its type field. */ export function ensureNestedSchemaTypes(schema) { if (!schema || typeof schema !== "object") { return {}; } let result = { ...schema }; // CRITICAL FIX: Flatten single-item allOf for Google Vertex AI compatibility // When we have { allOf: [{ type: "object", ... }], nullable: true }, flatten it to: // { type: "object", ..., nullable: true } if (result.allOf && Array.isArray(result.allOf) && result.allOf.length === 1 && result.allOf[0] && typeof result.allOf[0] === "object") { const innerSchema = result.allOf[0]; // Only flatten if inner schema has meaningful content (type, properties, items, etc.) if (innerSchema.type || innerSchema.properties || innerSchema.items || innerSchema.enum) { logger.debug(`[SCHEMA-TYPE-FIX] Flattening single-item allOf with type: ${innerSchema.type}`); // Merge: inner schema properties take precedence, except for wrapper's metadata const { allOf: _ignored, ...wrapperProps } = result; result = { ...innerSchema, ...wrapperProps, // Keep wrapper's nullable, description, etc. }; // If inner schema had its own nullable/description, restore them if (innerSchema.description && !wrapperProps.description) { result.description = innerSchema.description; } } } // Infer type from structure if missing if (!result.type) { // If it has properties, it's an object if (result.properties) { result.type = "object"; logger.debug(`[SCHEMA-TYPE-FIX] Added type:"object" to schema with properties`); } // If it has items, it's an array else if (result.items) { result.type = "array"; logger.debug(`[SCHEMA-TYPE-FIX] Added type:"array" to schema with items`); } // If it has enum, infer from enum values — but only when ALL elements // share the same primitive type. A mixed enum like [1, "x"] would // otherwise be silently narrowed to whatever the first element is. else if (result.enum && Array.isArray(result.enum) && result.enum.length > 0) { if (result.enum.every((v) => typeof v === "string")) { result.type = "string"; logger.debug(`[SCHEMA-TYPE-FIX] Added type:"string" to schema with enum`); } else if (result.enum.every((v) => typeof v === "number")) { result.type = "number"; logger.debug(`[SCHEMA-TYPE-FIX] Added type:"number" to schema with enum`); } // Mixed-type enum: leave result.type unset rather than narrow it. } // If it has allOf with typed schemas, infer from first item else if (result.allOf && Array.isArray(result.allOf) && result.allOf.length > 0) { const firstItem = result.allOf[0]; if (firstItem && firstItem.type) { result.type = firstItem.type; logger.debug(`[SCHEMA-TYPE-FIX] Inferred type from allOf: ${result.type}`); } } } // Recursively process properties if (result.properties && typeof result.properties === "object") { const properties = {}; for (const [propName, propSchema] of Object.entries(result.properties)) { if (propSchema && typeof propSchema === "object") { properties[propName] = ensureNestedSchemaTypes(propSchema); } else { properties[propName] = propSchema; } } result.properties = properties; } // Recursively process items (for arrays) if (result.items && typeof result.items === "object") { if (Array.isArray(result.items)) { result.items = result.items.map((item) => item && typeof item === "object" ? ensureNestedSchemaTypes(item) : item); } else { result.items = ensureNestedSchemaTypes(result.items); } } // Recursively process additionalProperties if (result.additionalProperties && typeof result.additionalProperties === "object") { result.additionalProperties = ensureNestedSchemaTypes(result.additionalProperties); } // Recursively process allOf, anyOf, oneOf for (const key of ["allOf", "anyOf", "oneOf"]) { if (result[key] && Array.isArray(result[key])) { result[key] = result[key].map((item) => item && typeof item === "object" ? ensureNestedSchemaTypes(item) : item); } } return result; } /** * Convert Zod schema to JSON Schema format for provider APIs. * * Handles three input types: * 1. Zod schemas (have `_def.typeName`) -- converted via zod-to-json-schema * 2. AI SDK `jsonSchema()` wrappers (have `.jsonSchema` property) -- extracted directly * 3. Plain JSON Schema objects (have `type`/`properties` but no `_def`) -- returned as-is */ export function convertZodToJsonSchema( // The union mirrors the three documented input shapes: Zod schemas plus // plain-object forms (AI SDK `jsonSchema()` wrappers and raw JSON Schema). zodSchema, // Default to JSON Schema draft-07 so non-Vertex consumers (Bedrock, MCP // tool registration, etc.) keep their pre-migration dialect. Vertex/Gemini // callers opt into "openApi3" explicitly to get `nullable: true` instead // of `anyOf: [..., {type: "null"}]`. target = "jsonSchema7") { const schema = zodSchema; if (!schema || typeof schema !== "object") { return { type: "object", properties: {} }; } // AI SDK jsonSchema() wrapper — extract the inner JSON Schema directly if ("jsonSchema" in schema && schema.jsonSchema !== null && typeof schema.jsonSchema === "object") { const extracted = schema.jsonSchema; return ensureNestedSchemaTypes(ensureTypeField(extracted)); } // Plain JSON Schema object (from external MCP tools) — no Zod internals if (!isZodSchema(schema)) { return ensureNestedSchemaTypes(ensureTypeField(schema)); } // Actual Zod schema — prefer Zod 4's native `z.toJSONSchema` when // available (the runtime version of `zod` here is Zod 4), then fall // back to `zod-to-json-schema` for Zod 3 schemas that external callers // might still pass in. // // Translate our `target` to Zod 4's native dialect identifier so the // openApi3 path emits the OpenAPI 3 schema shape Vertex/Gemini expect // (and not the default draft-07 anyOf/null union). if (zodToJsonSchemaV4 && isZod4Schema(zodSchema)) { const nativeTarget = target === "openApi3" ? "openapi-3.0" : "draft-07"; try { const native = zodToJsonSchemaV4(zodSchema, { target: nativeTarget, }); // Drop the $schema metadata Vertex/Gemini doesn't need, then walk to // backfill any missing nested types (Zod 4's output is already flat // — no $defs/$ref by default — but the helper is cheap and matches // the Zod 3 path's contract). const flat = { ...native }; delete flat.$schema; const inlined = inlineJsonSchema(flat); return ensureNestedSchemaTypes(ensureTypeField(inlined)); } catch (error) { logger.warn("Native z.toJSONSchema failed; falling back to zod-to-json-schema", { error: error instanceof Error ? error.message : String(error) }); } } // Zod 3 fallback path const convertWithZodToJsonSchema = loadZodToJsonSchema(); if (!convertWithZodToJsonSchema) { return { type: "object", properties: {} }; } try { // Zod 4→3 boundary: zodToJsonSchema types reference Zod 3's ZodSchema via zod/v3. // Runtime compatible — cast through unknown at this third-party boundary only. // eslint-disable-next-line no-restricted-syntax -- genuine third-party type-system boundary: zod-to-json-schema's input type is zod/v3's ZodType, structurally incompatible with the Zod 4 schema held here (no overlap, single assertion cannot compile); runtime-compatible fallback contract documented above. const zodV3Schema = zodSchema; const jsonSchema = convertWithZodToJsonSchema(zodV3Schema, { name: "ToolParameters", target, errorMessages: true, }); // zodToJsonSchema with 'name' produces { $ref: "#/definitions/ToolParameters", definitions: {...} } // Inline the $ref to produce a flat schema, ensure the root has a type // field, then walk the tree so nested objects/arrays/additionalProperties // also pick up an inferred type (Vertex/Gemini require it everywhere). const inlined = inlineJsonSchema(jsonSchema); return ensureNestedSchemaTypes(ensureTypeField(inlined)); } catch (error) { logger.warn("Failed to convert Zod schema to JSON Schema", { error: error instanceof Error ? error.message : String(error), }); return { type: "object", properties: {} }; } } export function normalizeJsonSchemaObject(schema) { return ensureTypeField(inlineJsonSchema(schema ? { ...schema } : { type: "object", properties: {} })); } /** * Ensure a JSON Schema object has a `type` field (required by Vertex/Gemini). */ function ensureTypeField(schema) { if (!schema.type) { // Schemas using composition keywords (anyOf/oneOf/allOf) deliberately omit type if (schema.anyOf || schema.oneOf || schema.allOf) { return schema; } const hadProperties = !!schema.properties; const result = { ...schema, type: "object", }; if (!result.properties) { result.properties = {}; } logger.debug("[SCHEMA-TYPE-FIX] Added missing type field to JSON Schema", { fixedType: "object", addedProperties: !hadProperties, }); return result; } return schema; } /** JSON Schema keys whose values are themselves schemas (single). */ const SCHEMA_VALUE_KEYS = new Set([ "items", "additionalProperties", "not", "if", "then", "else", "contains", "propertyNames", ]); /** JSON Schema keys whose values are arrays of schemas. */ const SCHEMA_ARRAY_KEYS = new Set(["allOf", "anyOf", "oneOf", "prefixItems"]); /** JSON Schema keys whose values are name → schema maps. */ const SCHEMA_MAP_KEYS = new Set([ "properties", "patternProperties", "$defs", "definitions", ]); /** * Annotation keys that carry no validation semantics for a model and that * generic OpenAI-compatible backends (vllm, GLM/Qwen chat templates, local * servers) may render into the prompt verbatim, confusing argument * generation. Removed only at SCHEMA positions — never inside data values * like `default`, `const`, `enum` or `examples`, where `$id` etc. are * legitimate payload. */ const SCHEMA_ANNOTATION_KEYS = new Set(["$schema", "$id", "$comment"]); /** Walk schema positions only, dropping annotation-only keys. */ function stripSchemaAnnotations(schema) { const result = {}; for (const [key, value] of Object.entries(schema)) { if (SCHEMA_ANNOTATION_KEYS.has(key)) { continue; } if (SCHEMA_VALUE_KEYS.has(key) && value && typeof value === "object" && !Array.isArray(value)) { result[key] = stripSchemaAnnotations(value); } else if (SCHEMA_ARRAY_KEYS.has(key) && Array.isArray(value)) { result[key] = value.map((item) => item && typeof item === "object" && !Array.isArray(item) ? stripSchemaAnnotations(item) : item); } else if (SCHEMA_MAP_KEYS.has(key) && value && typeof value === "object" && !Array.isArray(value)) { const map = {}; for (const [name, sub] of Object.entries(value)) { map[name] = sub && typeof sub === "object" && !Array.isArray(sub) ? stripSchemaAnnotations(sub) : sub; } result[key] = map; } else { result[key] = value; } } return result; } /** * Collapse the zod-emitted nullable pattern `anyOf/oneOf: [T, {type:"null"}]` * into `{...T, type: [t, "null"]}`. Weaker OSS models handle a plain type * array far better than composition keywords when choosing tool arguments. * Conservative: only fires for exactly-two variants where the non-null * variant has a primitive `type` string — everything else passes through. */ function collapseNullableVariants(schema) { const result = {}; for (const [key, value] of Object.entries(schema)) { if (SCHEMA_VALUE_KEYS.has(key) && value && typeof value === "object" && !Array.isArray(value)) { result[key] = collapseNullableVariants(value); } else if (SCHEMA_ARRAY_KEYS.has(key) && Array.isArray(value)) { result[key] = value.map((item) => item && typeof item === "object" && !Array.isArray(item) ? collapseNullableVariants(item) : item); } else if (SCHEMA_MAP_KEYS.has(key) && value && typeof value === "object" && !Array.isArray(value)) { const map = {}; for (const [name, sub] of Object.entries(value)) { map[name] = sub && typeof sub === "object" && !Array.isArray(sub) ? collapseNullableVariants(sub) : sub; } result[key] = map; } else { result[key] = value; } } for (const compositionKey of ["anyOf", "oneOf"]) { const variants = result[compositionKey]; if (!Array.isArray(variants) || variants.length !== 2) { continue; } const isNullVariant = (v) => !!v && typeof v === "object" && v.type === "null"; const nullIndex = variants.findIndex(isNullVariant); if (nullIndex === -1) { continue; } const other = variants[1 - nullIndex]; if (!other || typeof other !== "object" || Array.isArray(other) || typeof other.type !== "string" || other.type === "null") { continue; } // Merge: variant content + outer annotations (description etc.) win. const { [compositionKey]: _dropped, ...outer } = result; return { ...other, ...outer, type: [other.type, "null"] }; } return result; } /** * Normalize a JSON Schema for the OpenAI chat-completions `tools` wire * block. Generic proxied backends (LiteLLM → vllm/GLM/Qwen, local servers) * render tool schemas into chat templates more or less verbatim, so `$ref` * indirection, `$defs` containers, `$schema` annotations, and zod's * nullable `anyOf` pattern all measurably degrade argument generation — * this is the parity gap behind "the model can't find the right arguments" * on litellm while the same tools work on Gemini/Claude native paths * (which sanitize schemas before the wire). * * Pipeline: strip annotation keys → inline local `$ref`/`$defs`/ * `definitions` → collapse nullable variants → guarantee a top-level object * shape. Identity for already-clean schemas; `description`, `required`, * `enum`, and `default` — what models actually read — are always preserved. */ export function normalizeWireToolSchema(schema) { if (!schema || typeof schema !== "object" || Array.isArray(schema)) { return { type: "object", properties: {} }; } return ensureTypeField(collapseNullableVariants(inlineJsonSchema(stripSchemaAnnotations(schema)))); } /** * Check if a value is a Zod schema */ export function isZodSchema(value) { return !!(value && typeof value === "object" && "_def" in value && typeof value.parse === "function"); } /** * Whether a schema was built by Zod 4 specifically. * * Zod 4 hangs its internals off `_zod` (and `z.toJSONSchema` reads * `schema._zod.def`); Zod 3 has only `_def`. The two can coexist in one install * — a host on Zod 3 still resolves NeuroLink's own Zod 4 — so the presence of * `z.toJSONSchema` says nothing about the schema actually being handed to it. */ export function isZod4Schema(value) { return !!(value && typeof value === "object" && "_zod" in value); } /** * Convert JSON Schema to Zod schema format using official json-schema-to-zod library * This ensures complete preservation of all schema structure and validation rules */ export function convertJsonSchemaToZod(jsonSchema) { const startTime = Date.now(); try { // Handle empty or invalid schemas if (!jsonSchema || typeof jsonSchema !== "object") { logger.debug("🔍 [SCHEMA-CONVERSION] Invalid or empty JSON schema, using fallback"); return z.object({}).passthrough(); } // Log detailed input schema for debugging logger.debug("🔍 [SCHEMA-CONVERSION] ===== STARTING OFFICIAL LIBRARY CONVERSION ====="); logger.debug("🔍 [SCHEMA-CONVERSION] Input JSON Schema:", { type: jsonSchema.type, hasProperties: !!jsonSchema.properties, propertiesCount: jsonSchema.properties ? Object.keys(jsonSchema.properties).length : 0, requiredCount: Array.isArray(jsonSchema.required) ? jsonSchema.required.length : 0, required: jsonSchema.required, sampleProperties: jsonSchema.properties ? Object.keys(jsonSchema.properties).slice(0, 5) : [], }); // Use official library to convert JSON Schema to Zod code const convertToZodCode = loadJsonSchemaToZod(); if (!convertToZodCode) { return z.object({}).passthrough(); } const zodCodeResult = convertToZodCode(jsonSchema, { module: "esm", name: "schema", }); logger.debug("🔍 [SCHEMA-CONVERSION] Generated Zod code:", { codeLength: zodCodeResult.length, codePreview: zodCodeResult.substring(0, 200) + "...", }); // Extract the actual Zod schema expression from the generated code // Generated code looks like: "import { z } from "zod"\n\nexport const schema = z.object({...})\n" const schemaMatch = zodCodeResult.match(/export const schema = (z\..+?)(?:\n|$)/s); if (!schemaMatch) { throw new Error("Could not extract Zod schema from generated code"); } const schemaExpression = schemaMatch[1].trim(); logger.debug("🔍 [SCHEMA-CONVERSION] Extracted schema expression:", { expression: schemaExpression.substring(0, 300) + "...", }); // Use Function constructor instead of eval for better scope control const createZodSchema = new Function("z", `return ${schemaExpression}`); const zodSchema = createZodSchema(z); const conversionTime = Date.now() - startTime; logger.debug("🔍 [SCHEMA-CONVERSION] ===== CONVERSION SUCCESSFUL =====", { inputType: jsonSchema.type, propertiesCount: jsonSchema.properties ? Object.keys(jsonSchema.properties).length : 0, requiredCount: Array.isArray(jsonSchema.required) ? jsonSchema.required.length : 0, conversionSuccess: true, conversionTimeMs: conversionTime, libraryUsed: "json-schema-to-zod-official", zodSchemaType: zodSchema?.constructor?.name || "unknown", }); return zodSchema; } catch (error) { const conversionTime = Date.now() - startTime; logger.warn("🚨 [SCHEMA-CONVERSION] Official library conversion failed, using passthrough fallback", { error: error instanceof Error ? error.message : String(error), errorType: error instanceof Error ? error.constructor.name : typeof error, inputSchemaType: jsonSchema?.type, inputSchemaKeys: jsonSchema && typeof jsonSchema === "object" ? Object.keys(jsonSchema) : [], conversionTimeMs: conversionTime, libraryUsed: "json-schema-to-zod-official-FAILED", }); return z.object({}).passthrough(); } }