@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.
- package/CHANGELOG.md +2 -3
- package/dist/browser/neurolink.min.js +356 -356
- package/dist/providers/openaiChatCompletionsClient.js +139 -2
- package/package.json +1 -1
|
@@ -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:
|
|
538
|
+
schema: schema,
|
|
402
539
|
...(rf.description ? { description: rf.description } : {}),
|
|
403
|
-
strict:
|
|
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.
|
|
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": {
|