@juspay/neurolink 12.17.2 → 12.17.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.
package/dist/neurolink.js CHANGED
@@ -4470,6 +4470,14 @@ Current user's request: ${currentInput}`;
4470
4470
  // remains false for backwards-compat with existing listeners that only
4471
4471
  // branch on the boolean.
4472
4472
  const aborted = isAbortError(error);
4473
+ // A native path that already emitted its own `generation:end` for this
4474
+ // failure marks the error, because on the failure path it has no result
4475
+ // object to mark. Re-emitting here would double every native failure.
4476
+ if (error &&
4477
+ typeof error === "object" &&
4478
+ error._generationEndEmitted) {
4479
+ return;
4480
+ }
4473
4481
  try {
4474
4482
  this.emitter.emit("generation:end", {
4475
4483
  provider: errProvider,
@@ -5899,7 +5907,10 @@ Current user's request: ${currentInput}`;
5899
5907
  toolsUsed: result.toolsUsed || [],
5900
5908
  toolCalls: result.toolCalls ?? [],
5901
5909
  toolExecutions: transformedToolExecutions,
5902
- enhancedWithTools: Boolean(hasToolExecutions),
5910
+ // A tool RAN (toolsUsed), not merely dispatched (toolExecutions can
5911
+ // include failed/never-found calls) — see hasToolExecutions above,
5912
+ // which still gates the null-result check and must not change.
5913
+ enhancedWithTools: !!(result.toolsUsed && result.toolsUsed.length > 0),
5903
5914
  availableTools: transformToolsForMCP(transformToolsToExpectedFormat(availableTools)),
5904
5915
  audio: result.audio,
5905
5916
  ttsMetadata: result.ttsMetadata,
@@ -6060,7 +6071,8 @@ Current user's request: ${currentInput}`;
6060
6071
  executionTime: te.durationMs,
6061
6072
  success: !te.isError,
6062
6073
  })),
6063
- enhancedWithTools: !!poolResult.toolExecutions?.length,
6074
+ // A tool RAN (toolsUsed), not merely dispatched.
6075
+ enhancedWithTools: !!poolResult.toolsUsed?.length,
6064
6076
  analytics: poolResult.analytics,
6065
6077
  evaluation: poolResult.evaluation,
6066
6078
  audio: poolResult.audio,
@@ -6432,7 +6444,8 @@ Current user's request: ${currentInput}`;
6432
6444
  executionTime: te.durationMs,
6433
6445
  success: !te.isError,
6434
6446
  })),
6435
- enhancedWithTools: !!result.toolExecutions?.length,
6447
+ // A tool RAN (toolsUsed), not merely dispatched.
6448
+ enhancedWithTools: !!result.toolsUsed?.length,
6436
6449
  analytics: result.analytics,
6437
6450
  evaluation: result.evaluation,
6438
6451
  audio: result.audio,
@@ -6496,10 +6509,27 @@ Current user's request: ${currentInput}`;
6496
6509
  // naming the one provider that failed is clearer than the generic
6497
6510
  // multi-provider wording. The multi-provider prefix is left untouched
6498
6511
  // so existing message-matching suites keep passing.
6512
+ // Both wrappers replace the provider's error with a new one, discarding
6513
+ // anything attached to it. `_generationEndEmitted` has to survive: lose it
6514
+ // and emitGenerateErrorEvent emits a second event for a failure the native
6515
+ // provider already reported. Returns the error rather than throwing so each
6516
+ // call site keeps its own `throw` — a never-returning helper reads better
6517
+ // but tsconfig.cli.json will not accept it as terminating the enclosing
6518
+ // function (TS2366). `cause` is set because the original error and its
6519
+ // stack were simply being thrown away.
6520
+ const wrapProviderError = (message) => {
6521
+ const wrapped = new Error(message, { cause: lastError });
6522
+ if (lastError &&
6523
+ lastError._generationEndEmitted) {
6524
+ wrapped._generationEndEmitted =
6525
+ true;
6526
+ }
6527
+ return wrapped;
6528
+ };
6499
6529
  if (providersToTry.length === 1) {
6500
- throw new Error(`Provider ${providersToTry[0]} failed: ${lastError?.message || "Unknown error"}`);
6530
+ throw wrapProviderError(`Provider ${providersToTry[0]} failed: ${lastError?.message || "Unknown error"}`);
6501
6531
  }
6502
- throw new Error(`Failed to generate text with all providers. Last error: ${lastError?.message || "Unknown error"}`);
6532
+ throw wrapProviderError(`Failed to generate text with all providers. Last error: ${lastError?.message || "Unknown error"}`);
6503
6533
  }
