@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
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) {
@@ -14298,7 +14329,13 @@ Current user's request: ${currentInput}`;
14298
14329
  timeoutMs: options.timeoutMs,
14299
14330
  });
14300
14331
  const answerCount = Object.keys(result.answers).length;
14301
- const cost = calculateCost(providerName, result.model, {
14332
+ // `providerName` is what the caller passed, so it can be an alias
14333
+ // (`jev`, `scaledown-classify`). The pricing table is keyed by the
14334
+ // canonical id and does not read the decide catalog, so an alias
14335
+ // priced the decision at 0.
14336
+ const pricedProvider = PROVIDER_ALIAS_INDEX.get(providerName.toLowerCase()) ??
14337
+ providerName;
14338
+ const cost = calculateCost(pricedProvider, result.model, {
14302
14339
  input: result.usage.inputTokens,
14303
14340
  output: result.usage.outputTokens,
14304
14341
  total: result.usage.inputTokens + result.usage.outputTokens,
@@ -89,11 +89,10 @@
89
89
  },
90
90
  "vision": true,
91
91
  "status": "production",
92
- "description": "Moonshot Kimi K3 — vision-capable per the live roster; reasoning effort levels low/high/xhigh (default high). Context window, max output tokens, vision and pricing per GET https://hyper.charm.land/v1/models (unauthenticated, 2026-09-28)."
92
+ "description": "Moonshot Kimi K3 — vision-capable per the live roster; reasoning effort levels low/high/xhigh (default high). Context window, max output tokens, vision and pricing per GET https://hyper.charm.land/v1/models (unauthenticated, 2026-09-28). The 16000 max output tokens is Hyper's own cap for this model: Hyper's List Models docs (https://hyper.charm.land/docs/api/list-models.html) define `max_output_tokens` as the model's max output tokens, and the roster still reported 16000 on 2026-10-18. Moonshot's own API allows more (`max_completion_tokens` defaults to 131072 and can be set up to 1048576, per https://platform.kimi.ai/docs/guide/kimi-k3-quickstart), so the cap belongs to this gateway, not to the model."
93
93
  },
94
94
  "kimi-k2-thinking": {
95
95
  "contextWindow": 262144,
96
- "maxOutputTokens": 26214,
97
96
  "pricingPerMTok": {
98
97
  "input": 0.6,
99
98
  "output": 2.5,
@@ -101,7 +100,7 @@
101
100
  },
102
101
  "vision": false,
103
102
  "status": "production",
104
- "description": "Moonshot Kimi K2 Thinking — reasoning-first Kimi variant; reasoning effort levels low/high/xhigh (default high). Context window, max output tokens, vision and pricing per GET https://hyper.charm.land/v1/models (unauthenticated, 2026-09-28)."
103
+ "description": "Moonshot Kimi K2 Thinking — reasoning-first Kimi variant; reasoning effort levels low/high/xhigh (default high). Context window, max output tokens, vision and pricing per GET https://hyper.charm.land/v1/models (unauthenticated, 2026-09-28). No maxOutputTokens is recorded: the roster reports 26214, which is a fixed fraction of the 262144-token context window and reads like a gateway default rather than a model limit."
105
104
  },
106
105
  "kimi-k2.7-code": {
107
106
  "contextWindow": 256000,
@@ -113,7 +112,7 @@
113
112
  },
114
113
  "vision": true,
115
114
  "status": "production",
116
- "description": "Moonshot Kimi K2.7 Code — code-focused Kimi variant, vision-capable per the live roster; reasoning effort levels low/high/xhigh (default high). Context window, max output tokens, vision and pricing per GET https://hyper.charm.land/v1/models (unauthenticated, 2026-09-28)."
115
+ "description": "Moonshot Kimi K2.7 Code — code-focused Kimi variant, vision-capable per the live roster; reasoning effort levels low/high/xhigh (default high). Context window, max output tokens, vision and pricing per GET https://hyper.charm.land/v1/models (unauthenticated, 2026-09-28). The 16000 max output tokens is Hyper's own cap for this model: Hyper's List Models docs (https://hyper.charm.land/docs/api/list-models.html) define `max_output_tokens` as the model's max output tokens, and the roster still reported 16000 on 2026-10-18. Moonshot's own API defaults `max_tokens` to 32768 (https://platform.kimi.ai/docs/guide/kimi-k2-7-code-quickstart), so the cap belongs to this gateway, not to the model."
117
116
  },
118
117
  "glm-5.3": {
119
118
  "contextWindow": 1000000,
@@ -165,7 +164,6 @@
165
164
  },
166
165
  "minimax-m2.7": {
167
166
  "contextWindow": 262100,
168
- "maxOutputTokens": 6553,
169
167
  "pricingPerMTok": {
170
168
  "input": 0.484,
171
169
  "output": 1.852,
@@ -173,7 +171,7 @@
173
171
  },
174
172
  "vision": false,
175
173
  "status": "production",
176
- "description": "MiniMax M2.7 — prior MiniMax generation. Context window, max output tokens, vision and pricing per GET https://hyper.charm.land/v1/models (unauthenticated, 2026-09-28)."
174
+ "description": "MiniMax M2.7 — prior MiniMax generation. Context window, max output tokens, vision and pricing per GET https://hyper.charm.land/v1/models (unauthenticated, 2026-09-28). No maxOutputTokens is recorded: the roster reports 6553, which is a fixed fraction of the 262100-token context window and reads like a gateway default rather than a model limit."
177
175
  },
178
176
  "inkling": {
179
177
  "contextWindow": 1048576,
@@ -273,7 +271,6 @@
273
271
  },
274
272
  "gpt-oss-120b": {
275
273
  "contextWindow": 131072,
276
- "maxOutputTokens": 13107,
277
274
  "pricingPerMTok": {
278
275
  "input": 0.178,
279
276
  "output": 0.68,
@@ -281,11 +278,10 @@
281
278
  },
282
279
  "vision": false,
283
280
  "status": "production",
284
- "description": "OpenAI's open-weight gpt-oss-120b. Context window, max output tokens, vision and pricing per GET https://hyper.charm.land/v1/models (unauthenticated, 2026-09-28)."
281
+ "description": "OpenAI's open-weight gpt-oss-120b. Context window, max output tokens, vision and pricing per GET https://hyper.charm.land/v1/models (unauthenticated, 2026-09-28). No maxOutputTokens is recorded: the roster reports 13107, which is a fixed fraction of the 131072-token context window and reads like a gateway default rather than a model limit."
285
282
  },
286
283
  "gemma-4-26b-a4b-it": {
287
284
  "contextWindow": 256000,
288
- "maxOutputTokens": 25600,
289
285
  "pricingPerMTok": {
290
286
  "input": 0.098,
291
287
  "output": 0.334,
@@ -293,7 +289,7 @@
293
289
  },
294
290
  "vision": false,
295
291
  "status": "production",
296
- "description": "Google Gemma 4 26B A4B, instruction-tuned. Context window, max output tokens, vision and pricing per GET https://hyper.charm.land/v1/models (unauthenticated, 2026-09-28)."
292
+ "description": "Google Gemma 4 26B A4B, instruction-tuned. Context window, max output tokens, vision and pricing per GET https://hyper.charm.land/v1/models (unauthenticated, 2026-09-28). No maxOutputTokens is recorded: the roster reports 25600, which is a fixed fraction of the 256000-token context window and reads like a gateway default rather than a model limit."
297
293
  }
298
294
  },
