@juspay/neurolink 9.79.2 → 9.80.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 +351 -347
- package/dist/cli/factories/commandFactory.js +61 -0
- package/dist/cli/utils/classifierRouterFlags.d.ts +19 -0
- package/dist/cli/utils/classifierRouterFlags.js +111 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/lib/neurolink.d.ts +14 -0
- package/dist/lib/neurolink.js +131 -0
- package/dist/lib/providers/googleNativeGemini3.d.ts +26 -0
- package/dist/lib/providers/googleNativeGemini3.js +48 -0
- package/dist/lib/providers/googleVertex.d.ts +16 -0
- package/dist/lib/providers/googleVertex.js +200 -24
- package/dist/lib/routing/classifierRouter.d.ts +52 -0
- package/dist/lib/routing/classifierRouter.js +269 -0
- package/dist/lib/routing/classifierStrategies.d.ts +23 -0
- package/dist/lib/routing/classifierStrategies.js +156 -0
- package/dist/lib/routing/index.d.ts +2 -0
- package/dist/lib/routing/index.js +2 -0
- package/dist/lib/session/globalSessionState.d.ts +10 -1
- package/dist/lib/session/globalSessionState.js +18 -0
- package/dist/lib/types/classifierRouter.d.ts +193 -0
- package/dist/lib/types/classifierRouter.js +17 -0
- package/dist/lib/types/cli.d.ts +27 -3
- package/dist/lib/types/config.d.ts +10 -0
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.js +2 -0
- package/dist/neurolink.d.ts +14 -0
- package/dist/neurolink.js +131 -0
- package/dist/providers/googleNativeGemini3.d.ts +26 -0
- package/dist/providers/googleNativeGemini3.js +48 -0
- package/dist/providers/googleVertex.d.ts +16 -0
- package/dist/providers/googleVertex.js +200 -24
- package/dist/routing/classifierRouter.d.ts +52 -0
- package/dist/routing/classifierRouter.js +268 -0
- package/dist/routing/classifierStrategies.d.ts +23 -0
- package/dist/routing/classifierStrategies.js +155 -0
- package/dist/routing/index.d.ts +2 -0
- package/dist/routing/index.js +2 -0
- package/dist/session/globalSessionState.d.ts +10 -1
- package/dist/session/globalSessionState.js +18 -0
- package/dist/types/classifierRouter.d.ts +193 -0
- package/dist/types/classifierRouter.js +16 -0
- package/dist/types/cli.d.ts +27 -3
- package/dist/types/config.d.ts +10 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +2 -0
- package/package.json +2 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClassifierRouter — the single "classify → pick model + tools → run" engine.
|
|
3
|
+
*
|
|
4
|
+
* Given a request snapshot it (1) classifies difficulty (heuristic or a cheap
|
|
5
|
+
* LLM), (2) selects the best provider/model from the host-declared base pool —
|
|
6
|
+
* cheaper/faster for easy tiers, more capable for hard tiers — enriching pool
|
|
7
|
+
* members with real cost/quality metadata from the model registry, and
|
|
8
|
+
* (3) narrows the tool set via per-difficulty directives or classifier hints.
|
|
9
|
+
*
|
|
10
|
+
* Pure of provider imports (mirrors ModelPool): the LLM caller is injected.
|
|
11
|
+
* The only data dependency is the read-only `ModelResolver` registry lookup,
|
|
12
|
+
* used purely for metadata enrichment and never for provider construction.
|
|
13
|
+
*/
|
|
14
|
+
import type { ClassifierRouterConfig, ClassifierRouterDecision, ClassifierRouterDeps, ClassifierRouterInput } from "../types/index.js";
|
|
15
|
+
export declare class ClassifierRouter {
|
|
16
|
+
private readonly config;
|
|
17
|
+
private readonly deps;
|
|
18
|
+
private readonly metaCache;
|
|
19
|
+
constructor(config: ClassifierRouterConfig, deps?: ClassifierRouterDeps);
|
|
20
|
+
/**
|
|
21
|
+
* Classify the request and produce a combined model + tool decision, or
|
|
22
|
+
* `null` when nothing should change. Never throws (fails open).
|
|
23
|
+
*/
|
|
24
|
+
route(input: ClassifierRouterInput): Promise<ClassifierRouterDecision | null>;
|
|
25
|
+
/** Run the configured strategy; LLM falls back to heuristic on failure. */
|
|
26
|
+
private classify;
|
|
27
|
+
/**
|
|
28
|
+
* Build LLM-facing model descriptors + an id→member map so the classifier can
|
|
29
|
+
* pick a model directly. Works for ANY pool, registry-backed or not.
|
|
30
|
+
*/
|
|
31
|
+
private buildCandidates;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the ranked model list for a decision. Prefers the LLM's direct pick
|
|
34
|
+
* (`selectedModelId`); otherwise falls back to difficulty-based tierMap /
|
|
35
|
+
* metadata scoring.
|
|
36
|
+
*/
|
|
37
|
+
private selectModels;
|
|
38
|
+
/** Candidate members for a difficulty: explicit tierMap or eligible pool. */
|
|
39
|
+
private candidatesFor;
|
|
40
|
+
/** Drop members that cannot satisfy the required capabilities (lenient). */
|
|
41
|
+
private filterByCapabilities;
|
|
42
|
+
/** Rank candidates best-first for the difficulty's strategy. */
|
|
43
|
+
private rank;
|
|
44
|
+
/** Tool narrowing: per-difficulty directive, then classifier hints. */
|
|
45
|
+
private selectTools;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve cost/quality/capabilities for a member. Declared values win;
|
|
48
|
+
* gaps are filled from the model registry (by model name/alias) when known.
|
|
49
|
+
* Results are cached per (provider/model) for the router's lifetime.
|
|
50
|
+
*/
|
|
51
|
+
private metaFor;
|
|
52
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClassifierRouter — the single "classify → pick model + tools → run" engine.
|
|
3
|
+
*
|
|
4
|
+
* Given a request snapshot it (1) classifies difficulty (heuristic or a cheap
|
|
5
|
+
* LLM), (2) selects the best provider/model from the host-declared base pool —
|
|
6
|
+
* cheaper/faster for easy tiers, more capable for hard tiers — enriching pool
|
|
7
|
+
* members with real cost/quality metadata from the model registry, and
|
|
8
|
+
* (3) narrows the tool set via per-difficulty directives or classifier hints.
|
|
9
|
+
*
|
|
10
|
+
* Pure of provider imports (mirrors ModelPool): the LLM caller is injected.
|
|
11
|
+
* The only data dependency is the read-only `ModelResolver` registry lookup,
|
|
12
|
+
* used purely for metadata enrichment and never for provider construction.
|
|
13
|
+
*/
|
|
14
|
+
import { classifyHeuristic, classifyLlm } from "./classifierStrategies.js";
|
|
15
|
+
import { ModelResolver } from "../models/modelResolver.js";
|
|
16
|
+
/** Maps a registry quality enum to a comparable numeric score. */
|
|
17
|
+
const QUALITY_SCORE = { high: 3, medium: 2, low: 1 };
|
|
18
|
+
/** How each difficulty ranks candidate members. */
|
|
19
|
+
const DIFFICULTY_RANK_MODE = {
|
|
20
|
+
trivial: "cost-asc",
|
|
21
|
+
simple: "cost-asc",
|
|
22
|
+
moderate: "balanced",
|
|
23
|
+
hard: "quality-desc",
|
|
24
|
+
expert: "quality-desc",
|
|
25
|
+
};
|
|
26
|
+
/** Neutral default for an unknown numeric metric (keeps ordering stable). */
|
|
27
|
+
const NEUTRAL = 0.5;
|
|
28
|
+
/**
|
|
29
|
+
* Cap on the per-(provider,model) metadata cache so it can't grow unbounded in
|
|
30
|
+
* long-lived processes with large or dynamic pools. FIFO eviction.
|
|
31
|
+
*/
|
|
32
|
+
const MAX_META_CACHE_ENTRIES = 1000;
|
|
33
|
+
export class ClassifierRouter {
|
|
34
|
+
config;
|
|
35
|
+
deps;
|
|
36
|
+
metaCache = new Map();
|
|
37
|
+
constructor(config, deps = {}) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
this.deps = deps;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Classify the request and produce a combined model + tool decision, or
|
|
43
|
+
* `null` when nothing should change. Never throws (fails open).
|
|
44
|
+
*/
|
|
45
|
+
async route(input) {
|
|
46
|
+
try {
|
|
47
|
+
// For the LLM strategy, hand the pool to the classifier so it can select
|
|
48
|
+
// a model directly — the generic path for custom/registry-less models.
|
|
49
|
+
const useLlm = this.config.classifier === "llm" && !!this.deps.generate;
|
|
50
|
+
const built = useLlm ? this.buildCandidates() : undefined;
|
|
51
|
+
const decision = await this.classify(input, built?.descriptors);
|
|
52
|
+
const ranked = this.selectModels(decision, built?.byId);
|
|
53
|
+
const primary = ranked[0];
|
|
54
|
+
const { toolFilter, excludeTools } = this.selectTools(decision);
|
|
55
|
+
if (!primary && !toolFilter && !excludeTools) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
provider: primary?.provider,
|
|
60
|
+
model: primary?.model,
|
|
61
|
+
region: primary?.region,
|
|
62
|
+
difficulty: decision.difficulty,
|
|
63
|
+
modelFallbacks: ranked.slice(1),
|
|
64
|
+
toolFilter,
|
|
65
|
+
excludeTools,
|
|
66
|
+
reason: decision.reason,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
this.deps.logger?.warn?.("[ClassifierRouter] route failed — no-op", {
|
|
71
|
+
error: err instanceof Error ? err.message : String(err),
|
|
72
|
+
});
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Run the configured strategy; LLM falls back to heuristic on failure. */
|
|
77
|
+
async classify(input, candidates) {
|
|
78
|
+
if (this.config.classifier === "llm" && this.deps.generate) {
|
|
79
|
+
try {
|
|
80
|
+
return await classifyLlm(input, this.deps.generate, this.config.classifierModel, this.config.timeoutMs, candidates);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
this.deps.logger?.warn?.("[ClassifierRouter] LLM classify failed — using heuristic", { error: err instanceof Error ? err.message : String(err) });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return classifyHeuristic(input);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build LLM-facing model descriptors + an id→member map so the classifier can
|
|
90
|
+
* pick a model directly. Works for ANY pool, registry-backed or not.
|
|
91
|
+
*/
|
|
92
|
+
buildCandidates() {
|
|
93
|
+
const pool = this.config.pool ?? [];
|
|
94
|
+
const byId = new Map();
|
|
95
|
+
const used = new Set();
|
|
96
|
+
const descriptors = [];
|
|
97
|
+
pool.forEach((m, i) => {
|
|
98
|
+
let id = m.id ?? (m.model ? `${m.provider}/${m.model}` : m.provider);
|
|
99
|
+
if (used.has(id)) {
|
|
100
|
+
id = `${id}#${i}`;
|
|
101
|
+
}
|
|
102
|
+
used.add(id);
|
|
103
|
+
byId.set(id, m);
|
|
104
|
+
descriptors.push({
|
|
105
|
+
id,
|
|
106
|
+
provider: m.provider,
|
|
107
|
+
model: m.model,
|
|
108
|
+
description: m.description,
|
|
109
|
+
tiers: m.tiers,
|
|
110
|
+
capabilities: this.metaFor(m).capabilities,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
return { descriptors, byId };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the ranked model list for a decision. Prefers the LLM's direct pick
|
|
117
|
+
* (`selectedModelId`); otherwise falls back to difficulty-based tierMap /
|
|
118
|
+
* metadata scoring.
|
|
119
|
+
*/
|
|
120
|
+
selectModels(decision, byId) {
|
|
121
|
+
if (decision.selectedModelId && byId) {
|
|
122
|
+
const picked = byId.get(decision.selectedModelId);
|
|
123
|
+
if (picked) {
|
|
124
|
+
const rest = (this.config.pool ?? []).filter((m) => m !== picked);
|
|
125
|
+
const rankedRest = this.rank(this.filterByCapabilities(rest, decision.requiredCapabilities), decision.difficulty);
|
|
126
|
+
return [picked, ...rankedRest];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const eligible = this.candidatesFor(decision);
|
|
130
|
+
const capable = this.filterByCapabilities(eligible, decision.requiredCapabilities);
|
|
131
|
+
return this.rank(capable, decision.difficulty);
|
|
132
|
+
}
|
|
133
|
+
/** Candidate members for a difficulty: explicit tierMap or eligible pool. */
|
|
134
|
+
candidatesFor(decision) {
|
|
135
|
+
const tierMembers = this.config.tierMap?.[decision.difficulty];
|
|
136
|
+
if (tierMembers && tierMembers.length > 0) {
|
|
137
|
+
return tierMembers;
|
|
138
|
+
}
|
|
139
|
+
const pool = this.config.pool ?? [];
|
|
140
|
+
const eligible = pool.filter((m) => !m.tiers || m.tiers.includes(decision.difficulty));
|
|
141
|
+
return eligible.length > 0 ? eligible : pool;
|
|
142
|
+
}
|
|
143
|
+
/** Drop members that cannot satisfy the required capabilities (lenient). */
|
|
144
|
+
filterByCapabilities(members, required) {
|
|
145
|
+
if (!required || required.length === 0) {
|
|
146
|
+
return members;
|
|
147
|
+
}
|
|
148
|
+
const kept = members.filter((m) => {
|
|
149
|
+
const caps = this.metaFor(m).capabilities;
|
|
150
|
+
// Unknown capabilities → keep (don't starve the pool on missing metadata).
|
|
151
|
+
if (!caps || caps.length === 0) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return required.every((c) => caps.includes(c));
|
|
155
|
+
});
|
|
156
|
+
return kept.length > 0 ? kept : members;
|
|
157
|
+
}
|
|
158
|
+
/** Rank candidates best-first for the difficulty's strategy. */
|
|
159
|
+
rank(members, difficulty) {
|
|
160
|
+
const mode = DIFFICULTY_RANK_MODE[difficulty];
|
|
161
|
+
const originalIndex = new Map(members.map((m, i) => [m, i]));
|
|
162
|
+
const num = (v) => (typeof v === "number" ? v : NEUTRAL);
|
|
163
|
+
return [...members].sort((a, b) => {
|
|
164
|
+
const ma = this.metaFor(a);
|
|
165
|
+
const mb = this.metaFor(b);
|
|
166
|
+
let delta;
|
|
167
|
+
if (mode === "cost-asc") {
|
|
168
|
+
delta = num(ma.cost) - num(mb.cost);
|
|
169
|
+
}
|
|
170
|
+
else if (mode === "quality-desc") {
|
|
171
|
+
delta = num(mb.quality) - num(ma.quality);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
// balanced: maximize quality-minus-cost
|
|
175
|
+
delta =
|
|
176
|
+
num(mb.quality) - num(mb.cost) - (num(ma.quality) - num(ma.cost));
|
|
177
|
+
}
|
|
178
|
+
if (delta !== 0) {
|
|
179
|
+
return delta;
|
|
180
|
+
}
|
|
181
|
+
const weightDelta = (b.weight ?? 1) - (a.weight ?? 1);
|
|
182
|
+
if (weightDelta !== 0) {
|
|
183
|
+
return weightDelta;
|
|
184
|
+
}
|
|
185
|
+
// Stable: preserve declared pool order on a tie.
|
|
186
|
+
return (originalIndex.get(a) ?? 0) - (originalIndex.get(b) ?? 0);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/** Tool narrowing: per-difficulty directive, then classifier hints. */
|
|
190
|
+
selectTools(decision) {
|
|
191
|
+
const directive = this.config.toolDirectives?.[decision.difficulty];
|
|
192
|
+
let toolFilter = directive?.toolFilter
|
|
193
|
+
? [...directive.toolFilter]
|
|
194
|
+
: undefined;
|
|
195
|
+
const excludeTools = directive?.excludeTools
|
|
196
|
+
? [...directive.excludeTools]
|
|
197
|
+
: undefined;
|
|
198
|
+
// When no explicit allowlist is configured, honor the classifier's hint.
|
|
199
|
+
if ((!toolFilter || toolFilter.length === 0) &&
|
|
200
|
+
decision.suggestedTools &&
|
|
201
|
+
decision.suggestedTools.length > 0) {
|
|
202
|
+
toolFilter = [...decision.suggestedTools];
|
|
203
|
+
}
|
|
204
|
+
return { toolFilter, excludeTools };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Resolve cost/quality/capabilities for a member. Declared values win;
|
|
208
|
+
* gaps are filled from the model registry (by model name/alias) when known.
|
|
209
|
+
* Results are cached per (provider/model) for the router's lifetime.
|
|
210
|
+
*/
|
|
211
|
+
metaFor(member) {
|
|
212
|
+
const key = `${member.provider}::${member.model ?? ""}`;
|
|
213
|
+
const cached = this.metaCache.get(key);
|
|
214
|
+
if (cached) {
|
|
215
|
+
return cached;
|
|
216
|
+
}
|
|
217
|
+
let cost = member.cost;
|
|
218
|
+
let quality = member.quality;
|
|
219
|
+
let capabilities = member.capabilities
|
|
220
|
+
? [...member.capabilities]
|
|
221
|
+
: undefined;
|
|
222
|
+
const needsEnrichment = cost === undefined || quality === undefined || capabilities === undefined;
|
|
223
|
+
if (needsEnrichment && member.model) {
|
|
224
|
+
try {
|
|
225
|
+
const info = ModelResolver.resolveModel(member.model);
|
|
226
|
+
if (info) {
|
|
227
|
+
if (cost === undefined) {
|
|
228
|
+
cost = info.pricing.inputCostPer1K + info.pricing.outputCostPer1K;
|
|
229
|
+
}
|
|
230
|
+
if (quality === undefined) {
|
|
231
|
+
quality = QUALITY_SCORE[info.performance.quality] ?? NEUTRAL;
|
|
232
|
+
}
|
|
233
|
+
if (capabilities === undefined) {
|
|
234
|
+
const caps = [];
|
|
235
|
+
if (info.capabilities.vision) {
|
|
236
|
+
caps.push("vision");
|
|
237
|
+
}
|
|
238
|
+
if (info.capabilities.functionCalling) {
|
|
239
|
+
caps.push("tools");
|
|
240
|
+
}
|
|
241
|
+
if (info.capabilities.reasoning) {
|
|
242
|
+
caps.push("reasoning");
|
|
243
|
+
}
|
|
244
|
+
if (info.capabilities.codeGeneration) {
|
|
245
|
+
caps.push("code");
|
|
246
|
+
}
|
|
247
|
+
if (info.capabilities.multimodal) {
|
|
248
|
+
caps.push("multimodal");
|
|
249
|
+
}
|
|
250
|
+
capabilities = caps;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// Enrichment is best-effort; ignore registry lookup failures.
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const meta = { cost, quality, capabilities };
|
|
259
|
+
if (this.metaCache.size >= MAX_META_CACHE_ENTRIES) {
|
|
260
|
+
const oldest = this.metaCache.keys().next().value;
|
|
261
|
+
if (oldest !== undefined) {
|
|
262
|
+
this.metaCache.delete(oldest);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
this.metaCache.set(key, meta);
|
|
266
|
+
return meta;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classification strategies for the ClassifierRouter.
|
|
3
|
+
*
|
|
4
|
+
* - `classifyHeuristic` — zero-cost, no LLM. Generalizes the existing binary
|
|
5
|
+
* task classifier scorer (fast vs reasoning) into five difficulty tiers.
|
|
6
|
+
* - `classifyLlm` — runs a cheap "classifier model" via an injected generate
|
|
7
|
+
* function, asking for a schema-constrained difficulty verdict. Falls back to
|
|
8
|
+
* the heuristic on any failure.
|
|
9
|
+
*/
|
|
10
|
+
import type { ClassifierCandidate, ClassifierDecision, ClassifierDifficulty, ClassifierGenerateFn, ClassifierModelRef, ClassifierRouterInput } from "../types/index.js";
|
|
11
|
+
/** Difficulty tiers, ordered easiest → hardest. */
|
|
12
|
+
export declare const CLASSIFIER_DIFFICULTIES: ClassifierDifficulty[];
|
|
13
|
+
/**
|
|
14
|
+
* Heuristic classifier — maps the binary fast/reasoning scores plus prompt
|
|
15
|
+
* length into one of five difficulty tiers. Deterministic and dependency-free.
|
|
16
|
+
*/
|
|
17
|
+
export declare function classifyHeuristic(input: ClassifierRouterInput): ClassifierDecision;
|
|
18
|
+
/**
|
|
19
|
+
* LLM classifier — asks a cheap model for a schema-constrained verdict.
|
|
20
|
+
* Falls back to the heuristic if the model is unavailable, times out, or
|
|
21
|
+
* returns output that does not match the schema.
|
|
22
|
+
*/
|
|
23
|
+
export declare function classifyLlm(input: ClassifierRouterInput, generate: ClassifierGenerateFn, classifierModel?: ClassifierModelRef, timeoutMs?: number, candidates?: ClassifierCandidate[]): Promise<ClassifierDecision>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classification strategies for the ClassifierRouter.
|
|
3
|
+
*
|
|
4
|
+
* - `classifyHeuristic` — zero-cost, no LLM. Generalizes the existing binary
|
|
5
|
+
* task classifier scorer (fast vs reasoning) into five difficulty tiers.
|
|
6
|
+
* - `classifyLlm` — runs a cheap "classifier model" via an injected generate
|
|
7
|
+
* function, asking for a schema-constrained difficulty verdict. Falls back to
|
|
8
|
+
* the heuristic on any failure.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { analyzePrompt, calculateConfidence, } from "../utils/taskClassificationUtils.js";
|
|
12
|
+
import { withTimeout } from "../utils/async/index.js";
|
|
13
|
+
/** Difficulty tiers, ordered easiest → hardest. */
|
|
14
|
+
export const CLASSIFIER_DIFFICULTIES = [
|
|
15
|
+
"trivial",
|
|
16
|
+
"simple",
|
|
17
|
+
"moderate",
|
|
18
|
+
"hard",
|
|
19
|
+
"expert",
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Add capability tags implied by the request shape (vision/tools) to a set the
|
|
23
|
+
* classifier already produced.
|
|
24
|
+
*/
|
|
25
|
+
function withRequestCapabilities(input, base) {
|
|
26
|
+
const caps = new Set(base ?? []);
|
|
27
|
+
if (input.requiresVision) {
|
|
28
|
+
caps.add("vision");
|
|
29
|
+
}
|
|
30
|
+
if (input.hasTools) {
|
|
31
|
+
caps.add("tools");
|
|
32
|
+
}
|
|
33
|
+
return caps.size > 0 ? Array.from(caps) : undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Heuristic classifier — maps the binary fast/reasoning scores plus prompt
|
|
37
|
+
* length into one of five difficulty tiers. Deterministic and dependency-free.
|
|
38
|
+
*/
|
|
39
|
+
export function classifyHeuristic(input) {
|
|
40
|
+
const prompt = input.prompt ?? "";
|
|
41
|
+
const { fastScore, reasoningScore, reasons } = analyzePrompt(prompt);
|
|
42
|
+
const net = reasoningScore - fastScore;
|
|
43
|
+
const len = prompt.trim().length;
|
|
44
|
+
let difficulty;
|
|
45
|
+
if (fastScore === 0 && reasoningScore === 0) {
|
|
46
|
+
// No signal — fall back to length as a weak proxy.
|
|
47
|
+
difficulty = len < 80 ? "simple" : len < 400 ? "moderate" : "hard";
|
|
48
|
+
}
|
|
49
|
+
else if (net <= -2) {
|
|
50
|
+
difficulty = "trivial";
|
|
51
|
+
}
|
|
52
|
+
else if (net <= 0) {
|
|
53
|
+
difficulty = "simple";
|
|
54
|
+
}
|
|
55
|
+
else if (net <= 3) {
|
|
56
|
+
difficulty = "moderate";
|
|
57
|
+
}
|
|
58
|
+
else if (net <= 7) {
|
|
59
|
+
difficulty = "hard";
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
difficulty = "expert";
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
difficulty,
|
|
66
|
+
confidence: calculateConfidence(fastScore, reasoningScore),
|
|
67
|
+
requiredCapabilities: withRequestCapabilities(input),
|
|
68
|
+
reason: `heuristic: net=${net}, len=${len}${reasons.length ? ` (${reasons.slice(0, 3).join("; ")})` : ""}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Schema the LLM classifier is forced to answer with. */
|
|
72
|
+
const classifierOutputSchema = z.object({
|
|
73
|
+
difficulty: z.enum(["trivial", "simple", "moderate", "hard", "expert"]),
|
|
74
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
75
|
+
requiredCapabilities: z.array(z.string()).optional(),
|
|
76
|
+
suggestedTools: z.array(z.string()).optional(),
|
|
77
|
+
selectedModelId: z.string().optional(),
|
|
78
|
+
reason: z.string().optional(),
|
|
79
|
+
});
|
|
80
|
+
const CLASSIFIER_SYSTEM_PROMPT = [
|
|
81
|
+
"You are a routing classifier inside an AI gateway.",
|
|
82
|
+
"Classify the user's task by difficulty into EXACTLY one of:",
|
|
83
|
+
"trivial, simple, moderate, hard, expert.",
|
|
84
|
+
"Judge by reasoning depth, number of steps, domain expertise required, and ambiguity.",
|
|
85
|
+
"Greetings/lookups/one-liners are trivial/simple; multi-step analysis, design,",
|
|
86
|
+
"or expert-domain work is hard/expert.",
|
|
87
|
+
'Also list required model capabilities (e.g. "vision", "tools", "reasoning")',
|
|
88
|
+
"and, only if obvious, the names of tools the task needs.",
|
|
89
|
+
"If a list of available models is provided, also set selectedModelId to the",
|
|
90
|
+
"single best model id for this task.",
|
|
91
|
+
"Respond ONLY via the structured schema.",
|
|
92
|
+
].join(" ");
|
|
93
|
+
/**
|
|
94
|
+
* LLM classifier — asks a cheap model for a schema-constrained verdict.
|
|
95
|
+
* Falls back to the heuristic if the model is unavailable, times out, or
|
|
96
|
+
* returns output that does not match the schema.
|
|
97
|
+
*/
|
|
98
|
+
export async function classifyLlm(input, generate, classifierModel, timeoutMs, candidates) {
|
|
99
|
+
const lines = [
|
|
100
|
+
"Task to classify:",
|
|
101
|
+
'"""',
|
|
102
|
+
(input.prompt ?? "").slice(0, 4000),
|
|
103
|
+
'"""',
|
|
104
|
+
`hasTools=${!!input.hasTools} requiresVision=${!!input.requiresVision}`,
|
|
105
|
+
];
|
|
106
|
+
if (candidates && candidates.length > 0) {
|
|
107
|
+
lines.push("", "Available models — pick the single best `id` for THIS task:");
|
|
108
|
+
for (const c of candidates) {
|
|
109
|
+
const bits = [c.provider + (c.model ? `/${c.model}` : "")];
|
|
110
|
+
if (c.description) {
|
|
111
|
+
bits.push(c.description);
|
|
112
|
+
}
|
|
113
|
+
if (c.tiers && c.tiers.length > 0) {
|
|
114
|
+
bits.push(`tiers: ${c.tiers.join("/")}`);
|
|
115
|
+
}
|
|
116
|
+
if (c.capabilities && c.capabilities.length > 0) {
|
|
117
|
+
bits.push(`caps: ${c.capabilities.join(",")}`);
|
|
118
|
+
}
|
|
119
|
+
lines.push(`- id="${c.id}": ${bits.join(" — ")}`);
|
|
120
|
+
}
|
|
121
|
+
lines.push("", "Set selectedModelId to the chosen id (omit if unsure).");
|
|
122
|
+
}
|
|
123
|
+
// `timeout` lets the provider abort its own request; withTimeout adds a hard
|
|
124
|
+
// wall-clock ceiling so a stalled classifier call can never block the turn.
|
|
125
|
+
// A TimeoutError propagates to ClassifierRouter.classify(), which falls back
|
|
126
|
+
// to the heuristic (fail-open).
|
|
127
|
+
const hardTimeoutMs = timeoutMs ?? 8000;
|
|
128
|
+
const result = await withTimeout(generate({
|
|
129
|
+
input: { text: lines.join("\n") },
|
|
130
|
+
systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
|
|
131
|
+
provider: classifierModel?.provider,
|
|
132
|
+
model: classifierModel?.model,
|
|
133
|
+
region: classifierModel?.region,
|
|
134
|
+
temperature: classifierModel?.temperature ?? 0,
|
|
135
|
+
disableTools: true,
|
|
136
|
+
schema: classifierOutputSchema,
|
|
137
|
+
timeout: hardTimeoutMs,
|
|
138
|
+
// Marker consumed by NeuroLink.applyClassifierRouting to prevent the
|
|
139
|
+
// classifier's own generate() call from recursively re-routing.
|
|
140
|
+
context: { __classifierRouted: true },
|
|
141
|
+
}), hardTimeoutMs, `Classifier LLM call exceeded ${hardTimeoutMs}ms`);
|
|
142
|
+
const parsed = classifierOutputSchema.safeParse(result?.structuredData);
|
|
143
|
+
if (!parsed.success) {
|
|
144
|
+
return classifyHeuristic(input);
|
|
145
|
+
}
|
|
146
|
+
const d = parsed.data;
|
|
147
|
+
return {
|
|
148
|
+
difficulty: d.difficulty,
|
|
149
|
+
confidence: d.confidence ?? 0.7,
|
|
150
|
+
requiredCapabilities: withRequestCapabilities(input, d.requiredCapabilities),
|
|
151
|
+
suggestedTools: d.suggestedTools,
|
|
152
|
+
selectedModelId: d.selectedModelId,
|
|
153
|
+
reason: d.reason ?? "llm classifier",
|
|
154
|
+
};
|
|
155
|
+
}
|
package/dist/routing/index.d.ts
CHANGED
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { classifyProviderError, ModelPool } from "./modelPool.js";
|
|
7
7
|
export { createDefaultRequestRouter } from "./requestRouter.js";
|
|
8
|
+
export { ClassifierRouter } from "./classifierRouter.js";
|
|
9
|
+
export { classifyHeuristic, classifyLlm } from "./classifierStrategies.js";
|
package/dist/routing/index.js
CHANGED
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { classifyProviderError, ModelPool } from "./modelPool.js";
|
|
7
7
|
export { createDefaultRequestRouter } from "./requestRouter.js";
|
|
8
|
+
export { ClassifierRouter } from "./classifierRouter.js";
|
|
9
|
+
export { classifyHeuristic, classifyLlm } from "./classifierStrategies.js";
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { NeuroLink } from "../neurolink.js";
|
|
2
|
-
import type { ConversationMemoryConfig, LoopSessionState, SessionVariableValue, ToolRoutingConfig } from "../types/index.js";
|
|
2
|
+
import type { ClassifierRouterConfig, ConversationMemoryConfig, LoopSessionState, SessionVariableValue, ToolRoutingConfig } from "../types/index.js";
|
|
3
3
|
export declare class GlobalSessionManager {
|
|
4
4
|
private static instance;
|
|
5
5
|
private loopSession;
|
|
6
6
|
/** Optional tool-routing config set by CLI handlers before SDK construction. */
|
|
7
7
|
private _toolRoutingConfig;
|
|
8
|
+
/** Optional classifier-router config set by CLI handlers before SDK construction. */
|
|
9
|
+
private _classifierRouterConfig;
|
|
8
10
|
static getInstance(): GlobalSessionManager;
|
|
9
11
|
setLoopSession(config?: ConversationMemoryConfig): string;
|
|
10
12
|
/**
|
|
@@ -42,6 +44,13 @@ export declare class GlobalSessionManager {
|
|
|
42
44
|
* already exists).
|
|
43
45
|
*/
|
|
44
46
|
setToolRoutingConfig(config: ToolRoutingConfig): void;
|
|
47
|
+
/**
|
|
48
|
+
* Store a classifier-router config to be injected at SDK construction time.
|
|
49
|
+
* Call this BEFORE `getOrCreateNeuroLink()` inside a command handler.
|
|
50
|
+
* When a loop session is already active the config is ignored (the instance
|
|
51
|
+
* already exists).
|
|
52
|
+
*/
|
|
53
|
+
setClassifierRouterConfig(config: ClassifierRouterConfig): void;
|
|
45
54
|
getOrCreateNeuroLink(): NeuroLink;
|
|
46
55
|
getCurrentSessionId(): string | undefined;
|
|
47
56
|
setSessionVariable(key: string, value: SessionVariableValue): void;
|
|
@@ -33,6 +33,8 @@ export class GlobalSessionManager {
|
|
|
33
33
|
loopSession = null;
|
|
34
34
|
/** Optional tool-routing config set by CLI handlers before SDK construction. */
|
|
35
35
|
_toolRoutingConfig = undefined;
|
|
36
|
+
/** Optional classifier-router config set by CLI handlers before SDK construction. */
|
|
37
|
+
_classifierRouterConfig = undefined;
|
|
36
38
|
static getInstance() {
|
|
37
39
|
if (!GlobalSessionManager.instance) {
|
|
38
40
|
GlobalSessionManager.instance = new GlobalSessionManager();
|
|
@@ -141,6 +143,18 @@ export class GlobalSessionManager {
|
|
|
141
143
|
}
|
|
142
144
|
this._toolRoutingConfig = config;
|
|
143
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Store a classifier-router config to be injected at SDK construction time.
|
|
148
|
+
* Call this BEFORE `getOrCreateNeuroLink()` inside a command handler.
|
|
149
|
+
* When a loop session is already active the config is ignored (the instance
|
|
150
|
+
* already exists).
|
|
151
|
+
*/
|
|
152
|
+
setClassifierRouterConfig(config) {
|
|
153
|
+
if (this.hasActiveSession()) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this._classifierRouterConfig = config;
|
|
157
|
+
}
|
|
144
158
|
getOrCreateNeuroLink() {
|
|
145
159
|
const session = this.getLoopSession();
|
|
146
160
|
if (session) {
|
|
@@ -160,6 +174,10 @@ export class GlobalSessionManager {
|
|
|
160
174
|
options.toolRouting = this._toolRoutingConfig;
|
|
161
175
|
this._toolRoutingConfig = undefined;
|
|
162
176
|
}
|
|
177
|
+
if (this._classifierRouterConfig) {
|
|
178
|
+
options.classifierRouter = this._classifierRouterConfig;
|
|
179
|
+
this._classifierRouterConfig = undefined;
|
|
180
|
+
}
|
|
163
181
|
return new NeuroLink(Object.keys(options).length ? options : undefined);
|
|
164
182
|
}
|
|
165
183
|
getCurrentSessionId() {
|