@juspay/neurolink 9.79.3 → 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.
Files changed (42) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/browser/neurolink.min.js +349 -348
  3. package/dist/cli/factories/commandFactory.js +61 -0
  4. package/dist/cli/utils/classifierRouterFlags.d.ts +19 -0
  5. package/dist/cli/utils/classifierRouterFlags.js +111 -0
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/lib/index.d.ts +1 -1
  9. package/dist/lib/index.js +1 -1
  10. package/dist/lib/neurolink.d.ts +14 -0
  11. package/dist/lib/neurolink.js +131 -0
  12. package/dist/lib/routing/classifierRouter.d.ts +52 -0
  13. package/dist/lib/routing/classifierRouter.js +269 -0
  14. package/dist/lib/routing/classifierStrategies.d.ts +23 -0
  15. package/dist/lib/routing/classifierStrategies.js +156 -0
  16. package/dist/lib/routing/index.d.ts +2 -0
  17. package/dist/lib/routing/index.js +2 -0
  18. package/dist/lib/session/globalSessionState.d.ts +10 -1
  19. package/dist/lib/session/globalSessionState.js +18 -0
  20. package/dist/lib/types/classifierRouter.d.ts +193 -0
  21. package/dist/lib/types/classifierRouter.js +17 -0
  22. package/dist/lib/types/cli.d.ts +27 -3
  23. package/dist/lib/types/config.d.ts +10 -0
  24. package/dist/lib/types/index.d.ts +1 -0
  25. package/dist/lib/types/index.js +2 -0
  26. package/dist/neurolink.d.ts +14 -0
  27. package/dist/neurolink.js +131 -0
  28. package/dist/routing/classifierRouter.d.ts +52 -0
  29. package/dist/routing/classifierRouter.js +268 -0
  30. package/dist/routing/classifierStrategies.d.ts +23 -0
  31. package/dist/routing/classifierStrategies.js +155 -0
  32. package/dist/routing/index.d.ts +2 -0
  33. package/dist/routing/index.js +2 -0
  34. package/dist/session/globalSessionState.d.ts +10 -1
  35. package/dist/session/globalSessionState.js +18 -0
  36. package/dist/types/classifierRouter.d.ts +193 -0
  37. package/dist/types/classifierRouter.js +16 -0
  38. package/dist/types/cli.d.ts +27 -3
  39. package/dist/types/config.d.ts +10 -0
  40. package/dist/types/index.d.ts +1 -0
  41. package/dist/types/index.js +2 -0
  42. package/package.json +2 -1
