@juspay/neurolink 12.14.2 → 12.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -387,6 +387,118 @@ export const v3ToolChoiceToOpenAI = (choice, toolNameToWire) => {
387
387
  };
388
388
  }
389
389
  };
390
+ /**
391
+ * OpenAI's strict structured-output mode rejects a schema unless every object
392
+ * node carries `additionalProperties: false` AND lists every one of its
393
+ * properties in `required` — recursively, including through array `items`.
394
+ * A plain JSON Schema satisfies neither, so sending one with `strict: true`
395
+ * fails the request outright rather than degrading.
396
+ *
397
+ * Adding `additionalProperties: false` is safe: it forbids keys the caller
398
+ * never asked for, which strict mode would forbid anyway. Filling in
399
+ * `required` is NOT safe — it would silently make the caller's optional
400
+ * fields mandatory. So when a schema still has optional properties after
401
+ * normalisation, the request drops to `strict: false`, which OpenAI accepts
402
+ * and which honours optionality. Callers whose schemas are already strict-
403
+ * compatible keep the stronger guarantee.
404
+ */
405
+ // `properties` and `$defs` are MAPS of schemas, not schemas — recursing into
406
+ // them as if they were nodes silently skips every child, which is exactly the
407
+ // bug that let a nested object through without `additionalProperties: false`.
408
+ const SCHEMA_MAPS = [
409
+ "properties",
410
+ "patternProperties",
411
+ "$defs",
412
+ "definitions",
413
+ ];
414
+ const SCHEMA_NODES = [
415
+ "items",
416
+ "prefixItems",
417
+ "anyOf",
418
+ "oneOf",
419
+ "allOf",
420
+ "not",
421
+ "then",
422
+ "else",
423
+ ];
424
+ // OpenAI's strict mode does not accept these composition keywords. A schema
425
+ // carrying one cannot be sent with `strict: true` at all, so it is not merely
426
+ // "not yet normalised" — it must drop to non-strict, where the schema is
427
+ // honoured as written.
428
+ const STRICT_UNSUPPORTED = ["allOf", "not", "if", "then", "else"];
429
+ const mapValues = (obj, fn) => Object.fromEntries(Object.entries((obj ?? {})).map(([k, v]) => [
430
+ k,
431
+ fn(v),
432
+ ]));
433
+ const withClosedObjects = (node) => {
434
+ if (Array.isArray(node)) {
435
+ return node.map(withClosedObjects);
436
+ }
437
+ if (!node || typeof node !== "object") {
438
+ return node;
439
+ }
440
+ const next = {
441
+ ...node,
442
+ };
443
+ for (const key of SCHEMA_MAPS) {
444
+ if (key in next) {
445
+ next[key] = mapValues(next[key], withClosedObjects);
446
+ }
447
+ }
448
+ for (const key of SCHEMA_NODES) {
449
+ if (key in next) {
450
+ next[key] = withClosedObjects(next[key]);
451
+ }
452
+ }
453
+ // A schema-valued `additionalProperties` is itself a schema (the index/value
454
+ // pattern) and its nested objects need closing too. A boolean one is a flag
455
+ // and must be left exactly as the caller wrote it.
456
+ if (next.additionalProperties &&
457
+ typeof next.additionalProperties === "object") {
458
+ next.additionalProperties = withClosedObjects(next.additionalProperties);
459
+ }
460
+ else if (next.type === "object" && !("additionalProperties" in next)) {
461
+ // Closed whether or not it declares `properties`: strict mode requires the
462
+ // key on EVERY object, including an empty one.
463
+ next.additionalProperties = false;
464
+ }
465
+ return next;
466
+ };
467
+ /**
468
+ * True when the schema can legally be sent with `strict: true`: every object
469
+ * node lists all of its properties as required, every object is closed, and
470
+ * no composition keyword OpenAI rejects appears anywhere.
471
+ *
472
+ * Deliberately conservative — a false negative costs only the stronger
473
+ * guarantee, while a false positive costs the whole request.
474
+ */
475
+ const satisfiesStrictRequired = (node) => {
476
+ if (Array.isArray(node)) {
477
+ return node.every(satisfiesStrictRequired);
478
+ }
479
+ if (!node || typeof node !== "object") {
480
+ return true;
481
+ }
482
+ const rec = node;
483
+ if (STRICT_UNSUPPORTED.some((k) => k in rec)) {
484
+ return false;
485
+ }
486
+ if (rec.type === "object") {
487
+ if (rec.additionalProperties !== false) {
488
+ return false;
489
+ }
490
+ const names = Object.keys((rec.properties ?? {}));
491
+ const required = Array.isArray(rec.required)
492
+ ? rec.required
493
+ : [];
494
+ if (names.some((n) => !required.includes(n))) {
495
+ return false;
496
+ }
497
+ }
498
+ const mapsOk = SCHEMA_MAPS.filter((k) => k in rec).every((k) => Object.values((rec[k] ?? {})).every(satisfiesStrictRequired));
499
+ const nodesOk = SCHEMA_NODES.filter((k) => k in rec).every((k) => satisfiesStrictRequired(rec[k]));
500
+ return mapsOk && nodesOk;
501
+ };
390
502
  export const v3ResponseFormatToOpenAI = (rf) => {
391
503
  if (rf.type === "text") {
392
504
  return { type: "text" };
@@ -394,13 +506,38 @@ export const v3ResponseFormatToOpenAI = (rf) => {
394
506
  if (!rf.schema) {
395
507
  return { type: "json_object" };
396
508
  }
509
+ // Mutate as little as possible, in this order:
510
+ //
511
+ // 1. already strict-legal -> send it UNTOUCHED with strict: true
512
+ // 2. legal once closed -> send the closed copy with strict: true
513
+ // 3. neither -> send it UNTOUCHED with strict: false
514
+ //
515
+ // Case 3 is why closure is not applied unconditionally. Non-strict mode
516
+ // honours the schema exactly as written, so injecting
517
+ // `additionalProperties: false` there would silently change the caller's
518
+ // contract — and for a composition it can make the schema unsatisfiable:
519
+ // closing two `allOf` members that declare different properties leaves no
520
+ // object able to satisfy both. Leaving case 3 untouched also means
521
+ // non-OpenAI endpoints in this family, which may not implement OpenAI's
522
+ // strict contract at all, see exactly the schema the caller wrote.
523
+ //
524
+ // Case 1 matters for parity: a Zod schema whose properties are all required
525
+ // already converts to a strict-legal shape, so it goes out byte-identical to
526
+ // what it did before this change.
527
+ const original = rf.schema;
528
+ const closed = withClosedObjects(original);
529
+ const schema = satisfiesStrictRequired(original)
530
+ ? original
531
+ : satisfiesStrictRequired(closed)
532
+ ? closed
533
+ : original;
397
534
  return {
398
535
  type: "json_schema",
399
536
  json_schema: {
400
537
  name: rf.name ?? "response",
401
- schema: rf.schema,
538
+ schema: schema,
402
539
  ...(rf.description ? { description: rf.description } : {}),
403
- strict: true,
540
+ strict: satisfiesStrictRequired(schema),
404
541
  },
405
542
  };
406
543
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.14.2",
3
+ "version": "12.14.3",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {