@juspay/neurolink 9.80.3 → 9.80.4

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.
@@ -7,6 +7,8 @@ import { ErrorCategory, ErrorSeverity, } from "../constants/enums.js";
7
7
  import { BaseProvider } from "../core/baseProvider.js";
8
8
  import { DEFAULT_GEMINI_STREAM_TIMEOUT_MS, DEFAULT_MAX_STEPS, DEFAULT_TOOL_MAX_RETRIES, GLOBAL_LOCATION_MODELS, IMAGE_GENERATION_MODELS, TOOL_STORAGE_TIMEOUT_MS, } from "../core/constants.js";
9
9
  import { ModelConfigurationManager } from "../core/modelConfiguration.js";
10
+ import { isSchemaComplexityError } from "../core/modules/structuredOutputPolicy.js";
11
+ import { stringifyContentSafe } from "../utils/logSanitize.js";
10
12
  import { createProxyFetch } from "../proxy/proxyFetch.js";
11
13
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
12
14
  import { ERROR_CODES, NeuroLinkError } from "../utils/errorHandling.js";
@@ -2351,7 +2353,25 @@ export class GoogleVertexProvider extends BaseProvider {
2351
2353
  wasAborted = true;
2352
2354
  break;
2353
2355
  }
2354
- logger.error("[GoogleVertex] Native SDK generate error", error);
2356
+ logger.error("[GoogleVertex] Native SDK generate error", {
2357
+ error,
2358
+ model: modelName,
2359
+ location: effectiveLocation,
2360
+ status: error?.status,
2361
+ });
2362
+ // Best-effort request context for formatProviderError —
2363
+ // this.modelName can be stale when options.model overrides the
2364
+ // instance default.
2365
+ try {
2366
+ if (error && typeof error === "object") {
2367
+ const e = error;
2368
+ e.requestModel = modelName;
2369
+ e.requestRegion = effectiveLocation;
2370
+ }
2371
+ }
2372
+ catch {
2373
+ /* frozen/sealed error — context stays best-effort */
2374
+ }
2355
2375
  throw this.handleProviderError(error);
2356
2376
  }
2357
2377
  }
@@ -2585,9 +2605,7 @@ export class GoogleVertexProvider extends BaseProvider {
2585
2605
  if (msg.role === "user" || msg.role === "assistant") {
2586
2606
  messages.push({
2587
2607
  role: msg.role,
2588
- content: typeof msg.content === "string"
2589
- ? msg.content
2590
- : JSON.stringify(msg.content),
2608
+ content: stringifyContentSafe(msg.content),
2591
2609
  });
2592
2610
  }
2593
2611
  }
