@juspay/neurolink 12.37.5 → 12.38.1

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 (37) hide show
  1. package/CHANGELOG.md +2 -3
  2. package/README.md +59 -49
  3. package/dist/browser/neurolink.min.js +411 -411
  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/modelConfiguration.js +1 -0
  11. package/dist/factories/providerDescriptors.d.ts +2 -0
  12. package/dist/factories/providerDescriptors.js +52 -6
  13. package/dist/factories/providerRegistry.js +9 -1
  14. package/dist/models/manifestRegistry.js +2 -0
  15. package/dist/models/manifests/xor.d.ts +12 -0
  16. package/dist/models/manifests/xor.js +30 -0
  17. package/dist/neurolink.js +6 -0
  18. package/dist/providers/anthropic/client.js +8 -1
  19. package/dist/providers/laya.js +1 -19
  20. package/dist/providers/openRouter/client.js +1 -0
  21. package/dist/providers/openaiChatCompletionsBase.js +37 -1
  22. package/dist/providers/systemOneDecision.d.ts +21 -2
  23. package/dist/providers/systemOneDecision.js +64 -4
  24. package/dist/providers/xor.d.ts +48 -0
  25. package/dist/providers/xor.js +192 -0
  26. package/dist/types/classifierRouter.d.ts +5 -4
  27. package/dist/types/cli.d.ts +6 -2
  28. package/dist/types/decision.d.ts +60 -1
  29. package/dist/types/openaiCompatible.d.ts +10 -0
  30. package/dist/types/providers.d.ts +10 -0
  31. package/dist/utils/decisionMedia.d.ts +9 -0
  32. package/dist/utils/decisionMedia.js +160 -0
  33. package/dist/utils/modelChoices.js +9 -1
  34. package/dist/utils/providerConfig.d.ts +5 -0
  35. package/dist/utils/providerConfig.js +17 -0
  36. package/docs-site/static/search-index.json +61 -34
  37. package/package.json +3 -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 = {}));
@@ -322,6 +322,7 @@ export class ModelConfigurationManager {
322
322
  ]),
