@juspay/neurolink 12.57.0 → 12.58.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 +3 -3
- package/README.md +75 -51
- package/dist/browser/neurolink.min.js +419 -416
- package/dist/cli/commands/decide.d.ts +4 -1
- package/dist/cli/commands/decide.js +6 -3
- package/dist/cli/commands/setup.d.ts +1 -1
- package/dist/cli/commands/setup.js +1 -1
- package/dist/cli/factories/commandFactory.js +1 -1
- package/dist/constants/enums.d.ts +4 -0
- package/dist/constants/enums.js +5 -0
- package/dist/core/factory.d.ts +7 -0
- package/dist/core/factory.js +20 -0
- package/dist/factories/providerDescriptors.js +96 -7
- package/dist/processors/media/VideoProcessor.d.ts +2 -2
- package/dist/providers/catalog/index.generated.d.ts +1 -1
- package/dist/providers/catalog/index.generated.js +8 -1
- package/dist/providers/catalog/openai-decisions.json +75 -0
- package/dist/providers/catalog/provider-catalog.schema.json +56 -3
- package/dist/providers/catalog/schema.js +92 -11
- package/dist/providers/configuredDecision.js +9 -14
- package/dist/providers/decide/dialects/index.js +2 -0
- package/dist/providers/decide/dialects/openaiDecisions.d.ts +2 -0
- package/dist/providers/decide/dialects/openaiDecisions.js +228 -0
- package/dist/providers/decide/dialects/systemOne.js +17 -0
- package/dist/providers/systemOneDecision.js +6 -1
- package/dist/telemetry/attributes.d.ts +10 -3
- package/dist/telemetry/attributes.js +142 -10
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +1 -1
- package/dist/types/classifierRouter.d.ts +21 -9
- package/dist/types/cli.d.ts +4 -2
- package/dist/types/decision.d.ts +67 -3
- package/dist/types/processor.d.ts +10 -0
- package/dist/types/providerCatalog.d.ts +7 -11
- package/dist/types/providerCatalog.generated.d.ts +2 -2
- package/dist/types/providers.d.ts +12 -1
- package/dist/utils/pricing.js +9 -0
- package/docs-site/static/search-index.json +84 -59
- package/package.json +6 -2
- package/dist/client/reactHooks.tsx +0 -1473
|
@@ -545,8 +545,50 @@ const decideMediaSchema = z
|
|
|
545
545
|
path: ["videoKey"],
|
|
546
546
|
message: "decide.media.videoKey must differ from imagesKey: one body key cannot carry both",
|
|
547
547
|
});
|
|
548
|
+
const GUARD_LIST_MAX = 8;
|
|
549
|
+
const ambientGuardEnvNameSchema = z
|
|
550
|
+
.string()
|
|
551
|
+
.max(64)
|
|
552
|
+
.regex(/^[A-Z][A-Z0-9_]*$/, "an environment variable name must be upper-case letters, digits and underscores, and start with a letter");
|
|
553
|
+
// Lower-case scheme, host labels and an optional port, nothing else: the
|
|
554
|
+
// origin is compared after the same parsing a base URL gets, so a spelling
|
|
555
|
+
// that URL would normalise (a trailing slash, a path, an upper-case host) is
|
|
556
|
+
// refused here instead of being quietly accepted.
|
|
557
|
+
const AMBIENT_GUARD_ORIGIN_PATTERN = /^https?:\/\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?::[0-9]{1,5})?$/;
|
|
558
|
+
function parsesAsUrl(value) {
|
|
559
|
+
try {
|
|
560
|
+
new URL(value);
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const ambientGuardOriginSchema = z
|
|
568
|
+
.string()
|
|
569
|
+
.max(100)
|
|
570
|
+
.regex(AMBIENT_GUARD_ORIGIN_PATTERN, "a trusted origin must be an http(s) origin: a lower-case scheme, host and optional port, with no path, query, fragment or userinfo")
|
|
571
|
+
.refine(parsesAsUrl, {
|
|
572
|
+
message: "a trusted origin must parse as a URL (a valid host, port 0 to 65535)",
|
|
573
|
+
});
|
|
574
|
+
const ambientGuardCredentialKeySchema = z
|
|
575
|
+
.string()
|
|
576
|
+
.max(64)
|
|
577
|
+
.regex(/^[a-zA-Z][a-zA-Z0-9]*$/, "a credentials key must be letters and digits, and start with a letter");
|
|
578
|
+
const ambientKeyGuardSchema = z.strictObject({
|
|
579
|
+
baseURLEnvVars: z.array(ambientGuardEnvNameSchema).min(1).max(GUARD_LIST_MAX),
|
|
580
|
+
trustedOrigins: z.array(ambientGuardOriginSchema).min(1).max(GUARD_LIST_MAX),
|
|
581
|
+
credentialBaseURLs: z
|
|
582
|
+
.array(z.strictObject({
|
|
583
|
+
key: ambientGuardCredentialKeySchema,
|
|
584
|
+
field: z.literal("baseURL"),
|
|
585
|
+
}))
|
|
586
|
+
.min(1)
|
|
587
|
+
.max(GUARD_LIST_MAX)
|
|
588
|
+
.optional(),
|
|
589
|
+
});
|
|
548
590
|
const decideBlockSchema = z.strictObject({
|
|
549
|
-
dialect: z.enum(["system-one"]),
|
|
591
|
+
dialect: z.enum(["system-one", "openai-decisions"]),
|
|
550
592
|
order: decidePositiveInt,
|
|
551
593
|
baseURLRequired: z.boolean().optional(),
|
|
552
594
|
baseURLNormalize: z
|
|
@@ -559,6 +601,7 @@ const decideBlockSchema = z.strictObject({
|
|
|
559
601
|
alias: z.string().min(1).optional(),
|
|
560
602
|
}),
|
|
561
603
|
media: decideMediaSchema.optional(),
|
|
604
|
+
ambientKeyGuard: ambientKeyGuardSchema.optional(),
|
|
562
605
|
requestIdHeaders: z.array(decideHeaderNameSchema).optional(),
|
|
563
606
|
response: z
|
|
564
607
|
.strictObject({ modelPaths: z.array(decidePathSchema).optional() })
|
|
@@ -705,18 +748,34 @@ const decideCatalogJsonSchema = z
|
|
|
705
748
|
}
|
|
706
749
|
const mediaKeys = decide.media;
|
|
707
750
|
const mediaAllowance = entry.decisionLimits?.media;
|
|
708
|
-
if (
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
751
|
+
if (decide.dialect === "openai-decisions") {
|
|
752
|
+
// This dialect writes images into the request input itself, so it has no
|
|
753
|
+
// body keys to name, and the input has no video part. The allowance
|
|
754
|
+
// alone is how many images a request may carry.
|
|
755
|
+
if (mediaKeys !== undefined) {
|
|
756
|
+
issue(["decide", "media"], "decide.media is not valid with dialect openai-decisions: that dialect places images in the request input itself and has no body keys for them");
|
|
757
|
+
}
|
|
758
|
+
if (mediaAllowance?.video === true) {
|
|
759
|
+
issue(["decisionLimits", "media", "video"], "decisionLimits.media.video must be false with dialect openai-decisions: its request input has no video part");
|
|
760
|
+
}
|
|
761
|
+
if (placement !== "body") {
|
|
762
|
+
issue(["decide", "model", "placement"], "decide.model.placement must be body with dialect openai-decisions: the model is always a field of the request");
|
|
763
|
+
}
|
|
713
764
|
}
|
|
714
|
-
|
|
715
|
-
if (
|
|
716
|
-
issue(["decide", "media"
|
|
765
|
+
else {
|
|
766
|
+
if (mediaKeys !== undefined && mediaAllowance === undefined) {
|
|
767
|
+
issue(["decide", "media"], "decide.media needs decisionLimits.media: without an allowance the base class refuses every image and video, so these keys would never be written");
|
|
717
768
|
}
|
|
718
|
-
if (
|
|
719
|
-
issue(["
|
|
769
|
+
if (mediaKeys === undefined && mediaAllowance !== undefined) {
|
|
770
|
+
issue(["decisionLimits", "media"], "decisionLimits.media needs decide.media: without body keys the engine would prepare a request's media and then leave it out of the body");
|
|
771
|
+
}
|
|
772
|
+
if (mediaKeys !== undefined && mediaAllowance !== undefined) {
|
|
773
|
+
if (mediaAllowance.video && mediaKeys.videoKey === undefined) {
|
|
774
|
+
issue(["decide", "media", "videoKey"], "decide.media.videoKey is required when decisionLimits.media.video is true");
|
|
775
|
+
}
|
|
776
|
+
if (!mediaAllowance.video && mediaKeys.videoKey !== undefined) {
|
|
777
|
+
issue(["decide", "media", "videoKey"], "decide.media.videoKey must be omitted when decisionLimits.media.video is false");
|
|
778
|
+
}
|
|
720
779
|
}
|
|
721
780
|
}
|
|
722
781
|
if (decide.baseURLRequired === true &&
|
|
@@ -738,6 +797,28 @@ const decideCatalogJsonSchema = z
|
|
|
738
797
|
}
|
|
739
798
|
seenHeaders.add(header.toLowerCase());
|
|
740
799
|
});
|
|
800
|
+
const guard = decide.ambientKeyGuard;
|
|
801
|
+
if (guard !== undefined) {
|
|
802
|
+
const ownBaseURLVar = wire.envOverrides?.baseURL ??
|
|
803
|
+
`${entry.id.toUpperCase().replace(/-/g, "_")}_BASE_URL`;
|
|
804
|
+
const lists = [
|
|
805
|
+
["baseURLEnvVars", guard.baseURLEnvVars, "variable"],
|
|
806
|
+
["trustedOrigins", guard.trustedOrigins, "origin"],
|
|
807
|
+
];
|
|
808
|
+
for (const [field, values, noun] of lists) {
|
|
809
|
+
if (new Set(values).size !== values.length) {
|
|
810
|
+
issue(["decide", "ambientKeyGuard", field], `lists the same ${noun} twice`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const credentialFields = guard.credentialBaseURLs ?? [];
|
|
814
|
+
if (new Set(credentialFields.map((c) => `${c.key}.${c.field}`)).size !==
|
|
815
|
+
credentialFields.length) {
|
|
816
|
+
issue(["decide", "ambientKeyGuard", "credentialBaseURLs"], "lists the same credentials field twice");
|
|
817
|
+
}
|
|
818
|
+
if (guard.baseURLEnvVars.includes(ownBaseURLVar)) {
|
|
819
|
+
issue(["decide", "ambientKeyGuard", "baseURLEnvVars"], `must not list the provider's own base-URL variable ${ownBaseURLVar}: setting it already exempts the key`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
741
822
|
for (const key of UNSUPPORTED_DECIDE_WIRE_KEYS) {
|
|
742
823
|
if (wire[key] !== undefined) {
|
|
743
824
|
issue(["wire", key], `wire.${key} is not supported on decide entries yet; ConfiguredDecisionProvider does not read it`);
|
|
@@ -75,20 +75,15 @@ export class ConfiguredDecisionProvider extends SystemOneDecisionProvider {
|
|
|
75
75
|
}
|
|
76
76
|
buildDecisionBody(state, questions, model, media) {
|
|
77
77
|
const { placement, alias } = this.entry.decide.model;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
...body,
|
|
89
|
-
...(media.images.length > 0 ? { [keys.imagesKey]: media.images } : {}),
|
|
90
|
-
...(media.video && keys.videoKey ? { [keys.videoKey]: media.video } : {}),
|
|
91
|
-
};
|
|
78
|
+
return this.dialect().buildBody({
|
|
79
|
+
state,
|
|
80
|
+
questions,
|
|
81
|
+
model: placement === "body-unless-alias" && model === alias
|
|
82
|
+
? undefined
|
|
83
|
+
: model,
|
|
84
|
+
media,
|
|
85
|
+
mediaKeys: this.entry.decide.media,
|
|
86
|
+
});
|
|
92
87
|
}
|
|
93
88
|
parseDecisionError(status, payload, requestId) {
|
|
94
89
|
return classifyDecisionError(this.entry.decide.errors, {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { openaiDecisionsDialect } from "./openaiDecisions.js";
|
|
1
2
|
import { systemOneDialect } from "./systemOne.js";
|
|
2
3
|
const DIALECTS = {
|
|
3
4
|
"system-one": systemOneDialect,
|
|
5
|
+
"openai-decisions": openaiDecisionsDialect,
|
|
4
6
|
};
|
|
5
7
|
export const getDecisionDialect = (name) => DIALECTS[name];
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { asNumber, isRecord } from "../guards.js";
|
|
2
|
+
const PROBABILITY_SUM_TOLERANCE = 0.01;
|
|
3
|
+
// A sum one tolerance away (0.99, 1.01) lands a rounding error past the
|
|
4
|
+
// tolerance in floating point; it is within it.
|
|
5
|
+
const SUM_ROUNDING_SLACK = 1e-9;
|
|
6
|
+
const text = (value) => typeof value === "string" ? value : JSON.stringify(value);
|
|
7
|
+
const described = (value) => value === null || value === undefined ? {} : { description: text(value) };
|
|
8
|
+
function encodeQuestion(question) {
|
|
9
|
+
if (question.type === "boolean") {
|
|
10
|
+
// The wire has no per-side text, so each side joins the instructions.
|
|
11
|
+
const { true: whenTrue, false: whenFalse } = question.criteria ?? {};
|
|
12
|
+
return {
|
|
13
|
+
type: "predicate",
|
|
14
|
+
instructions: text(question.instructions) +
|
|
15
|
+
(whenTrue === null || whenTrue === undefined
|
|
16
|
+
? ""
|
|
17
|
+
: `\n\nAnswer true when: ${text(whenTrue)}`) +
|
|
18
|
+
(whenFalse === null || whenFalse === undefined
|
|
19
|
+
? ""
|
|
20
|
+
: `\nAnswer false when: ${text(whenFalse)}`),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (question.type === "choice") {
|
|
24
|
+
return {
|
|
25
|
+
type: "choice",
|
|
26
|
+
instructions: text(question.instructions),
|
|
27
|
+
choices: Object.entries(question.criteria).map(([value, rubric]) => ({
|
|
28
|
+
value,
|
|
29
|
+
...described(rubric),
|
|
30
|
+
})),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// The index is the label so a returned `value` and `label` can never
|
|
34
|
+
// disagree about which level it is; the rubric text rides in `description`.
|
|
35
|
+
return {
|
|
36
|
+
type: "score",
|
|
37
|
+
instructions: text(question.instructions),
|
|
38
|
+
levels: question.criteria.map((rubric, index) => ({
|
|
39
|
+
label: String(index),
|
|
40
|
+
...described(rubric),
|
|
41
|
+
})),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const inUnitRange = (value) => value >= 0 && value <= 1;
|
|
45
|
+
/**
|
|
46
|
+
* The answer's distribution keyed by the keys that were asked, or null unless
|
|
47
|
+
* it names each asked key exactly once, with probabilities that sum to one.
|
|
48
|
+
* The result is in asked order, not the server's.
|
|
49
|
+
*/
|
|
50
|
+
function readDistribution(raw, asked, keyOf) {
|
|
51
|
+
if (!Array.isArray(raw) || raw.length !== asked.length) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const expected = new Set(asked);
|
|
55
|
+
const seen = new Map();
|
|
56
|
+
for (const entry of raw) {
|
|
57
|
+
if (!isRecord(entry)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const key = keyOf(entry.value);
|
|
61
|
+
const probability = asNumber(entry.probability);
|
|
62
|
+
if (key === undefined ||
|
|
63
|
+
probability === undefined ||
|
|
64
|
+
!inUnitRange(probability) ||
|
|
65
|
+
!expected.has(key) ||
|
|
66
|
+
seen.has(key)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
seen.set(key, probability);
|
|
70
|
+
}
|
|
71
|
+
let sum = 0;
|
|
72
|
+
for (const probability of seen.values()) {
|
|
73
|
+
sum += probability;
|
|
74
|
+
}
|
|
75
|
+
if (Math.abs(sum - 1) > PROBABILITY_SUM_TOLERANCE + SUM_ROUNDING_SLACK) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return Object.fromEntries(asked.map((key) => [key, seen.get(key) ?? 0]));
|
|
79
|
+
}
|
|
80
|
+
const peak = (probabilities) => Math.max(...Object.values(probabilities));
|
|
81
|
+
/** The answer's own confidence, else the peak; null when its own is outside 0 to 1. */
|
|
82
|
+
function readConfidence(raw, probabilities) {
|
|
83
|
+
const own = asNumber(raw);
|
|
84
|
+
if (own === undefined) {
|
|
85
|
+
return peak(probabilities);
|
|
86
|
+
}
|
|
87
|
+
return inUnitRange(own) ? own : null;
|
|
88
|
+
}
|
|
89
|
+
function readChoice(question, raw) {
|
|
90
|
+
const options = Object.keys(question.criteria);
|
|
91
|
+
if (typeof raw.choice !== "string" || !options.includes(raw.choice)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const probabilities = readDistribution(raw.probabilities, options, (value) => typeof value === "string" ? value : undefined);
|
|
95
|
+
if (!probabilities) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const confidence = readConfidence(raw.confidence, probabilities);
|
|
99
|
+
return confidence === null
|
|
100
|
+
? null
|
|
101
|
+
: { type: "choice", choice: raw.choice, confidence, probabilities };
|
|
102
|
+
}
|
|
103
|
+
function readScore(question, raw) {
|
|
104
|
+
const score = asNumber(raw.score);
|
|
105
|
+
const levels = question.criteria.map((_rubric, index) => String(index));
|
|
106
|
+
const probabilities = readDistribution(raw.probabilities, levels, (value) => typeof value === "number" && Number.isInteger(value)
|
|
107
|
+
? String(value)
|
|
108
|
+
: undefined);
|
|
109
|
+
// The probability-weighted mean of the level indices cannot leave 0 to n-1.
|
|
110
|
+
if (score === undefined ||
|
|
111
|
+
score < 0 ||
|
|
112
|
+
score > levels.length - 1 ||
|
|
113
|
+
!probabilities) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const confidence = readConfidence(raw.confidence, probabilities);
|
|
117
|
+
if (confidence === null) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
type: "score",
|
|
122
|
+
score,
|
|
123
|
+
confidence,
|
|
124
|
+
legend: Object.fromEntries(question.criteria.map((rubric, index) => [
|
|
125
|
+
String(index),
|
|
126
|
+
rubric === null || rubric === undefined ? "" : text(rubric),
|
|
127
|
+
])),
|
|
128
|
+
probabilities,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function readAnswer(question, raw) {
|
|
132
|
+
if (question.type === "boolean") {
|
|
133
|
+
const probability = asNumber(raw.probability);
|
|
134
|
+
return raw.type === "predicate" &&
|
|
135
|
+
probability !== undefined &&
|
|
136
|
+
inUnitRange(probability)
|
|
137
|
+
? { type: "boolean", probability }
|
|
138
|
+
: null;
|
|
139
|
+
}
|
|
140
|
+
if (question.type === "choice") {
|
|
141
|
+
return raw.type === "choice" ? readChoice(question, raw) : null;
|
|
142
|
+
}
|
|
143
|
+
return raw.type === "score" ? readScore(question, raw) : null;
|
|
144
|
+
}
|
|
145
|
+
/** Number of answers carrying each asked name, so a repeated one can be refused. */
|
|
146
|
+
function countAskedNames(raw, asked) {
|
|
147
|
+
const counts = new Map();
|
|
148
|
+
for (const item of raw) {
|
|
149
|
+
const name = isRecord(item) ? item.name : undefined;
|
|
150
|
+
if (typeof name === "string" && Object.hasOwn(asked, name)) {
|
|
151
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return counts;
|
|
155
|
+
}
|
|
156
|
+
export const openaiDecisionsDialect = {
|
|
157
|
+
name: "openai-decisions",
|
|
158
|
+
encodeQuestion,
|
|
159
|
+
buildBody: ({ state, questions, model, media }) => {
|
|
160
|
+
const images = media?.images ?? [];
|
|
161
|
+
const stateText = text(state);
|
|
162
|
+
return {
|
|
163
|
+
...(model === undefined ? {} : { model }),
|
|
164
|
+
input: images.length === 0
|
|
165
|
+
? stateText
|
|
166
|
+
: [
|
|
167
|
+
{
|
|
168
|
+
role: "user",
|
|
169
|
+
content: [
|
|
170
|
+
...(stateText === ""
|
|
171
|
+
? []
|
|
172
|
+
: [{ type: "input_text", text: stateText }]),
|
|
173
|
+
...images.map((url) => ({
|
|
174
|
+
type: "input_image",
|
|
175
|
+
image_url: url,
|
|
176
|
+
})),
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
questions: Object.entries(questions).map(([name, question]) => ({
|
|
181
|
+
...question,
|
|
182
|
+
name,
|
|
183
|
+
})),
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
// The wire echoes each question's `name`, and the server's answer is checked
|
|
187
|
+
// against the question asked: one that fails any check is dropped and named,
|
|
188
|
+
// never repaired, so the other answers in the batch survive.
|
|
189
|
+
readAnswers: (decoded, _reportedConfidence, asked) => {
|
|
190
|
+
const raw = decoded.answers;
|
|
191
|
+
if (!Array.isArray(raw)) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const counts = countAskedNames(raw, asked);
|
|
195
|
+
const answers = new Map();
|
|
196
|
+
const dropped = [];
|
|
197
|
+
const refused = [];
|
|
198
|
+
raw.forEach((item, index) => {
|
|
199
|
+
const record = isRecord(item) ? item : undefined;
|
|
200
|
+
const name = typeof record?.name === "string" ? record.name : undefined;
|
|
201
|
+
const question = name !== undefined && Object.hasOwn(asked, name)
|
|
202
|
+
? asked[name]
|
|
203
|
+
: undefined;
|
|
204
|
+
if (!record || name === undefined || !question) {
|
|
205
|
+
dropped.push(`#${index}`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if ((counts.get(name) ?? 0) > 1) {
|
|
209
|
+
if (!dropped.includes(name)) {
|
|
210
|
+
dropped.push(name);
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (record.type === "refusal") {
|
|
215
|
+
refused.push(name);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const answer = readAnswer(question, record);
|
|
219
|
+
if (answer) {
|
|
220
|
+
answers.set(name, answer);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
dropped.push(name);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return { answers: Object.fromEntries(answers), dropped, refused };
|
|
227
|
+
},
|
|
228
|
+
};
|
|
@@ -83,6 +83,23 @@ function encodeQuestion(question) {
|
|
|
83
83
|
export const systemOneDialect = {
|
|
84
84
|
name: "system-one",
|
|
85
85
|
encodeQuestion,
|
|
86
|
+
buildBody: ({ state, questions, model, media, mediaKeys }) => {
|
|
87
|
+
const body = model === undefined ? { state, questions } : { model, state, questions };
|
|
88
|
+
if (!mediaKeys || !media) {
|
|
89
|
+
return body;
|
|
90
|
+
}
|
|
91
|
+
// A key is written only for media the request carries: a server may refuse
|
|
92
|
+
// an empty `images`.
|
|
93
|
+
return {
|
|
94
|
+
...body,
|
|
95
|
+
...(media.images.length > 0
|
|
96
|
+
? { [mediaKeys.imagesKey]: media.images }
|
|
97
|
+
: {}),
|
|
98
|
+
...(media.video && mediaKeys.videoKey
|
|
99
|
+
? { [mediaKeys.videoKey]: media.video }
|
|
100
|
+
: {}),
|
|
101
|
+
};
|
|
102
|
+
},
|
|
86
103
|
readAnswers: (decoded, reportedConfidence) => {
|
|
87
104
|
const raw = decoded.answers;
|
|
88
105
|
if (!isRecord(raw)) {
|
|
@@ -297,7 +297,7 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
297
297
|
}
|
|
298
298
|
const decoded = this.readDecisionPayload(payload);
|
|
299
299
|
const reading = isRecord(decoded)
|
|
300
|
-
? this.dialect().readAnswers(decoded, this.reportedConfidence(decoded))
|
|
300
|
+
? this.dialect().readAnswers(decoded, this.reportedConfidence(decoded), request.questions)
|
|
301
301
|
: undefined;
|
|
302
302
|
if (!isRecord(decoded) || !reading) {
|
|
303
303
|
throw this.decisionError({
|
|
@@ -313,6 +313,11 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
313
313
|
requestId,
|
|
314
314
|
});
|
|
315
315
|
}
|
|
316
|
+
for (const id of reading.refused ?? []) {
|
|
317
|
+
logger.warn(`${label}: the model declined to answer "${id}"`, {
|
|
318
|
+
requestId,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
316
321
|
const answers = { ...reading.answers };
|
|
317
322
|
const usage = isRecord(decoded.usage) ? decoded.usage : {};
|
|
318
323
|
return {
|
|
@@ -130,6 +130,12 @@ export declare const LANGFUSE_ATTR: {
|
|
|
130
130
|
};
|
|
131
131
|
/** Default ceiling for serialized span attribute values. */
|
|
132
132
|
export declare const SPAN_ATTRIBUTE_MAX_CHARS = 40000;
|
|
133
|
+
/**
|
|
134
|
+
* The ceiling in force: `NEUROLINK_SPAN_ATTRIBUTE_MAX_CHARS` when it is a
|
|
135
|
+
* positive whole number (raised to the minimum if lower), otherwise the
|
|
136
|
+
* default. Read on every call, so a change applies to the next span.
|
|
137
|
+
*/
|
|
138
|
+
export declare function spanAttributeMaxChars(): number;
|
|
133
139
|
/**
|
|
134
140
|
* Returns a copy of `value` with credentials masked: values under keys such as
|
|
135
141
|
* `access_token`, `password`, `authorization` or `set-cookie`, bearer tokens
|
|
@@ -161,8 +167,9 @@ export declare function spanJsonAttribute(value: unknown, maxChars?: number): st
|
|
|
161
167
|
*
|
|
162
168
|
* A head cut keeps the system prompt and drops everything after it, so a long
|
|
163
169
|
* system prompt hides every message, the user's question included. Here the
|
|
164
|
-
* prompt
|
|
165
|
-
* dropped whole and counted, and the system prompt
|
|
166
|
-
*
|
|
170
|
+
* prompt, the current question and this turn's newest messages are budgeted
|
|
171
|
+
* first, earlier messages are dropped whole and counted, and the system prompt
|
|
172
|
+
* gets whatever room is left. A tool result is never kept without the call it
|
|
173
|
+
* answers. The result is always valid JSON.
|
|
167
174
|
*/
|
|
168
175
|
export declare function spanModelRequestAttribute(rawRequest: SpanModelRequest, maxChars?: number): string;
|