@@ -3190,7 +3208,7 @@ export class GoogleVertexProvider extends BaseProvider {
3190
3208
  // so coerce defensively to keep the follow-up turn valid.
3191
3209
  const resultContent = typeof result === "string"
3192
3210
  ? result
3193
- : (JSON.stringify(result ?? null) ?? String(result));
3211
+ : stringifyContentSafe(result ?? null);
3194
3212
  toolResults.push({
3195
3213
  type: "tool_result",
3196
3214
  tool_use_id: toolUse.id,
@@ -3651,9 +3669,7 @@ export class GoogleVertexProvider extends BaseProvider {
3651
3669
  if (msg.role === "user" || msg.role === "assistant") {
3652
3670
  messages.push({
3653
3671
  role: msg.role,
3654
- content: typeof msg.content === "string"
3655
- ? msg.content
3656
- : JSON.stringify(msg.content),
3672
+ content: stringifyContentSafe(msg.content),
3657
3673
  });
3658
3674
  }
3659
3675
  }
@@ -4032,7 +4048,7 @@ export class GoogleVertexProvider extends BaseProvider {
4032
4048
  // so coerce defensively to keep the follow-up turn valid.
4033
4049
  const resultContent = typeof result === "string"
4034
4050
  ? result
4035
- : (JSON.stringify(result ?? null) ?? String(result));
4051
+ : stringifyContentSafe(result ?? null);
4036
4052
  toolResults.push({
4037
4053
  type: "tool_result",
4038
4054
  tool_use_id: toolUse.id,
@@ -4158,7 +4174,22 @@ export class GoogleVertexProvider extends BaseProvider {
4158
4174
  wasAborted = true;
4159
4175
  break;
4160
4176
  }
4161
- logger.error("[GoogleVertex] Native Anthropic SDK generate error", error);
4177
+ logger.error("[GoogleVertex] Native Anthropic SDK generate error", {
4178
+ error,
4179
+ model: modelName,
4180
+ step,
4181
+ status: error?.status,
4182
+ });
4183
+ // Best-effort request context for formatProviderError — see the
4184
+ // native Gemini catch for rationale.
4185
+ try {
4186
+ if (error && typeof error === "object") {
4187
+ error.requestModel = modelName;
4188
+ }
4189
+ }
4190
+ catch {
4191
+ /* frozen/sealed error — context stays best-effort */
4192
+ }
4162
4193
  throw this.handleProviderError(error);
4163
4194
  }
4164
4195
  }
@@ -4622,8 +4653,49 @@ export class GoogleVertexProvider extends BaseProvider {
4622
4653
  model: modelName,
4623
4654
  totalToolCount: Object.keys(mergedOptions.tools).length,
4624
4655
  });
4625
- nativeResult =
4626
- await this.executeNativeGemini3Generate(mergedOptions);
4656
+ try {
4657
+ nativeResult =
4658
+ await this.executeNativeGemini3Generate(mergedOptions);
4659
+ }
4660
+ catch (nativeError) {
4661
+ // Vertex rejects over-constrained responseSchemas with a
4662
+ // deterministic 400 ("too many states" — constrained-decoding
4663
+ // state explosion). Re-sending the same schema can never
4664
+ // succeed, so retry ONCE with the schema dropped but JSON
4665
+ // mode kept: output.format "json" still sets
4666
+ // responseMimeType application/json on the no-tools path,
4667
+ // so the model is forced to emit JSON — just without the
4668
+ // offending schema constraints. The NeuroLink layer then
4669
+ // coerces the JSON text into the caller's original schema
4670
+ // (generate({schema}) guarantee holds).
4671
+ const requestedStructured = mergedOptions.schema !== undefined ||
4672
+ mergedOptions.output?.format === "json";
4673
+ // Tools present → the executor skips responseMimeType, so a
4674
+ // schema-less retry would NOT actually run in JSON mode and
4675
+ // the structured-output contract would silently degrade to
4676
+ // prose. Only the no-tools case retries faithfully; with
4677
+ // tools, surface the 400 to the caller instead.
4678
+ const hasTools = !mergedOptions.disableTools &&
4679
+ Object.keys(mergedOptions.tools ?? {}).length > 0;
4680
+ if (requestedStructured &&
4681
+ !hasTools &&
4682
+ isSchemaComplexityError(nativeError)) {
4683
+ logger.warn("[GoogleVertex] responseSchema too complex for constrained decoding — retrying native generate in schema-less JSON mode", {
4684
+ model: modelName,
4685
+ error: nativeError instanceof Error
4686
+ ? nativeError.message
4687
+ : String(nativeError),
4688
+ });
4689
+ nativeResult = await this.executeNativeGemini3Generate({
4690
+ ...mergedOptions,
4691
+ schema: undefined,
4692
+ output: { format: "json" },
4693
+ });
4694
+ }
4695
+ else {
4696
+ throw nativeError;
4697
+ }
4698
+ }
4627
4699
  }
4628
4700
  executionSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_OUTPUT, spanJsonAttribute(nativeResult?.content ?? ""));
4629
4701
  return nativeResult;
@@ -4960,17 +5032,42 @@ export class GoogleVertexProvider extends BaseProvider {
4960
5032
  `3. Ensure your project has access to the model ` +
4961
5033
  `4. For Claude models, enable Anthropic integration in Google Cloud Console`, this.providerName);
4962
5034
  }
