@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/browser/neurolink.min.js +372 -372
  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/models/manifestRegistry.js +2 -0
  15. package/dist/models/manifests/laya.d.ts +12 -0
  16. package/dist/models/manifests/laya.js +54 -0
  17. package/dist/neurolink.js +8 -3
  18. package/dist/providers/anthropic/client.js +17 -1
  19. package/dist/providers/laya.d.ts +40 -0
  20. package/dist/providers/laya.js +170 -0
  21. package/dist/providers/systemOneDecision.d.ts +102 -0
  22. package/dist/providers/systemOneDecision.js +443 -0
  23. package/dist/providers/typesafe.d.ts +32 -47
  24. package/dist/providers/typesafe.js +64 -386
  25. package/dist/routing/classifierRouter.d.ts +2 -2
  26. package/dist/routing/classifierRouter.js +10 -8
  27. package/dist/types/classifierRouter.d.ts +13 -3
  28. package/dist/types/cli.d.ts +3 -2
  29. package/dist/types/decision.d.ts +29 -0
  30. package/dist/types/providers.d.ts +20 -1
  31. package/dist/utils/modelChoices.js +21 -1
  32. package/dist/utils/providerConfig.d.ts +5 -0
  33. package/dist/utils/providerConfig.js +17 -0
  34. package/docs-site/static/search-index.json +24 -10
  35. package/package.json +3 -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 { AIProviderName } from "../constants/enums.js";
2
- import { BaseProvider } from "../core/baseProvider.js";
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. It emits no text, so `generate()` / `stream()` are not
23
- * available and `getAISDKModel()` throws — the same shape Voyage and Jina
24
- * already use for embedding-only providers.
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, so those throws should be unreachable
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 whether
39
- * `confidence` is reported at all) and not at all in `DecisionResult`; the
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 BaseProvider {
41
+ export declare class TypeSafeProvider extends SystemOneDecisionProvider {
47
42
  private readonly apiKey;
48
43
  private readonly baseURL;
49
- private readonly proxyFetch;
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 getDefaultDecisionModel(): string | undefined;
64
- supportsTools(): boolean;
49
+ protected vendorLabel(): string;
50
+ protected vendorDisplayName(): string;
51
+ protected decisionApiKey(): string;
52
+ protected missingKeyMessage(): string;
65
53
  /**
66
- * Jev produces no text, so there is no chat model behind this provider.
67
- * Unreachable while the descriptor declares `inferenceKinds: ["decide"]`;
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 getAISDKModel(): LanguageModel;
71
- protected executeStream(_options: StreamOptions, _analysisSchema?: ValidationSchema): Promise<StreamResult>;
57
+ protected encodeQuestion(question: DecisionQuestion): Record<string, unknown>;
72
58
  /**
73
- * Must RETURN the error, never throw it (CLAUDE.md rule 6).
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 formatProviderError(error: unknown): Error;
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
- * Evaluate a state against a batch of typed questions.
78
- *
79
- * Throws a {@link ProviderError} carrying a normalised {@link DecisionError}
80
- * in `cause`. Internal hot paths should prefer `NeuroLink.tryDecide()`,
81
- * which returns null instead.
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
- decide(request: DecisionRequest): Promise<DecisionResult>;
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
- private endpoint;
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
- private headers;
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
  }