@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.
- package/CHANGELOG.md +3 -3
- package/README.md +71 -52
- package/dist/browser/neurolink.min.js +411 -411
- package/dist/cli/commands/decide.d.ts +2 -1
- package/dist/cli/commands/decide.js +2 -1
- package/dist/cli/commands/setup.d.ts +1 -1
- package/dist/cli/commands/setup.js +1 -1
- package/dist/cli/factories/commandFactory.js +1 -1
- package/dist/constants/contextWindows.d.ts +7 -0
- package/dist/constants/contextWindows.js +48 -0
- package/dist/constants/enums.d.ts +5 -0
- package/dist/constants/enums.js +6 -0
- package/dist/core/baseProvider.d.ts +0 -1
- package/dist/core/baseProvider.js +5 -2
- package/dist/factories/providerDescriptors.js +13 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/middleware/defaults.d.ts +68 -0
- package/dist/middleware/defaults.js +225 -0
- package/dist/middleware/factory.js +4 -15
- package/dist/middleware/presets.d.ts +12 -0
- package/dist/middleware/presets.js +29 -0
- package/dist/neurolink.d.ts +9 -0
- package/dist/neurolink.js +38 -1
- package/dist/providers/catalog/charm-hyper.json +6 -10
- package/dist/providers/catalog/index.generated.d.ts +1 -1
- package/dist/providers/catalog/index.generated.js +3 -0
- package/dist/providers/catalog/openai-decisions.json +1 -1
- package/dist/providers/catalog/provider-catalog.schema.json +8 -0
- package/dist/providers/catalog/scaledown.json +68 -0
- package/dist/providers/catalog/schema.js +2 -0
- package/dist/providers/configuredDecision.d.ts +5 -0
- package/dist/providers/configuredDecision.js +27 -5
- package/dist/types/classifierRouter.d.ts +10 -9
- package/dist/types/cli.d.ts +2 -2
- package/dist/types/config.d.ts +22 -0
- package/dist/types/providerCatalog.d.ts +22 -0
- package/dist/types/providerCatalog.generated.d.ts +2 -2
- package/dist/types/providers.d.ts +4 -0
- package/dist/utils/pricing.js +12 -0
- package/dist/utils/tokenLimits.js +20 -1
- package/docs-site/static/search-index.json +128 -100
- 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,
|
|
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,
|
|
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,
|
|
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",
|
package/dist/constants/enums.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
480
|
-
// each shares its environment key with a text provider: a key set only
|
|
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.
|
|
556
|
-
//
|
|
557
|
-
//
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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);
|
package/dist/neurolink.d.ts
CHANGED
|
@@ -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;
|