@juspay/neurolink 12.7.5 → 12.7.7
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 -2
- package/dist/browser/neurolink.min.js +171 -168
- package/dist/core/baseProvider.js +30 -1
- package/dist/providers/anthropic/client.js +36 -17
- package/dist/proxy/claudeFormat.js +22 -0
- package/dist/types/conversation.d.ts +7 -0
- package/dist/types/generate.d.ts +13 -4
- package/dist/utils/conversationMemory.js +9 -1
- package/dist/utils/errorHandling.js +5 -1
- package/package.json +1 -1
|
@@ -1260,7 +1260,18 @@ export class BaseProvider {
|
|
|
1260
1260
|
// never enforced on this path, and enforcing them now would break
|
|
1261
1261
|
// long-running generations that have always been allowed.
|
|
1262
1262
|
const descriptorGenerateMs = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName)?.timeouts?.generateMs;
|
|
1263
|
-
|
|
1263
|
+
// An explicit, valid turnTimeoutMs is the caller's whole-turn contract
|
|
1264
|
+
// and owns this hard abort; `timeout` then keeps its per-model-call
|
|
1265
|
+
// meaning (it reaches the model layer via providerOptions.neurolink).
|
|
1266
|
+
// Before this, `timeout` alone bounded the ENTIRE multi-step loop, so a
|
|
1267
|
+
// caller asking for a 40-minute turn of 5-minute calls was killed at 5
|
|
1268
|
+
// minutes flat — mid-loop, dressed as "Request was aborted.".
|
|
1269
|
+
const hasValidTurnTimeout = typeof options.turnTimeoutMs === "number" &&
|
|
1270
|
+
Number.isFinite(options.turnTimeoutMs) &&
|
|
1271
|
+
options.turnTimeoutMs > 0;
|
|
1272
|
+
const effectiveTimeout = hasValidTurnTimeout
|
|
1273
|
+
? options.turnTimeoutMs
|
|
1274
|
+
: (options.timeout ?? Math.max(descriptorGenerateMs ?? 0, 180_000));
|
|
1264
1275
|
const timeoutController = createTimeoutController(effectiveTimeout, this.providerName, "generate");
|
|
1265
1276
|
const composedSignal = composeAbortSignals(options.abortSignal, timeoutController?.controller.signal);
|
|
1266
1277
|
const composedOptions = composedSignal
|
|
@@ -1270,6 +1281,24 @@ export class BaseProvider {
|
|
|
1270
1281
|
try {
|
|
1271
1282
|
generateResult = await this.executeGeneration(model, messages, tools, composedOptions);
|
|
1272
1283
|
}
|
|
1284
|
+
catch (error) {
|
|
1285
|
+
// When OUR timer fired, provider SDKs typically normalize the abort
|
|
1286
|
+
// into their own generic cancel shape (e.g. Anthropic's
|
|
1287
|
+
// APIUserAbortError, "Request was aborted.") and discard the signal's
|
|
1288
|
+
// reason. The TimeoutError on the signal is the honest identity —
|
|
1289
|
+
// rethrow it so logs and abort classification see a timeout, not a
|
|
1290
|
+
// caller cancel. A genuine caller abort (their signal fired) keeps its
|
|
1291
|
+
// original shape even if our timer also expired in the race window.
|
|
1292
|
+
const reason = timeoutController?.controller.signal.aborted
|
|
1293
|
+
? timeoutController.controller.signal.reason
|
|
1294
|
+
: undefined;
|
|
1295
|
+
if (reason instanceof TimeoutError &&
|
|
1296
|
+
isAbortError(error) &&
|
|
1297
|
+
options.abortSignal?.aborted !== true) {
|
|
1298
|
+
throw reason;
|
|
1299
|
+
}
|
|
1300
|
+
throw error;
|
|
1301
|
+
}
|
|
1273
1302
|
finally {
|
|
1274
1303
|
timeoutController?.cleanup();
|
|
1275
1304
|
}
|
|
@@ -26,7 +26,7 @@ import { stringifyAnthropicToolOutput } from "./toolOutput.js";
|
|
|
26
26
|
import { createAnthropicLoopAdapter } from "./loopAdapter.js";
|
|
27
27
|
import { runAgenticLoop } from "../../core/loopEngine.js";
|
|
28
28
|
import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../../utils/providerConfig.js";
|
|
29
|
-
import { composeAbortSignals, createTimeoutController, mergeAbortSignals, } from "../../utils/timeout.js";
|
|
29
|
+
import { composeAbortSignals, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../../utils/timeout.js";
|
|
30
30
|
import { resolveToolChoice } from "../../utils/toolChoice.js";
|
|
31
31
|
import { emitToolEndFromStepFinish } from "../../utils/toolEndEmitter.js";
|
|
32
32
|
import { NoOutputGeneratedError } from "../../utils/generationErrors.js";
|
|
@@ -1126,28 +1126,47 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1126
1126
|
...(toolChoice ? { tool_choice: toolChoice } : {}),
|
|
1127
1127
|
...(thinking ? { thinking } : {}),
|
|
1128
1128
|
};
|
|
1129
|
-
// The
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1134
|
-
//
|
|
1135
|
-
//
|
|
1136
|
-
//
|
|
1137
|
-
|
|
1138
|
-
const
|
|
1139
|
-
.
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1129
|
+
// The caller's resolved `timeout` reaches this layer only through
|
|
1130
|
+
// providerOptions.neurolink.timeoutMs (AI-SDK call options carry no
|
|
1131
|
+
// `timeout`; the old `options.timeout` read here never fired on V3).
|
|
1132
|
+
// An explicit value is a per-call contract: never floored, never
|
|
1133
|
+
// extended. Without one, the 60s anthropic default was tuned for the
|
|
1134
|
+
// old ~4096 max_tokens — now that the default ceiling is the model's
|
|
1135
|
+
// real max, raise the floor to 5 min when a large output budget is
|
|
1136
|
+
// in play. The abort signal stays the real bound.
|
|
1137
|
+
const neurolinkNs = options.providerOptions?.neurolink;
|
|
1138
|
+
const forwardedTimeoutMs = typeof neurolinkNs?.timeoutMs === "number" &&
|
|
1139
|
+
Number.isFinite(neurolinkNs.timeoutMs) &&
|
|
1140
|
+
neurolinkNs.timeoutMs > 0
|
|
1141
|
+
? neurolinkNs.timeoutMs
|
|
1142
|
+
: undefined;
|
|
1143
|
+
const generateTimeoutMs = forwardedTimeoutMs !== undefined
|
|
1144
|
+
? forwardedTimeoutMs
|
|
1145
|
+
: params.max_tokens > 8192
|
|
1146
|
+
? Math.max(getTimeoutForOptions(options), 300_000)
|
|
1147
|
+
: getTimeoutForOptions(options);
|
|
1144
1148
|
const timeoutController = createTimeoutController(generateTimeoutMs, providerName, "generate");
|
|
1149
|
+
const requestSignal = composeAbortSignals(options.abortSignal, timeoutController?.controller.signal);
|
|
1145
1150
|
let response;
|
|
1146
1151
|
try {
|
|
1147
1152
|
response = await client.messages.create(params, {
|
|
1148
|
-
signal:
|
|
1153
|
+
signal: requestSignal,
|
|
1149
1154
|
});
|
|
1150
1155
|
}
|
|
1156
|
+
catch (error) {
|
|
1157
|
+
// The Anthropic SDK collapses ANY fired signal into its generic
|
|
1158
|
+
// APIUserAbortError ("Request was aborted."), discarding the
|
|
1159
|
+
// signal's reason. When the abort came from one of NeuroLink's own
|
|
1160
|
+
// timers (this per-call timer, or the turn-level one upstream),
|
|
1161
|
+
// the TimeoutError reason is the honest identity — surface it.
|
|
1162
|
+
const reason = requestSignal?.aborted
|
|
1163
|
+
? requestSignal.reason
|
|
1164
|
+
: undefined;
|
|
1165
|
+
if (reason instanceof TimeoutError) {
|
|
1166
|
+
throw reason;
|
|
1167
|
+
}
|
|
1168
|
+
throw error;
|
|
1169
|
+
}
|
|
1151
1170
|
finally {
|
|
1152
1171
|
timeoutController?.cleanup();
|
|
1153
1172
|
}
|
|
@@ -71,6 +71,28 @@ export function parseClaudeRequest(body) {
|
|
|
71
71
|
for (let msgIdx = 0; msgIdx < body.messages.length; msgIdx++) {
|
|
72
72
|
const msg = body.messages[msgIdx];
|
|
73
73
|
const isLatestUserMsg = msgIdx === lastUserMsgIdx;
|
|
74
|
+
// The Messages API restricts `messages[].role` to "user"/"assistant" —
|
|
75
|
+
// system prompts are a separate top-level field — but nothing at the
|
|
76
|
+
// wire boundary enforces that. A client that inlines a "system" message
|
|
77
|
+
// here would otherwise flow straight into conversationMessages and land
|
|
78
|
+
// at a non-leading index once translated to an OpenAI-shaped request for
|
|
79
|
+
// a fallback provider (e.g. LiteLLM/vLLM), whose chat templates reject
|
|
80
|
+
// any system message not at index 0. Fold it into systemPrompt instead,
|
|
81
|
+
// mirroring parseOpenAIRequest's handling of inline system messages.
|
|
82
|
+
if (msg.role === "system") {
|
|
83
|
+
const text = typeof msg.content === "string"
|
|
84
|
+
? msg.content
|
|
85
|
+
: Array.isArray(msg.content)
|
|
86
|
+
? msg.content
|
|
87
|
+
.map((b) => (b.type === "text" ? b.text : ""))
|
|
88
|
+
.filter((t) => t.length > 0)
|
|
89
|
+
.join("\n")
|
|
90
|
+
: "";
|
|
91
|
+
if (text) {
|
|
92
|
+
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${text}` : text;
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
74
96
|
if (typeof msg.content === "string") {
|
|
75
97
|
conversationMessages.push({ role: msg.role, content: msg.content });
|
|
76
98
|
if (msg.role === "user") {
|
|
@@ -60,6 +60,13 @@ export type ConversationMemoryConfig = {
|
|
|
60
60
|
summarizationProvider?: string;
|
|
61
61
|
/** Model to use for summarization */
|
|
62
62
|
summarizationModel?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Wall-clock cap for one summarization generate call, in milliseconds
|
|
65
|
+
* (default: 60000). A summary that overruns is dropped, not fatal — the
|
|
66
|
+
* turn continues without it — so size this for the slowest summary a real
|
|
67
|
+
* conversation produces rather than losing compaction summaries silently.
|
|
68
|
+
*/
|
|
69
|
+
summarizationTimeoutMs?: number;
|
|
63
70
|
/** Memory SDK config (condensed key-value memory per user). Set enabled: true to activate. */
|
|
64
71
|
memory?: HippocampusMemory;
|
|
65
72
|
/** Redis configuration (optional) - overrides environment variables */
|
package/dist/types/generate.d.ts
CHANGED
|
@@ -348,7 +348,14 @@ export type GenerateOptions = {
|
|
|
348
348
|
* multi-step tool loop (Vertex Gemini / Vertex Claude), this bounds EACH
|
|
349
349
|
* model call in the loop, not the whole turn — a tool-heavy turn may run
|
|
350
350
|
* far longer than this value in total. Size it for the slowest single
|
|
351
|
-
* step (default 300s), and use `abortSignal` for a
|
|
351
|
+
* step (default 300s), and use `turnTimeoutMs` (or `abortSignal`) for a
|
|
352
|
+
* total-turn deadline.
|
|
353
|
+
*
|
|
354
|
+
* On the AI-SDK loop path (direct Anthropic, litellm, OpenAI-compatible)
|
|
355
|
+
* the same split holds only when `turnTimeoutMs` is ALSO set: then this
|
|
356
|
+
* value bounds each model call and `turnTimeoutMs` bounds the turn. With
|
|
357
|
+
* `turnTimeoutMs` unset, this value bounds the WHOLE turn there (the
|
|
358
|
+
* pre-existing defensive behavior, kept for backward compatibility).
|
|
352
359
|
*
|
|
353
360
|
* When set explicitly, a step timeout is surfaced immediately instead of
|
|
354
361
|
* burning internal retries/fallbacks that would re-run the same
|
|
@@ -363,9 +370,11 @@ export type GenerateOptions = {
|
|
|
363
370
|
* imposes no product policy).
|
|
364
371
|
*
|
|
365
372
|
* Enforced by the native Vertex loops (Gemini + Claude) AND the AI-SDK
|
|
366
|
-
* loop path (litellm and other OpenAI-compatible
|
|
367
|
-
* path
|
|
368
|
-
* `
|
|
373
|
+
* loop path (direct Anthropic, litellm and other OpenAI-compatible
|
|
374
|
+
* providers). On the AI-SDK path this value also owns the whole-turn hard
|
|
375
|
+
* abort: when set, `timeout` keeps its per-model-call meaning instead of
|
|
376
|
+
* bounding the entire loop. An explicit `timeout` also engages the same
|
|
377
|
+
* wrap-up when `turnTimeoutMs` is unset. Once the wrap-up window begins (see
|
|
369
378
|
* `wrapupTimeLeadMs`), the loop forcibly sets `toolChoice: "none"` for the
|
|
370
379
|
* remaining steps — overriding any caller-supplied `toolChoice` or
|
|
371
380
|
* `prepareStep` tool selection — and appends an honest time message that a
|
|
@@ -593,7 +593,15 @@ export function getEffectiveTokenThreshold(provider, model, envOverride, session
|
|
|
593
593
|
export async function generateSummary(messages, config, logPrefix = "[ConversationMemory]", previousSummary, requestId) {
|
|
594
594
|
const summarizationPrompt = createSummarizationPrompt(messages, previousSummary);
|
|
595
595
|
const SUMMARIZER_INIT_TIMEOUT = 15_000;
|
|
596
|
-
|
|
596
|
+
// Config-driven: a compaction summary of a large conversation routinely
|
|
597
|
+
// needs more than the old hard-coded 60s, and each overrun silently loses
|
|
598
|
+
// one summary (non-fatal — the turn continues) with no knob to raise it.
|
|
599
|
+
const configuredTimeoutMs = config.summarizationTimeoutMs;
|
|
600
|
+
const SUMMARIZER_GENERATE_TIMEOUT = typeof configuredTimeoutMs === "number" &&
|
|
601
|
+
Number.isFinite(configuredTimeoutMs) &&
|
|
602
|
+
configuredTimeoutMs > 0
|
|
603
|
+
? configuredTimeoutMs
|
|
604
|
+
: 60_000;
|
|
597
605
|
try {
|
|
598
606
|
if (!cachedSummarizer) {
|
|
599
607
|
cachedSummarizer = await withTimeout((async () => {
|
|
@@ -1189,7 +1189,11 @@ export function isAbortError(error) {
|
|
|
1189
1189
|
if (error instanceof Error &&
|
|
1190
1190
|
(error.message?.includes("This operation was aborted") ||
|
|
1191
1191
|
error.message?.includes("The operation was aborted") ||
|
|
1192
|
-
error.message?.includes("The user aborted a request")
|
|
1192
|
+
error.message?.includes("The user aborted a request") ||
|
|
1193
|
+
// Anthropic SDK's APIUserAbortError — name is plain "Error", so only
|
|
1194
|
+
// the message identifies it. Missing this classified real aborts as
|
|
1195
|
+
// provider failures (ERROR log + fallback consulted on a user cancel).
|
|
1196
|
+
error.message?.includes("Request was aborted"))) {
|
|
1193
1197
|
return true;
|
|
1194
1198
|
}
|
|
1195
1199
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.7.
|
|
3
|
+
"version": "12.7.7",
|
|
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": {
|