@juspay/neurolink 13.4.10 → 13.5.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 +3 -2
- package/dist/browser/neurolink.min.js +419 -419
- package/dist/cli/commands/decide.js +25 -5
- package/dist/factories/providerDescriptors.d.ts +4 -3
- package/dist/factories/providerDescriptors.js +6 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/neurolink.d.ts +4 -2
- package/dist/neurolink.js +19 -4
- package/dist/providers/catalog/provider-catalog.schema.json +64 -3
- package/dist/providers/catalog/schema.js +81 -1
- package/dist/providers/catalog/xor.json +30 -10
- package/dist/providers/configuredDecision.d.ts +8 -1
- package/dist/providers/configuredDecision.js +22 -0
- package/dist/providers/decide/dialects/openaiDecisions.js +8 -0
- package/dist/providers/decide/dialects/systemOne.js +28 -2
- package/dist/providers/decide/guards.d.ts +6 -0
- package/dist/providers/decide/guards.js +22 -0
- package/dist/providers/systemOneDecision.d.ts +30 -7
- package/dist/providers/systemOneDecision.js +71 -14
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/decision.d.ts +117 -10
- package/dist/types/providerCatalog.d.ts +12 -1
- package/dist/types/providers.d.ts +2 -0
- package/dist/utils/decisionAnswers.d.ts +7 -1
- package/dist/utils/decisionAnswers.js +43 -2
- package/dist/utils/decisionLimits.d.ts +13 -1
- package/dist/utils/decisionLimits.js +52 -1
- package/dist/utils/decisionMedia.d.ts +4 -2
- package/dist/utils/decisionMedia.js +216 -14
- package/docs-site/static/search-index.json +24 -21
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ import { NeuroLink } from "../../neurolink.js";
|
|
|
23
23
|
import { readDecisionChoice } from "../../utils/decisionAnswers.js";
|
|
24
24
|
import { calculateCost, hasPricing } from "../../utils/pricing.js";
|
|
25
25
|
import { logger } from "../../utils/logger.js";
|
|
26
|
-
const VALID_QUESTION_TYPES = new Set(["boolean", "choice", "score"]);
|
|
26
|
+
const VALID_QUESTION_TYPES = new Set(["boolean", "choice", "score", "value"]);
|
|
27
27
|
function isRecord(value) {
|
|
28
28
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
29
|
}
|
|
@@ -123,6 +123,10 @@ function formatAnswerLine(id, answers) {
|
|
|
123
123
|
}
|
|
124
124
|
case "score":
|
|
125
125
|
return `${id}: score ${formatNumber(answer.score)}, confidence ${formatNumber(answer.confidence)}`;
|
|
126
|
+
case "value":
|
|
127
|
+
return answer.value === null
|
|
128
|
+
? `${id}: no value found, confidence ${formatNumber(answer.confidence)}`
|
|
129
|
+
: `${id}: value ${JSON.stringify(answer.value)}, confidence ${formatNumber(answer.confidence)}`;
|
|
126
130
|
default:
|
|
127
131
|
return `${id}: (unrecognised answer type)`;
|
|
128
132
|
}
|
|
@@ -195,12 +199,17 @@ export const decideCommand = {
|
|
|
195
199
|
type: "string",
|
|
196
200
|
array: true,
|
|
197
201
|
nargs: 1,
|
|
198
|
-
describe: "Image for the model to read: a file path or a data: URL. Repeat for several (
|
|
202
|
+
describe: "Image for the model to read: a file path or a data: URL. Repeat for several (Perplexity and OpenAI Decisions take up to 8, Cloudflare Clef up to 4, XOR 1)",
|
|
199
203
|
})
|
|
200
204
|
.option("video", {
|
|
201
205
|
type: "string",
|
|
202
206
|
nargs: 1,
|
|
203
|
-
describe: "One video for the model to read: a file path or a data: URL",
|
|
207
|
+
describe: "One video for the model to read: a file path or a data: URL. No current decision provider reads video, so it is refused before any request",
|
|
208
|
+
})
|
|
209
|
+
.option("audio", {
|
|
210
|
+
type: "string",
|
|
211
|
+
nargs: 1,
|
|
212
|
+
describe: "One audio clip for the model to hear: a WAV, FLAC, Ogg or MP3 file path or a data: URL (XOR with its xor-omni model; the first 30 seconds are heard). State may then be omitted",
|
|
204
213
|
})
|
|
205
214
|
.option("timeout", {
|
|
206
215
|
type: "number",
|
|
@@ -221,7 +230,8 @@ export const decideCommand = {
|
|
|
221
230
|
})
|
|
222
231
|
.example('$0 decide "Refund request for a damaged item" --questions \'{"urgent":{"type":"boolean","instructions":"Is this urgent?"}}\'', "Ask a single yes/no question")
|
|
223
232
|
.example("$0 decide --state-file ticket.json --questions-file questions.json --format json", "Read state and questions from files, emit raw JSON")
|
|
224
|
-
.example('$0 decide "What color is this?" --provider xor --model xor-omni --image ./photo.png --questions \'{"color":{"type":"choice","instructions":"What color is the image?","criteria":{"red":"red","blue":"blue"}}}\'', "Ask about an image (XOR with its xor-omni model, Perplexity, Cloudflare Clef and OpenAI Decisions read images)")
|
|
233
|
+
.example('$0 decide "What color is this?" --provider xor --model xor-omni --image ./photo.png --questions \'{"color":{"type":"choice","instructions":"What color is the image?","criteria":{"red":"red","blue":"blue"}}}\'', "Ask about an image (XOR with its xor-omni model, Perplexity, Cloudflare Clef and OpenAI Decisions read images)")
|
|
234
|
+
.example('$0 decide "Invoice INV-2024-0892 for 18,275 rupees" --provider xor --model xor-omni --questions \'{"amount":{"type":"value","dtype":"int","instructions":"Invoice amount?"}}\'', "Extract a typed value (XOR)"),
|
|
225
235
|
handler: async (argv) => {
|
|
226
236
|
const outputFormat = argv.format ?? "text";
|
|
227
237
|
// --- Validate before any provider work ---------------------------------
|
|
@@ -256,8 +266,13 @@ export const decideCommand = {
|
|
|
256
266
|
logger.error(chalk.red("Error: pass at most one --video."));
|
|
257
267
|
process.exit(1);
|
|
258
268
|
}
|
|
269
|
+
const audios = [].concat(argv.audio ?? []);
|
|
270
|
+
if (audios.length > 1) {
|
|
271
|
+
logger.error(chalk.red("Error: pass at most one --audio."));
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
259
274
|
if (!isDecisionQuestionMap(parsedQuestions)) {
|
|
260
|
-
logger.error(chalk.red('Error: questions must be a non-empty object of { type: "boolean" | "choice" | "score", ... }.'));
|
|
275
|
+
logger.error(chalk.red('Error: questions must be a non-empty object of { type: "boolean" | "choice" | "score" | "value", ... }.'));
|
|
261
276
|
process.exit(1);
|
|
262
277
|
}
|
|
263
278
|
let state;
|
|
@@ -268,6 +283,10 @@ export const decideCommand = {
|
|
|
268
283
|
logger.error(chalk.red(`Error: could not read state file — ${error instanceof Error ? error.message : String(error)}`));
|
|
269
284
|
process.exit(1);
|
|
270
285
|
}
|
|
286
|
+
// An audio clip can be the whole input.
|
|
287
|
+
if (state === undefined && audios.length === 1) {
|
|
288
|
+
state = "";
|
|
289
|
+
}
|
|
271
290
|
if (state === undefined) {
|
|
272
291
|
logger.error(chalk.red("Error: provide state as a positional argument or via --state-file."));
|
|
273
292
|
process.exit(1);
|
|
@@ -284,6 +303,7 @@ export const decideCommand = {
|
|
|
284
303
|
timeoutMs: argv.timeout,
|
|
285
304
|
...(argv.image && argv.image.length > 0 ? { images: argv.image } : {}),
|
|
286
305
|
...(videos.length === 1 ? { video: videos[0] } : {}),
|
|
306
|
+
...(audios.length === 1 ? { audio: audios[0] } : {}),
|
|
287
307
|
});
|
|
288
308
|
spinner?.succeed("Decision complete");
|
|
289
309
|
if (outputFormat === "json") {
|
|
@@ -45,12 +45,13 @@ export declare const DECISION_PROVIDERS: readonly ProviderDescriptor[];
|
|
|
45
45
|
*/
|
|
46
46
|
export declare function resolveDefaultDecisionProvider(credentials?: NeurolinkCredentials): string | undefined;
|
|
47
47
|
/**
|
|
48
|
-
* Names of the decision providers that accept images, or, with `video
|
|
49
|
-
* ones that accept
|
|
50
|
-
* the same request is worse than none.
|
|
48
|
+
* Names of the decision providers that accept images, or, with `video` or
|
|
49
|
+
* `audio`, the ones that accept that too: a hint that names a provider which
|
|
50
|
+
* would refuse the same request is worse than none.
|
|
51
51
|
*/
|
|
52
52
|
export declare function listMediaDecisionProviders(needs?: {
|
|
53
53
|
video?: boolean;
|
|
54
|
+
audio?: boolean;
|
|
54
55
|
}): string[];
|
|
55
56
|
/**
|
|
56
57
|
* The variables that would configure each decision provider, one clause per
|
|
@@ -909,14 +909,16 @@ export function resolveDefaultDecisionProvider(credentials) {
|
|
|
909
909
|
return DECISION_PROVIDERS.find((descriptor) => isDecisionProviderConfigured(descriptor, credentials))?.name;
|
|
910
910
|
}
|
|
911
911
|
/**
|
|
912
|
-
* Names of the decision providers that accept images, or, with `video
|
|
913
|
-
* ones that accept
|
|
914
|
-
* the same request is worse than none.
|
|
912
|
+
* Names of the decision providers that accept images, or, with `video` or
|
|
913
|
+
* `audio`, the ones that accept that too: a hint that names a provider which
|
|
914
|
+
* would refuse the same request is worse than none.
|
|
915
915
|
*/
|
|
916
916
|
export function listMediaDecisionProviders(needs) {
|
|
917
917
|
return DECISION_PROVIDERS.filter((descriptor) => {
|
|
918
918
|
const media = descriptor.decisionLimits?.media;
|
|
919
|
-
return media !== undefined &&
|
|
919
|
+
return (media !== undefined &&
|
|
920
|
+
(!needs?.video || media.video) &&
|
|
921
|
+
(!needs?.audio || media.audio !== undefined));
|
|
920
922
|
}).map((descriptor) => descriptor.name);
|
|
921
923
|
}
|
|
922
924
|
/** "a", "a or b", "a, b or c". */
|
package/dist/index.d.ts
CHANGED
|
@@ -416,7 +416,7 @@ export { classifyProviderError, ModelPool, createDefaultRequestRouter, Classifie
|
|
|
416
416
|
* if (team && team.confidence > 0.7) { route(team.choice); }
|
|
417
417
|
* ```
|
|
418
418
|
*/
|
|
419
|
-
export { decisionBooleanConfidence, decisionKey, gateDecisionBoolean, HOST_DECISION_NAMESPACE, isDecisionQuestion, readDecisionBoolean, readDecisionChoice, readDecisionScore, servesInferenceKind, } from "./utils/decisionAnswers.js";
|
|
419
|
+
export { decisionBooleanConfidence, decisionKey, gateDecisionBoolean, HOST_DECISION_NAMESPACE, isDecisionQuestion, readDecisionBoolean, readDecisionChoice, readDecisionScore, readDecisionValue, servesInferenceKind, } from "./utils/decisionAnswers.js";
|
|
420
420
|
export { estimateDecisionStateTokens, resolveDecisionLimitsReading, } from "./utils/decisionLimits.js";
|
|
421
421
|
export { DECISION_PROVIDERS, resolveDefaultDecisionProvider, } from "./factories/providerDescriptors.js";
|
|
422
422
|
export { TYPESAFE_MAX_REQUEST_TOKENS, TYPESAFE_MAX_STATE_TOKENS, } from "./providers/typesafe.js";
|
package/dist/index.js
CHANGED
|
@@ -633,7 +633,7 @@ export { classifyProviderError, ModelPool, createDefaultRequestRouter, Classifie
|
|
|
633
633
|
* if (team && team.confidence > 0.7) { route(team.choice); }
|
|
634
634
|
* ```
|
|
635
635
|
*/
|
|
636
|
-
export { decisionBooleanConfidence, decisionKey, gateDecisionBoolean, HOST_DECISION_NAMESPACE, isDecisionQuestion, readDecisionBoolean, readDecisionChoice, readDecisionScore, servesInferenceKind, } from "./utils/decisionAnswers.js";
|
|
636
|
+
export { decisionBooleanConfidence, decisionKey, gateDecisionBoolean, HOST_DECISION_NAMESPACE, isDecisionQuestion, readDecisionBoolean, readDecisionChoice, readDecisionScore, readDecisionValue, servesInferenceKind, } from "./utils/decisionAnswers.js";
|
|
637
637
|
// The estimator the pre-flight refusal uses, and the flattening behind
|
|
638
638
|
// `neurolink.decisionLimits()` — exported so a host measures a state with the
|
|
639
639
|
// very function that will judge it.
|
package/dist/neurolink.d.ts
CHANGED
|
@@ -2694,12 +2694,14 @@ export declare class NeuroLink {
|
|
|
2694
2694
|
* many would have the WHOLE request refused, and NeuroLink's own routing
|
|
2695
2695
|
* would silently degrade to the heuristic — the host's problem would
|
|
2696
2696
|
* become NeuroLink's. A provider that declares no `maxQuestions`
|
|
2697
|
-
* (TypeSafe
|
|
2697
|
+
* (TypeSafe) takes every well-formed addition. For the same reason a
|
|
2698
|
+
* question the provider cannot answer (a type it does not list, a choice
|
|
2699
|
+
* or score past its cap) is dropped on its own.
|
|
2698
2700
|
* 4. Answers are split: the consumer sees only its own ids, exactly as it
|
|
2699
2701
|
* would without hooks; `onAnswers` gets the host's answers under the
|
|
2700
2702
|
* host's original ids plus the full result.
|
|
2701
2703
|
*
|
|
2702
|
-
* The request's `images` and `
|
|
2704
|
+
* The request's `images`, `video` and `audio` ride along untouched — `merged` and
|
|
2703
2705
|
* the wire call spread the request, and only `state`, the questions and
|
|
2704
2706
|
* the result are cloned — so an XOR request keeps its Buffers and its
|
|
2705
2707
|
* result keeps `mediaBytes`.
|
package/dist/neurolink.js
CHANGED
|
@@ -37,7 +37,7 @@ import { describeDecisionProviderKeys, PROVIDER_ALIAS_INDEX, PROVIDER_DESCRIPTOR
|
|
|
37
37
|
import { describeSTTProviderKeys, getSTTProviderDescriptor, listConfiguredSTTProviders, resolveCallSTTEndpoints, resolveDefaultSTTProvider, resolveSTTProviderName, } from "./factories/sttDescriptors.js";
|
|
38
38
|
import { MAX_PARALLEL_DECISION_BATCHES, mergeDecisionResults, splitDecisionRequest, } from "./utils/decisionBatches.js";
|
|
39
39
|
import { decisionKey, HOST_DECISION_NAMESPACE, isHostDecisionKey, snapshotDecisionQuestion, stripHostAnswers, } from "./utils/decisionAnswers.js";
|
|
40
|
-
import { resolveDecisionLimitsReading } from "./utils/decisionLimits.js";
|
|
40
|
+
import { resolveDecisionLimitsReading, unsupportedDecisionQuestion, } from "./utils/decisionLimits.js";
|
|
41
41
|
import { isDirectTTSRequest, resolveRequestKind, } from "./core/resolveRequestKind.js";
|
|
42
42
|
import { createToolEventPayload } from "./core/toolEvents.js";
|
|
43
43
|
import { ProviderFactory } from "./factories/providerFactory.js";
|
|
@@ -14046,12 +14046,14 @@ Current user's request: ${currentInput}`;
|
|
|
14046
14046
|
* many would have the WHOLE request refused, and NeuroLink's own routing
|
|
14047
14047
|
* would silently degrade to the heuristic — the host's problem would
|
|
14048
14048
|
* become NeuroLink's. A provider that declares no `maxQuestions`
|
|
14049
|
-
* (TypeSafe
|
|
14049
|
+
* (TypeSafe) takes every well-formed addition. For the same reason a
|
|
14050
|
+
* question the provider cannot answer (a type it does not list, a choice
|
|
14051
|
+
* or score past its cap) is dropped on its own.
|
|
14050
14052
|
* 4. Answers are split: the consumer sees only its own ids, exactly as it
|
|
14051
14053
|
* would without hooks; `onAnswers` gets the host's answers under the
|
|
14052
14054
|
* host's original ids plus the full result.
|
|
14053
14055
|
*
|
|
14054
|
-
* The request's `images` and `
|
|
14056
|
+
* The request's `images`, `video` and `audio` ride along untouched — `merged` and
|
|
14055
14057
|
* the wire call spread the request, and only `state`, the questions and
|
|
14056
14058
|
* the result are cloned — so an XOR request keeps its Buffers and its
|
|
14057
14059
|
* result keeps `mediaBytes`.
|
|
@@ -14145,7 +14147,7 @@ Current user's request: ${currentInput}`;
|
|
|
14145
14147
|
model: merged.model,
|
|
14146
14148
|
credentials: merged.credentials,
|
|
14147
14149
|
});
|
|
14148
|
-
// A provider without a question-count cap (TypeSafe
|
|
14150
|
+
// A provider without a question-count cap (TypeSafe) takes
|
|
14149
14151
|
// every well-formed addition; its token/byte limits still apply
|
|
14150
14152
|
// and are what `decisionLimits()` reports for sizing.
|
|
14151
14153
|
const room = limits?.maxQuestions !== undefined
|
|
@@ -14155,6 +14157,7 @@ Current user's request: ${currentInput}`;
|
|
|
14155
14157
|
...ownQuestions,
|
|
14156
14158
|
};
|
|
14157
14159
|
let invalidCount = 0;
|
|
14160
|
+
let unsupportedCount = 0;
|
|
14158
14161
|
let overflowCount = 0;
|
|
14159
14162
|
for (const [hostId, proposal] of proposedEntries) {
|
|
14160
14163
|
// The wire carries a snapshot of the host's question, never the
|
|
@@ -14164,6 +14167,12 @@ Current user's request: ${currentInput}`;
|
|
|
14164
14167
|
invalidCount += 1;
|
|
14165
14168
|
continue;
|
|
14166
14169
|
}
|
|
14170
|
+
// A question the provider cannot read would have the whole
|
|
14171
|
+
// request refused, NeuroLink's own questions with it.
|
|
14172
|
+
if (unsupportedDecisionQuestion(question, limits) !== undefined) {
|
|
14173
|
+
unsupportedCount += 1;
|
|
14174
|
+
continue;
|
|
14175
|
+
}
|
|
14167
14176
|
if (hostIdByWireId.size >= room) {
|
|
14168
14177
|
overflowCount += 1;
|
|
14169
14178
|
continue;
|
|
@@ -14175,6 +14184,9 @@ Current user's request: ${currentInput}`;
|
|
|
14175
14184
|
if (invalidCount > 0) {
|
|
14176
14185
|
logger.warn(`[Decision] dropped ${invalidCount} malformed host question(s) for site "${site}"`);
|
|
14177
14186
|
}
|
|
14187
|
+
if (unsupportedCount > 0) {
|
|
14188
|
+
logger.warn(`[Decision] dropped ${unsupportedCount} host question(s) for site "${site}" that ${limits?.provider ?? "the decision provider"} cannot answer`);
|
|
14189
|
+
}
|
|
14178
14190
|
if (overflowCount > 0 && limits) {
|
|
14179
14191
|
logger.warn(`[Decision] dropped ${overflowCount} host question(s) for site "${site}": ${limits.provider} accepts ${limits.maxQuestions} per request and NeuroLink asks ${ownCount}`);
|
|
14180
14192
|
}
|
|
@@ -14306,6 +14318,7 @@ Current user's request: ${currentInput}`;
|
|
|
14306
14318
|
"ai.provider": providerName,
|
|
14307
14319
|
"decision.question_count": questionCount,
|
|
14308
14320
|
"decision.images.count": options.images?.length ?? 0,
|
|
14321
|
+
"decision.audio.count": options.audio === undefined ? 0 : 1,
|
|
14309
14322
|
...siteSpanAttributes,
|
|
14310
14323
|
},
|
|
14311
14324
|
}, async (otelSpan) => {
|
|
@@ -14335,6 +14348,7 @@ Current user's request: ${currentInput}`;
|
|
|
14335
14348
|
"ai.model": options.model ?? "",
|
|
14336
14349
|
"decision.question_count": questionCount,
|
|
14337
14350
|
"decision.images.count": options.images?.length ?? 0,
|
|
14351
|
+
"decision.audio.count": options.audio === undefined ? 0 : 1,
|
|
14338
14352
|
...siteSpanAttributes,
|
|
14339
14353
|
}, parentSpanId, traceId);
|
|
14340
14354
|
try {
|
|
@@ -14348,6 +14362,7 @@ Current user's request: ${currentInput}`;
|
|
|
14348
14362
|
questions: options.questions,
|
|
14349
14363
|
images: options.images,
|
|
14350
14364
|
video: options.video,
|
|
14365
|
+
audio: options.audio,
|
|
14351
14366
|
model: options.model,
|
|
14352
14367
|
signal: options.signal,
|
|
14353
14368
|
timeoutMs: options.timeoutMs,
|
|
@@ -55,6 +55,25 @@
|
|
|
55
55
|
"properties": {
|
|
56
56
|
"maxStateTokens": { "type": "integer", "minimum": 1 },
|
|
57
57
|
"maxQuestions": { "type": "integer", "minimum": 1 },
|
|
58
|
+
"questionTypes": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"minItems": 1,
|
|
61
|
+
"uniqueItems": true,
|
|
62
|
+
"items": {
|
|
63
|
+
"enum": ["boolean", "choice", "score", "value", "choice-unknown"]
|
|
64
|
+
},
|
|
65
|
+
"description": "The question types the provider reads; choice-unknown is a choice that sets unknown: true. Absent means boolean, choice and score. openai-decisions may list only boolean, choice and score."
|
|
66
|
+
},
|
|
67
|
+
"maxScoreLevels": {
|
|
68
|
+
"type": "integer",
|
|
69
|
+
"minimum": 1,
|
|
70
|
+
"description": "Most levels one score question may have."
|
|
71
|
+
},
|
|
72
|
+
"maxChoiceOptions": {
|
|
73
|
+
"type": "integer",
|
|
74
|
+
"minimum": 1,
|
|
75
|
+
"description": "Most options one choice question may have."
|
|
76
|
+
},
|
|
58
77
|
"nonAsciiTokensPerChar": { "type": "number", "exclusiveMinimum": 0 },
|
|
59
78
|
"digitTokensPerChar": { "type": "number", "exclusiveMinimum": 0 },
|
|
60
79
|
"symbolTokensPerChar": { "type": "number", "exclusiveMinimum": 0 },
|
|
@@ -75,6 +94,19 @@
|
|
|
75
94
|
"type": "integer",
|
|
76
95
|
"minimum": 1,
|
|
77
96
|
"description": "Ceiling on the encoded request body, in bytes."
|
|
97
|
+
},
|
|
98
|
+
"audio": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"additionalProperties": false,
|
|
101
|
+
"required": ["maxSeconds"],
|
|
102
|
+
"description": "One audio clip is accepted. Requires decide.media.audioKey; not valid with openai-decisions.",
|
|
103
|
+
"properties": {
|
|
104
|
+
"maxSeconds": {
|
|
105
|
+
"type": "integer",
|
|
106
|
+
"minimum": 1,
|
|
107
|
+
"description": "Seconds of audio the model reads; a WAV, FLAC or Ogg clip that runs longer is refused."
|
|
108
|
+
}
|
|
109
|
+
}
|
|
78
110
|
}
|
|
79
111
|
}
|
|
80
112
|
},
|
|
@@ -154,10 +186,32 @@
|
|
|
154
186
|
"type": "object",
|
|
155
187
|
"additionalProperties": false,
|
|
156
188
|
"required": ["imagesKey"],
|
|
157
|
-
"description": "Body keys prepared media is written under, after model, state and questions: imagesKey only when the request carries an image, videoKey only when it carries a video, and
|
|
189
|
+
"description": "Body keys prepared media is written under, after model, state and questions: imagesKey only when the request carries an image, videoKey only when it carries a video, audioKey only when it carries audio, and all differ. With dialect system-one it requires decisionLimits.media; videoKey is required exactly when that allowance's video is true, and audioKey exactly when it sets audio. Not valid with openai-decisions.",
|
|
158
190
|
"properties": {
|
|
159
191
|
"imagesKey": { "$ref": "#/definitions/decideMediaKey" },
|
|
160
|
-
"videoKey": { "$ref": "#/definitions/decideMediaKey" }
|
|
192
|
+
"videoKey": { "$ref": "#/definitions/decideMediaKey" },
|
|
193
|
+
"audioKey": { "$ref": "#/definitions/decideMediaKey" }
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
"capableModel": {
|
|
197
|
+
"type": "object",
|
|
198
|
+
"additionalProperties": false,
|
|
199
|
+
"required": ["model", "questionTypes"],
|
|
200
|
+
"description": "A models.catalog key (not the default) the request is sent to instead of the default when the call names no model, no model variable is set, and the request carries media or a question of one of questionTypes; that model's decisionLimits row is the one checked. questionTypes must be listed in decisionLimits.questionTypes.",
|
|
201
|
+
"properties": {
|
|
202
|
+
"model": { "type": "string", "minLength": 1 },
|
|
203
|
+
"questionTypes": {
|
|
204
|
+
"type": "array",
|
|
205
|
+
"items": {
|
|
206
|
+
"enum": [
|
|
207
|
+
"boolean",
|
|
208
|
+
"choice",
|
|
209
|
+
"score",
|
|
210
|
+
"value",
|
|
211
|
+
"choice-unknown"
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
}
|
|
161
215
|
}
|
|
162
216
|
},
|
|
163
217
|
"ambientKeyGuard": {
|
|
@@ -570,6 +624,11 @@
|
|
|
570
624
|
"type": "integer",
|
|
571
625
|
"minimum": 1,
|
|
572
626
|
"description": "Decide entries only: the per-call budget for decide(), in milliseconds. When omitted, the base provider's default timeout applies."
|
|
627
|
+
},
|
|
628
|
+
"decideMsPerKStateTokens": {
|
|
629
|
+
"type": "integer",
|
|
630
|
+
"minimum": 1,
|
|
631
|
+
"description": "Decide entries only: added to decideMs for every 1,000 estimated state tokens, for a model whose latency grows with the state."
|
|
573
632
|
}
|
|
574
633
|
}
|
|
575
634
|
},
|
|
@@ -690,7 +749,9 @@
|
|
|
690
749
|
"defaultMaxOutputTokens"
|
|
691
750
|
]
|
|
692
751
|
},
|
|
693
|
-
"timeouts": {
|
|
752
|
+
"timeouts": {
|
|
753
|
+
"properties": { "decideMs": false, "decideMsPerKStateTokens": false }
|
|
754
|
+
}
|
|
694
755
|
}
|
|
695
756
|
},
|
|
696
757
|
"definitions": {
|
|
@@ -540,11 +540,30 @@ const decideMediaSchema = z
|
|
|
540
540
|
.strictObject({
|
|
541
541
|
imagesKey: decideMediaKeySchema,
|
|
542
542
|
videoKey: decideMediaKeySchema.optional(),
|
|
543
|
+
audioKey: decideMediaKeySchema.optional(),
|
|
543
544
|
})
|
|
544
545
|
.refine((media) => media.imagesKey !== media.videoKey, {
|
|
545
546
|
path: ["videoKey"],
|
|
546
547
|
message: "decide.media.videoKey must differ from imagesKey: one body key cannot carry both",
|
|
548
|
+
})
|
|
549
|
+
.refine((media) => media.audioKey === undefined ||
|
|
550
|
+
(media.audioKey !== media.imagesKey && media.audioKey !== media.videoKey), {
|
|
551
|
+
path: ["audioKey"],
|
|
552
|
+
message: "decide.media.audioKey must differ from imagesKey and videoKey: one body key cannot carry two kinds of media",
|
|
547
553
|
});
|
|
554
|
+
const DECIDE_QUESTION_TYPES = [
|
|
555
|
+
"boolean",
|
|
556
|
+
"choice",
|
|
557
|
+
"score",
|
|
558
|
+
"value",
|
|
559
|
+
"choice-unknown",
|
|
560
|
+
];
|
|
561
|
+
// The openai-decisions dialect writes predicates, choices and levels only.
|
|
562
|
+
const OPENAI_DECISIONS_QUESTION_TYPES = new Set([
|
|
563
|
+
"boolean",
|
|
564
|
+
"choice",
|
|
565
|
+
"score",
|
|
566
|
+
]);
|
|
548
567
|
const GUARD_LIST_MAX = 8;
|
|
549
568
|
const ambientGuardEnvNameSchema = z
|
|
550
569
|
.string()
|
|
@@ -603,6 +622,12 @@ const decideBlockSchema = z.strictObject({
|
|
|
603
622
|
authHeader: z.enum(["bearer", "x-api-key"]).optional(),
|
|
604
623
|
stateEncoding: z.enum(["text"]).optional(),
|
|
605
624
|
media: decideMediaSchema.optional(),
|
|
625
|
+
capableModel: z
|
|
626
|
+
.strictObject({
|
|
627
|
+
model: z.string().min(1),
|
|
628
|
+
questionTypes: z.array(z.enum(DECIDE_QUESTION_TYPES)),
|
|
629
|
+
})
|
|
630
|
+
.optional(),
|
|
606
631
|
ambientKeyGuard: ambientKeyGuardSchema.optional(),
|
|
607
632
|
requestIdHeaders: z.array(decideHeaderNameSchema).optional(),
|
|
608
633
|
response: z
|
|
@@ -653,6 +678,15 @@ const decideModelsSchema = z
|
|
|
653
678
|
const decideLimitsSchema = z.strictObject({
|
|
654
679
|
maxStateTokens: decidePositiveInt,
|
|
655
680
|
maxQuestions: decidePositiveInt.optional(),
|
|
681
|
+
questionTypes: z
|
|
682
|
+
.array(z.enum(DECIDE_QUESTION_TYPES))
|
|
683
|
+
.min(1)
|
|
684
|
+
.refine((types) => new Set(types).size === types.length, {
|
|
685
|
+
message: "decisionLimits.questionTypes lists the same type twice",
|
|
686
|
+
})
|
|
687
|
+
.optional(),
|
|
688
|
+
maxScoreLevels: decidePositiveInt.optional(),
|
|
689
|
+
maxChoiceOptions: decidePositiveInt.optional(),
|
|
656
690
|
nonAsciiTokensPerChar: decidePositiveNumber.optional(),
|
|
657
691
|
digitTokensPerChar: decidePositiveNumber.optional(),
|
|
658
692
|
symbolTokensPerChar: decidePositiveNumber.optional(),
|
|
@@ -668,10 +702,36 @@ const decideLimitsSchema = z.strictObject({
|
|
|
668
702
|
maxImages: decidePositiveInt,
|
|
669
703
|
video: z.boolean(),
|
|
670
704
|
maxRequestBytes: decidePositiveInt,
|
|
705
|
+
audio: z.strictObject({ maxSeconds: decidePositiveInt }).optional(),
|
|
671
706
|
})
|
|
672
707
|
.optional(),
|
|
673
708
|
advisory: z.boolean().optional(),
|
|
674
709
|
});
|
|
710
|
+
// decide.capableModel names another key of the catalog, never the default, and
|
|
711
|
+
// only question types the entry reads: anything else is refused before a model
|
|
712
|
+
// is chosen.
|
|
713
|
+
function checkCapableModel(entry, modelKeys, issue) {
|
|
714
|
+
const capable = entry.decide.capableModel;
|
|
715
|
+
if (capable === undefined) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (!modelKeys.has(capable.model)) {
|
|
719
|
+
issue(["decide", "capableModel", "model"], "decide.capableModel.model must be a key of models.catalog");
|
|
720
|
+
}
|
|
721
|
+
if (capable.model === entry.models.default) {
|
|
722
|
+
issue(["decide", "capableModel", "model"], "decide.capableModel.model must differ from models.default: a request is sent to it instead of the default");
|
|
723
|
+
}
|
|
724
|
+
const declared = entry.decisionLimits?.questionTypes ?? [
|
|
725
|
+
"boolean",
|
|
726
|
+
"choice",
|
|
727
|
+
"score",
|
|
728
|
+
];
|
|
729
|
+
capable.questionTypes.forEach((type, index) => {
|
|
730
|
+
if (!declared.includes(type)) {
|
|
731
|
+
issue(["decide", "capableModel", "questionTypes", index], `decide.capableModel.questionTypes lists ${type}, which decisionLimits.questionTypes does not: such a question is refused before any model is chosen`);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
}
|
|
675
735
|
const decideCatalogJsonSchema = z
|
|
676
736
|
.strictObject({
|
|
677
737
|
...catalogIdentityShape,
|
|
@@ -680,7 +740,10 @@ const decideCatalogJsonSchema = z
|
|
|
680
740
|
wire: decideWireSchema,
|
|
681
741
|
models: decideModelsSchema,
|
|
682
742
|
timeouts: z
|
|
683
|
-
.strictObject({
|
|
743
|
+
.strictObject({
|
|
744
|
+
decideMs: decidePositiveInt.optional(),
|
|
745
|
+
decideMsPerKStateTokens: decidePositiveInt.optional(),
|
|
746
|
+
})
|
|
684
747
|
.optional(),
|
|
685
748
|
decisionLimits: decideLimitsSchema.optional(),
|
|
686
749
|
decide: decideBlockSchema,
|
|
@@ -743,6 +806,7 @@ const decideCatalogJsonSchema = z
|
|
|
743
806
|
if (alias !== undefined && !modelKeys.has(alias)) {
|
|
744
807
|
issue(["decide", "model", "alias"], "decide.model.alias must be a key of models.catalog");
|
|
745
808
|
}
|
|
809
|
+
checkCapableModel(entry, modelKeys, issue);
|
|
746
810
|
for (const id of Object.keys(entry.decisionLimits?.models ?? {})) {
|
|
747
811
|
if (!modelKeys.has(id)) {
|
|
748
812
|
issue(["decisionLimits", "models", id], "decisionLimits.models key must be a key of models.catalog");
|
|
@@ -763,6 +827,14 @@ const decideCatalogJsonSchema = z
|
|
|
763
827
|
if (placement !== "body") {
|
|
764
828
|
issue(["decide", "model", "placement"], "decide.model.placement must be body with dialect openai-decisions: the model is always a field of the request");
|
|
765
829
|
}
|
|
830
|
+
if (mediaAllowance?.audio !== undefined) {
|
|
831
|
+
issue(["decisionLimits", "media", "audio"], "decisionLimits.media.audio is not valid with dialect openai-decisions: its request input has no audio part");
|
|
832
|
+
}
|
|
833
|
+
(entry.decisionLimits?.questionTypes ?? []).forEach((type, index) => {
|
|
834
|
+
if (!OPENAI_DECISIONS_QUESTION_TYPES.has(type)) {
|
|
835
|
+
issue(["decisionLimits", "questionTypes", index], `decisionLimits.questionTypes cannot list ${type} with dialect openai-decisions: that dialect writes boolean, choice and score questions only`);
|
|
836
|
+
}
|
|
837
|
+
});
|
|
766
838
|
}
|
|
767
839
|
else {
|
|
768
840
|
if (mediaKeys !== undefined && mediaAllowance === undefined) {
|
|
@@ -778,6 +850,14 @@ const decideCatalogJsonSchema = z
|
|
|
778
850
|
if (!mediaAllowance.video && mediaKeys.videoKey !== undefined) {
|
|
779
851
|
issue(["decide", "media", "videoKey"], "decide.media.videoKey must be omitted when decisionLimits.media.video is false");
|
|
780
852
|
}
|
|
853
|
+
if (mediaAllowance.audio !== undefined &&
|
|
854
|
+
mediaKeys.audioKey === undefined) {
|
|
855
|
+
issue(["decide", "media", "audioKey"], "decide.media.audioKey is required when decisionLimits.media.audio is set");
|
|
856
|
+
}
|
|
857
|
+
if (mediaAllowance.audio === undefined &&
|
|
858
|
+
mediaKeys.audioKey !== undefined) {
|
|
859
|
+
issue(["decide", "media", "audioKey"], "decide.media.audioKey must be omitted when decisionLimits.media.audio is not set");
|
|
860
|
+
}
|
|
781
861
|
}
|
|
782
862
|
}
|
|
783
863
|
if (decide.baseURLRequired === true &&
|
|
@@ -10,18 +10,18 @@
|
|
|
10
10
|
"topModels": ["xor-latest", "xor-omni"],
|
|
11
11
|
"catalog": {
|
|
12
12
|
"xor-latest": {
|
|
13
|
-
"contextWindow":
|
|
13
|
+
"contextWindow": 131072,
|
|
14
14
|
"maxOutputTokens": 256,
|
|
15
15
|
"vision": false,
|
|
16
16
|
"status": "production",
|
|
17
|
-
"description": "Recommended - XOR text alias; boolean, choice and score questions,
|
|
17
|
+
"description": "Recommended - XOR text alias; boolean, choice and score questions over a 131,072-token window. A request with a value question, an image or audio goes to xor-omni when no model was chosen (decide.capableModel)"
|
|
18
18
|
},
|
|
19
19
|
"xor-omni": {
|
|
20
|
-
"contextWindow":
|
|
20
|
+
"contextWindow": 32768,
|
|
21
21
|
"maxOutputTokens": 256,
|
|
22
22
|
"vision": true,
|
|
23
23
|
"status": "production",
|
|
24
|
-
"description": "XOR multimodal alias;
|
|
24
|
+
"description": "XOR multimodal alias; boolean, choice, score and value questions over a 32,768-token window, plus one image and up to 30 seconds of audio"
|
|
25
25
|
},
|
|
26
26
|
"xor-1.1": {
|
|
27
27
|
"contextWindow": 250000,
|
|
@@ -32,11 +32,24 @@
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
|
-
"timeouts": { "decideMs": 5000 },
|
|
35
|
+
"timeouts": { "decideMs": 5000, "decideMsPerKStateTokens": 150 },
|
|
36
36
|
"decisionLimits": {
|
|
37
|
-
"maxStateTokens":
|
|
37
|
+
"maxStateTokens": 28000,
|
|
38
|
+
"maxQuestions": 128,
|
|
39
|
+
"questionTypes": ["boolean", "choice", "score", "value", "choice-unknown"],
|
|
40
|
+
"maxScoreLevels": 10,
|
|
41
|
+
"maxChoiceOptions": 64,
|
|
38
42
|
"nonAsciiTokensPerChar": 1,
|
|
39
|
-
"
|
|
43
|
+
"models": {
|
|
44
|
+
"xor-latest": { "maxStateTokens": 120000 },
|
|
45
|
+
"xor-omni": { "maxStateTokens": 28000 }
|
|
46
|
+
},
|
|
47
|
+
"media": {
|
|
48
|
+
"maxImages": 1,
|
|
49
|
+
"video": false,
|
|
50
|
+
"maxRequestBytes": 25000000,
|
|
51
|
+
"audio": { "maxSeconds": 30 }
|
|
52
|
+
}
|
|
40
53
|
},
|
|
41
54
|
"decide": {
|
|
42
55
|
"dialect": "system-one",
|
|
@@ -46,7 +59,8 @@
|
|
|
46
59
|
"baseURLShape": "origin",
|
|
47
60
|
"endpoint": "{baseURL}/v1/decisions",
|
|
48
61
|
"model": { "placement": "body" },
|
|
49
|
-
"media": { "imagesKey": "images", "
|
|
62
|
+
"media": { "imagesKey": "images", "audioKey": "audio" },
|
|
63
|
+
"capableModel": { "model": "xor-omni", "questionTypes": ["value"] },
|
|
50
64
|
"requestIdHeaders": [
|
|
51
65
|
"x-request-id",
|
|
52
66
|
"x-typesafe-request-id",
|
|
@@ -63,6 +77,12 @@
|
|
|
63
77
|
"messagePattern": "(context|prefill|prompt).{0,40}(length|limit|too long|exceed)|(too long|exceeds?).{0,40}(context|tokens)",
|
|
64
78
|
"kind": "max_tokens_exceeded"
|
|
65
79
|
},
|
|
80
|
+
{
|
|
81
|
+
"statusMin": 500,
|
|
82
|
+
"messagePattern": "backend 4[0-9]{2}",
|
|
83
|
+
"kind": "invalid_request",
|
|
84
|
+
"retryable": false
|
|
85
|
+
},
|
|
66
86
|
{ "statusMin": 500, "kind": "server" }
|
|
67
87
|
],
|
|
68
88
|
"default": "invalid_request",
|
|
@@ -91,8 +111,8 @@
|
|
|
91
111
|
},
|
|
92
112
|
"evidence": {
|
|
93
113
|
"rosterVerified": {
|
|
94
|
-
"date": "2026-10-
|
|
95
|
-
"method": "POST <origin>/v1/decisions with
|
|
114
|
+
"date": "2026-10-19",
|
|
115
|
+
"method": "Probed POST <origin>/v1/decisions through a LiteLLM gateway with both aliases. xor-latest read a needle at 120,076 input tokens and refused 200,076 as longer than its 131,072-token context; xor-omni refused past 32,768. 128 questions were answered and 256 refused; a choice took 64 options (63 plus unknown; 64 plus unknown crashed with a 500); a score took 10 levels and 11 were refused by the gateway. value answered int, float and pattern-bound strings on xor-omni, and on xor-latest through the gateway's fallback to xor-omni. One image was read (a second was refused with 422) at a fixed ~600 tokens whatever its resolution. Audio as {data, format} was heard in WAV, FLAC, Ogg, Opus and MP3 (an English speech clip identified at 0.9999 in each) at ~25 tokens a second and cut at 30 seconds, keeping the start; M4A/AAC decoded to nothing. A video, sent as a data URL or as {data, format}, returned the same answer as garbage bytes, so the gateway drops it. The body cap was 24 MiB. A malformed image or audio clip and an over-long state returned 502 wrapping the backend's 400"
|
|
96
116
|
},
|
|
97
117
|
"liveMatrix": null,
|
|
98
118
|
"addedInPR": "https://github.com/juspay/neurolink/pull/1984"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DecideCatalogJson, DecisionDialect, DecisionError, DecisionPreparedMedia, DecisionState, UnknownRecord } from "../types/index.js";
|
|
1
|
+
import type { DecideCatalogJson, DecisionDialect, DecisionError, DecisionPreparedMedia, DecisionRequest, DecisionState, UnknownRecord } from "../types/index.js";
|
|
2
2
|
import { SystemOneDecisionProvider } from "./systemOneDecision.js";
|
|
3
3
|
/** A `decide` provider described entirely by its catalog entry. */
|
|
4
4
|
export declare class ConfiguredDecisionProvider extends SystemOneDecisionProvider {
|
|
@@ -14,6 +14,13 @@ export declare class ConfiguredDecisionProvider extends SystemOneDecisionProvide
|
|
|
14
14
|
protected missingKeyMessage(): string;
|
|
15
15
|
protected missingConfigMessage(): string | undefined;
|
|
16
16
|
protected decisionEndpoint(): string;
|
|
17
|
+
/**
|
|
18
|
+
* `decide.capableModel` in place of the default, for a request that carries
|
|
19
|
+
* media or one of its question types, when nobody chose a model: the call
|
|
20
|
+
* names none, the model variable is unset, and the instance is on the
|
|
21
|
+
* default. A model somebody chose is always the one sent.
|
|
22
|
+
*/
|
|
23
|
+
protected resolveRequestModel(request: DecisionRequest): string;
|
|
17
24
|
protected decisionHeaders(): Record<string, string>;
|
|
18
25
|
/**
|
|
19
26
|
* The state the dialect is given. Only an entry that sets `stateEncoding`
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decisionQuestionTypeOf } from "../utils/decisionLimits.js";
|
|
1
2
|
import { logger } from "../utils/logger.js";
|
|
2
3
|
import { redactUrlForError } from "../utils/logSanitize.js";
|
|
3
4
|
import { getProviderModel } from "../utils/providerConfig.js";
|
|
@@ -70,6 +71,27 @@ export class ConfiguredDecisionProvider extends SystemOneDecisionProvider {
|
|
|
70
71
|
decisionEndpoint() {
|
|
71
72
|
return this.endpoint;
|
|
72
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* `decide.capableModel` in place of the default, for a request that carries
|
|
76
|
+
* media or one of its question types, when nobody chose a model: the call
|
|
77
|
+
* names none, the model variable is unset, and the instance is on the
|
|
78
|
+
* default. A model somebody chose is always the one sent.
|
|
79
|
+
*/
|
|
80
|
+
resolveRequestModel(request) {
|
|
81
|
+
const chosen = super.resolveRequestModel(request);
|
|
82
|
+
const capable = this.entry.decide.capableModel;
|
|
83
|
+
if (capable === undefined ||
|
|
84
|
+
request.model !== undefined ||
|
|
85
|
+
chosen !== this.entry.models.default ||
|
|
86
|
+
readText(process.env[catalogEnvVar(this.entry, "model")]) !== "") {
|
|
87
|
+
return chosen;
|
|
88
|
+
}
|
|
89
|
+
const carriesMedia = (request.images?.length ?? 0) > 0 ||
|
|
90
|
+
request.video !== undefined ||
|
|
91
|
+
request.audio !== undefined;
|
|
92
|
+
const asksFor = Object.values(request.questions).some((question) => capable.questionTypes.includes(decisionQuestionTypeOf(question)));
|
|
93
|
+
return carriesMedia || asksFor ? capable.model : chosen;
|
|
94
|
+
}
|
|
73
95
|
decisionHeaders() {
|
|
74
96
|
const credential = this.entry.decide.authHeader === "x-api-key"
|
|
75
97
|
? { "x-api-key": this.apiKey }
|