@juspay/neurolink 12.17.2 → 12.17.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.
- package/CHANGELOG.md +6 -2
- package/dist/browser/neurolink.min.js +396 -396
- package/dist/neurolink.js +8 -3
- package/dist/providers/openaiChatCompletionsBase.js +60 -51
- package/dist/utils/fileDetector.js +28 -7
- package/dist/utils/messageBuilder.js +51 -26
- package/dist/utils/pdfProcessor.js +26 -4
- package/package.json +5 -2
package/dist/neurolink.js
CHANGED
|
@@ -5899,7 +5899,10 @@ Current user's request: ${currentInput}`;
|
|
|
5899
5899
|
toolsUsed: result.toolsUsed || [],
|
|
5900
5900
|
toolCalls: result.toolCalls ?? [],
|
|
5901
5901
|
toolExecutions: transformedToolExecutions,
|
|
5902
|
-
|
|
5902
|
+
// A tool RAN (toolsUsed), not merely dispatched (toolExecutions can
|
|
5903
|
+
// include failed/never-found calls) — see hasToolExecutions above,
|
|
5904
|
+
// which still gates the null-result check and must not change.
|
|
5905
|
+
enhancedWithTools: !!(result.toolsUsed && result.toolsUsed.length > 0),
|
|
5903
5906
|
availableTools: transformToolsForMCP(transformToolsToExpectedFormat(availableTools)),
|
|
5904
5907
|
audio: result.audio,
|
|
5905
5908
|
ttsMetadata: result.ttsMetadata,
|
|
@@ -6060,7 +6063,8 @@ Current user's request: ${currentInput}`;
|
|
|
6060
6063
|
executionTime: te.durationMs,
|
|
6061
6064
|
success: !te.isError,
|
|
6062
6065
|
})),
|
|
6063
|
-
|
|
6066
|
+
// A tool RAN (toolsUsed), not merely dispatched.
|
|
6067
|
+
enhancedWithTools: !!poolResult.toolsUsed?.length,
|
|
6064
6068
|
analytics: poolResult.analytics,
|
|
6065
6069
|
evaluation: poolResult.evaluation,
|
|
6066
6070
|
audio: poolResult.audio,
|
|
@@ -6432,7 +6436,8 @@ Current user's request: ${currentInput}`;
|
|
|
6432
6436
|
executionTime: te.durationMs,
|
|
6433
6437
|
success: !te.isError,
|
|
6434
6438
|
})),
|
|
6435
|
-
|
|
6439
|
+
// A tool RAN (toolsUsed), not merely dispatched.
|
|
6440
|
+
enhancedWithTools: !!result.toolsUsed?.length,
|
|
6436
6441
|
analytics: result.analytics,
|
|
6437
6442
|
evaluation: result.evaluation,
|
|
6438
6443
|
audio: result.audio,
|
|
@@ -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) =>
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
? {
|
|
944
|
-
:
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
//
|
|
691
|
-
//
|
|
692
|
-
|
|
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
|
-
//
|
|
1547
|
-
//
|
|
1548
|
-
|
|
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:
|
|
1881
|
-
//
|
|
1882
|
-
|
|
1883
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
2357
|
-
logger.warn(`[PDF→Image] ${safeBasename(pdf.filename)}: ${
|
|
2358
|
-
`failed to render and were omitted (page ${
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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.
|
|
3
|
+
"version": "12.17.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": {
|
|
@@ -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,8 @@
|
|
|
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"
|
|
255
258
|
},
|
|
256
259
|
"files": [
|
|
257
260
|
"dist",
|