@juspay/neurolink 9.70.5 → 9.70.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.
@@ -76,7 +76,7 @@ import { resolveModel } from "./utils/modelAliasResolver.js";
76
76
  // Import orchestration components
77
77
  import { ModelRouter } from "./utils/modelRouter.js";
78
78
  import { getBestProvider } from "./utils/providerUtils.js";
79
- import { NON_RETRYABLE_HTTP_STATUS_CODES } from "./utils/retryability.js";
79
+ import { NON_RETRYABLE_HTTP_STATUS_CODES, isDeterministicClientErrorMessage, } from "./utils/retryability.js";
80
80
  import { isZodSchema } from "./utils/schemaConversion.js";
81
81
  import { BinaryTaskClassifier } from "./utils/taskClassifier.js";
82
82
  // Tool detection and execution imports
@@ -226,6 +226,15 @@ function isNonRetryableProviderError(error) {
226
226
  msg.includes("UNAUTHENTICATED")) {
227
227
  return true;
228
228
  }
229
+ // A deterministic 400 / malformed-request whose status is only present in
230
+ // the message string (e.g. Vertex wraps `{"code":400,"status":
231
+ // "INVALID_ARGUMENT"}` inside the message). The object-level status check
232
+ // above misses it, so without this the fallback orchestrator retries the
233
+ // identical bad payload on every other provider — they reject it the same
234
+ // way. The request itself is malformed, so abort fast.
235
+ if (isDeterministicClientErrorMessage(msg)) {
236
+ return true;
237
+ }
229
238
  }
230
239
  return false;
231
240
  }
@@ -49,6 +49,73 @@ function parseOrRepair(candidate) {
49
49
  return undefined;
50
50
  }
51
51
  }
52
+ /** Bounds the recursive nested-string unwrap against pathological inputs. */
53
+ const MAX_NESTED_UNWRAP_DEPTH = 6;
54
+ /**
55
+ * Recursively replace any string-valued field whose content is itself a JSON
56
+ * object/array with the parsed value. Models sometimes double-encode a NESTED
57
+ * field — e.g. `{ "attachment": "{\"k\":1}" }` instead of
58
+ * `{ "attachment": { "k": 1 } }` — which fails schema validation even though the
59
+ * intended object is right there. (`coerceJsonToSchema` already unwraps a
60
+ * stringified TOP-LEVEL object; this handles the nested case.)
61
+ *
62
+ * A parsed string is NOT re-descended into: its own string fields (e.g. an
63
+ * attachment's `content`) are the model's intended values and must be left
64
+ * alone. Recursion only walks already-structural objects/arrays to find
65
+ * stringified fields anywhere in the tree. Returns a NEW value (never mutates
66
+ * the input) plus whether anything changed, so the caller can skip a redundant
67
+ * re-validation when nothing was unwrapped. Callers MUST re-validate the result
68
+ * against the schema — that gate is what keeps an over-eager unwrap (a field
69
+ * that should stay a string) from being accepted.
70
+ */
71
+ function deepUnwrapJsonStrings(value, depth = 0) {
72
+ if (depth > MAX_NESTED_UNWRAP_DEPTH) {
73
+ return { value, changed: false };
74
+ }
75
+ if (typeof value === "string") {
76
+ const s = value.trim();
77
+ const looksJson = (s.startsWith("{") && s.endsWith("}")) ||
78
+ (s.startsWith("[") && s.endsWith("]"));
79
+ if (looksJson) {
80
+ try {
81
+ const parsed = JSON.parse(s);
82
+ if (parsed !== null && typeof parsed === "object") {
83
+ // Parsed one stringified layer. Do NOT descend into `parsed` — its
84
+ // own string fields are intended values, not double-encodings.
85
+ return { value: parsed, changed: true };
86
+ }
87
+ }
88
+ catch {
89
+ // not JSON — leave the string as-is
90
+ }
91
+ }
92
+ return { value, changed: false };
93
+ }
94
+ if (Array.isArray(value)) {
95
+ let changed = false;
96
+ const out = value.map((item) => {
97
+ const r = deepUnwrapJsonStrings(item, depth + 1);
98
+ if (r.changed) {
99
+ changed = true;
100
+ }
101
+ return r.value;
102
+ });
103
+ return { value: changed ? out : value, changed };
104
+ }
105
+ if (value !== null && typeof value === "object") {
106
+ let changed = false;
107
+ const out = {};
108
+ for (const [k, v] of Object.entries(value)) {
109
+ const r = deepUnwrapJsonStrings(v, depth + 1);
110
+ if (r.changed) {
111
+ changed = true;
112
+ }
113
+ out[k] = r.value;
114
+ }
115
+ return { value: changed ? out : value, changed };
116
+ }
117
+ return { value, changed: false };
118
+ }
52
119
  /**
53
120
  * Try to produce canonical JSON from `text`. Returns null when no JSON object
54
121
  * could be recovered (caller should then keep the raw text).
@@ -147,6 +214,24 @@ export function coerceJsonToSchema(text, schema) {
147
214
  if (safeParseable.safeParse(outcome.value).success) {
148
215
  schemaValid.push(record);
149
216
  }
217
+ else {
218
+ // The model may have double-encoded a NESTED field as a JSON string
219
+ // (e.g. `{"attachment":"{...}"}` instead of `{"attachment":{...}}`),
220
+ // which fails validation even though the intended object is present.
221
+ // Unwrap stringified object/array fields and re-validate before giving
222
+ // up — the safeParse gate rejects any over-eager unwrap.
223
+ const unwrapped = deepUnwrapJsonStrings(outcome.value);
224
+ if (unwrapped.changed &&
225
+ unwrapped.value !== null &&
226
+ typeof unwrapped.value === "object" &&
227
+ safeParseable.safeParse(unwrapped.value).success) {
228
+ schemaValid.push({
229
+ value: unwrapped.value,
230
+ repaired: true,
231
+ truncated: candidate.truncated,
232
+ });
233
+ }
234
+ }
150
235
  }
151
236
  // Among schema-valid candidates prefer the MOST COMPLETE one. With nullable
152
237
  // fields a lean object (e.g. `{summary, attachment: null}`) validates
@@ -12,3 +12,16 @@ export declare const NON_RETRYABLE_HTTP_STATUS_CODES: readonly number[];
12
12
  export declare function isRetryableStatusCode(code: number): boolean;
13
13
  /** Check whether an HTTP status code is non-retryable. */