6504
6534
  /**
6505
6535
  * Create tool-aware system prompt that informs AI about available tools
@@ -1364,6 +1364,16 @@ export class GoogleAIStudioProvider extends BaseProvider {
1364
1364
  result._generationEndEmitted =
1365
1365
  true;
1366
1366
  }
1367
+ // The failure path has no result to carry that flag — `result` is null —
1368
+ // so mark the error instead. Without this the provider's own
1369
+ // `generation:end` (success: false) and the SDK's emitGenerateErrorEvent
1370
+ // both fire for one failed native generate, double-counting every failure
1371
+ // in analytics and as two Pipeline B spans. The success path above was
1372
+ // already deduplicated; only the catch branch was missed.
1373
+ if (!success && error && typeof error === "object") {
1374
+ error._generationEndEmitted =
1375
+ true;
1376
+ }
1367
1377
  emitter.emit("generation:end", {
1368
1378
  provider: this.providerName,
1369
1379
  responseTime: Date.now() - startTime,
@@ -899,57 +899,66 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
899
899
  // Omit the tools for a pass that only has to reformat an answer the
900
900
  // model has already given. Dropping them is what makes `response_format`
901
901
  // legal again on a vendor that refuses the two together.
902
- withTools = true) => runNativeGenerateLoop({
903
- doGenerate,
904
- ...createNativeGenerateGuard({
905
- provider: this.providerName,
906
- availableInputTokens: getAvailableInputTokens(this.providerName, modelId, options.maxTokens ?? undefined),
907
- // Runs on every step of the generate loop, so a cyclic or
908
- // oversized tool schema must not throw the turn away. Shares the
909
- // estimator's own serializer: its fallback over-reports, which
910
- // makes the guard fire rather than silently size the overhead at
911
- // nothing.
912
- getFixedOverheadTokens: () => estimateTokens(serializeForEstimate({
913
- tools: v3Tools,
914
- responseFormat: format,
915
- }), this.providerName),
916
- }),
917
- conversation: conv,
918
- ...(withTools && v3Tools ? { tools: v3Tools } : {}),
919
- toolsRecord,
920
- ...(withTools && hasTools && options.toolChoice
921
- ? { toolChoice: resolveToolChoice(options, toolsRecord, true) }
922
- : {}),
923
- // The per-call `timeout` keeps its per-MODEL-CALL meaning once
924
- // `turnTimeoutMs` owns the whole-turn deadline, and it reaches the
925
- // model layer only through this channel. Without it each step fell
926
- // back to the provider default, so a caller asking for a short
927
- // per-call timeout got the default on every request.
928
- ...(typeof options.timeout === "number"
929
- ? {
930
- providerOptions: {
931
- neurolink: { timeoutMs: options.timeout },
932
- },
933
- }
934
- : {}),
935
- ...(format ? { responseFormat: format } : {}),
936
- maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
937
- ...(options.maxTokens ? { maxOutputTokens: options.maxTokens } : {}),
938
- ...(options.temperature !== undefined
939
- ? { temperature: options.temperature }
940
- : {}),
941
- ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),
942
- ...(options.toolTimeoutMs !== undefined
943
- ? { toolTimeoutMs: options.toolTimeoutMs }
944
- : {}),
945
- // withProviderRetry + handleProviderError are what the ai loop
946
- // supplied around each call: without them a 429 surfaces as a raw
947
- // upstream string instead of a RateLimitError, and a throttle is
948
- // never retried.
949
- runStep: (call) => withProviderRetry(call, trace.getActiveSpan() ?? undefined, `${this.providerName} generate`).catch((err) => {
950
- throw this.handleProviderError(err);
951
- }),
952
- }, toolExecutionSummaries);
902
+ withTools = true) => {
903
+ return runNativeGenerateLoop({
904
+ doGenerate,
905
+ ...createNativeGenerateGuard({
906
+ provider: this.providerName,
907
+ availableInputTokens: getAvailableInputTokens(this.providerName, modelId, options.maxTokens ?? undefined),
908
+ // Runs on every step of the generate loop, so a cyclic or
909
+ // oversized tool schema must not throw the turn away. Shares the
910
+ // estimator's own serializer: its fallback over-reports, which
911
+ // makes the guard fire rather than silently size the overhead at
912
+ // nothing.
913
+ // Charge the guard only for what this pass actually sends. A
914
+ // reformat pass runs with `withTools === false` and omits `tools`
915
+ // from the request (see the spread below), so counting them here
916
+ // over-reports the fixed overhead. That inflation is not cosmetic:
917
+ // it feeds `planLoopGuardReclaim`, so a large tool schema can push
918
+ // the estimate past the reclaim threshold and drop older tool
919
+ // exchanges out of a reformat request that would have fit.
920
+ getFixedOverheadTokens: () => estimateTokens(serializeForEstimate({
921
+ ...(withTools && v3Tools ? { tools: v3Tools } : {}),
922
+ responseFormat: format,
923
+ }), this.providerName),
924
+ }),
925
+ conversation: conv,
926
+ ...(withTools && v3Tools ? { tools: v3Tools } : {}),
927
+ toolsRecord,
928
+ ...(withTools && hasTools && options.toolChoice
929
+ ? { toolChoice: resolveToolChoice(options, toolsRecord, true) }
930
+ : {}),
931
+ // The per-call `timeout` keeps its per-MODEL-CALL meaning once
932
+ // `turnTimeoutMs` owns the whole-turn deadline, and it reaches the
933
+ // model layer only through this channel. Without it each step fell
934
+ // back to the provider default, so a caller asking for a short
935
+ // per-call timeout got the default on every request.
936
+ ...(typeof options.timeout === "number"
937
+ ? {
938
+ providerOptions: {
939
+ neurolink: { timeoutMs: options.timeout },
940
+ },
941
+ }
942
+ : {}),
943
+ ...(format ? { responseFormat: format } : {}),
944
+ maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
945
+ ...(options.maxTokens ? { maxOutputTokens: options.maxTokens } : {}),
946
+ ...(options.temperature !== undefined
947
+ ? { temperature: options.temperature }
948
+ : {}),
949
+ ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),
950
+ ...(options.toolTimeoutMs !== undefined
951
+ ? { toolTimeoutMs: options.toolTimeoutMs }
952
+ : {}),
953
+ // withProviderRetry + handleProviderError are what the ai loop
954
+ // supplied around each call: without them a 429 surfaces as a raw
955
+ // upstream string instead of a RateLimitError, and a throttle is
956
+ // never retried.
957
+ runStep: (call) => withProviderRetry(call, trace.getActiveSpan() ?? undefined, `${this.providerName} generate`).catch((err) => {
958
+ throw this.handleProviderError(err);
959
+ }),
960
+ }, toolExecutionSummaries);
961
+ };
953
962
  // Structured output rides `response_format` first. When a vendor rejects
954
963
  // that outright — a tools/JSON-mode conflict, or a schema its constrained
955
964
  // decoder will not accept — the recovery is to ask for the same object in
@@ -4,6 +4,7 @@
4
4
  * Uses multi-strategy approach for reliable type identification
5
5
  */
