@juspay/neurolink 12.37.4 → 12.38.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/README.md +60 -50
- package/dist/browser/neurolink.min.js +423 -423
- package/dist/cli/commands/decide.d.ts +2 -2
- package/dist/cli/commands/decide.js +22 -3
- package/dist/cli/commands/setup.js +2 -1
- package/dist/cli/factories/commandFactory.js +1 -1
- package/dist/constants/enums.d.ts +9 -0
- package/dist/constants/enums.js +10 -0
- package/dist/core/baseProvider.d.ts +10 -1
- package/dist/core/baseProvider.js +35 -7
- package/dist/factories/providerDescriptors.d.ts +2 -0
- package/dist/factories/providerDescriptors.js +52 -6
- package/dist/factories/providerRegistry.js +9 -1
- package/dist/models/manifestRegistry.js +2 -0
- package/dist/models/manifests/anthropic.js +2 -2
- package/dist/models/manifests/xor.d.ts +12 -0
- package/dist/models/manifests/xor.js +30 -0
- package/dist/models/modelRegistry.d.ts +12 -1
- package/dist/models/modelRegistry.js +57 -5
- package/dist/neurolink.js +6 -0
- package/dist/providers/amazonSagemaker.js +1 -1
- package/dist/providers/anthropic/client.js +29 -5
- package/dist/providers/anthropic/structuredOutput.d.ts +4 -1
- package/dist/providers/anthropic/structuredOutput.js +13 -5
- package/dist/providers/catalog/cerebras.json +7 -8
- package/dist/providers/catalog/fireworks.json +10 -12
- package/dist/providers/googleVertex/client.d.ts +3 -3
- package/dist/providers/googleVertex/client.js +79 -26
- package/dist/providers/laya.js +1 -19
- package/dist/providers/openaiChatCompletionsBase.js +1 -1
- package/dist/providers/systemOneDecision.d.ts +21 -2
- package/dist/providers/systemOneDecision.js +64 -4
- package/dist/providers/xor.d.ts +48 -0
- package/dist/providers/xor.js +192 -0
- package/dist/proxy/vertexAnthropicFallback.d.ts +1 -12
- package/dist/proxy/vertexAnthropicFallback.js +43 -2
- package/dist/types/classifierRouter.d.ts +5 -4
- package/dist/types/cli.d.ts +6 -2
- package/dist/types/decision.d.ts +60 -1
- package/dist/types/model.d.ts +6 -0
- package/dist/types/providers.d.ts +10 -0
- package/dist/utils/decisionMedia.d.ts +9 -0
- package/dist/utils/decisionMedia.js +160 -0
- package/dist/utils/modelChoices.js +9 -1
- package/dist/utils/providerConfig.d.ts +5 -0
- package/dist/utils/providerConfig.js +17 -0
- package/docs-site/static/search-index.json +63 -46
- package/package.json +2 -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
|
|
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 (
|
|
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
|
+
}
|
|
@@ -22,18 +22,7 @@ import type { VertexAccessTokenProvider, UsageContext, VertexPassthroughTerminal
|
|
|
22
22
|
* `effort` key, never the sibling settings beside it.
|
|
23
23
|
*/
|
|
24
24
|
export declare function mapVertexOutputConfig(value: unknown): Record<string, unknown> | undefined;
|
|
25
|
-
|
|
26
|
-
* Strip what Vertex rejects and pin the API version it requires.
|
|
27
|
-
*
|
|
28
|
-
* Prompt caching constrains every rewrite here. Anthropic caches only up to an
|
|
29
|
-
* explicit `cache_control` marker, at most four per request, over a prefix
|
|
30
|
-
* rendered `tools → system → messages`; anything removed or inserted before a
|
|
31
|
-
* marker invalidates it and everything after. So: never drop a marker, never
|
|
32
|
-
* reorder `system`, and keep each rewrite deterministic so consecutive turns
|
|
33
|
-
* share a prefix. Vertex holds its own cache, apart from the first-party one,
|
|
34
|
-
* so nothing here can disturb first-party hits.
|
|
35
|
-
*/
|
|
36
|
-
export declare function buildVertexAnthropicPayload(body: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
25
|
+
export declare function buildVertexAnthropicPayload(body: Readonly<Record<string, unknown>>, targetModel?: string): Record<string, unknown>;
|
|
37
26
|
/**
|
|
38
27
|
* Publisher endpoint for a Claude model on Vertex.
|
|
39
28
|
*
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* here because nothing reshapes them.
|
|
14
14
|
*/
|
|
15
15
|
import { logger } from "../utils/logger.js";
|
|
16
|
+
import { claudeDisabledThinkingReplacement, modelSupportsForcedToolChoice, } from "../models/modelRegistry.js";
|
|
16
17
|
import { isTransientNetworkError } from "./proxyFetch.js";
|
|
17
18
|
/** Vertex pins the Anthropic API version in the body, not a header. */
|
|
18
19
|
const VERTEX_ANTHROPIC_VERSION = "vertex-2023-10-16";
|
|
@@ -334,7 +335,44 @@ function stripBillingBlock(system) {
|
|
|
334
335
|
* share a prefix. Vertex holds its own cache, apart from the first-party one,
|
|
335
336
|
* so nothing here can disturb first-party hits.
|
|
336
337
|
*/
|
|
337
|
-
|
|
338
|
+
/**
|
|
339
|
+
* The client addressed its own model; the fallback target may be a Claude
|
|
340
|
+
* 5.5 / 5.1 model, which answers a forced `tool_choice` and
|
|
341
|
+
* `thinking: {type: "disabled"}` with a 400. Reshape both into what the target
|
|
342
|
+
* accepts rather than fail the turn the fallback exists to rescue.
|
|
343
|
+
*/
|
|
344
|
+
function fitRequestToTarget(payload, targetModel) {
|
|
345
|
+
const choice = payload.tool_choice;
|
|
346
|
+
if (record(choice) &&
|
|
347
|
+
(choice.type === "any" || choice.type === "tool") &&
|
|
348
|
+
!modelSupportsForcedToolChoice(targetModel)) {
|
|
349
|
+
payload.tool_choice = {
|
|
350
|
+
type: "auto",
|
|
351
|
+
...(choice.disable_parallel_tool_use !== undefined
|
|
352
|
+
? { disable_parallel_tool_use: choice.disable_parallel_tool_use }
|
|
353
|
+
: {}),
|
|
354
|
+
};
|
|
355
|
+
logger.warn(`[vertex-anthropic] ${targetModel} rejects tool_choice "${choice.type}"; sending "auto" instead`);
|
|
356
|
+
}
|
|
357
|
+
const thinking = payload.thinking;
|
|
358
|
+
if (record(thinking) && thinking.type === "disabled") {
|
|
359
|
+
const outputConfig = payload.output_config;
|
|
360
|
+
const effort = record(outputConfig) && typeof outputConfig.effort === "string"
|
|
361
|
+
? outputConfig.effort
|
|
362
|
+
: undefined;
|
|
363
|
+
const replacement = claudeDisabledThinkingReplacement(targetModel, effort);
|
|
364
|
+
if (replacement === "between_tools") {
|
|
365
|
+
payload.thinking = { type: "between_tools" };
|
|
366
|
+
}
|
|
367
|
+
else if (replacement === "omit") {
|
|
368
|
+
delete payload.thinking;
|
|
369
|
+
}
|
|
370
|
+
if (replacement !== "keep") {
|
|
371
|
+
logger.debug(`[vertex-anthropic] ${targetModel} rejects thinking "disabled"; ${replacement === "omit" ? "omitting thinking" : "sending between_tools"}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
export function buildVertexAnthropicPayload(body, targetModel) {
|
|
338
376
|
const dropped = new Set(UNSUPPORTED_FIELDS);
|
|
339
377
|
const payload = Object.fromEntries(Object.entries(body).filter(([key]) => !dropped.has(key)));
|
|
340
378
|
const outputConfig = resolveOutputConfig(payload.output_config, payload.messages);
|
|
@@ -359,6 +397,9 @@ export function buildVertexAnthropicPayload(body) {
|
|
|
359
397
|
payload.system = system;
|
|
360
398
|
}
|
|
361
399
|
}
|
|
400
|
+
if (targetModel !== undefined) {
|
|
401
|
+
fitRequestToTarget(payload, targetModel);
|
|
402
|
+
}
|
|
362
403
|
return { ...payload, anthropic_version: VERTEX_ANTHROPIC_VERSION };
|
|
363
404
|
}
|
|
364
405
|
/**
|
|
@@ -500,7 +541,7 @@ export async function dispatchVertexAnthropicPassthrough(request) {
|
|
|
500
541
|
"content-type": "application/json",
|
|
501
542
|
accept: stream ? "text/event-stream" : "application/json",
|
|
502
543
|
},
|
|
503
|
-
body: JSON.stringify(buildVertexAnthropicPayload(request.body)),
|
|
544
|
+
body: JSON.stringify(buildVertexAnthropicPayload(request.body, request.model)),
|
|
504
545
|
...(request.signal ? { signal: request.signal } : {}),
|
|
505
546
|
});
|
|
506
547
|
}
|
|
@@ -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`,
|
|
29
|
-
* `LAYA_BASE_URL`), otherwise
|
|
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`,
|
|
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
|
*/
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -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,
|
|
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
|
/**
|
package/dist/types/decision.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/types/model.d.ts
CHANGED
|
@@ -111,6 +111,12 @@ export type DynamicModelConfig = z.infer<typeof ModelConfigSchema>;
|
|
|
111
111
|
* Dynamic model registry type
|
|
112
112
|
*/
|
|
113
113
|
export type ModelRegistry = z.infer<typeof ModelRegistrySchema>;
|
|
114
|
+
/**
|
|
115
|
+
* What a request should carry in place of `thinking: {type: "disabled"}` for a
|
|
116
|
+
* Claude model: the original (`keep`), `between_tools`, or no `thinking` field
|
|
117
|
+
* at all (`omit`, i.e. adaptive).
|
|
118
|
+
*/
|
|
119
|
+
export type ClaudeDisabledThinkingReplacement = "keep" | "between_tools" | "omit";
|
|
114
120
|
/**
|
|
115
121
|
* Model capabilities interface
|
|
116
122
|
*/
|
|
@@ -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>;
|