299
295
  "topModels": [
@@ -2,4 +2,4 @@ import type { DecideCatalogJson, ProviderCatalogJson } from "../../types/index.j
2
2
  export declare const CATALOG_JSON_ENTRIES: ProviderCatalogJson[];
3
3
  export declare const CATALOG_PROVIDER_IDS: readonly ["a2agent", "abliteration", "above-dev", "ai21", "aiand", "aihubmix", "aionlabs", "ambient", "api-route", "arcee", "atlas-cloud", "avian-io", "baidu-qianfan", "baseten", "bee-heossi", "byteplus-modelark", "bytez", "cerebras", "charm-hyper", "chutes", "cloudflare", "crusoe", "dashscope", "deepinfra", "deepseek", "empiriolabs", "featherless-ai", "fireworks", "friendli", "gmicloud", "gradientai", "groq", "hetzner-inference", "huggingface", "inception-labs", "inceptron", "inco", "inference-net", "io-intelligence", "koscompute", "lemonfox-ai", "lilac", "llmtech", "longcat", "mancer", "meta-model-api", "minimax", "mistral", "moark", "modelscope", "moonshot-ai", "morph", "naver-clova-studio", "nebius", "neuralwatt", "novita", "ovhcloud", "parasail", "pareto-inference", "perplexity", "poolside", "prime-intellect", "reka", "runinfra", "sakana-ai", "sambanova", "sarvam", "scaleway", "siliconflow", "stackit", "stepfun", "subconscious", "synthetic", "telnyx", "thinking-machines", "tinfoil", "together-ai", "umans-ai", "upstage", "venice-ai", "vispark", "vultr-inference", "wafer", "wandb-inference", "xai", "xiaomi-mimo", "z-ai"];
4
4
  export declare const DECIDE_CATALOG_JSON_ENTRIES: DecideCatalogJson[];
5
- export declare const DECIDE_CATALOG_PROVIDER_IDS: readonly ["laya", "openai-decisions", "subq", "xor"];
5
+ export declare const DECIDE_CATALOG_PROVIDER_IDS: readonly ["laya", "openai-decisions", "scaledown", "subq", "xor"];
@@ -69,6 +69,7 @@ import runinfraJson from "./runinfra.json" with { type: "json" };
69
69
  import sakanaAiJson from "./sakana-ai.json" with { type: "json" };
70
70
  import sambanovaJson from "./sambanova.json" with { type: "json" };
71
71
  import sarvamJson from "./sarvam.json" with { type: "json" };
72
+ import scaledownJson from "./scaledown.json" with { type: "json" };
72
73
  import scalewayJson from "./scaleway.json" with { type: "json" };
73
74
  import siliconflowJson from "./siliconflow.json" with { type: "json" };
74
75
  import stackitJson from "./stackit.json" with { type: "json" };
@@ -280,12 +281,14 @@ export const CATALOG_PROVIDER_IDS = [
280
281
  export const DECIDE_CATALOG_JSON_ENTRIES = [
281
282
  layaJson,
282
283
  openaiDecisionsJson,
284
+ scaledownJson,
283
285
  subqJson,
284
286
  xorJson,
285
287
  ].map(asDecideEntry);
286
288
  export const DECIDE_CATALOG_PROVIDER_IDS = [
287
289
  "laya",
288
290
  "openai-decisions",
291
+ "scaledown",
289
292
  "subq",
290
293
  "xor",
291
294
  ];
@@ -60,7 +60,7 @@
60
60
  "instructions": [
61
61
  "1. Create an API key at https://platform.openai.com/api-keys",
62
62
  "2. Set {apiKeyEnvVar}; it is shared with the openai chat provider",
63
- "3. With no other decision provider configured, {apiKeyEnvVar} alone makes openai-decisions the default decision provider (the last resort, after every other one, as PERPLEXITY_API_KEY does for perplexity-decider), unless OPENAI_BASE_URL, OPENAI_API_BASE or credentials.openai.baseURL names a host other than https://api.openai.com: the key is then taken to be a gateway's and is not used by default. To use it while another provider is configured, name it in a call (provider: \"openai-decisions\" or --provider openai-decisions), but only if the key is OpenAI's, because a named call sends the environment key to api.openai.com. Behind a gateway, route it on purpose with OPENAI_DECISIONS_BASE_URL or credentials.openaiDecisions, or do not use it",
63
+ "3. With no other decision provider configured, {apiKeyEnvVar} alone makes openai-decisions the default decision provider (a last resort, after every other one except ScaleDown, as PERPLEXITY_API_KEY does for perplexity-decider), unless OPENAI_BASE_URL, OPENAI_API_BASE or credentials.openai.baseURL names a host other than https://api.openai.com: the key is then taken to be a gateway's and is not used by default. To use it while another provider is configured, name it in a call (provider: \"openai-decisions\" or --provider openai-decisions), but only if the key is OpenAI's, because a named call sends the environment key to api.openai.com. Behind a gateway, route it on purpose with OPENAI_DECISIONS_BASE_URL or credentials.openaiDecisions, or do not use it",
64
64
  "4. Optional: OPENAI_DECISIONS_BASE_URL (an origin) and OPENAI_DECISIONS_MODEL (default gpt-6-luna)"
65
65
  ]
66
66
  },
@@ -142,6 +142,14 @@
142
142
  }
