@juspay/neurolink 12.37.5 → 12.38.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 (37) hide show
  1. package/CHANGELOG.md +2 -3
  2. package/README.md +59 -49
  3. package/dist/browser/neurolink.min.js +411 -411
  4. package/dist/cli/commands/decide.d.ts +2 -2
  5. package/dist/cli/commands/decide.js +22 -3
  6. package/dist/cli/commands/setup.js +2 -1
  7. package/dist/cli/factories/commandFactory.js +1 -1
  8. package/dist/constants/enums.d.ts +9 -0
  9. package/dist/constants/enums.js +10 -0
  10. package/dist/core/modelConfiguration.js +1 -0
  11. package/dist/factories/providerDescriptors.d.ts +2 -0
  12. package/dist/factories/providerDescriptors.js +52 -6
  13. package/dist/factories/providerRegistry.js +9 -1
  14. package/dist/models/manifestRegistry.js +2 -0
  15. package/dist/models/manifests/xor.d.ts +12 -0
  16. package/dist/models/manifests/xor.js +30 -0
  17. package/dist/neurolink.js +6 -0
  18. package/dist/providers/anthropic/client.js +8 -1
  19. package/dist/providers/laya.js +1 -19
  20. package/dist/providers/openRouter/client.js +1 -0
  21. package/dist/providers/openaiChatCompletionsBase.js +37 -1
  22. package/dist/providers/systemOneDecision.d.ts +21 -2
  23. package/dist/providers/systemOneDecision.js +64 -4
  24. package/dist/providers/xor.d.ts +48 -0
  25. package/dist/providers/xor.js +192 -0
  26. package/dist/types/classifierRouter.d.ts +5 -4
  27. package/dist/types/cli.d.ts +6 -2
  28. package/dist/types/decision.d.ts +60 -1
  29. package/dist/types/openaiCompatible.d.ts +10 -0
  30. package/dist/types/providers.d.ts +10 -0
  31. package/dist/utils/decisionMedia.d.ts +9 -0
  32. package/dist/utils/decisionMedia.js +160 -0
  33. package/dist/utils/modelChoices.js +9 -1
  34. package/dist/utils/providerConfig.d.ts +5 -0
  35. package/dist/utils/providerConfig.js +17 -0
  36. package/docs-site/static/search-index.json +61 -34
  37. package/package.json +3 -1
@@ -1,9 +1,11 @@
1
1
  import { BaseProvider } from "../core/baseProvider.js";
2
- import { PROVIDER_DESCRIPTORS_BY_NAME } from "../factories/providerDescriptors.js";
2
+ import { listMediaDecisionProviders, PROVIDER_DESCRIPTORS_BY_NAME, } from "../factories/providerDescriptors.js";
3
3
  import { isNeuroLink } from "../neurolink.js";
4
4
  import { createProxyFetch } from "../proxy/proxyFetch.js";
5
5
  import { ProviderError } from "../types/index.js";
6
+ import { prepareDecisionMedia } from "../utils/decisionMedia.js";
6
7
  import { logger } from "../utils/logger.js";
8
+ import { redactUrlsInText } from "../utils/logSanitize.js";
7
9
  import { estimateTokens, serializeForEstimate, } from "../utils/tokenEstimation.js";