@@ -0,0 +1,193 @@
1
+ /**
2
+ * ClassifierRouter types — generic "classify → pick model + tools → run".
3
+ *
4
+ * A ClassifierRouter inspects an incoming request, classifies it by difficulty
5
+ * (and optional required capabilities / suggested tools), then selects a
6
+ * provider/model from a host-declared "available base" pool — routing harder
7
+ * tasks to more capable models and easier tasks to cheaper/faster ones — and
8
+ * optionally narrows the tool set for that request.
9
+ *
10
+ * It is entirely opt-in (constructor config, `enabled: false` by default) and
11
+ * fails open: any classifier or selection error leaves the call unrouted.
12
+ *
13
+ * Type names are domain-prefixed `Classifier*` to stay globally unique across
14
+ * `src/lib/types/` (see CLAUDE.md rule 9).
15
+ */
16
+ /** Coarse difficulty buckets the classifier maps a request into. */
17
+ export type ClassifierDifficulty = "trivial" | "simple" | "moderate" | "hard" | "expert";
18
+ /** Which classification strategy to run. */
19
+ export type ClassifierStrategyKind = "heuristic" | "llm";
20
+ /**
21
+ * The classifier's verdict for a single request. Strategy-agnostic: produced
22
+ * by both the heuristic and the LLM classifier.
23
+ */
24
+ export type ClassifierDecision = {
25
+ /** The classified difficulty bucket. */
26
+ difficulty: ClassifierDifficulty;
27
+ /** Confidence in the classification (0–1). */
28
+ confidence: number;
29
+ /** Capability tags the request needs (e.g. "vision", "tools", "reasoning"). */
30
+ requiredCapabilities?: string[];
31
+ /** Tool names the classifier thinks the task needs (allowlist hint). */
32
+ suggestedTools?: string[];
33
+ /**
34
+ * When the LLM classifier picks a model directly, the chosen candidate id
35
+ * (matches a `ClassifierCandidate.id`). Ignored by the heuristic classifier.
36
+ */
37
+ selectedModelId?: string;
38
+ /** Human-readable explanation, emitted at debug level. */
39
+ reason?: string;
40
+ };
41
+ /**
42
+ * Lightweight model descriptor handed to the LLM classifier so it can select a
43
+ * model directly from the pool by `id` — the generic path for custom models.
44
+ */
45
+ export type ClassifierCandidate = {
46
+ id: string;
47
+ provider: string;
48
+ model?: string;
49
+ description?: string;
50
+ tiers?: ClassifierDifficulty[];
51
+ capabilities?: string[];
52
+ };
53
+ /**
54
+ * One candidate (provider, model, region) in the available base pool, with
55
+ * optional routing metadata. When `cost`/`quality`/`capabilities` are omitted,
56
+ * the router enriches them from the model registry (by `model` name/alias).
57
+ */
58
+ export type ClassifierRouterPoolMember = {
59
+ provider: string;
60
+ model?: string;
61
+ region?: string;
62
+ /**
63
+ * Stable id the LLM classifier references when selecting a model directly.
64
+ * Defaults to `${provider}/${model}` (or just `provider`) when omitted.
65
+ */
66
+ id?: string;
67
+ /**
68
+ * Plain-English description of when to use this model (e.g. "cheap & fast,
69
+ * for simple Q&A" / "powerful reasoning model for complex analysis"). Drives
70
+ * LLM-based model selection — the only metadata needed for custom models that
71
+ * are NOT in the registry (LiteLLM, OpenAI-compatible, self-hosted, …).
72
+ */
73
+ description?: string;
74
+ /** Difficulty tiers this member is eligible for. Omit = eligible for all. */
75
+ tiers?: ClassifierDifficulty[];
76
+ /** Relative cost (lower = cheaper). Preferred for easy tiers. */
77
+ cost?: number;
78
+ /** Relative quality/capability (higher = more capable). Preferred for hard tiers. */
79
+ quality?: number;
80
+ /** Capability tags this member supports (e.g. "vision", "tools"). */
81
+ capabilities?: string[];
82
+ /** Tiebreak weight when scores are equal. Default: 1. */
83
+ weight?: number;
84
+ };
85
+ /** Per-difficulty tool policy applied to the request. */
86
+ export type ClassifierToolDirective = {
87
+ /** Allowlist of tool names to keep (maps to `options.toolFilter`). */
88
+ toolFilter?: string[];
89
+ /** Denylist of tool names to drop (appended to `options.excludeTools`). */
90
+ excludeTools?: string[];
91
+ };
92
+ /** Provider/model the LLM classifier strategy itself runs on. */
93
+ export type ClassifierModelRef = {
94
+ provider?: string;
95
+ model?: string;
96
+ region?: string;
97
+ temperature?: number;
98
+ };
99
+ /** Constructor-level configuration for the classifier router. */
100
+ export type ClassifierRouterConfig = {
101
+ /** Master switch. When false/absent, the router is never built. */
102
+ enabled: boolean;
103
+ /**
104
+ * Classification strategy. Default: "heuristic" (no LLM, zero added latency).
105
+ * "llm" runs a cheap classifier model (see `classifierModel`).
106
+ */
107
+ classifier?: ClassifierStrategyKind;
108
+ /** Model used by the "llm" strategy. Defaults to provider/model auto. */
109
+ classifierModel?: ClassifierModelRef;
110
+ /** The available base pool the router selects a model from. */
111
+ pool: ClassifierRouterPoolMember[];
112
+ /**
113
+ * Explicit difficulty → members map. When a difficulty has entries here they
114
+ * take precedence over metadata scoring of `pool`.
115
+ */
116
+ tierMap?: Partial<Record<ClassifierDifficulty, ClassifierRouterPoolMember[]>>;
117
+ /** Per-difficulty tool directives applied to the request. */
118
+ toolDirectives?: Partial<Record<ClassifierDifficulty, ClassifierToolDirective>>;
119
+ /** Hard timeout (ms) for the LLM classifier call. Default: 8000. */
120
+ timeoutMs?: number;
121
+ };
122
+ /**
123
+ * The router's combined decision: a provider/model/region override plus an
124
+ * optional tool narrowing. Any undefined field means "keep what the caller
125
+ * already configured". Returning `null` from the router is a valid no-op.
126
+ */
127
+ export type ClassifierRouterDecision = {
128
+ provider?: string;
129
+ model?: string;
130
+ region?: string;
131
+ /** Allowlist applied to `options.toolFilter`. */
132
+ toolFilter?: string[];
133
+ /** Denylist appended to `options.excludeTools`. */
134
+ excludeTools?: string[];
135
+ /** The difficulty this decision was made for (debug/telemetry). */
136
+ difficulty?: ClassifierDifficulty;
137
+ /** Remaining ranked candidates, best-first, for downstream failover. */
138
+ modelFallbacks?: ClassifierRouterPoolMember[];
139
+ /** Human-readable explanation, emitted at debug level. */
140
+ reason?: string;
141
+ };
142
+ /** Lightweight request snapshot handed to the router. */
143
+ export type ClassifierRouterInput = {
144
+ prompt: string;
145
+ estimatedInputTokens?: number;
146
+ hasTools?: boolean;
147
+ requiresVision?: boolean;
148
+ thinkingLevel?: string;
149
+ sessionId?: string;
150
+ };
151
+ /** Enriched per-model metadata used while ranking pool members. */
152
+ export type ClassifierModelMeta = {
153
+ cost?: number;
154
+ quality?: number;
155
+ capabilities?: string[];
156
+ };
157
+ /** Minimal options accepted by the injected LLM-classifier `generate` fn. */
158
+ export type ClassifierGenerateOptions = {
159
+ input: {
160
+ text: string;
161
+ };
162
+ systemPrompt?: string;
163
+ provider?: string;
164
+ model?: string;
165
+ region?: string;
166
+ temperature?: number;
167
+ maxTokens?: number;
168
+ disableTools?: boolean;
169
+ schema?: unknown;
170
+ timeout?: number | string;
171
+ context?: Record<string, unknown>;
172
+ };
173
+ /** Minimal result shape the LLM classifier reads back. */
174
+ export type ClassifierGenerateResult = {
175
+ content?: string;
176
+ structuredData?: unknown;
177
+ };
178
+ /** Injected LLM caller — typically a bound `NeuroLink.generate`. */
179
+ export type ClassifierGenerateFn = (options: ClassifierGenerateOptions) => Promise<ClassifierGenerateResult>;
180
+ /** Minimal logger surface the router uses (debug/warn). */
181
+ export type ClassifierLogger = {
182
+ debug: (message: string, meta?: unknown) => void;
183
+ warn: (message: string, meta?: unknown) => void;
184
+ };
185
+ /**
186
+ * Injected dependencies — keep `ClassifierRouter` provider-import-free and
187
+ * unit-testable (mirrors the `toolRouting` generateFn-injection pattern).
188
+ */
189
+ export type ClassifierRouterDeps = {
190
+ /** LLM caller for the "llm" strategy. Omit to disable LLM classification. */
191
+ generate?: ClassifierGenerateFn;
192
+ logger?: ClassifierLogger;
193
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * ClassifierRouter types — generic "classify → pick model + tools → run".
3
+ *
4
+ * A ClassifierRouter inspects an incoming request, classifies it by difficulty
5
+ * (and optional required capabilities / suggested tools), then selects a
6
+ * provider/model from a host-declared "available base" pool — routing harder
7
+ * tasks to more capable models and easier tasks to cheaper/faster ones — and
8
+ * optionally narrows the tool set for that request.
9
+ *
10
+ * It is entirely opt-in (constructor config, `enabled: false` by default) and
11
+ * fails open: any classifier or selection error leaves the call unrouted.
12
+ *
13
+ * Type names are domain-prefixed `Classifier*` to stay globally unique across
14
+ * `src/lib/types/` (see CLAUDE.md rule 9).
15
+ */
16
+ export {};
17
+ //# sourceMappingURL=classifierRouter.js.map
@@ -45,7 +45,7 @@ export type BaseCommandArgs = {
45
45
  /**
46
46
  * Generate command arguments
47
47
  */
48
- export type GenerateCommandArgs = BaseCommandArgs & CliToolRoutingFlags & {
48
+ export type GenerateCommandArgs = BaseCommandArgs & CliToolRoutingFlags & CliClassifierRouterFlags & {
49
49
  /** Input text or prompt */
50
50
  input?: string;
51
51
  /** AI provider to use */
@@ -110,7 +110,7 @@ export type GenerateCommandArgs = BaseCommandArgs & CliToolRoutingFlags & {
110
110
  /**
111
111
  * Stream command arguments
112
112
  */
113
- export type StreamCommandArgs = BaseCommandArgs & CliToolRoutingFlags & {
113
+ export type StreamCommandArgs = BaseCommandArgs & CliToolRoutingFlags & CliClassifierRouterFlags & {
114
114
  /** Input text or prompt */
115
115
  input?: string;
116
116
  /** AI provider to use */
@@ -137,7 +137,7 @@ export type StreamCommandArgs = BaseCommandArgs & CliToolRoutingFlags & {
137
137
  /**
138
138
  * Batch command arguments
139
139
  */
140
- export type BatchCommandArgs = BaseCommandArgs & CliToolRoutingFlags & {
140
+ export type BatchCommandArgs = BaseCommandArgs & CliToolRoutingFlags & CliClassifierRouterFlags & {
141
141
  /** Input file path */
142
142
  file?: string;
143
143
  /** AI provider to use */
@@ -1526,3 +1526,27 @@ export type CliToolRoutingFlags = {
1526
1526
  */
1527
1527
  toolRoutingServers?: string;
1528
1528
  };
1529
+ /**
1530
+ * CLI flags for the classifier router (`--classifier-*`). Builds a
1531
+ * ClassifierRouterConfig that is injected at SDK construction time.
1532
+ */
1533
+ export type CliClassifierRouterFlags = {
1534
+ /** Master enable switch (--classifier-router). */
1535
+ classifierRouter?: boolean;
1536
+ /** Strategy: "heuristic" (default) or "llm" (--classifier-strategy). */
1537
+ classifierStrategy?: string;
1538
+ /** LLM-classifier provider override (--classifier-model-provider). */
1539
+ classifierModelProvider?: string;
1540
+ /** LLM-classifier model override (--classifier-model-name). */
1541
+ classifierModelName?: string;
1542
+ /** LLM-classifier region override (--classifier-model-region). */
1543
+ classifierModelRegion?: string;
1544
+ /**
1545
+ * Path to a JSON file OR inline JSON array of pool members
1546
+ * (--classifier-pool). Each entry: { provider, model?, region?, description?,
1547
+ * tiers?, cost?, quality?, capabilities?, id? }.
1548
+ */
1549
+ classifierPool?: string;
1550
+ /** LLM-classifier hard timeout in ms (--classifier-timeout). */
1551
+ classifierTimeout?: number;
1552
+ };
@@ -11,6 +11,7 @@ import type { AuthProvider, AuthProviderType, AuthProviderConfig, Auth0Config, C
11
11
  import type { NeurolinkCredentials } from "./providers.js";
12
12
  import type { ModelPoolConfig } from "./modelPool.js";
13
13
  import type { RequestRouter } from "./requestRouter.js";
14
+ import type { ClassifierRouterConfig } from "./classifierRouter.js";
14
15
  /**
15
16
  * Main NeuroLink configuration type
16
17
  */
@@ -104,6 +105,15 @@ export type NeurolinkConstructorConfig = {
104
105
  * error proceeds unrouted.
105
106
  */
106
107
  requestRouter?: RequestRouter;
108
+ /**
109
+ * Pre-call classifier router: classifies each request by difficulty and
110
+ * selects a provider/model from a configured "available base" pool — routing
111
+ * harder tasks to more capable models and easier tasks to cheaper/faster
112
+ * ones — and optionally narrows the tool set. Opt-in (`enabled: false` by
113
+ * default) and fails open. Skipped when a `modelPool` is configured or the
114
+ * caller pinned both `provider` and `model`. See {@link ClassifierRouterConfig}.
115
+ */
116
+ classifierRouter?: ClassifierRouterConfig;
107
117
  };
108
118
  /**
109
119
  * Configuration for MCP enhancement modules wired into generate()/stream() paths.
@@ -71,3 +71,4 @@ export * from "./replicate.js";
71
71
  export * from "./safeFetch.js";
72
72
  export * from "./modelPool.js";
73
73
  export * from "./requestRouter.js";
74
+ export * from "./classifierRouter.js";
@@ -80,4 +80,6 @@ export * from "./safeFetch.js";
80
80
  export * from "./modelPool.js";
81
81
  // RequestRouter — pluggable pre-call provider/model selection (M9.x+)
82
82
  export * from "./requestRouter.js";
83
+ // ClassifierRouter — classify → pick model + tools from a base pool (M9.x+)
84
+ export * from "./classifierRouter.js";
83
85
  //# sourceMappingURL=index.js.map
@@ -112,6 +112,7 @@ export declare class NeuroLink {
112
112
  private readonly fallbackConfig;
113
113
  private readonly modelPool;
114
114
  private readonly requestRouter;
115
+ private readonly classifierRouter;
115
116
  /**
116
117
  * Merge instance-level credentials with per-call credentials.
117
118
  *
@@ -634,6 +635,19 @@ export declare class NeuroLink {
634
635
  * @param thinkingLevel — optional thinking level string from the call options.
635
636
  */
636
637
  private applyRequestRouter;
638
+ /**
639
+ * Applies the host-configured `classifierRouter` to `options` in place.
640
+ *
641
+ * Classifies the request by difficulty and selects a provider/model from the
642
+ * configured base pool (cheaper/faster for easy tasks, more capable for hard
643
+ * ones), and optionally narrows the tool set via `toolFilter`/`excludeTools`.
644
+ *
645
+ * Skipped when: no router is configured; the inbound call is the classifier's
646
+ * own generate() (marked `__classifierRouted`); the caller pinned BOTH
647
+ * provider and model; or a ModelPool is configured (the pool owns selection).
648
+ * Fails open — any error leaves options unchanged.
649
+ */
650
+ private applyClassifierRouting;
637
651
  private prepareGenerateAugmentations;
638
652
  private buildGenerateTextOptions;
639
653
  private finalizeGenerateRequestResult;
package/dist/neurolink.js CHANGED
@@ -87,6 +87,7 @@ import { isNonNullObject } from "./utils/typeUtils.js";
87
87
  import { getWorkflow } from "./workflow/core/workflowRegistry.js";
88
88
  import { runWorkflow } from "./workflow/core/workflowRunner.js";
89
89
  import { ModelPool, classifyProviderError } from "./routing/index.js";
90
+ import { ClassifierRouter } from "./routing/classifierRouter.js";
90
91
  import { looksLikeModelAccessDenied as sharedLooksLikeModelAccessDenied, isNonRetryableProviderError as sharedIsNonRetryableProviderError, } from "./utils/providerErrorClassification.js";
91
92
  /**
92
93
  * NL-002: Classify MCP error messages into categories for AI disambiguation.
@@ -388,6 +389,9 @@ export class NeuroLink {
388
389
  // RequestRouter: pluggable pre-call provider/model selector.
389
390
  // Stored directly from config.requestRouter; null when not configured.
390
391
  requestRouter;
392
+ // ClassifierRouter: opt-in "classify → pick model + tools" pre-call router.
393
+ // Built from config.classifierRouter; null when not configured.
394
+ classifierRouter;
391
395
  /**
392
396
  * Merge instance-level credentials with per-call credentials.
393
397
  *
@@ -796,6 +800,17 @@ export class NeuroLink {
796
800
  this.modelPool = config?.modelPool ? new ModelPool(config.modelPool) : null;
797
801
  // RequestRouter: store the host-supplied function; null when not configured.
798
802
  this.requestRouter = config?.requestRouter ?? null;
803
+ // ClassifierRouter: opt-in. The LLM strategy reuses this instance's
804
+ // generate() (marked so it never recursively re-routes). Fails open.
805
+ this.classifierRouter = config?.classifierRouter?.enabled
806
+ ? new ClassifierRouter(config.classifierRouter, {
807
+ generate: (genOptions) => this.generate(genOptions),
808
+ logger: {
809
+ debug: (message, meta) => logger.debug(message, meta),
810
+ warn: (message, meta) => logger.warn(message, meta),
811
+ },
812
+ })
813
+ : null;
799
814
  logger.setEventEmitter(this.emitter);
800
815
  // Read tool cache duration from environment variables, with a default
801
816
  const cacheDurationEnv = process.env.NEUROLINK_TOOL_CACHE_DURATION;
@@ -3105,6 +3120,13 @@ Current user's request: ${currentInput}`;
3105
3120
  }
3106
3121
  async runStandardGenerateRequest(options, originalPrompt, generateSpan) {
3107
3122
  const startTime = Date.now();
3123
+ // Pre-call classifier router: opt-in. Classifies the request and selects a
3124
+ // provider/model from the configured base pool (and optionally narrows the
3125
+ // tool set) BEFORE orchestration/requestRouter, so it takes precedence.
3126
+ // Fails open.
3127
+ await this.applyClassifierRouting(options, options.input?.text ?? originalPrompt ?? "", !options.disableTools &&
3128
+ (!!(options.tools && Object.keys(options.tools).length > 0) ||
3129
+ this.getCustomTools().size > 0), !!(options.input?.images && options.input.images.length > 0), options.thinkingConfig?.thinkingLevel);
3108
3130
  await this.maybeApplyGenerateOrchestration(options);
3109
3131
  // Pre-call request router: opt-in, only when the caller did not set both
3110
3132
  // provider+model. Fails open (any router error leaves options unchanged).
@@ -3241,6 +3263,12 @@ Current user's request: ${currentInput}`;
3241
3263
  if (!this.requestRouter) {
3242
3264
  return;
3243
3265
  }
3266
+ // Stand down if the classifier router already selected a provider/model on
3267
+ // this turn — one explicit precedence order: classifierRouter wins.
3268
+ if (options.context
3269
+ ?.__classifierRouted) {
3270
+ return;
3271
+ }
3244
3272
  // Skip if the caller explicitly set both provider AND model — they know
3245
3273
  // what they want; the router should not override an intentional choice.
3246
3274
  if (options.provider && options.model) {
@@ -3307,6 +3335,103 @@ Current user's request: ${currentInput}`;
3307
3335
  });
3308
3336
  }
3309
3337
  }
3338
+ /**
3339
+ * Applies the host-configured `classifierRouter` to `options` in place.
3340
+ *
3341
+ * Classifies the request by difficulty and selects a provider/model from the
3342
+ * configured base pool (cheaper/faster for easy tasks, more capable for hard
3343
+ * ones), and optionally narrows the tool set via `toolFilter`/`excludeTools`.
3344
+ *
3345
+ * Skipped when: no router is configured; the inbound call is the classifier's
3346
+ * own generate() (marked `__classifierRouted`); the caller pinned BOTH
3347
+ * provider and model; or a ModelPool is configured (the pool owns selection).
3348
+ * Fails open — any error leaves options unchanged.
3349
+ */
3350
+ async applyClassifierRouting(options, promptText, hasTools, requiresVision, thinkingLevel) {
3351
+ if (!this.classifierRouter) {
3352
+ return;
3353
+ }
3354
+ const opt = options;
3355
+ // Prevent recursion: the LLM classifier itself calls generate() with this
3356
+ // marker set. Also respect any explicit prior routing decision.
3357
+ if (opt.context?.__classifierRouted) {
3358
+ return;
3359
+ }
3360
+ // Honor an intentional caller choice of BOTH provider and model.
3361
+ if (options.provider && options.model) {
3362
+ return;
3363
+ }
3364
+ // A ModelPool overrides provider/model per member unconditionally; let it
3365
+ // own selection (mirrors applyRequestRouter precedence).
3366
+ if (this.modelPool) {
3367
+ logger.debug("[NeuroLink] applyClassifierRouting: skipped — modelPool takes precedence");
3368
+ return;
3369
+ }
3370
+ const sessionId = typeof opt.context?.sessionId === "string"
3371
+ ? opt.context.sessionId
3372
+ : undefined;
3373
+ try {
3374
+ const decision = await this.classifierRouter.route({
3375
+ prompt: promptText,
3376
+ estimatedInputTokens: Math.ceil((promptText?.length ?? 0) / 4),
3377
+ hasTools,
3378
+ requiresVision,
3379
+ thinkingLevel,
3380
+ sessionId,
3381
+ });
3382
+ if (!decision) {
3383
+ return;
3384
+ }
3385
+ // Apply provider/model/region as a coherent set (same discipline as
3386
+ // applyRequestRouter): only fill fields the caller did not pin.
3387
+ if (decision.provider && !options.provider) {
3388
+ options.provider = decision.provider;
3389
+ if (decision.model && !options.model) {
3390
+ options.model = decision.model;
3391
+ }
3392
+ if (decision.region && !options.region) {
3393
+ options.region = decision.region;
3394
+ }
3395
+ }
3396
+ else if (!decision.provider && decision.model && !options.model) {
3397
+ options.model = decision.model;
3398
+ }
3399
+ // Narrow tools unless the caller disabled tools entirely.
3400
+ if (!opt.disableTools) {
3401
+ if (decision.excludeTools && decision.excludeTools.length > 0) {
3402
+ opt.excludeTools = Array.from(new Set([...(opt.excludeTools ?? []), ...decision.excludeTools]));
3403
+ }
3404
+ if (decision.toolFilter && decision.toolFilter.length > 0) {
3405
+ const allow = decision.toolFilter;
3406
+ opt.toolFilter =
3407
+ opt.toolFilter && opt.toolFilter.length > 0
3408
+ ? opt.toolFilter.filter((t) => allow.includes(t))
3409
+ : [...allow];
3410
+ }
3411
+ }
3412
+ // Mark routed so orchestration/requestRouter stand down this turn.
3413
+ if (decision.provider || decision.model) {
3414
+ const ctx = opt.context ?? {};
3415
+ ctx.__classifierRouted = true;
3416
+ opt.context = ctx;
3417
+ }
3418
+ if (decision.reason) {
3419
+ logger.debug("[NeuroLink] classifierRouter decision", {
3420
+ reason: decision.reason,
3421
+ difficulty: decision.difficulty,
3422
+ provider: options.provider,
3423
+ model: options.model,
3424
+ });
3425
+ }
3426
+ }
3427
+ catch (classifierErr) {
3428
+ logger.warn("[NeuroLink] classifierRouter threw — proceeding unrouted", {
3429
+ error: classifierErr instanceof Error
3430
+ ? classifierErr.message
3431
+ : String(classifierErr),
3432
+ });
3433
+ }
3434
+ }
3310
3435
  async prepareGenerateAugmentations(options) {
3311
3436
  if (options.rag?.files?.length) {
3312
3437
  try {
@@ -5829,6 +5954,12 @@ Current user's request: ${currentInput}`;
5829
5954
  // classify correctly. After the workflow short-circuit, so workflow
5830
5955
  // streams skip it.
5831
5956
  await context.with(streamSpanContext, () => this.setLangfuseContextFromOptions(options, () => this.applyToolRoutingExclusions(options, originalPrompt)));
5957
+ // Pre-call classifier router for stream: opt-in, fails open. Runs before
5958
+ // the request router so it takes precedence.
5959
+ await this.applyClassifierRouting(options, originalPrompt, !options.disableTools &&
5960
+ (!!(options.tools && Object.keys(options.tools).length > 0) ||
5961
+ this.getCustomTools().size > 0), !!(options.input?.images && options.input.images.length > 0), options
5962
+ .thinking?.thinkingLevel);
5832
5963
  // Pre-call request router for stream: opt-in, fails open.
5833
5964
  await this.applyRequestRouter(options, originalPrompt, !options.disableTools &&
5834
5965
  (!!(options.tools && Object.keys(options.tools).length > 0) ||
@@ -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
+ }