@juspay/neurolink 13.1.1 → 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.
Files changed (43) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/README.md +71 -52
  3. package/dist/browser/neurolink.min.js +411 -411
  4. package/dist/cli/commands/decide.d.ts +2 -1
  5. package/dist/cli/commands/decide.js +2 -1
  6. package/dist/cli/commands/setup.d.ts +1 -1
  7. package/dist/cli/commands/setup.js +1 -1
  8. package/dist/cli/factories/commandFactory.js +1 -1
  9. package/dist/constants/contextWindows.d.ts +7 -0
  10. package/dist/constants/contextWindows.js +48 -0
  11. package/dist/constants/enums.d.ts +5 -0
  12. package/dist/constants/enums.js +6 -0
  13. package/dist/core/baseProvider.d.ts +0 -1
  14. package/dist/core/baseProvider.js +5 -2
  15. package/dist/factories/providerDescriptors.js +13 -9
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/middleware/defaults.d.ts +68 -0
  19. package/dist/middleware/defaults.js +225 -0
  20. package/dist/middleware/factory.js +4 -15
  21. package/dist/middleware/presets.d.ts +12 -0
  22. package/dist/middleware/presets.js +29 -0
  23. package/dist/neurolink.d.ts +9 -0
  24. package/dist/neurolink.js +38 -1
  25. package/dist/providers/catalog/charm-hyper.json +6 -10
  26. package/dist/providers/catalog/index.generated.d.ts +1 -1
  27. package/dist/providers/catalog/index.generated.js +3 -0
  28. package/dist/providers/catalog/openai-decisions.json +1 -1
  29. package/dist/providers/catalog/provider-catalog.schema.json +8 -0
  30. package/dist/providers/catalog/scaledown.json +68 -0
  31. package/dist/providers/catalog/schema.js +2 -0
  32. package/dist/providers/configuredDecision.d.ts +5 -0
  33. package/dist/providers/configuredDecision.js +27 -5
  34. package/dist/types/classifierRouter.d.ts +10 -9
  35. package/dist/types/cli.d.ts +2 -2
  36. package/dist/types/config.d.ts +22 -0
  37. package/dist/types/providerCatalog.d.ts +22 -0
  38. package/dist/types/providerCatalog.generated.d.ts +2 -2
  39. package/dist/types/providers.d.ts +4 -0
  40. package/dist/utils/pricing.js +12 -0
  41. package/dist/utils/tokenLimits.js +20 -1
  42. package/docs-site/static/search-index.json +128 -100
  43. package/package.json +5 -2
@@ -10,7 +10,8 @@
10
10
  * Credentials are env-only here, exactly as every other CLI command: the
11
11
  * decision provider reads its own settings (TYPESAFE_API_KEY, LAYA_API_KEY and
12
12
  * LAYA_BASE_URL, XOR_API_KEY and XOR_BASE_URL, SUBQ_API_KEY, PERPLEXITY_API_KEY,
13
- * CLOUDFLARE_API_KEY and CLOUDFLARE_ACCOUNT_ID, or OPENAI_API_KEY) itself. An
13
+ * CLOUDFLARE_API_KEY and CLOUDFLARE_ACCOUNT_ID, OPENAI_API_KEY, or
14
+ * SCALEDOWN_API_KEY) itself. An
14
15
  * OPENAI_API_KEY beside an OPENAI_BASE_URL, OPENAI_API_BASE or credentials.openai.baseURL that names a host
15
16
  * other than https://api.openai.com is a gateway's and is not used by default.
16
17
  */
@@ -10,7 +10,8 @@
10
10
  * Credentials are env-only here, exactly as every other CLI command: the
11
11
  * decision provider reads its own settings (TYPESAFE_API_KEY, LAYA_API_KEY and
12
12
  * LAYA_BASE_URL, XOR_API_KEY and XOR_BASE_URL, SUBQ_API_KEY, PERPLEXITY_API_KEY,
13
- * CLOUDFLARE_API_KEY and CLOUDFLARE_ACCOUNT_ID, or OPENAI_API_KEY) itself. An
13
+ * CLOUDFLARE_API_KEY and CLOUDFLARE_ACCOUNT_ID, OPENAI_API_KEY, or
14
+ * SCALEDOWN_API_KEY) itself. An
14
15
  * OPENAI_API_KEY beside an OPENAI_BASE_URL, OPENAI_API_BASE or credentials.openai.baseURL that names a host
15
16
  * other than https://api.openai.com is a gateway's and is not used by default.
16
17
  */