8
10
  /**
9
11
  * Generous enough for a cold start (measured at 2.0–2.7s after idle on Jev)
@@ -128,6 +130,25 @@ export function describeValidationErrors(detail) {
128
130
  .slice(0, 5);
129
131
  return fields.join("; ") || "unspecified field";
130
132
  }
133
+ /**
134
+ * A LiteLLM proxy echoes credentials back in its error texts, in more than one
135
+ * wording: a masked key plus its hash on a rejected key, the whole key when it
136
+ * does not look like a LiteLLM key, a key hash on a rate limit. None of it
137
+ * belongs in an error message or a log, whatever the wording, so the
138
+ * configured key, anything shaped like a LiteLLM key, any long hex run and any
139
+ * embedded `data:` URL are all removed.
140
+ */
141
+ export function redactCredentials(message, apiKey) {
142
+ const withoutKey = apiKey
143
+ ? message.split(apiKey).join("[redacted]")
144
+ : message;
145
+ return withoutKey
146
+ .replace(/\.?\s*Received API Key\s*=[\s\S]*$/, "")
147
+ .replace(/\bsk-[A-Za-z0-9._-]+/g, "[redacted]")
148
+ .replace(/\b[0-9a-f]{32,}\b/gi, "[redacted]")
149
+ .replace(/data:[a-z]+\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+/gi, "[media]")
150
+ .trim();
151
+ }
131
152
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
132
153
  /**
133
154
  * `estimateTokens` assumes ~4 characters per token. That holds for English
@@ -281,7 +302,9 @@ export class SystemOneDecisionProvider extends BaseProvider {
281
302
  id,
282
303
  this.encodeQuestion(question),
283
304
  ]));
284
- const body = JSON.stringify(this.buildDecisionBody(request.state, wireQuestions, resolvedModel));
305
+ const media = await this.prepareMedia(request);
306
+ const body = JSON.stringify(this.buildDecisionBody(request.state, wireQuestions, resolvedModel, media));
307
+ this.assertWithinRequestBytes(body);
285
308
  const timeoutMs = request.timeoutMs ??
286
309
  this.getDescriptorDecideMs() ??
287
310
  this.defaultTimeout ??
@@ -369,6 +392,7 @@ export class SystemOneDecisionProvider extends BaseProvider {
369
392
  requestId,
370
393
  latencyMs,
371
394
  upstreamMs,
395
+ ...(media ? { mediaBytes: media.bytes } : {}),
372
396
  };
373
397
  }
374
398
  catch (error) {
@@ -381,7 +405,7 @@ export class SystemOneDecisionProvider extends BaseProvider {
381
405
  const isTimeout = error instanceof Error && error.name === "TimeoutError";
382
406
  lastError = {
383
407
  kind: aborted ? "network" : isTimeout ? "timeout" : "network",
384
- message: error instanceof Error ? error.message : String(error),
408
+ message: redactCredentials(redactUrlsInText(error instanceof Error ? error.message : String(error)), this.decisionApiKey()),
385
409
  retryable: !aborted,
386
410
  };
387
411
  if (aborted || attempt === this.maxRetries) {
@@ -396,6 +420,41 @@ export class SystemOneDecisionProvider extends BaseProvider {
396
420
  retryable: false,
397
421
  });
398
422
  }
423
+ /**
424
+ * Turn the request's images and video into data URLs, or refuse it. Refused
425
+ * as a non-retryable `invalid_request`: resending the same media cannot help.
426
+ */
427
+ async prepareMedia(request) {
428
+ const limits = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName)?.decisionLimits;
429
+ const prepared = await prepareDecisionMedia(request, limits?.media, this.vendorLabel(), listMediaDecisionProviders());
430
+ if (prepared.status === "refused") {
431
+ throw this.decisionError({
432
+ kind: "invalid_request",
433
+ message: prepared.message,
434
+ retryable: false,
435
+ });
436
+ }
437
+ return prepared.media;
438
+ }
439
+ /**
440
+ * The encoded body, not just the media, is what the server's size limit
441
+ * applies to.
442
+ */
443
+ assertWithinRequestBytes(body) {
444
+ const limit = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName)
445
+ ?.decisionLimits?.media?.maxRequestBytes;
446
+ if (limit === undefined) {
447
+ return;
448
+ }
449
+ const bytes = Buffer.byteLength(body);
450
+ if (bytes > limit) {
451
+ throw this.decisionError({
452
+ kind: "invalid_request",
453
+ message: `The request is ${bytes} bytes; ${this.vendorLabel()} accepts at most ${limit}. Send fewer or smaller images, or a shorter video.`,
454
+ retryable: false,
455
+ });
456
+ }
457
+ }
399
458
  /**
400
459
  * Refuse a request the model cannot read in full. An encoder cuts the state
401
460
  * off past its window without saying so, so without this a decision would
@@ -407,7 +466,8 @@ export class SystemOneDecisionProvider extends BaseProvider {
407
466
  return;
408
467
  }
409
468
  const label = this.vendorLabel();
410
- if (questionCount > limits.maxQuestions) {
469
+ if (limits.maxQuestions !== undefined &&
470
+ questionCount > limits.maxQuestions) {
411
471
  throw this.decisionError({
412
472
  kind: "max_tokens_exceeded",
413
473
  message: `${label} accepts at most ${limits.maxQuestions} questions per request; this one has ${questionCount}. Ask fewer, or use a decision provider without this cap.`,
@@ -0,0 +1,48 @@
1
+ import type { DecisionError, DecisionPreparedMedia, DecisionState, NeurolinkCredentials } from "../types/index.js";
2
+ import { SystemOneDecisionProvider } from "./systemOneDecision.js";
3
+ /**
4
+ * XOR Provider — the `decide` inference type only.
5
+ *
6
+ * XOR is Juspay's open-weights "System One" decision model, post-trained from
7
+ * Qwen3.6-35B-A3B: the same typed `noul` / `choice` / `score` questions as
8
+ * TypeSafe's Jev, on the same wire, in one batched pass. Unlike Jev and Laya it
9
+ * also reads images and one video. The request loop, retries, auth circuit
10
+ * breaker and answer parsing are shared with every decision provider in
11
+ * {@link SystemOneDecisionProvider}.
12
+ *
13
+ * @see https://huggingface.co/juspay/xor
14
+ */
15
+ export declare class XorProvider extends SystemOneDecisionProvider {
16
+ private readonly apiKey;
17
+ private readonly baseURL;
18
+ constructor(modelName?: string, sdk?: unknown, _region?: string, credentials?: NeurolinkCredentials["xor"]);
19
+ protected getDefaultModel(): string;
20
+ protected vendorLabel(): string;
21
+ protected vendorDisplayName(): string;
22
+ protected decisionApiKey(): string;
23
+ protected missingKeyMessage(): string;
24
+ protected missingConfigMessage(): string | undefined;
25
+ protected decisionEndpoint(): string;
26
+ protected decisionHeaders(): Record<string, string>;
27
+ /**
28
+ * `model` is always sent. A LiteLLM proxy checks team access against the
29
+ * body's `model`, and refuses a request that names none. `images` is
30
+ * omitted rather than sent empty, which the server rejects.
31
+ */
32
+ protected buildDecisionBody(state: DecisionState, questions: Record<string, Record<string, unknown>>, model: string, media?: DecisionPreparedMedia): Record<string, unknown>;
33
+ /**
34
+ * A 403 is deliberately NOT `authentication`: on a LiteLLM proxy it means the
35
+ * team's allow-list lacks the model, which an admin can fix at any time, and
36
+ * `authentication` would trip the per-instance breaker and keep XOR disabled
37
+ * until restart even after access is granted. A 402 (a budget) is the same
38
+ * kind of fixable gap. `DecisionErrorKind` has no billing kind, so both map
39
+ * to `invalid_request`.
40
+ */
41
+ protected parseDecisionError(status: number, payload: unknown, requestId: string | undefined): DecisionError;
42
+ /**
43
+ * XOR's server sets `x-request-id` and `x-typesafe-request-id` to one value.
44
+ * A proxy in front may replace or drop them, so its own call id is the last
45
+ * resort.
46
+ */
47
+ protected readRequestId(headers: Headers): string | undefined;
48
+ }
@@ -0,0 +1,192 @@
1
+ import { XorModels } from "../constants/enums.js";
2
+ import { logger } from "../utils/logger.js";
3
+ import { redactUrlForError } from "../utils/logSanitize.js";
4
+ import { getProviderModel } from "../utils/providerConfig.js";
5
+ import { describeValidationErrors, isRecord, redactCredentials, SystemOneDecisionProvider, } from "./systemOneDecision.js";
6
+ const XOR_ROUTE = "/v1/systemone";
7
+ /**
8
+ * XOR_BASE_URL is the origin of a deployment, but a `LITELLM_BASE_URL`-style
9
+ * value ends in `/v1`, which the route already carries. Both spellings reduce
10
+ * to the same base.
11
+ */
12
+ function normalizeBaseURL(raw) {
13
+ return raw.replace(/\/+$/, "").replace(/\/v1$/i, "");
14
+ }
15
+ /** What a 500 says when the prefill was too long for the model to read. */
16
+ const CONTEXT_LENGTH_MESSAGE = /(context|prefill|prompt).{0,40}(length|limit|too long|exceed)|(too long|exceeds?).{0,40}(context|tokens)/i;
17
+ /**
18
+ * A base URL that cannot work is refused up front, and its text is never
19
+ * repeated: `fetch` rejects a URL with userinfo and echoes it in the error, the
20
+ * route is appended after a query string or fragment, and any of them can hold a
21
+ * credential.
22
+ */
23
+ function baseURLProblem(baseURL) {
24
+ const fix = "Set XOR_BASE_URL or pass credentials.xor.baseURL to the origin, and give the key through XOR_API_KEY or credentials.xor.apiKey.";
25
+ try {
26
+ const url = new URL(baseURL);
27
+ // `new URL()` accepts any scheme, so a host:port with no scheme parses as
28
+ // one (`xor.internal:8080` has the scheme `xor.internal:`) and would reach
29
+ // `fetch` as a network error instead of this message.
30
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
31
+ return `The XOR base URL must start with https:// or http://. ${fix}`;
32
+ }
33
+ return url.username || url.password || url.search || url.hash
34
+ ? `The XOR base URL must not carry credentials, a query string or a fragment. ${fix}`
35
+ : undefined;
36
+ }
37
+ catch {
38
+ return `The XOR base URL is not a valid absolute URL. ${fix}`;
39
+ }
40
+ }
41
+ function xorErrorKind(status, message) {
42
+ if (status === 401) {
43
+ return "authentication";
44
+ }
45
+ if (status === 413) {
46
+ return "max_tokens_exceeded";
47
+ }
48
+ if (status === 429) {
49
+ return "rate_limit";
50
+ }
51
+ if (status === 503) {
52
+ return "overloaded";
53
+ }
54
+ if (status >= 500) {
55
+ return CONTEXT_LENGTH_MESSAGE.test(message)
56
+ ? "max_tokens_exceeded"
57
+ : "server";
58
+ }
59
+ return "invalid_request";
60
+ }
61
+ /**
62
+ * Two envelopes reach the client. A LiteLLM proxy answers its own rejections
63
+ * (a bad key, a team without the model, a budget) as
64
+ * `{"error":{"message","type","code"}}`; XOR's own server answers as
65
+ * `{"error":"<string>"}` — a shape that neither TypeSafe's nor Laya's parser
66
+ * recognises, since both require `body.error` to be an object. FastAPI's
67
+ * `{"detail":…}` is read too, in case something in front speaks it.
68
+ */
69
+ function readXorErrorMessage(body) {
70
+ if (!isRecord(body)) {
71
+ return undefined;
72
+ }
73
+ const { error, detail } = body;
74
+ if (isRecord(error) &&
75
+ typeof error.message === "string" &&
76
+ error.message.length > 0) {
77
+ return error.message;
78
+ }
79
+ if (typeof error === "string" && error.length > 0) {
80
+ return error;
81
+ }
82
+ if (Array.isArray(detail)) {
83
+ return `Request failed validation — ${describeValidationErrors(detail)}`;
84
+ }
85
+ return typeof detail === "string" && detail.length > 0 ? detail : undefined;
86
+ }
87
+ /**
88
+ * XOR Provider — the `decide` inference type only.
89
+ *
90
+ * XOR is Juspay's open-weights "System One" decision model, post-trained from
91
+ * Qwen3.6-35B-A3B: the same typed `noul` / `choice` / `score` questions as
92
+ * TypeSafe's Jev, on the same wire, in one batched pass. Unlike Jev and Laya it
93
+ * also reads images and one video. The request loop, retries, auth circuit
94
+ * breaker and answer parsing are shared with every decision provider in
95
+ * {@link SystemOneDecisionProvider}.
96
+ *
97
+ * @see https://huggingface.co/juspay/xor
98
+ */
99
+ export class XorProvider extends SystemOneDecisionProvider {
100
+ apiKey;
101
+ baseURL;
102
+ constructor(modelName, sdk, _region, credentials) {
103
+ super(modelName, "xor", sdk);
104
+ this.apiKey =
105
+ credentials?.apiKey?.trim() || (process.env.XOR_API_KEY?.trim() ?? "");
106
+ // No built-in endpoint: XOR is reached at a deployment or through a proxy
107
+ // route to one, so the base URL comes only from config. `||`, not `??`, so
108
+ // a blank one counts as unset.
109
+ this.baseURL = normalizeBaseURL(credentials?.baseURL?.trim() || process.env.XOR_BASE_URL?.trim() || "");
110
+ // A base URL with `user:pass@` or a `?token=` is refused when a request is
111
+ // made, but it is redacted here as well so a misconfigured value cannot leak
112
+ // through this line either.
113
+ logger.debug("XOR Provider initialized (decide only)", {
114
+ modelName: this.modelName,
115
+ baseURL: this.baseURL ? redactUrlForError(this.baseURL) : "(not set)",
116
+ });
117
+ }
118
+ getDefaultModel() {
119
+ return getProviderModel("XOR_MODEL", XorModels.XOR_1_1);
120
+ }
121
+ vendorLabel() {
122
+ return "XOR";
123
+ }
124
+ vendorDisplayName() {
125
+ return "XOR";
126
+ }
127
+ decisionApiKey() {
128
+ return this.apiKey;
129
+ }
130
+ missingKeyMessage() {
131
+ return "XOR requires an API key. Set XOR_API_KEY or pass credentials.xor.apiKey.";
132
+ }
133
+ missingConfigMessage() {
134
+ return this.baseURL
135
+ ? baseURLProblem(this.baseURL)
136
+ : "XOR requires a base URL. Set XOR_BASE_URL or pass credentials.xor.baseURL.";
137
+ }
138
+ decisionEndpoint() {
139
+ return `${this.baseURL}${XOR_ROUTE}`;
140
+ }
141
+ decisionHeaders() {
142
+ return {
143
+ Authorization: `Bearer ${this.apiKey}`,
144
+ "Content-Type": "application/json",
145
+ };
146
+ }
147
+ /**
148
+ * `model` is always sent. A LiteLLM proxy checks team access against the
149
+ * body's `model`, and refuses a request that names none. `images` is
150
+ * omitted rather than sent empty, which the server rejects.
151
+ */
152
+ buildDecisionBody(state, questions, model, media) {
153
+ return {
154
+ model,
155
+ state,
156
+ questions,
157
+ ...(media && media.images.length > 0 ? { images: media.images } : {}),
158
+ ...(media?.video ? { video: media.video } : {}),
159
+ };
160
+ }
161
+ /**
162
+ * A 403 is deliberately NOT `authentication`: on a LiteLLM proxy it means the
163
+ * team's allow-list lacks the model, which an admin can fix at any time, and
164
+ * `authentication` would trip the per-instance breaker and keep XOR disabled
165
+ * until restart even after access is granted. A 402 (a budget) is the same
166
+ * kind of fixable gap. `DecisionErrorKind` has no billing kind, so both map
167
+ * to `invalid_request`.
168
+ */
169
+ parseDecisionError(status, payload, requestId) {
170
+ const fallback = `XOR request failed with HTTP ${status}`;
171
+ const message = redactCredentials(readXorErrorMessage(payload) ?? fallback, this.apiKey) || fallback;
172
+ const kind = xorErrorKind(status, message);
173
+ return {
174
+ kind,
175
+ message,
176
+ status,
177
+ requestId,
178
+ retryable: kind === "rate_limit" || kind === "overloaded" || kind === "server",
179
+ };
180
+ }
181
+ /**
182
+ * XOR's server sets `x-request-id` and `x-typesafe-request-id` to one value.
183
+ * A proxy in front may replace or drop them, so its own call id is the last
184
+ * resort.
185
+ */
186
+ readRequestId(headers) {
187
+ return (headers.get("x-request-id") ??
188
+ headers.get("x-typesafe-request-id") ??
189
+ headers.get("x-litellm-call-id") ??
190
+ undefined);
191
+ }
192
+ }
@@ -25,8 +25,9 @@ export type ClassifierDifficulty = "trivial" | "simple" | "moderate" | "hard" |
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
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
+ * or in SDK credentials (`TYPESAFE_API_KEY`, `LAYA_API_KEY` with
29
+ * `LAYA_BASE_URL`, or `XOR_API_KEY` with `XOR_BASE_URL`), otherwise
30
+ * `heuristic`.
30
31
  */
