@juspay/neurolink 9.95.1 → 9.95.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +352 -352
  3. package/dist/cli/factories/commandFactory.d.ts +10 -0
  4. package/dist/cli/factories/commandFactory.js +30 -0
  5. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  6. package/dist/core/baseProvider.js +1 -0
  7. package/dist/core/constants.d.ts +5 -0
  8. package/dist/core/constants.js +19 -0
  9. package/dist/core/modules/MessageBuilder.js +2 -0
  10. package/dist/lib/core/baseProvider.js +1 -0
  11. package/dist/lib/core/constants.d.ts +5 -0
  12. package/dist/lib/core/constants.js +19 -0
  13. package/dist/lib/core/modules/MessageBuilder.js +2 -0
  14. package/dist/lib/neurolink.js +8 -4
  15. package/dist/lib/types/file.d.ts +48 -1
  16. package/dist/lib/types/generate.d.ts +14 -0
  17. package/dist/lib/types/stream.d.ts +7 -0
  18. package/dist/lib/utils/errorHandling.d.ts +10 -0
  19. package/dist/lib/utils/errorHandling.js +28 -0
  20. package/dist/lib/utils/fileDetector.js +35 -0
  21. package/dist/lib/utils/messageBuilder.js +53 -5
  22. package/dist/lib/utils/multimodalOptionsBuilder.d.ts +4 -0
  23. package/dist/lib/utils/multimodalOptionsBuilder.js +1 -0
  24. package/dist/lib/utils/pdfProcessor.d.ts +39 -1
  25. package/dist/lib/utils/pdfProcessor.js +267 -48
  26. package/dist/neurolink.js +8 -4
  27. package/dist/types/file.d.ts +48 -1
  28. package/dist/types/generate.d.ts +14 -0
  29. package/dist/types/stream.d.ts +7 -0
  30. package/dist/utils/errorHandling.d.ts +10 -0
  31. package/dist/utils/errorHandling.js +28 -0
  32. package/dist/utils/fileDetector.js +35 -0
  33. package/dist/utils/messageBuilder.js +53 -5
  34. package/dist/utils/multimodalOptionsBuilder.d.ts +4 -0
  35. package/dist/utils/multimodalOptionsBuilder.js +1 -0
  36. package/dist/utils/pdfProcessor.d.ts +39 -1
  37. package/dist/utils/pdfProcessor.js +267 -48
  38. package/package.json +1 -1
@@ -150,6 +150,13 @@ export class PDFProcessor {
150
150
  throw new Error(`PDF size ${sizeMB.toFixed(2)}MB exceeds ${config.maxSizeMB}MB limit for ${provider}`);
151
151
  }
152
152
  const metadata = PDFProcessor.extractBasicMetadata(content);
153
+ // #287: prefer an accurate page count (pdf-parse/pdfjs) over the unreliable
154
+ // header regex for both limit enforcement and returned metadata; fall back
155
+ // to the regex estimate when the accurate probe times out or fails.
156
+ const accuratePages = await PDFProcessor.getAccuratePageCount(content);
157
+ if (accuratePages !== null) {
158
+ metadata.estimatedPages = accuratePages;
159
+ }
153
160
  if (metadata.estimatedPages && metadata.estimatedPages > config.maxPages) {
154
161
  const enforceLimits = options?.enforceLimits !== false;
155
162
  if (enforceLimits) {
@@ -185,6 +192,10 @@ export class PDFProcessor {
185
192
  ...metadata,
186
193
  provider,
187
194
  apiType: config.apiType,
195
+ // #349: surface the provider's citations requirement so downstream
196
+ // provider adapters (e.g. Bedrock Converse document blocks) can act on
197
+ // it, instead of the config field being read nowhere.
198
+ requiresCitations: config.requiresCitations,
188
199
  },
189
200
  };
190
201
  }
@@ -220,6 +231,35 @@ export class PDFProcessor {
220
231
  filename: undefined,
221
232
  };
222
233
  }