6
6
  import { open, readFile, realpath } from "fs/promises";
7
+ import { STATUS_CODES } from "node:http";
7
8
  import { basename, isAbsolute as isAbsolutePath, relative as relativePath, resolve as resolvePath, sep, } from "path";
8
9
  import { request } from "undici";
9
10
  import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
@@ -675,21 +676,35 @@ export class FileDetector {
675
676
  new ContentHeuristicStrategy(),
676
677
  ];
677
678
  let best = null;
679
+ // #397: track which strategy produced `best` so the eventual log line
680
+ // can name it — the strategy instances themselves are gone by the time
681
+ // logging happens below, only their results survive.
682
+ let bestStrategyName = "none";
678
683
  for (const strategy of strategies) {
684
+ const strategyName = strategy.constructor.name;
679
685
  const result = await strategy.detect(strategy instanceof ExtensionStrategy && extensionInput
680
686
  ? extensionInput
681
687
  : input);
682
688
  if (!best || result.metadata.confidence > best.metadata.confidence) {
683
689
  best = result;
690
+ bestStrategyName = strategyName;
684
691
  }
685
692
  if (result.metadata.confidence >= confidenceThreshold) {
686
- logger.info(`[FileDetector] Type: ${result.type} (${result.metadata.confidence}%)`);
693
+ logger.info(`[FileDetector] Type: ${result.type} (${result.metadata.confidence}%, strategy: ${strategyName})`);
687
694
  return FileDetector.withResolvedExtension(result, input, options);
688
695
  }
689
696
  }
