@juspay/neurolink 12.37.5 → 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.
- package/CHANGELOG.md +3 -4
- package/README.md +59 -49
- package/dist/browser/neurolink.min.js +391 -391
- package/dist/cli/commands/decide.d.ts +2 -2
- package/dist/cli/commands/decide.js +22 -3
- package/dist/cli/commands/setup.js +2 -1
- package/dist/cli/factories/commandFactory.js +1 -1
- package/dist/constants/enums.d.ts +9 -0
- package/dist/constants/enums.js +10 -0
- package/dist/factories/providerDescriptors.d.ts +2 -0
- package/dist/factories/providerDescriptors.js +52 -6
- package/dist/factories/providerRegistry.js +9 -1
- package/dist/models/manifestRegistry.js +2 -0
- package/dist/models/manifests/xor.d.ts +12 -0
- package/dist/models/manifests/xor.js +30 -0
- package/dist/neurolink.js +6 -0
- package/dist/providers/laya.js +1 -19
- package/dist/providers/systemOneDecision.d.ts +21 -2
- package/dist/providers/systemOneDecision.js +64 -4
- package/dist/providers/xor.d.ts +48 -0
- package/dist/providers/xor.js +192 -0
- package/dist/types/classifierRouter.d.ts +5 -4
- package/dist/types/cli.d.ts +6 -2
- package/dist/types/decision.d.ts +60 -1
- package/dist/types/providers.d.ts +10 -0
- package/dist/utils/decisionMedia.d.ts +9 -0
- package/dist/utils/decisionMedia.js +160 -0
- package/dist/utils/modelChoices.js +9 -1
- package/dist/utils/providerConfig.d.ts +5 -0
- package/dist/utils/providerConfig.js +17 -0
- package/docs-site/static/search-index.json +51 -34
- 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,
|
|
12
|
-
* and
|
|
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,
|
|
12
|
-
* and
|
|
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,
|
|
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
|
+
}
|
package/dist/constants/enums.js
CHANGED
|
@@ -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 = {}));
|
|
@@ -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
|
|
455
|
-
// the first configured
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
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 }
|
package/dist/providers/laya.js
CHANGED
|
@@ -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
|
*
|
|
@@ -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
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
2
|
-
import { PROVIDER_DESCRIPTORS_BY_NAME } from "../factories/providerDescriptors.js";
|
|
2
|
+
import { listMediaDecisionProviders, PROVIDER_DESCRIPTORS_BY_NAME, } from "../factories/providerDescriptors.js";
|
|
3
3
|
import { isNeuroLink } from "../neurolink.js";
|
|
4
4
|
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
5
5
|
import { ProviderError } from "../types/index.js";
|
|
6
|
+
import { prepareDecisionMedia } from "../utils/decisionMedia.js";
|
|
6
7
|
import { logger } from "../utils/logger.js";
|
|
8
|
+
import { redactUrlsInText } from "../utils/logSanitize.js";
|
|
7
9
|
import { estimateTokens, serializeForEstimate, } from "../utils/tokenEstimation.js";
|
|
8
10
|
/**
|
|
9
11
|
* Generous enough for a cold start (measured at 2.0–2.7s after idle on Jev)
|
|
@@ -128,6 +130,25 @@ export function describeValidationErrors(detail) {
|
|
|
128
130
|
.slice(0, 5);
|
|
129
131
|
return fields.join("; ") || "unspecified field";
|
|
130
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* A LiteLLM proxy echoes credentials back in its error texts, in more than one
|
|
135
|
+
* wording: a masked key plus its hash on a rejected key, the whole key when it
|
|
136
|
+
* does not look like a LiteLLM key, a key hash on a rate limit. None of it
|
|
137
|
+
* belongs in an error message or a log, whatever the wording, so the
|
|
138
|
+
* configured key, anything shaped like a LiteLLM key, any long hex run and any
|
|
139
|
+
* embedded `data:` URL are all removed.
|
|
140
|
+
*/
|
|
141
|
+
export function redactCredentials(message, apiKey) {
|
|
142
|
+
const withoutKey = apiKey
|
|
143
|
+
? message.split(apiKey).join("[redacted]")
|
|
144
|
+
: message;
|
|
145
|
+
return withoutKey
|
|
146
|
+
.replace(/\.?\s*Received API Key\s*=[\s\S]*$/, "")
|
|
147
|
+
.replace(/\bsk-[A-Za-z0-9._-]+/g, "[redacted]")
|
|
148
|
+
.replace(/\b[0-9a-f]{32,}\b/gi, "[redacted]")
|
|
149
|
+
.replace(/data:[a-z]+\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+/gi, "[media]")
|
|
150
|
+
.trim();
|
|
151
|
+
}
|
|
131
152
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
132
153
|
/**
|
|
133
154
|
* `estimateTokens` assumes ~4 characters per token. That holds for English
|
|
@@ -281,7 +302,9 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
281
302
|
id,
|
|
282
303
|
this.encodeQuestion(question),
|
|
283
304
|
]));
|
|
284
|
-
const
|
|
305
|
+
const media = await this.prepareMedia(request);
|
|
306
|
+
const body = JSON.stringify(this.buildDecisionBody(request.state, wireQuestions, resolvedModel, media));
|
|
307
|
+
this.assertWithinRequestBytes(body);
|
|
285
308
|
const timeoutMs = request.timeoutMs ??
|
|
286
309
|
this.getDescriptorDecideMs() ??
|
|
287
310
|
this.defaultTimeout ??
|
|
@@ -369,6 +392,7 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
369
392
|
requestId,
|
|
370
393
|
latencyMs,
|
|
371
394
|
upstreamMs,
|
|
395
|
+
...(media ? { mediaBytes: media.bytes } : {}),
|
|
372
396
|
};
|
|
373
397
|
}
|
|
374
398
|
catch (error) {
|
|
@@ -381,7 +405,7 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
381
405
|
const isTimeout = error instanceof Error && error.name === "TimeoutError";
|
|
382
406
|
lastError = {
|
|
383
407
|
kind: aborted ? "network" : isTimeout ? "timeout" : "network",
|
|
384
|
-
message: error instanceof Error ? error.message : String(error),
|
|
408
|
+
message: redactCredentials(redactUrlsInText(error instanceof Error ? error.message : String(error)), this.decisionApiKey()),
|
|
385
409
|
retryable: !aborted,
|
|
386
410
|
};
|
|
387
411
|
if (aborted || attempt === this.maxRetries) {
|
|
@@ -396,6 +420,41 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
396
420
|
retryable: false,
|
|
397
421
|
});
|
|
398
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Turn the request's images and video into data URLs, or refuse it. Refused
|
|
425
|
+
* as a non-retryable `invalid_request`: resending the same media cannot help.
|
|
426
|
+
*/
|
|
427
|
+
async prepareMedia(request) {
|
|
428
|
+
const limits = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName)?.decisionLimits;
|
|
429
|
+
const prepared = await prepareDecisionMedia(request, limits?.media, this.vendorLabel(), listMediaDecisionProviders());
|
|
430
|
+
if (prepared.status === "refused") {
|
|
431
|
+
throw this.decisionError({
|
|
432
|
+
kind: "invalid_request",
|
|
433
|
+
message: prepared.message,
|
|
434
|
+
retryable: false,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return prepared.media;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* The encoded body, not just the media, is what the server's size limit
|
|
441
|
+
* applies to.
|
|
442
|
+
*/
|
|
443
|
+
assertWithinRequestBytes(body) {
|
|
444
|
+
const limit = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName)
|
|
445
|
+
?.decisionLimits?.media?.maxRequestBytes;
|
|
446
|
+
if (limit === undefined) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const bytes = Buffer.byteLength(body);
|
|
450
|
+
if (bytes > limit) {
|
|
451
|
+
throw this.decisionError({
|
|
452
|
+
kind: "invalid_request",
|
|
453
|
+
message: `The request is ${bytes} bytes; ${this.vendorLabel()} accepts at most ${limit}. Send fewer or smaller images, or a shorter video.`,
|
|
454
|
+
retryable: false,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
399
458
|
/**
|
|
400
459
|
* Refuse a request the model cannot read in full. An encoder cuts the state
|
|
401
460
|
* off past its window without saying so, so without this a decision would
|
|
@@ -407,7 +466,8 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
407
466
|
return;
|
|
408
467
|
}
|
|
409
468
|
const label = this.vendorLabel();
|
|
410
|
-
if (
|
|
469
|
+
if (limits.maxQuestions !== undefined &&
|
|
470
|
+
questionCount > limits.maxQuestions) {
|
|
411
471
|
throw this.decisionError({
|
|
412
472
|
kind: "max_tokens_exceeded",
|
|
413
473
|
message: `${label} accepts at most ${limits.maxQuestions} questions per request; this one has ${questionCount}. Ask fewer, or use a decision provider without this cap.`,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { DecisionError, DecisionPreparedMedia, DecisionState, NeurolinkCredentials } from "../types/index.js";
|
|
2
|
+
import { SystemOneDecisionProvider } from "./systemOneDecision.js";
|
|
3
|
+
/**
|
|
4
|
+
* XOR Provider — the `decide` inference type only.
|
|
5
|
+
*
|
|
6
|
+
* XOR is Juspay's open-weights "System One" decision model, post-trained from
|
|
7
|
+
* Qwen3.6-35B-A3B: the same typed `noul` / `choice` / `score` questions as
|
|
8
|
+
* TypeSafe's Jev, on the same wire, in one batched pass. Unlike Jev and Laya it
|
|
9
|
+
* also reads images and one video. The request loop, retries, auth circuit
|
|
10
|
+
* breaker and answer parsing are shared with every decision provider in
|
|
11
|
+
* {@link SystemOneDecisionProvider}.
|
|
12
|
+
*
|
|
13
|
+
* @see https://huggingface.co/juspay/xor
|
|
14
|
+
*/
|
|
15
|
+
export declare class XorProvider extends SystemOneDecisionProvider {
|
|
16
|
+
private readonly apiKey;
|
|
17
|
+
private readonly baseURL;
|
|
18
|
+
constructor(modelName?: string, sdk?: unknown, _region?: string, credentials?: NeurolinkCredentials["xor"]);
|
|
19
|
+
protected getDefaultModel(): string;
|
|
20
|
+
protected vendorLabel(): string;
|
|
21
|
+
protected vendorDisplayName(): string;
|
|
22
|
+
protected decisionApiKey(): string;
|
|
23
|
+
protected missingKeyMessage(): string;
|
|
24
|
+
protected missingConfigMessage(): string | undefined;
|
|
25
|
+
protected decisionEndpoint(): string;
|
|
26
|
+
protected decisionHeaders(): Record<string, string>;
|
|
27
|
+
/**
|
|
28
|
+
* `model` is always sent. A LiteLLM proxy checks team access against the
|
|
29
|
+
* body's `model`, and refuses a request that names none. `images` is
|
|
30
|
+
* omitted rather than sent empty, which the server rejects.
|
|
31
|
+
*/
|
|
32
|
+
protected buildDecisionBody(state: DecisionState, questions: Record<string, Record<string, unknown>>, model: string, media?: DecisionPreparedMedia): Record<string, unknown>;
|
|
33
|
+
/**
|
|
34
|
+
* A 403 is deliberately NOT `authentication`: on a LiteLLM proxy it means the
|
|
35
|
+
* team's allow-list lacks the model, which an admin can fix at any time, and
|
|
36
|
+
* `authentication` would trip the per-instance breaker and keep XOR disabled
|
|
37
|
+
* until restart even after access is granted. A 402 (a budget) is the same
|
|
38
|
+
* kind of fixable gap. `DecisionErrorKind` has no billing kind, so both map
|
|
39
|
+
* to `invalid_request`.
|
|
40
|
+
*/
|
|
41
|
+
protected parseDecisionError(status: number, payload: unknown, requestId: string | undefined): DecisionError;
|
|
42
|
+
/**
|
|
43
|
+
* XOR's server sets `x-request-id` and `x-typesafe-request-id` to one value.
|
|
44
|
+
* A proxy in front may replace or drop them, so its own call id is the last
|
|
45
|
+
* resort.
|
|
46
|
+
*/
|
|
47
|
+
protected readRequestId(headers: Headers): string | undefined;
|
|
48
|
+
}
|