@juspay/neurolink 12.25.5 → 12.26.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/dist/browser/neurolink.min.js +369 -369
- package/dist/cli/commands/decide.d.ts +2 -1
- package/dist/cli/commands/decide.js +4 -2
- package/dist/cli/commands/setup.js +2 -1
- package/dist/cli/factories/commandFactory.js +1 -1
- package/dist/cli/utils/classifierRouterFlags.js +1 -1
- package/dist/constants/enums.d.ts +15 -0
- package/dist/constants/enums.js +16 -0
- package/dist/core/baseProvider.js +2 -2
- package/dist/factories/providerDescriptors.d.ts +16 -7
- package/dist/factories/providerDescriptors.js +97 -15
- package/dist/factories/providerRegistry.js +9 -1
- package/dist/models/manifestRegistry.js +2 -0
- package/dist/models/manifests/laya.d.ts +12 -0
- package/dist/models/manifests/laya.js +54 -0
- package/dist/neurolink.js +8 -3
- package/dist/providers/laya.d.ts +40 -0
- package/dist/providers/laya.js +170 -0
- package/dist/providers/systemOneDecision.d.ts +102 -0
- package/dist/providers/systemOneDecision.js +443 -0
- package/dist/providers/typesafe.d.ts +32 -47
- package/dist/providers/typesafe.js +64 -386
- package/dist/routing/classifierRouter.d.ts +2 -2
- package/dist/routing/classifierRouter.js +10 -8
- package/dist/types/classifierRouter.d.ts +13 -3
- package/dist/types/cli.d.ts +3 -2
- package/dist/types/decision.d.ts +29 -0
- package/dist/types/providers.d.ts +20 -1
- package/dist/utils/modelChoices.js +21 -1
- package/dist/utils/providerConfig.d.ts +5 -0
- package/dist/utils/providerConfig.js +17 -0
- package/docs-site/static/search-index.json +24 -10
- package/package.json +2 -1
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { BaseProvider } from "../core/baseProvider.js";
|
|
2
|
+
import { PROVIDER_DESCRIPTORS_BY_NAME } from "../factories/providerDescriptors.js";
|
|
3
|
+
import { isNeuroLink } from "../neurolink.js";
|
|
4
|
+
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
5
|
+
import { ProviderError } from "../types/index.js";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
import { estimateTokens, serializeForEstimate, } from "../utils/tokenEstimation.js";
|
|
8
|
+
/**
|
|
9
|
+
* Generous enough for a cold start (measured at 2.0–2.7s after idle on Jev)
|
|
10
|
+
* while still bounded. Every internal caller is fail-open, so this only bites
|
|
11
|
+
* on a genuine stall.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_DECISION_TIMEOUT_MS = 5000;
|
|
14
|
+
const DEFAULT_MAX_RETRIES = 1;
|
|
15
|
+
export const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
16
|
+
export const asNumber = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
17
|
+
const asNumberMap = (v) => {
|
|
18
|
+
if (!isRecord(v)) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const [k, raw] of Object.entries(v)) {
|
|
23
|
+
const n = asNumber(raw);
|
|
24
|
+
if (n === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
out[k] = n;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
};
|
|
31
|
+
const asStringMap = (v) => {
|
|
32
|
+
if (!isRecord(v)) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const [k, raw] of Object.entries(v)) {
|
|
37
|
+
if (typeof raw !== "string") {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
out[k] = raw;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Last-resort confidence, when no transport reported one.
|
|
46
|
+
*
|
|
47
|
+
* The peak probability is what a calibrated confidence approximates, but it is
|
|
48
|
+
* NOT the same number, so a threshold tuned against a vendor's calibrated
|
|
49
|
+
* figure does not transfer unexamined; an even distribution lands near 1/N
|
|
50
|
+
* rather than 0.
|
|
51
|
+
*/
|
|
52
|
+
function deriveConfidence(probabilities) {
|
|
53
|
+
const values = Object.values(probabilities);
|
|
54
|
+
return values.length > 0 ? Math.max(...values) : 0;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate one answer off the wire. Returns null rather than throwing so a
|
|
58
|
+
* single malformed answer degrades to "unanswered" instead of failing the
|
|
59
|
+
* whole batch — the batch may hold hundreds of usable answers.
|
|
60
|
+
*/
|
|
61
|
+
function parseDecisionAnswer(raw, reportedConfidence) {
|
|
62
|
+
if (!isRecord(raw)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
switch (raw.type) {
|
|
66
|
+
// "noul" is the System One wire's own spelling; "boolean" is the neutral
|
|
67
|
+
// one (Vercel's gateway), which renames both the type and the field.
|
|
68
|
+
// Accepting both keeps one parser for every transport.
|
|
69
|
+
case "noul":
|
|
70
|
+
case "boolean": {
|
|
71
|
+
const probability = asNumber(raw.noul) ?? asNumber(raw.probability);
|
|
72
|
+
return probability === undefined
|
|
73
|
+
? null
|
|
74
|
+
: { type: "boolean", probability };
|
|
75
|
+
}
|
|
76
|
+
case "choice": {
|
|
77
|
+
const probabilities = asNumberMap(raw.probabilities);
|
|
78
|
+
if (typeof raw.choice !== "string" || !probabilities) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
type: "choice",
|
|
83
|
+
choice: raw.choice,
|
|
84
|
+
confidence: asNumber(raw.confidence) ??
|
|
85
|
+
reportedConfidence ??
|
|
86
|
+
deriveConfidence(probabilities),
|
|
87
|
+
probabilities,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
case "score": {
|
|
91
|
+
const score = asNumber(raw.score);
|
|
92
|
+
const legend = asStringMap(raw.legend);
|
|
93
|
+
const probabilities = asNumberMap(raw.probabilities);
|
|
94
|
+
if (score === undefined || !legend || !probabilities) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
type: "score",
|
|
99
|
+
score,
|
|
100
|
+
confidence: asNumber(raw.confidence) ??
|
|
101
|
+
reportedConfidence ??
|
|
102
|
+
deriveConfidence(probabilities),
|
|
103
|
+
legend,
|
|
104
|
+
probabilities,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
default:
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* FastAPI's request-validation envelope, `{"detail":[{loc, msg, input}]}`,
|
|
113
|
+
* reduced to its field paths and messages. `input` echoes the caller's own
|
|
114
|
+
* request back — possibly the whole state — so it is never surfaced.
|
|
115
|
+
*/
|
|
116
|
+
export function describeValidationErrors(detail) {
|
|
117
|
+
const fields = detail
|
|
118
|
+
.filter(isRecord)
|
|
119
|
+
.map((d) => {
|
|
120
|
+
const loc = Array.isArray(d.loc)
|
|
121
|
+
? d.loc
|
|
122
|
+
.filter((p) => typeof p === "string" || typeof p === "number")
|
|
123
|
+
.join(".")
|
|
124
|
+
: "";
|
|
125
|
+
const msg = typeof d.msg === "string" ? d.msg : "invalid";
|
|
126
|
+
return loc ? `${loc}: ${msg}` : msg;
|
|
127
|
+
})
|
|
128
|
+
.slice(0, 5);
|
|
129
|
+
return fields.join("; ") || "unspecified field";
|
|
130
|
+
}
|
|
131
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
132
|
+
/**
|
|
133
|
+
* `estimateTokens` assumes ~4 characters per token. That holds for English
|
|
134
|
+
* and is several times too generous for other scripts on an English
|
|
135
|
+
* tokenizer, so when a provider declares a rate, non-ASCII characters are
|
|
136
|
+
* counted at it instead. ASCII text is estimated exactly as before.
|
|
137
|
+
*/
|
|
138
|
+
function estimateDecisionStateTokens(text, nonAsciiTokensPerChar) {
|
|
139
|
+
if (nonAsciiTokensPerChar === undefined) {
|
|
140
|
+
return estimateTokens(text);
|
|
141
|
+
}
|
|
142
|
+
const chars = [...text];
|
|
143
|
+
const isAscii = (c) => (c.codePointAt(0) ?? 0) <= 0x7f;
|
|
144
|
+
const ascii = chars.filter(isAscii).join("");
|
|
145
|
+
const nonAsciiCount = chars.length - ascii.length;
|
|
146
|
+
return (estimateTokens(ascii) + Math.ceil(nonAsciiCount * nonAsciiTokensPerChar));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The shared half of every "System One" decision provider — a model that takes
|
|
150
|
+
* one `state` plus typed questions and returns one typed answer each, with no
|
|
151
|
+
* text anywhere.
|
|
152
|
+
*
|
|
153
|
+
* Vendors differ in where the request goes, how it is authenticated, what the
|
|
154
|
+
* body carries and what their error envelopes look like. They do not differ in
|
|
155
|
+
* anything else, so everything else lives here: the missing-key check, the
|
|
156
|
+
* auth circuit breaker, the timeout and retry loop, answer parsing and usage.
|
|
157
|
+
* A new decision provider implements the abstract hooks and nothing more.
|
|
158
|
+
*/
|
|
159
|
+
export class SystemOneDecisionProvider extends BaseProvider {
|
|
160
|
+
proxyFetch;
|
|
161
|
+
maxRetries;
|
|
162
|
+
/**
|
|
163
|
+
* Set once the key has been rejected. A bad key cannot fix itself, and
|
|
164
|
+
* without this every later request would pay a full round trip (~265ms
|
|
165
|
+
* measured on Jev) to be told so again — a permanent tax on the hot path for
|
|
166
|
+
* a misconfiguration.
|
|
167
|
+
*/
|
|
168
|
+
authFailed = false;
|
|
169
|
+
constructor(modelName, providerName, sdk) {
|
|
170
|
+
super(modelName, providerName, isNeuroLink(sdk) ? sdk : undefined);
|
|
171
|
+
this.proxyFetch = createProxyFetch();
|
|
172
|
+
this.maxRetries = DEFAULT_MAX_RETRIES;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Why this instance cannot send a request even though it has a key, such as
|
|
176
|
+
* a provider with no built-in endpoint and none configured. Undefined when
|
|
177
|
+
* nothing is missing.
|
|
178
|
+
*/
|
|
179
|
+
missingConfigMessage() {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The System One wire calls the yes/no primitive `noul`; this codebase and
|
|
184
|
+
* every SDK exposing it call it `boolean`.
|
|
185
|
+
*/
|
|
186
|
+
encodeQuestion(question) {
|
|
187
|
+
if (question.type === "boolean") {
|
|
188
|
+
return {
|
|
189
|
+
type: "noul",
|
|
190
|
+
instructions: question.instructions,
|
|
191
|
+
...(question.criteria ? { criteria: question.criteria } : {}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return question;
|
|
195
|
+
}
|
|
196
|
+
/** Per-question confidence a transport reports outside the answer objects. */
|
|
197
|
+
reportedConfidence(_payload) {
|
|
198
|
+
return {};
|
|
199
|
+
}
|
|
200
|
+
resolveResponseModel(payload, requestedModel) {
|
|
201
|
+
return typeof payload.model === "string" ? payload.model : requestedModel;
|
|
202
|
+
}
|
|
203
|
+
/** Extra fields for the credential-rejected warning. */
|
|
204
|
+
diagnosticContext() {
|
|
205
|
+
return {};
|
|
206
|
+
}
|
|
207
|
+
// ===== BaseProvider obligations a text-less model satisfies identically =====
|
|
208
|
+
getProviderName() {
|
|
209
|
+
return this.providerName;
|
|
210
|
+
}
|
|
211
|
+
getDefaultDecisionModel() {
|
|
212
|
+
return this.getDefaultModel();
|
|
213
|
+
}
|
|
214
|
+
supportsTools() {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* A decision model produces no text, so there is no chat model behind it.
|
|
219
|
+
* Unreachable while the descriptor declares `inferenceKinds: ["decide"]`;
|
|
220
|
+
* kept actionable for the case where something routes here anyway.
|
|
221
|
+
*/
|
|
222
|
+
getAISDKModel() {
|
|
223
|
+
throw new ProviderError(`${this.vendorDisplayName()} is a decision-only provider; it generates no text, so chat completions are not available. Use \`decide()\` instead, or pick a different provider for \`generate()\` / \`stream()\`.`, this.providerName);
|
|
224
|
+
}
|
|
225
|
+
async executeStream(_options, _analysisSchema) {
|
|
226
|
+
throw new ProviderError(`${this.vendorDisplayName()} is a decision-only provider; streaming is not available. Use \`decide()\`, or pick another provider for \`stream()\`.`, this.providerName);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Must RETURN the error, never throw it (CLAUDE.md rule 6).
|
|
230
|
+
*/
|
|
231
|
+
formatProviderError(error) {
|
|
232
|
+
if (error instanceof ProviderError) {
|
|
233
|
+
return error;
|
|
234
|
+
}
|
|
235
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
236
|
+
return new ProviderError(`${this.vendorLabel()} request failed: ${message}`, this.providerName);
|
|
237
|
+
}
|
|
238
|
+
// ===== The decide inference type =====
|
|
239
|
+
/**
|
|
240
|
+
* Evaluate a state against a batch of typed questions.
|
|
241
|
+
*
|
|
242
|
+
* Throws a {@link ProviderError} carrying a normalised {@link DecisionError}
|
|
243
|
+
* in `cause`. Internal hot paths should prefer `NeuroLink.tryDecide()`,
|
|
244
|
+
* which returns null instead.
|
|
245
|
+
*/
|
|
246
|
+
async decide(request) {
|
|
247
|
+
const label = this.vendorLabel();
|
|
248
|
+
if (!this.decisionApiKey()) {
|
|
249
|
+
throw this.decisionError({
|
|
250
|
+
kind: "authentication",
|
|
251
|
+
message: this.missingKeyMessage(),
|
|
252
|
+
retryable: false,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const missingConfig = this.missingConfigMessage();
|
|
256
|
+
if (missingConfig) {
|
|
257
|
+
throw this.decisionError({
|
|
258
|
+
kind: "invalid_request",
|
|
259
|
+
message: missingConfig,
|
|
260
|
+
retryable: false,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (this.authFailed) {
|
|
264
|
+
throw this.decisionError({
|
|
265
|
+
kind: "authentication",
|
|
266
|
+
message: `${label} rejected this API key earlier; not retrying. Fix the key and construct a new provider.`,
|
|
267
|
+
retryable: false,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const questionEntries = Object.entries(request.questions);
|
|
271
|
+
if (questionEntries.length === 0) {
|
|
272
|
+
throw this.decisionError({
|
|
273
|
+
kind: "invalid_request",
|
|
274
|
+
message: "At least one question is required.",
|
|
275
|
+
retryable: false,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
const resolvedModel = request.model ?? this.modelName ?? this.getDefaultModel();
|
|
279
|
+
this.assertWithinDecisionLimits(request, resolvedModel, questionEntries.length);
|
|
280
|
+
const wireQuestions = Object.fromEntries(questionEntries.map(([id, question]) => [
|
|
281
|
+
id,
|
|
282
|
+
this.encodeQuestion(question),
|
|
283
|
+
]));
|
|
284
|
+
const body = JSON.stringify(this.buildDecisionBody(request.state, wireQuestions, resolvedModel));
|
|
285
|
+
const timeoutMs = request.timeoutMs ??
|
|
286
|
+
this.getDescriptorDecideMs() ??
|
|
287
|
+
this.defaultTimeout ??
|
|
288
|
+
DEFAULT_DECISION_TIMEOUT_MS;
|
|
289
|
+
let lastError;
|
|
290
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
291
|
+
const startedAt = Date.now();
|
|
292
|
+
try {
|
|
293
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
294
|
+
const signal = request.signal
|
|
295
|
+
? AbortSignal.any([request.signal, timeout])
|
|
296
|
+
: timeout;
|
|
297
|
+
const response = await this.proxyFetch(this.decisionEndpoint(), {
|
|
298
|
+
method: "POST",
|
|
299
|
+
headers: this.decisionHeaders(),
|
|
300
|
+
body,
|
|
301
|
+
signal,
|
|
302
|
+
});
|
|
303
|
+
const latencyMs = Date.now() - startedAt;
|
|
304
|
+
const requestId = this.readRequestId(response.headers);
|
|
305
|
+
const upstreamMs = Number(response.headers.get("x-envoy-upstream-service-time")) ||
|
|
306
|
+
undefined;
|
|
307
|
+
const payload = await response.json().catch(() => null);
|
|
308
|
+
if (!response.ok) {
|
|
309
|
+
lastError = this.parseDecisionError(response.status, payload, requestId);
|
|
310
|
+
if (lastError.kind === "authentication") {
|
|
311
|
+
this.authFailed = true;
|
|
312
|
+
// The breaker is right to trip — none of these clear themselves
|
|
313
|
+
// inside one process — but it must not assert WHICH credential
|
|
314
|
+
// problem it was. A gateway 403 with a valid key and no card on
|
|
315
|
+
// file is not a rejected key, and saying so sends the reader off
|
|
316
|
+
// to regenerate the one thing that works.
|
|
317
|
+
logger.warn(`${label}: credential not accepted — disabling this provider instance. ${lastError.message}`, {
|
|
318
|
+
status: lastError.status,
|
|
319
|
+
...this.diagnosticContext(),
|
|
320
|
+
requestId,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (!lastError.retryable || attempt === this.maxRetries) {
|
|
324
|
+
throw this.decisionError(lastError);
|
|
325
|
+
}
|
|
326
|
+
await sleep(2 ** attempt * 250 + Math.random() * 250);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (!isRecord(payload) || !isRecord(payload.answers)) {
|
|
330
|
+
throw this.decisionError({
|
|
331
|
+
kind: "server",
|
|
332
|
+
message: `${label} returned a response without an answers map.`,
|
|
333
|
+
status: response.status,
|
|
334
|
+
requestId,
|
|
335
|
+
retryable: false,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
const reported = this.reportedConfidence(payload);
|
|
339
|
+
const answers = {};
|
|
340
|
+
for (const [id, raw] of Object.entries(payload.answers)) {
|
|
341
|
+
const parsed = parseDecisionAnswer(raw, reported[id]);
|
|
342
|
+
if (parsed) {
|
|
343
|
+
answers[id] = parsed;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
logger.warn(`${label}: dropped unparseable answer "${id}"`, {
|
|
347
|
+
requestId,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const usage = isRecord(payload.usage) ? payload.usage : {};
|
|
352
|
+
return {
|
|
353
|
+
// `resolvedModel`, not `this.modelName`: when the caller pinned a
|
|
354
|
+
// model for this one request and the response omits its own, the
|
|
355
|
+
// instance default would be reported instead of the model actually
|
|
356
|
+
// asked for.
|
|
357
|
+
model: this.resolveResponseModel(payload, resolvedModel),
|
|
358
|
+
provider: this.providerName,
|
|
359
|
+
answers,
|
|
360
|
+
// Two spellings for one field. The System One wire sends
|
|
361
|
+
// `input_tokens`; Vercel's gateway sends `inputTokens`. Reading only
|
|
362
|
+
// the first priced every gateway decision at exactly $0, silently.
|
|
363
|
+
usage: {
|
|
364
|
+
inputTokens: asNumber(usage.input_tokens) ?? asNumber(usage.inputTokens) ?? 0,
|
|
365
|
+
outputTokens: asNumber(usage.output_tokens) ??
|
|
366
|
+
asNumber(usage.outputTokens) ??
|
|
367
|
+
0,
|
|
368
|
+
},
|
|
369
|
+
requestId,
|
|
370
|
+
latencyMs,
|
|
371
|
+
upstreamMs,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (error instanceof ProviderError) {
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
// An abort from the caller's own signal must not be retried — the
|
|
379
|
+
// turn is already being torn down.
|
|
380
|
+
const aborted = request.signal?.aborted === true;
|
|
381
|
+
const isTimeout = error instanceof Error && error.name === "TimeoutError";
|
|
382
|
+
lastError = {
|
|
383
|
+
kind: aborted ? "network" : isTimeout ? "timeout" : "network",
|
|
384
|
+
message: error instanceof Error ? error.message : String(error),
|
|
385
|
+
retryable: !aborted,
|
|
386
|
+
};
|
|
387
|
+
if (aborted || attempt === this.maxRetries) {
|
|
388
|
+
throw this.decisionError(lastError);
|
|
389
|
+
}
|
|
390
|
+
await sleep(2 ** attempt * 250 + Math.random() * 250);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
throw this.decisionError(lastError ?? {
|
|
394
|
+
kind: "network",
|
|
395
|
+
message: `${label} request failed.`,
|
|
396
|
+
retryable: false,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Refuse a request the model cannot read in full. An encoder cuts the state
|
|
401
|
+
* off past its window without saying so, so without this a decision would
|
|
402
|
+
* be made on input the model never saw — and reported as if it had.
|
|
403
|
+
*/
|
|
404
|
+
assertWithinDecisionLimits(request, model, questionCount) {
|
|
405
|
+
const limits = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName)?.decisionLimits;
|
|
406
|
+
if (!limits) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const label = this.vendorLabel();
|
|
410
|
+
if (questionCount > limits.maxQuestions) {
|
|
411
|
+
throw this.decisionError({
|
|
412
|
+
kind: "max_tokens_exceeded",
|
|
413
|
+
message: `${label} accepts at most ${limits.maxQuestions} questions per request; this one has ${questionCount}. Ask fewer, or use a decision provider without this cap.`,
|
|
414
|
+
retryable: false,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const modelLimits = limits.models?.[model];
|
|
418
|
+
const maxStateTokens = modelLimits?.maxStateTokens ?? limits.maxStateTokens;
|
|
419
|
+
const stateTokens = estimateDecisionStateTokens(serializeForEstimate(request.state), modelLimits?.nonAsciiTokensPerChar ?? limits.nonAsciiTokensPerChar);
|
|
420
|
+
if (stateTokens > maxStateTokens) {
|
|
421
|
+
throw this.decisionError({
|
|
422
|
+
kind: "max_tokens_exceeded",
|
|
423
|
+
message: `The state is ~${stateTokens} tokens; ${label}'s "${model}" model reads at most ${maxStateTokens}. Shorten the state, or use a decision provider with a larger window.`,
|
|
424
|
+
retryable: false,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Wrap a normalised decision failure in the provider error type, keeping
|
|
430
|
+
* the structured detail reachable via `cause` for callers that branch on
|
|
431
|
+
* `kind` (authentication vs rate_limit vs max_tokens_exceeded).
|
|
432
|
+
*/
|
|
433
|
+
decisionError(detail) {
|
|
434
|
+
const error = new ProviderError(detail.message, this.providerName);
|
|
435
|
+
Object.defineProperty(error, "cause", {
|
|
436
|
+
value: detail,
|
|
437
|
+
enumerable: false,
|
|
438
|
+
writable: true,
|
|
439
|
+
configurable: true,
|
|
440
|
+
});
|
|
441
|
+
return error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
import type { DecisionModelCard, DecisionRequest, DecisionResult, LanguageModel, NeurolinkCredentials, StreamOptions, StreamResult, ValidationSchema } from "../types/index.js";
|
|
1
|
+
import type { DecisionError, DecisionModelCard, DecisionQuestion, DecisionState, NeurolinkCredentials } from "../types/index.js";
|
|
2
|
+
import { SystemOneDecisionProvider } from "./systemOneDecision.js";
|
|
4
3
|
/**
|
|
5
4
|
* The binding limit in practice: `state` plus the SINGLE LONGEST question.
|
|
6
5
|
* Measured by bisection at single-character resolution — 33,002 reported
|
|
@@ -19,13 +18,12 @@ export declare const TYPESAFE_MAX_REQUEST_TOKENS = 64000;
|
|
|
19
18
|
*
|
|
20
19
|
* Jev is a "System One" model: it takes one `state` plus a map of typed
|
|
21
20
|
* questions and returns one typed answer each, evaluated in a single
|
|
22
|
-
* parallel pass.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* parallel pass. The request loop, retries, auth circuit breaker and answer
|
|
22
|
+
* parsing are shared with every decision provider in
|
|
23
|
+
* {@link SystemOneDecisionProvider}; this class holds only what is TypeSafe's.
|
|
25
24
|
*
|
|
26
25
|
* Its descriptor declares `inferenceKinds: ["decide"]`, which keeps it out
|
|
27
|
-
* of auto-select and the health sweep
|
|
28
|
-
* in practice.
|
|
26
|
+
* of auto-select and the health sweep.
|
|
29
27
|
*
|
|
30
28
|
* Two measured properties shape the API: latency is flat in question count
|
|
31
29
|
* (1 question 393ms, 400 questions 465ms) while concurrent requests queue.
|
|
@@ -35,54 +33,47 @@ export declare const TYPESAFE_MAX_REQUEST_TOKENS = 64000;
|
|
|
35
33
|
* billed against a TypeSafe key. The gateway one is Vercel's AI Gateway
|
|
36
34
|
* evaluation-model route, billed against a Vercel account — useful to a host
|
|
37
35
|
* that already has one and does not want a second vendor relationship. They
|
|
38
|
-
* differ on the wire (vocabulary, where the model is named, and
|
|
39
|
-
* `confidence` is reported
|
|
40
|
-
* one thing to know is that gateway confidence is DERIVED from the
|
|
41
|
-
* probability distribution rather than being the vendor's calibrated figure,
|
|
42
|
-
* so a threshold tuned on one transport should be re-checked on the other.
|
|
36
|
+
* differ on the wire (vocabulary, where the model is named, and where
|
|
37
|
+
* `confidence` is reported) and not at all in `DecisionResult`.
|
|
43
38
|
*
|
|
44
39
|
* @see https://docs.typesafe.ai/api
|
|
45
40
|
*/
|
|
46
|
-
export declare class TypeSafeProvider extends
|
|
41
|
+
export declare class TypeSafeProvider extends SystemOneDecisionProvider {
|
|
47
42
|
private readonly apiKey;
|
|
48
43
|
private readonly baseURL;
|
|
49
|
-
private readonly
|
|
50
|
-
private readonly maxRetries;
|
|
44
|
+
private readonly gatewayURL;
|
|
51
45
|
/** Which wire this instance speaks. Resolved once, in the constructor. */
|
|
52
46
|
private readonly transport;
|
|
53
|
-
/**
|
|
54
|
-
* Set once the key has been rejected. A bad key cannot fix itself, and
|
|
55
|
-
* without this every later request would pay a full round trip (~265ms
|
|
56
|
-
* measured) to be told so again — a permanent tax on the hot path for a
|
|
57
|
-
* misconfiguration.
|
|
58
|
-
*/
|
|
59
|
-
private authFailed;
|
|
60
47
|
constructor(modelName?: string, sdk?: unknown, _region?: string, credentials?: NeurolinkCredentials["typesafe"]);
|
|
61
|
-
protected getProviderName(): AIProviderName;
|
|
62
48
|
protected getDefaultModel(): string;
|
|
63
|
-
protected
|
|
64
|
-
|
|
49
|
+
protected vendorLabel(): string;
|
|
50
|
+
protected vendorDisplayName(): string;
|
|
51
|
+
protected decisionApiKey(): string;
|
|
52
|
+
protected missingKeyMessage(): string;
|
|
65
53
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* kept actionable for the case where something routes here anyway.
|
|
54
|
+
* The gateway already speaks the neutral vocabulary this codebase uses, so
|
|
55
|
+
* only the direct transport needs the `noul` rename.
|
|
69
56
|
*/
|
|
70
|
-
protected
|
|
71
|
-
protected executeStream(_options: StreamOptions, _analysisSchema?: ValidationSchema): Promise<StreamResult>;
|
|
57
|
+
protected encodeQuestion(question: DecisionQuestion): Record<string, unknown>;
|
|
72
58
|
/**
|
|
73
|
-
*
|
|
59
|
+
* The gateway names the model in a header and rejects a body that carries
|
|
60
|
+
* one; the direct API requires it in the body.
|
|
74
61
|
*/
|
|
75
|
-
protected
|
|
62
|
+
protected buildDecisionBody(state: DecisionState, questions: Record<string, Record<string, unknown>>, model: string): Record<string, unknown>;
|
|
63
|
+
protected parseDecisionError(status: number, payload: unknown, requestId: string | undefined): DecisionError;
|
|
76
64
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
65
|
+
* The gateway does not omit confidence after all — it RELOCATES it, to
|
|
66
|
+
* `providerMetadata.typesafe.confidence.<questionId>`, and leaves the answer
|
|
67
|
+
* objects themselves without one. Preferring it over the derived peak
|
|
68
|
+
* matters because the two disagree on any distribution that is not already
|
|
69
|
+
* near-certain, and the router's asymmetric bars are tuned against the
|
|
70
|
+
* vendor's calibrated figure, not against max(probabilities).
|
|
82
71
|
*/
|
|
83
|
-
|
|
72
|
+
protected reportedConfidence(payload: unknown): Record<string, number>;
|
|
73
|
+
protected readRequestId(headers: Headers): string | undefined;
|
|
74
|
+
protected diagnosticContext(): Record<string, unknown>;
|
|
84
75
|
/** Where a decision request goes for this instance's transport. */
|
|
85
|
-
|
|
76
|
+
protected decisionEndpoint(): string;
|
|
86
77
|
/**
|
|
87
78
|
* Headers for this instance's transport.
|
|
88
79
|
*
|
|
@@ -92,7 +83,7 @@ export declare class TypeSafeProvider extends BaseProvider {
|
|
|
92
83
|
* carries no `model`), and `ai-evaluation-model-specification-version`
|
|
93
84
|
* selects the request/response shape this provider parses.
|
|
94
85
|
*/
|
|
95
|
-
|
|
86
|
+
protected decisionHeaders(): Record<string, string>;
|
|
96
87
|
/** Which transport this instance resolved to. Exposed for diagnostics. */
|
|
97
88
|
getDecisionTransport(): "direct" | "gateway";
|
|
98
89
|
/**
|
|
@@ -103,10 +94,4 @@ export declare class TypeSafeProvider extends BaseProvider {
|
|
|
103
94
|
* this key.
|
|
104
95
|
*/
|
|
105
96
|
listDecisionModels(): Promise<DecisionModelCard[]>;
|
|
106
|
-
/**
|
|
107
|
-
* Wrap a normalised decision failure in the provider error type, keeping
|
|
108
|
-
* the structured detail reachable via `cause` for callers that branch on
|
|
109
|
-
* `kind` (authentication vs rate_limit vs max_tokens_exceeded).
|
|
110
|
-
*/
|
|
111
|
-
private decisionError;
|
|
112
97
|
}
|