690
- // Below-threshold detection is the common case for any file under the
691
- // ContentHeuristic ceiling — a debug detail, not a warning-worthy anomaly.
692
- logger.debug(`[FileDetector] Best-effort type below threshold: ${best?.type ?? "unknown"} (${best?.metadata.confidence ?? 0}%, threshold ${confidenceThreshold}%)`);
697
+ // A genuinely unidentifiable input — every strategy failed to produce
698
+ // even a type — is a real failure, not a best-effort guess, so it is
699
+ // surfaced at error level instead of being buried in debug output.
700
+ if (!best || best.type === "unknown") {
701
+ logger.error(`[FileDetector] Detection failed: no strategy identified a type (best: ${bestStrategyName}, ${best?.metadata.confidence ?? 0}%, threshold ${confidenceThreshold}%)`);
702
+ }
703
+ else {
704
+ // Below-threshold detection is the common case for any file under the
705
+ // ContentHeuristic ceiling — a debug detail, not a warning-worthy anomaly.
706
+ logger.debug(`[FileDetector] Best-effort type below threshold: ${best.type} (${best.metadata.confidence}%, threshold ${confidenceThreshold}%, strategy: ${bestStrategyName})`);
707
+ }
693
708
  return FileDetector.withResolvedExtension(best, input, options);
694
709
  }