@@ -13,7 +13,7 @@ import type { ProviderConfigOptions, SetupArgs } from "../../types/index.js";
13
13
  * perplexity, sambanova, together-ai, xai — mistral is the 9th catalog
14
14
  * provider but keeps its own handleMistralSetup() flow above, so it's
15
15
  * excluded here), spread directly from the catalog JSON via
16
- * buildCatalogConfigOptions(); decide-kind catalog entries (laya, xor, subq, openai-decisions) are
16
+ * buildCatalogConfigOptions(); decide-kind catalog entries (laya, xor, subq, openai-decisions, scaledown) are
17
17
  * spread the same way from getDecideCatalogEntries(). The hand rows listed
18
18
  * first reuse the existing createXConfig() factories in providerConfig.ts; the
19
19
  * remaining 5 (ollama, litellm, sagemaker, lm-studio, llamacpp) don't have a
@@ -150,7 +150,7 @@ const PROVIDERS = [
150
150
  * perplexity, sambanova, together-ai, xai — mistral is the 9th catalog
151
151
  * provider but keeps its own handleMistralSetup() flow above, so it's
152
152
  * excluded here), spread directly from the catalog JSON via
153
- * buildCatalogConfigOptions(); decide-kind catalog entries (laya, xor, subq, openai-decisions) are
153
+ * buildCatalogConfigOptions(); decide-kind catalog entries (laya, xor, subq, openai-decisions, scaledown) are
154
154
  * spread the same way from getDecideCatalogEntries(). The hand rows listed
155
155
  * first reuse the existing createXConfig() factories in providerConfig.ts; the
156
156
  * remaining 5 (ollama, litellm, sagemaker, lm-studio, llamacpp) don't have a
@@ -649,7 +649,7 @@ export class CLICommandFactory {
649
649
  },
650
650
  classifierStrategy: {
651
651
  type: "string",
652
- description: "Classifier strategy: 'auto' (default — 'jev' when a decision provider is configured, such as TYPESAFE_API_KEY, LAYA_API_KEY with LAYA_BASE_URL, XOR_API_KEY with XOR_BASE_URL, SUBQ_API_KEY, PERPLEXITY_API_KEY, CLOUDFLARE_API_KEY with CLOUDFLARE_ACCOUNT_ID, or OPENAI_API_KEY unless OPENAI_BASE_URL, OPENAI_API_BASE or credentials.openai.baseURL names a host other than https://api.openai.com, else 'heuristic'), 'heuristic' (no LLM), 'llm' (a cheap model picks per prompt), or 'jev' (a decision model — whichever decision provider is configured: TypeSafe Jev, Laya, XOR, SubQ, Perplexity, Cloudflare Clef or OpenAI Decisions — with the confidence it reports).",
652
+ description: "Classifier strategy: 'auto' (default — 'jev' when a decision provider is configured, such as TYPESAFE_API_KEY, LAYA_API_KEY with LAYA_BASE_URL, XOR_API_KEY with XOR_BASE_URL, SUBQ_API_KEY, PERPLEXITY_API_KEY, CLOUDFLARE_API_KEY with CLOUDFLARE_ACCOUNT_ID, OPENAI_API_KEY unless OPENAI_BASE_URL, OPENAI_API_BASE or credentials.openai.baseURL names a host other than https://api.openai.com, or SCALEDOWN_API_KEY, else 'heuristic'), 'heuristic' (no LLM), 'llm' (a cheap model picks per prompt), or 'jev' (a decision model — whichever decision provider is configured: TypeSafe Jev, Laya, XOR, SubQ, Perplexity, Cloudflare Clef, OpenAI Decisions or ScaleDown — with the confidence it reports).",
653
653
  choices: ["auto", "heuristic", "llm", "jev"],
654
654
  alias: "classifier-strategy",
655
655
  },
@@ -11,6 +11,13 @@
11
11
  * - Others: Provider documentation as of Feb 2026
12
12
  */
13
13
  export { stripBedrockGeoPrefix } from "./bedrockIdentifiers.js";
14
+ /**
15
+ * The output-token ceiling a JSON-catalog provider declares for one model, or
16
+ * undefined when the provider is not a catalog provider, the model is not
17
+ * listed, or the listing records no maxOutputTokens for it. Never falls back
18
+ * to the provider-wide default (see {@link CATALOG_OUTPUT_CEILINGS}).
19
+ */
20
+ export declare function getCatalogOutputCeiling(provider: string, model?: string): number | undefined;
14
21
  /** Default context window when provider/model is unknown */