4963
- // Rate limit and quota errors
5035
+ // Rate limit / quota / capacity errors. Anthropic-on-Vertex capacity
5036
+ // exhaustion surfaces as overloaded_error (HTTP 529) — same operational
5037
+ // meaning as a 429, so classify it here instead of the generic 5xx branch.
4964
5038
  if (message.includes("QUOTA_EXCEEDED") ||
4965
5039
  message.includes("RATE_LIMIT_EXCEEDED") ||
4966
5040
  message.includes("rate limit") ||
4967
5041
  message.includes("429") ||
4968
- statusCode === 429) {
4969
- return new RateLimitError(`Google Vertex AI quota/rate limit exceeded. ` +
4970
- `Solutions: 1. Check your Vertex AI quotas in Google Cloud Console ` +
4971
- `2. Request quota increase if needed ` +
4972
- `3. Try a different model or reduce request frequency ` +
4973
- `4. Consider using a different region`, this.providerName);
5042
+ statusCode === 429 ||
5043
+ statusCode === 529 ||
5044
+ /overloaded/i.test(message)) {
5045
+ // Surface retry guidance when the SDK error carries it. @google/genai
5046
+ // ApiError nests RetryInfo inside the JSON error body's details array,
5047
+ // so fall back to scraping retryDelay out of the raw message.
5048
+ const retryDelay = typeof errorRecord?.retryDelay === "string"
5049
+ ? errorRecord.retryDelay
5050
+ : (/["']?retryDelay["']?\s*[:=]\s*["']?(\d+(?:\.\d+)?s)/.exec(message)?.[1] ?? undefined);
5051
+ // Prefer the per-request context the native catches attach to the
5052
+ // error (this.modelName can be stale when options.model overrides the
5053
+ // instance default). Gemini models are force-routed to the "global"
5054
+ // endpoint regardless of configured location — report the region the
5055
+ // request actually hit.
5056
+ const requestModel = typeof errorRecord?.requestModel === "string"
5057
+ ? errorRecord.requestModel
5058
+ : this.modelName;
5059
+ const effectiveRegion = typeof errorRecord?.requestRegion === "string"
5060
+ ? errorRecord.requestRegion
5061
+ : resolveVertexRegionForModel(requestModel, this.location);
5062
+ return new RateLimitError(`Google Vertex AI rate limit / shared-capacity exhausted (429 RESOURCE_EXHAUSTED / overloaded) ` +
5063
+ `for model '${requestModel}' in region '${effectiveRegion}'.` +
5064
+ (retryDelay
5065
+ ? ` Upstream suggests retrying after ${retryDelay}.`
5066
+ : "") +
5067
+ ` Solutions: 1. Retry with backoff ` +
5068
+ `2. Check your Vertex AI quotas in Google Cloud Console (shared-capacity 429s can occur below quota) ` +
5069
+ `3. Try a different region or model ` +
5070
+ `4. Request provisioned throughput for sustained load`, this.providerName);
4974
5071
  }
4975
5072
  // Network connectivity errors
