@juspay/neurolink 12.0.2 → 12.0.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.
@@ -85,6 +85,23 @@ export declare class NeuroLink {
85
85
  /** Artifact store for externalized MCP tool outputs (set when strategy=externalize). */
86
86
  private mcpArtifactStore?;
87
87
  private _disableToolCacheForCurrentRequest;
88
+ /**
89
+ * (toolName + args) keys already served during the CURRENT request.
90
+ * A repeat occurrence within one request bypasses the tool-result cache:
91
+ * when the model deliberately re-calls a tool with identical args in the
92
+ * same turn (a counter, a poll, "check status again"), it wants fresh
93
+ * state — serving the memoized first result silently freezes stateful
94
+ * tools (observed live: a 5-round counter loop executed once). Cross-
95
+ * request dedup — BZ-664's actual goal — is untouched: the first
96
+ * occurrence in a request may still be served from cache. Request-scoped
97
+ * like _disableToolCacheForCurrentRequest above (assigned a fresh Set at
98
+ * request start so the router's save/restore-by-reference pattern works).
99
+ */
100
+ private _toolCacheKeysServedThisRequest;
101
+ /** True only while a generate()/stream() turn is executing — the
102
+ * repeat-call cache bypass applies inside a turn; direct executeTool
103
+ * calls keep full BZ-664 cache semantics. */
104
+ private _generationTurnActive;
88
105
  private mcpEnhancementsConfig?;
89
106
  private toolCircuitBreakers;
90
107
  private toolExecutionMetrics;
@@ -1524,6 +1541,7 @@ export declare class NeuroLink {
1524
1541
  * - Annotations: skip cache for destructive tools, retry safe tools on failure
1525
1542
  * - Middleware: apply global middleware chain before execution
1526
1543
  */
1544
+ private toolCacheRepeatKey;
1527
1545
  private executeToolInternal;
1528
1546
  /**
1529
1547
  * Get tool annotations for execution decisions (cache, retry).
package/dist/neurolink.js CHANGED
@@ -377,6 +377,23 @@ export class NeuroLink {
377
377
  /** Artifact store for externalized MCP tool outputs (set when strategy=externalize). */
378
378
  mcpArtifactStore;
379
379
  _disableToolCacheForCurrentRequest = false;
380
+ /**
381
+ * (toolName + args) keys already served during the CURRENT request.
382
+ * A repeat occurrence within one request bypasses the tool-result cache:
383
+ * when the model deliberately re-calls a tool with identical args in the
384
+ * same turn (a counter, a poll, "check status again"), it wants fresh
385
+ * state — serving the memoized first result silently freezes stateful
386
+ * tools (observed live: a 5-round counter loop executed once). Cross-
387
+ * request dedup — BZ-664's actual goal — is untouched: the first
388
+ * occurrence in a request may still be served from cache. Request-scoped
389
+ * like _disableToolCacheForCurrentRequest above (assigned a fresh Set at
390
+ * request start so the router's save/restore-by-reference pattern works).
391
+ */
392
+ _toolCacheKeysServedThisRequest = new Set();
393
+ /** True only while a generate()/stream() turn is executing — the
394
+ * repeat-call cache bypass applies inside a turn; direct executeTool
395
+ * calls keep full BZ-664 cache semantics. */
396
+ _generationTurnActive = false;
380
397
  mcpEnhancementsConfig;
381
398
  // Enhanced error handling support
382
399
  toolCircuitBreakers = new Map();
@@ -3509,6 +3526,8 @@ Current user's request: ${currentInput}`;
3509
3526
  }
3510
3527
  finally {
3511
3528
  this._disableToolCacheForCurrentRequest = false;
3529
+ this._toolCacheKeysServedThisRequest = new Set();
3530
+ this._generationTurnActive = false;
3512
3531
  generateSpan.end();
3513
3532
  }
3514
3533
  }
@@ -3527,6 +3546,8 @@ Current user's request: ${currentInput}`;
3527
3546
  await this.resolveDynamicOptions(options);
3528
3547
  options.model = resolveModel(options.model, this.modelAliasConfig);
3529
3548
  this._disableToolCacheForCurrentRequest = !!options.disableToolCache;
