@juspay/neurolink 12.14.1 → 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/dist/neurolink.js
CHANGED
|
@@ -4159,6 +4159,22 @@ Current user's request: ${currentInput}`;
|
|
|
4159
4159
|
middleware: options.middleware,
|
|
4160
4160
|
conversationMessages: options.conversationMessages,
|
|
4161
4161
|
credentials: options.credentials,
|
|
4162
|
+
// Extended thinking. Every provider gates it on `thinkingConfig`, so
|
|
4163
|
+
// omitting the field here meant it never reached one: the request went
|
|
4164
|
+
// out with no thinking block and the result carried no reasoning, with
|
|
4165
|
+
// nothing raised to say the option had been discarded. This is the same
|
|
4166
|
+
// failure the note above records for `disableInternalFallback` — an
|
|
4167
|
+
// allowlist that silently swallows a documented option.
|
|
4168
|
+
//
|
|
4169
|
+
// `thinkingConfig` is the only one of the documented thinking options
|
|
4170
|
+
// that `GenerateOptions` actually declares. `thinking`, `thinkingBudget`
|
|
4171
|
+
// and `thinkingLevel` are folded into a `thinkingConfig` only by the CLI,
|
|
4172
|
+
// in `src/lib/utils/thinkingConfig.ts`; nothing on the SDK path does that
|
|
4173
|
+
// merge. They exist solely on the internal `TextGenerationOptions`, so no
|
|
4174
|
+
// caller can pass them through
|
|
4175
|
+
// generate() today. Declaring them is a public-type decision and is
|
|
4176
|
+
// deliberately left out of this fix.
|
|
4177
|
+
thinkingConfig: options.thinkingConfig,
|
|
4162
4178
|
// Lifecycle callbacks must reach the provider so non-AI-SDK paths
|
|
4163
4179
|
// (Vertex's native @google/genai, native Bedrock, Ollama, etc.) can
|
|
4164
4180
|
// invoke them directly. Pipeline A also still receives them via the
|
|
@@ -292,6 +292,12 @@ const messagesToAnthropic = (msgs) => {
|
|
|
292
292
|
}
|
|
293
293
|
case "assistant": {
|
|
294
294
|
const blocks = [];
|
|
295
|
+
// Extended thinking must come back byte-identical — signature
|
|
296
|
+
// included — or Anthropic rejects the turn, and the loop replays this
|
|
297
|
+
// message on every tool step. Blocks are emitted in content order
|
|
298
|
+
// rather than hoisted: `interleaved-thinking-2025-05-14` (requested in
|
|
299
|
+
// the beta header) lets thinking appear between tool calls, so
|
|
300
|
+
// reordering would corrupt the chain it validates.
|
|
295
301
|
for (const part of partsOf(msg.content)) {
|
|
296
302
|
if (typeof part === "string") {
|
|
297
303
|
if (part.length > 0) {
|
|
@@ -300,6 +306,29 @@ const messagesToAnthropic = (msgs) => {
|
|
|
300
306
|
continue;
|
|
301
307
|
}
|
|
302
308
|
const p = part;
|
|
309
|
+
if (p?.type === "reasoning") {
|
|
310
|
+
const meta = p.providerOptions?.anthropic;
|
|
311
|
+
const redacted = meta?.redactedData;
|
|
312
|
+
if (typeof redacted === "string" && redacted.length > 0) {
|
|
313
|
+
blocks.push({ type: "redacted_thinking", data: redacted });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const signature = meta?.signature;
|
|
317
|
+
// Both halves required, matching loopAdapter's check on the
|
|
318
|
+
// streaming path: Anthropic rejects a thinking block that is
|
|
319
|
+
// unsigned, and equally one whose text is empty. Reasoning from a
|
|
320
|
+
// provider that never produced a signature (a reasoner model's
|
|
321
|
+
// plain text) is not an Anthropic thinking block at all, and an
|
|
322
|
+
// empty one carries nothing worth replaying — either way, dropping
|
|
323
|
+
// it beats sending a block that will be refused.
|
|
324
|
+
if (typeof signature === "string" &&
|
|
325
|
+
signature.length > 0 &&
|
|
326
|
+
typeof p.text === "string" &&
|
|
327
|
+
p.text.length > 0) {
|
|
328
|
+
blocks.push({ type: "thinking", thinking: p.text, signature });
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
303
332
|
if (p?.type === "text" && typeof p.text === "string") {
|
|
304
333
|
if (p.text.length > 0) {
|
|
305
334
|
const cc = cacheControlOf(p);
|
|
@@ -1118,15 +1147,28 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1118
1147
|
? { topP: options.topP }
|
|
1119
1148
|
: {}),
|
|
1120
1149
|
}, "anthropic.doGenerate");
|
|
1150
|
+
// Dropping a caller's explicit sampling parameters is exactly the
|
|
1151
|
+
// kind of silent discard this change fixes elsewhere, so say so.
|
|
1152
|
+
if (thinking &&
|
|
1153
|
+
(samplingParams.temperature !== undefined ||
|
|
1154
|
+
samplingParams.topP !== undefined)) {
|
|
1155
|
+
logger.debug("[anthropic] extended thinking is enabled, so temperature/top_p are omitted — Anthropic rejects any temperature but 1 while thinking is set");
|
|
1156
|
+
}
|
|
1121
1157
|
const params = {
|
|
1122
1158
|
model: modelId,
|
|
1123
1159
|
messages: cachedMessages,
|
|
1124
1160
|
max_tokens: resolveClaudeMaxTokens(modelId, options.maxOutputTokens),
|
|
1125
1161
|
...(system ? { system } : {}),
|
|
1126
|
-
|
|
1162
|
+
// Extended thinking fixes sampling: Anthropic rejects any
|
|
1163
|
+
// temperature but 1 while `thinking` is set, and does not honour
|
|
1164
|
+
// top_p there. The CLI always sends a default temperature, so
|
|
1165
|
+
// forwarding it alongside thinking turns a call that used to work
|
|
1166
|
+
// into a 400. Drop the sampling knobs for exactly those turns and
|
|
1167
|
+
// let Anthropic's thinking defaults stand.
|
|
1168
|
+
...(!thinking && samplingParams.temperature !== undefined
|
|
1127
1169
|
? { temperature: samplingParams.temperature }
|
|
1128
1170
|
: {}),
|
|
1129
|
-
...(samplingParams.topP !== undefined
|
|
1171
|
+
...(!thinking && samplingParams.topP !== undefined
|
|
1130
1172
|
? { top_p: samplingParams.topP }
|
|
1131
1173
|
: {}),
|
|
1132
1174
|
...(options.stopSequences && options.stopSequences.length > 0
|
|
@@ -1187,7 +1229,29 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1187
1229
|
let jsonToolAnswered = false;
|
|
1188
1230
|
for (const block of response.content) {
|
|
1189
1231
|
if (block.type === "thinking") {
|
|
1190
|
-
|
|
1232
|
+
// The signature rides along in providerOptions because Anthropic
|
|
1233
|
+
// rejects a replayed thinking block without it, and the tool loop
|
|
1234
|
+
// pushes this part straight back into the conversation.
|
|
1235
|
+
content.push({
|
|
1236
|
+
type: "reasoning",
|
|
1237
|
+
text: block.thinking,
|
|
1238
|
+
providerOptions: {
|
|
1239
|
+
anthropic: { signature: block.signature },
|
|
1240
|
+
},
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
else if (block.type === "redacted_thinking") {
|
|
1244
|
+
// Encrypted reasoning: no readable text, but it must still be
|
|
1245
|
+
// replayed verbatim or the turn is rejected.
|
|
1246
|
+
content.push({
|
|
1247
|
+
type: "reasoning",
|
|
1248
|
+
text: "",
|
|
1249
|
+
providerOptions: {
|
|
1250
|
+
anthropic: {
|
|
1251
|
+
redactedData: block.data,
|
|
1252
|
+
},
|
|
1253
|
+
},
|
|
1254
|
+
});
|
|
1191
1255
|
}
|
|
1192
1256
|
else if (block.type === "text") {
|
|
1193
1257
|
// In forced-json mode the payload arrives via the tool input, not
|
|
@@ -1824,6 +1888,9 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1824
1888
|
const streamSamplingParams = resolveSamplingParams("anthropic", modelId, options.temperature !== undefined && options.temperature !== null
|
|
1825
1889
|
? { temperature: options.temperature }
|
|
1826
1890
|
: {}, "anthropic.executeStream");
|
|
1891
|
+
if (thinking && streamSamplingParams.temperature !== undefined) {
|
|
1892
|
+
logger.debug("[anthropic] extended thinking is enabled, so temperature is omitted on the stream path — Anthropic rejects any temperature but 1 while thinking is set");
|
|
1893
|
+
}
|
|
1827
1894
|
return {
|
|
1828
1895
|
model: modelId,
|
|
1829
1896
|
messages: cachedConversation,
|
|
@@ -1833,7 +1900,9 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1833
1900
|
// the adapter immediately overwrites — and forced this whole params
|
|
1834
1901
|
// object into the streaming variant for a field it does not own.
|
|
1835
1902
|
...(payload.system ? { system: payload.system } : {}),
|
|
1836
|
-
|
|
1903
|
+
// Same constraint on the streaming path: a temperature alongside
|
|
1904
|
+
// `thinking` is rejected outright.
|
|
1905
|
+
...(!thinking && streamSamplingParams.temperature !== undefined
|
|
1837
1906
|
? { temperature: streamSamplingParams.temperature }
|
|
1838
1907
|
: {}),
|
|
1839
1908
|
...(cachedTools && cachedTools.length > 0
|
|
@@ -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/dist/types/aiCompat.d.ts
CHANGED
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": {
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
"test:new-providers": "pnpm exec tsx test/continuous-test-suite-new-providers.ts",
|
|
111
111
|
"test:matrix": "pnpm exec tsx test/continuous-test-suite-provider-matrix.ts",
|
|
112
112
|
"test:matrix:cli": "pnpm exec tsx test/continuous-test-suite-provider-matrix-cli.ts",
|
|
113
|
+
"test:thinking-config": "pnpm exec tsx test/continuous-test-suite-sdk-thinking-config.ts",
|
|
113
114
|
"test:mcp:spans": "pnpm exec tsx test/continuous-test-suite-mcp-spans.ts",
|
|
114
115
|
"test:mcp:infra": "pnpm exec tsx test/continuous-test-suite-mcp-infra.ts",
|
|
115
116
|
"test:vendor-recovery": "pnpm exec tsx test/continuous-test-suite-native-vendor-recovery.ts",
|
|
@@ -179,10 +180,10 @@
|
|
|
179
180
|
"docs:validate": "tsx tools/content/documentationSync.ts --validate",
|
|
180
181
|
"docs:generate": "pnpm run docs:validate",
|
|
181
182
|
"// Documentation (Docusaurus)": "",
|
|
182
|
-
"docs:start": "pnpm --
|
|
183
|
-
"docs:build": "pnpm --
|
|
184
|
-
"docs:serve": "pnpm --
|
|
185
|
-
"docs:clear": "pnpm --
|
|
183
|
+
"docs:start": "pnpm --dir docs-site start",
|
|
184
|
+
"docs:build": "pnpm --dir docs-site build",
|
|
185
|
+
"docs:serve": "pnpm --dir docs-site serve",
|
|
186
|
+
"docs:clear": "pnpm --dir docs-site clear",
|
|
186
187
|
"// Proxy Observability (Local OpenObserve)": "",
|
|
187
188
|
"proxy:observability:setup": "bash scripts/observability/manage-local-openobserve.sh setup",
|
|
188
189
|
"proxy:observability:up": "bash scripts/observability/manage-local-openobserve.sh up",
|