143
143
  }
144
144
  },
145
+ "authHeader": {
146
+ "enum": ["bearer", "x-api-key"],
147
+ "description": "How the API key is sent. bearer, the default when omitted, sends Authorization: Bearer <key>. x-api-key sends x-api-key: <key> and no Authorization header at all. This is the decide request's own setting; quirks.authHeaderStyle belongs to the chat path and is not read here."
148
+ },
149
+ "stateEncoding": {
150
+ "enum": ["text"],
151
+ "description": "text makes the state that goes out always a string: a string is sent unchanged (not trimmed), any other value is sent as its JSON text, and a state that is blank after trimming is refused before any request, as an invalid_request. Omitted means the state goes out as given."
152
+ },
145
153
  "media": {
146
154
  "type": "object",
147
155
  "additionalProperties": false,
@@ -0,0 +1,68 @@
1
+ {
2
+ "$schema": "./provider-catalog.schema.json",
3
+ "id": "scaledown",
4
+ "displayName": "ScaleDown",
5
+ "enumTypeName": "ScaleDownModels",
6
+ "aliases": ["scaledown-classify"],
7
+ "inferenceKinds": ["decide"],
8
+ "wire": { "baseURL": "https://api.scaledown.xyz" },
9
+ "models": {
10
+ "default": "classify-1",
11
+ "topModels": ["classify-1", "classify"],
12
+ "catalog": {
13
+ "classify-1": {
14
+ "contextWindow": 128000,
15
+ "maxOutputTokens": 1,
16
+ "vision": false,
17
+ "status": "preview",
18
+ "description": "Recommended - ScaleDown Classify 1, decision model for boolean, choice and score questions; context window and output limit not established"
19
+ },
20
+ "classify": {
21
+ "vision": false,
22
+ "status": "preview",
23
+ "description": "ScaleDown's name for its latest Classify model; whether it moves to a newer model is not established"
24
+ }
25
+ }
26
+ },
27
+ "decide": {
28
+ "dialect": "system-one",
29
+ "order": 8,
30
+ "baseURLNormalize": ["strip-trailing-slash"],
31
+ "endpoint": "{baseURL}/v1/scaledown",
32
+ "model": { "placement": "body" },
33
+ "authHeader": "x-api-key",
34
+ "stateEncoding": "text",
35
+ "errors": {
36
+ "rules": [
37
+ { "status": 401, "kind": "authentication" },
38
+ { "status": [402, 422], "kind": "invalid_request" },
39
+ { "status": 429, "kind": "rate_limit" },
40
+ { "status": 502, "kind": "server" },
41
+ { "status": 504, "kind": "timeout" },
42
+ { "statusMin": 500, "kind": "server" }
43
+ ],
44
+ "default": "invalid_request",
45
+ "bodyShapes": [{ "path": "detail.message" }, { "path": "detail" }]
46
+ }
47
+ },
48
+ "setup": {
49
+ "url": "https://docs.scaledown.ai",
50
+ "apiKeyFormat": null,
51
+ "billingPolicy": "promotional-credit",
52
+ "description": "API key from ScaleDown (see its documentation). Sign-up gives a free allowance; whether it expires is not established",
53
+ "instructions": [
54
+ "1. Create an account and an API key, following https://docs.scaledown.ai",
55
+ "2. Set {apiKeyEnvVar} in your .env file",
56
+ "3. ScaleDown's documentation says the key goes in an x-api-key header rather than Authorization: Bearer, and NeuroLink sends it that way",
57
+ "4. ScaleDown's documentation says a request made with no credit left returns HTTP 402"
58
+ ]
59
+ },
60
+ "evidence": {
61
+ "rosterVerified": {
62
+ "date": "2026-10-15",
63
+ "method": "Checked by the marketing session, not in this change: ScaleDown's docs at docs.scaledown.ai were read and probes were made against api.scaledown.xyz. No request was made to ScaleDown in this change"
64
+ },
65
+ "liveMatrix": null,
66
+ "addedInPR": "https://github.com/juspay/neurolink/pull/2005"
67
+ }
68
+ }
@@ -600,6 +600,8 @@ const decideBlockSchema = z.strictObject({
600
600
  placement: z.enum(["body", "body-unless-alias"]),
601
601
  alias: z.string().min(1).optional(),
602
602
  }),
603
+ authHeader: z.enum(["bearer", "x-api-key"]).optional(),
604
+ stateEncoding: z.enum(["text"]).optional(),
603
605
  media: decideMediaSchema.optional(),
604
606
  ambientKeyGuard: ambientKeyGuardSchema.optional(),
605
607
  requestIdHeaders: z.array(decideHeaderNameSchema).optional(),
@@ -15,6 +15,11 @@ export declare class ConfiguredDecisionProvider extends SystemOneDecisionProvide
15
15
  protected missingConfigMessage(): string | undefined;
16
16
  protected decisionEndpoint(): string;
17
17
  protected decisionHeaders(): Record<string, string>;
18
+ /**
19
+ * The state the dialect is given. Only an entry that sets `stateEncoding`
20
+ * changes it, so every other entry sends exactly what the caller passed.
21
+ */
22
+ private encodeState;
18
23
  protected buildDecisionBody(state: DecisionState, questions: Record<string, Record<string, unknown>>, model: string, media?: DecisionPreparedMedia): Record<string, unknown>;
19
24
  protected parseDecisionError(status: number, payload: unknown, requestId: string | undefined): DecisionError;
20
25
  protected readRequestId(headers: Headers): string | undefined;
@@ -10,6 +10,9 @@ import { readPath } from "./decide/paths.js";
10
10
  import { fillEndpointTemplate } from "./decide/template.js";
11
11
  import { SystemOneDecisionProvider } from "./systemOneDecision.js";
12
12
  const readText = (value) => typeof value === "string" ? value.trim() : "";
13
+ // JSON.stringify answers undefined for a value JSON cannot hold, such as a
14
+ // state a JavaScript caller left out, although its own return type says string.
15
+ const stateText = (state) => typeof state === "string" ? state : JSON.stringify(state);
13
16
  /** A `decide` provider described entirely by its catalog entry. */
14
17
  export class ConfiguredDecisionProvider extends SystemOneDecisionProvider {
15
18
  entry;
@@ -68,15 +71,34 @@ export class ConfiguredDecisionProvider extends SystemOneDecisionProvider {
68
71
  return this.endpoint;
69
72
  }
70
73
  decisionHeaders() {
71
- return {
72
- Authorization: `Bearer ${this.apiKey}`,
73
- "Content-Type": "application/json",
74
- };
74
+ const credential = this.entry.decide.authHeader === "x-api-key"
75
+ ? { "x-api-key": this.apiKey }
76
+ : { Authorization: `Bearer ${this.apiKey}` };
77
+ return { ...credential, "Content-Type": "application/json" };
78
+ }
79
+ /**
80
+ * The state the dialect is given. Only an entry that sets `stateEncoding`
81
+ * changes it, so every other entry sends exactly what the caller passed.
82
+ */
83
+ encodeState(state) {
84
+ if (this.entry.decide.stateEncoding !== "text") {
85
+ return state;
86
+ }
87
+ const text = stateText(state);
88
+ if (text === undefined || text.trim() === "") {
89
+ throw this.decisionError({
90
+ kind: "invalid_request",
91
+ // Never the state itself, which can hold anything the caller was sent.
92
+ message: `${this.vendorLabel()} needs non-blank state text; the state was empty or only whitespace.`,
93
+ retryable: false,
94
+ });
95
+ }
96
+ return text;
75
97
  }
76
98
  buildDecisionBody(state, questions, model, media) {
77
99
  const { placement, alias } = this.entry.decide.model;
78
100
  return this.dialect().buildBody({
79
- state,
101
+ state: this.encodeState(state),
80
102
  questions,
81
103
  model: placement === "body-unless-alias" && model === alias
82
104
  ? undefined
@@ -24,16 +24,16 @@ export type ClassifierDifficulty = "trivial" | "simple" | "moderate" | "hard" |
24
24
  * - `heuristic` — keyword/length scoring. Deterministic, zero latency.
25
25
  * - `llm` — a cheap classifier model via the injected `generate`.
26
26
  * - `jev` — a decision model, whichever decision provider is configured
27
- * (TypeSafe's Jev, Laya, XOR, SubQ, Perplexity, Cloudflare Clef or OpenAI
28
- * Decisions); one round trip that returns the confidence the provider
27
+ * (TypeSafe's Jev, Laya, XOR, SubQ, Perplexity, Cloudflare Clef, OpenAI
28
+ * Decisions or ScaleDown); one round trip that returns the confidence the provider
29
29
  * reports (*calibrated* on TypeSafe's Jev, ~400ms there) rather than a
30
30
  * self-reported one.
31
31
  * - `auto` — `jev` when a decision provider is configured, in the environment
32
32
  * or in SDK credentials (`TYPESAFE_API_KEY`, `LAYA_API_KEY` with
33
33
  * `LAYA_BASE_URL`, `XOR_API_KEY` with `XOR_BASE_URL`, `SUBQ_API_KEY`, `PERPLEXITY_API_KEY`,
34
- * `CLOUDFLARE_API_KEY` with `CLOUDFLARE_ACCOUNT_ID`, or `OPENAI_API_KEY`,
34
+ * `CLOUDFLARE_API_KEY` with `CLOUDFLARE_ACCOUNT_ID`, `OPENAI_API_KEY`,
35
35
  * which does not count while `OPENAI_BASE_URL`, `OPENAI_API_BASE` or `credentials.openai.baseURL` names a
36
- * host other than `https://api.openai.com`), otherwise `heuristic`.
36
+ * host other than `https://api.openai.com`, or `SCALEDOWN_API_KEY`), otherwise `heuristic`.
37
37
  */
38
38
  export type ClassifierStrategyKind = "heuristic" | "llm" | "jev" | "auto";
39
39
  /**
@@ -195,15 +195,16 @@ export type ClassifierRouterConfig = {
195
195
  * decision provider is configured, in the environment or in SDK credentials
196
196
  * (`TYPESAFE_API_KEY`, `LAYA_API_KEY` with `LAYA_BASE_URL`,
197
197
  * `XOR_API_KEY` with `XOR_BASE_URL`, `SUBQ_API_KEY`, `PERPLEXITY_API_KEY`,
198
- * `CLOUDFLARE_API_KEY` with `CLOUDFLARE_ACCOUNT_ID`, or `OPENAI_API_KEY`) and
199
- * "heuristic" otherwise, so configuring one upgrades routing without any code
198
+ * `CLOUDFLARE_API_KEY` with `CLOUDFLARE_ACCOUNT_ID`, `OPENAI_API_KEY` or
199
+ * `SCALEDOWN_API_KEY`) and "heuristic" otherwise, so configuring one upgrades routing without any code
200
200
  * change. Behaviour for a caller with none of these is unchanged. Three of them
201
201
  * are shared with a text provider and count here too: `PERPLEXITY_API_KEY`
202
202
  * (Perplexity's), the Cloudflare pair (Workers AI's) and `OPENAI_API_KEY` (the
203
203
  * OpenAI chat provider's). A caller whose only key is `OPENAI_API_KEY` now
204
- * resolves to "jev", answered by OpenAI Decisions as the last-resort decision
205
- * provider; each classification is then a paid call that still falls back to
206
- * the heuristic on any failure. The exception is a key that sits beside an
204
+ * resolves to "jev", answered by OpenAI Decisions (the default decision
205
+ * provider whenever none ahead of it in the order is configured); each
206
+ * classification is then a paid call that still falls back to the heuristic
207
+ * on any failure. The exception is a key that sits beside an
207
208
  * `OPENAI_BASE_URL`, `OPENAI_API_BASE` or `credentials.openai.baseURL` naming a host other than
208
209
  * `https://api.openai.com`: that key is a gateway's and does not count.
209
210
  */
@@ -1770,9 +1770,9 @@ export type CliClassifierRouterFlags = {
1770
1770
  * (--classifier-strategy). "auto" resolves to "jev" when a decision
1771
1771
  * provider is configured (TYPESAFE_API_KEY, LAYA_API_KEY with
1772
1772
  * LAYA_BASE_URL, XOR_API_KEY with XOR_BASE_URL, SUBQ_API_KEY, PERPLEXITY_API_KEY,
1773
- * CLOUDFLARE_API_KEY with CLOUDFLARE_ACCOUNT_ID, or OPENAI_API_KEY, which does
1773
+ * CLOUDFLARE_API_KEY with CLOUDFLARE_ACCOUNT_ID, OPENAI_API_KEY, which does
1774
1774
  * not count beside an OPENAI_BASE_URL, OPENAI_API_BASE or credentials.openai.baseURL naming a host other
1775
- * than https://api.openai.com) and "heuristic" otherwise.
1775
+ * than https://api.openai.com, or SCALEDOWN_API_KEY) and "heuristic" otherwise.
1776
1776
  */
1777
1777
  classifierStrategy?: string;
1778
1778
  /**
@@ -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.
@@ -25,6 +25,14 @@ export type CatalogTimeouts = {
25
25
  };
26
26
  export type CatalogModelSpec = {
27
27
  contextWindow?: number;
28
+ /**
29
+ * The model's own output-token limit. Enforced at runtime: for a provider
30
+ * with no PROVIDER_MAX_TOKENS row, getSafeMaxTokens clamps a larger
31
+ * requested maxTokens to this value. Record only a number the vendor states
32
+ * as a hard limit; omit it when the source is a roster field that looks
33
+ * derived from the context window, because an omitted value leaves the
34
+ * model uncapped while a wrong one silently truncates callers.
35
+ */
28
36
  maxOutputTokens?: number;
29
37
  pricingPerMTok?: CatalogPricingPerMTok;
30
38
  vision: boolean;
@@ -314,6 +322,20 @@ export type DecideBlock = {
314
322
  /** Request URL; `{baseURL}` is the only placeholder. */
315
323
  endpoint: string;
316
324
  model: DecideModel;
325
+ /**
326
+ * How the API key is sent. `bearer`, the default when omitted, sends
327
+ * `Authorization: Bearer <key>`. `x-api-key` sends `x-api-key: <key>` and no
328
+ * `Authorization` header at all. This is the decide request's own setting;
329
+ * `quirks.authHeaderStyle` belongs to the chat path and is not read here.
330
+ */
331
+ authHeader?: "bearer" | "x-api-key";
332
+ /**
333
+ * `text` makes the state that goes out always a string: a string is sent
334
+ * unchanged (not trimmed), any other value is sent as its JSON text, and a
335
+ * state that is blank after trimming is refused before any request, as an
336
+ * `invalid_request`. Omitted means the state goes out as given.
337
+ */
338
+ stateEncoding?: "text";
317
339
  /** Body keys for images and video, after `model`, `state` and `questions`. Omitted means the body never carries media. */
318
340
  media?: DecideMedia;
319
341
  /**
@@ -1,2 +1,2 @@
1
- export type CatalogProviderName = "a2agent" | "abliteration" | "above-dev" | "ai21" | "aiand" | "aihubmix" | "aionlabs" | "ambient" | "api-route" | "arcee" | "atlas-cloud" | "avian-io" | "baidu-qianfan" | "baseten" | "bee-heossi" | "byteplus-modelark" | "bytez" | "cerebras" | "charm-hyper" | "chutes" | "cloudflare" | "crusoe" | "dashscope" | "deepinfra" | "deepseek" | "empiriolabs" | "featherless-ai" | "fireworks" | "friendli" | "gmicloud" | "gradientai" | "groq" | "hetzner-inference" | "huggingface" | "inception-labs" | "inceptron" | "inco" | "inference-net" | "io-intelligence" | "koscompute" | "laya" | "lemonfox-ai" | "lilac" | "llmtech" | "longcat" | "mancer" | "meta-model-api" | "minimax" | "mistral" | "moark" | "modelscope" | "moonshot-ai" | "morph" | "naver-clova-studio" | "nebius" | "neuralwatt" | "novita" | "openai-decisions" | "ovhcloud" | "parasail" | "pareto-inference" | "perplexity" | "poolside" | "prime-intellect" | "reka" | "runinfra" | "sakana-ai" | "sambanova" | "sarvam" | "scaleway" | "siliconflow" | "stackit" | "stepfun" | "subconscious" | "subq" | "synthetic" | "telnyx" | "thinking-machines" | "tinfoil" | "together-ai" | "umans-ai" | "upstage" | "venice-ai" | "vispark" | "vultr-inference" | "wafer" | "wandb-inference" | "xai" | "xiaomi-mimo" | "xor" | "z-ai";
2
- export type CatalogCredentialKey = "a2agent" | "abliteration" | "aboveDev" | "ai21" | "aiand" | "aihubmix" | "aionlabs" | "ambient" | "apiRoute" | "arcee" | "atlasCloud" | "avianIo" | "baiduQianfan" | "baseten" | "beeHeossi" | "byteplusModelark" | "bytez" | "cerebras" | "charmHyper" | "chutes" | "cloudflare" | "crusoe" | "dashscope" | "deepinfra" | "deepseek" | "empiriolabs" | "featherlessAi" | "fireworks" | "friendli" | "gmicloud" | "gradientai" | "groq" | "hetznerInference" | "huggingFace" | "inceptionLabs" | "inceptron" | "inco" | "inferenceNet" | "ioIntelligence" | "koscompute" | "laya" | "lemonfoxAi" | "lilac" | "llmtech" | "longcat" | "mancer" | "metaModelApi" | "minimax" | "mistral" | "moark" | "modelscope" | "moonshotAi" | "morph" | "naverClovaStudio" | "nebius" | "neuralwatt" | "novita" | "openaiDecisions" | "ovhcloud" | "parasail" | "paretoInference" | "perplexity" | "poolside" | "primeIntellect" | "reka" | "runinfra" | "sakanaAi" | "sambanova" | "sarvam" | "scaleway" | "siliconflow" | "stackit" | "stepfun" | "subconscious" | "subq" | "synthetic" | "telnyx" | "thinkingMachines" | "tinfoil" | "together" | "umansAi" | "upstage" | "veniceAi" | "vispark" | "vultrInference" | "wafer" | "wandbInference" | "xai" | "xiaomiMimo" | "xor" | "zAi";
1
+ export type CatalogProviderName = "a2agent" | "abliteration" | "above-dev" | "ai21" | "aiand" | "aihubmix" | "aionlabs" | "ambient" | "api-route" | "arcee" | "atlas-cloud" | "avian-io" | "baidu-qianfan" | "baseten" | "bee-heossi" | "byteplus-modelark" | "bytez" | "cerebras" | "charm-hyper" | "chutes" | "cloudflare" | "crusoe" | "dashscope" | "deepinfra" | "deepseek" | "empiriolabs" | "featherless-ai" | "fireworks" | "friendli" | "gmicloud" | "gradientai" | "groq" | "hetzner-inference" | "huggingface" | "inception-labs" | "inceptron" | "inco" | "inference-net" | "io-intelligence" | "koscompute" | "laya" | "lemonfox-ai" | "lilac" | "llmtech" | "longcat" | "mancer" | "meta-model-api" | "minimax" | "mistral" | "moark" | "modelscope" | "moonshot-ai" | "morph" | "naver-clova-studio" | "nebius" | "neuralwatt" | "novita" | "openai-decisions" | "ovhcloud" | "parasail" | "pareto-inference" | "perplexity" | "poolside" | "prime-intellect" | "reka" | "runinfra" | "sakana-ai" | "sambanova" | "sarvam" | "scaledown" | "scaleway" | "siliconflow" | "stackit" | "stepfun" | "subconscious" | "subq" | "synthetic" | "telnyx" | "thinking-machines" | "tinfoil" | "together-ai" | "umans-ai" | "upstage" | "venice-ai" | "vispark" | "vultr-inference" | "wafer" | "wandb-inference" | "xai" | "xiaomi-mimo" | "xor" | "z-ai";
2
+ export type CatalogCredentialKey = "a2agent" | "abliteration" | "aboveDev" | "ai21" | "aiand" | "aihubmix" | "aionlabs" | "ambient" | "apiRoute" | "arcee" | "atlasCloud" | "avianIo" | "baiduQianfan" | "baseten" | "beeHeossi" | "byteplusModelark" | "bytez" | "cerebras" | "charmHyper" | "chutes" | "cloudflare" | "crusoe" | "dashscope" | "deepinfra" | "deepseek" | "empiriolabs" | "featherlessAi" | "fireworks" | "friendli" | "gmicloud" | "gradientai" | "groq" | "hetznerInference" | "huggingFace" | "inceptionLabs" | "inceptron" | "inco" | "inferenceNet" | "ioIntelligence" | "koscompute" | "laya" | "lemonfoxAi" | "lilac" | "llmtech" | "longcat" | "mancer" | "metaModelApi" | "minimax" | "mistral" | "moark" | "modelscope" | "moonshotAi" | "morph" | "naverClovaStudio" | "nebius" | "neuralwatt" | "novita" | "openaiDecisions" | "ovhcloud" | "parasail" | "paretoInference" | "perplexity" | "poolside" | "primeIntellect" | "reka" | "runinfra" | "sakanaAi" | "sambanova" | "sarvam" | "scaledown" | "scaleway" | "siliconflow" | "stackit" | "stepfun" | "subconscious" | "subq" | "synthetic" | "telnyx" | "thinkingMachines" | "tinfoil" | "together" | "umansAi" | "upstage" | "veniceAi" | "vispark" | "vultrInference" | "wafer" | "wandbInference" | "xai" | "xiaomiMimo" | "xor" | "zAi";
@@ -469,6 +469,10 @@ export type NeurolinkCredentials = {
469
469
  apiKey?: string;
470
470
  baseURL?: string;
471
471
  };
472
+ scaledown?: {
473
+ apiKey?: string;
474
+ baseURL?: string;
475
+ };
472
476
  scaleway?: {
473
477
  apiKey?: string;
474
478
  baseURL?: string;
@@ -812,6 +812,18 @@ const PRICING = {
812
812
  // name, so a `"gpt-6-luna"` key would price that chat model at this rate.
813
813
  _default: { input: 0.1 / 1_000_000, output: 0 },
814
814
  },
815
+ scaledown: {
816
+ // $0.05 per million input tokens, output free: `output: 0` is a price, not
817
+ // a token count, so do not "correct" it from `usage.output_tokens`. A list
818
+ // price from ScaleDown's launch post and from `usage.cost` in a response,
819
+ // both as reported to this project on 2026-10-15; this project has not
820
+ // priced a call itself. Per ScaleDown's docs each choice option is a
821
+ // separate internal call, so input tokens grow with options times state
822
+ // length (not measured here). Only `_default`: LiteLLM, OpenRouter and
823
+ // openai-compatible search every table by model name, so a `"classify"` key
824
+ // would price an unrelated model at this rate.
825
+ _default: { input: 0.05 / 1_000_000, output: 0 },
826
+ },
815
827
  "cloudflare-clef": {
816
828
  // Workers AI bills Clef per million INPUT tokens ($0.24 for clef, $0.09 for
817
829
  // clef-flash); no output price is listed. `output: 0` is a price, not a token
@@ -2,7 +2,7 @@
2
2
  * Provider-specific token limit utilities
3
3
  * Provides safe maxTokens values based on provider and model capabilities
4
4
  */
5
- import { getRuntimeOutputCeiling } from "../constants/contextWindows.js";
5
+ import { getCatalogOutputCeiling, getRuntimeOutputCeiling, } from "../constants/contextWindows.js";
6
6
  import { bedrockModelIdForLookup, stripBedrockGeoPrefix, } from "../constants/bedrockIdentifiers.js";
7
7
  import { PROVIDER_MAX_TOKENS } from "../core/constants.js";
8
8
  import { logger } from "./logger.js";
@@ -68,6 +68,25 @@ export function getSafeMaxTokens(provider, model, requestedMaxTokens) {
68
68
  // Get provider-specific limits
69
69
  const providerLimits = PROVIDER_MAX_TOKENS[canonicalProvider];
70
70
  if (!providerLimits) {
71
+ // A JSON-catalog provider has no PROVIDER_MAX_TOKENS row, but its catalog
72
+ // entry can declare the model's own output ceiling. Only that per-model
73
+ // value counts: the provider-wide default is a 4096 placeholder for many
74
+ // providers. An over-large request is clamped. Unlike the runtime ceiling
75
+ // above, this ceiling is not injected as a default: an unset request stays
76
+ // unset, because a ceiling-sized default would shrink usable input on
77
+ // total-context backends.
78
+ const catalogCeiling = getCatalogOutputCeiling(provider, model);
79
+ if (catalogCeiling !== undefined) {
80
+ if (requestedMaxTokens === undefined || requestedMaxTokens === null) {
81
+ return undefined;
82
+ }
83
+ if (requestedMaxTokens > catalogCeiling) {
84
+ logger.warn(`Requested maxTokens ${requestedMaxTokens} exceeds the catalog output ceiling ` +
85
+ `of ${catalogCeiling} for ${provider}/${model}. Using ${catalogCeiling} instead.`);
86
+ return catalogCeiling;
87
+ }
88
+ return requestedMaxTokens;
89
+ }
71
90
  logger.warn(`Unknown provider ${provider}, no token limits enforced`);
72
91
  // Explicit undefined/null check so a caller-supplied 0 is preserved
73
92
  // (truthy guard would silently drop 0 here as it does in the restricted branch).