3549
+ this._toolCacheKeysServedThisRequest = new Set();
3550
+ this._generationTurnActive = true;
3530
3551
  generateSpan.setAttribute("neurolink.provider", options.provider || "default");
3531
3552
  generateSpan.setAttribute("neurolink.model", options.model || "default");
3532
3553
  generateSpan.setAttribute("neurolink.input_length", typeof optionsOrPrompt === "string"
@@ -6672,6 +6693,8 @@ Current user's request: ${currentInput}`;
6672
6693
  const streamIsRoot = !trace.getSpan(context.active());
6673
6694
  const spanStartTime = Date.now();
6674
6695
  this._disableToolCacheForCurrentRequest = !!options.disableToolCache;
6696
+ this._toolCacheKeysServedThisRequest = new Set();
6697
+ this._generationTurnActive = true;
6675
6698
  try {
6676
6699
  options.model = resolveModel(options.model, this.modelAliasConfig);
6677
6700
  const startTime = Date.now();
@@ -7304,6 +7327,8 @@ Current user's request: ${currentInput}`;
7304
7327
  }
7305
7328
  finally {
7306
7329
  self._disableToolCacheForCurrentRequest = false;
7330
+ self._toolCacheKeysServedThisRequest = new Set();
7331
+ self._generationTurnActive = false;
7307
7332
  params.streamSpan.setAttribute("neurolink.response_time_ms", Date.now() - params.spanStartTime);
7308
7333
  params.streamSpan.end();
7309
7334
  }
@@ -7603,6 +7628,8 @@ Current user's request: ${currentInput}`;
7603
7628
  }
7604
7629
  }
7605
7630
  self._disableToolCacheForCurrentRequest = false;
7631
+ self._toolCacheKeysServedThisRequest = new Set();
7632
+ self._generationTurnActive = false;
7606
7633
  cleanupListeners();
7607
7634
  streamSpan.setAttribute("neurolink.response_time_ms", Date.now() - spanStartTime);
7608
7635
  streamSpan.setAttribute(ATTR.NL_OUTPUT_LENGTH, accumulatedContent.length);
@@ -10047,6 +10074,16 @@ Current user's request: ${currentInput}`;
10047
10074
  * - Annotations: skip cache for destructive tools, retry safe tools on failure
10048
10075
  * - Middleware: apply global middleware chain before execution
10049
10076
  */
10077
+ toolCacheRepeatKey(toolName, params) {
10078
+ try {
10079
+ return `${toolName}:${JSON.stringify(params) ?? ""}`;
10080
+ }
10081
+ catch {
10082
+ // Unserializable args (circular refs) — no repeat tracking; the
10083
+ // ToolResultCache's own keying handles (or rejects) them as before.
10084
+ return undefined;
10085
+ }
10086
+ }
10050
10087
  async executeToolInternal(toolName, params, options, HITLState) {
10051
10088
  const functionTag = "NeuroLink.executeToolInternal";
10052
10089
  // === MCP ENHANCEMENT: Infer annotations for cache/retry decisions ===
@@ -10064,13 +10101,24 @@ Current user's request: ${currentInput}`;
10064
10101
  __ctx: options.authContext ?? this.toolExecutionContext,
10065
10102
  }
10066
10103
  : params;