15
22
  export declare const DEFAULT_CONTEXT_WINDOW = 128000;
16
23
  /** Maximum output reserve when maxTokens not specified */
@@ -37,6 +37,54 @@ const CATALOG_CONTEXT_WINDOWS = Object.fromEntries(getCatalogJsonEntries().map((
37
37
  : [])),
38
38
  },
39
39
  ]));
40
+ /**
41
+ * Per-model output-token ceilings for the JSON-catalog providers, derived
42
+ * from models.catalog[*].maxOutputTokens and keyed by lowercased provider id
43
+ * and alias. Each provider's map holds every model id as written plus its
44
+ * lowercased form, so a lookup tries the exact spelling first.
45
+ *
46
+ * Deliberately NOT models.defaultMaxOutputTokens: for about 28 catalog
47
+ * providers (groq, xai, together-ai, fireworks, perplexity, cloudflare,
48
+ * mistral ...) that is a generic 4096 placeholder (see manifestRegistry.ts),
49
+ * far below the real ceilings of their models. A model with no recorded
50
+ * maxOutputTokens has no entry here, so it is never capped by a guess.
51
+ */
52
+ const CATALOG_OUTPUT_CEILINGS = new Map(getCatalogJsonEntries().flatMap((entry) => {
53
+ const ceilings = Object.entries(entry.models.catalog).flatMap(([modelId, spec]) => spec.maxOutputTokens !== undefined &&
54
+ Number.isFinite(spec.maxOutputTokens) &&
55
+ spec.maxOutputTokens > 0
56
+ ? [[modelId, spec.maxOutputTokens]]
57
+ : []);
58
+ if (ceilings.length === 0) {
59
+ return [];
60
+ }
61
+ // Lowercased spellings go in first so an exact id always overrides a
62
+ // case-folded one; two ids differing only by case keep the smaller cap.
63
+ const byModel = new Map();
64
+ for (const [modelId, ceiling] of ceilings) {
65
+ const folded = modelId.toLowerCase();
66
+ byModel.set(folded, Math.min(byModel.get(folded) ?? ceiling, ceiling));
67
+ }
68
+ for (const [modelId, ceiling] of ceilings) {
69
+ byModel.set(modelId, ceiling);
70
+ }
71
+ return [entry.id, ...entry.aliases].map((name) => [name.toLowerCase(), byModel]);
72
+ }));
73
+ /**
74
+ * The output-token ceiling a JSON-catalog provider declares for one model, or
75
+ * undefined when the provider is not a catalog provider, the model is not
76
+ * listed, or the listing records no maxOutputTokens for it. Never falls back
77
+ * to the provider-wide default (see {@link CATALOG_OUTPUT_CEILINGS}).
78
+ */
79
+ export function getCatalogOutputCeiling(provider, model) {
80
+ // getSafeMaxTokens has always tolerated a missing provider, so this lookup
81
+ // must too (callers outside the typed option normalisers can pass one).
82
+ if (!model || typeof provider !== "string") {
83
+ return undefined;
84
+ }
85
+ const ceilings = CATALOG_OUTPUT_CEILINGS.get(provider.toLowerCase());
86
+ return ceilings?.get(model) ?? ceilings?.get(model.toLowerCase());
87
+ }
40
88
  /** Default context window when provider/model is unknown */
41
89
  export const DEFAULT_CONTEXT_WINDOW = 128_000;
42
90
  /** Maximum output reserve when maxTokens not specified */
@@ -85,6 +85,7 @@ export declare enum AIProviderName {
85
85
  SAKANA_AI = "sakana-ai",
86
86
  SAMBANOVA = "sambanova",
87
87
  SARVAM = "sarvam",
88
+ SCALEDOWN = "scaledown",
88
89
  SCALEWAY = "scaleway",
89
90
  SILICONFLOW = "siliconflow",
90
91
  STACKIT = "stackit",
@@ -1570,6 +1571,10 @@ export declare enum SarvamModels {
1570
1571
  SARVAM_105B = "sarvam-105b",
1571
1572
  SARVAM_105B_CONVERSATIONS = "sarvam-105b-conversations"
1572
1573
  }
1574
+ export declare enum ScaleDownModels {
1575
+ CLASSIFY_1 = "classify-1",
1576
+ CLASSIFY = "classify"
1577
+ }
1573
1578
  export declare enum ScalewayModels {
1574
1579
  MISTRAL_SMALL_3_2_24B_INSTRUCT_2506 = "mistral-small-3.2-24b-instruct-2506",
1575
1580
  LLAMA_3_3_70B_INSTRUCT = "llama-3.3-70b-instruct",
@@ -90,6 +90,7 @@ export var AIProviderName;
90
90
  AIProviderName["SAKANA_AI"] = "sakana-ai";
91
91
  AIProviderName["SAMBANOVA"] = "sambanova";
92
92
  AIProviderName["SARVAM"] = "sarvam";
93
+ AIProviderName["SCALEDOWN"] = "scaledown";
93
94
  AIProviderName["SCALEWAY"] = "scaleway";
94
95
  AIProviderName["SILICONFLOW"] = "siliconflow";
95
96
  AIProviderName["STACKIT"] = "stackit";
@@ -1875,6 +1876,11 @@ export var SarvamModels;
1875
1876
  SarvamModels["SARVAM_105B"] = "sarvam-105b";
1876
1877
  SarvamModels["SARVAM_105B_CONVERSATIONS"] = "sarvam-105b-conversations";
1877
1878
  })(SarvamModels || (SarvamModels = {}));
