@juspay/neurolink 9.76.0 → 9.78.0
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 +12 -0
- package/dist/browser/neurolink.min.js +341 -341
- package/dist/core/toolRouting.d.ts +9 -0
- package/dist/core/toolRouting.js +178 -1
- package/dist/core/toolRoutingEmbedding.d.ts +112 -0
- package/dist/core/toolRoutingEmbedding.js +322 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +35 -0
- package/dist/lib/core/toolRouting.d.ts +9 -0
- package/dist/lib/core/toolRouting.js +178 -1
- package/dist/lib/core/toolRoutingEmbedding.d.ts +112 -0
- package/dist/lib/core/toolRoutingEmbedding.js +323 -0
- package/dist/lib/index.d.ts +31 -0
- package/dist/lib/index.js +35 -0
- package/dist/lib/neurolink.d.ts +22 -0
- package/dist/lib/neurolink.js +544 -175
- package/dist/lib/routing/index.d.ts +7 -0
- package/dist/lib/routing/index.js +8 -0
- package/dist/lib/routing/modelPool.d.ts +83 -0
- package/dist/lib/routing/modelPool.js +243 -0
- package/dist/lib/routing/requestRouter.d.ts +30 -0
- package/dist/lib/routing/requestRouter.js +81 -0
- package/dist/lib/types/config.d.ts +18 -0
- package/dist/lib/types/index.d.ts +2 -0
- package/dist/lib/types/index.js +4 -0
- package/dist/lib/types/modelPool.d.ts +47 -0
- package/dist/lib/types/modelPool.js +11 -0
- package/dist/lib/types/requestRouter.d.ts +75 -0
- package/dist/lib/types/requestRouter.js +16 -0
- package/dist/lib/types/toolRouting.d.ts +171 -0
- package/dist/lib/utils/providerErrorClassification.d.ts +24 -0
- package/dist/lib/utils/providerErrorClassification.js +89 -0
- package/dist/neurolink.d.ts +22 -0
- package/dist/neurolink.js +544 -175
- package/dist/routing/index.d.ts +7 -0
- package/dist/routing/index.js +7 -0
- package/dist/routing/modelPool.d.ts +83 -0
- package/dist/routing/modelPool.js +242 -0
- package/dist/routing/requestRouter.d.ts +30 -0
- package/dist/routing/requestRouter.js +80 -0
- package/dist/types/config.d.ts +18 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +4 -0
- package/dist/types/modelPool.d.ts +47 -0
- package/dist/types/modelPool.js +10 -0
- package/dist/types/requestRouter.d.ts +75 -0
- package/dist/types/requestRouter.js +15 -0
- package/dist/types/toolRouting.d.ts +171 -0
- package/dist/utils/providerErrorClassification.d.ts +24 -0
- package/dist/utils/providerErrorClassification.js +88 -0
- package/package.json +6 -2
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedding-retrieval fast-path for pre-call tool routing (ITEM B / L2).
|
|
3
|
+
*
|
|
4
|
+
* For large tool catalogs, ranking by semantic similarity is far cheaper than
|
|
5
|
+
* an LLM router call and benchmarks at sub-10 ms when embedding vectors are
|
|
6
|
+
* cached. This module is intentionally PURE with no provider imports: the
|
|
7
|
+
* caller injects an `embedFn` so the retriever stays testable and free of
|
|
8
|
+
* circular dependencies.
|
|
9
|
+
*
|
|
10
|
+
* Hybrid scoring formula:
|
|
11
|
+
* score = weights.cosine * cosineSimilarity(queryVec, toolVec)
|
|
12
|
+
* + weights.bm25 * bm25Score(query, toolText)
|
|
13
|
+
*
|
|
14
|
+
* Both components are independently normalized to [0, 1] before combination
|
|
15
|
+
* so neither scale dominates.
|
|
16
|
+
*
|
|
17
|
+
* Fail-safe contract:
|
|
18
|
+
* `ToolEmbeddingIndex.rank()` and `selectRelevantToolNames()` propagate any
|
|
19
|
+
* error thrown by the injected `embedFn`. The CALLER (tool-routing
|
|
20
|
+
* orchestration) is responsible for catching that error and degrading
|
|
21
|
+
* gracefully to the LLM-router / server-granularity path.
|
|
22
|
+
*
|
|
23
|
+
* Determinism:
|
|
24
|
+
* All public functions are deterministic given the same inputs and embedFn.
|
|
25
|
+
* No Math.random(), no argless Date, no global mutable state outside the
|
|
26
|
+
* instance-level vector cache (which is keyed by text content).
|
|
27
|
+
*/
|
|
28
|
+
import { withTimeout } from "../utils/async/index.js";
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Constants
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
const DEFAULT_WEIGHTS = { cosine: 0.8, bm25: 0.2 };
|
|
33
|
+
/** BM25 free-parameter k1 — controls term-frequency saturation. */
|
|
34
|
+
const BM25_K1 = 1.5;
|
|
35
|
+
/** BM25 free-parameter b — controls document-length normalisation. */
|
|
36
|
+
const BM25_B = 0.75;
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Primitive helpers (pure, no side-effects)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
/**
|
|
41
|
+
* Tokenise text for BM25/lexical scoring: lowercase, strip punctuation, split
|
|
42
|
+
* on whitespace, drop empty tokens.
|
|
43
|
+
*/
|
|
44
|
+
function tokenize(text) {
|
|
45
|
+
return text
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.replace(/[^\w\s]/g, " ")
|
|
48
|
+
.split(/\s+/)
|
|
49
|
+
.filter((t) => t.length > 0);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Cosine similarity between two vectors.
|
|
53
|
+
*
|
|
54
|
+
* Returns 0 when either vector is zero-length, has zero magnitude, or the
|
|
55
|
+
* lengths differ — all of which indicate the comparison is meaningless.
|
|
56
|
+
*/
|
|
57
|
+
export function cosineSimilarity(a, b) {
|
|
58
|
+
if (a.length === 0 || b.length === 0 || a.length !== b.length) {
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
let dot = 0;
|
|
62
|
+
let magA = 0;
|
|
63
|
+
let magB = 0;
|
|
64
|
+
for (let i = 0; i < a.length; i++) {
|
|
65
|
+
dot += a[i] * b[i];
|
|
66
|
+
magA += a[i] * a[i];
|
|
67
|
+
magB += b[i] * b[i];
|
|
68
|
+
}
|
|
69
|
+
if (magA === 0 || magB === 0) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// BM25 scorer
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
/**
|
|
78
|
+
* A minimal, deterministic BM25 corpus over a fixed set of documents.
|
|
79
|
+
* Documents are indexed at construction time; only `score()` is exposed
|
|
80
|
+
* for query-time use.
|
|
81
|
+
*
|
|
82
|
+
* This is a TF-IDF/BM25 hybrid consistent with the InMemoryBM25Index in
|
|
83
|
+
* `src/lib/rag/retrieval/hybridSearch.ts`, adapted for a read-once static
|
|
84
|
+
* corpus (tool descriptions do not change within a turn).
|
|
85
|
+
*
|
|
86
|
+
* `ToolRoutingBm25Doc` is the per-document shape; it lives in
|
|
87
|
+
* `src/lib/types/toolRouting.ts` per Critical Rule 2.
|
|
88
|
+
*/
|
|
89
|
+
class Bm25Corpus {
|
|
90
|
+
docs;
|
|
91
|
+
avgDocLength;
|
|
92
|
+
/** IDF per unique token across all documents (computed once). */
|
|
93
|
+
idf;
|
|
94
|
+
constructor(texts) {
|
|
95
|
+
this.docs = texts.map((text) => {
|
|
96
|
+
const tokens = tokenize(text);
|
|
97
|
+
const tf = new Map();
|
|
98
|
+
for (const t of tokens) {
|
|
99
|
+
tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
100
|
+
}
|
|
101
|
+
return { tokens, tf };
|
|
102
|
+
});
|
|
103
|
+
const totalLength = this.docs.reduce((s, d) => s + d.tokens.length, 0);
|
|
104
|
+
this.avgDocLength =
|
|
105
|
+
this.docs.length > 0 ? totalLength / this.docs.length : 1;
|
|
106
|
+
// Precompute IDF for every token that appears in any document.
|
|
107
|
+
const N = this.docs.length;
|
|
108
|
+
const df = new Map();
|
|
109
|
+
for (const doc of this.docs) {
|
|
110
|
+
for (const token of doc.tf.keys()) {
|
|
111
|
+
df.set(token, (df.get(token) ?? 0) + 1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
this.idf = new Map();
|
|
115
|
+
for (const [token, docFreq] of df) {
|
|
116
|
+
// Robertson/Sparck-Jones IDF (safe against 0):
|
|
117
|
+
// log((N - df + 0.5) / (df + 0.5) + 1)
|
|
118
|
+
this.idf.set(token, Math.log((N - docFreq + 0.5) / (docFreq + 0.5) + 1));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Returns the BM25 score of `docIndex` against `query`.
|
|
123
|
+
* Score is always >= 0. Returns 0 for empty queries or out-of-range indices.
|
|
124
|
+
*/
|
|
125
|
+
score(docIndex, query) {
|
|
126
|
+
if (docIndex < 0 || docIndex >= this.docs.length) {
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
const queryTokens = tokenize(query);
|
|
130
|
+
if (queryTokens.length === 0) {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
const doc = this.docs[docIndex];
|
|
134
|
+
const docLength = doc.tokens.length;
|
|
135
|
+
let total = 0;
|
|
136
|
+
for (const qt of queryTokens) {
|
|
137
|
+
const tf = doc.tf.get(qt) ?? 0;
|
|
138
|
+
if (tf === 0) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const idf = this.idf.get(qt) ?? 0;
|
|
142
|
+
// BM25 term contribution
|
|
143
|
+
const numerator = tf * (BM25_K1 + 1);
|
|
144
|
+
const denominator = tf + BM25_K1 * (1 - BM25_B + BM25_B * (docLength / this.avgDocLength));
|
|
145
|
+
total += idf * (numerator / denominator);
|
|
146
|
+
}
|
|
147
|
+
return total;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Normalisation helper
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
/**
|
|
154
|
+
* Min-max normalises an array of non-negative scores to [0, 1].
|
|
155
|
+
* Returns an array of zeros when all scores are identical (range === 0).
|
|
156
|
+
*/
|
|
157
|
+
function normalizeScores(scores) {
|
|
158
|
+
if (scores.length === 0) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
let min = scores[0] ?? 0;
|
|
162
|
+
let max = scores[0] ?? 0;
|
|
163
|
+
for (let i = 1; i < scores.length; i++) {
|
|
164
|
+
const s = scores[i] ?? 0;
|
|
165
|
+
if (s < min) {
|
|
166
|
+
min = s;
|
|
167
|
+
}
|
|
168
|
+
if (s > max) {
|
|
169
|
+
max = s;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const range = max - min;
|
|
173
|
+
if (range === 0) {
|
|
174
|
+
return scores.map(() => 0);
|
|
175
|
+
}
|
|
176
|
+
return scores.map((s) => (s - min) / range);
|
|
177
|
+
}
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// ToolEmbeddingIndex
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
/**
|
|
182
|
+
* An in-process index that ranks tool catalog items by hybrid semantic +
|
|
183
|
+
* lexical relevance to a query.
|
|
184
|
+
*
|
|
185
|
+
* Embedding vectors for tool descriptions are computed lazily on the first
|
|
186
|
+
* `rank()` call and cached by description text, so subsequent turns that
|
|
187
|
+
* share the same catalog pay only the cost of embedding the query itself.
|
|
188
|
+
*
|
|
189
|
+
* ### Fail-safe
|
|
190
|
+
* Any error thrown by `embedFn` propagates out of `rank()`. The CALLER must
|
|
191
|
+
* catch it and degrade to the LLM-router path; `ToolEmbeddingIndex` itself
|
|
192
|
+
* never silently swallows embedFn errors.
|
|
193
|
+
*
|
|
194
|
+
* ### Thread-safety
|
|
195
|
+
* The internal cache is a plain `Map`. Node.js is single-threaded so there
|
|
196
|
+
* are no data races, but if the same index instance is used concurrently
|
|
197
|
+
* (e.g. two turns in parallel) both calls will race to populate the cache;
|
|
198
|
+
* the last writer wins (same content either way because `embedFn` is
|
|
199
|
+
* deterministic for a given text).
|
|
200
|
+
*/
|
|
201
|
+
export class ToolEmbeddingIndex {
|
|
202
|
+
items;
|
|
203
|
+
embedFn;
|
|
204
|
+
bm25CorpusCache;
|
|
205
|
+
/**
|
|
206
|
+
* Cached embedding vectors keyed by the item's `text` field.
|
|
207
|
+
* Populated on first `rank()` call for texts not yet seen.
|
|
208
|
+
*
|
|
209
|
+
* Callers can share a single Map across multiple index instances (e.g.
|
|
210
|
+
* across turns in the same NeuroLink session) so tool vectors are computed
|
|
211
|
+
* once and reused. Pass the shared Map via the constructor's third argument.
|
|
212
|
+
*/
|
|
213
|
+
vectorCache;
|
|
214
|
+
constructor(items, embedFn,
|
|
215
|
+
/**
|
|
216
|
+
* Optional shared vector cache. When supplied, cached vectors from
|
|
217
|
+
* previous turns are reused and any newly-computed vectors are stored
|
|
218
|
+
* into this same Map, making it warm for the next call.
|
|
219
|
+
*/
|
|
220
|
+
sharedVectorCache) {
|
|
221
|
+
this.items = items;
|
|
222
|
+
this.embedFn = embedFn;
|
|
223
|
+
this.vectorCache = sharedVectorCache ?? new Map();
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Returns the top-K catalog items ranked by hybrid score descending.
|
|
227
|
+
*
|
|
228
|
+
* @throws If `embedFn` throws — propagated verbatim so the caller can fail
|
|
229
|
+
* open. No wrapping, no swallowing.
|
|
230
|
+
*/
|
|
231
|
+
async rank(query, opts) {
|
|
232
|
+
if (this.items.length === 0) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
const weights = opts.weights ?? DEFAULT_WEIGHTS;
|
|
236
|
+
// Identify which item texts still need embedding vectors.
|
|
237
|
+
const uncachedTexts = [
|
|
238
|
+
...new Set(this.items
|
|
239
|
+
.map((item) => item.text)
|
|
240
|
+
.filter((text) => !this.vectorCache.has(text))),
|
|
241
|
+
];
|
|
242
|
+
if (uncachedTexts.length > 0) {
|
|
243
|
+
// embedFn errors propagate — caller is responsible for fail-open.
|
|
244
|
+
const vectors = await withTimeout(this.embedFn(uncachedTexts), opts.timeoutMs ?? 10000, "ToolEmbeddingIndex.embed");
|
|
245
|
+
for (let i = 0; i < uncachedTexts.length; i++) {
|
|
246
|
+
const vec = vectors[i];
|
|
247
|
+
if (vec !== undefined) {
|
|
248
|
+
this.vectorCache.set(uncachedTexts[i], vec);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Embed the query (single call; the query is not cached since it changes
|
|
253
|
+
// every turn — caching query vectors is the caller's responsibility if
|
|
254
|
+
// desired, e.g. via ToolRoutingCache).
|
|
255
|
+
const [queryVector] = await withTimeout(this.embedFn([query]), opts.timeoutMs ?? 10000, "ToolEmbeddingIndex.queryEmbed");
|
|
256
|
+
// Build BM25 corpus lazily; rebuild only when the item texts change.
|
|
257
|
+
const corpusKey = this.items.map((item) => item.text).join("\x00");
|
|
258
|
+
if (!this.bm25CorpusCache || this.bm25CorpusCache.key !== corpusKey) {
|
|
259
|
+
this.bm25CorpusCache = {
|
|
260
|
+
key: corpusKey,
|
|
261
|
+
corpus: new Bm25Corpus(this.items.map((item) => item.text)),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const bm25Corpus = this.bm25CorpusCache.corpus;
|
|
265
|
+
// Compute raw cosine and BM25 scores for each item.
|
|
266
|
+
const rawCosine = [];
|
|
267
|
+
const rawBm25 = [];
|
|
268
|
+
for (let i = 0; i < this.items.length; i++) {
|
|
269
|
+
const toolVec = this.vectorCache.get(this.items[i].text) ?? [];
|
|
270
|
+
rawCosine.push(cosineSimilarity(queryVector ?? [], toolVec));
|
|
271
|
+
rawBm25.push(bm25Corpus.score(i, query));
|
|
272
|
+
}
|
|
273
|
+
// Normalize each component independently so scales don't interact.
|
|
274
|
+
const normCosine = normalizeScores(rawCosine);
|
|
275
|
+
const normBm25 = normalizeScores(rawBm25);
|
|
276
|
+
// Combine into hybrid score and zip with item names.
|
|
277
|
+
const ranked = this.items.map((item, i) => ({
|
|
278
|
+
name: item.name,
|
|
279
|
+
score: weights.cosine * (normCosine[i] ?? 0) +
|
|
280
|
+
weights.bm25 * (normBm25[i] ?? 0),
|
|
281
|
+
}));
|
|
282
|
+
// Sort descending by score, then by name for a deterministic tie-break.
|
|
283
|
+
ranked.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
284
|
+
return ranked.slice(0, opts.topK);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Convenience wrapper
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
/**
|
|
291
|
+
* Selects the most relevant tool names from a catalog given a query.
|
|
292
|
+
*
|
|
293
|
+
* Creates a temporary `ToolEmbeddingIndex`, runs `rank()`, and returns just
|
|
294
|
+
* the tool names. Use `ToolEmbeddingIndex` directly when you want to reuse
|
|
295
|
+
* cached tool vectors across multiple queries (e.g. multiple turns with the
|
|
296
|
+
* same catalog).
|
|
297
|
+
*
|
|
298
|
+
* @throws If `opts.embedFn` throws — propagated so the caller can degrade to
|
|
299
|
+
* the LLM-router path.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```ts
|
|
303
|
+
* const tools = await selectRelevantToolNames(
|
|
304
|
+
* "show me yesterday's sales",
|
|
305
|
+
* catalog.flatMap(server => server.toolNames.map(name => ({
|
|
306
|
+
* name,
|
|
307
|
+
* text: `${server.description} — ${name}`,
|
|
308
|
+
* }))),
|
|
309
|
+
* { topK: 5, embedFn: myEmbedFn },
|
|
310
|
+
* );
|
|
311
|
+
* // => ["analytics_getSales", "analytics_getRevenue", ...]
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
export async function selectRelevantToolNames(query, items, opts) {
|
|
315
|
+
const index = new ToolEmbeddingIndex(items, opts.embedFn, opts.vectorCache);
|
|
316
|
+
const ranked = await index.rank(query, {
|
|
317
|
+
topK: opts.topK,
|
|
318
|
+
weights: opts.weights,
|
|
319
|
+
timeoutMs: opts.timeoutMs,
|
|
320
|
+
});
|
|
321
|
+
return ranked.map((r) => r.name);
|
|
322
|
+
}
|
|
323
|
+
//# sourceMappingURL=toolRoutingEmbedding.js.map
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -219,6 +219,7 @@ export { logger } from "./utils/logger.js";
|
|
|
219
219
|
export { getPoolStats } from "./utils/redis.js";
|
|
220
220
|
export { ToolRoutingCache } from "./core/toolRoutingCache.js";
|
|
221
221
|
export { resolveToolRoutingExclusions, buildToolRoutingCatalog, buildRoutingQueryFromHistory, DEFAULT_ROUTER_PROMPT_PREFIX, } from "./core/toolRouting.js";
|
|
222
|
+
export { cosineSimilarity, ToolEmbeddingIndex, selectRelevantToolNames, } from "./core/toolRoutingEmbedding.js";
|
|
222
223
|
export declare function initializeTelemetry(): Promise<boolean>;
|
|
223
224
|
export declare function getTelemetryStatus(): Promise<{
|
|
224
225
|
enabled: boolean;
|
|
@@ -337,6 +338,36 @@ export { BALANCED_ADAPTIVE_WORKFLOW, createAdaptiveWorkflow, QUALITY_MAX_WORKFLO
|
|
|
337
338
|
export { CONSENSUS_3_FAST_WORKFLOW, CONSENSUS_3_WORKFLOW, createConsensus3WithPrompt, } from "./workflow/workflows/consensusWorkflow.js";
|
|
338
339
|
export { AGGRESSIVE_FALLBACK_WORKFLOW, FAST_FALLBACK_WORKFLOW, } from "./workflow/workflows/fallbackWorkflow.js";
|
|
339
340
|
export { createMultiJudgeWorkflow, MULTI_JUDGE_3_WORKFLOW, MULTI_JUDGE_5_WORKFLOW, } from "./workflow/workflows/multiJudgeWorkflow.js";
|
|
341
|
+
/**
|
|
342
|
+
* ModelPool and RequestRouter — opt-in multi-provider failover with
|
|
343
|
+
* error-class-aware cooldown, and a pluggable pre-call provider/model router.
|
|
344
|
+
*
|
|
345
|
+
* @example ModelPool
|
|
346
|
+
* ```typescript
|
|
347
|
+
* import { ModelPool, classifyProviderError } from '@juspay/neurolink';
|
|
348
|
+
*
|
|
349
|
+
* const pool = new ModelPool({
|
|
350
|
+
* members: [
|
|
351
|
+
* { provider: 'anthropic', model: 'claude-sonnet-4-5' },
|
|
352
|
+
* { provider: 'vertex', model: 'gemini-2.5-flash' },
|
|
353
|
+
* ],
|
|
354
|
+
* strategy: 'priority',
|
|
355
|
+
* cooldownMs: 30_000,
|
|
356
|
+
* });
|
|
357
|
+
* ```
|
|
358
|
+
*
|
|
359
|
+
* @example RequestRouter
|
|
360
|
+
* ```typescript
|
|
361
|
+
* import { createDefaultRequestRouter } from '@juspay/neurolink';
|
|
362
|
+
*
|
|
363
|
+
* const router = createDefaultRequestRouter({
|
|
364
|
+
* visionTier: { provider: 'vertex', model: 'gemini-2.5-pro' },
|
|
365
|
+
* largeTier: { provider: 'anthropic', model: 'claude-opus-4-5' },
|
|
366
|
+
* smallTier: { provider: 'anthropic', model: 'claude-haiku-3-5' },
|
|
367
|
+
* });
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
export { classifyProviderError, ModelPool, createDefaultRequestRouter, } from "./routing/index.js";
|
|
340
371
|
/**
|
|
341
372
|
* Server Adapters for exposing NeuroLink as HTTP APIs
|
|
342
373
|
*
|
package/dist/lib/index.js
CHANGED
|
@@ -362,6 +362,8 @@ export { getPoolStats } from "./utils/redis.js";
|
|
|
362
362
|
export { ToolRoutingCache } from "./core/toolRoutingCache.js";
|
|
363
363
|
// Pre-call tool routing resolver — exported for host integrations and testing
|
|
364
364
|
export { resolveToolRoutingExclusions, buildToolRoutingCatalog, buildRoutingQueryFromHistory, DEFAULT_ROUTER_PROMPT_PREFIX, } from "./core/toolRouting.js";
|
|
365
|
+
// Pre-call tool routing — embedding fast-path (ITEM B: L2 semantic retrieval)
|
|
366
|
+
export { cosineSimilarity, ToolEmbeddingIndex, selectRelevantToolNames, } from "./core/toolRoutingEmbedding.js";
|
|
365
367
|
// ============================================================================
|
|
366
368
|
// REAL-TIME SERVICES & TELEMETRY - Enterprise Platform Features
|
|
367
369
|
// ============================================================================
|
|
@@ -541,6 +543,39 @@ export { AGGRESSIVE_FALLBACK_WORKFLOW, FAST_FALLBACK_WORKFLOW, } from "./workflo
|
|
|
541
543
|
// Pre-built workflows - Multi-judge
|
|
542
544
|
export { createMultiJudgeWorkflow, MULTI_JUDGE_3_WORKFLOW, MULTI_JUDGE_5_WORKFLOW, } from "./workflow/workflows/multiJudgeWorkflow.js";
|
|
543
545
|
// ============================================================================
|
|
546
|
+
// MODEL POOL & REQUEST ROUTER - Pluggable pre-call routing + failover
|
|
547
|
+
// ============================================================================
|
|
548
|
+
/**
|
|
549
|
+
* ModelPool and RequestRouter — opt-in multi-provider failover with
|
|
550
|
+
* error-class-aware cooldown, and a pluggable pre-call provider/model router.
|
|
551
|
+
*
|
|
552
|
+
* @example ModelPool
|
|
553
|
+
* ```typescript
|
|
554
|
+
* import { ModelPool, classifyProviderError } from '@juspay/neurolink';
|
|
555
|
+
*
|
|
556
|
+
* const pool = new ModelPool({
|
|
557
|
+
* members: [
|
|
558
|
+
* { provider: 'anthropic', model: 'claude-sonnet-4-5' },
|
|
559
|
+
* { provider: 'vertex', model: 'gemini-2.5-flash' },
|
|
560
|
+
* ],
|
|
561
|
+
* strategy: 'priority',
|
|
562
|
+
* cooldownMs: 30_000,
|
|
563
|
+
* });
|
|
564
|
+
* ```
|
|
565
|
+
*
|
|
566
|
+
* @example RequestRouter
|
|
567
|
+
* ```typescript
|
|
568
|
+
* import { createDefaultRequestRouter } from '@juspay/neurolink';
|
|
569
|
+
*
|
|
570
|
+
* const router = createDefaultRequestRouter({
|
|
571
|
+
* visionTier: { provider: 'vertex', model: 'gemini-2.5-pro' },
|
|
572
|
+
* largeTier: { provider: 'anthropic', model: 'claude-opus-4-5' },
|
|
573
|
+
* smallTier: { provider: 'anthropic', model: 'claude-haiku-3-5' },
|
|
574
|
+
* });
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
577
|
+
export { classifyProviderError, ModelPool, createDefaultRequestRouter, } from "./routing/index.js";
|
|
578
|
+
// ============================================================================
|
|
544
579
|
// SERVER ADAPTERS - HTTP API Framework Integration
|
|
545
580
|
// ============================================================================
|
|
546
581
|
// Server Types
|
package/dist/lib/neurolink.d.ts
CHANGED
|
@@ -102,6 +102,7 @@ export declare class NeuroLink {
|
|
|
102
102
|
private conversationMemoryConfig?;
|
|
103
103
|
private toolRoutingConfig?;
|
|
104
104
|
private toolRoutingCacheInstance?;
|
|
105
|
+
private toolRoutingVectorCache?;
|
|
105
106
|
private toolDedupConfig?;
|
|
106
107
|
private enableOrchestration;
|
|
107
108
|
private authProvider?;
|
|
@@ -109,6 +110,8 @@ export declare class NeuroLink {
|
|
|
109
110
|
private authInitPromise?;
|
|
110
111
|
private credentials?;
|
|
111
112
|
private readonly fallbackConfig;
|
|
113
|
+
private readonly modelPool;
|
|
114
|
+
private readonly requestRouter;
|
|
112
115
|
/**
|
|
113
116
|
* Merge instance-level credentials with per-call credentials.
|
|
114
117
|
*
|
|
@@ -612,6 +615,25 @@ export declare class NeuroLink {
|
|
|
612
615
|
private maybeHandleEarlyGenerateResult;
|
|
613
616
|
private runStandardGenerateRequest;
|
|
614
617
|
private maybeApplyGenerateOrchestration;
|
|
618
|
+
/**
|
|
619
|
+
* Applies the host-configured `requestRouter` to `options` in place.
|
|
620
|
+
*
|
|
621
|
+
* The router is skipped when:
|
|
622
|
+
* - no `requestRouter` is configured on this instance, or
|
|
623
|
+
* - the caller explicitly set both `options.provider` AND `options.model`
|
|
624
|
+
* (we only skip the router if BOTH are set; a caller setting only one
|
|
625
|
+
* still lets the router fill in the other field).
|
|
626
|
+
*
|
|
627
|
+
* Fails open: any router error is logged at WARN level and the call
|
|
628
|
+
* continues with the original options unmodified.
|
|
629
|
+
*
|
|
630
|
+
* @param options — the mutable options object (generate or stream).
|
|
631
|
+
* @param promptText — the text prompt used to build the RouterInputContext.
|
|
632
|
+
* @param hasTools — true when at least one tool is available for this call.
|
|
633
|
+
* @param requiresVision — true when the call includes image attachments.
|
|
634
|
+
* @param thinkingLevel — optional thinking level string from the call options.
|
|
635
|
+
*/
|
|
636
|
+
private applyRequestRouter;
|
|
615
637
|
private prepareGenerateAugmentations;
|
|
616
638
|
private buildGenerateTextOptions;
|
|
617
639
|
private finalizeGenerateRequestResult;
|