@juspay/neurolink 13.4.9 → 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 -3
- package/README.md +14 -4
- 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/hitl/hitlManager.d.ts +1 -0
- package/dist/hitl/hitlManager.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/neurolink.d.ts +4 -2
- package/dist/neurolink.js +36 -10
- 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
|
@@ -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) => {
|
|
@@ -2,3 +2,9 @@ export declare const isRecord: (v: unknown) => v is Record<string, unknown>;
|
|
|
2
2
|
export declare const asNumber: (v: unknown) => number | undefined;
|
|
3
3
|
export declare const asNumberMap: (v: unknown) => Record<string, number> | undefined;
|
|
4
4
|
export declare const asStringMap: (v: unknown) => Record<string, string> | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* A map whose values are shown as text: a string as is, anything else a JSON
|
|
7
|
+
* value can hold as its JSON text. A score legend echoes each level as the
|
|
8
|
+
* caller gave it, and a level may be a number or an object.
|
|
9
|
+
*/
|
|
10
|
+
export declare const asTextMap: (v: unknown) => Record<string, string> | undefined;
|
|
@@ -27,3 +27,25 @@ export const asStringMap = (v) => {
|
|
|
27
27
|
}
|
|
28
28
|
return out;
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* A map whose values are shown as text: a string as is, anything else a JSON
|
|
32
|
+
* value can hold as its JSON text. A score legend echoes each level as the
|
|
33
|
+
* caller gave it, and a level may be a number or an object.
|
|
34
|
+
*/
|
|
35
|
+
export const asTextMap = (v) => {
|
|
36
|
+
if (!isRecord(v)) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const [k, raw] of Object.entries(v)) {
|
|
41
|
+
if (typeof raw === "string") {
|
|
42
|
+
out[k] = raw;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (raw === undefined || typeof raw === "function") {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
out[k] = JSON.stringify(raw);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
};
|
|
@@ -22,6 +22,12 @@ export declare function describeValidationErrors(detail: readonly unknown[]): st
|
|
|
22
22
|
* belongs in an error message or a log, whatever the wording, so the
|
|
23
23
|
* configured key, anything shaped like a LiteLLM key, any long hex run and any
|
|
24
24
|
* embedded `data:` URL are all removed.
|
|
25
|
+
*
|
|
26
|
+
* Its request-validation errors also quote the request back (pydantic's
|
|
27
|
+
* `input_value=…`, the start of the state and questions), and its routing
|
|
28
|
+
* errors append the gateway's fallback table, which names every model group it
|
|
29
|
+
* serves. The first is the caller's data and the second is the gateway's, so
|
|
30
|
+
* both are cut.
|
|
25
31
|
*/
|
|
26
32
|
export declare function redactCredentials(message: string, apiKey: string): string;
|
|
27
33
|
/**
|
|
@@ -85,12 +91,14 @@ export declare abstract class SystemOneDecisionProvider extends BaseProvider {
|
|
|
85
91
|
*/
|
|
86
92
|
protected readRetryAfterMs(_headers: Headers): number | undefined;
|
|
87
93
|
/**
|
|
88
|
-
* The per-attempt timeout when the caller sets none
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
94
|
+
* The per-attempt timeout when the caller sets none: the descriptor's
|
|
95
|
+
* `decideMs`, plus `decideMsPerKStateTokens` for every full 1,000 estimated
|
|
96
|
+
* state tokens when it declares that, since a long state takes longer to read
|
|
97
|
+
* and a flat budget would cut off a request well inside the model's window. A
|
|
98
|
+
* provider whose latency grows another way overrides this. `stateTokens` is
|
|
99
|
+
* the local estimate, 0 when the descriptor sets no local limits.
|
|
92
100
|
*/
|
|
93
|
-
protected defaultTimeoutMs(_questionCount: number,
|
|
101
|
+
protected defaultTimeoutMs(_questionCount: number, stateTokens: number): number | undefined;
|
|
94
102
|
/**
|
|
95
103
|
* The part of a successful response that holds `answers`, `usage` and
|
|
96
104
|
* `model`. The default is the response itself; a vendor that wraps every
|
|
@@ -101,6 +109,12 @@ export declare abstract class SystemOneDecisionProvider extends BaseProvider {
|
|
|
101
109
|
/** Per-question confidence a transport reports outside the answer objects. */
|
|
102
110
|
protected reportedConfidence(_payload: unknown): Record<string, number>;
|
|
103
111
|
protected resolveResponseModel(payload: Record<string, unknown>, requestedModel: string): string;
|
|
112
|
+
/**
|
|
113
|
+
* The model this request goes to and is checked against: the call's own, else
|
|
114
|
+
* the instance's. A provider whose default cannot read some requests may pick
|
|
115
|
+
* another for them here.
|
|
116
|
+
*/
|
|
117
|
+
protected resolveRequestModel(request: DecisionRequest): string;
|
|
104
118
|
/** Extra fields for the credential-rejected warning. */
|
|
105
119
|
protected diagnosticContext(): Record<string, unknown>;
|
|
106
120
|
protected getProviderName(): AIProviderName;
|
|
@@ -126,8 +140,17 @@ export declare abstract class SystemOneDecisionProvider extends BaseProvider {
|
|
|
126
140
|
*/
|
|
127
141
|
decide(request: DecisionRequest): Promise<DecisionResult>;
|
|
128
142
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
143
|
+
* Refuse a question this provider does not read: a type it does not list in
|
|
144
|
+
* `decisionLimits.questionTypes` (absent means boolean, choice and score), or
|
|
145
|
+
* a choice or score past its per-question cap. Checked even when the limits
|
|
146
|
+
* are advisory, since a server that does not know a question type cannot
|
|
147
|
+
* answer it.
|
|
148
|
+
*/
|
|
149
|
+
private assertSupportedQuestions;
|
|
150
|
+
/**
|
|
151
|
+
* Turn the request's images, video and audio into wire form, or refuse it.
|
|
152
|
+
* Refused as a non-retryable `invalid_request`: resending the same media
|
|
153
|
+
* cannot help.
|
|
131
154
|
*/
|
|
132
155
|
private prepareMedia;
|
|
133
156
|
/**
|
|
@@ -6,7 +6,7 @@ import { ProviderError } from "../types/index.js";
|
|
|
6
6
|
import { prepareDecisionMedia } from "../utils/decisionMedia.js";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
8
|
import { redactUrlsInText } from "../utils/logSanitize.js";
|
|
9
|
-
import { estimateDecisionStateTokens, resolveDecisionLimitsReading, } from "../utils/decisionLimits.js";
|
|
9
|
+
import { estimateDecisionStateTokens, resolveDecisionLimitsReading, unsupportedDecisionQuestion, } from "../utils/decisionLimits.js";
|
|
10
10
|
import { systemOneDialect } from "./decide/dialects/systemOne.js";
|
|
11
11
|
import { asNumber, isRecord } from "./decide/guards.js";
|
|
12
12
|
/**
|
|
@@ -44,17 +44,31 @@ export function describeValidationErrors(detail) {
|
|
|
44
44
|
* belongs in an error message or a log, whatever the wording, so the
|
|
45
45
|
* configured key, anything shaped like a LiteLLM key, any long hex run and any
|
|
46
46
|
* embedded `data:` URL are all removed.
|
|
47
|
+
*
|
|
48
|
+
* Its request-validation errors also quote the request back (pydantic's
|
|
49
|
+
* `input_value=…`, the start of the state and questions), and its routing
|
|
50
|
+
* errors append the gateway's fallback table, which names every model group it
|
|
51
|
+
* serves. The first is the caller's data and the second is the gateway's, so
|
|
52
|
+
* both are cut.
|
|
47
53
|
*/
|
|
48
54
|
export function redactCredentials(message, apiKey) {
|
|
49
55
|
const withoutKey = apiKey
|
|
50
56
|
? message.split(apiKey).join("[redacted]")
|
|
51
57
|
: message;
|
|
52
|
-
return withoutKey
|
|
58
|
+
return (withoutKey
|
|
53
59
|
.replace(/\.?\s*Received API Key\s*=[\s\S]*$/, "")
|
|
60
|
+
.replace(/\.?\s*Received Model Group=[\s\S]*$/, "")
|
|
61
|
+
.replace(/\s*No fallback model group found[\s\S]*$/, "")
|
|
62
|
+
// Greedy: the quoted value may itself contain ", input_type=", so only the
|
|
63
|
+
// last one is a delimiter the server wrote.
|
|
64
|
+
.replace(/input_value=[\s\S]*(?=, input_type=)/g, "input_value=[redacted]")
|
|
65
|
+
// Cut short by the server, a quoted value has no input_type after it, and
|
|
66
|
+
// may run over several lines; everything after it is the value.
|
|
67
|
+
.replace(/input_value=(?!\[redacted\])[\s\S]*/g, "input_value=[redacted]")
|
|
54
68
|
.replace(/\bsk-[A-Za-z0-9._-]+/g, "[redacted]")
|
|
55
69
|
.replace(/\b[0-9a-f]{32,}\b/gi, "[redacted]")
|
|
56
70
|
.replace(/data:[a-z]+\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+/gi, "[media]")
|
|
57
|
-
.trim();
|
|
71
|
+
.trim());
|
|
58
72
|
}
|
|
59
73
|
/**
|
|
60
74
|
* Whether a failed `fetch` was one that refused a redirect. undici words every
|
|
@@ -151,13 +165,20 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
151
165
|
return undefined;
|
|
152
166
|
}
|
|
153
167
|
/**
|
|
154
|
-
* The per-attempt timeout when the caller sets none
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
168
|
+
* The per-attempt timeout when the caller sets none: the descriptor's
|
|
169
|
+
* `decideMs`, plus `decideMsPerKStateTokens` for every full 1,000 estimated
|
|
170
|
+
* state tokens when it declares that, since a long state takes longer to read
|
|
171
|
+
* and a flat budget would cut off a request well inside the model's window. A
|
|
172
|
+
* provider whose latency grows another way overrides this. `stateTokens` is
|
|
173
|
+
* the local estimate, 0 when the descriptor sets no local limits.
|
|
158
174
|
*/
|
|
159
|
-
defaultTimeoutMs(_questionCount,
|
|
160
|
-
|
|
175
|
+
defaultTimeoutMs(_questionCount, stateTokens) {
|
|
176
|
+
const base = this.getDescriptorDecideMs();
|
|
177
|
+
const perK = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName)?.timeouts
|
|
178
|
+
?.decideMsPerKStateTokens;
|
|
179
|
+
return base === undefined || perK === undefined
|
|
180
|
+
? base
|
|
181
|
+
: base + Math.floor(stateTokens / 1000) * perK;
|
|
161
182
|
}
|
|
162
183
|
/**
|
|
163
184
|
* The part of a successful response that holds `answers`, `usage` and
|
|
@@ -175,6 +196,14 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
175
196
|
resolveResponseModel(payload, requestedModel) {
|
|
176
197
|
return typeof payload.model === "string" ? payload.model : requestedModel;
|
|
177
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* The model this request goes to and is checked against: the call's own, else
|
|
201
|
+
* the instance's. A provider whose default cannot read some requests may pick
|
|
202
|
+
* another for them here.
|
|
203
|
+
*/
|
|
204
|
+
resolveRequestModel(request) {
|
|
205
|
+
return request.model ?? this.modelName ?? this.getDefaultModel();
|
|
206
|
+
}
|
|
178
207
|
/** Extra fields for the credential-rejected warning. */
|
|
179
208
|
diagnosticContext() {
|
|
180
209
|
return {};
|
|
@@ -250,7 +279,8 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
250
279
|
retryable: false,
|
|
251
280
|
});
|
|
252
281
|
}
|
|
253
|
-
const resolvedModel =
|
|
282
|
+
const resolvedModel = this.resolveRequestModel(request);
|
|
283
|
+
this.assertSupportedQuestions(request, resolvedModel);
|
|
254
284
|
const stateTokens = this.assertWithinDecisionLimits(request, resolvedModel, questionEntries.length);
|
|
255
285
|
const wireQuestions = Object.fromEntries(questionEntries.map(([id, question]) => [
|
|
256
286
|
id,
|
|
@@ -393,12 +423,39 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
393
423
|
});
|
|
394
424
|
}
|
|
395
425
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
426
|
+
* Refuse a question this provider does not read: a type it does not list in
|
|
427
|
+
* `decisionLimits.questionTypes` (absent means boolean, choice and score), or
|
|
428
|
+
* a choice or score past its per-question cap. Checked even when the limits
|
|
429
|
+
* are advisory, since a server that does not know a question type cannot
|
|
430
|
+
* answer it.
|
|
431
|
+
*/
|
|
432
|
+
assertSupportedQuestions(request, model) {
|
|
433
|
+
const descriptor = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName);
|
|
434
|
+
const limits = descriptor
|
|
435
|
+
? resolveDecisionLimitsReading(descriptor, model)
|
|
436
|
+
: null;
|
|
437
|
+
for (const [id, question] of Object.entries(request.questions)) {
|
|
438
|
+
const problem = unsupportedDecisionQuestion(question, limits);
|
|
439
|
+
if (problem !== undefined) {
|
|
440
|
+
throw this.decisionError({
|
|
441
|
+
kind: "invalid_request",
|
|
442
|
+
message: `${this.vendorLabel()} cannot answer question "${id}": ${problem}.`,
|
|
443
|
+
retryable: false,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Turn the request's images, video and audio into wire form, or refuse it.
|
|
450
|
+
* Refused as a non-retryable `invalid_request`: resending the same media
|
|
451
|
+
* cannot help.
|
|
398
452
|
*/
|
|
399
453
|
async prepareMedia(request) {
|
|
400
454
|
const limits = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName)?.decisionLimits;
|
|
401
|
-
const prepared = await prepareDecisionMedia(request, limits?.media, this.vendorLabel(), listMediaDecisionProviders({
|
|
455
|
+
const prepared = await prepareDecisionMedia(request, limits?.media, this.vendorLabel(), listMediaDecisionProviders({
|
|
456
|
+
video: request.video !== undefined,
|
|
457
|
+
audio: request.audio !== undefined,
|
|
458
|
+
}));
|
|
402
459
|
if (prepared.status === "refused") {
|
|
403
460
|
throw this.decisionError({
|
|
404
461
|
kind: "invalid_request",
|
|
@@ -423,7 +480,7 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
423
480
|
if (bytes > limit) {
|
|
424
481
|
throw this.decisionError({
|
|
425
482
|
kind: "invalid_request",
|
|
426
|
-
message: `The request is ${bytes} bytes; ${this.vendorLabel()} accepts at most ${limit}. Send fewer or smaller images${media?.video ? ", or a shorter video" : ""}.`,
|
|
483
|
+
message: `The request is ${bytes} bytes; ${this.vendorLabel()} accepts at most ${limit}. Send fewer or smaller images${media?.video ? ", or a shorter video" : ""}${media?.audio ? ", or a shorter audio clip" : ""}.`,
|
|
427
484
|
retryable: false,
|
|
428
485
|
});
|
|
429
486
|
}
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -1312,6 +1312,8 @@ export type CliDecideArgs = {
|
|
|
1312
1312
|
image?: string[];
|
|
1313
1313
|
/** One video file (or data: URL) for a provider that reads video. */
|
|
1314
1314
|
video?: string;
|
|
1315
|
+
/** One audio file (or data: URL) for a provider that reads audio. */
|
|
1316
|
+
audio?: string;
|
|
1315
1317
|
provider?: string;
|
|
1316
1318
|
model?: string;
|
|
1317
1319
|
timeout?: number;
|