@juspay/neurolink 12.37.4 → 12.38.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 (49) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/README.md +60 -50
  3. package/dist/browser/neurolink.min.js +423 -423
  4. package/dist/cli/commands/decide.d.ts +2 -2
  5. package/dist/cli/commands/decide.js +22 -3
  6. package/dist/cli/commands/setup.js +2 -1
  7. package/dist/cli/factories/commandFactory.js +1 -1
  8. package/dist/constants/enums.d.ts +9 -0
  9. package/dist/constants/enums.js +10 -0
  10. package/dist/core/baseProvider.d.ts +10 -1
  11. package/dist/core/baseProvider.js +35 -7
  12. package/dist/factories/providerDescriptors.d.ts +2 -0
  13. package/dist/factories/providerDescriptors.js +52 -6
  14. package/dist/factories/providerRegistry.js +9 -1
  15. package/dist/models/manifestRegistry.js +2 -0
  16. package/dist/models/manifests/anthropic.js +2 -2
  17. package/dist/models/manifests/xor.d.ts +12 -0
  18. package/dist/models/manifests/xor.js +30 -0
  19. package/dist/models/modelRegistry.d.ts +12 -1
  20. package/dist/models/modelRegistry.js +57 -5
  21. package/dist/neurolink.js +6 -0
  22. package/dist/providers/amazonSagemaker.js +1 -1
  23. package/dist/providers/anthropic/client.js +29 -5
  24. package/dist/providers/anthropic/structuredOutput.d.ts +4 -1
  25. package/dist/providers/anthropic/structuredOutput.js +13 -5
  26. package/dist/providers/catalog/cerebras.json +7 -8
  27. package/dist/providers/catalog/fireworks.json +10 -12
  28. package/dist/providers/googleVertex/client.d.ts +3 -3
  29. package/dist/providers/googleVertex/client.js +79 -26
  30. package/dist/providers/laya.js +1 -19
  31. package/dist/providers/openaiChatCompletionsBase.js +1 -1
  32. package/dist/providers/systemOneDecision.d.ts +21 -2
  33. package/dist/providers/systemOneDecision.js +64 -4
  34. package/dist/providers/xor.d.ts +48 -0
  35. package/dist/providers/xor.js +192 -0
  36. package/dist/proxy/vertexAnthropicFallback.d.ts +1 -12
  37. package/dist/proxy/vertexAnthropicFallback.js +43 -2
  38. package/dist/types/classifierRouter.d.ts +5 -4
  39. package/dist/types/cli.d.ts +6 -2
  40. package/dist/types/decision.d.ts +60 -1
  41. package/dist/types/model.d.ts +6 -0
  42. package/dist/types/providers.d.ts +10 -0
  43. package/dist/utils/decisionMedia.d.ts +9 -0
  44. package/dist/utils/decisionMedia.js +160 -0
  45. package/dist/utils/modelChoices.js +9 -1
  46. package/dist/utils/providerConfig.d.ts +5 -0
  47. package/dist/utils/providerConfig.js +17 -0
  48. package/docs-site/static/search-index.json +63 -46
  49. package/package.json +2 -1
@@ -8,8 +8,8 @@
8
8
  * for the full vocabulary.
9
9
  *
10
10
  * Credentials are env-only here, exactly as every other CLI command: the
11
- * decision provider reads its own settings (TYPESAFE_API_KEY, or LAYA_API_KEY
12
- * and LAYA_BASE_URL) itself.
11
+ * decision provider reads its own settings (TYPESAFE_API_KEY, LAYA_API_KEY and
12
+ * LAYA_BASE_URL, or XOR_API_KEY and XOR_BASE_URL) itself.
13
13
  */
14
14
  import type { CommandModule } from "yargs";
15
15
  import type { CliDecideArgs } from "../../types/index.js";
@@ -8,8 +8,8 @@
8
8
  * for the full vocabulary.
9
9
  *
10
10
  * Credentials are env-only here, exactly as every other CLI command: the
11
- * decision provider reads its own settings (TYPESAFE_API_KEY, or LAYA_API_KEY
12
- * and LAYA_BASE_URL) itself.
11
+ * decision provider reads its own settings (TYPESAFE_API_KEY, LAYA_API_KEY and
12
+ * LAYA_BASE_URL, or XOR_API_KEY and XOR_BASE_URL) itself.
13
13
  */
14
14
  import chalk from "chalk";
15
15
  import ora from "ora";
