@juspay/neurolink 12.25.5 → 12.26.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/browser/neurolink.min.js +382 -382
  3. package/dist/cli/commands/decide.d.ts +2 -1
  4. package/dist/cli/commands/decide.js +4 -2
  5. package/dist/cli/commands/setup.js +2 -1
  6. package/dist/cli/factories/commandFactory.js +1 -1
  7. package/dist/cli/utils/classifierRouterFlags.js +1 -1
  8. package/dist/constants/enums.d.ts +15 -0
  9. package/dist/constants/enums.js +16 -0
  10. package/dist/core/baseProvider.js +2 -2
  11. package/dist/factories/providerDescriptors.d.ts +16 -7
  12. package/dist/factories/providerDescriptors.js +97 -15
  13. package/dist/factories/providerRegistry.js +9 -1
  14. package/dist/mcp/externalServerManager.js +16 -2
  15. package/dist/mcp/mcpCircuitBreaker.d.ts +24 -3
  16. package/dist/mcp/mcpCircuitBreaker.js +57 -18
  17. package/dist/mcp/toolDiscoveryService.js +20 -4
  18. package/dist/models/manifestRegistry.js +2 -0
  19. package/dist/models/manifests/laya.d.ts +12 -0
  20. package/dist/models/manifests/laya.js +54 -0
  21. package/dist/neurolink.js +8 -3
  22. package/dist/providers/laya.d.ts +40 -0
  23. package/dist/providers/laya.js +170 -0
  24. package/dist/providers/systemOneDecision.d.ts +102 -0
  25. package/dist/providers/systemOneDecision.js +443 -0
  26. package/dist/providers/typesafe.d.ts +32 -47
  27. package/dist/providers/typesafe.js +64 -386
  28. package/dist/routing/classifierRouter.d.ts +2 -2
  29. package/dist/routing/classifierRouter.js +10 -8
  30. package/dist/types/classifierRouter.d.ts +13 -3
  31. package/dist/types/cli.d.ts +3 -2
  32. package/dist/types/decision.d.ts +29 -0
  33. package/dist/types/externalMcp.d.ts +7 -0
  34. package/dist/types/providers.d.ts +20 -1
  35. package/dist/utils/modelChoices.js +21 -1
  36. package/dist/utils/providerConfig.d.ts +5 -0
  37. package/dist/utils/providerConfig.js +17 -0
  38. package/docs-site/static/search-index.json +24 -10
  39. package/package.json +4 -2
@@ -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 GATEWAY_URL = "https://ai-gateway.vercel.sh/v4/ai/evaluation-model";
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 parseError(status, body, requestId) {
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 — ${fields.join("; ") || "unspecified field"}`,
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. It emits no text, so `generate()` / `stream()` are not
302
- * available and `getAISDKModel()` throws — the same shape Voyage and Jina
303
- * already use for embedding-only providers.
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, so those throws should be unreachable
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 whether
318
- * `confidence` is reported at all) and not at all in `DecisionResult`; the
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 BaseProvider {
176
+ export class TypeSafeProvider extends SystemOneDecisionProvider {
326
177
  apiKey;
327
178
  baseURL;
328
- proxyFetch;
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
- const validatedNeurolink = isNeuroLink(sdk) ? sdk : undefined;
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.proxyFetch = createProxyFetch();
363
- this.maxRetries = DEFAULT_MAX_RETRIES;
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" ? GATEWAY_URL : this.baseURL,
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
- getDefaultDecisionModel() {
378
- return this.getDefaultModel();
216
+ vendorLabel() {
217
+ return "TypeSafe";
218
+ }
219
+ vendorDisplayName() {
220
+ return "TypeSafe (Jev)";
379
221
  }
380
- supportsTools() {
381
- return false;
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
- * Jev produces no text, so there is no chat model behind this provider.
385
- * Unreachable while the descriptor declares `inferenceKinds: ["decide"]`;
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
- getAISDKModel() {
389
- throw new ProviderError("TypeSafe (Jev) 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()`.", "typesafe");
390
- }
391
- async executeStream(_options, _analysisSchema) {
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
- * Must RETURN the error, never throw it (CLAUDE.md rule 6).
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
- formatProviderError(error) {
398
- if (error instanceof ProviderError) {
399
- return error;
400
- }
401
- const message = error instanceof Error ? error.message : String(error);
402
- return new ProviderError(`TypeSafe request failed: ${message}`, "typesafe");
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
- * Evaluate a state against a batch of typed questions.
407
- *
408
- * Throws a {@link ProviderError} carrying a normalised {@link DecisionError}
409
- * in `cause`. Internal hot paths should prefer `NeuroLink.tryDecide()`,
410
- * which returns null instead.
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
- async decide(request) {
413
- if (!this.apiKey) {
414
- throw this.decisionError({
415
- kind: "authentication",
416
- message: this.transport === "gateway"
417
- ? "The TypeSafe gateway transport requires a Vercel AI Gateway key. Set AI_GATEWAY_API_KEY or pass credentials.typesafe.gatewayApiKey."
418
- : "TypeSafe requires an API key. Set TYPESAFE_API_KEY or pass credentials.typesafe.apiKey.",
419
- retryable: false,
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
- endpoint() {
269
+ decisionEndpoint() {
577
270
  return this.transport === "gateway"
578
- ? GATEWAY_URL
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
- headers() {
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 ?? DEFAULT_TIMEOUT_MS),
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(parseError(response.status, payload, response.headers.get("x-typesafe-request-id") ?? undefined));
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 TypeSafe key is present; without one the behaviour is exactly
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 TypeSafe key is present; without one the behaviour is exactly
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 has its key set is the
147
- // whole activation condition — this is where "if somebody sets it we
148
- // start using it" lives for routing.
149
- return this.deps.decide && resolveDefaultDecisionProvider()
150
- ? "jev"
151
- : "heuristic";
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 `TYPESAFE_API_KEY` is set, otherwise `heuristic`.
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
- * `TYPESAFE_API_KEY` is set and "heuristic" otherwise, so configuring a key
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
  };