@juspay/neurolink 13.1.0 → 13.2.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 (33) hide show
  1. package/CHANGELOG.md +3 -7
  2. package/README.md +71 -52
  3. package/dist/browser/neurolink.min.js +374 -374
  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 +14 -5
  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/factories/providerDescriptors.js +13 -9
  14. package/dist/neurolink.d.ts +4 -0
  15. package/dist/neurolink.js +24 -1
  16. package/dist/providers/catalog/charm-hyper.json +6 -10
  17. package/dist/providers/catalog/index.generated.d.ts +1 -1
  18. package/dist/providers/catalog/index.generated.js +3 -0
  19. package/dist/providers/catalog/openai-decisions.json +1 -1
  20. package/dist/providers/catalog/provider-catalog.schema.json +8 -0
  21. package/dist/providers/catalog/scaledown.json +68 -0
  22. package/dist/providers/catalog/schema.js +2 -0
  23. package/dist/providers/configuredDecision.d.ts +5 -0
  24. package/dist/providers/configuredDecision.js +27 -5
  25. package/dist/types/classifierRouter.d.ts +10 -9
  26. package/dist/types/cli.d.ts +2 -2
  27. package/dist/types/providerCatalog.d.ts +22 -0
  28. package/dist/types/providerCatalog.generated.d.ts +2 -2
  29. package/dist/types/providers.d.ts +4 -0
  30. package/dist/utils/pricing.js +12 -0
  31. package/dist/utils/tokenLimits.js +20 -1
  32. package/docs-site/static/search-index.json +92 -67
  33. package/package.json +4 -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
  },
@@ -2303,10 +2303,14 @@ export class CLICommandFactory {
2303
2303
  }
2304
2304
  // Use SDK's provider diagnostic method instead of manual testing
2305
2305
  const results = await sdk.getProviderStatus({ quiet: !!argv.quiet });
2306
+ // A configured provider whose live check the SDK skips comes back as
2307
+ // "not-configured" with configured: true. It is neither working nor
2308
+ // unconfigured, so it is shown as skipped and left out of the count.
2309
+ const isSkipped = (r) => r.configured && r.status === "not-configured";
2306
2310
  if (spinner) {
2307
2311
  const working = results.filter((r) => r.status === "working").length;
2308
- const configured = results.filter((r) => r.configured).length;
2309
- spinner.succeed(`Provider check complete: ${working}/${configured} providers working`);
2312
+ const checked = results.filter((r) => r.configured && !isSkipped(r)).length;
2313
+ spinner.succeed(`Provider check complete: ${working}/${checked} providers working`);
2310
2314
  }
2311
2315
  // Display results
2312
2316
  for (const result of results) {
@@ -2314,11 +2318,16 @@ export class CLICommandFactory {
2314
2318
  ? chalk.green("✅ Working")
2315
2319
  : result.status === "failed"
2316
2320
  ? chalk.red("❌ Failed")
2317
- : chalk.gray("⚪ Not configured");
2321
+ : isSkipped(result)
2322
+ ? chalk.gray("⏭ Skipped")
2323
+ : chalk.gray("⚪ Not configured");
2318
2324
  const time = result.responseTime ? ` (${result.responseTime}ms)` : "";
2319
2325
  const model = result.model ? ` [${result.model}]` : "";
2320
2326
  logger.always(`${result.provider}: ${status}${time}${model}`);
2321
- if (argv.verbose && result.error) {
2327
+ if (isSkipped(result) && result.error) {
2328
+ logger.always(` ${chalk.gray(result.error)}`);
2329
+ }
2330
+ else if (argv.verbose && result.error) {
2322
2331
  logger.always(` Error: ${chalk.red(result.error)}`);
2323
2332
  }
2324
2333
  }
@@ -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";
@@ -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
@@ -1845,6 +1845,10 @@ export declare class NeuroLink {
1845
1845
  /**
1846
1846
  * Get comprehensive status of all AI providers
1847
1847
  * Primary method for provider health checking and diagnostics
1848
+ *
1849
+ * Bedrock and SageMaker are never called live: their entries come back as
1850
+ * `status: "not-configured"` with `configured: true` and the reason in
1851
+ * `error`. Use `testProvider()` to check either one explicitly.
1848
1852
  */
1849
1853
  getProviderStatus(options?: {
1850
1854
  quiet?: boolean;
package/dist/neurolink.js CHANGED
@@ -11870,6 +11870,10 @@ Current user's request: ${currentInput}`;
11870
11870
  /**
11871
11871
  * Get comprehensive status of all AI providers
11872
11872
  * Primary method for provider health checking and diagnostics
11873
+ *
11874
+ * Bedrock and SageMaker are never called live: their entries come back as
11875
+ * `status: "not-configured"` with `configured: true` and the reason in
11876
+ * `error`. Use `testProvider()` to check either one explicitly.
11873
11877
  */
11874
11878
  async getProviderStatus(options) {
11875
11879
  // Track memory and timing for provider status checks
@@ -11912,6 +11916,19 @@ Current user's request: ${currentInput}`;
11912
11916
  responseTime: Date.now() - startTime,
11913
11917
  };
11914
11918
  }
11919
+ // These probes are billable generate() calls signed with ambient AWS
11920
+ // credentials, against models the account may not be allowed to
11921
+ // call, so they are never made here.
11922
+ if (providerName === "bedrock" || providerName === "sagemaker") {
11923
+ return {
11924
+ provider: providerName,
11925
+ status: "not-configured",
11926
+ configured: true,
11927
+ authenticated: false,
11928
+ error: `Live status check is skipped for ${providerName}`,
11929
+ responseTime: Date.now() - startTime,
11930
+ };
11931
+ }
11915
11932
  // Special handling for Ollama
11916
11933
  if (providerName === "ollama") {
11917
11934
  let ollamaAttempt;
@@ -14281,7 +14298,13 @@ Current user's request: ${currentInput}`;
14281
14298
  timeoutMs: options.timeoutMs,
14282
14299
  });
14283
14300
  const answerCount = Object.keys(result.answers).length;
14284
- const cost = calculateCost(providerName, result.model, {
14301
+ // `providerName` is what the caller passed, so it can be an alias
14302
+ // (`jev`, `scaledown-classify`). The pricing table is keyed by the
14303
+ // canonical id and does not read the decide catalog, so an alias
14304
+ // priced the decision at 0.
14305
+ const pricedProvider = PROVIDER_ALIAS_INDEX.get(providerName.toLowerCase()) ??
14306
+ providerName;
14307
+ const cost = calculateCost(pricedProvider, result.model, {
14285
14308
  input: result.usage.inputTokens,
14286
14309
  output: result.usage.outputTokens,
14287
14310
  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