@juspay/neurolink 13.4.10 → 13.5.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.
- package/CHANGELOG.md +2 -3
- package/README.md +3 -2
- package/dist/adapters/video/videoAnalyzer.d.ts +1 -0
- package/dist/adapters/video/videoAnalyzer.js +4 -0
- package/dist/autoresearch/index.d.ts +1 -0
- package/dist/autoresearch/index.js +4 -0
- package/dist/autoresearch/worker.js +4 -0
- package/dist/browser/neurolink.min.js +466 -466
- package/dist/cli/commands/decide.js +25 -5
- package/dist/cli/commands/observability.js +11 -4
- package/dist/cli/commands/telemetry.js +36 -6
- package/dist/cli/utils/formatters.d.ts +17 -0
- package/dist/cli/utils/formatters.js +39 -0
- package/dist/constants/langfuse.d.ts +17 -1
- package/dist/constants/langfuse.js +23 -1
- package/dist/constants/telemetry.d.ts +6 -0
- package/dist/constants/telemetry.js +6 -0
- package/dist/core/baseProvider.js +2 -0
- package/dist/core/geminiLoopAdapter.js +19 -0
- package/dist/core/loopEngine.js +10 -7
- package/dist/core/modules/StreamHandler.js +5 -4
- package/dist/core/modules/TelemetryHandler.d.ts +5 -1
- package/dist/core/modules/TelemetryHandler.js +16 -0
- package/dist/core/modules/ToolsManager.js +3 -0
- package/dist/core/nativeGenerateLoop.js +1 -1
- package/dist/evaluation/index.d.ts +1 -0
- package/dist/evaluation/index.js +4 -0
- package/dist/evaluation/ragasEvaluator.js +1 -0
- 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/mcp/externalServerManager.js +6 -2
- package/dist/mcp/toolRegistry.js +3 -0
- package/dist/neurolink.d.ts +20 -10
- package/dist/neurolink.js +134 -65
- package/dist/observability/FEATURE-STATUS.md +0 -4
- package/dist/observability/exporterRegistry.d.ts +6 -1
- package/dist/observability/exporterRegistry.js +18 -0
- package/dist/observability/exporterSpanProcessor.d.ts +20 -6
- package/dist/observability/exporterSpanProcessor.js +166 -34
- package/dist/observability/exporters/baseExporter.d.ts +22 -3
- package/dist/observability/exporters/baseExporter.js +87 -5
- package/dist/observability/exporters/langfuseExporter.d.ts +29 -3
- package/dist/observability/exporters/langfuseExporter.js +251 -41
- package/dist/observability/langfuseMasking.d.ts +1 -1
- package/dist/observability/langfuseMasking.js +1 -1
- package/dist/observability/otlpEncoding.d.ts +18 -5
- package/dist/observability/otlpEncoding.js +69 -12
- package/dist/observability/utils/spanSerializer.d.ts +3 -2
- package/dist/observability/utils/spanSerializer.js +5 -3
- package/dist/processors/base/BaseFileProcessor.d.ts +1 -0
- package/dist/processors/base/BaseFileProcessor.js +4 -0
- package/dist/processors/index.d.ts +1 -0
- package/dist/processors/index.js +4 -0
- package/dist/processors/registry/ProcessorRegistry.d.ts +1 -0
- package/dist/processors/registry/ProcessorRegistry.js +5 -0
- package/dist/providers/amazonBedrock/client.js +7 -5
- package/dist/providers/amazonSagemaker.js +32 -12
- package/dist/providers/anthropic/client.js +1 -0
- package/dist/providers/anthropic/loopAdapter.js +7 -3
- 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/googleAiStudio/client.js +2 -0
- package/dist/providers/googleVertex/client.d.ts +6 -5
- package/dist/providers/googleVertex/client.js +46 -19
- package/dist/providers/openaiChatCompletionsBase.js +4 -4
- package/dist/providers/systemOneDecision.d.ts +30 -7
- package/dist/providers/systemOneDecision.js +71 -14
- package/dist/proxy/codexUsage.js +59 -2
- package/dist/proxy/proxyTracer.d.ts +25 -1
- package/dist/proxy/proxyTracer.js +146 -22
- package/dist/proxy/proxyTranslationEngine.js +4 -2
- package/dist/proxy/sseInterceptor.js +6 -4
- package/dist/rag/chunkers/BaseChunker.js +1 -0
- package/dist/rag/index.d.ts +1 -0
- package/dist/rag/index.js +4 -0
- package/dist/rag/ragIntegration.js +1 -0
- package/dist/rag/reranker/reranker.js +2 -0
- package/dist/rag/retrieval/vectorQueryTool.js +1 -0
- package/dist/server/routes/codexProxyRoutes.js +3 -1
- package/dist/server/utils/validation.d.ts +3 -3
- package/dist/services/server/ai/observability/instrumentation.d.ts +8 -7
- package/dist/services/server/ai/observability/instrumentation.js +217 -47
- package/dist/services/server/ai/observability/langfuseSetup.d.ts +19 -1
- package/dist/services/server/ai/observability/langfuseSetup.js +62 -4
- package/dist/services/server/ai/observability/otlpSetup.d.ts +12 -2
- package/dist/services/server/ai/observability/otlpSetup.js +55 -4
- package/dist/telemetry/aiMetrics.d.ts +56 -0
- package/dist/telemetry/aiMetrics.js +229 -0
- package/dist/telemetry/attributes.d.ts +24 -1
- package/dist/telemetry/attributes.js +94 -9
- package/dist/telemetry/chatMessages.d.ts +6 -0
- package/dist/telemetry/chatMessages.js +37 -0
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +1 -1
- package/dist/telemetry/observation.d.ts +5 -5
- package/dist/telemetry/observation.js +128 -39
- package/dist/telemetry/spanSanitizer.d.ts +2 -2
- package/dist/telemetry/spanSanitizer.js +76 -8
- package/dist/telemetry/telemetryStarter.d.ts +16 -0
- package/dist/telemetry/telemetryStarter.js +28 -0
- package/dist/telemetry/traceContext.d.ts +3 -3
- package/dist/telemetry/traceContext.js +3 -3
- package/dist/telemetry/tracers.d.ts +13 -1
- package/dist/telemetry/tracers.js +49 -10
- package/dist/telemetry/withSpan.d.ts +19 -3
- package/dist/telemetry/withSpan.js +149 -20
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/decision.d.ts +117 -10
- package/dist/types/exporter.d.ts +98 -3
- package/dist/types/livekit.d.ts +16 -1
- package/dist/types/loopEngine.d.ts +16 -1
- package/dist/types/observability.d.ts +38 -5
- package/dist/types/providerCatalog.d.ts +12 -1
- package/dist/types/providers.d.ts +2 -0
- package/dist/types/proxy.d.ts +2 -0
- package/dist/types/span.d.ts +26 -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/dist/utils/timeout.js +4 -2
- package/dist/voice/livekit/realtimeMcpTools.js +11 -2
- package/dist/voice/livekit/realtimeMemory.d.ts +20 -2
- package/dist/voice/livekit/realtimeMemory.js +139 -46
- package/dist/voice/livekit/realtimeVoiceAgent.js +20 -8
- package/dist/voice/livekit/turnRecorder.js +22 -9
- package/dist/workflow/core/ensembleExecutor.js +2 -0
- package/dist/workflow/core/judgeScorer.js +1 -0
- package/dist/workflow/core/responseConditioner.js +1 -0
- package/dist/workflow/core/workflowRunner.js +6 -1
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.js +4 -0
- package/docs-site/static/search-index.json +56 -48
- package/package.json +21 -2
package/dist/processors/index.js
CHANGED
|
@@ -64,6 +64,10 @@
|
|
|
64
64
|
* });
|
|
65
65
|
* ```
|
|
66
66
|
*/
|
|
67
|
+
// Imported on its own, this entry starts tracing at its first span, as the
|
|
68
|
+
// main entry does. The import sits here rather than in the shared tracers so
|
|
69
|
+
// that the client entry stays free of the OpenTelemetry Node SDK.
|
|
70
|
+
import "../services/server/ai/observability/instrumentation.js";
|
|
67
71
|
// =============================================================================
|
|
68
72
|
// BASE PROCESSOR INFRASTRUCTURE (single source of truth for ALL types)
|
|
69
73
|
// =============================================================================
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
import type { FileInfo, ProcessorFileProcessingResult, ProcessedFileBase, ProcessOptions, ProcessorMatch, RegistryOptions, RegistryProcessResult, ProcessorRegistration } from "../../types/index.js";
|
|
47
|
+
import "../../services/server/ai/observability/instrumentation.js";
|
|
47
48
|
/**
|
|
48
49
|
* Central registry for file processors.
|
|
49
50
|
* Uses singleton pattern and priority-based selection.
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
*/
|
|
46
46
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
47
47
|
import { tracers } from "../../telemetry/tracers.js";
|
|
48
|
+
// Starts tracing at the first span, as this module always did: the shared
|
|
49
|
+
// tracers only start what a loaded entry registered, and this path can be
|
|
50
|
+
// imported without the package entry. The client entry stays free of it.
|
|
51
|
+
import "../../services/server/ai/observability/instrumentation.js";
|
|
48
52
|
// =============================================================================
|
|
49
53
|
// UTILITY FUNCTIONS
|
|
50
54
|
// =============================================================================
|
|
@@ -404,6 +408,7 @@ export class ProcessorRegistry {
|
|
|
404
408
|
return withSpan({
|
|
405
409
|
name: "neurolink.processor.processFile",
|
|
406
410
|
tracer: tracers.processor,
|
|
411
|
+
observationType: "span",
|
|
407
412
|
attributes: {
|
|
408
413
|
"processor.filename": fileInfo.name ?? "unknown",
|
|
409
414
|
"processor.mimetype": fileInfo.mimetype ?? "unknown",
|
|
@@ -279,9 +279,9 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
279
279
|
}
|
|
280
280
|
throw error;
|
|
281
281
|
}
|
|
282
|
-
// Emit generation:end
|
|
283
|
-
// Bedrock bypasses the Vercel AI SDK so
|
|
284
|
-
//
|
|
282
|
+
// Emit generation:end for the event listeners and the in-process metrics
|
|
283
|
+
// aggregator. Bedrock bypasses the Vercel AI SDK so nothing else emits it;
|
|
284
|
+
// its Langfuse GENERATION observations are the loop's own per-step spans.
|
|
285
285
|
const generateEmitter = this.neurolink?.getEventEmitter();
|
|
286
286
|
if (generateEmitter) {
|
|
287
287
|
generateEmitter.emit("generation:end", {
|
|
@@ -383,6 +383,7 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
383
383
|
}),
|
|
384
384
|
});
|
|
385
385
|
const { resultPromise } = runAgenticLoop(adapter, conversation, {
|
|
386
|
+
operation: "generate",
|
|
386
387
|
tools: this.toEngineTools(tools),
|
|
387
388
|
abortSignal: signal,
|
|
388
389
|
// `toEngineTools` adds no deadline of its own, so this is the only
|
|
@@ -1261,6 +1262,7 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
1261
1262
|
},
|
|
1262
1263
|
};
|
|
1263
1264
|
return runAgenticLoop(adapter, conversation, {
|
|
1265
|
+
operation: "stream",
|
|
1264
1266
|
tools: this.toEngineTools(tools),
|
|
1265
1267
|
abortSignal: signal,
|
|
1266
1268
|
// `toEngineTools` adds no deadline of its own, so this is the only
|
|
@@ -1368,8 +1370,8 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
1368
1370
|
// Analytics is resolved off `settled` above, so it lands whether or
|
|
1369
1371
|
// not anyone drains the stream.
|
|
1370
1372
|
// Bedrock bypasses the Vercel AI SDK, so experimental_telemetry is
|
|
1371
|
-
// never injected and generation:end is emitted by hand for
|
|
1372
|
-
//
|
|
1373
|
+
// never injected and generation:end is emitted by hand for the
|
|
1374
|
+
// event listeners and the in-process metrics aggregator.
|
|
1373
1375
|
if (streamEmitter) {
|
|
1374
1376
|
// #1741: tell the orchestration's finally block this stream's
|
|
1375
1377
|
// `generation:end` has already been emitted here, exactly as the
|
|
@@ -13,6 +13,7 @@ import { hasNativeDoGenerate, runNativeGenerateLoop, } from "../core/nativeGener
|
|
|
13
13
|
import { generationName } from "../telemetry/observationNames.js";
|
|
14
14
|
import { observeGeneration } from "../telemetry/observation.js";
|
|
15
15
|
import { tracers } from "../telemetry/tracers.js";
|
|
16
|
+
import { markFailure } from "../telemetry/withSpan.js";
|
|
16
17
|
// SageMaker-specific imports
|
|
17
18
|
import { getDefaultSageMakerEndpoint, getSageMakerConfig, getSageMakerModel, getSageMakerModelConfig, } from "./sagemaker/config.js";
|
|
18
19
|
import { handleSageMakerError, SageMakerError } from "./sagemaker/errors.js";
|
|
@@ -375,10 +376,18 @@ export class AmazonSageMakerProvider extends BaseProvider {
|
|
|
375
376
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
376
377
|
}
|
|
377
378
|
catch (error) {
|
|
378
|
-
span
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
379
|
+
markFailure(span, error);
|
|
380
|
+
// markFailure leaves an abort at WARNING; the released pump ended
|
|
381
|
+
// every failure at ERROR, an abort included.
|
|
382
|
+
try {
|
|
383
|
+
span.setStatus({
|
|
384
|
+
code: SpanStatusCode.ERROR,
|
|
385
|
+
message: error instanceof Error ? error.message : String(error),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// An error whose text cannot be read is already recorded above.
|
|
390
|
+
}
|
|
382
391
|
channel.error(error);
|
|
383
392
|
}
|
|
384
393
|
finally {
|
|
@@ -390,14 +399,25 @@ export class AmazonSageMakerProvider extends BaseProvider {
|
|
|
390
399
|
settleFinishReason("unknown");
|
|
391
400
|
settleUsage({ inputTokens: 0, outputTokens: 0 });
|
|
392
401
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
402
|
+
// Recording is best effort and span.end() must follow it either
|
|
403
|
+
// way: this pump is detached, so nothing else would catch a throw.
|
|
404
|
+
try {
|
|
405
|
+
observeGeneration(span, {
|
|
406
|
+
...call,
|
|
407
|
+
output: text,
|
|
408
|
+
usage: tokens,
|
|
409
|
+
finishReason: finish,
|
|
410
|
+
completionStartTime: firstChunkAt,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
logger.debug("[SageMaker] Failed to record the stream generation", {
|
|
415
|
+
error: error instanceof Error ? error.message : String(error),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
span.end();
|
|
420
|
+
}
|
|
401
421
|
}
|
|
402
422
|
})();
|
|
403
423
|
return {
|
|
@@ -2208,6 +2208,7 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
2208
2208
|
}
|
|
2209
2209
|
: undefined;
|
|
2210
2210
|
const { stream, resultPromise } = runAgenticLoop(adapter, payload.messages.slice(), {
|
|
2211
|
+
operation: "stream",
|
|
2211
2212
|
tools: engineTools,
|
|
2212
2213
|
...(abortSignal ? { abortSignal } : {}),
|
|
2213
2214
|
...(activeSpan ? { span: activeSpan } : {}),
|
|
@@ -401,9 +401,13 @@ export function createAnthropicLoopAdapter(config) {
|
|
|
401
401
|
? params.thinking.budget_tokens
|
|
402
402
|
: undefined,
|
|
403
403
|
},
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
404
|
+
// Read only by a recording span, so a request nobody traces is not
|
|
405
|
+
// copied and sanitized on every step.
|
|
406
|
+
get input() {
|
|
407
|
+
return {
|
|
408
|
+
system: params.system,
|
|
409
|
+
messages: sanitizeAnthropicMessagesForTrace(params.messages),
|
|
410
|
+
};
|
|
407
411
|
},
|
|
408
412
|
},
|
|
409
413
|
};
|
|
@@ -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 }
|
|
@@ -30,6 +30,11 @@ function encodeQuestion(question) {
|
|
|
30
30
|
})),
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
if (question.type === "value") {
|
|
34
|
+
// Unreachable through decide(): an entry on this dialect cannot list
|
|
35
|
+
// `value` in its questionTypes, so the pre-flight check refuses it first.
|
|
36
|
+
throw new Error("The openai-decisions dialect has no value question; it writes boolean, choice and score questions only.");
|
|
37
|
+
}
|
|
33
38
|
// The index is the label so a returned `value` and `label` can never
|
|
34
39
|
// disagree about which level it is; the rubric text rides in `description`.
|
|
35
40
|
return {
|
|
@@ -140,6 +145,9 @@ function readAnswer(question, raw) {
|
|
|
140
145
|
if (question.type === "choice") {
|
|
141
146
|
return raw.type === "choice" ? readChoice(question, raw) : null;
|
|
142
147
|
}
|
|
148
|
+
if (question.type === "value") {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
143
151
|
return raw.type === "score" ? readScore(question, raw) : null;
|
|
144
152
|
}
|
|
145
153
|
/** Number of answers carrying each asked name, so a repeated one can be refused. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asNumber, asNumberMap,
|
|
1
|
+
import { asNumber, asNumberMap, asTextMap, isRecord } from "../guards.js";
|
|
2
2
|
/**
|
|
3
3
|
* Last-resort confidence, when no transport reported one.
|
|
4
4
|
*
|
|
@@ -43,11 +43,12 @@ function parseDecisionAnswer(raw, reportedConfidence) {
|
|
|
43
43
|
reportedConfidence ??
|
|
44
44
|
deriveConfidence(probabilities),
|
|
45
45
|
probabilities,
|
|
46
|
+
...(raw.unknown === true ? { unknown: true } : {}),
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
case "score": {
|
|
49
50
|
const score = asNumber(raw.score);
|
|
50
|
-
const legend =
|
|
51
|
+
const legend = asTextMap(raw.legend);
|
|
51
52
|
const probabilities = asNumberMap(raw.probabilities);
|
|
52
53
|
if (score === undefined || !legend || !probabilities) {
|
|
53
54
|
return null;
|
|
@@ -62,6 +63,21 @@ function parseDecisionAnswer(raw, reportedConfidence) {
|
|
|
62
63
|
probabilities,
|
|
63
64
|
};
|
|
64
65
|
}
|
|
66
|
+
case "value": {
|
|
67
|
+
const value = raw.value === null || typeof raw.value === "string"
|
|
68
|
+
? raw.value
|
|
69
|
+
: asNumber(raw.value);
|
|
70
|
+
const confidence = asNumber(raw.confidence) ?? reportedConfidence;
|
|
71
|
+
if (value === undefined || confidence === undefined) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
type: "value",
|
|
76
|
+
value,
|
|
77
|
+
confidence,
|
|
78
|
+
...(raw.unknown === true ? { unknown: true } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
65
81
|
default:
|
|
66
82
|
return null;
|
|
67
83
|
}
|
|
@@ -78,6 +94,13 @@ function encodeQuestion(question) {
|
|
|
78
94
|
...(question.criteria ? { criteria: question.criteria } : {}),
|
|
79
95
|
};
|
|
80
96
|
}
|
|
97
|
+
if (question.type === "choice" && question.unknown !== undefined) {
|
|
98
|
+
// `unknown` goes out only when it is on: a server that does not know the
|
|
99
|
+
// field may refuse it even as false. A choice without it is the caller's
|
|
100
|
+
// own object, as every other question is.
|
|
101
|
+
const { unknown, ...rest } = question;
|
|
102
|
+
return unknown ? { ...rest, unknown: true } : rest;
|
|
103
|
+
}
|
|
81
104
|
return question;
|
|
82
105
|
}
|
|
83
106
|
export const systemOneDialect = {
|
|
@@ -98,6 +121,9 @@ export const systemOneDialect = {
|
|
|
98
121
|
...(media.video && mediaKeys.videoKey
|
|
99
122
|
? { [mediaKeys.videoKey]: media.video }
|
|
100
123
|
: {}),
|
|
124
|
+
...(media.audio && mediaKeys.audioKey
|
|
125
|
+
? { [mediaKeys.audioKey]: media.audio }
|
|
126
|
+
: {}),
|
|
101
127
|
};
|
|
102
128
|
},
|
|
103
129
|
readAnswers: (decoded, reportedConfidence) => {
|