14
14
  export declare function isNonRetryableStatusCode(code: number): boolean;
15
+ /**
16
+ * Detect a deterministic client-error (HTTP 400 / malformed request) signature
17
+ * embedded in a provider error MESSAGE, for cases where the numeric status is
18
+ * not exposed as a structured `status`/`statusCode` field. Vertex/Gemini wrap a
19
+ * 400 INVALID_ARGUMENT inside the message string (e.g. `Google Vertex AI Invalid
20
+ * Request: {"error":{"code":400,"status":"INVALID_ARGUMENT", …}}`), so the
21
+ * object-level status check misses it and the fallback orchestrator keeps
22
+ * retrying the identical, malformed payload on every other provider — they
23
+ * reject it the same way. A 400 means the request itself is bad, so retrying is
24
+ * pointless regardless of provider. Matches only unambiguous markers to avoid
25
+ * misclassifying transient (5xx/429) failures.
26
+ */
27
+ export declare function isDeterministicClientErrorMessage(message: string): boolean;
@@ -20,4 +20,26 @@ export function isRetryableStatusCode(code) {
20
20
  export function isNonRetryableStatusCode(code) {
21
21
  return NON_RETRYABLE_HTTP_STATUS_CODES.includes(code);
22
22
  }
23
+ /**
24
+ * Detect a deterministic client-error (HTTP 400 / malformed request) signature
25
+ * embedded in a provider error MESSAGE, for cases where the numeric status is
26
+ * not exposed as a structured `status`/`statusCode` field. Vertex/Gemini wrap a
27
+ * 400 INVALID_ARGUMENT inside the message string (e.g. `Google Vertex AI Invalid
28
+ * Request: {"error":{"code":400,"status":"INVALID_ARGUMENT", …}}`), so the
29
+ * object-level status check misses it and the fallback orchestrator keeps
30
+ * retrying the identical, malformed payload on every other provider — they
31
+ * reject it the same way. A 400 means the request itself is bad, so retrying is
32
+ * pointless regardless of provider. Matches only unambiguous markers to avoid
33
+ * misclassifying transient (5xx/429) failures.
34
+ */
35
+ export function isDeterministicClientErrorMessage(message) {
36
+ if (!message) {
37
+ return false;
38
+ }
39
+ return (message.includes("INVALID_ARGUMENT") ||
40
+ message.includes("Invalid JSON payload") ||
41
+ /\bInvalid Request\b/i.test(message) ||
42
+ /"code"\s*:\s*400\b/.test(message) ||
43
+ /\b400\s+Bad Request\b/i.test(message));
44
+ }
23
45
  //# sourceMappingURL=retryability.js.map
package/dist/neurolink.js CHANGED
@@ -76,7 +76,7 @@ import { resolveModel } from "./utils/modelAliasResolver.js";
76
76
  // Import orchestration components
77
77
  import { ModelRouter } from "./utils/modelRouter.js";
78
78
  import { getBestProvider } from "./utils/providerUtils.js";
79
- import { NON_RETRYABLE_HTTP_STATUS_CODES } from "./utils/retryability.js";
79
+ import { NON_RETRYABLE_HTTP_STATUS_CODES, isDeterministicClientErrorMessage, } from "./utils/retryability.js";
80
80
  import { isZodSchema } from "./utils/schemaConversion.js";
81
81
  import { BinaryTaskClassifier } from "./utils/taskClassifier.js";
82
82
  // Tool detection and execution imports
@@ -226,6 +226,15 @@ function isNonRetryableProviderError(error) {
226
226
  msg.includes("UNAUTHENTICATED")) {
227
227
  return true;
228
228
  }
229
+ // A deterministic 400 / malformed-request whose status is only present in
230
+ // the message string (e.g. Vertex wraps `{"code":400,"status":
231
+ // "INVALID_ARGUMENT"}` inside the message). The object-level status check
232
+ // above misses it, so without this the fallback orchestrator retries the
233
+ // identical bad payload on every other provider — they reject it the same
234
+ // way. The request itself is malformed, so abort fast.
235
+ if (isDeterministicClientErrorMessage(msg)) {
236
+ return true;
237
+ }
229
238
  }
