@juspay/neurolink 13.2.0 → 13.3.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.
@@ -599,7 +599,6 @@ export declare abstract class BaseProvider implements AIProvider {
599
599
  /**
600
600
  * Get AI SDK model with middleware applied
601
601
  * This method wraps the base model with any configured middleware
602
- * TODO(#1953): Implement global level middlewares that can be used
603
602
  */
604
603
  protected getAISDKModelWithMiddleware(options?: TextGenerationOptions | StreamOptions): Promise<LanguageModel>;
605
604
  /**
@@ -117,7 +117,11 @@ export class BaseProvider {
117
117
  modelName;
118
118
  providerName;
119
119
  defaultTimeout = 30000; // 30 seconds
120
- middlewareOptions; // TODO(#1953): Implement global level middlewares that can be used
120
+ // Only set by a subclass passing the 4th constructor argument; no provider
121
+ // does. NeuroLink resolves its instance, global and environment middleware
122
+ // defaults into `options.middleware` before a call reaches here (see
123
+ // middleware/defaults.ts), so a per-call value already carries them.
124
+ middlewareOptions;
121
125
  // Tools are conditionally included based on centralized configuration
122
126
  directTools = shouldDisableBuiltinTools()
123
127
  ? {}
@@ -2509,7 +2513,6 @@ export class BaseProvider {
2509
2513
  /**
2510
2514
  * Get AI SDK model with middleware applied
2511
2515
  * This method wraps the base model with any configured middleware
2512
- * TODO(#1953): Implement global level middlewares that can be used
2513
2516
  */
2514
2517
  async getAISDKModelWithMiddleware(options = {}) {
2515
2518
  return this.applyMiddlewareToModel(await this.getAISDKModel(), options);
package/dist/index.d.ts CHANGED
@@ -89,6 +89,7 @@ export { shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, set
89
89
  export { clearAnalyticsMetrics, createAnalyticsMiddleware, getAnalyticsMetrics, } from "./middleware/builtin/analytics.js";
90
90
  export { createLifecycleMiddleware } from "./middleware/builtin/lifecycle.js";
91
91
  export { MiddlewareFactory } from "./middleware/factory.js";
92
+ export { clearGlobalMiddleware, configureGlobalMiddleware, } from "./middleware/defaults.js";
92
93
  export { tool, jsonSchema, stepCountIs } from "./utils/tool.js";
93
94
  export { ExporterRegistry } from "./observability/exporterRegistry.js";
94
95
  export { NoOpExporter } from "./observability/exporters/baseExporter.js";
package/dist/index.js CHANGED
@@ -178,6 +178,7 @@ export { shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, set
178
178
  export { clearAnalyticsMetrics, createAnalyticsMiddleware, getAnalyticsMetrics, } from "./middleware/builtin/analytics.js";
179
179
  export { createLifecycleMiddleware } from "./middleware/builtin/lifecycle.js";
180
180
  export { MiddlewareFactory } from "./middleware/factory.js";
181
+ export { clearGlobalMiddleware, configureGlobalMiddleware, } from "./middleware/defaults.js";
181
182
  // Tool + schema helpers. Previously a consumer reached for `tool()` and
182
183
  // `jsonSchema()` from the ai package to build tools for generate({tools}).
183
184
  // That package is no longer a dependency, so the equivalents ship here.
@@ -0,0 +1,68 @@
1
+ import type { MiddlewareFactoryOptions } from "../types/index.js";
2
+ /**
3
+ * Middleware defaults.
4
+ *
5
+ * A generate()/stream() call resolves its middleware from up to four layers,
6
+ * lowest precedence first:
7
+ *
8
+ * 1. NEUROLINK_MIDDLEWARE_PRESET (process environment)
9
+ * 2. configureGlobalMiddleware() (module-level, set at runtime)
10
+ * 3. new NeuroLink({ middleware }) (per instance)
11
+ * 4. generate({ middleware }) (per call)
12
+ *
13
+ * A layer that is absent changes nothing, so a call made with no default set
14
+ * keeps exactly the middleware it passed.
15
+ */
16
+ export declare const MIDDLEWARE_PRESET_ENV_VAR = "NEUROLINK_MIDDLEWARE_PRESET";
17
+ /**
18
+ * Layer `override` over `base`.
19
+ *
20
+ * - `preset`, `middleware`, `enabledMiddleware` and `disabledMiddleware`: the
21
+ * override's value replaces the base's whole; arrays are never concatenated.
22
+ * - `middlewareConfig`: merged per middleware id. Within an id, `enabled` and
23
+ * `conditions` come from the override when it sets them, and `config` is
24
+ * merged key by key, so a key the override sets replaces the base's value
25
+ * for that key as a whole (nested objects and arrays are not combined).
26
+ * - `global`: merged key by key.
27
+ *
28
+ * The factory applies `disabledMiddleware` after `enabledMiddleware`, so an id
29
+ * the override switches on must be taken out of a disabled list it inherits
30
+ * (and the reverse), or a lower layer's list would out-vote a call that asked
31
+ * for the opposite.
32
+ */
33
+ export declare function mergeMiddlewareOptions(base: MiddlewareFactoryOptions, override: MiddlewareFactoryOptions): MiddlewareFactoryOptions;
34
+ /**
35
+ * Resolve the options a call runs with. `defaults` is ordered lowest
36
+ * precedence first; the per-call options sit on top.
37
+ *
38
+ * With no default present this returns `call` itself, so callers that never
39
+ * opted in see no change at all.
40
+ */
41
+ export declare function resolveMiddlewareOptions(defaults: ReadonlyArray<MiddlewareFactoryOptions | undefined>, call: MiddlewareFactoryOptions | undefined): MiddlewareFactoryOptions | undefined;
42
+ /**
43
+ * Validate a default layer and copy it, so later edits to the caller's object
44
+ * cannot change a default that was already set. Plain data (a word list, a
45
+ * replacement string) is copied in full; custom middleware, callbacks and
46
+ * model handles keep their identity. Throws a TypeError for a value that is
47
+ * not an object and for a preset name that is not built in.
48
+ */
49
+ export declare function snapshotMiddlewareOptions(options: MiddlewareFactoryOptions, source: string): MiddlewareFactoryOptions;
50
+ /**
51
+ * Process-wide middleware defaults. Applies to every NeuroLink instance, below
52
+ * the instance's own `middleware` option and above NEUROLINK_MIDDLEWARE_PRESET.
53
+ * Replaces any earlier call rather than adding to it, and is read on each
54
+ * generate()/stream(), so instances that already exist pick it up. Throws a
55
+ * TypeError when `options` is not an object or names a preset that is not
56
+ * built in.
57
+ */
58
+ export declare function configureGlobalMiddleware(options: MiddlewareFactoryOptions): void;
59
+ /** Remove the defaults set by {@link configureGlobalMiddleware}. */
60
+ export declare function clearGlobalMiddleware(): void;
61
+ /** The current global defaults, or undefined when none are set. */
62
+ export declare function getGlobalMiddleware(): MiddlewareFactoryOptions | undefined;
63
+ /**
64
+ * The preset named by NEUROLINK_MIDDLEWARE_PRESET, as the lowest default layer.
65
+ * An unset or blank variable is no layer; an unrecognised name is reported
66
+ * on stderr once per distinct value and ignored.
67
+ */
68
+ export declare function readEnvMiddlewarePreset(env?: NodeJS.ProcessEnv): MiddlewareFactoryOptions | undefined;
@@ -0,0 +1,225 @@
1
+ import { logger } from "../utils/logger.js";
2
+ import { BUILT_IN_MIDDLEWARE_PRESET_NAMES } from "./presets.js";
3
+ /**
4
+ * Middleware defaults.
5
+ *
6
+ * A generate()/stream() call resolves its middleware from up to four layers,
7
+ * lowest precedence first:
8
+ *
9
+ * 1. NEUROLINK_MIDDLEWARE_PRESET (process environment)
10
+ * 2. configureGlobalMiddleware() (module-level, set at runtime)
11
+ * 3. new NeuroLink({ middleware }) (per instance)
12
+ * 4. generate({ middleware }) (per call)
13
+ *
14
+ * A layer that is absent changes nothing, so a call made with no default set
15
+ * keeps exactly the middleware it passed.
16
+ */
17
+ export const MIDDLEWARE_PRESET_ENV_VAR = "NEUROLINK_MIDDLEWARE_PRESET";
18
+ let globalMiddleware;
19
+ const warnedPresets = new Set();
20
+ const isPlainObject = (value) => typeof value === "object" &&
21
+ value !== null &&
22
+ !Array.isArray(value) &&
23
+ !(value instanceof Date) &&
24
+ !(value instanceof RegExp);
25
+ /** Own entries whose value is not `undefined`, so an unset key never wins. */
26
+ const definedEntries = (value) => Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined));
27
+ const isPureData = (value) => {
28
+ if (value === null || typeof value !== "object") {
29
+ return typeof value !== "function" && typeof value !== "symbol";
30
+ }
31
+ if (Array.isArray(value)) {
32
+ return value.every(isPureData);
33
+ }
34
+ const proto = Object.getPrototypeOf(value);
35
+ return ((proto === Object.prototype || proto === null) &&
36
+ Object.values(value).every(isPureData));
37
+ };
38
+ /**
39
+ * Copy a value only when it is plain data (JSON-like, no functions or class
40
+ * instances anywhere inside). A model handle, a callback or a custom
41
+ * middleware keeps its identity, because copying one would split its state
42
+ * from the object the caller still holds.
43
+ */
44
+ const copyIfPureData = (value) => isPureData(value) ? structuredClone(value) : value;
45
+ const isEmptyLayer = (layer) => Object.keys(definedEntries(layer)).length === 0;
46
+ const mergeConfigEntry = (base, override) => {
47
+ const merged = {
48
+ ...definedEntries(base),
49
+ ...definedEntries(override),
50
+ };
51
+ if (base?.config !== undefined || override?.config !== undefined) {
52
+ merged.config = {
53
+ ...definedEntries(base?.config),
54
+ ...definedEntries(override?.config),
55
+ };
56
+ }
57
+ return merged;
58
+ };
59
+ /** Middleware ids this layer switches on or off, by any of the three routes. */
60
+ const idsSwitched = (layer, state) => {
61
+ const ids = new Set((state ? layer.enabledMiddleware : layer.disabledMiddleware) ?? []);
62
+ for (const [id, entry] of Object.entries(layer.middlewareConfig ?? {})) {
63
+ if (entry?.enabled === state) {
64
+ ids.add(id);
65
+ }
66
+ }
67
+ return ids;
68
+ };
69
+ /**
70
+ * Layer `override` over `base`.
71
+ *
72
+ * - `preset`, `middleware`, `enabledMiddleware` and `disabledMiddleware`: the
73
+ * override's value replaces the base's whole; arrays are never concatenated.
74
+ * - `middlewareConfig`: merged per middleware id. Within an id, `enabled` and
75
+ * `conditions` come from the override when it sets them, and `config` is
76
+ * merged key by key, so a key the override sets replaces the base's value
77
+ * for that key as a whole (nested objects and arrays are not combined).
78
+ * - `global`: merged key by key.
79
+ *
80
+ * The factory applies `disabledMiddleware` after `enabledMiddleware`, so an id
81
+ * the override switches on must be taken out of a disabled list it inherits
82
+ * (and the reverse), or a lower layer's list would out-vote a call that asked
83
+ * for the opposite.
84
+ */
85
+ export function mergeMiddlewareOptions(base, override) {
86
+ const merged = {};
87
+ const preset = override.preset ?? base.preset;
88
+ if (preset !== undefined) {
89
+ merged.preset = preset;
90
+ }
91
+ const custom = override.middleware ?? base.middleware;
92
+ if (custom !== undefined) {
93
+ merged.middleware = [...custom];
94
+ }
95
+ const switchedOn = idsSwitched(override, true);
96
+ const switchedOff = idsSwitched(override, false);
97
+ const enabled = override.enabledMiddleware ??
98
+ base.enabledMiddleware?.filter((id) => !switchedOff.has(id));
99
+ if (enabled !== undefined) {
100
+ merged.enabledMiddleware = [...enabled];
101
+ }
102
+ const disabled = override.disabledMiddleware ??
103
+ base.disabledMiddleware?.filter((id) => !switchedOn.has(id));
104
+ if (disabled !== undefined) {
105
+ merged.disabledMiddleware = [...disabled];
106
+ }
107
+ if (base.middlewareConfig || override.middlewareConfig) {
108
+ const ids = new Set([
109
+ ...Object.keys(base.middlewareConfig ?? {}),
110
+ ...Object.keys(override.middlewareConfig ?? {}),
111
+ ]);
112
+ merged.middlewareConfig = Object.fromEntries([...ids].map((id) => [
113
+ id,
114
+ mergeConfigEntry(base.middlewareConfig?.[id], override.middlewareConfig?.[id]),
115
+ ]));
116
+ }
117
+ if (base.global || override.global) {
118
+ merged.global = {
119
+ ...definedEntries(base.global),
120
+ ...definedEntries(override.global),
121
+ };
122
+ }
123
+ return merged;
124
+ }
125
+ /**
126
+ * Resolve the options a call runs with. `defaults` is ordered lowest
127
+ * precedence first; the per-call options sit on top.
128
+ *
129
+ * With no default present this returns `call` itself, so callers that never
130
+ * opted in see no change at all.
131
+ */
132
+ export function resolveMiddlewareOptions(defaults, call) {
133
+ const layers = defaults.filter((layer) => layer !== undefined && !isEmptyLayer(layer));
134
+ if (layers.length === 0) {
135
+ return call;
136
+ }
137
+ const inherited = layers.reduce((acc, layer) => mergeMiddlewareOptions(acc, layer), {});
138
+ return isPlainObject(call)
139
+ ? mergeMiddlewareOptions(inherited, call)
140
+ : inherited;
141
+ }
142
+ /**
143
+ * Validate a default layer and copy it, so later edits to the caller's object
144
+ * cannot change a default that was already set. Plain data (a word list, a
145
+ * replacement string) is copied in full; custom middleware, callbacks and
146
+ * model handles keep their identity. Throws a TypeError for a value that is
147
+ * not an object and for a preset name that is not built in.
148
+ */
149
+ export function snapshotMiddlewareOptions(options, source) {
150
+ if (!isPlainObject(options)) {
151
+ throw new TypeError(`${source}: middleware must be an object of MiddlewareFactoryOptions`);
152
+ }
153
+ if (options.preset !== undefined &&
154
+ !BUILT_IN_MIDDLEWARE_PRESET_NAMES.includes(options.preset)) {
155
+ // An unknown name left in the layer would replace a valid lower-layer
156
+ // preset and leave guardrails off with no sign of it, so a typo fails here.
157
+ throw new TypeError(`${source}: unknown middleware preset ${JSON.stringify(options.preset)}. Known presets: ${BUILT_IN_MIDDLEWARE_PRESET_NAMES.join(", ")}.`);
158
+ }
159
+ const merged = mergeMiddlewareOptions({}, options);
160
+ return {
161
+ ...merged,
162
+ ...(merged.middlewareConfig
163
+ ? {
164
+ middlewareConfig: Object.fromEntries(Object.entries(merged.middlewareConfig).map(([id, entry]) => [
165
+ id,
166
+ {
167
+ ...entry,
168
+ ...(entry.config
169
+ ? {
170
+ config: Object.fromEntries(Object.entries(entry.config).map(([key, value]) => [
171
+ key,
172
+ copyIfPureData(value),
173
+ ])),
174
+ }
175
+ : {}),
176
+ ...(entry.conditions
177
+ ? { conditions: copyIfPureData(entry.conditions) }
178
+ : {}),
179
+ },
180
+ ])),
181
+ }
182
+ : {}),
183
+ };
184
+ }
185
+ /**
186
+ * Process-wide middleware defaults. Applies to every NeuroLink instance, below
187
+ * the instance's own `middleware` option and above NEUROLINK_MIDDLEWARE_PRESET.
188
+ * Replaces any earlier call rather than adding to it, and is read on each
189
+ * generate()/stream(), so instances that already exist pick it up. Throws a
190
+ * TypeError when `options` is not an object or names a preset that is not
191
+ * built in.
192
+ */
193
+ export function configureGlobalMiddleware(options) {
194
+ globalMiddleware = snapshotMiddlewareOptions(options, "configureGlobalMiddleware");
195
+ }
196
+ /** Remove the defaults set by {@link configureGlobalMiddleware}. */
197
+ export function clearGlobalMiddleware() {
198
+ globalMiddleware = undefined;
199
+ }
200
+ /** The current global defaults, or undefined when none are set. */
201
+ export function getGlobalMiddleware() {
202
+ return globalMiddleware;
203
+ }
204
+ /**
205
+ * The preset named by NEUROLINK_MIDDLEWARE_PRESET, as the lowest default layer.
206
+ * An unset or blank variable is no layer; an unrecognised name is reported
207
+ * on stderr once per distinct value and ignored.
208
+ */
209
+ export function readEnvMiddlewarePreset(env = process.env) {
210
+ const raw = env[MIDDLEWARE_PRESET_ENV_VAR]?.trim();
211
+ if (!raw) {
212
+ return undefined;
213
+ }
214
+ if (BUILT_IN_MIDDLEWARE_PRESET_NAMES.includes(raw)) {
215
+ return { preset: raw };
216
+ }
217
+ if (!warnedPresets.has(raw)) {
218
+ warnedPresets.add(raw);
219
+ // Written to stderr rather than through logger.warn, which drops warnings
220
+ // unless debugging: a mistyped preset would otherwise leave guardrails off
221
+ // with no sign of it.
222
+ logger.alwaysStderr(`[Middleware] Ignoring ${MIDDLEWARE_PRESET_ENV_VAR}=${JSON.stringify(raw)}: not a known preset. Known presets: ${BUILT_IN_MIDDLEWARE_PRESET_NAMES.join(", ")}.`);
223
+ }
224
+ return undefined;
225
+ }
@@ -1,4 +1,5 @@
1
1
  import { MiddlewareRegistry } from "./registry.js";
2
+ import { createBuiltInMiddlewarePresets } from "./presets.js";
2
3
  import { createAnalyticsMiddleware } from "./builtin/analytics.js";
3
4
  import { createGuardrailsMiddleware } from "./builtin/guardrails.js";
4
5
  import { createAutoEvaluationMiddleware } from "./builtin/autoEvaluation.js";
@@ -30,21 +31,9 @@ export class MiddlewareFactory {
30
31
  lifecycle: createLifecycleMiddleware,
31
32
  };
32
33
  // Register built-in presets
33
- this.registerPreset({
34
- name: "default",
35
- description: "Default preset with analytics enabled.",
36
- config: { analytics: { enabled: true } },
37
- });
38
- this.registerPreset({
39
- name: "all",
40
- description: "Enables all available middleware.",
41
- config: { analytics: { enabled: true }, guardrails: { enabled: true } },
42
- });
43
- this.registerPreset({
44
- name: "security",
45
- description: "Focuses on security with guardrails.",
46
- config: { guardrails: { enabled: true } },
47
- });
34
+ for (const preset of createBuiltInMiddlewarePresets()) {
35
+ this.registerPreset(preset);
36
+ }
48
37
  // Register custom middleware if provided
49
38
  logger.debug("Initializing MiddlewareFactory", { options });
50
39
  if (options.middleware) {
@@ -0,0 +1,12 @@
1
+ import type { MiddlewarePreset } from "../types/index.js";
2
+ /**
3
+ * The presets every `MiddlewareFactory` registers. Built fresh on each call so
4
+ * a factory that edits its own `presets` map cannot change another factory's.
5
+ *
6
+ * `all` is deliberately not "everything": autoEvaluation and lifecycle are
7
+ * opt-in per call and stay out of it. `security` only switches guardrails on;
8
+ * it ships no word list and no model filter, so it redacts nothing until the
9
+ * caller supplies `badWords` or `modelFilter`.
10
+ */
11
+ export declare function createBuiltInMiddlewarePresets(): MiddlewarePreset[];
12
+ export declare const BUILT_IN_MIDDLEWARE_PRESET_NAMES: readonly string[];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The presets every `MiddlewareFactory` registers. Built fresh on each call so
3
+ * a factory that edits its own `presets` map cannot change another factory's.
4
+ *
5
+ * `all` is deliberately not "everything": autoEvaluation and lifecycle are
6
+ * opt-in per call and stay out of it. `security` only switches guardrails on;
7
+ * it ships no word list and no model filter, so it redacts nothing until the
8
+ * caller supplies `badWords` or `modelFilter`.
9
+ */
10
+ export function createBuiltInMiddlewarePresets() {
11
+ return [
12
+ {
13
+ name: "default",
14
+ description: "Default preset with analytics enabled.",
15
+ config: { analytics: { enabled: true } },
16
+ },
17
+ {
18
+ name: "all",
19
+ description: "Enables analytics and guardrails.",
20
+ config: { analytics: { enabled: true }, guardrails: { enabled: true } },
21
+ },
22
+ {
23
+ name: "security",
24
+ description: "Enables guardrails. Filters nothing until badWords or modelFilter is configured.",
25
+ config: { guardrails: { enabled: true } },
26
+ },
27
+ ];
28
+ }
29
+ export const BUILT_IN_MIDDLEWARE_PRESET_NAMES = createBuiltInMiddlewarePresets().map((preset) => preset.name);
@@ -159,6 +159,8 @@ export declare class NeuroLink {
159
159
  /** Session-scoped pins of tools discovered via search_tools (tools.discovery mode). */
160
160
  private discoveryPins;
161
161
  private enableOrchestration;
162
+ private readonly envMiddleware?;
163
+ private readonly constructorMiddleware?;
162
164
  private authProvider?;
163
165
  private pendingAuthConfig?;
164
166
  private authInitPromise?;
@@ -234,6 +236,13 @@ export declare class NeuroLink {
234
236
  private enforceSessionBudget;
235
237
  private assertInputText;
236
238
  private applyAuthenticatedRequestContext;
239
+ /**
240
+ * Fold the instance, global and environment middleware defaults beneath the
241
+ * call's own `middleware`. Runs before the lifecycle helpers so they layer
242
+ * onFinish/onError on top of the resolved config rather than replacing it.
243
+ * With no default set the options are left untouched.
244
+ */
245
+ private applyMiddlewareDefaults;
237
246
  private applyGenerateLifecycleMiddleware;
238
247
  private applyStreamLifecycleMiddleware;
239
248
  private initializeMemoryConfig;
package/dist/neurolink.js CHANGED
@@ -94,6 +94,7 @@ import { hasLifecycleErrorFired, markLifecycleErrorFired, markLifecycleStreamCal
94
94
  import { interleaveTTSStream } from "./utils/ttsStream.js";
95
95
  import { resolveLifecycleTimeoutMs } from "./utils/lifecycleTimeout.js";
96
96
  import { cloneOptionsForCallIsolation } from "./utils/cloneOptions.js";
97
+ import { getGlobalMiddleware, readEnvMiddlewarePreset, resolveMiddlewareOptions, snapshotMiddlewareOptions, } from "./middleware/defaults.js";
97
98
  import { preserveLiveStreamAccessors } from "./utils/streamResultAccessors.js";
98
99
  import { coerceJsonToSchema, recoverScalarRoot, schemaAccepts, } from "./utils/json/coerce.js";
99
100
  // Factory processing imports
@@ -655,6 +656,12 @@ export class NeuroLink {
655
656
  discoveryPins = new Map();
656
657
  // Add orchestration property
657
658
  enableOrchestration;
659
+ // Language-model middleware defaults, lowest precedence first: the layer
660
+ // read from NEUROLINK_MIDDLEWARE_PRESET, then the constructor's `middleware`
661
+ // option. The module-level global registry sits between them and is read on
662
+ // each call, so it can change after construction. See middleware/defaults.ts.
663
+ envMiddleware;
664
+ constructorMiddleware;
658
665
  // Authentication provider for secure access control
659
666
  authProvider;
660
667
  pendingAuthConfig;
@@ -900,6 +907,18 @@ export class NeuroLink {
900
907
  ...tokenDerivedFields,
901
908
  };
902
909
  }
910
+ /**
911
+ * Fold the instance, global and environment middleware defaults beneath the
912
+ * call's own `middleware`. Runs before the lifecycle helpers so they layer
913
+ * onFinish/onError on top of the resolved config rather than replacing it.
914
+ * With no default set the options are left untouched.
915
+ */
916
+ applyMiddlewareDefaults(options) {
917
+ const resolved = resolveMiddlewareOptions([this.envMiddleware, getGlobalMiddleware(), this.constructorMiddleware], options.middleware);
918
+ if (resolved !== options.middleware) {
919
+ options.middleware = resolved;
920
+ }
921
+ }
903
922
  applyGenerateLifecycleMiddleware(options) {
904
923
  if (!options.onFinish && !options.onError) {
905
924
  return;
@@ -1207,6 +1226,13 @@ export class NeuroLink {
1207
1226
  return metricsTraceContextStorage.getStore() ?? null;
1208
1227
  }
1209
1228
  constructor(config) {
1229
+ // Validated first: an invalid default must throw before the constructor
1230
+ // starts telemetry or any other process-wide state.
1231
+ this.envMiddleware = readEnvMiddlewarePreset();
1232
+ this.constructorMiddleware =
1233
+ config?.middleware === undefined
1234
+ ? undefined
1235
+ : snapshotMiddlewareOptions(config.middleware, "NeuroLink constructor");
1210
1236
  if (config?.toolRegistry) {
1211
1237
  // A supplied registry (a host's, shared with a worker) keeps its owner's
1212
1238
  // roots; per-call executeTool() binds this instance's policy anyway.
@@ -4157,6 +4183,7 @@ Current user's request: ${currentInput}`;
4157
4183
  options.input.text = piiResult.text;
4158
4184
  }
4159
4185
  this.enforceSessionBudget(options.maxBudgetUsd);
4186
+ this.applyMiddlewareDefaults(options);
4160
4187
  this.applyGenerateLifecycleMiddleware(options);
4161
4188
  await this.applyAuthenticatedRequestContext(options);
4162
4189
  return { options, originalPrompt };
@@ -5470,6 +5497,9 @@ Current user's request: ${currentInput}`;
5470
5497
  // NL-004: Resolve model aliases/deprecations before processing
5471
5498
  isolated.model = resolveModel(isolated.model, this.modelAliasConfig);
5472
5499
  bindFileToolRoots(isolated, this.resolveFileToolRoots(isolated.toolRoots));
5500
+ // generateText() reaches the same providers as generate() without passing
5501
+ // through its prelude, so the middleware defaults are folded in here too.
5502
+ this.applyMiddlewareDefaults(isolated);
5473
5503
  // Use internal generation method directly
5474
5504
  return await this.generateTextInternal(isolated);
5475
5505
  }
@@ -8291,6 +8321,7 @@ Current user's request: ${currentInput}`;
8291
8321
  this.enforceSessionBudget(options.maxBudgetUsd);
8292
8322
  await this.applyAuthenticatedRequestContext(options);
8293
8323
  this.emitStreamStartEvents(options, startTime);
8324
+ this.applyMiddlewareDefaults(options);
8294
8325
  this.applyStreamLifecycleMiddleware(options);
8295
8326
  }
8296
8327
  async maybeHandleWorkflowStreamRequest(params) {
@@ -17,6 +17,7 @@ import type { ContextRelevanceOptions } from "./context.js";
17
17
  import type { DecisionHooks } from "./decision.js";
18
18
  import type { SkillsConfig } from "./skills.js";
19
19
  import type { KnowledgeGroundingConfig } from "./knowledge.js";
20
+ import type { MiddlewareFactoryOptions } from "./middleware.js";
20
21
  /**
21
22
  * Main NeuroLink configuration type
22
23
  */
@@ -184,6 +185,27 @@ export type NeurolinkConstructorConfig = {
184
185
  * {@link KnowledgeGroundingConfig}.
185
186
  */
186
187
  knowledgeGrounding?: KnowledgeGroundingConfig;
188
+ /**
189
+ * Language-model middleware applied to every generate()/stream() on this
190
+ * instance (guardrails, analytics, custom middleware). Spares each call from
191
+ * repeating the same `middleware` option.
192
+ *
193
+ * Precedence, lowest to highest: `NEUROLINK_MIDDLEWARE_PRESET`,
194
+ * `configureGlobalMiddleware()`, this option, then the call's own
195
+ * `middleware`. `middlewareConfig` merges per middleware id, and a key set
196
+ * by a higher layer replaces the lower layer's value; arrays are replaced,
197
+ * never concatenated. A call can switch off an inherited middleware with
198
+ * `disabledMiddleware` or `middlewareConfig.<id>.enabled: false`.
199
+ *
200
+ * Throws a TypeError at construction when this is not an object or its
201
+ * `preset` is not a built-in preset (`default`, `all` or `security`).
202
+ *
203
+ * Unset by default, in which case calls behave exactly as before. Note that
204
+ * `preset: "security"` only turns guardrails on: nothing is redacted until
205
+ * `middlewareConfig.guardrails.config` supplies `badWords` or `modelFilter`.
206
+ * See {@link MiddlewareFactoryOptions}.
207
+ */
208
+ middleware?: MiddlewareFactoryOptions;
187
209
  };
188
210
  /**
189
211
  * Configuration for MCP enhancement modules wired into generate()/stream() paths.