@juspay/neurolink 12.25.4 → 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 +372 -372
- 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/anthropic/client.js +17 -1
- 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 +3 -1
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { TypeSafeModels } from "../constants/enums.js";
|
|
2
|
-
import { BaseProvider } from "../core/baseProvider.js";
|
|
3
|
-
import { isNeuroLink } from "../neurolink.js";
|
|
4
|
-
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
5
|
-
import { ProviderError } from "../types/index.js";
|
|
6
2
|
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { redactUrlForError } from "../utils/logSanitize.js";
|
|
7
4
|
import { getProviderModel } from "../utils/providerConfig.js";
|
|
5
|
+
import { asNumber, DEFAULT_DECISION_TIMEOUT_MS, describeValidationErrors, isRecord, SystemOneDecisionProvider, } from "./systemOneDecision.js";
|
|
8
6
|
const TYPESAFE_DEFAULT_BASE_URL = "https://api.typesafe.ai";
|
|
9
7
|
/**
|
|
10
8
|
* Vercel AI Gateway's evaluation-model endpoint.
|
|
@@ -14,7 +12,7 @@ const TYPESAFE_DEFAULT_BASE_URL = "https://api.typesafe.ai";
|
|
|
14
12
|
* `inferenceKinds`, arrived at independently. The gateway therefore has its
|
|
15
13
|
* own route rather than reusing the chat one.
|
|
16
14
|
*/
|
|
17
|
-
const
|
|
15
|
+
const TYPESAFE_DEFAULT_GATEWAY_URL = "https://ai-gateway.vercel.sh/v4/ai/evaluation-model";
|
|
18
16
|
/** Gateway model id for Jev. The gateway carries it in a header, not the body. */
|
|
19
17
|
const GATEWAY_MODEL_ID = "typesafe-ai/jev";
|
|
20
18
|
/**
|
|
@@ -25,13 +23,6 @@ const GATEWAY_MODEL_ID = "typesafe-ai/jev";
|
|
|
25
23
|
const GATEWAY_PROTOCOL_VERSION = "0.0.1";
|
|
26
24
|
/** Evaluation-model spec version the request body conforms to. */
|
|
27
25
|
const GATEWAY_SPEC_VERSION = "4";
|
|
28
|
-
/**
|
|
29
|
-
* Generous enough for a cold start (measured at 2.0–2.7s after idle) while
|
|
30
|
-
* still bounded. Steady-state p50 is ~400ms, so this only bites on the first
|
|
31
|
-
* call or a genuine stall — and every internal caller is fail-open anyway.
|
|
32
|
-
*/
|
|
33
|
-
const DEFAULT_TIMEOUT_MS = 5000;
|
|
34
|
-
const DEFAULT_MAX_RETRIES = 1;
|
|
35
26
|
/**
|
|
36
27
|
* The binding limit in practice: `state` plus the SINGLE LONGEST question.
|
|
37
28
|
* Measured by bisection at single-character resolution — 33,002 reported
|
|
@@ -45,112 +36,6 @@ export const TYPESAFE_MAX_STATE_TOKENS = 33_000;
|
|
|
45
36
|
* 33K budget, only for this one.
|
|
46
37
|
*/
|
|
47
38
|
export const TYPESAFE_MAX_REQUEST_TOKENS = 64_000;
|
|
48
|
-
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
49
|
-
const asNumber = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
50
|
-
const asNumberMap = (v) => {
|
|
51
|
-
if (!isRecord(v)) {
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
const out = {};
|
|
55
|
-
for (const [k, raw] of Object.entries(v)) {
|
|
56
|
-
const n = asNumber(raw);
|
|
57
|
-
if (n === undefined) {
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
out[k] = n;
|
|
61
|
-
}
|
|
62
|
-
return out;
|
|
63
|
-
};
|
|
64
|
-
const asStringMap = (v) => {
|
|
65
|
-
if (!isRecord(v)) {
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
const out = {};
|
|
69
|
-
for (const [k, raw] of Object.entries(v)) {
|
|
70
|
-
if (typeof raw !== "string") {
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
out[k] = raw;
|
|
74
|
-
}
|
|
75
|
-
return out;
|
|
76
|
-
};
|
|
77
|
-
/**
|
|
78
|
-
* Translate the provider-neutral question vocabulary onto TypeSafe's wire.
|
|
79
|
-
*
|
|
80
|
-
* The only difference is the name of the yes/no primitive: TypeSafe calls it
|
|
81
|
-
* `noul`, and everyone exposing it — the Vercel AI SDK, Pydantic AI, and this
|
|
82
|
-
* codebase — calls it `boolean`. Vercel's AI Gateway fronts the same model
|
|
83
|
-
* and uses the neutral name, so the gateway transport needs no translation
|
|
84
|
-
* at all.
|
|
85
|
-
*/
|
|
86
|
-
function toWireQuestion(question, transport) {
|
|
87
|
-
// The gateway already speaks the neutral vocabulary this codebase uses, so
|
|
88
|
-
// only the direct transport needs the rename.
|
|
89
|
-
if (question.type === "boolean" && transport === "direct") {
|
|
90
|
-
return {
|
|
91
|
-
type: "noul",
|
|
92
|
-
instructions: question.instructions,
|
|
93
|
-
...(question.criteria ? { criteria: question.criteria } : {}),
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
return question;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Validate one answer off the wire. Returns null rather than throwing so a
|
|
100
|
-
* single malformed answer degrades to "unanswered" instead of failing the
|
|
101
|
-
* whole batch — the batch may hold hundreds of usable answers.
|
|
102
|
-
*/
|
|
103
|
-
function parseAnswer(raw, reportedConfidence) {
|
|
104
|
-
if (!isRecord(raw)) {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
switch (raw.type) {
|
|
108
|
-
// "noul" is TypeSafe's own spelling; "boolean" is the gateway's, which
|
|
109
|
-
// renames both the type and the field. Accepting both keeps one parser
|
|
110
|
-
// for two transports — and keeps the direct path working if TypeSafe
|
|
111
|
-
// ever adopts the neutral name itself.
|
|
112
|
-
case "noul":
|
|
113
|
-
case "boolean": {
|
|
114
|
-
const probability = asNumber(raw.noul) ?? asNumber(raw.probability);
|
|
115
|
-
return probability === undefined
|
|
116
|
-
? null
|
|
117
|
-
: { type: "boolean", probability };
|
|
118
|
-
}
|
|
119
|
-
case "choice": {
|
|
120
|
-
const probabilities = asNumberMap(raw.probabilities);
|
|
121
|
-
if (typeof raw.choice !== "string" || !probabilities) {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
return {
|
|
125
|
-
type: "choice",
|
|
126
|
-
choice: raw.choice,
|
|
127
|
-
confidence: asNumber(raw.confidence) ??
|
|
128
|
-
reportedConfidence ??
|
|
129
|
-
deriveConfidence(probabilities),
|
|
130
|
-
probabilities,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
case "score": {
|
|
134
|
-
const score = asNumber(raw.score);
|
|
135
|
-
const legend = asStringMap(raw.legend);
|
|
136
|
-
const probabilities = asNumberMap(raw.probabilities);
|
|
137
|
-
if (score === undefined || !legend || !probabilities) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
return {
|
|
141
|
-
type: "score",
|
|
142
|
-
score,
|
|
143
|
-
confidence: asNumber(raw.confidence) ??
|
|
144
|
-
reportedConfidence ??
|
|
145
|
-
deriveConfidence(probabilities),
|
|
146
|
-
legend,
|
|
147
|
-
probabilities,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
default:
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
39
|
/**
|
|
155
40
|
* Per-question confidence the gateway reports out-of-band, keyed by question
|
|
156
41
|
* id, under `providerMetadata.typesafe.confidence`. Absent on the direct
|
|
@@ -173,23 +58,6 @@ function readReportedConfidence(payload) {
|
|
|
173
58
|
}
|
|
174
59
|
return out;
|
|
175
60
|
}
|
|
176
|
-
/**
|
|
177
|
-
* Last-resort confidence, when no transport reported one.
|
|
178
|
-
*
|
|
179
|
-
* The gateway leaves `confidence` off every answer object, so this was once
|
|
180
|
-
* the only value a gateway caller ever saw. It is not: the gateway reports the
|
|
181
|
-
* vendor's calibrated figure under `providerMetadata` (see
|
|
182
|
-
* {@link readReportedConfidence}), which is preferred wherever it is present.
|
|
183
|
-
* This remains the fallback for an answer neither source covers.
|
|
184
|
-
*
|
|
185
|
-
* The peak probability is what a calibrated confidence approximates, but it is
|
|
186
|
-
* NOT the same number, so a threshold tuned against the direct transport does
|
|
187
|
-
* not transfer unexamined; an even distribution lands near 1/N rather than 0.
|
|
188
|
-
*/
|
|
189
|
-
function deriveConfidence(probabilities) {
|
|
190
|
-
const values = Object.values(probabilities);
|
|
191
|
-
return values.length > 0 ? Math.max(...values) : 0;
|
|
192
|
-
}
|
|
193
61
|
/**
|
|
194
62
|
* Gateway error `type` → our kind. The gateway's own slugs are the only
|
|
195
63
|
* reliable discriminator: it returns 403 for both a credential that cannot
|
|
@@ -236,7 +104,7 @@ function parseGatewayError(status, error, requestId, retryable) {
|
|
|
236
104
|
* breaker under the message "API key rejected", which points the reader at
|
|
237
105
|
* the one thing that is not wrong.
|
|
238
106
|
*/
|
|
239
|
-
function
|
|
107
|
+
function parseTypeSafeError(status, body, requestId) {
|
|
240
108
|
const retryable = status === 429 || status === 529 || status >= 500;
|
|
241
109
|
const gateway = isRecord(body) && isRecord(body.error) ? body.error : undefined;
|
|
242
110
|
if (gateway) {
|
|
@@ -244,21 +112,9 @@ function parseError(status, body, requestId) {
|
|
|
244
112
|
}
|
|
245
113
|
const detail = isRecord(body) ? body.detail : undefined;
|
|
246
114
|
if (Array.isArray(detail)) {
|
|
247
|
-
const fields = detail
|
|
248
|
-
.filter(isRecord)
|
|
249
|
-
.map((d) => {
|
|
250
|
-
const loc = Array.isArray(d.loc)
|
|
251
|
-
? d.loc
|
|
252
|
-
.filter((p) => typeof p === "string" || typeof p === "number")
|
|
253
|
-
.join(".")
|
|
254
|
-
: "";
|
|
255
|
-
const msg = typeof d.msg === "string" ? d.msg : "invalid";
|
|
256
|
-
return loc ? `${loc}: ${msg}` : msg;
|
|
257
|
-
})
|
|
258
|
-
.slice(0, 5);
|
|
259
115
|
return {
|
|
260
116
|
kind: "invalid_request",
|
|
261
|
-
message: `Request failed validation — ${
|
|
117
|
+
message: `Request failed validation — ${describeValidationErrors(detail)}`,
|
|
262
118
|
status,
|
|
263
119
|
requestId,
|
|
264
120
|
retryable: false,
|
|
@@ -292,19 +148,17 @@ function parseError(status, body, requestId) {
|
|
|
292
148
|
: fallback;
|
|
293
149
|
return { kind, message, status, requestId, retryable };
|
|
294
150
|
}
|
|
295
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
296
151
|
/**
|
|
297
152
|
* TypeSafe Provider — the `decide` inference type only.
|
|
298
153
|
*
|
|
299
154
|
* Jev is a "System One" model: it takes one `state` plus a map of typed
|
|
300
155
|
* questions and returns one typed answer each, evaluated in a single
|
|
301
|
-
* parallel pass.
|
|
302
|
-
*
|
|
303
|
-
*
|
|
156
|
+
* parallel pass. The request loop, retries, auth circuit breaker and answer
|
|
157
|
+
* parsing are shared with every decision provider in
|
|
158
|
+
* {@link SystemOneDecisionProvider}; this class holds only what is TypeSafe's.
|
|
304
159
|
*
|
|
305
160
|
* Its descriptor declares `inferenceKinds: ["decide"]`, which keeps it out
|
|
306
|
-
* of auto-select and the health sweep
|
|
307
|
-
* in practice.
|
|
161
|
+
* of auto-select and the health sweep.
|
|
308
162
|
*
|
|
309
163
|
* Two measured properties shape the API: latency is flat in question count
|
|
310
164
|
* (1 question 393ms, 400 questions 465ms) while concurrent requests queue.
|
|
@@ -314,31 +168,19 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
314
168
|
* billed against a TypeSafe key. The gateway one is Vercel's AI Gateway
|
|
315
169
|
* evaluation-model route, billed against a Vercel account — useful to a host
|
|
316
170
|
* that already has one and does not want a second vendor relationship. They
|
|
317
|
-
* differ on the wire (vocabulary, where the model is named, and
|
|
318
|
-
* `confidence` is reported
|
|
319
|
-
* one thing to know is that gateway confidence is DERIVED from the
|
|
320
|
-
* probability distribution rather than being the vendor's calibrated figure,
|
|
321
|
-
* so a threshold tuned on one transport should be re-checked on the other.
|
|
171
|
+
* differ on the wire (vocabulary, where the model is named, and where
|
|
172
|
+
* `confidence` is reported) and not at all in `DecisionResult`.
|
|
322
173
|
*
|
|
323
174
|
* @see https://docs.typesafe.ai/api
|
|
324
175
|
*/
|
|
325
|
-
export class TypeSafeProvider extends
|
|
176
|
+
export class TypeSafeProvider extends SystemOneDecisionProvider {
|
|
326
177
|
apiKey;
|
|
327
178
|
baseURL;
|
|
328
|
-
|
|
329
|
-
maxRetries;
|
|
179
|
+
gatewayURL;
|
|
330
180
|
/** Which wire this instance speaks. Resolved once, in the constructor. */
|
|
331
181
|
transport;
|
|
332
|
-
/**
|
|
333
|
-
* Set once the key has been rejected. A bad key cannot fix itself, and
|
|
334
|
-
* without this every later request would pay a full round trip (~265ms
|
|
335
|
-
* measured) to be told so again — a permanent tax on the hot path for a
|
|
336
|
-
* misconfiguration.
|
|
337
|
-
*/
|
|
338
|
-
authFailed = false;
|
|
339
182
|
constructor(modelName, sdk, _region, credentials) {
|
|
340
|
-
|
|
341
|
-
super(modelName, "typesafe", validatedNeurolink);
|
|
183
|
+
super(modelName, "typesafe", sdk);
|
|
342
184
|
const directKey = credentials?.apiKey?.trim() ||
|
|
343
185
|
(process.env.TYPESAFE_API_KEY?.trim() ?? "");
|
|
344
186
|
const gatewayKey = credentials?.gatewayApiKey?.trim() ||
|
|
@@ -359,223 +201,74 @@ export class TypeSafeProvider extends BaseProvider {
|
|
|
359
201
|
this.baseURL = (credentials?.baseURL ??
|
|
360
202
|
process.env.TYPESAFE_BASE_URL ??
|
|
361
203
|
TYPESAFE_DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
362
|
-
this.
|
|
363
|
-
|
|
204
|
+
this.gatewayURL = (credentials?.gatewayURL?.trim() ||
|
|
205
|
+
process.env.TYPESAFE_GATEWAY_URL?.trim() ||
|
|
206
|
+
TYPESAFE_DEFAULT_GATEWAY_URL).replace(/\/+$/, "");
|
|
364
207
|
logger.debug("TypeSafe Provider initialized (decide only)", {
|
|
365
208
|
modelName: this.modelName,
|
|
366
209
|
transport: this.transport,
|
|
367
|
-
baseURL: this.transport === "gateway" ?
|
|
210
|
+
baseURL: redactUrlForError(this.transport === "gateway" ? this.gatewayURL : this.baseURL),
|
|
368
211
|
});
|
|
369
212
|
}
|
|
370
|
-
// ===== Required abstract overrides =====
|
|
371
|
-
getProviderName() {
|
|
372
|
-
return this.providerName;
|
|
373
|
-
}
|
|
374
213
|
getDefaultModel() {
|
|
375
214
|
return getProviderModel("TYPESAFE_MODEL", TypeSafeModels.JEV_LATEST);
|
|
376
215
|
}
|
|
377
|
-
|
|
378
|
-
return
|
|
216
|
+
vendorLabel() {
|
|
217
|
+
return "TypeSafe";
|
|
218
|
+
}
|
|
219
|
+
vendorDisplayName() {
|
|
220
|
+
return "TypeSafe (Jev)";
|
|
379
221
|
}
|
|
380
|
-
|
|
381
|
-
return
|
|
222
|
+
decisionApiKey() {
|
|
223
|
+
return this.apiKey;
|
|
224
|
+
}
|
|
225
|
+
missingKeyMessage() {
|
|
226
|
+
return this.transport === "gateway"
|
|
227
|
+
? "The TypeSafe gateway transport requires a Vercel AI Gateway key. Set AI_GATEWAY_API_KEY or pass credentials.typesafe.gatewayApiKey."
|
|
228
|
+
: "TypeSafe requires an API key. Set TYPESAFE_API_KEY or pass credentials.typesafe.apiKey.";
|
|
382
229
|
}
|
|
383
230
|
/**
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* kept actionable for the case where something routes here anyway.
|
|
231
|
+
* The gateway already speaks the neutral vocabulary this codebase uses, so
|
|
232
|
+
* only the direct transport needs the `noul` rename.
|
|
387
233
|
*/
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
throw new ProviderError("TypeSafe (Jev) is a decision-only provider; streaming is not available. Use `decide()`, or pick another provider for `stream()`.", "typesafe");
|
|
234
|
+
encodeQuestion(question) {
|
|
235
|
+
return this.transport === "gateway"
|
|
236
|
+
? question
|
|
237
|
+
: super.encodeQuestion(question);
|
|
393
238
|
}
|
|
394
239
|
/**
|
|
395
|
-
*
|
|
240
|
+
* The gateway names the model in a header and rejects a body that carries
|
|
241
|
+
* one; the direct API requires it in the body.
|
|
396
242
|
*/
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
243
|
+
buildDecisionBody(state, questions, model) {
|
|
244
|
+
return this.transport === "gateway"
|
|
245
|
+
? { state, questions }
|
|
246
|
+
: { model, state, questions };
|
|
247
|
+
}
|
|
248
|
+
parseDecisionError(status, payload, requestId) {
|
|
249
|
+
return parseTypeSafeError(status, payload, requestId);
|
|
403
250
|
}
|
|
404
|
-
// ===== The decide inference type =====
|
|
405
251
|
/**
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
252
|
+
* The gateway does not omit confidence after all — it RELOCATES it, to
|
|
253
|
+
* `providerMetadata.typesafe.confidence.<questionId>`, and leaves the answer
|
|
254
|
+
* objects themselves without one. Preferring it over the derived peak
|
|
255
|
+
* matters because the two disagree on any distribution that is not already
|
|
256
|
+
* near-certain, and the router's asymmetric bars are tuned against the
|
|
257
|
+
* vendor's calibrated figure, not against max(probabilities).
|
|
411
258
|
*/
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
if (this.authFailed) {
|
|
423
|
-
throw this.decisionError({
|
|
424
|
-
kind: "authentication",
|
|
425
|
-
message: "TypeSafe rejected this API key earlier; not retrying. Fix the key and construct a new provider.",
|
|
426
|
-
retryable: false,
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
const questionEntries = Object.entries(request.questions);
|
|
430
|
-
if (questionEntries.length === 0) {
|
|
431
|
-
throw this.decisionError({
|
|
432
|
-
kind: "invalid_request",
|
|
433
|
-
message: "At least one question is required.",
|
|
434
|
-
retryable: false,
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
const wireQuestions = Object.fromEntries(questionEntries.map(([id, question]) => [
|
|
438
|
-
id,
|
|
439
|
-
toWireQuestion(question, this.transport),
|
|
440
|
-
]));
|
|
441
|
-
const resolvedModel = request.model ?? this.modelName ?? this.getDefaultModel();
|
|
442
|
-
// The gateway names the model in a header and rejects a body that carries
|
|
443
|
-
// one; the direct API requires it in the body.
|
|
444
|
-
const body = JSON.stringify(this.transport === "gateway"
|
|
445
|
-
? { state: request.state, questions: wireQuestions }
|
|
446
|
-
: {
|
|
447
|
-
model: resolvedModel,
|
|
448
|
-
state: request.state,
|
|
449
|
-
questions: wireQuestions,
|
|
450
|
-
});
|
|
451
|
-
const timeoutMs = request.timeoutMs ??
|
|
452
|
-
this.getDescriptorDecideMs() ??
|
|
453
|
-
this.defaultTimeout ??
|
|
454
|
-
DEFAULT_TIMEOUT_MS;
|
|
455
|
-
let lastError;
|
|
456
|
-
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
457
|
-
const startedAt = Date.now();
|
|
458
|
-
try {
|
|
459
|
-
const timeout = AbortSignal.timeout(timeoutMs);
|
|
460
|
-
const signal = request.signal
|
|
461
|
-
? AbortSignal.any([request.signal, timeout])
|
|
462
|
-
: timeout;
|
|
463
|
-
const response = await this.proxyFetch(this.endpoint(), {
|
|
464
|
-
method: "POST",
|
|
465
|
-
headers: this.headers(),
|
|
466
|
-
body,
|
|
467
|
-
signal,
|
|
468
|
-
});
|
|
469
|
-
const latencyMs = Date.now() - startedAt;
|
|
470
|
-
const requestId = response.headers.get("x-typesafe-request-id") ?? undefined;
|
|
471
|
-
const upstreamMs = Number(response.headers.get("x-envoy-upstream-service-time")) ||
|
|
472
|
-
undefined;
|
|
473
|
-
const payload = await response.json().catch(() => null);
|
|
474
|
-
if (!response.ok) {
|
|
475
|
-
lastError = parseError(response.status, payload, requestId);
|
|
476
|
-
if (lastError.kind === "authentication") {
|
|
477
|
-
this.authFailed = true;
|
|
478
|
-
// The breaker is right to trip — none of these clear themselves
|
|
479
|
-
// inside one process — but it must not assert WHICH credential
|
|
480
|
-
// problem it was. A gateway 403 with a valid key and no card on
|
|
481
|
-
// file is not a rejected key, and saying so sends the reader off
|
|
482
|
-
// to regenerate the one thing that works.
|
|
483
|
-
logger.warn(`TypeSafe: credential not accepted — disabling this provider instance. ${lastError.message}`, {
|
|
484
|
-
status: lastError.status,
|
|
485
|
-
transport: this.transport,
|
|
486
|
-
requestId,
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
if (!lastError.retryable || attempt === this.maxRetries) {
|
|
490
|
-
throw this.decisionError(lastError);
|
|
491
|
-
}
|
|
492
|
-
await sleep(2 ** attempt * 250 + Math.random() * 250);
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
if (!isRecord(payload) || !isRecord(payload.answers)) {
|
|
496
|
-
throw this.decisionError({
|
|
497
|
-
kind: "server",
|
|
498
|
-
message: "TypeSafe returned a response without an answers map.",
|
|
499
|
-
status: response.status,
|
|
500
|
-
requestId,
|
|
501
|
-
retryable: false,
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
// The gateway does not omit confidence after all — it RELOCATES it,
|
|
505
|
-
// to `providerMetadata.typesafe.confidence.<questionId>`, and leaves
|
|
506
|
-
// the answer objects themselves without one. Preferring it over the
|
|
507
|
-
// derived peak matters because the two disagree on any distribution
|
|
508
|
-
// that is not already near-certain, and the router's asymmetric bars
|
|
509
|
-
// are tuned against the vendor's calibrated figure, not against
|
|
510
|
-
// max(probabilities).
|
|
511
|
-
const reported = readReportedConfidence(payload);
|
|
512
|
-
const answers = {};
|
|
513
|
-
for (const [id, raw] of Object.entries(payload.answers)) {
|
|
514
|
-
const parsed = parseAnswer(raw, reported[id]);
|
|
515
|
-
if (parsed) {
|
|
516
|
-
answers[id] = parsed;
|
|
517
|
-
}
|
|
518
|
-
else {
|
|
519
|
-
logger.warn(`TypeSafe: dropped unparseable answer "${id}"`, {
|
|
520
|
-
requestId,
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
const usage = isRecord(payload.usage) ? payload.usage : {};
|
|
525
|
-
return {
|
|
526
|
-
// `resolvedModel`, not `this.modelName`: when the caller pinned a
|
|
527
|
-
// model for this one request and the response omits its own, the
|
|
528
|
-
// instance default would be reported instead of the model actually
|
|
529
|
-
// asked for.
|
|
530
|
-
model: typeof payload.model === "string" ? payload.model : resolvedModel,
|
|
531
|
-
provider: this.providerName,
|
|
532
|
-
answers,
|
|
533
|
-
// Two spellings for one field. The direct API sends `input_tokens`;
|
|
534
|
-
// the gateway sends `inputTokens`. Reading only the first meant every
|
|
535
|
-
// gateway decision reported zero tokens — and since decisions are
|
|
536
|
-
// priced on input alone, every gateway decision was also costed at
|
|
537
|
-
// exactly $0, silently, in both the span attributes and the cost
|
|
538
|
-
// aggregate. Nothing failed; the number was simply always zero.
|
|
539
|
-
usage: {
|
|
540
|
-
inputTokens: asNumber(usage.input_tokens) ?? asNumber(usage.inputTokens) ?? 0,
|
|
541
|
-
outputTokens: asNumber(usage.output_tokens) ??
|
|
542
|
-
asNumber(usage.outputTokens) ??
|
|
543
|
-
0,
|
|
544
|
-
},
|
|
545
|
-
requestId,
|
|
546
|
-
latencyMs,
|
|
547
|
-
upstreamMs,
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
catch (error) {
|
|
551
|
-
if (error instanceof ProviderError) {
|
|
552
|
-
throw error;
|
|
553
|
-
}
|
|
554
|
-
// An abort from the caller's own signal must not be retried — the
|
|
555
|
-
// turn is already being torn down.
|
|
556
|
-
const aborted = request.signal?.aborted === true;
|
|
557
|
-
const isTimeout = error instanceof Error && error.name === "TimeoutError";
|
|
558
|
-
lastError = {
|
|
559
|
-
kind: aborted ? "network" : isTimeout ? "timeout" : "network",
|
|
560
|
-
message: error instanceof Error ? error.message : String(error),
|
|
561
|
-
retryable: !aborted,
|
|
562
|
-
};
|
|
563
|
-
if (aborted || attempt === this.maxRetries) {
|
|
564
|
-
throw this.decisionError(lastError);
|
|
565
|
-
}
|
|
566
|
-
await sleep(2 ** attempt * 250 + Math.random() * 250);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
throw this.decisionError(lastError ?? {
|
|
570
|
-
kind: "network",
|
|
571
|
-
message: "TypeSafe request failed.",
|
|
572
|
-
retryable: false,
|
|
573
|
-
});
|
|
259
|
+
reportedConfidence(payload) {
|
|
260
|
+
return readReportedConfidence(payload);
|
|
261
|
+
}
|
|
262
|
+
readRequestId(headers) {
|
|
263
|
+
return headers.get("x-typesafe-request-id") ?? undefined;
|
|
264
|
+
}
|
|
265
|
+
diagnosticContext() {
|
|
266
|
+
return { transport: this.transport };
|
|
574
267
|
}
|
|
575
268
|
/** Where a decision request goes for this instance's transport. */
|
|
576
|
-
|
|
269
|
+
decisionEndpoint() {
|
|
577
270
|
return this.transport === "gateway"
|
|
578
|
-
?
|
|
271
|
+
? this.gatewayURL
|
|
579
272
|
: `${this.baseURL}/v1/systemone`;
|
|
580
273
|
}
|
|
581
274
|
/**
|
|
@@ -587,7 +280,7 @@ export class TypeSafeProvider extends BaseProvider {
|
|
|
587
280
|
* carries no `model`), and `ai-evaluation-model-specification-version`
|
|
588
281
|
* selects the request/response shape this provider parses.
|
|
589
282
|
*/
|
|
590
|
-
|
|
283
|
+
decisionHeaders() {
|
|
591
284
|
const base = {
|
|
592
285
|
Authorization: `Bearer ${this.apiKey}`,
|
|
593
286
|
"Content-Type": "application/json",
|
|
@@ -623,11 +316,11 @@ export class TypeSafeProvider extends BaseProvider {
|
|
|
623
316
|
}
|
|
624
317
|
const response = await this.proxyFetch(`${this.baseURL}/v1/models`, {
|
|
625
318
|
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
626
|
-
signal: AbortSignal.timeout(this.defaultTimeout ??
|
|
319
|
+
signal: AbortSignal.timeout(this.defaultTimeout ?? DEFAULT_DECISION_TIMEOUT_MS),
|
|
627
320
|
});
|
|
628
321
|
const payload = await response.json().catch(() => null);
|
|
629
322
|
if (!response.ok) {
|
|
630
|
-
throw this.decisionError(
|
|
323
|
+
throw this.decisionError(parseTypeSafeError(response.status, payload, response.headers.get("x-typesafe-request-id") ?? undefined));
|
|
631
324
|
}
|
|
632
325
|
const models = isRecord(payload) && Array.isArray(payload.models) ? payload.models : [];
|
|
633
326
|
return models.filter(isRecord).map((m) => ({
|
|
@@ -636,19 +329,4 @@ export class TypeSafeProvider extends BaseProvider {
|
|
|
636
329
|
releaseDate: typeof m.release_date === "string" ? m.release_date : "",
|
|
637
330
|
}));
|
|
638
331
|
}
|
|
639
|
-
/**
|
|
640
|
-
* Wrap a normalised decision failure in the provider error type, keeping
|
|
641
|
-
* the structured detail reachable via `cause` for callers that branch on
|
|
642
|
-
* `kind` (authentication vs rate_limit vs max_tokens_exceeded).
|
|
643
|
-
*/
|
|
644
|
-
decisionError(detail) {
|
|
645
|
-
const error = new ProviderError(detail.message, "typesafe");
|
|
646
|
-
Object.defineProperty(error, "cause", {
|
|
647
|
-
value: detail,
|
|
648
|
-
enumerable: false,
|
|
649
|
-
writable: true,
|
|
650
|
-
configurable: true,
|
|
651
|
-
});
|
|
652
|
-
return error;
|
|
653
|
-
}
|
|
654
332
|
}
|
|
@@ -52,8 +52,8 @@ export declare class ClassifierRouter {
|
|
|
52
52
|
route(input: ClassifierRouterInput): Promise<ClassifierRouterDecision | null>;
|
|
53
53
|
/**
|
|
54
54
|
* Resolve "auto" (the default) to a concrete strategy. Jev is preferred the
|
|
55
|
-
* moment a
|
|
56
|
-
* what it was before Jev existed.
|
|
55
|
+
* moment a decision provider is configured; without one the behaviour is
|
|
56
|
+
* exactly what it was before Jev existed.
|
|
57
57
|
*/
|
|
58
58
|
private resolveStrategy;
|
|
59
59
|
/** Run the chosen strategy; every one falls back to heuristic on failure. */
|
|
@@ -135,20 +135,22 @@ export class ClassifierRouter {
|
|
|
135
135
|
}
|
|
136
136
|
/**
|
|
137
137
|
* Resolve "auto" (the default) to a concrete strategy. Jev is preferred the
|
|
138
|
-
* moment a
|
|
139
|
-
* what it was before Jev existed.
|
|
138
|
+
* moment a decision provider is configured; without one the behaviour is
|
|
139
|
+
* exactly what it was before Jev existed.
|
|
140
140
|
*/
|
|
141
141
|
resolveStrategy() {
|
|
142
142
|
const configured = this.config.classifier ?? "auto";
|
|
143
143
|
if (configured !== "auto") {
|
|
144
144
|
return configured;
|
|
145
145
|
}
|
|
146
|
-
// A decision provider that is both registered AND
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
146
|
+
// A decision provider that is both registered AND configured is the whole
|
|
147
|
+
// activation condition — this is where "if somebody sets it we start
|
|
148
|
+
// using it" lives for routing. The caller's check also counts the
|
|
149
|
+
// credentials it was constructed with.
|
|
150
|
+
const hasProvider = this.deps.hasDecisionProvider
|
|
151
|
+
? this.deps.hasDecisionProvider()
|
|
152
|
+
: resolveDefaultDecisionProvider() !== undefined;
|
|
153
|
+
return this.deps.decide && hasProvider ? "jev" : "heuristic";
|
|
152
154
|
}
|
|
153
155
|
/** Run the chosen strategy; every one falls back to heuristic on failure. */
|
|
154
156
|
async classify(strategy, input, candidates) {
|
|
@@ -24,7 +24,9 @@ export type ClassifierDifficulty = "trivial" | "simple" | "moderate" | "hard" |
|
|
|
24
24
|
* - `llm` — a cheap classifier model via the injected `generate`.
|
|
25
25
|
* - `jev` — TypeSafe's System One model; one ~400ms round trip that returns a
|
|
26
26
|
* *calibrated* confidence rather than a self-reported one.
|
|
27
|
-
* - `auto` — `jev` when
|
|
27
|
+
* - `auto` — `jev` when a decision provider is configured, in the environment
|
|
28
|
+
* or in SDK credentials (`TYPESAFE_API_KEY`, or `LAYA_API_KEY` with
|
|
29
|
+
* `LAYA_BASE_URL`), otherwise `heuristic`.
|
|
28
30
|
*/
|
|
29
31
|
export type ClassifierStrategyKind = "heuristic" | "llm" | "jev" | "auto";
|
|
30
32
|
/**
|
|
@@ -182,8 +184,10 @@ export type ClassifierRouterConfig = {
|
|
|
182
184
|
/** Master switch. When false/absent, the router is never built. */
|
|
183
185
|
enabled: boolean;
|
|
184
186
|
/**
|
|
185
|
-
* Classification strategy. Default: "auto" — which resolves to "jev" when
|
|
186
|
-
*
|
|
187
|
+
* Classification strategy. Default: "auto" — which resolves to "jev" when a
|
|
188
|
+
* decision provider is configured, in the environment or in SDK credentials
|
|
189
|
+
* (`TYPESAFE_API_KEY`, or `LAYA_API_KEY` with `LAYA_BASE_URL`) and
|
|
190
|
+
* "heuristic" otherwise, so configuring one
|
|
187
191
|
* upgrades routing without any code change. Behaviour for callers with no
|
|
188
192
|
* key is unchanged.
|
|
189
193
|
*/
|
|
@@ -350,5 +354,11 @@ export type ClassifierRouterDeps = {
|
|
|
350
354
|
generate?: ClassifierGenerateFn;
|
|
351
355
|
/** Decision caller for the "jev" strategy. Omit to disable it. */
|
|
352
356
|
decide?: ClassifierDecideFn;
|
|
357
|
+
/**
|
|
358
|
+
* Whether a decision provider is configured for this caller, counting the
|
|
359
|
+
* credentials it was given as well as the environment. Omit to check the
|
|
360
|
+
* environment alone.
|
|
361
|
+
*/
|
|
362
|
+
hasDecisionProvider?: () => boolean;
|
|
353
363
|
logger?: ClassifierLogger;
|
|
354
364
|
};
|