230
239
  return false;
231
240
  }
@@ -49,6 +49,73 @@ function parseOrRepair(candidate) {
49
49
  return undefined;
50
50
  }
51
51
  }
52
+ /** Bounds the recursive nested-string unwrap against pathological inputs. */
53
+ const MAX_NESTED_UNWRAP_DEPTH = 6;
54
+ /**
55
+ * Recursively replace any string-valued field whose content is itself a JSON
56
+ * object/array with the parsed value. Models sometimes double-encode a NESTED
57
+ * field — e.g. `{ "attachment": "{\"k\":1}" }` instead of
58
+ * `{ "attachment": { "k": 1 } }` — which fails schema validation even though the
59
+ * intended object is right there. (`coerceJsonToSchema` already unwraps a
60
+ * stringified TOP-LEVEL object; this handles the nested case.)
61
+ *
62
+ * A parsed string is NOT re-descended into: its own string fields (e.g. an
63
+ * attachment's `content`) are the model's intended values and must be left
64
+ * alone. Recursion only walks already-structural objects/arrays to find
65
+ * stringified fields anywhere in the tree. Returns a NEW value (never mutates
66
+ * the input) plus whether anything changed, so the caller can skip a redundant
67
+ * re-validation when nothing was unwrapped. Callers MUST re-validate the result
68
+ * against the schema — that gate is what keeps an over-eager unwrap (a field
69
+ * that should stay a string) from being accepted.
70
+ */
71
+ function deepUnwrapJsonStrings(value, depth = 0) {
72
+ if (depth > MAX_NESTED_UNWRAP_DEPTH) {
73
+ return { value, changed: false };
74
+ }
75
+ if (typeof value === "string") {
76
+ const s = value.trim();
77
+ const looksJson = (s.startsWith("{") && s.endsWith("}")) ||
78
+ (s.startsWith("[") && s.endsWith("]"));
79
+ if (looksJson) {
80
+ try {
81
+ const parsed = JSON.parse(s);
82
+ if (parsed !== null && typeof parsed === "object") {
83
+ // Parsed one stringified layer. Do NOT descend into `parsed` — its
84
+ // own string fields are intended values, not double-encodings.
85
+ return { value: parsed, changed: true };
86
+ }
87
+ }
88
+ catch {
89
+ // not JSON — leave the string as-is
90
+ }
91
+ }
92
+ return { value, changed: false };
93
+ }
94
+ if (Array.isArray(value)) {
95
+ let changed = false;
96
+ const out = value.map((item) => {
97
+ const r = deepUnwrapJsonStrings(item, depth + 1);
98
+ if (r.changed) {
99
+ changed = true;
100
+ }
101
+ return r.value;
102
+ });
103
+ return { value: changed ? out : value, changed };
104
+ }
105
+ if (value !== null && typeof value === "object") {
106
+ let changed = false;
107
+ const out = {};
108
+ for (const [k, v] of Object.entries(value)) {
109
+ const r = deepUnwrapJsonStrings(v, depth + 1);
110
+ if (r.changed) {
111
+ changed = true;
112
+ }
113
+ out[k] = r.value;
114
+ }
115
+ return { value: changed ? out : value, changed };
116
+ }
117
+ return { value, changed: false };
118
+ }
52
119
  /**
53
120
  * Try to produce canonical JSON from `text`. Returns null when no JSON object
54
121
  * could be recovered (caller should then keep the raw text).
@@ -147,6 +214,24 @@ export function coerceJsonToSchema(text, schema) {
147
214
  if (safeParseable.safeParse(outcome.value).success) {
148
215
  schemaValid.push(record);
149
216
  }
217
+ else {
218
+ // The model may have double-encoded a NESTED field as a JSON string
219
+ // (e.g. `{"attachment":"{...}"}` instead of `{"attachment":{...}}`),
220
+ // which fails validation even though the intended object is present.
221
+ // Unwrap stringified object/array fields and re-validate before giving
222
+ // up — the safeParse gate rejects any over-eager unwrap.
223
+ const unwrapped = deepUnwrapJsonStrings(outcome.value);
224
+ if (unwrapped.changed &&
225
+ unwrapped.value !== null &&
226
+ typeof unwrapped.value === "object" &&
227
+ safeParseable.safeParse(unwrapped.value).success) {
228
+ schemaValid.push({
229
+ value: unwrapped.value,
230
+ repaired: true,
231
+ truncated: candidate.truncated,
232
+ });
233
+ }
234
+ }
150
235
  }
151
236
  // Among schema-valid candidates prefer the MOST COMPLETE one. With nullable
152
237
  // fields a lean object (e.g. `{summary, attachment: null}`) validates
@@ -12,3 +12,16 @@ export declare const NON_RETRYABLE_HTTP_STATUS_CODES: readonly number[];
12
12
  export declare function isRetryableStatusCode(code: number): boolean;
13
13
  /** Check whether an HTTP status code is non-retryable. */