@@ -186,6 +186,17 @@ export const decideCommand = {
186
186
  .option("model", {
187
187
  type: "string",
188
188
  describe: "Overrides the provider's configured model for this call",
189
+ })
190
+ .option("image", {
191
+ type: "string",
192
+ array: true,
193
+ nargs: 1,
194
+ describe: "Image for the model to read: a file path or a data: URL. Repeat for several (XOR takes up to 8)",
195
+ })
196
+ .option("video", {
197
+ type: "string",
198
+ nargs: 1,
199
+ describe: "One video for the model to read: a file path or a data: URL",
189
200
  })
190
201
  .option("timeout", {
191
202
  type: "number",
@@ -205,7 +216,8 @@ export const decideCommand = {
205
216
  describe: "Enable debug logging (written to stderr when --format json is used)",
206
217
  })
207
218
  .example('$0 decide "Refund request for a damaged item" --questions \'{"urgent":{"type":"boolean","instructions":"Is this urgent?"}}\'', "Ask a single yes/no question")
208
- .example("$0 decide --state-file ticket.json --questions-file questions.json --format json", "Read state and questions from files, emit raw JSON"),
219
+ .example("$0 decide --state-file ticket.json --questions-file questions.json --format json", "Read state and questions from files, emit raw JSON")
220
+ .example('$0 decide "What color is this?" --provider xor --image ./photo.png --questions \'{"color":{"type":"choice","instructions":"What color is the image?","criteria":{"red":"red","blue":"blue"}}}\'', "Ask about an image (XOR only)"),
209
221
  handler: async (argv) => {
210
222
  const outputFormat = argv.format ?? "text";
211
223
  // --- Validate before any provider work ---------------------------------
@@ -235,6 +247,11 @@ export const decideCommand = {
235
247
  logger.error(chalk.red("Error: --timeout must be a positive number of milliseconds."));
236
248
  process.exit(1);
237
249
  }
250
+ const videos = [].concat(argv.video ?? []);
251
+ if (videos.length > 1) {
252
+ logger.error(chalk.red("Error: pass at most one --video."));
253
+ process.exit(1);
254
+ }
238
255
  if (!isDecisionQuestionMap(parsedQuestions)) {
239
256
  logger.error(chalk.red('Error: questions must be a non-empty object of { type: "boolean" | "choice" | "score", ... }.'));
240
257
  process.exit(1);
@@ -261,6 +278,8 @@ export const decideCommand = {
261
278
  provider: argv.provider,
262
279
  model: argv.model,
263
280
  timeoutMs: argv.timeout,
281
+ ...(argv.image && argv.image.length > 0 ? { images: argv.image } : {}),
282
+ ...(videos.length === 1 ? { video: videos[0] } : {}),
264
283
  });
265
284
  spinner?.succeed("Decision complete");
266
285
  if (outputFormat === "json") {
@@ -20,7 +20,7 @@ import { handleGCPSetup } from "./setup-gcp.js";
20
20
  import { handleHuggingFaceSetup } from "./setup-huggingface.js";
21
21
  import { handleMistralSetup } from "./setup-mistral.js";
22
22
  import { PROVIDER_DESCRIPTORS_BY_NAME } from "../../factories/providerDescriptors.js";
23
- import { createCohereConfig, createDeepSeekConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createVoyageConfig, createTypeSafeConfig, createLayaConfig, satisfiesFallbacks, } from "../../utils/providerConfig.js";
23
+ import { createCohereConfig, createDeepSeekConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createVoyageConfig, createTypeSafeConfig, createLayaConfig, createXorConfig, satisfiesFallbacks, } from "../../utils/providerConfig.js";
24
24
  import { getCatalogJsonEntries, buildCatalogConfigOptions, } from "../../providers/catalog/loader.js";
25
25
  // Provider information database
26
26
  const PROVIDERS = [
@@ -170,6 +170,7 @@ export const EXTRA_PROVIDER_CONFIGS = {
170
170
  recraft: createRecraftConfig(),
171
171
  typesafe: createTypeSafeConfig(),
172
172
  laya: createLayaConfig(),
173
+ xor: createXorConfig(),
173
174
  ...Object.fromEntries(getCatalogJsonEntries()
174
175
  .filter((e) => e.id !== "mistral")
175
176
  .map((e) => [e.id, buildCatalogConfigOptions(e)])),
@@ -616,7 +616,7 @@ export class CLICommandFactory {
616
616
  },
617
617
  classifierStrategy: {
618
618
  type: "string",
619
- description: "Classifier strategy: 'auto' (default — 'jev' when a decision provider is configured, such as TYPESAFE_API_KEY, or LAYA_API_KEY with LAYA_BASE_URL, else 'heuristic'), 'heuristic' (no LLM), 'llm' (a cheap model picks per prompt), or 'jev' (a System One decision model — TypeSafe Jev, or Laya — with calibrated confidence).",
619
+ description: "Classifier strategy: 'auto' (default — 'jev' when a decision provider is configured, such as TYPESAFE_API_KEY, LAYA_API_KEY with LAYA_BASE_URL, or XOR_API_KEY with XOR_BASE_URL, else 'heuristic'), 'heuristic' (no LLM), 'llm' (a cheap model picks per prompt), or 'jev' (a System One decision model — TypeSafe Jev, Laya or XOR — with calibrated confidence).",
620
620
  choices: ["auto", "heuristic", "llm", "jev"],
621
621
  alias: "classifier-strategy",
622
622
  },
@@ -48,6 +48,8 @@ export declare enum AIProviderName {
48
48
  TYPESAFE = "typesafe",
49
49
  /** Laya (Convai, open weights) — serves the `decide` inference type only. */
50
50
  LAYA = "laya",
51
+ /** XOR (Juspay, open weights) — serves the `decide` inference type only. */
52
+ XOR = "xor",
51
53
  AUTO = "auto"
52
54
  }
53
55
  /**
@@ -1097,3 +1099,10 @@ export declare enum LayaModels {
1097
1099
  MULTILINGUAL = "multilingual",
1098
1100
  AUTO = "auto"
1099
1101
  }
1102
+ /**
1103
+ * XOR decision models. Hand-written: XOR is a Tier-3 provider, so it is not in
1104
+ * the provider catalog and codegen never touches this.
1105
+ */
1106
+ export declare enum XorModels {
1107
+ XOR_1_1 = "xor-1.1"
1108
+ }
@@ -54,6 +54,8 @@ export var AIProviderName;
54
54
  AIProviderName["TYPESAFE"] = "typesafe";
55
55
  /** Laya (Convai, open weights) — serves the `decide` inference type only. */
56
56
  AIProviderName["LAYA"] = "laya";
57
+ /** XOR (Juspay, open weights) — serves the `decide` inference type only. */
58
+ AIProviderName["XOR"] = "xor";
57
59
  AIProviderName["AUTO"] = "auto";
58
60
  })(AIProviderName || (AIProviderName = {}));
59
61
  /**
@@ -1357,3 +1359,11 @@ export var LayaModels;
1357
1359
  LayaModels["MULTILINGUAL"] = "multilingual";
1358
1360
  LayaModels["AUTO"] = "auto";
1359
1361
  })(LayaModels || (LayaModels = {}));
1362
+ /**
1363
+ * XOR decision models. Hand-written: XOR is a Tier-3 provider, so it is not in
1364
+ * the provider catalog and codegen never touches this.
1365
+ */
1366
+ export var XorModels;
1367
+ (function (XorModels) {
1368
+ XorModels["XOR_1_1"] = "xor-1.1";
1369
+ })(XorModels || (XorModels = {}));
@@ -262,12 +262,21 @@ export declare abstract class BaseProvider implements AIProvider {
262
262
  * and a retired default model stops degrading to the next live one in the
263
263
  * catalog — which is exactly what the provider contract gate checks.
264
264
  */
265
- protected runGenerateWithModelFallback(attempt: () => Promise<EnhancedGenerateResult | null>, callerOwnsFallback: boolean): Promise<EnhancedGenerateResult | null>;
265
+ protected runGenerateWithModelFallback(attempt: () => Promise<EnhancedGenerateResult | null>, callerOwnsFallback: boolean, options?: TextGenerationOptions): Promise<EnhancedGenerateResult | null>;
266
266
  /**
267
267
  * Model ids to try when this provider rejects its current model as invalid.
268
268
  * Empty by default; catalog-driven providers return their `fallbacks`.
269
269
  */
270
270
  protected getModelFallbacks(): string[];
271
+ /**
272
+ * The fallbacks an invalid-model retry may try for this request. A catalog
273
+ * orders `fallbacks` for text, so a request carrying an image keeps only the
274
+ * vision-capable ones: a text-only candidate fails the vision guard, and
275
+ * that error names the fallback instead of the retired model the caller
276
+ * asked for. With no vision-capable candidate left, the caller gets the
277
+ * original invalid-model error.
278
+ */
279
+ protected modelFallbacksFor(options: TextGenerationOptions | StreamOptions, requestedModel: string): string[];
271
280
  /**
272
281
  * Alias for generate method - implements AIProvider interface
273
282
  */
@@ -46,6 +46,7 @@ import { StreamHandler } from "./modules/StreamHandler.js";
46
46
  import { TelemetryHandler } from "./modules/TelemetryHandler.js";
47
47
  import { ToolsManager } from "./modules/ToolsManager.js";
48
48
  import { Utilities } from "./modules/Utilities.js";
49
+ import { ProviderImageAdapter } from "../adapters/providerImageAdapter.js";
49
50
  import { generateOnceNative } from "../utils/nativeSingleShot.js";
50
51
  import { validateExecutionControl } from "../utils/parameterValidation.js";
51
52
  import { extractTokenUsage } from "../utils/tokenUtils.js";
@@ -107,6 +108,18 @@ function isProviderErrorClassified(error) {
107
108
  }
108
109
  return error[PROVIDER_ERROR_CLASSIFIED] === true;
109
110
  }
111
+ function requestCarriesImages(options) {
112
+ const input = options.input;
113
+ if (!input) {
114
+ return false;
115
+ }
116
+ if ((input.images?.length ?? 0) > 0) {
117
+ return true;
118
+ }
119
+ return ("content" in input &&
120
+ Array.isArray(input.content) &&
121
+ input.content.some((part) => part.type === "image"));
122
+ }
110
123
  export class BaseProvider {
111
124
  // Not `readonly` because providers that auto-discover the model from a
112
125
  // /v1/models endpoint (lm-studio, llamacpp) need to update modelName after
@@ -523,9 +536,7 @@ export class BaseProvider {
523
536
  throw error;
524
537
  }
525
538
  const requestedModel = provider.modelName;
526
- const candidates = provider
527
- .getModelFallbacks()
528
- .filter((model) => model !== requestedModel);
539
+ const candidates = provider.modelFallbacksFor(options, requestedModel);
529
540
  for (const candidate of candidates) {
530
541
  logger.warn(`[${provider.providerName}] model "${requestedModel}" was rejected as invalid — retrying stream with fallback "${candidate}". This provider's catalog entry is stale; run "pnpm run check:models".`);
531
542
  provider.refreshHandlersForModel(candidate);
@@ -671,7 +682,7 @@ export class BaseProvider {
671
682
  return undefined;
672
683
  }
673
684
  const requestedModel = this.modelName;
674
- const candidates = this.getModelFallbacks().filter((model) => model !== requestedModel);
685
+ const candidates = this.modelFallbacksFor(options, requestedModel);
675
686
  for (const candidate of candidates) {
676
687
  logger.warn(`[${this.providerName}] model "${requestedModel}" was rejected as invalid — retrying stream with fallback "${candidate}". This provider's catalog entry is stale; run "pnpm run check:models".`);
677
688
  this.refreshHandlersForModel(candidate);
@@ -1493,7 +1504,7 @@ export class BaseProvider {
1493
1504
  // or a router that retries on its own) pass the flag on both paths;
1494
1505
  // TextGenerationOptions declares it, so a plain read is enough here.
1495
1506
  const callerOwnsFallback = options.disableInternalFallback === true;
1496
- return await this.runGenerateWithModelFallback(attempt, callerOwnsFallback);
1507
+ return await this.runGenerateWithModelFallback(attempt, callerOwnsFallback, options);
1497
1508
  }
1498
1509
  /**
1499
1510
  * Models the catalog names are the models a vendor served when the entry was
@@ -1520,7 +1531,7 @@ export class BaseProvider {
1520
1531
  * and a retired default model stops degrading to the next live one in the
1521
1532
  * catalog — which is exactly what the provider contract gate checks.
1522
1533
  */
1523
- async runGenerateWithModelFallback(attempt, callerOwnsFallback) {
1534
+ async runGenerateWithModelFallback(attempt, callerOwnsFallback, options) {
1524
1535
  const requestedModel = this.modelName;
1525
1536
  try {
1526
1537
  return await attempt();
@@ -1529,7 +1540,9 @@ export class BaseProvider {
1529
1540
  if (callerOwnsFallback || !isInvalidModelError(error)) {
1530
1541
  throw error;
1531
1542
  }
1532
- const candidates = this.getModelFallbacks().filter((model) => model !== requestedModel);
1543
+ const candidates = options
1544
+ ? this.modelFallbacksFor(options, requestedModel)
1545
+ : this.getModelFallbacks().filter((model) => model !== requestedModel);
1533
1546
  if (candidates.length === 0) {
1534
1547
  throw error;
1535
1548
  }
@@ -1563,6 +1576,21 @@ export class BaseProvider {
1563
1576
  getModelFallbacks() {
1564
1577
  return [];
1565
1578
  }
1579
+ /**
1580
+ * The fallbacks an invalid-model retry may try for this request. A catalog
1581
+ * orders `fallbacks` for text, so a request carrying an image keeps only the
1582
+ * vision-capable ones: a text-only candidate fails the vision guard, and
1583
+ * that error names the fallback instead of the retired model the caller
1584
+ * asked for. With no vision-capable candidate left, the caller gets the
1585
+ * original invalid-model error.
1586
+ */
1587
+ modelFallbacksFor(options, requestedModel) {
1588
+ const candidates = this.getModelFallbacks().filter((model) => model !== requestedModel);
1589
+ if (!requestCarriesImages(options)) {
1590
+ return candidates;
1591
+ }
1592
+ return candidates.filter((model) => ProviderImageAdapter.supportsVision(this.providerName, model));
1593
+ }
1566
1594
  /**
1567
1595
  * Alias for generate method - implements AIProvider interface
1568
1596
  */
@@ -30,6 +30,8 @@ export declare const DECISION_PROVIDERS: readonly ProviderDescriptor[];
30
30
  * internal consumer treats as "carry on exactly as before".
31
31
  */
32
32
  export declare function resolveDefaultDecisionProvider(credentials?: NeurolinkCredentials): string | undefined;
33
+ /** Names of the decision providers that accept images or video. */
34
+ export declare function listMediaDecisionProviders(): string[];
33
35
  /**
34
36
  * The variables that would configure each decision provider, one clause per
35
37
  * provider in precedence order, e.g. "TYPESAFE_API_KEY or AI_GATEWAY_API_KEY
@@ -1,5 +1,5 @@
1
1
  import { AIProviderName } from "../constants/enums.js";
2
- import { GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, TypeSafeModels, LayaModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, ReplicateModels, } from "../constants/enums.js";
2
+ import { GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, TypeSafeModels, LayaModels, XorModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, ReplicateModels, } from "../constants/enums.js";
3
3
  import { API_KEY_FORMATS } from "../utils/providerConfig.js";
4
4
  import { getCatalogJsonEntries, catalogCredentialsKey, catalogEnvVar, } from "../providers/catalog/loader.js";
5
5
  import { DEFAULT_INFERENCE_KINDS } from "../types/index.js";
@@ -451,11 +451,12 @@ const HAND_DESCRIPTORS = [
451
451
  timeouts: { decideMs: 5000 },
452
452
  setupUrl: "https://console.typesafe.ai/keys",
453
453
  },
454
- // Laya MUST stay after TypeSafe. resolveDefaultDecisionProvider() returns
455
- // the first configured DECISION_PROVIDERS entry, in this order, so a host
456
- // configured for both keeps Jev for every built-in consumer and reaches
457
- // Laya only by naming it. Reordering these two silently changes which model
458
- // routes, compacts and plans for every such host.
454
+ // Laya MUST stay after TypeSafe, and XOR after Laya.
455
+ // resolveDefaultDecisionProvider() returns the first configured
456
+ // DECISION_PROVIDERS entry, in this order, so a host configured for several
457
+ // keeps Jev for every built-in consumer and reaches the others only by
458
+ // naming them. Reordering these silently changes which model routes,
459
+ // compacts and plans for every such host.
459
460
  {
460
461
  name: AIProviderName.LAYA,
461
462
  aliases: [],
@@ -501,6 +502,47 @@ const HAND_DESCRIPTORS = [
501
502
  },
502
503
  setupUrl: "https://github.com/NandhaKishorM/laya",
503
504
  },
505
+ {
506
+ name: AIProviderName.XOR,
507
+ aliases: [],
508
+ credentialsKey: "xor",
509
+ envVars: {
510
+ apiKey: "XOR_API_KEY",
511
+ baseURL: "XOR_BASE_URL",
512
+ // XOR has no built-in endpoint (it is reached at a deployment or behind a
513
+ // proxy), so a key is useless without a base URL. Both are required for
514
+ // it to count as configured, from the environment or credentials.xor.
515
+ extraRequired: ["XOR_BASE_URL"],
516
+ model: "XOR_MODEL",
517
+ },
518
+ defaultModel: XorModels.XOR_1_1,
519
+ // Serves only `decide`, like TypeSafe and Laya — the one declaration that
520
+ // keeps it out of every generation code path.
521
+ inferenceKinds: ["decide"],
522
+ toolSupport: "none",
523
+ localRuntime: false,
524
+ healthCheck: "env-only",
525
+ // Deliberately NO autoSelectPriority / autoSelectPreference /
526
+ // defaultHealthSweepPriority, for the same reason as TypeSafe above.
527
+ timeouts: { decideMs: 5000 },
528
+ // The deployment's prefill is 250,000 tokens, shared by the state, the
529
+ // questions and any media. 200,000 leaves 20% for those, and a state
530
+ // between this and the server's 4 MB would otherwise pass validation and
531
+ // fail inside the engine as a 500, which the base class would retry. There
532
+ // is no question cap (the server only limits a choice to 2..255 options).
533
+ // Non-ASCII text is charged per character because the four-characters-
534
+ // per-token estimate is several times too generous for CJK on this
535
+ // tokenizer. One token per character is a deliberate upper bound, to be
536
+ // replaced by a live measurement.
537
+ decisionLimits: {
538
+ maxStateTokens: 200_000,
539
+ nonAsciiTokensPerChar: 1,
540
+ // The server takes 1..8 images and one video, and refuses a body over
541
+ // 8 MB.
542
+ media: { maxImages: 8, video: true, maxRequestBytes: 8 * 1024 * 1024 },
543
+ },
544
+ setupUrl: "https://huggingface.co/juspay/xor",
545
+ },
504
546
  ];
505
547
  /**
506
548
  * Builds a ProviderDescriptor for every JSON-catalog provider. Every field
@@ -636,6 +678,10 @@ function isDecisionProviderConfigured(descriptor, credentials) {
636
678
  export function resolveDefaultDecisionProvider(credentials) {
637
679
  return DECISION_PROVIDERS.find((descriptor) => isDecisionProviderConfigured(descriptor, credentials))?.name;
638
680
  }
681
+ /** Names of the decision providers that accept images or video. */
682
+ export function listMediaDecisionProviders() {
683
+ return DECISION_PROVIDERS.filter((descriptor) => descriptor.decisionLimits?.media !== undefined).map((descriptor) => descriptor.name);
684
+ }
639
685
  /**
640
686
  * The variables that would configure each decision provider, one clause per
641
687
  * provider in precedence order, e.g. "TYPESAFE_API_KEY or AI_GATEWAY_API_KEY
@@ -1,6 +1,6 @@
1
1
  import { ProviderFactory } from "./providerFactory.js";
2
2
  import { logger } from "../utils/logger.js";
3
- import { AIProviderName, GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, LayaModels, ReplicateModels, } from "../constants/enums.js";
3
+ import { AIProviderName, GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, LayaModels, XorModels, ReplicateModels, } from "../constants/enums.js";
4
4
  import { PROVIDER_DESCRIPTORS_BY_NAME } from "./providerDescriptors.js";
5
5
  import { OPENAI_COMPAT_CATALOG } from "../providers/openaiCompatCatalog.js";
6
6
  import { providerChoicesFor } from "./mediaHandlerCatalog.js";
@@ -242,6 +242,14 @@ export class ProviderRegistry {
242
242
  const { LayaProvider } = await import("../providers/laya.js");
243
243
  return new LayaProvider(modelName, sdk, undefined, layaCreds);
244
244
  }, process.env.LAYA_MODEL || LayaModels.TYPED_DECISIONS, [], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.LAYA));
245
+ // Register XOR — the third `decide` provider, reached at whatever base
246
+ // URL its config names. Its descriptor declares inferenceKinds:
247
+ // ["decide"], so nothing in the generation fallback chain can reach it.
248
+ ProviderFactory.registerProvider(AIProviderName.XOR, async (modelName, _providerName, sdk, _region, credentials) => {
249
+ const xorCreds = credentials;
250
+ const { XorProvider } = await import("../providers/xor.js");
251
+ return new XorProvider(modelName, sdk, undefined, xorCreds);
252
+ }, process.env.XOR_MODEL || XorModels.XOR_1_1, [], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.XOR));
245
253
  logger.debug("All AI providers registered successfully");
246
254
  // ===== MEDIA HANDLER REGISTRATION =====
247
255
  // Single registration path (Task 11): each ecosystem barrel (voice,
@@ -21,6 +21,7 @@ import { xaiManifest } from "./manifests/xai.js";
21
21
  import { groqManifest } from "./manifests/groq.js";
22
22
  import { typesafeManifest } from "./manifests/typesafe.js";
23
23
  import { layaManifest } from "./manifests/laya.js";
24
+ import { xorManifest } from "./manifests/xor.js";
24
25
  import { cohereManifest } from "./manifests/cohere.js";
25
26
  import { togetherAiManifest } from "./manifests/together-ai.js";
26
27
  import { fireworksManifest } from "./manifests/fireworks.js";
@@ -120,6 +121,7 @@ export const MANIFEST_REGISTRY = {
120
121
  groq: groqManifest,
121
122
  typesafe: typesafeManifest,
122
123
  laya: layaManifest,
124
+ xor: xorManifest,
123
125
  cerebras: catalogManifest("cerebras"),
124
126
  sambanova: catalogManifest("sambanova"),
125
127
  cohere: cohereManifest,
@@ -38,7 +38,7 @@ export const anthropicManifest = {
38
38
  samplingParams: false, // matches SAMPLING_PARAM_REJECTING_FAMILIES /opus[-_.]?5(?![0-9])/i
39
39
  },
40
40
  "claude-sonnet-5-5": {
41
- aliases: ["sonnet-5.5"],
41
+ aliases: ["sonnet-5.5", "claude-sonnet"],
42
42
  displayName: "Claude Sonnet 5.5",
43
43
  contextWindow: 1_000_000,
44
44
  maxOutputTokens: 128_000,
@@ -66,7 +66,7 @@ export const anthropicManifest = {
66
66
  samplingParams: false, // matches SAMPLING_PARAM_REJECTING_FAMILIES /fable/i
67
67
  },
68
68
  "claude-sonnet-5": {
69
- aliases: ["sonnet-5", "claude-sonnet"],
69
+ aliases: ["sonnet-5"],
70
70
  displayName: "Claude Sonnet 5",
71
71
  contextWindow: 1_000_000,
72
72
  // Was 64_000; corrected to 128_000 to match the official spec
@@ -0,0 +1,12 @@
1
+ import type { ProviderModelManifest } from "../../types/index.js";
2
+ /**
3
+ * XOR — the `decide` inference type, not text generation.
4
+ *
5
+ * `contextWindow` is the deployment's maximum prefill, shared by the state,
6
+ * the questions and any images or video. `vision` describes `generate()`
7
+ * input, which XOR does not serve; its image input is `DecisionRequest.images`.
8
+ *
9
+ * `maxOutputTokens` is required by the manifest type but has no honest value
10
+ * for a model that emits no text, so a small non-zero figure is used.
11
+ */
12
+ export declare const xorManifest: ProviderModelManifest;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * XOR — the `decide` inference type, not text generation.
3
+ *
4
+ * `contextWindow` is the deployment's maximum prefill, shared by the state,
5
+ * the questions and any images or video. `vision` describes `generate()`
6
+ * input, which XOR does not serve; its image input is `DecisionRequest.images`.
7
+ *
8
+ * `maxOutputTokens` is required by the manifest type but has no honest value
9
+ * for a model that emits no text, so a small non-zero figure is used.
10
+ */
11
+ export const xorManifest = {
12
+ defaultContextWindow: 250_000,
13
+ models: {
14
+ _default: {
15
+ aliases: [],
16
+ contextWindow: 250_000,
17
+ maxOutputTokens: 256,
18
+ vision: false,
19
+ functionCalling: false,
20
+ },
21
+ "xor-1.1": {
22
+ aliases: [],
23
+ displayName: "XOR 1.1",
24
+ contextWindow: 250_000,
25
+ maxOutputTokens: 256,
26
+ vision: false,
27
+ functionCalling: false,
28
+ },
29
+ },
30
+ };
@@ -4,7 +4,7 @@
4
4
  * Part of Phase 4.1 - Models Command System
5
5
  */
6
6
  import { AIProviderName } from "../constants/enums.js";
7
- import type { JsonValue, ModelCapabilities, ModelInfo } from "../types/index.js";
7
+ import type { ClaudeDisabledThinkingReplacement, JsonValue, ModelCapabilities, ModelInfo } from "../types/index.js";
8
8
  /**
9
9
  * Public MODEL_REGISTRY: the manifest-derived entries merged over the
10
10
  * hand-authored LEGACY_MODEL_REGISTRY base. Spread order matters — manifest
@@ -90,6 +90,17 @@ export declare function resolveSamplingParams(provider: string, model: string |
90
90
  temperature?: number;
91
91
  topP?: number;
92
92
  };
93
+ /**
94
+ * Whether a Claude model accepts a forced `tool_choice` (`any` or a named
95
+ * `tool`). Unknown models default to accepting it.
96
+ */
97
+ export declare function modelSupportsForcedToolChoice(model: string | undefined): boolean;
98
+ /**
99
+ * What to send instead of `thinking: {type: "disabled"}` to a Claude model.
100
+ * `between_tools` is refused at `xhigh` and `max` effort, so those fall back
101
+ * to adaptive thinking (`omit`).
102
+ */
103
+ export declare function claudeDisabledThinkingReplacement(model: string | undefined, effort: string | undefined): ClaudeDisabledThinkingReplacement;
93
104
  /**
94
105
  * Get models by provider
95
106
  */
@@ -1168,14 +1168,14 @@ const LEGACY_MODEL_REGISTRY = {
1168
1168
  translation: 9,
1169
1169
  summarization: 9,
1170
1170
  },
1171
- aliases: ["sonnet-5", "claude-sonnet-latest"],
1171
+ aliases: ["sonnet-5"],
1172
1172
  deprecated: false,
1173
1173
  isLocal: false,
1174
1174
  releaseDate: "2026-06-30",
1175
1175
  category: "general",
1176
1176
  },
1177
- // "claude-sonnet-latest" stays on CLAUDE_SONNET_5 until someone decides to
1178
- // move it; this entry sits after that one so resolver ties still go to it.
1177
+ // Owns "claude-sonnet-latest", but sits after CLAUDE_SONNET_5 so resolver
1178
+ // score ties still go to Sonnet 5.
1179
1179
  [AnthropicModels.CLAUDE_SONNET_5_5]: {
1180
1180
  id: AnthropicModels.CLAUDE_SONNET_5_5,
1181
1181
  name: "Claude Sonnet 5.5",
@@ -1210,7 +1210,7 @@ const LEGACY_MODEL_REGISTRY = {
1210
1210
  translation: 9,
1211
1211
  summarization: 9,
1212
1212
  },
1213
- aliases: ["sonnet-5.5"],
1213
+ aliases: ["sonnet-5.5", "claude-sonnet-latest"],
1214
1214
  deprecated: false,
1215
1215
  isLocal: false,
1216
1216
  releaseDate: "2026-09-28",
@@ -1374,7 +1374,7 @@ const LEGACY_MODEL_REGISTRY = {
1374
1374
  translation: 8,
1375
1375
  summarization: 8,
1376
1376
  },
1377
- // "claude-sonnet-latest" now belongs to CLAUDE_SONNET_5 — see the note
1377
+ // "claude-sonnet-latest" now belongs to CLAUDE_SONNET_5_5 — see the note
1378
1378
  // on CLAUDE_OPUS_4_5's aliases.
1379
1379
  aliases: ["claude-4.5-sonnet", "sonnet-4.5"],
1380
1380
  deprecated: false,
@@ -2846,6 +2846,58 @@ export function resolveSamplingParams(provider, model, params, site) {
2846
2846
  }
2847
2847
  return {};
2848
2848
  }
2849
+ /**
2850
+ * Claude models that answer a forced `tool_choice` (`any` or `tool`) with a
2851
+ * 400: "tool_choice: type "tool" and "any" are not supported for this model."
2852
+ * Sonnet 5.5 and Opus 5.5 were probed live on Vertex; the Fable 5.1 docs state
2853
+ * the same, and Mythos 5.1 shares Fable 5.1's capabilities. The same families
2854
+ * also refuse `thinking: {type: "disabled"}`.
2855
+ */
2856
+ const FORCED_TOOL_CHOICE_REJECTING_FAMILIES = [
2857
+ /sonnet[-_.]?5[-_.]5(?![0-9])/i,
2858
+ /opus[-_.]?5[-_.]5(?![0-9])/i,
2859
+ /(?:fable|mythos)[-_.]?5[-_.]1(?![0-9])/i,
2860
+ ];
2861
+ /**
2862
+ * Of those, the ones that accept `thinking: {type: "between_tools"}` as their
2863
+ * lowest setting. The rest think adaptively, always on, and take no
2864
+ * `thinking` field in its place.
2865
+ */
2866
+ const BETWEEN_TOOLS_THINKING_FAMILIES = [
2867
+ /sonnet[-_.]?5[-_.]5(?![0-9])/i,
2868
+ ];
2869
+ function matchesClaudeFamily(families, model) {
2870
+ const normalizedModel = (model ?? "").toLowerCase();
2871
+ if (!normalizedModel) {
2872
+ return false;
2873
+ }
2874
+ const modelId = MODEL_ALIASES[normalizedModel] ?? normalizedModel;
2875
+ return families.some((family) => family.test(modelId) || family.test(normalizedModel));
2876
+ }
2877
+ /**
2878
+ * Whether a Claude model accepts a forced `tool_choice` (`any` or a named
2879
+ * `tool`). Unknown models default to accepting it.
2880
+ */
2881
+ export function modelSupportsForcedToolChoice(model) {
2882
+ return !matchesClaudeFamily(FORCED_TOOL_CHOICE_REJECTING_FAMILIES, model);
2883
+ }
2884
+ /**
2885
+ * What to send instead of `thinking: {type: "disabled"}` to a Claude model.
2886
+ * `between_tools` is refused at `xhigh` and `max` effort, so those fall back
2887
+ * to adaptive thinking (`omit`).
2888
+ */
2889
+ export function claudeDisabledThinkingReplacement(model, effort) {
2890
+ if (!matchesClaudeFamily(FORCED_TOOL_CHOICE_REJECTING_FAMILIES, model)) {
2891
+ return "keep";
2892
+ }
2893
+ const level = effort?.toLowerCase();
2894
+ if (matchesClaudeFamily(BETWEEN_TOOLS_THINKING_FAMILIES, model) &&
2895
+ level !== "xhigh" &&
2896
+ level !== "max") {
2897
+ return "between_tools";
2898
+ }
2899
+ return "omit";
2900
+ }
2849
2901
  /**
2850
2902
  * Get models by provider
2851
2903
  */
package/dist/neurolink.js CHANGED
@@ -12956,6 +12956,7 @@ Current user's request: ${currentInput}`;
12956
12956
  "gen_ai.operation.name": "decide",
12957
12957
  "ai.provider": providerName,
12958
12958
  "decision.question_count": questionCount,
12959
+ "decision.images.count": options.images?.length ?? 0,
12959
12960
  },
12960
12961
  }, async (otelSpan) => {
12961
12962
  const { traceId, parentSpanId } = getActiveTraceContext();
@@ -12964,6 +12965,7 @@ Current user's request: ${currentInput}`;
12964
12965
  "ai.provider": providerName,
12965
12966
  "ai.model": options.model ?? "",
12966
12967
  "decision.question_count": questionCount,
12968
+ "decision.images.count": options.images?.length ?? 0,
12967
12969
  }, parentSpanId, traceId);
12968
12970
  try {
12969
12971
  // enableMCP: false — a decision model has no tools and never will.
@@ -12974,6 +12976,8 @@ Current user's request: ${currentInput}`;
12974
12976
  const result = await provider.decide({
12975
12977
  state: options.state,
12976
12978
  questions: options.questions,
12979
+ images: options.images,
12980
+ video: options.video,
12977
12981
  model: options.model,
12978
12982
  signal: options.signal,
12979
12983
  timeoutMs: options.timeoutMs,
@@ -12988,6 +12992,7 @@ Current user's request: ${currentInput}`;
12988
12992
  otelSpan.setAttribute("gen_ai.usage.input_tokens", result.usage.inputTokens);
12989
12993
  otelSpan.setAttribute("gen_ai.usage.output_tokens", result.usage.outputTokens);
12990
12994
  otelSpan.setAttribute("decision.answer_count", answerCount);
12995
+ otelSpan.setAttribute("decision.media.bytes", result.mediaBytes ?? 0);
12991
12996
  otelSpan.setAttribute("ai.cost.total", cost);
12992
12997
  if (result.upstreamMs !== undefined) {
12993
12998
  otelSpan.setAttribute("decision.upstream_ms", result.upstreamMs);
@@ -13008,6 +13013,7 @@ Current user's request: ${currentInput}`;
13008
13013
  "ai.cost.output": 0,
13009
13014
  "ai.cost.total": cost,
13010
13015
  "decision.answer_count": answerCount,
13016
+ "decision.media.bytes": result.mediaBytes ?? 0,
13011
13017
  "decision.latency_ms": result.latencyMs,
13012
13018
  ...(result.upstreamMs !== undefined
13013
13019
  ? { "decision.upstream_ms": result.upstreamMs }
@@ -126,7 +126,7 @@ export class AmazonSageMakerProvider extends BaseProvider {
126
126
  options.disableInternalFallback === true;
127
127
  // The native loop bypasses BaseProvider.executeGeneration, so the turn
128
128
  // budget has to be composed here or it stops existing for this provider.
129
- return this.runGenerateWithModelFallback(() => this.withTurnTimeout({ ...options, tools: mergedTools }, this.getDescriptorGenerateMs(), (timedOptions) => this.executeNativeGenerate(timedOptions)), callerOwnsFallback);
129
+ return this.runGenerateWithModelFallback(() => this.withTurnTimeout({ ...options, tools: mergedTools }, this.getDescriptorGenerateMs(), (timedOptions) => this.executeNativeGenerate(timedOptions)), callerOwnsFallback, options);
130
130
  }
131
131
  async executeNativeGenerate(options) {
132
132
  const startTime = Date.now();