10067
- if (isCacheEnabled && toolResultCache) {
10104
+ const repeatKey = this._generationTurnActive
10105
+ ? this.toolCacheRepeatKey(toolName, cacheParams)
10106
+ : undefined;
10107
+ const isRepeatCallThisRequest = repeatKey !== undefined &&
10108
+ this._toolCacheKeysServedThisRequest.has(repeatKey);
10109
+ if (repeatKey !== undefined) {
10110
+ this._toolCacheKeysServedThisRequest.add(repeatKey);
10111
+ }
10112
+ if (isCacheEnabled && toolResultCache && !isRepeatCallThisRequest) {
10068
10113
  const cached = toolResultCache.getCachedResult(toolName, cacheParams);
10069
10114
  if (cached !== undefined) {
10070
10115
  logger.debug(`[${functionTag}] Cache HIT for tool: ${toolName}`);
10071
10116
  return cached;
10072
10117
  }
10073
10118
  }
10119
+ else if (isRepeatCallThisRequest) {
10120
+ logger.debug(`[${functionTag}] Repeat call within this request — bypassing tool cache for: ${toolName}`);
10121
+ }
10074
10122
  // === MCP ENHANCEMENT: Middleware chain wrapper ===
10075
10123
  const executeWithMiddleware = async (executeFn) => {
10076
10124
  if (this.mcpToolMiddlewares.length === 0) {
@@ -11515,7 +11563,19 @@ Current user's request: ${currentInput}`;
11515
11563
  ? { __ctx: this.toolExecutionContext }
11516
11564
  : {}),
11517
11565
  };
11518
- if (cacheEnabled && this.mcpToolResultCache) {
11566
+ // Same repeat-within-request bypass as executeToolInternal: a model
11567
+ // re-calling the identical tool+args in one turn wants fresh state.
11568
+ const externalRepeatKey = this._generationTurnActive
11569
+ ? this.toolCacheRepeatKey(toolName, cacheKeyArgs)
11570
+ : undefined;
11571
+ const isExternalRepeatThisRequest = externalRepeatKey !== undefined &&
11572
+ this._toolCacheKeysServedThisRequest.has(externalRepeatKey);
11573
+ if (externalRepeatKey !== undefined) {
11574
+ this._toolCacheKeysServedThisRequest.add(externalRepeatKey);
11575
+ }
11576
+ if (cacheEnabled &&
11577
+ this.mcpToolResultCache &&
11578
+ !isExternalRepeatThisRequest) {
11519
11579
  const cached = this.mcpToolResultCache.getCachedResult(toolName, cacheKeyArgs);
11520
11580
  if (cached !== undefined) {
11521
11581
  mcpLogger.debug(`[NeuroLink] Tool result cache HIT: ${toolName} on ${serverId}`);
@@ -2184,76 +2184,146 @@ pdfFiles, provider, model, audioFiles = []) {
2184
2184
  }));
2185
2185
  }