323
323
  toolCapableModels: this.getConfigArray("OLLAMA_TOOL_CAPABLE_MODELS", [
324
324
  "llama3.1",
325
+ "llama3.2",
325
326
  "mistral",
326
327
  "hermes3",
327
328
  "qwen2.5",
@@ -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,
@@ -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
+ };
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 }
@@ -2307,7 +2307,14 @@ export class AnthropicProvider extends BaseProvider {
2307
2307
  return {
2308
2308
  stream: transformedStream(),
2309
2309
  provider: this.providerName,
2310
- model: this.modelName,
2310
+ // Cell-3 identity fix: `this.modelName` is the raw, possibly-unset
2311
+ // constructor arg (undefined whenever a caller relies on the default
2312
+ // model). `modelId` (resolved above as
2313
+ // `this.modelName || getDefaultAnthropicModel()`) is what was actually
2314
+ // put on the wire for this turn — the same value `buildParams` uses to
2315
+ // build the request. Reporting the raw field previously let a
2316
+ // default-model turn's StreamResult claim an undefined/empty model.
2317
+ model: modelId,
2311
2318
  toolCalls: [],
2312
2319
  toolResults: [],
2313
2320
  metadata: turnMetadata,
@@ -2,25 +2,7 @@ import { LayaModels } from "../constants/enums.js";
2
2
  import { logger } from "../utils/logger.js";
3
3
  import { redactUrlForError } from "../utils/logSanitize.js";
4
4
  import { getProviderModel } from "../utils/providerConfig.js";
5
- import { describeValidationErrors, isRecord, SystemOneDecisionProvider, } from "./systemOneDecision.js";
6
- /**
7
- * LiteLLM echoes credentials back in its error texts, in more than one
8
- * wording: a masked key plus its hash on a rejected key, the whole key when it
9
- * does not look like a LiteLLM key, a key hash on a rate limit. None of it
10
- * belongs in an error message or a log, whatever the wording, so the
11
- * configured key, anything shaped like a LiteLLM key and any long hex run are
12
- * all removed.
13
- */
14
- function redactCredentials(message, apiKey) {
15
- const withoutKey = apiKey
16
- ? message.split(apiKey).join("[redacted]")
17
- : message;
18
- return withoutKey
19
- .replace(/\.?\s*Received API Key\s*=[\s\S]*$/, "")
20
- .replace(/\bsk-[A-Za-z0-9._-]+/g, "[redacted]")
21
- .replace(/\b[0-9a-f]{32,}\b/gi, "[redacted]")
22
- .trim();
23
- }
5
+ import { describeValidationErrors, isRecord, redactCredentials, SystemOneDecisionProvider, } from "./systemOneDecision.js";
24
6
  /**
25
7
  * Normalise the two envelopes a Laya failure can arrive in.
26
8
  *
@@ -203,6 +203,7 @@ export class OpenRouterProvider extends OpenAIChatCompletionsProvider {
203
203
  "mistralai/devstral",
204
204
  "meta-llama/llama-3.3",
205
205
  "meta-llama/llama-3.2",
206
+ "meta-llama/llama-3.1",
206
207
  "qwen/qwen3",
207
208
  "nvidia/nemotron",
208
209
  ];
@@ -1648,6 +1648,12 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1648
1648
  // middleware short-circuits the request — the case where there is no
1649
1649
  // turn to build on at all.
1650
1650
  let loopConversation;
1651
+ // Cell-3 identity fix: the last model id the SERVER actually echoed on
1652
+ // the wire (via `onModelObserved`, see `runStreamLoop`), distinct from
1653
+ // the pre-call resolved/requested `modelId` below. Stays undefined for
1654
+ // backends that never echo `model` on a chunk, in which case the
1655
+ // `result.model` getter falls back to `modelId` — unchanged behavior.
1656
+ let observedServerModel;
1651
1657
  const providerNameForLoop = this.providerName;
1652
1658
  const streamBaseModel = {
1653
1659
  specificationVersion: "v3",
@@ -1700,6 +1706,9 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1700
1706
  resolveUsage,
1701
1707
  resolveFinish,
1702
1708
  responseFormat,
1709
+ onModelObserved: (model) => {
1710
+ observedServerModel = model;
1711
+ },
1703
1712
  });
1704
1713
  const completion = loopPromise.then(() => Promise.all([usagePromise, finishPromise]).then(([usage, reason]) => ({
1705
1714
  type: "finish",
@@ -1930,6 +1939,24 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1930
1939
  toolsUsed,
1931
1940
  metadata: streamMetadata,
1932
1941
  };
1942
+ // Cell-3 identity fix: `model` starts as a plain `modelId` data property
1943
+ // above (satisfying StreamResult's type and giving every reader a value
1944
+ // even before the loop starts), then gets redefined as a live getter.
1945
+ // A plain reassignment (`result.model = observedServerModel`) would not
1946
+ // survive `preserveLiveStreamAccessors`' re-wrapping at the
1947
+ // BaseProvider/NeuroLink boundaries — those boundaries spread `result`
1948
+ // into a new object BEFORE the background loop has produced a single
1949
+ // chunk, which freezes a plain field at its construction-time value.
1950
+ // Re-defining as a getter (the same mechanism already used for
1951
+ // `toolExecutions` below) means every boundary's spread re-applies the
1952
+ // getter itself, so a caller reading `.model` after draining `.stream`
1953
+ // sees whatever the server actually echoed, falling back to the
1954
+ // pre-call resolved `modelId` when the backend never echoed one.
1955
+ Object.defineProperty(result, "model", {
1956
+ enumerable: true,
1957
+ configurable: true,
1958
+ get: () => observedServerModel ?? modelId,
1959
+ });
1933
1960
  // Lazy getter: every read transforms the live `toolExecutionSummaries`
1934
1961
  // through the canonical `transformToolExecutions()` so consumers see
1935
1962
  // `{name, input, output, duration}[]` (codebase convention), while still
@@ -1952,7 +1979,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1952
1979
  return result;
1953
1980
  }
1954
1981
  async runStreamLoop(args) {
1955
- const { maxSteps, modelId, url, fetchImpl, abortSignal, options, conversation, openAITools, openAIToolChoice, toolsRecord, toolNameFromWire, emitter, toolsUsed, toolExecutionSummaries, pushChunk, closeChannel, resolveUsage, resolveFinish, responseFormat, } = args;
1982
+ const { maxSteps, modelId, url, fetchImpl, abortSignal, options, conversation, openAITools, openAIToolChoice, toolsRecord, toolNameFromWire, emitter, toolsUsed, toolExecutionSummaries, pushChunk, closeChannel, resolveUsage, resolveFinish, responseFormat, onModelObserved, } = args;
1956
1983
  // Hoisted above the try so the catch can resolve the usage accumulated
1957
1984
  // by steps that completed BEFORE the failure — those steps were billed.
1958
1985
  let stepFinish = null;
@@ -2060,6 +2087,15 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
2060
2087
  if (stepResult.usage) {
2061
2088
  stepUsage = mergeUsage(stepUsage, stepResult.usage);
2062
2089
  }
2090
+ // Cell-3 identity fix: `stepResult.model` is what the SERVER echoed
2091
+ // on the wire (parseSSEStream captures `chunk.model`), which can
2092
+ // differ from the pre-call resolved `modelId` a gateway/router was
2093
+ // asked for. Surfaced via callback rather than the method's return
2094
+ // value so `executeStream` can update the already-returned
2095
+ // `StreamResult` in place (see `onModelObserved` wiring there).
2096
+ if (stepResult.model) {
2097
+ onModelObserved?.(stepResult.model);
2098
+ }
2063
2099
  if (stepResult.toolCalls.size === 0) {
2064
2100
  break;
2065
2101
  }
@@ -1,7 +1,7 @@
1
1
  import type { AIProviderName } from "../constants/enums.js";
2
2
  import { BaseProvider } from "../core/baseProvider.js";
3
3
  import { ProviderError } from "../types/index.js";
4
- import type { DecisionError, DecisionQuestion, DecisionRequest, DecisionResult, DecisionState, LanguageModel, StreamOptions, StreamResult, ValidationSchema } from "../types/index.js";
4
+ import type { DecisionError, DecisionQuestion, DecisionRequest, DecisionResult, DecisionPreparedMedia, DecisionState, LanguageModel, StreamOptions, StreamResult, ValidationSchema } from "../types/index.js";
5
5
  /**
6
6
  * Generous enough for a cold start (measured at 2.0–2.7s after idle on Jev)
7
7
  * while still bounded. Every internal caller is fail-open, so this only bites
@@ -16,6 +16,15 @@ export declare const asNumber: (v: unknown) => number | undefined;
16
16
  * request back — possibly the whole state — so it is never surfaced.
17
17
  */
18
18
  export declare function describeValidationErrors(detail: readonly unknown[]): string;
19
+ /**
20
+ * A LiteLLM proxy echoes credentials back in its error texts, in more than one
21
+ * wording: a masked key plus its hash on a rejected key, the whole key when it
22
+ * does not look like a LiteLLM key, a key hash on a rate limit. None of it
23
+ * belongs in an error message or a log, whatever the wording, so the
24
+ * configured key, anything shaped like a LiteLLM key, any long hex run and any
25
+ * embedded `data:` URL are all removed.
26
+ */
27
+ export declare function redactCredentials(message: string, apiKey: string): string;
19
28
  /**
20
29
  * The shared half of every "System One" decision provider — a model that takes
21
30
  * one `state` plus typed questions and returns one typed answer each, with no
@@ -52,7 +61,7 @@ export declare abstract class SystemOneDecisionProvider extends BaseProvider {
52
61
  protected missingConfigMessage(): string | undefined;
53
62
  protected abstract decisionEndpoint(): string;
54
63
  protected abstract decisionHeaders(): Record<string, string>;
55
- protected abstract buildDecisionBody(state: DecisionState, questions: Record<string, Record<string, unknown>>, model: string): Record<string, unknown>;
64
+ protected abstract buildDecisionBody(state: DecisionState, questions: Record<string, Record<string, unknown>>, model: string, media?: DecisionPreparedMedia): Record<string, unknown>;
56
65
  protected abstract parseDecisionError(status: number, payload: unknown, requestId: string | undefined): DecisionError;
57
66
  protected abstract readRequestId(headers: Headers): string | undefined;
58
67
  /**
@@ -87,6 +96,16 @@ export declare abstract class SystemOneDecisionProvider extends BaseProvider {
87
96
  * which returns null instead.
88
97
  */
89
98
  decide(request: DecisionRequest): Promise<DecisionResult>;
99
+ /**
100
+ * Turn the request's images and video into data URLs, or refuse it. Refused
101
+ * as a non-retryable `invalid_request`: resending the same media cannot help.
102
+ */
103
+ private prepareMedia;
104
+ /**
105
+ * The encoded body, not just the media, is what the server's size limit
106
+ * applies to.
107
+ */
108
+ private assertWithinRequestBytes;
90
109
  /**
91
110
  * Refuse a request the model cannot read in full. An encoder cuts the state
92
111
  * off past its window without saying so, so without this a decision would