14
14
  export declare function isNonRetryableStatusCode(code: number): boolean;
15
+ /**
16
+ * Detect a deterministic client-error (HTTP 400 / malformed request) signature
17
+ * embedded in a provider error MESSAGE, for cases where the numeric status is
18
+ * not exposed as a structured `status`/`statusCode` field. Vertex/Gemini wrap a
19
+ * 400 INVALID_ARGUMENT inside the message string (e.g. `Google Vertex AI Invalid
20
+ * Request: {"error":{"code":400,"status":"INVALID_ARGUMENT", …}}`), so the
21
+ * object-level status check misses it and the fallback orchestrator keeps
22
+ * retrying the identical, malformed payload on every other provider — they
23
+ * reject it the same way. A 400 means the request itself is bad, so retrying is
24
+ * pointless regardless of provider. Matches only unambiguous markers to avoid
25
+ * misclassifying transient (5xx/429) failures.
26
+ */
27
+ export declare function isDeterministicClientErrorMessage(message: string): boolean;
@@ -20,3 +20,25 @@ export function isRetryableStatusCode(code) {
20
20
  export function isNonRetryableStatusCode(code) {
21
21
  return NON_RETRYABLE_HTTP_STATUS_CODES.includes(code);
22
22
  }
23
+ /**
24
+ * Detect a deterministic client-error (HTTP 400 / malformed request) signature
25
+ * embedded in a provider error MESSAGE, for cases where the numeric status is
26
+ * not exposed as a structured `status`/`statusCode` field. Vertex/Gemini wrap a
27
+ * 400 INVALID_ARGUMENT inside the message string (e.g. `Google Vertex AI Invalid
28
+ * Request: {"error":{"code":400,"status":"INVALID_ARGUMENT", …}}`), so the
29
+ * object-level status check misses it and the fallback orchestrator keeps
30
+ * retrying the identical, malformed payload on every other provider — they
31
+ * reject it the same way. A 400 means the request itself is bad, so retrying is
32
+ * pointless regardless of provider. Matches only unambiguous markers to avoid
33
+ * misclassifying transient (5xx/429) failures.
34
+ */
35
+ export function isDeterministicClientErrorMessage(message) {
36
+ if (!message) {
37
+ return false;
38
+ }
39
+ return (message.includes("INVALID_ARGUMENT") ||
40
+ message.includes("Invalid JSON payload") ||
41
+ /\bInvalid Request\b/i.test(message) ||
42
+ /"code"\s*:\s*400\b/.test(message) ||
43
+ /\b400\s+Bad Request\b/i.test(message));
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.70.5",
3
+ "version": "9.70.7",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applications with 21+ providers: OpenAI, Anthropic, Google AI Studio, Google Vertex, AWS Bedrock, Azure OpenAI, Mistral, LiteLLM, SageMaker, Hugging Face, Ollama, OpenAI-compatible, OpenRouter, DeepSeek, NVIDIA NIM, LM Studio, llama.cpp, plus voice (OpenAI TTS, ElevenLabs, Deepgram, Azure Speech).",
6
6
  "author": {
@@ -418,7 +418,7 @@
418
418
  "@changesets/changelog-github": "^0.6.0",
419
419
  "@changesets/cli": "^2.29.8",
420
420
  "@eslint/js": "^10.0.1",
421
- "@juspay/hippocampus": "^0.1.6",
421
+ "@juspay/hippocampus": ">=0.1.7",
422
422
  "@opentelemetry/api": "^1.9.0",
423
423
  "@opentelemetry/sdk-trace-base": "^2.6.0",
424
424
  "@opentelemetry/sdk-trace-node": "^2.6.0",