@juspay/neurolink 12.25.5 → 12.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -3
- package/dist/browser/neurolink.min.js +369 -369
- package/dist/cli/commands/decide.d.ts +2 -1
- package/dist/cli/commands/decide.js +4 -2
- package/dist/cli/commands/setup.js +2 -1
- package/dist/cli/factories/commandFactory.js +1 -1
- package/dist/cli/utils/classifierRouterFlags.js +1 -1
- package/dist/constants/enums.d.ts +15 -0
- package/dist/constants/enums.js +16 -0
- package/dist/core/baseProvider.js +2 -2
- package/dist/factories/providerDescriptors.d.ts +16 -7
- package/dist/factories/providerDescriptors.js +97 -15
- package/dist/factories/providerRegistry.js +9 -1
- package/dist/models/manifestRegistry.js +2 -0
- package/dist/models/manifests/laya.d.ts +12 -0
- package/dist/models/manifests/laya.js +54 -0
- package/dist/neurolink.js +8 -3
- package/dist/providers/laya.d.ts +40 -0
- package/dist/providers/laya.js +170 -0
- package/dist/providers/systemOneDecision.d.ts +102 -0
- package/dist/providers/systemOneDecision.js +443 -0
- package/dist/providers/typesafe.d.ts +32 -47
- package/dist/providers/typesafe.js +64 -386
- package/dist/routing/classifierRouter.d.ts +2 -2
- package/dist/routing/classifierRouter.js +10 -8
- package/dist/types/classifierRouter.d.ts +13 -3
- package/dist/types/cli.d.ts +3 -2
- package/dist/types/decision.d.ts +29 -0
- package/dist/types/providers.d.ts +20 -1
- package/dist/utils/modelChoices.js +21 -1
- package/dist/utils/providerConfig.d.ts +5 -0
- package/dist/utils/providerConfig.js +17 -0
- package/docs-site/static/search-index.json +24 -10
- package/package.json +2 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { LayaModels } 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, SystemOneDecisionProvider, } from "./systemOneDecision.js";
|
|
6
|
+
/**
|
|
7
|
+
* LiteLLM echoes credentials back in its error texts, in more than one
|
|
8
|
+
* wording: a masked key plus its hash on a rejected key, the whole key when it
|
|
9
|
+
* does not look like a LiteLLM key, a key hash on a rate limit. None of it
|
|
10
|
+
* belongs in an error message or a log, whatever the wording, so the
|
|
11
|
+
* configured key, anything shaped like a LiteLLM key and any long hex run are
|
|
12
|
+
* all removed.
|
|
13
|
+
*/
|
|
14
|
+
function redactCredentials(message, apiKey) {
|
|
15
|
+
const withoutKey = apiKey
|
|
16
|
+
? message.split(apiKey).join("[redacted]")
|
|
17
|
+
: message;
|
|
18
|
+
return withoutKey
|
|
19
|
+
.replace(/\.?\s*Received API Key\s*=[\s\S]*$/, "")
|
|
20
|
+
.replace(/\bsk-[A-Za-z0-9._-]+/g, "[redacted]")
|
|
21
|
+
.replace(/\b[0-9a-f]{32,}\b/gi, "[redacted]")
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Normalise the two envelopes a Laya failure can arrive in.
|
|
26
|
+
*
|
|
27
|
+
* LiteLLM's own, `{"error":{"message","type","code"}}`, for anything the proxy
|
|
28
|
+
* rejects before Laya sees it (a bad key, a rate limit). FastAPI's, from
|
|
29
|
+
* Laya's server: `{"detail":"..."}` for its own checks (question types, the
|
|
30
|
+
* question cap) and `{"detail":[{loc,msg,input}]}` for request validation.
|
|
31
|
+
*/
|
|
32
|
+
function parseLayaError(status, body, requestId) {
|
|
33
|
+
const retryable = status === 429 || status >= 500;
|
|
34
|
+
const kind = status === 401 || status === 403
|
|
35
|
+
? "authentication"
|
|
36
|
+
: status === 413
|
|
37
|
+
? "max_tokens_exceeded"
|
|
38
|
+
: status === 429
|
|
39
|
+
? "rate_limit"
|
|
40
|
+
: status === 503
|
|
41
|
+
? "overloaded"
|
|
42
|
+
: status >= 500
|
|
43
|
+
? "server"
|
|
44
|
+
: "invalid_request";
|
|
45
|
+
const proxyError = isRecord(body) && isRecord(body.error) ? body.error : undefined;
|
|
46
|
+
if (proxyError &&
|
|
47
|
+
typeof proxyError.message === "string" &&
|
|
48
|
+
proxyError.message.length > 0) {
|
|
49
|
+
return {
|
|
50
|
+
kind,
|
|
51
|
+
message: proxyError.message,
|
|
52
|
+
status,
|
|
53
|
+
requestId,
|
|
54
|
+
retryable,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const detail = isRecord(body) ? body.detail : undefined;
|
|
58
|
+
if (Array.isArray(detail)) {
|
|
59
|
+
return {
|
|
60
|
+
kind: "invalid_request",
|
|
61
|
+
message: `Request failed validation — ${describeValidationErrors(detail)}`,
|
|
62
|
+
status,
|
|
63
|
+
requestId,
|
|
64
|
+
retryable: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (typeof detail === "string" && detail.length > 0) {
|
|
68
|
+
return { kind, message: detail, status, requestId, retryable };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
kind,
|
|
72
|
+
message: `Laya request failed with HTTP ${status}`,
|
|
73
|
+
status,
|
|
74
|
+
requestId,
|
|
75
|
+
retryable,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Laya Provider — the `decide` inference type only.
|
|
80
|
+
*
|
|
81
|
+
* Laya is Convai Innovations' open-weights "System One" decision model: the
|
|
82
|
+
* same typed `noul` / `choice` / `score` questions as TypeSafe's Jev, answered
|
|
83
|
+
* by an encoder in one forward pass, on the same wire. The request loop,
|
|
84
|
+
* retries, auth circuit breaker and answer parsing are shared with every
|
|
85
|
+
* decision provider in {@link SystemOneDecisionProvider}.
|
|
86
|
+
*
|
|
87
|
+
* Its encoders read far less than Jev: 1,024 tokens on the `typed-decisions`
|
|
88
|
+
* and `multilingual` checkpoints, 512 on `english`. The descriptor's
|
|
89
|
+
* `decisionLimits` refuses what does not fit rather than letting the server
|
|
90
|
+
* cut the state off silently.
|
|
91
|
+
*
|
|
92
|
+
* @see https://github.com/NandhaKishorM/laya
|
|
93
|
+
*/
|
|
94
|
+
export class LayaProvider extends SystemOneDecisionProvider {
|
|
95
|
+
apiKey;
|
|
96
|
+
baseURL;
|
|
97
|
+
constructor(modelName, sdk, _region, credentials) {
|
|
98
|
+
super(modelName, "laya", sdk);
|
|
99
|
+
this.apiKey =
|
|
100
|
+
credentials?.apiKey?.trim() || (process.env.LAYA_API_KEY?.trim() ?? "");
|
|
101
|
+
// No built-in endpoint: Laya is self-hosted or reached through a proxy
|
|
102
|
+
// (a Laya server, or a LiteLLM pass-through route to one), so the base URL
|
|
103
|
+
// comes only from config. `||`, not `??`, so a blank one counts as unset.
|
|
104
|
+
this.baseURL = (credentials?.baseURL?.trim() ||
|
|
105
|
+
process.env.LAYA_BASE_URL?.trim() ||
|
|
106
|
+
"").replace(/\/+$/, "");
|
|
107
|
+
// A self-hosted LAYA_BASE_URL can carry `user:pass@` or a `?token=`.
|
|
108
|
+
logger.debug("Laya Provider initialized (decide only)", {
|
|
109
|
+
modelName: this.modelName,
|
|
110
|
+
baseURL: this.baseURL ? redactUrlForError(this.baseURL) : "(not set)",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
getDefaultModel() {
|
|
114
|
+
return getProviderModel("LAYA_MODEL", LayaModels.TYPED_DECISIONS);
|
|
115
|
+
}
|
|
116
|
+
vendorLabel() {
|
|
117
|
+
return "Laya";
|
|
118
|
+
}
|
|
119
|
+
vendorDisplayName() {
|
|
120
|
+
return "Laya";
|
|
121
|
+
}
|
|
122
|
+
decisionApiKey() {
|
|
123
|
+
return this.apiKey;
|
|
124
|
+
}
|
|
125
|
+
missingKeyMessage() {
|
|
126
|
+
return "Laya requires an API key. Set LAYA_API_KEY or pass credentials.laya.apiKey.";
|
|
127
|
+
}
|
|
128
|
+
missingConfigMessage() {
|
|
129
|
+
return this.baseURL
|
|
130
|
+
? undefined
|
|
131
|
+
: "Laya requires a base URL. Set LAYA_BASE_URL or pass credentials.laya.baseURL.";
|
|
132
|
+
}
|
|
133
|
+
decisionEndpoint() {
|
|
134
|
+
return `${this.baseURL}/predict`;
|
|
135
|
+
}
|
|
136
|
+
decisionHeaders() {
|
|
137
|
+
return {
|
|
138
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
139
|
+
"Content-Type": "application/json",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** `auto` means "let Laya's router choose", which it does when no model is named. */
|
|
143
|
+
buildDecisionBody(state, questions, model) {
|
|
144
|
+
return model === LayaModels.AUTO
|
|
145
|
+
? { state, questions }
|
|
146
|
+
: { model, state, questions };
|
|
147
|
+
}
|
|
148
|
+
parseDecisionError(status, payload, requestId) {
|
|
149
|
+
const parsed = parseLayaError(status, payload, requestId);
|
|
150
|
+
const message = redactCredentials(parsed.message, this.apiKey);
|
|
151
|
+
return {
|
|
152
|
+
...parsed,
|
|
153
|
+
message: message || `Laya request failed with HTTP ${status}`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
readRequestId(headers) {
|
|
157
|
+
return headers.get("x-litellm-call-id") ?? undefined;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The checkpoint that actually answered is `routing.model` — the one Laya's
|
|
161
|
+
* router picked when the request said `auto`.
|
|
162
|
+
*/
|
|
163
|
+
resolveResponseModel(payload, requestedModel) {
|
|
164
|
+
const routing = payload.routing;
|
|
165
|
+
if (isRecord(routing) && typeof routing.model === "string") {
|
|
166
|
+
return routing.model;
|
|
167
|
+
}
|
|
168
|
+
return super.resolveResponseModel(payload, requestedModel);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { AIProviderName } from "../constants/enums.js";
|
|
2
|
+
import { BaseProvider } from "../core/baseProvider.js";
|
|
3
|
+
import { ProviderError } from "../types/index.js";
|
|
4
|
+
import type { DecisionError, DecisionQuestion, DecisionRequest, DecisionResult, DecisionState, LanguageModel, StreamOptions, StreamResult, ValidationSchema } from "../types/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Generous enough for a cold start (measured at 2.0–2.7s after idle on Jev)
|
|
7
|
+
* while still bounded. Every internal caller is fail-open, so this only bites
|
|
8
|
+
* on a genuine stall.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_DECISION_TIMEOUT_MS = 5000;
|
|
11
|
+
export declare const isRecord: (v: unknown) => v is Record<string, unknown>;
|
|
12
|
+
export declare const asNumber: (v: unknown) => number | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* FastAPI's request-validation envelope, `{"detail":[{loc, msg, input}]}`,
|
|
15
|
+
* reduced to its field paths and messages. `input` echoes the caller's own
|
|
16
|
+
* request back — possibly the whole state — so it is never surfaced.
|
|
17
|
+
*/
|
|
18
|
+
export declare function describeValidationErrors(detail: readonly unknown[]): string;
|
|
19
|
+
/**
|
|
20
|
+
* The shared half of every "System One" decision provider — a model that takes
|
|
21
|
+
* one `state` plus typed questions and returns one typed answer each, with no
|
|
22
|
+
* text anywhere.
|
|
23
|
+
*
|
|
24
|
+
* Vendors differ in where the request goes, how it is authenticated, what the
|
|
25
|
+
* body carries and what their error envelopes look like. They do not differ in
|
|
26
|
+
* anything else, so everything else lives here: the missing-key check, the
|
|
27
|
+
* auth circuit breaker, the timeout and retry loop, answer parsing and usage.
|
|
28
|
+
* A new decision provider implements the abstract hooks and nothing more.
|
|
29
|
+
*/
|
|
30
|
+
export declare abstract class SystemOneDecisionProvider extends BaseProvider {
|
|
31
|
+
protected readonly proxyFetch: typeof fetch;
|
|
32
|
+
private readonly maxRetries;
|
|
33
|
+
/**
|
|
34
|
+
* Set once the key has been rejected. A bad key cannot fix itself, and
|
|
35
|
+
* without this every later request would pay a full round trip (~265ms
|
|
36
|
+
* measured on Jev) to be told so again — a permanent tax on the hot path for
|
|
37
|
+
* a misconfiguration.
|
|
38
|
+
*/
|
|
39
|
+
private authFailed;
|
|
40
|
+
protected constructor(modelName: string | undefined, providerName: AIProviderName, sdk: unknown);
|
|
41
|
+
/** Short vendor name for messages and logs, e.g. "TypeSafe". */
|
|
42
|
+
protected abstract vendorLabel(): string;
|
|
43
|
+
/** How the model is named when describing it, e.g. "TypeSafe (Jev)". */
|
|
44
|
+
protected abstract vendorDisplayName(): string;
|
|
45
|
+
protected abstract decisionApiKey(): string;
|
|
46
|
+
protected abstract missingKeyMessage(): string;
|
|
47
|
+
/**
|
|
48
|
+
* Why this instance cannot send a request even though it has a key, such as
|
|
49
|
+
* a provider with no built-in endpoint and none configured. Undefined when
|
|
50
|
+
* nothing is missing.
|
|
51
|
+
*/
|
|
52
|
+
protected missingConfigMessage(): string | undefined;
|
|
53
|
+
protected abstract decisionEndpoint(): string;
|
|
54
|
+
protected abstract decisionHeaders(): Record<string, string>;
|
|
55
|
+
protected abstract buildDecisionBody(state: DecisionState, questions: Record<string, Record<string, unknown>>, model: string): Record<string, unknown>;
|
|
56
|
+
protected abstract parseDecisionError(status: number, payload: unknown, requestId: string | undefined): DecisionError;
|
|
57
|
+
protected abstract readRequestId(headers: Headers): string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* The System One wire calls the yes/no primitive `noul`; this codebase and
|
|
60
|
+
* every SDK exposing it call it `boolean`.
|
|
61
|
+
*/
|
|
62
|
+
protected encodeQuestion(question: DecisionQuestion): Record<string, unknown>;
|
|
63
|
+
/** Per-question confidence a transport reports outside the answer objects. */
|
|
64
|
+
protected reportedConfidence(_payload: unknown): Record<string, number>;
|
|
65
|
+
protected resolveResponseModel(payload: Record<string, unknown>, requestedModel: string): string;
|
|
66
|
+
/** Extra fields for the credential-rejected warning. */
|
|
67
|
+
protected diagnosticContext(): Record<string, unknown>;
|
|
68
|
+
protected getProviderName(): AIProviderName;
|
|
69
|
+
protected getDefaultDecisionModel(): string | undefined;
|
|
70
|
+
supportsTools(): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* A decision model produces no text, so there is no chat model behind it.
|
|
73
|
+
* Unreachable while the descriptor declares `inferenceKinds: ["decide"]`;
|
|
74
|
+
* kept actionable for the case where something routes here anyway.
|
|
75
|
+
*/
|
|
76
|
+
protected getAISDKModel(): LanguageModel;
|
|
77
|
+
protected executeStream(_options: StreamOptions, _analysisSchema?: ValidationSchema): Promise<StreamResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Must RETURN the error, never throw it (CLAUDE.md rule 6).
|
|
80
|
+
*/
|
|
81
|
+
protected formatProviderError(error: unknown): Error;
|
|
82
|
+
/**
|
|
83
|
+
* Evaluate a state against a batch of typed questions.
|
|
84
|
+
*
|
|
85
|
+
* Throws a {@link ProviderError} carrying a normalised {@link DecisionError}
|
|
86
|
+
* in `cause`. Internal hot paths should prefer `NeuroLink.tryDecide()`,
|
|
87
|
+
* which returns null instead.
|
|
88
|
+
*/
|
|
89
|
+
decide(request: DecisionRequest): Promise<DecisionResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Refuse a request the model cannot read in full. An encoder cuts the state
|
|
92
|
+
* off past its window without saying so, so without this a decision would
|
|
93
|
+
* be made on input the model never saw — and reported as if it had.
|
|
94
|
+
*/
|
|
95
|
+
private assertWithinDecisionLimits;
|
|
96
|
+
/**
|
|
97
|
+
* Wrap a normalised decision failure in the provider error type, keeping
|
|
98
|
+
* the structured detail reachable via `cause` for callers that branch on
|
|
99
|
+
* `kind` (authentication vs rate_limit vs max_tokens_exceeded).
|
|
100
|
+
*/
|
|
101
|
+
protected decisionError(detail: DecisionError): ProviderError;
|
|
102
|
+
}
|