695
710
  /**
@@ -1543,9 +1558,15 @@ export class FileDetector {
1543
1558
  bodyTimeout: timeout,
1544
1559
  });
1545
1560
  if (response.statusCode !== 200) {
1546
- // Query string / fragment stripped — a presigned URL's token must
1547
- // not be echoed into a thrown error.
1548
- throw new Error(`HTTP ${response.statusCode} fetching ${redactUrlForError(url)}`);
1561
+ // #360: pair the code with its standard reason phrase (e.g. 404 →
1562
+ // "Not Found") so the message is readable rather than a bare
1563
+ // number. Sourced from Node's own `http.STATUS_CODES` table
1564
+ // rather than a hand-written map that could drift from it, and
1565
+ // falls back gracefully for a non-standard code that table
1566
+ // doesn't recognize. Query string / fragment stays stripped — a
1567
+ // presigned URL's token must not be echoed into a thrown error.
1568
+ const statusText = STATUS_CODES[response.statusCode];
1569
+ throw new Error(`HTTP ${response.statusCode}${statusText ? ` ${statusText}` : ""} fetching ${redactUrlForError(url)}`);
1549
1570
  }
1550
1571
  const contentType = response.headers["content-type"] || "";
1551
1572
  const declaredLength = Number(response.headers["content-length"]);
@@ -1877,10 +1877,14 @@ async function processImageToBase64(image, index) {
1877
1877
  const match = image.match(/^data:([^;]+);base64,(.+)$/);
1878
1878
  if (match) {
1879
1879
  const declaredMime = match[1];
1880
- // #348: only accept image/* data URIs; reject a non-image MIME before
1881
- // it reaches a provider API rather than passing it through unchecked.
1882
- if (!declaredMime.startsWith("image/")) {
1883
- throw new Error(`Unsupported data URI MIME type for image input at index ${index}: "${declaredMime}" (expected image/*)`);
1880
+ // #348: validate against the actual supported-image allowlist, not
1881
+ // just the "image/*" prefix — "image/invalid-format" started with
1882
+ // "image/" and passed here, only to fail later at the provider.
1883
+ // Reuses ImageProcessor's allowlist (itself sourced from
1884
+ // imageFormatSupport's SUPPORTED_INPUT_IMAGE_MIME_TYPES) so intake
1885
+ // validation can never drift from the set NeuroLink actually accepts.
1886
+ if (!ImageProcessor.validateImageFormat(declaredMime)) {
1887
+ throw new Error(`Unsupported data URI MIME type for image input at index ${index}: "${declaredMime}" (not a supported image format)`);
1884
1888
  }
1885
1889
  mimeType = declaredMime;
1886
1890
  imageData = match[2]; // Raw base64 only — NOT the full data: URI
@@ -2323,7 +2327,16 @@ pdfFiles, provider, model, audioFiles = []) {
2323
2327
  for (const pdf of pdfFiles) {
2324
2328
  try {
2325
2329
  const effectiveMaxPages = pdf.maxPages ?? PDF_LIMITS.DEFAULT_MAX_PAGES;
2326
- const conversionResult = await PDFImageConverter.convertToImages(pdf.buffer, {
2330
+ // #302: stream pages one at a time instead of materialising every
2331
+ // rendered page before any of them reaches `content` — this is the
2332
+ // path generate()/stream() callers actually exercise, so it must
2333
+ // carry the memory characteristic convertToImagesStream was built
2334
+ // for. Per-page ordering, error isolation (#294) and the resulting
2335
+ // message content are kept identical to the batch path below;
2336
+ // only how the pages are produced changes.
2337
+ const pageErrors = [];
2338
+ let convertedCount = 0;
2339
+ for await (const page of PDFImageConverter.convertToImagesStream(pdf.buffer, {
2327
2340
  // #297: this is the only PDF→image call the product actually makes,
2328
2341
  // and it used to hardcode scale 2.0 — silently overriding the
2329
2342
  // lowered PDF_LIMITS.DEFAULT_SCALE and keeping the memory cost the
@@ -2337,7 +2350,34 @@ pdfFiles, provider, model, audioFiles = []) {
2337
2350
  ...(pdf.maxCanvasPixels
2338
2351
  ? { maxCanvasPixels: pdf.maxCanvasPixels }
2339
2352
  : {}), // #260
2340
- });
2353
+ })) {
2354
+ if (page.error) {
2355
+ // #294: per-page isolation — a failed page is recorded and
2356
+ // skipped, not thrown, so the rest of the document still
2357
+ // reaches the model.
2358
+ pageErrors.push({ page: page.pageIndex, error: page.error });
2359
+ continue;
2360
+ }
2361
+ convertedCount++;
2362
+ // Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
2363
+ content.push({
2364
+ type: "image",
2365
+ image: page.image,
2366
+ mimeType: "image/png",
2367
+ });
2368
+ logger.debug(`[PDF→Image] Added page ${page.pageIndex} of ${pdf.filename}`);
2369
+ }
2370
+ // Batch's convertToImages throws when nothing converted (either a
2371
+ // zero-page PDF or every page failed) instead of returning an empty
2372
+ // result; convertToImagesStream has no such guard (it simply yields
2373
+ // nothing / all-error pages), so replicate that contract explicitly
2374
+ // to keep the two paths' failure behavior identical.
2375
+ if (convertedCount === 0) {
2376
+ if (pageErrors.length > 0) {
2377
+ throw new Error(`All ${pageErrors.length} page(s) failed to render. First error: ${pageErrors[0].error}`);
2378
+ }
2379
+ throw new Error("PDF has 0 pages. Cannot convert empty PDF to images.");
2380
+ }
2341
2381
  // The renderer stops at maxPages, so a longer document is silently
2342
2382
  // truncated — say so rather than letting the model answer from a
2343
2383
  // partial document as though it had the whole thing.
@@ -2348,31 +2388,16 @@ pdfFiles, provider, model, audioFiles = []) {
2348
2388
  // there. Reaching the cap is also unambiguous — a short count caused by
2349
2389
  // per-page render failures (#294 isolates those into `errors`) would
2350
2390
  // otherwise be misreported as a maxPages truncation.
2351
- if (conversionResult.pageCount >= effectiveMaxPages) {
2391
+ if (convertedCount >= effectiveMaxPages) {
2352
2392
  logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)} hit the ${effectiveMaxPages}-page ` +
2353
2393
  `conversion limit. Any pages beyond that were not sent — the model may be ` +
2354
2394
  `answering from a partial document. Raise pdfOptions.maxPages or split the file.`);
2355
2395
  }
2356
- if (conversionResult.errors && conversionResult.errors.length > 0) {
2357
- logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)}: ${conversionResult.errors.length} page(s) ` +
2358
- `failed to render and were omitted (page ${conversionResult.errors.map((e) => e.page).join(", ")}).`);
2359
- }
2360
- logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${conversionResult.pageCount} page(s) → images`);
2361
- // Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
2362
- conversionResult.images.forEach((base64Image, pageIndex) => {
2363
- content.push({
2364
- type: "image",
2365
- image: base64Image,
2366
- mimeType: "image/png",
2367
- });
2368
- logger.debug(`[PDF→Image] Added page ${pageIndex + 1}/${conversionResult.pageCount} of ${pdf.filename}`);
2369
- });
2370
- // Log any warnings from conversion
2371
- if (conversionResult.warnings) {
2372
- conversionResult.warnings.forEach((warning) => {
2373
- logger.warn(`[PDF→Image] ${warning}`);
2374
- });
2396
+ if (pageErrors.length > 0) {
2397
+ logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)}: ${pageErrors.length} page(s) ` +
2398
+ `failed to render and were omitted (page ${pageErrors.map((e) => e.page).join(", ")}).`);
2375
2399
  }
2400
+ logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${convertedCount} page(s) → images`);
2376
2401
  }
2377
2402
  catch (error) {
2378
2403
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -589,10 +589,32 @@ export class PDFProcessor {
589
589
  if (largestPixels > maxCanvasPixels) {
590
590
  effectiveScale = scale * Math.sqrt(maxCanvasPixels / largestPixels);
591
591
  }
592
- const document = await pdf(pdfBuffer, {
593
- scale: effectiveScale,
594
- ...(password ? { password } : {}),
595
- });
592
+ // #258: map pdfjs's PasswordException to the same actionable typed error
593
+ // convertToImages raises — kept in sync per the class docstring above.
594
+ // Unlike the per-page loop below, a failure here happens before any page
595
+ // can be isolated, so it must reject the generator outright rather than
596
+ // being yielded as a page error.
597
+ let document;
598
+ try {
599
+ document = await pdf(pdfBuffer, {
600
+ scale: effectiveScale,
601
+ ...(password ? { password } : {}),
602
+ });
603
+ }
604
+ catch (error) {
605
+ const errorMessage = error instanceof Error ? error.message : String(error);
606
+ const pdfErr = error;
607
+ if (pdfErr?.name === "PasswordException" ||
608
+ /password/i.test(errorMessage)) {
609
+ const incorrect = pdfErr.code === 2 || /incorrect|invalid/i.test(errorMessage);
610
+ throw incorrect
611
+ ? ErrorFactory.pdfIncorrectPassword()
612
+ : ErrorFactory.pdfPasswordRequired();
613
+ }
614
+ throw new Error(`PDF to image conversion failed: ${errorMessage}`, {
615
+ cause: error,
616
+ });
617
+ }
596
618
  const totalPages = document.length;
597
619
  let converted = 0;
598
620
  for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.17.2",
3
+ "version": "12.17.4",
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": {
@@ -80,6 +80,7 @@
80
80
  "test:context": "pnpm exec tsx test/continuous-test-suite-context.ts",
81
81
  "test:evaluation": "pnpm exec tsx test/continuous-test-suite-evaluation.ts",
82
82
  "test:handler-registry": "pnpm exec tsx test/continuous-test-suite-handler-registry.ts",
83
+ "test:harness-offline-timeout": "pnpm exec tsx test/continuous-test-suite-harness-offline-timeout.ts",
83
84
  "test:resolve-request-kind": "pnpm exec tsx test/continuous-test-suite-resolve-request-kind.ts",
84
85
  "test:mcp": "pnpm exec tsx test/continuous-test-suite-mcp-infra.ts",
85
86
  "test:tool-resolution": "pnpm exec tsx test/continuous-test-suite-tool-resolution.ts",
@@ -132,6 +133,7 @@
132
133
  "test:provider-fallback": "pnpm exec tsx test/continuous-test-suite-provider-fallback.ts",
133
134
  "test:provider-fallback-latency": "pnpm exec tsx test/continuous-test-suite-provider-fallback-latency.ts",
134
135
  "test:provider-wiring": "pnpm exec tsx test/continuous-test-suite-provider-wiring.ts",
136
+ "test:pdf-image-streaming": "pnpm exec tsx test/continuous-test-suite-pdf-image-streaming.ts",
135
137
  "test:rag": "pnpm exec tsx test/continuous-test-suite-rag.ts",
136
138
  "test:skills": "pnpm exec tsx test/continuous-test-suite-skills.ts",
137
139
  "test:servers": "pnpm exec tsx test/continuous-test-suite-servers.ts",
@@ -251,7 +253,9 @@
251
253
  "test:anthropic-execution-control": "tsx test/continuous-test-suite-anthropic-execution-control.ts",
252
254
  "test:aistudio-loop-characterization": "tsx test/continuous-test-suite-aistudio-loop-characterization.ts",
253
255
  "test:docs-mcp": "pnpm exec tsx test/continuous-test-suite-docs-mcp.ts",
254
- "test:vertex-claude-characterization": "tsx test/continuous-test-suite-vertex-claude-characterization.ts"
256
+ "test:vertex-claude-characterization": "tsx test/continuous-test-suite-vertex-claude-characterization.ts",
257
+ "test:toolfree-guard-overhead": "pnpm exec tsx test/continuous-test-suite-toolfree-guard-overhead.ts",
258
+ "test:native-failure-events": "pnpm exec tsx test/continuous-test-suite-native-failure-events.ts"
255
259
  },
256
260
  "files": [
257
261
  "dist",