234
+ /**
235
+ * Accurate page count via pdf-parse (pdfjs) (#287). The header regex in
236
+ * `extractBasicMetadata` only sees plaintext `/Type /Page` markers and misses
237
+ * compressed/object-stream PDFs (and miscounts when a page dict spans a chunk
238
+ * boundary). This parses the document properly, bounded by a timeout so a
239
+ * pathological PDF can't block; returns null on timeout/failure so the caller
240
+ * falls back to the regex estimate.
241
+ */
242
+ static async getAccuratePageCount(buffer) {
243
+ try {
244
+ const { PDFParse } = await import("pdf-parse");
245
+ const pdf = new PDFParse({ data: new Uint8Array(buffer) });
246
+ try {
247
+ const info = await Promise.race([
248
+ pdf.getInfo(),
249
+ new Promise((_, reject) => setTimeout(() => reject(new Error("page-count timeout")), PDF_LIMITS.PAGE_COUNT_TIMEOUT_MS)),
250
+ ]);
251
+ const total = info.total;
252
+ return typeof total === "number" && total > 0 ? total : null;
253
+ }
254
+ finally {
255
+ await pdf.destroy?.();
256
+ }
257
+ }
258
+ catch (error) {
259
+ logger.debug(`[PDF] Accurate page count unavailable; using regex estimate: ${error instanceof Error ? error.message : String(error)}`);
260
+ return null;
261
+ }
262
+ }
223
263
  static estimateTokens(pageCount, mode = "visual") {
224
264
  if (mode === "text-only") {
225
265
  return Math.ceil((pageCount / 3) * 1000);
@@ -231,6 +271,40 @@ export class PDFProcessor {
231
271
  // ============================================================================
232
272
  // PDF → Image Conversion (for providers without native PDF support)
233
273
  // ============================================================================
274
+ /**
275
+ * Estimate the largest page's rendered pixel count from the PDF's MediaBox
276
+ * entries, WITHOUT rendering (#260). Parses `/MediaBox [llx lly urx ury]`
277
+ * (dimensions in points); rendered pixels ≈ (width·scale)·(height·scale).
278
+ * Returns 0 when no plaintext MediaBox is found (e.g. compressed object
279
+ * streams) — the caller then skips downscaling, matching prior behavior.
280
+ *
281
+ * A crafted/malformed MediaBox can have finite but astronomically large
282
+ * width/height; `width * height * scale * scale` can then overflow past
283
+ * `Number.MAX_VALUE` to `Infinity`. Clamp the product to
284
+ * `Number.MAX_SAFE_INTEGER` so it stays finite — a large-but-finite pixel
285
+ * estimate still triggers downscaling, whereas `Infinity` would collapse
286
+ * the computed downscale factor to 0 (see `convertToImages`).
287
+ */
288
+ static largestPagePixels(pdfBuffer, scale) {
289
+ const text = pdfBuffer.toString("latin1");
290
+ const re = /\/MediaBox\s*\[\s*(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s*\]/g;
291
+ let max = 0;
292
+ let match;
293
+ while ((match = re.exec(text)) !== null) {
294
+ const width = Math.abs(parseFloat(match[3]) - parseFloat(match[1]));
295
+ const height = Math.abs(parseFloat(match[4]) - parseFloat(match[2]));
296
+ if (Number.isFinite(width) && Number.isFinite(height)) {
297
+ const pixels = width * height * scale * scale;
298
+ const boundedPixels = Number.isFinite(pixels)
299
+ ? Math.min(pixels, Number.MAX_SAFE_INTEGER)
300
+ : Number.MAX_SAFE_INTEGER;
301
+ if (boundedPixels > max) {
302
+ max = boundedPixels;
303
+ }
304
+ }
305
+ }
306
+ return max;
307
+ }
234
308
  /**
235
309
  * Convert a PDF buffer to an array of base64 PNG images
236
310
  *
@@ -257,43 +331,32 @@ export class PDFProcessor {
257
331
  */
258
332
  static async convertToImages(pdfBuffer, options) {
259
333
  const startTime = Date.now();
260
- const { scale = 2, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", } = options || {};
334
+ const { scale = PDF_LIMITS.DEFAULT_SCALE, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", maxCanvasPixels = PDF_LIMITS.DEFAULT_MAX_CANVAS_PIXELS, password, onProgress, } = options || {};
261
335
  const images = [];
262
336
  const warnings = [];
263
- // ============================================================================
264
- // INPUT VALIDATION (Security: Prevent malformed/malicious PDF processing)
265
- // ============================================================================
266
- // 0. Validate format is supported and case-sensitive
267
- if (format !== "png") {
268
- throw new Error(`Invalid format: "${format}". Only "png" format is currently supported.`);
269
- }
270
- // 0b. Validate the render scale. A non-finite or non-positive scale yields a
271
- // degenerate viewport (blank/zero-dimension render), and an excessive scale
272
- // can allocate hundreds of MB per page — reject both with a clear message.
273
- if (!Number.isFinite(scale) || scale <= 0 || scale > PDF_LIMITS.MAX_SCALE) {
274
- throw new Error(`Invalid scale: ${scale}. Scale must be a finite number greater than 0 and at most ${PDF_LIMITS.MAX_SCALE}.`);
275
- }
276
- // 1. Validate buffer is not empty or too small
277
- if (!pdfBuffer || pdfBuffer.length < 5) {
278
- throw new Error("Invalid PDF: Buffer is too small or empty. " +
279
- "A valid PDF must be at least 5 bytes (PDF header).");
280
- }
281
- // 2. Validate PDF magic bytes (%PDF-)
282
- if (!PDFProcessor.isValidPDF(pdfBuffer)) {
283
- throw new Error("Invalid PDF: File must start with %PDF- header. " +
284
- "The provided buffer does not appear to be a valid PDF file.");
285
- }
286
- // 3. Validate maximum buffer size to prevent memory exhaustion
287
- const sizeMB = pdfBuffer.length / (1024 * 1024);
288
- if (sizeMB > PDF_LIMITS.MAX_SIZE_MB) {
289
- throw new Error(`PDF too large for image conversion: ${sizeMB.toFixed(2)}MB exceeds ${PDF_LIMITS.MAX_SIZE_MB}MB limit. ` +
290
- "Consider splitting the PDF or using a provider with native PDF support.");
291
- }
337
+ const pageErrors = [];
338
+ // Validation shared with convertToImagesStream (#302).
339
+ const sizeMB = PDFProcessor.validateImageConversionInput(pdfBuffer, {
340
+ format,
341
+ scale,
342
+ maxCanvasPixels,
343
+ });
292
344
  logger.debug("[PDF→Image] ✅ PDF validation passed", {
293
345
  bufferSize: pdfBuffer.length,
294
346
  sizeMB: sizeMB.toFixed(2),
295
347
  maxPages,
296
348
  });
349
+ // #297: surface an up-front memory estimate so an operator can spot a
350
+ // conversion that will allocate a lot before it runs. Uses the cheap regex
351
+ // page estimate (the accurate pdf-parse count runs in process(), not here).
352
+ const estimatedPages = Math.max(1, PDFProcessor.extractBasicMetadata(pdfBuffer).estimatedPages ?? 1);
353
+ const estimatedMemoryMB = PDFProcessor.estimateConversionMemoryUsage(pdfBuffer.length, estimatedPages, scale);
354
+ logger.info("[PDF→Image] Estimated memory usage before conversion", {
355
+ scale,
356
+ estimatedPages,
357
+ estimatedMemoryMB,
358
+ sizeMB: Number(sizeMB.toFixed(2)),
359
+ });
297
360
  try {
298
361
  // Dynamic import to avoid loading MuPDF binaries until needed
299
362
  const pdfToImgModule = await import("pdf-to-img");
@@ -303,32 +366,87 @@ export class PDFProcessor {
303
366
  scale,
304
367
  maxPages: maxPages || "all",
305
368
  });
306
- // Create PDF document iterator
307
- const document = await pdf(pdfBuffer, { scale });
308
- let pageIndex = 0;
309
- // Iterate through pages and convert to base64
310
- for await (const page of document) {
311
- // Check if we've reached the max pages limit
312
- if (maxPages !== undefined && pageIndex >= maxPages) {
313
- warnings.push(`Stopped at page ${pageIndex} (maxPages limit: ${maxPages})`);
369
+ // #260: pre-flight page-size check WITHOUT rendering. pdf-to-img applies
370
+ // `scale` uniformly with no per-page hook and no pixel guard, so a very
371
+ // large page (e.g. an architectural drawing with a huge MediaBox) can
372
+ // allocate gigabytes of canvas. Read the largest MediaBox from the PDF
373
+ // bytes and, if that page at the requested scale would exceed
374
+ // maxCanvasPixels, downscale the whole render uniformly to stay under it.
375
+ let effectiveScale = scale;
376
+ const largestPixels = PDFProcessor.largestPagePixels(pdfBuffer, scale);
377
+ if (largestPixels > maxCanvasPixels) {
378
+ const downscale = Math.sqrt(maxCanvasPixels / largestPixels);
379
+ // Floor the result: an astronomically large (but now finite, see
380
+ // `largestPagePixels`) pixel estimate would otherwise push `downscale`
381
+ // — and therefore `effectiveScale` — toward 0, handing `pdf-to-img` a
382
+ // degenerate viewport instead of a small-but-renderable page.
383
+ effectiveScale = Math.max(PDF_LIMITS.MIN_EFFECTIVE_SCALE, scale * downscale);
384
+ // Recompute the ratio actually applied (may differ from `downscale`
385
+ // when the floor above kicks in) so the logged estimate stays honest.
386
+ const actualDownscale = effectiveScale / scale;
387
+ const beforeMB = (largestPixels * 4) / (1024 * 1024);
388
+ const afterMB = (largestPixels * actualDownscale * actualDownscale * 4) /
389
+ (1024 * 1024);
390
+ const msg = `Downscaled render (scale ${scale} → ${effectiveScale.toFixed(3)}): ` +
391
+ `the largest page would allocate ~${beforeMB.toFixed(0)}MB, above the ` +
392
+ `maxCanvasPixels ceiling; reduced to ~${afterMB.toFixed(0)}MB per page.`;
393
+ logger.warn(`[PDF→Image] ⚠️ ${msg}`);
394
+ warnings.push(msg);
395
+ }
396
+ // Create PDF document (password forwarded for encrypted PDFs, #258).
397
+ // pdf-to-img resolves `.length` (numPages) synchronously here and exposes
398
+ // `.getPage(n)`, so we drive an indexed loop rather than the async
399
+ // iterator — that lets one bad page be isolated instead of aborting all.
400
+ const document = await pdf(pdfBuffer, {
401
+ scale: effectiveScale,
402
+ ...(password ? { password } : {}),
403
+ });
404
+ const totalPages = document.length;
405
+ // #294: convert page-by-page with per-page isolation. A single page whose
406
+ // canvas render throws (e.g. a degenerate MediaBox) no longer discards
407
+ // every already-converted page — it is recorded in `errors` and the rest
408
+ // continue.
409
+ for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
410
+ if (maxPages !== undefined && pageNum - 1 >= maxPages) {
411
+ warnings.push(`Stopped at page ${pageNum - 1} (maxPages limit: ${maxPages})`);
314
412
  break;
315
413
  }
316
- // Convert PNG buffer to base64
317
- const base64Image = page.toString("base64");
318
- images.push(base64Image);
319
- pageIndex++;
320
- logger.debug(`[PDF→Image] Converted page ${pageIndex}`, {
321
- imageSizeBytes: page.length,
322
- base64Length: base64Image.length,
323
- });
414
+ try {
415
+ const page = await document.getPage(pageNum);
416
+ const base64Image = page.toString("base64");
417
+ images.push(base64Image);
418
+ logger.debug(`[PDF→Image] Converted page ${pageNum}`, {
419
+ imageSizeBytes: page.length,
420
+ base64Length: base64Image.length,
421
+ });
422
+ // #302: report progress after each successfully converted page.
423
+ if (onProgress) {
424
+ await onProgress({
425
+ pagesConverted: images.length,
426
+ totalPages,
427
+ elapsedMs: Date.now() - startTime,
428
+ });
429
+ }
430
+ }
431
+ catch (pageError) {
432
+ const msg = pageError instanceof Error ? pageError.message : String(pageError);
433
+ logger.warn(`[PDF→Image] ⚠️ page ${pageNum} failed to render: ${msg}`);
434
+ pageErrors.push({ page: pageNum, error: msg });
435
+ }
324
436
  }
325
- // Check for empty PDF (0 pages)
437
+ // Empty PDF (0 pages) or every page failed → treat as a conversion
438
+ // failure (kept inside the try so the password/format mapping below still
439
+ // applies), otherwise return the pages that did convert.
326
440
  if (images.length === 0) {
441
+ if (pageErrors.length > 0) {
442
+ throw new Error(`All ${pageErrors.length} page(s) failed to render. First error: ${pageErrors[0].error}`);
443
+ }
327
444
  throw new Error("PDF has 0 pages. Cannot convert empty PDF to images.");
328
445
  }
329
446
  const conversionTimeMs = Date.now() - startTime;
330
447
  logger.info("[PDF→Image] ✅ PDF conversion completed", {
331
448
  pageCount: images.length,
449
+ failedPages: pageErrors.length,
332
450
  conversionTimeMs,
333
451
  totalImageBytes: images.reduce((sum, img) => sum + img.length, 0),
334
452
  });
@@ -337,6 +455,7 @@ export class PDFProcessor {
337
455
  pageCount: images.length,
338
456
  conversionTimeMs,
339
457
  warnings: warnings.length > 0 ? warnings : undefined,
458
+ errors: pageErrors.length > 0 ? pageErrors : undefined,
340
459
  };
341
460
  }
342
461
  catch (error) {
@@ -346,11 +465,111 @@ export class PDFProcessor {
346
465
  error: errorMessage,
347
466
  conversionTimeMs,
348
467
  });
468
+ // #258: map pdfjs's PasswordException to an actionable typed error so a
469
+ // caller learns to supply (or correct) the password instead of seeing a
470
+ // generic "conversion failed". pdfjs code 1 = NEED_PASSWORD, 2 = INCORRECT.
471
+ const pdfErr = error;
472
+ if (pdfErr?.name === "PasswordException" ||
473
+ /password/i.test(errorMessage)) {
474
+ const incorrect = pdfErr.code === 2 || /incorrect|invalid/i.test(errorMessage);
475
+ throw incorrect
476
+ ? ErrorFactory.pdfIncorrectPassword()
477
+ : ErrorFactory.pdfPasswordRequired();
478
+ }
349
479
  throw new Error(`PDF to image conversion failed: ${errorMessage}`, {
350
480
  cause: error,
351
481
  });
352
482
  }
353
483
  }
484
+ /**
485
+ * Shared input validation for the image-conversion paths. Returns the PDF
486
+ * size in MB (needed by the memory-estimate log). Throws on any invalid input
487
+ * with the same messages the batch path has always used.
488
+ */
489
+ static validateImageConversionInput(pdfBuffer, opts) {
490
+ if (opts.format !== "png") {
491
+ throw new Error(`Invalid format: "${opts.format}". Only "png" format is currently supported.`);
492
+ }
493
+ if (!Number.isFinite(opts.scale) ||
494
+ opts.scale < PDF_LIMITS.MIN_SCALE ||
495
+ opts.scale > PDF_LIMITS.MAX_SCALE) {
496
+ throw new Error(`Invalid scale: ${opts.scale}. Scale must be a finite number between ${PDF_LIMITS.MIN_SCALE} and ${PDF_LIMITS.MAX_SCALE}.`);
497
+ }
498
+ if (!Number.isFinite(opts.maxCanvasPixels) || opts.maxCanvasPixels <= 0) {
499
+ throw new Error(`Invalid maxCanvasPixels: ${opts.maxCanvasPixels}. Must be a finite number greater than 0.`);
500
+ }
501
+ if (!pdfBuffer || pdfBuffer.length < 5) {
502
+ throw new Error("Invalid PDF: Buffer is too small or empty. " +
503
+ "A valid PDF must be at least 5 bytes (PDF header).");
504
+ }
505
+ if (!PDFProcessor.isValidPDF(pdfBuffer)) {
506
+ throw new Error("Invalid PDF: File must start with %PDF- header. " +
507
+ "The provided buffer does not appear to be a valid PDF file.");
508
+ }
509
+ const sizeMB = pdfBuffer.length / (1024 * 1024);
510
+ if (sizeMB > PDF_LIMITS.MAX_SIZE_MB) {
511
+ throw new Error(`PDF too large for image conversion: ${sizeMB.toFixed(2)}MB exceeds ${PDF_LIMITS.MAX_SIZE_MB}MB limit. ` +
512
+ "Consider splitting the PDF or using a provider with native PDF support.");
513
+ }
514
+ return sizeMB;
515
+ }
516
+ /**
517
+ * Streaming variant of {@link convertToImages} (#302): yields each page's
518
+ * base64 PNG as soon as it renders instead of buffering the whole document,
519
+ * and reports progress via `options.onProgress`. A page that fails to render
520
+ * is yielded with `error` set (per-page isolation, #294) rather than aborting
521
+ * the stream. `convertToImages` is the batch wrapper over this contract.
522
+ */
523
+ static async *convertToImagesStream(pdfBuffer, options) {
524
+ const startTime = Date.now();
525
+ const { scale = PDF_LIMITS.DEFAULT_SCALE, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", maxCanvasPixels = PDF_LIMITS.DEFAULT_MAX_CANVAS_PIXELS, password, onProgress, } = options || {};
526
+ PDFProcessor.validateImageConversionInput(pdfBuffer, {
527
+ format,
528
+ scale,
529
+ maxCanvasPixels,
530
+ });
531
+ const pdfToImgModule = await import("pdf-to-img");
532
+ const pdf = pdfToImgModule.pdf;
533
+ // #260: uniform downscale so the largest page stays under maxCanvasPixels.
534
+ let effectiveScale = scale;
535
+ const largestPixels = PDFProcessor.largestPagePixels(pdfBuffer, scale);
536
+ if (largestPixels > maxCanvasPixels) {
537
+ effectiveScale = scale * Math.sqrt(maxCanvasPixels / largestPixels);
538
+ }
539
+ const document = await pdf(pdfBuffer, {
540
+ scale: effectiveScale,
541
+ ...(password ? { password } : {}),
542
+ });
543
+ const totalPages = document.length;
544
+ let converted = 0;
545
+ for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
546
+ if (maxPages !== undefined && pageNum - 1 >= maxPages) {
547
+ break;
548
+ }
549
+ try {
550
+ const page = await document.getPage(pageNum);
551
+ const base64Image = page.toString("base64");
552
+ converted++;
553
+ if (onProgress) {
554
+ await onProgress({
555
+ pagesConverted: converted,
556
+ totalPages,
557
+ elapsedMs: Date.now() - startTime,
558
+ });
559
+ }
560
+ yield {
561
+ pageIndex: pageNum,
562
+ image: base64Image,
563
+ imageSizeBytes: page.length,
564
+ };
565
+ }
566
+ catch (pageError) {
567
+ const msg = pageError instanceof Error ? pageError.message : String(pageError);
568
+ logger.warn(`[PDF→Image] ⚠️ page ${pageNum} failed to render (stream): ${msg}`);
569
+ yield { pageIndex: pageNum, image: "", imageSizeBytes: 0, error: msg };
570
+ }
571
+ }
572
+ }
354
573
  /**
355
574
  * Convert a PDF file path to an array of base64 PNG images
356
575
  *
@@ -386,7 +605,7 @@ export class PDFProcessor {
386
605
  * @param scale - Scale factor
387
606
  * @returns Estimated memory usage in MB
388
607
  */
389
- static estimateConversionMemoryUsage(pdfSizeBytes, pageCount, scale = 2) {
608
+ static estimateConversionMemoryUsage(pdfSizeBytes, pageCount, scale = PDF_LIMITS.DEFAULT_SCALE) {
390
609
  // Rough estimation:
391
610
  // - Each page at scale 2 produces ~1-3MB PNG
392
611
  // - MuPDF needs ~2x PDF size for processing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.95.1",
3
+ "version": "9.95.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": {