4976
5073
  if (message.includes("ECONNRESET") ||
@@ -296,6 +296,19 @@ export type GenerateOptions = {
296
296
  * ```
297
297
  */
298
298
  enabledToolNames?: string[];
299
+ /**
300
+ * Request timeout (e.g. 30000, '30s', '2m').
301
+ *
302
+ * PER-STEP semantics in agentic loops: on providers that run a native
303
+ * multi-step tool loop (Vertex Gemini / Vertex Claude), this bounds EACH
304
+ * model call in the loop, not the whole turn — a tool-heavy turn may run
305
+ * far longer than this value in total. Size it for the slowest single
306
+ * step (default 300s), and use `abortSignal` for a total-turn deadline.
307
+ *
308
+ * When set explicitly, a step timeout is surfaced immediately instead of
309
+ * burning internal retries/fallbacks that would re-run the same
310
+ * provider+model with the same doomed budget.
311
+ */
299
312
  timeout?: number | string;
300
313
  /** AbortSignal for external cancellation of the AI call */
301
314
  abortSignal?: AbortSignal;
@@ -5,6 +5,7 @@
5
5
  import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
6
6
  import { tracers } from "../telemetry/tracers.js";
7
7
  import { withTimeout } from "./errorHandling.js";
8
+ import { safeDebugSerialize, sanitizeRecord } from "./logSanitize.js";
8
9
  import { DEFAULT_FALLBACK_THRESHOLD, getConversationMemoryDefaults, MEMORY_THRESHOLD_PERCENTAGE, } from "../config/conversationMemory.js";
9
10
  import { getAvailableInputTokens } from "../constants/contextWindows.js";
10
11
  import { buildSummarizationPrompt } from "../context/prompts/summarizationPrompt.js";
@@ -106,20 +107,30 @@ export function applyConversationMemoryDefaults(userConfig) {
106
107
  * Get conversation history as message array, summarizing if needed.
107
108
  */
108
109
  export async function getConversationMessages(conversationMemory, options) {
109
- logger.debug("[conversationMemoryUtils] getConversationMessages called", {
110
- hasMemory: !!conversationMemory,
111
- memoryType: conversationMemory?.constructor?.name || "NONE",
112
- hasContext: !!options.context,
113
- enableSummarization: options.enableSummarization ?? false,
114
- options: JSON.stringify(options, null, 2),
115
- });
110
+ // Logger Guard: options carries the full conversation history + tool
111
+ // outputs; eager JSON.stringify of it can throw RangeError: Invalid string
112
+ // length and abort the turn. Serialize lazily, bounded, never-throwing,
113
+ // and redacted (sanitizeRecord strips credential/PII keys).
114
+ if (logger.shouldLog("debug")) {
115
+ logger.debug("[conversationMemoryUtils] getConversationMessages called", {
116
+ hasMemory: !!conversationMemory,
117
+ memoryType: conversationMemory?.constructor?.name || "NONE",
118
+ hasContext: !!options.context,
119
+ enableSummarization: options.enableSummarization ?? false,
120
+ options: safeDebugSerialize(sanitizeRecord(options)),
121
+ });
122
+ }
116
123
  if (!conversationMemory || !options.context) {
117
124
  logger.warn("[conversationMemoryUtils] No memory or context, returning empty messages", {
118
125
  hasMemory: !!conversationMemory,
119
126
  memoryType: conversationMemory?.constructor?.name || "NONE",
120
127
  hasContext: !!options.context,
121
128
  enableSummarization: options.enableSummarization ?? false,
122
- options: JSON.stringify(options, null, 2),
129
+ // The options dump is debug-grade detail — don't pay for (or leak)
130
+ // the serialization on every memory-disabled call at warn level.
131
+ ...(logger.shouldLog("debug")
132
+ ? { options: safeDebugSerialize(sanitizeRecord(options)) }
133
+ : {}),
123
134
  });
124
135
  return [];
125
136
  }
@@ -26,6 +26,32 @@
26
26
  * @param maxLen - Maximum number of characters to keep (default 500).
27
27
  */
28
28
  export declare function sanitizeForLog(text: string, maxLen?: number): string;
29
+ /**
30
+ * Stringify non-string message/tool content without ever throwing.
31
+ * JSON.stringify on a giant multimodal/tool-result payload can exceed V8's
32
+ * maximum string length and throw `RangeError: Invalid string length` — a
33
+ * generation turn must degrade to a placeholder, not abort, when that
34
+ * happens. Shared by the SDK core and providers (single source of truth).
35
+ */
36
+ export declare function stringifyContentSafe(content: unknown): string;
37
+ /**
38
+ * Serialize an arbitrary value for a debug log without ever throwing or
39
+ * producing an unbounded string.
40
+ *
41
+ * `JSON.stringify` on a full options object (conversation history + tool
42
+ * outputs) can exceed V8's maximum string length and throw
43
+ * `RangeError: Invalid string length`, which — when evaluated eagerly inside
44
+ * a logger call — aborts the surrounding generation turn (observed in
45
+ * production). This helper caps the output and converts any stringify
46
+ * failure (RangeError, circular structure, BigInt, …) into a placeholder.
47
+ *
48
+ * Callers MUST still gate on `logger.shouldLog("debug")` per the Logger
49
+ * Guard rule — this helper makes serialization safe, not free.
50
+ *
51
+ * @param value - Arbitrary value to serialize.
52
+ * @param maxLen - Maximum number of characters to keep (default 10 000).
53
+ */
54
+ export declare function safeDebugSerialize(value: unknown, maxLen?: number): string;
29
55
  /**
30
56
  * Strip embedded `user:pass@` credentials from a URL's authority component
31
57
  * before logging it or surfacing it in a user-facing error.
@@ -74,6 +74,13 @@ const SENSITIVE_OBJECT_KEYS = [
74
74
  "oauth",
75
75
  "oauthToken",
76
76
  "credentials",
77
+ "authContext",
78
+ "authToken",
79
+ "sessionToken",
80
+ "serviceAccountKey",
81
+ "secretAccessKey",
82
+ // PII, not a secret — but it has no business in debug log dumps.
83
+ "userEmail",
77
84
  ];
78
85
  /**
79
86
  * Truncate `text` to `maxLen` chars then replace embedded secrets with `***`.
@@ -91,6 +98,55 @@ export function sanitizeForLog(text, maxLen = 500) {
91
98
  }
92
99
  return text.slice(0, maxLen).replace(SECRET_PATTERN, "***");
93
100
  }
101
+ /**
102
+ * Stringify non-string message/tool content without ever throwing.
103
+ * JSON.stringify on a giant multimodal/tool-result payload can exceed V8's
104
+ * maximum string length and throw `RangeError: Invalid string length` — a
105
+ * generation turn must degrade to a placeholder, not abort, when that
106
+ * happens. Shared by the SDK core and providers (single source of truth).
107
+ */
108
+ export function stringifyContentSafe(content) {
109
+ if (typeof content === "string") {
110
+ return content;
111
+ }
112
+ try {
113
+ return JSON.stringify(content) ?? String(content);
114
+ }
115
+ catch {
116
+ return "[content too large to serialize]";
117
+ }
118
+ }
119
+ /**
120
+ * Serialize an arbitrary value for a debug log without ever throwing or
121
+ * producing an unbounded string.
122
+ *
123
+ * `JSON.stringify` on a full options object (conversation history + tool
124
+ * outputs) can exceed V8's maximum string length and throw
125
+ * `RangeError: Invalid string length`, which — when evaluated eagerly inside
126
+ * a logger call — aborts the surrounding generation turn (observed in
127
+ * production). This helper caps the output and converts any stringify
128
+ * failure (RangeError, circular structure, BigInt, …) into a placeholder.
129
+ *
130
+ * Callers MUST still gate on `logger.shouldLog("debug")` per the Logger
131
+ * Guard rule — this helper makes serialization safe, not free.
132
+ *
133
+ * @param value - Arbitrary value to serialize.
134
+ * @param maxLen - Maximum number of characters to keep (default 10 000).
135
+ */
136
+ export function safeDebugSerialize(value, maxLen = 10_000) {
137
+ try {
138
+ const json = JSON.stringify(value);
139
+ if (json === undefined) {
140
+ return String(value);
141
+ }
142
+ return json.length > maxLen
143
+ ? `${json.slice(0, maxLen)}…[truncated ${json.length - maxLen} chars]`
144
+ : json;
145
+ }
146
+ catch (error) {
147
+ return `[unserializable: ${error instanceof Error ? error.message : String(error)}]`;
148
+ }
149
+ }
94
150
  /**
95
151
  * Strip embedded `user:pass@` credentials from a URL's authority component
96
152
  * before logging it or surfacing it in a user-facing error.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.80.3",
3
+ "version": "9.80.4",
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": {