31
32
  export type ClassifierStrategyKind = "heuristic" | "llm" | "jev" | "auto";
32
33
  /**
@@ -186,8 +187,8 @@ export type ClassifierRouterConfig = {
186
187
  /**
187
188
  * Classification strategy. Default: "auto" — which resolves to "jev" when a
188
189
  * 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
190
+ * (`TYPESAFE_API_KEY`, `LAYA_API_KEY` with `LAYA_BASE_URL`, or
191
+ * `XOR_API_KEY` with `XOR_BASE_URL`) and "heuristic" otherwise, so configuring one
191
192
  * upgrades routing without any code change. Behaviour for callers with no
192
193
  * key is unchanged.
193
194
  */
@@ -1310,6 +1310,10 @@ export type CliDecideArgs = {
1310
1310
  stateFile?: string;
1311
1311
  questions?: string;
1312
1312
  questionsFile?: string;
1313
+ /** Image files (or data: URLs) for a provider that reads images. */
1314
+ image?: string[];
1315
+ /** One video file (or data: URL) for a provider that reads video. */
1316
+ video?: string;
1313
1317
  provider?: string;
1314
1318
  model?: string;
1315
1319
  timeout?: number;
@@ -1722,8 +1726,8 @@ export type CliClassifierRouterFlags = {
1722
1726
  /**
1723
1727
  * Strategy: "auto" (default), "heuristic", "llm" or "jev"
1724
1728
  * (--classifier-strategy). "auto" resolves to "jev" when a decision
1725
- * provider is configured (TYPESAFE_API_KEY, or LAYA_API_KEY with
1726
- * LAYA_BASE_URL) and "heuristic" otherwise.
1729
+ * provider is configured (TYPESAFE_API_KEY, LAYA_API_KEY with
1730
+ * LAYA_BASE_URL, or XOR_API_KEY with XOR_BASE_URL) and "heuristic" otherwise.
1727
1731
  */
1728
1732
  classifierStrategy?: string;
1729
1733
  /**
@@ -95,9 +95,19 @@ export type DecisionUsage = {
95
95
  inputTokens: number;
96
96
  outputTokens: number;
97
97
  };
98
+ /** An image or video as the caller supplies it: raw bytes, a file path, or a data URL. */
99
+ export type DecisionMediaSource = Buffer | string;
98
100
  export type DecisionRequest = {
99
101
  state: DecisionState;
100
102
  questions: DecisionQuestionMap;
103
+ /**
104
+ * Images the model reads alongside `state`. Each is a Buffer, a local file
105
+ * path or a `data:image/…;base64,` URL; an http(s) URL is refused. Only a
106
+ * provider whose descriptor declares `decisionLimits.media` accepts them.
107
+ */
108
+ images?: readonly DecisionMediaSource[];
109
+ /** One video, in the same forms. Sending images as well is allowed. */
110
+ video?: DecisionMediaSource;
101
111
  /** Overrides the provider's configured model for this call only. */
102
112
  model?: string;
103
113
  signal?: AbortSignal;
@@ -116,6 +126,8 @@ export type DecisionResult = {
116
126
  latencyMs: number;
117
127
  /** Server-side time behind the edge; isolates model time from network. */
118
128
  upstreamMs?: number;
129
+ /** Encoded size of the images and video sent. Absent for a text-only request. */
130
+ mediaBytes?: number;
119
131
  };
120
132
  /**
121
133
  * Input a decision provider can actually read, declared on its descriptor.
@@ -132,7 +144,8 @@ export type DecisionLimits = {
132
144
  * name — so it should be the tightest window the provider has.
133
145
  */
134
146
  maxStateTokens: number;
135
- maxQuestions: number;
147
+ /** Absent when the provider has no cap on questions per request. */
148
+ maxQuestions?: number;
136
149
  /**
137
150
  * Tokens charged per non-ASCII character. The default estimate assumes ~4
138
151
  * characters per token, which holds for English and is several times too
@@ -145,6 +158,52 @@ export type DecisionLimits = {
145
158
  maxStateTokens: number;
146
159
  nonAsciiTokensPerChar?: number;
147
160
  }>>;
161
+ /** What the provider accepts besides text. Absent means text only. */
162
+ media?: DecisionMediaLimits;
163
+ };
164
+ export type DecisionMediaLimits = {
165
+ /** Most images one request may carry. */
166
+ maxImages: number;
167
+ /** Whether a video is accepted (at most one). */
168
+ video: boolean;
169
+ /** Ceiling on the encoded request body, in bytes. */
170
+ maxRequestBytes: number;
171
+ };
172
+ /** Media after preparation: everything is a `data:` URL, ready for the wire. */
173
+ export type DecisionPreparedMedia = {
174
+ images: string[];
175
+ video?: string;
176
+ /** Encoded size of every URL above. */
177
+ bytes: number;
178
+ };
179
+ /**
180
+ * Discriminated by a string literal, not a boolean: the package is also
181
+ * compiled without strictNullChecks, where a boolean discriminant does not
182
+ * narrow.
183
+ */
184
+ export type DecisionMediaResult = {
185
+ status: "prepared";
186
+ media: DecisionPreparedMedia | undefined;
187
+ } | {
188
+ status: "refused";
189
+ message: string;
190
+ };
191
+ export type DecisionMediaKind = "image" | "video";
192
+ /** One image or video turned into a data URL, or the reason it could not be. Same literal-discriminant rule as above. */
193
+ export type DecisionMediaConversion = {
194
+ status: "ok";
195
+ dataUrl: string;
196
+ } | {
197
+ status: "error";
198
+ message: string;
199
+ };
200
+ /** A media file read from disk, or the reason it could not be. */
201
+ export type DecisionMediaFileRead = {
202
+ status: "ok";
203
+ buffer: Buffer;
204
+ } | {
205
+ status: "error";
206
+ message: string;
148
207
  };
149
208
  /**
150
209
  * Why a decision call failed, normalised across vendors. TypeSafe alone
@@ -255,6 +255,16 @@ export type StreamLoopArgs = {
255
255
  * schema was requested, or when tools suppress it.
256
256
  */
257
257
  responseFormat?: OpenAICompatResponseFormat;
258
+ /**
259
+ * Fired once per step that echoes a `model` field on the wire (SSE
260
+ * `chunk.model`, captured by `parseSSEStream` into `OpenAICompatSSEResult.model`).
261
+ * Lets the caller learn what the SERVER actually served, distinct from the
262
+ * pre-call resolved/requested `modelId` — a gateway or router can rewrite
263
+ * an alias to a concrete model id. Not called when a step's response omits
264
+ * `model` (some backends don't echo it), so the caller must keep its own
265
+ * fallback.
266
+ */
267
+ onModelObserved?: (model: string) => void;
258
268
  };
259
269
  export type OpenAICompatBuildBodyArgs = {
260
270
  modelId: string;
@@ -322,6 +322,16 @@ export type NeurolinkCredentials = {
322
322
  apiKey?: string;
323
323
  baseURL?: string;
324
324
  };
325
+ /**
326
+ * XOR (Juspay, open weights) — the `decide` inference type. There is no
327
+ * built-in endpoint: `baseURL` (or XOR_BASE_URL) is required, pointing at the
328
+ * origin of an XOR deployment or a proxy route to one; requests go to
329
+ * `<baseURL>/v1/systemone`.
330
+ */
331
+ xor?: {
332
+ apiKey?: string;
333
+ baseURL?: string;
334
+ };
325
335
  };
326
336
  /**
327
337
  * Voyage AI /embeddings response shape.
@@ -0,0 +1,9 @@
1
+ import type { DecisionMediaLimits, DecisionMediaResult, DecisionMediaSource } from "../types/index.js";
2
+ /**
3
+ * Turn the caller's images and video into `data:` URLs, or say why not.
4
+ * Runs before any request, so every refusal costs nothing and none is retried.
5
+ */
6
+ export declare function prepareDecisionMedia(request: {
7
+ images?: readonly DecisionMediaSource[];
8
+ video?: DecisionMediaSource;
9
+ }, limits: DecisionMediaLimits | undefined, vendor: string, alternatives: readonly string[]): Promise<DecisionMediaResult>;