1879
+ export var ScaleDownModels;
1880
+ (function (ScaleDownModels) {
1881
+ ScaleDownModels["CLASSIFY_1"] = "classify-1";
1882
+ ScaleDownModels["CLASSIFY"] = "classify";
1883
+ })(ScaleDownModels || (ScaleDownModels = {}));
1878
1884
  export var ScalewayModels;
1879
1885
  (function (ScalewayModels) {
1880
1886
  ScalewayModels["MISTRAL_SMALL_3_2_24B_INSTRUCT_2506"] = "mistral-small-3.2-24b-instruct-2506";
@@ -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);
@@ -469,18 +469,22 @@ const HAND_DESCRIPTORS = [
469
469
  },
470
470
  // Decision precedence is decideOrder, not position: typesafe 1, laya 2 and
471
471
  // xor 3 (from catalog/laya.json and catalog/xor.json), subq 4 (from
472
- // catalog/subq.json), perplexity-decider 5, cloudflare-clef 6 and
473
- // openai-decisions 7 (catalog/openai-decisions.json).
472
+ // catalog/subq.json), perplexity-decider 5, cloudflare-clef 6,
473
+ // openai-decisions 7 (catalog/openai-decisions.json) and scaledown 8
474
+ // (catalog/scaledown.json).
474
475
  // resolveDefaultDecisionProvider() returns the first configured
475
476
  // DECISION_PROVIDERS entry in that order, so a host configured for several
476
477
  // keeps Jev for every built-in consumer and reaches the others only by
477
478
  // naming them. Renumbering silently changes which model routes, compacts
478
479
  // and plans for every such host.
479
- // perplexity-decider, cloudflare-clef and openai-decisions come last because
480
- // each shares its environment key with a text provider: a key set only for
481
- // that text provider must not displace a decision provider a host set up on
480
+ // perplexity-decider, cloudflare-clef and openai-decisions come after subq
481
+ // because each shares its environment key with a text provider: a key set only
482
+ // for that text provider must not displace a decision provider a host set up on
482
483
  // purpose. subq is ahead of them only because SUBQ_API_KEY belongs to no other
483
- // provider.
484
+ // provider. scaledown is last by the owner's decision of 2026-10-15, not by
485
+ // that rule: SCALEDOWN_API_KEY belongs to no other provider either, so an
486
+ // ambient OPENAI_API_KEY, PERPLEXITY_API_KEY or Cloudflare pair outranks a
487
+ // ScaleDown key a host set on purpose, and ScaleDown is reached by naming it.
484
488
  {
485
489
  name: AIProviderName.PERPLEXITY_DECIDER,
486
490
  aliases: [],
@@ -552,9 +556,9 @@ const HAND_DESCRIPTORS = [
552
556
  credentialsKey: "cloudflareClef",
553
557
  envVars: {
554
558
  // The same token and account id the `cloudflare` text provider reads, so
555
- // ambient Workers AI settings configure this provider too. It sits last
556
- // in this list, which is what keeps that from displacing any other
557
- // decision provider a host has configured.
559
+ // ambient Workers AI settings configure this provider too. Its place in
560
+ // the decision order (see the comment above) keeps that from displacing
561
+ // TypeSafe, Laya, XOR or SubQ.
558
562
  apiKey: "CLOUDFLARE_API_KEY",
559
563
  // Cloudflare's own API is the endpoint, so a base URL is optional; but
560
564
  // the path carries the account id, so a token alone is useless. Both are
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;