@juspay/neurolink 12.21.2 → 12.22.1
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 +2 -4
- package/dist/browser/neurolink.min.js +393 -393
- package/dist/core/baseProvider.d.ts +18 -0
- package/dist/core/baseProvider.js +22 -0
- package/dist/core/constants.js +1 -1
- package/dist/factories/providerFactory.d.ts +20 -0
- package/dist/factories/providerFactory.js +100 -19
- package/dist/models/modelRegistry.d.ts +25 -0
- package/dist/models/modelRegistry.js +39 -0
- package/dist/providers/googleAiStudio/client.js +382 -50
- package/dist/providers/googleNativeGemini3/utils.d.ts +22 -0
- package/dist/providers/googleNativeGemini3/utils.js +24 -0
- package/dist/providers/googleVertex/client.d.ts +3 -12
- package/dist/providers/googleVertex/client.js +15 -34
- package/dist/providers/openaiChatCompletionsClient.d.ts +4 -0
- package/dist/providers/openaiChatCompletionsClient.js +20 -5
- package/dist/types/file.d.ts +13 -0
- package/dist/utils/pdfProcessor.d.ts +21 -3
- package/dist/utils/pdfProcessor.js +124 -48
- package/dist/utils/stringDistance.d.ts +54 -0
- package/dist/utils/stringDistance.js +133 -0
- package/dist/utils/toolCallRepair.js +54 -55
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -3
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared string-distance helpers for "did you mean?" suggestions.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from toolCallRepair.ts, which had the only implementation, so
|
|
5
|
+
* provider-name validation can reuse it rather than carry a second copy.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Longest string the distance primitive will compare.
|
|
9
|
+
*
|
|
10
|
+
* Exported so every call site can apply the same limit *before* calling
|
|
11
|
+
* `levenshtein` — skipping an over-limit operand entirely, rather than
|
|
12
|
+
* comparing a truncated prefix of it. Truncating silently merged any two
|
|
13
|
+
* strings that happened to share their first `MAX_COMPARABLE_LENGTH`
|
|
14
|
+
* characters into distance 0, which let a direct MCP tool-name lookup
|
|
15
|
+
* (`resolveToolName`) resolve to the wrong tool for a long, colliding name.
|
|
16
|
+
*/
|
|
17
|
+
export const MAX_COMPARABLE_LENGTH = 128;
|
|
18
|
+
const OVER_LIMIT_MESSAGE = `levenshtein: input exceeds MAX_COMPARABLE_LENGTH (${MAX_COMPARABLE_LENGTH}); callers must skip over-limit operands rather than truncate them`;
|
|
19
|
+
/**
|
|
20
|
+
* Compute Levenshtein edit distance between two strings.
|
|
21
|
+
* Uses the iterative matrix approach — O(m*n) time, O(min(m,n)) space.
|
|
22
|
+
*
|
|
23
|
+
* Throws rather than truncating when either input exceeds
|
|
24
|
+
* `MAX_COMPARABLE_LENGTH`: silently comparing truncated prefixes produces a
|
|
25
|
+
* *wrong* distance (two distinct strings sharing a long prefix collapse to
|
|
26
|
+
* distance 0) instead of no distance, which is worse than refusing outright.
|
|
27
|
+
* Callers that want fuzzy matching over long input must apply the length
|
|
28
|
+
* check themselves and skip the comparison — see `suggestClosest` below and
|
|
29
|
+
* `resolveToolName`/`rankToolNameCandidates` in toolCallRepair.ts.
|
|
30
|
+
*/
|
|
31
|
+
export function levenshtein(a, b) {
|
|
32
|
+
// Names reach this from request bodies and model output, where the static
|
|
33
|
+
// type is no runtime guarantee: a parsed-JSON object with a forged `length`
|
|
34
|
+
// must never size the matrix below. This `typeof` test is what CodeQL's
|
|
35
|
+
// js/loop-bound-injection recognises as ruling such an object out; a length
|
|
36
|
+
// comparison alone is not.
|
|
37
|
+
if (typeof a !== "string") {
|
|
38
|
+
throw new TypeError("levenshtein: operands must be strings");
|
|
39
|
+
}
|
|
40
|
+
if (typeof b !== "string") {
|
|
41
|
+
throw new TypeError("levenshtein: operands must be strings");
|
|
42
|
+
}
|
|
43
|
+
// Ahead of every return, so the limit holds for all inputs, including equal
|
|
44
|
+
// strings and an empty operand.
|
|
45
|
+
if (a.length > MAX_COMPARABLE_LENGTH) {
|
|
46
|
+
throw new RangeError(OVER_LIMIT_MESSAGE);
|
|
47
|
+
}
|
|
48
|
+
if (b.length > MAX_COMPARABLE_LENGTH) {
|
|
49
|
+
throw new RangeError(OVER_LIMIT_MESSAGE);
|
|
50
|
+
}
|
|
51
|
+
if (a === b) {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
if (a.length === 0) {
|
|
55
|
+
return b.length;
|
|
56
|
+
}
|
|
57
|
+
if (b.length === 0) {
|
|
58
|
+
return a.length;
|
|
59
|
+
}
|
|
60
|
+
// Use shorter string as column to minimize space
|
|
61
|
+
if (a.length > b.length) {
|
|
62
|
+
[a, b] = [b, a];
|
|
63
|
+
}
|
|
64
|
+
const aLen = a.length;
|
|
65
|
+
const bLen = b.length;
|
|
66
|
+
let prev = new Array(aLen + 1);
|
|
67
|
+
let curr = new Array(aLen + 1);
|
|
68
|
+
for (let i = 0; i <= aLen; i++) {
|
|
69
|
+
prev[i] = i;
|
|
70
|
+
}
|
|
71
|
+
for (let j = 1; j <= bLen; j++) {
|
|
72
|
+
curr[0] = j;
|
|
73
|
+
for (let i = 1; i <= aLen; i++) {
|
|
74
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
75
|
+
curr[i] = Math.min(prev[i] + 1, // deletion
|
|
76
|
+
curr[i - 1] + 1, // insertion
|
|
77
|
+
prev[i - 1] + cost);
|
|
78
|
+
}
|
|
79
|
+
[prev, curr] = [curr, prev];
|
|
80
|
+
}
|
|
81
|
+
return prev[aLen];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Candidates within `maxDistance` edits of `input`, closest first.
|
|
85
|
+
*
|
|
86
|
+
* Ties break on the candidate's own order in `candidates`, so a caller that
|
|
87
|
+
* passes canonical names before aliases gets the canonical one suggested.
|
|
88
|
+
* Comparison is case-insensitive; the returned strings keep their original
|
|
89
|
+
* casing so they can be pasted back verbatim.
|
|
90
|
+
*
|
|
91
|
+
* `input` longer than `MAX_COMPARABLE_LENGTH` is rejected outright (no
|
|
92
|
+
* suggestions), and any individual candidate over the same limit is skipped
|
|
93
|
+
* rather than compared — never truncated. Truncating either side risks
|
|
94
|
+
* exactly the false distance-0 match `levenshtein`'s guard rejects: two
|
|
95
|
+
* distinct strings sharing a long-enough prefix would otherwise look
|
|
96
|
+
* identical and "suggest" the wrong one.
|
|
97
|
+
*
|
|
98
|
+
* @param input - The string the user actually typed
|
|
99
|
+
* @param candidates - Valid values, most-preferred first
|
|
100
|
+
* @param options.maxDistance - Edit budget (default 3)
|
|
101
|
+
* @param options.limit - Most suggestions to return (default 3)
|
|
102
|
+
*/
|
|
103
|
+
export function suggestClosest(input, candidates, options) {
|
|
104
|
+
const maxDistance = options?.maxDistance ?? 3;
|
|
105
|
+
const limit = options?.limit ?? 3;
|
|
106
|
+
// Limits apply to the lowercased strings, since those are what get compared
|
|
107
|
+
// and lowercasing can lengthen a string ("İ" becomes two code units).
|
|
108
|
+
const needle = input.toLowerCase();
|
|
109
|
+
if (needle.length > MAX_COMPARABLE_LENGTH) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const scored = [];
|
|
113
|
+
candidates.forEach((candidate, order) => {
|
|
114
|
+
const lowered = candidate.toLowerCase();
|
|
115
|
+
// Two strings whose lengths differ by more than the edit budget cannot be
|
|
116
|
+
// within it, so this rules most candidates out without building a matrix
|
|
117
|
+
// for them.
|
|
118
|
+
if (Math.abs(lowered.length - needle.length) > maxDistance) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (lowered.length > MAX_COMPARABLE_LENGTH) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const distance = levenshtein(needle, lowered);
|
|
125
|
+
if (distance <= maxDistance) {
|
|
126
|
+
scored.push({ candidate, order, distance });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return scored
|
|
130
|
+
.sort((a, b) => a.distance - b.distance || a.order - b.order)
|
|
131
|
+
.slice(0, limit)
|
|
132
|
+
.map((entry) => entry.candidate);
|
|
133
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { logger } from "./logger.js";
|
|
2
|
+
import { levenshtein, MAX_COMPARABLE_LENGTH } from "./stringDistance.js";
|
|
2
3
|
/**
|
|
3
4
|
* Create an `experimental_repairToolCall` handler for streamText/generateText.
|
|
4
5
|
* Fully dynamic — reads the tool schema at repair time, no configuration needed.
|
|
@@ -58,15 +59,27 @@ export function resolveToolName(calledName, availableTools) {
|
|
|
58
59
|
}
|
|
59
60
|
// 3. Levenshtein distance — accept if normalized distance < 0.3
|
|
60
61
|
// Compare by normalized score (not raw edits) so length differences don't skew selection.
|
|
62
|
+
// `levenshtein` throws rather than truncates past MAX_COMPARABLE_LENGTH (see
|
|
63
|
+
// stringDistance.ts), so an over-limit name is skipped here instead of
|
|
64
|
+
// compared on a truncated prefix — a called name and a tool name that
|
|
65
|
+
// merely share a long-enough prefix must never tie at distance 0 and
|
|
66
|
+
// resolve to the wrong tool. The guard checks the lowercased operands, since
|
|
67
|
+
// those are what get compared and lowercasing can lengthen a string ("İ").
|
|
61
68
|
let bestMatch = null;
|
|
62
69
|
let bestNormalized = Infinity;
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
if (calledLower.length <= MAX_COMPARABLE_LENGTH) {
|
|
71
|
+
for (const t of availableTools) {
|
|
72
|
+
const tLower = t.toLowerCase();
|
|
73
|
+
if (tLower.length > MAX_COMPARABLE_LENGTH) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const dist = levenshtein(calledLower, tLower);
|
|
77
|
+
const maxLen = Math.max(calledName.length, t.length);
|
|
78
|
+
const normalized = maxLen === 0 ? 0 : dist / maxLen;
|
|
79
|
+
if (normalized < 0.3 && normalized < bestNormalized) {
|
|
80
|
+
bestNormalized = normalized;
|
|
81
|
+
bestMatch = t;
|
|
82
|
+
}
|
|
70
83
|
}
|
|
71
84
|
}
|
|
72
85
|
if (bestMatch) {
|
|
@@ -85,10 +98,27 @@ export function resolveToolName(calledName, availableTools) {
|
|
|
85
98
|
*/
|
|
86
99
|
export function rankToolNameCandidates(calledName, availableTools, limit = 5) {
|
|
87
100
|
const calledLower = calledName.toLowerCase();
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
const calledWithinLimit = calledLower.length <= MAX_COMPARABLE_LENGTH;
|
|
102
|
+
// `levenshtein` throws past MAX_COMPARABLE_LENGTH instead of truncating, so
|
|
103
|
+
// an over-limit operand is scored as "furthest away" rather than compared
|
|
104
|
+
// on a truncated (and potentially falsely-tied) prefix. This still only
|
|
105
|
+
// orders the list — no candidate is dropped — matching this function's
|
|
106
|
+
// "no accept/reject judgment" contract.
|
|
107
|
+
const distanceTo = (name) => {
|
|
108
|
+
const nameLower = name.toLowerCase();
|
|
109
|
+
if (!calledWithinLimit || nameLower.length > MAX_COMPARABLE_LENGTH) {
|
|
110
|
+
return Number.POSITIVE_INFINITY;
|
|
111
|
+
}
|
|
112
|
+
return levenshtein(calledLower, nameLower);
|
|
113
|
+
};
|
|
114
|
+
// Precompute once per tool (not per comparison), and compare with an
|
|
115
|
+
// explicit equality check — two Infinity distances (both over-limit) would
|
|
116
|
+
// otherwise subtract to NaN, an invalid sort-comparator result.
|
|
117
|
+
return availableTools
|
|
118
|
+
.map((name) => ({ name, distance: distanceTo(name) }))
|
|
119
|
+
.sort((a, b) => (a.distance === b.distance ? 0 : a.distance - b.distance))
|
|
120
|
+
.slice(0, limit)
|
|
121
|
+
.map((entry) => entry.name);
|
|
92
122
|
}
|
|
93
123
|
/**
|
|
94
124
|
* Thrown at a direct MCP execution boundary (`NeuroLink.executeExternalMCPTool`)
|
|
@@ -220,14 +250,21 @@ function findMatchingKey(inputKey, schemaKeys) {
|
|
|
220
250
|
if (ciMatch) {
|
|
221
251
|
return ciMatch;
|
|
222
252
|
}
|
|
223
|
-
// Levenshtein — threshold ≤2 edits
|
|
253
|
+
// Levenshtein — threshold ≤2 edits. Same over-limit guard as
|
|
254
|
+
// resolveToolName above: skip rather than let levenshtein() truncate.
|
|
224
255
|
let best = null;
|
|
225
256
|
let bestDist = Infinity;
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
257
|
+
if (inputLower.length <= MAX_COMPARABLE_LENGTH) {
|
|
258
|
+
for (const k of schemaKeys) {
|
|
259
|
+
const kLower = k.toLowerCase();
|
|
260
|
+
if (kLower.length > MAX_COMPARABLE_LENGTH) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const dist = levenshtein(inputLower, kLower);
|
|
264
|
+
if (dist <= 2 && dist < bestDist) {
|
|
265
|
+
bestDist = dist;
|
|
266
|
+
best = k;
|
|
267
|
+
}
|
|
231
268
|
}
|
|
232
269
|
}
|
|
233
270
|
return best;
|
|
@@ -308,41 +345,3 @@ export function coerceType(value, propSchema) {
|
|
|
308
345
|
}
|
|
309
346
|
return value;
|
|
310
347
|
}
|
|
311
|
-
// ─── Levenshtein Distance ──────────────────────────────────────────
|
|
312
|
-
/**
|
|
313
|
-
* Compute Levenshtein edit distance between two strings.
|
|
314
|
-
* Uses the iterative matrix approach — O(m*n) time, O(min(m,n)) space.
|
|
315
|
-
*/
|
|
316
|
-
function levenshtein(a, b) {
|
|
317
|
-
if (a === b) {
|
|
318
|
-
return 0;
|
|
319
|
-
}
|
|
320
|
-
if (a.length === 0) {
|
|
321
|
-
return b.length;
|
|
322
|
-
}
|
|
323
|
-
if (b.length === 0) {
|
|
324
|
-
return a.length;
|
|
325
|
-
}
|
|
326
|
-
// Use shorter string as column to minimize space
|
|
327
|
-
if (a.length > b.length) {
|
|
328
|
-
[a, b] = [b, a];
|
|
329
|
-
}
|
|
330
|
-
const aLen = a.length;
|
|
331
|
-
const bLen = b.length;
|
|
332
|
-
let prev = new Array(aLen + 1);
|
|
333
|
-
let curr = new Array(aLen + 1);
|
|
334
|
-
for (let i = 0; i <= aLen; i++) {
|
|
335
|
-
prev[i] = i;
|
|
336
|
-
}
|
|
337
|
-
for (let j = 1; j <= bLen; j++) {
|
|
338
|
-
curr[0] = j;
|
|
339
|
-
for (let i = 1; i <= aLen; i++) {
|
|
340
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
341
|
-
curr[i] = Math.min(prev[i] + 1, // deletion
|
|
342
|
-
curr[i - 1] + 1, // insertion
|
|
343
|
-
prev[i - 1] + cost);
|
|
344
|
-
}
|
|
345
|
-
[prev, curr] = [curr, prev];
|
|
346
|
-
}
|
|
347
|
-
return prev[aLen];
|
|
348
|
-
}
|