2186
2186
  else {
2187
- // Provider doesn't support native PDF - convert PDF pages to images
2188
- // This enables PDF processing for providers like Mistral, Ollama that support images but not PDFs
2189
- logger.info(`[PDF→Image] Provider ${provider} doesn't support native PDF. Converting ${pdfFiles.length} PDF(s) to images...`);
2187
+ // No native PDF support: inline the PDF's text layer, clearly labeled per
2188
+ // file. Image-only conversion sent content a text-only backend can never
2189
+ // read observed live as the model flatly claiming no file was attached
2190
+ // — and for proxy providers (litellm/openrouter) supportsVision() is a
2191
+ // pass-through, so vision cannot be trusted to carry the content either.
2192
+ // Text is therefore always included (primary for text-only backends,
2193
+ // extra grounding otherwise); page images are appended below only when
2194
+ // the provider may actually see them. A PDF with no text layer (pure
2195
+ // scan) gets an explicit note rather than silence, so the model
2196
+ // acknowledges an unreadable attachment instead of denying it exists.
2197
+ const providerCanSeeImages = ProviderImageAdapter.supportsVision(provider, model);
2190
2198
  for (const pdf of pdfFiles) {
2199
+ const name = safeBasename(pdf.filename);
2191
2200
  try {
2192
- const effectiveMaxPages = pdf.maxPages ?? PDF_LIMITS.DEFAULT_MAX_PAGES;
2193
- const conversionResult = await PDFImageConverter.convertToImages(pdf.buffer, {
2194
- // #297: this is the only PDF→image call the product actually makes,
2195
- // and it used to hardcode scale 2.0 — silently overriding the
2196
- // lowered PDF_LIMITS.DEFAULT_SCALE and keeping the memory cost the
2197
- // issue reports (a 100-page render at 2.0 is ~776MB; 1.5 is ~44%
2198
- // fewer pixels per page). Callers can raise it back per request.
2199
- scale: pdf.scale ?? PDF_LIMITS.DEFAULT_SCALE,
2200
- // Page ceiling guards token overflow; also now caller-adjustable
2201
- // rather than a constant nothing could reach.
2202
- maxPages: effectiveMaxPages,
2201
+ const { PDFParse } = await import("pdf-parse");
2202
+ const parser = new PDFParse({
2203
+ data: new Uint8Array(pdf.buffer),
2203
2204
  ...(pdf.password ? { password: pdf.password } : {}), // #258
2204
- ...(pdf.maxCanvasPixels
2205
- ? { maxCanvasPixels: pdf.maxCanvasPixels }
2206
- : {}), // #260
2207
2205
  });
2208
- // The renderer stops at maxPages, so a longer document is silently
2209
- // truncated say so rather than letting the model answer from a
2210
- // partial document as though it had the whole thing.
2211
- //
2212
- // Keyed on the cap being reached, not on pdf.pageCount: that field is
2213
- // null whenever `input.content` omits `metadata.pages`, which is the
2214
- // common case, so a page-count comparison would simply never fire
2215
- // there. Reaching the cap is also unambiguous — a short count caused by
2216
- // per-page render failures (#294 isolates those into `errors`) would
2217
- // otherwise be misreported as a maxPages truncation.
2218
- if (conversionResult.pageCount >= effectiveMaxPages) {
2219
- logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)} hit the ${effectiveMaxPages}-page ` +
2220
- `conversion limit. Any pages beyond that were not sent — the model may be ` +
2221
- `answering from a partial document. Raise pdfOptions.maxPages or split the file.`);
2222
- }
2223
- if (conversionResult.errors && conversionResult.errors.length > 0) {
2224
- logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)}: ${conversionResult.errors.length} page(s) ` +
2225
- `failed to render and were omitted (page ${conversionResult.errors.map((e) => e.page).join(", ")}).`);
2206
+ try {
2207
+ const extracted = await parser.getText();
2208
+ const pdfText = (extracted?.text ?? "").trim();
2209
+ if (pdfText.length > 0) {
2210
+ content.push({
2211
+ type: "text",
2212
+ text: `\n[Attached PDF: ${name}]\n${pdfText}\n[End of PDF: ${name}]`,
2213
+ });
2214
+ logger.info(`[PDF→Text] Extracted text for non-vision provider ${provider}: ${name} (${pdfText.length} chars)`);
2215
+ }
2216
+ else {
2217
+ content.push({
2218
+ type: "text",
2219
+ text: `\n[Attached PDF: ${name} — no extractable text layer (likely a scanned document); its contents are unavailable to this text-only model.]`,
2220
+ });
2221
+ logger.warn(`[PDF→Text] ${name} has no text layer; provider ${provider} cannot see images — content unavailable`);
2222
+ }
2226
2223
  }
2227
- logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${conversionResult.pageCount} page(s) → images`);
2228
- // Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
2229
- conversionResult.images.forEach((base64Image, pageIndex) => {
2230
- content.push({
2231
- type: "image",
2232
- image: base64Image,
2233
- mimeType: "image/png",
2234
- });
2235
- logger.debug(`[PDF→Image] Added page ${pageIndex + 1}/${conversionResult.pageCount} of ${pdf.filename}`);
2236
- });
2237
- // Log any warnings from conversion
2238
- if (conversionResult.warnings) {
2239
- conversionResult.warnings.forEach((warning) => {
2240
- logger.warn(`[PDF→Image] ${warning}`);
2241
- });
2224
+ finally {
2225
+ await parser.destroy?.();
2242
2226
  }
2243
2227
  }
2244
2228
  catch (error) {
2245
2229
  const errorMessage = error instanceof Error ? error.message : String(error);
2246
- logger.error(`[PDF→Image] ❌ Failed to convert ${pdf.filename}: ${errorMessage}`);
2247
- // #258: password errors are already actionable typed errors re-throw
2248
- // them unwrapped so the "supply a password" guidance isn't buried.
2249
- const code = error?.code;
2250
- if (code === "PDF_PASSWORD_REQUIRED" ||
2251
- code === "PDF_INCORRECT_PASSWORD") {
2252
- throw error;
2230
+ logger.error(`[PDF→Text] ❌ Failed to parse ${name}: ${errorMessage}`);
2231
+ // #258: password errors are actionable and must surface. pdf-parse
2232
+ // raises pdf.js's PasswordException (not our typed PDF_PASSWORD_*
2233
+ // codes), so detect by name/message shape. When the provider can see
2234
+ // images, defer the throw to the image-conversion path below — it
2235
+ // raises the canonical typed error with the "supply the password"
2236
+ // guidance callers and tests rely on.
2237
+ const isPasswordError = error?.name === "PasswordException" ||
2238
+ /password/i.test(errorMessage);
2239
+ if (isPasswordError) {
2240
+ if (!providerCanSeeImages) {
2241
+ throw error;
2242
+ }
2243
+ // Image branch will produce the canonical typed password error.
2244
+ continue;
2245
+ }
2246
+ content.push({
2247
+ type: "text",
2248
+ text: `\n[Attached PDF: ${name} — could not be parsed (${errorMessage}); its contents are unavailable.]`,
2249
+ });
2250
+ }
2251
+ }
2252
+ // Page images in addition to the text, for providers that may actually
2253
+ // see them (vision models, and proxies whose upstream might).
2254
+ if (providerCanSeeImages) {
2255
+ logger.info(`[PDF→Image] Provider ${provider} doesn't support native PDF. Converting ${pdfFiles.length} PDF(s) to images...`);
2256
+ for (const pdf of pdfFiles) {
2257
+ try {
2258
+ const effectiveMaxPages = pdf.maxPages ?? PDF_LIMITS.DEFAULT_MAX_PAGES;
2259
+ const conversionResult = await PDFImageConverter.convertToImages(pdf.buffer, {
2260
+ // #297: this is the only PDF→image call the product actually makes,
2261
+ // and it used to hardcode scale 2.0 — silently overriding the
2262
+ // lowered PDF_LIMITS.DEFAULT_SCALE and keeping the memory cost the
2263
+ // issue reports (a 100-page render at 2.0 is ~776MB; 1.5 is ~44%
2264
+ // fewer pixels per page). Callers can raise it back per request.
2265
+ scale: pdf.scale ?? PDF_LIMITS.DEFAULT_SCALE,
2266
+ // Page ceiling guards token overflow; also now caller-adjustable
2267
+ // rather than a constant nothing could reach.
2268
+ maxPages: effectiveMaxPages,
2269
+ ...(pdf.password ? { password: pdf.password } : {}), // #258
2270
+ ...(pdf.maxCanvasPixels
2271
+ ? { maxCanvasPixels: pdf.maxCanvasPixels }
2272
+ : {}), // #260
2273
+ });
2274
+ // The renderer stops at maxPages, so a longer document is silently
2275
+ // truncated — say so rather than letting the model answer from a
2276
+ // partial document as though it had the whole thing.
2277
+ //
2278
+ // Keyed on the cap being reached, not on pdf.pageCount: that field is
2279
+ // null whenever `input.content` omits `metadata.pages`, which is the
2280
+ // common case, so a page-count comparison would simply never fire
2281
+ // there. Reaching the cap is also unambiguous — a short count caused by
2282
+ // per-page render failures (#294 isolates those into `errors`) would
2283
+ // otherwise be misreported as a maxPages truncation.
2284
+ if (conversionResult.pageCount >= effectiveMaxPages) {
2285
+ logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)} hit the ${effectiveMaxPages}-page ` +
2286
+ `conversion limit. Any pages beyond that were not sent — the model may be ` +
2287
+ `answering from a partial document. Raise pdfOptions.maxPages or split the file.`);
2288
+ }
2289
+ if (conversionResult.errors && conversionResult.errors.length > 0) {
2290
+ logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)}: ${conversionResult.errors.length} page(s) ` +
2291
+ `failed to render and were omitted (page ${conversionResult.errors.map((e) => e.page).join(", ")}).`);
2292
+ }
2293
+ logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${conversionResult.pageCount} page(s) → images`);
2294
+ // Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
2295
+ conversionResult.images.forEach((base64Image, pageIndex) => {
2296
+ content.push({
2297
+ type: "image",
2298
+ image: base64Image,
2299
+ mimeType: "image/png",
2300
+ });
2301
+ logger.debug(`[PDF→Image] Added page ${pageIndex + 1}/${conversionResult.pageCount} of ${pdf.filename}`);
2302
+ });
2303
+ // Log any warnings from conversion
2304
+ if (conversionResult.warnings) {
2305
+ conversionResult.warnings.forEach((warning) => {
2306
+ logger.warn(`[PDF→Image] ${warning}`);
2307
+ });
2308
+ }
2309
+ }
2310
+ catch (error) {
2311
+ const errorMessage = error instanceof Error ? error.message : String(error);
2312
+ logger.error(`[PDF→Image] ❌ Failed to convert ${pdf.filename}: ${errorMessage}`);
2313
+ // #258: password errors are already actionable typed errors — re-throw
2314
+ // them unwrapped so the "supply a password" guidance isn't buried.
2315
+ const code = error?.code;
2316
+ if (code === "PDF_PASSWORD_REQUIRED" ||
2317
+ code === "PDF_INCORRECT_PASSWORD") {
2318
+ throw error;
2319
+ }
2320
+ // Re-throw so the user knows PDF processing failed. The inlined
2321
+ // text layer above does NOT soften this: conversion failures
2322
+ // include caller mistakes (e.g. an invalid maxCanvasPixels, #260)
2323
+ // that must surface rather than be silently downgraded.
2324
+ throw new Error(`PDF to image conversion failed for ${pdf.filename}: ${errorMessage}. ` +
2325
+ `Provider ${provider} doesn't support native PDFs and image conversion failed.`, { cause: error });
2253
2326
  }
2254
- // Re-throw so the user knows PDF processing failed
2255
- throw new Error(`PDF to image conversion failed for ${pdf.filename}: ${errorMessage}. ` +
2256
- `Provider ${provider} doesn't support native PDFs and image conversion failed.`, { cause: error });
2257
2327
  }
2258
2328
  }
2259
2329
  }
@@ -33,6 +33,11 @@ export declare function transformToolExecutionsForMCP(toolExecutions?: unknown[]
33
33
  executionTime: number;
34
34
  success: boolean;
35
35
  serverId?: string;
36
+ params?: unknown;
37
+ output?: unknown;
38
+ startedAt?: number;
39
+ isError?: boolean;
40
+ error?: string;
36
41
  }>;
37
42
  /**
38
43
  * Convert ToolExecutionRecord entries (or legacy-shaped entries) to the
@@ -234,11 +234,36 @@ export function transformToolExecutionsForMCP(toolExecutions) {
234
234
  toolCall.server ||
235
235
  undefined;
236
236
  }
237
+ // Pass the execution payload through instead of discarding it. This
238
+ // transform used to keep only {toolName, executionTime, success,
239
+ // serverId}; toToolExecutionRecords() downstream then found no
240
+ // input/output on the entries and emitted params:{} with
241
+ // resultText:"undefined" for every tool execution on the MCP path —
242
+ // even when the source entry was a full record with real args and a
243
+ // real result. Field names chosen to match what
244
+ // toToolExecutionRecords() reads (input/params, output/result/
245
+ // resultText, startedAt, isError).
246
+ const params = teRecord.params ?? teRecord.input ?? teRecord.args ?? undefined;
247
+ const output = teRecord.output ??
248
+ teRecord.result ??
249
+ teRecord.response ??
250
+ (typeof teRecord.resultText === "string"
251
+ ? teRecord.resultText
252
+ : undefined);
237
253
  return {
238
254
  toolName: toolName,
239
255
  executionTime: executionTime,
240
256
  success: success,
241
257
  serverId: serverId,
258
+ ...(params !== undefined ? { params } : {}),
259
+ ...(output !== undefined ? { output } : {}),
260
+ ...(typeof teRecord.startedAt === "number"
261
+ ? { startedAt: teRecord.startedAt }
262
+ : {}),
263
+ ...(teRecord.isError !== undefined
264
+ ? { isError: teRecord.isError === true }
265
+ : { isError: !success }),
266
+ ...(typeof teRecord.error === "string" ? { error: teRecord.error } : {}),
242
267
  };
243
268
  });
244
269
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.0.2",
3
+ "version": "12.0.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": {