@juspay/neurolink 9.57.1 → 9.59.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 +12 -0
- package/dist/browser/neurolink.min.js +314 -314
- package/dist/lib/neurolink.d.ts +69 -0
- package/dist/lib/neurolink.js +367 -3
- package/dist/lib/providers/litellm.js +12 -1
- package/dist/lib/providers/openAI.js +19 -2
- package/dist/lib/types/config.d.ts +23 -0
- package/dist/lib/types/errors.d.ts +42 -0
- package/dist/lib/types/errors.js +94 -0
- package/dist/lib/types/generate.d.ts +13 -0
- package/dist/lib/types/stream.d.ts +13 -0
- package/dist/neurolink.d.ts +69 -0
- package/dist/neurolink.js +367 -3
- package/dist/providers/litellm.js +12 -1
- package/dist/providers/openAI.js +19 -2
- package/dist/types/config.d.ts +23 -0
- package/dist/types/errors.d.ts +42 -0
- package/dist/types/errors.js +94 -0
- package/dist/types/generate.d.ts +13 -0
- package/dist/types/stream.d.ts +13 -0
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { BaseProvider } from "../core/baseProvider.js";
|
|
|
5
5
|
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
|
|
6
6
|
import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
|
|
7
7
|
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
8
|
-
import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
|
|
8
|
+
import { AuthenticationError, InvalidModelError, ModelAccessDeniedError, NetworkError, ProviderError, RateLimitError, isModelAccessDeniedMessage, parseAllowedModels, } from "../types/index.js";
|
|
9
9
|
import { isAbortError } from "../utils/errorHandling.js";
|
|
10
10
|
import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
|
|
11
11
|
import { logger } from "../utils/logger.js";
|
|
@@ -100,6 +100,17 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
100
100
|
return new NetworkError("LiteLLM proxy server not available. Please start the LiteLLM proxy server at " +
|
|
101
101
|
`${process.env.LITELLM_BASE_URL || "http://localhost:4000"}`, this.providerName);
|
|
102
102
|
}
|
|
103
|
+
// Curator P1-1: detect "team not allowed to access model" responses
|
|
104
|
+
// and surface as ModelAccessDeniedError with the allowed_models array
|
|
105
|
+
// parsed from the body. Must run before the generic "API key" check
|
|
106
|
+
// because LiteLLM phrases this as a 403 distinct from auth.
|
|
107
|
+
if (isModelAccessDeniedMessage(errorRecord.message)) {
|
|
108
|
+
return new ModelAccessDeniedError(errorRecord.message, {
|
|
109
|
+
provider: this.providerName,
|
|
110
|
+
requestedModel: this.modelName,
|
|
111
|
+
allowedModels: parseAllowedModels(errorRecord.message),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
103
114
|
if (errorRecord.message.includes("API_KEY_INVALID") ||
|
|
104
115
|
errorRecord.message.includes("Invalid API key")) {
|
|
105
116
|
return new AuthenticationError("Invalid LiteLLM configuration. Please check your LITELLM_API_KEY environment variable.", this.providerName);
|
package/dist/providers/openAI.js
CHANGED
|
@@ -235,10 +235,27 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
235
235
|
const errorType = errorObj?.type && typeof errorObj.type === "string"
|
|
236
236
|
? errorObj.type
|
|
237
237
|
: undefined;
|
|
238
|
+
const statusCode = typeof errorObj?.status === "number"
|
|
239
|
+
? errorObj.status
|
|
240
|
+
: typeof errorObj?.statusCode === "number"
|
|
241
|
+
? errorObj.statusCode
|
|
242
|
+
: undefined;
|
|
243
|
+
// Curator P1-1 / Reviewer Finding #4: only the explicit auth markers
|
|
244
|
+
// map to AuthenticationError. Earlier we treated every
|
|
245
|
+
// `invalid_request_error` as an auth failure — that's OpenAI's catch-all
|
|
246
|
+
// for any bad request (unsupported parameter, malformed JSON, etc.) and
|
|
247
|
+
// mislabelled them as "invalid API key". Use credential-specific
|
|
248
|
+
// signals only.
|
|
238
249
|
if (message.includes("API_KEY_INVALID") ||
|
|
239
250
|
message.includes("Invalid API key") ||
|
|
240
|
-
|
|
241
|
-
|
|
251
|
+
message.includes("Incorrect API key") ||
|
|
252
|
+
message.includes("invalid_api_key") ||
|
|
253
|
+
errorType === "invalid_api_key" ||
|
|
254
|
+
statusCode === 401) {
|
|
255
|
+
return new AuthenticationError(message.includes("Incorrect API key") ||
|
|
256
|
+
message.includes("Invalid API key")
|
|
257
|
+
? message
|
|
258
|
+
: "Invalid OpenAI API key. Please check your OPENAI_API_KEY environment variable.", this.providerName);
|
|
242
259
|
}
|
|
243
260
|
if (message.includes("rate limit") || errorType === "rate_limit_error") {
|
|
244
261
|
return new RateLimitError("OpenAI rate limit exceeded. Please try again later.", this.providerName);
|
package/dist/types/config.d.ts
CHANGED
|
@@ -21,6 +21,16 @@ export type NeuroLinkConfig = {
|
|
|
21
21
|
configVersion?: string;
|
|
22
22
|
[key: string]: unknown;
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Curator P2-3: callback signature for centralized fallback policy. Invoked
|
|
26
|
+
* when a generate/stream call fails with what looks like a model-access-denied
|
|
27
|
+
* error. Return `{ provider, model }` (either / both optional) to drive a
|
|
28
|
+
* retry; return `null` to bubble the original error untouched.
|
|
29
|
+
*/
|
|
30
|
+
export type ProviderFallbackCallback = (error: unknown) => Promise<{
|
|
31
|
+
provider?: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
} | null>;
|
|
24
34
|
/**
|
|
25
35
|
* Configuration object for NeuroLink constructor.
|
|
26
36
|
*/
|
|
@@ -43,6 +53,19 @@ export type NeurolinkConstructorConfig = {
|
|
|
43
53
|
* from this NeuroLink instance. Per-call credentials override these.
|
|
44
54
|
*/
|
|
45
55
|
credentials?: NeurolinkCredentials;
|
|
56
|
+
/**
|
|
57
|
+
* Curator P2-3: callback invoked on model-access-denied. Lets a host (e.g.
|
|
58
|
+
* Curator) centrally drive fallback policy. The callback receives the
|
|
59
|
+
* original error and returns the next `{ provider, model }` to try, or
|
|
60
|
+
* `null` to bubble the error.
|
|
61
|
+
*/
|
|
62
|
+
providerFallback?: ProviderFallbackCallback;
|
|
63
|
+
/**
|
|
64
|
+
* Curator P2-3: ordered list of model names to try in sequence on
|
|
65
|
+
* model-access-denied. Sugar over `providerFallback`. The current
|
|
66
|
+
* provider is preserved across the chain; only the model name changes.
|
|
67
|
+
*/
|
|
68
|
+
modelChain?: string[];
|
|
46
69
|
};
|
|
47
70
|
/**
|
|
48
71
|
* Configuration for MCP enhancement modules wired into generate()/stream() paths.
|
package/dist/types/errors.d.ts
CHANGED
|
@@ -104,3 +104,45 @@ export declare class ModelAccessError extends BaseError {
|
|
|
104
104
|
readonly requiredTier: string;
|
|
105
105
|
constructor(model: string, tier: string, requiredTier: string);
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Curator P1-1: thrown when a provider rejects a request because the
|
|
109
|
+
* caller's team / API key is not whitelisted for the requested model.
|
|
110
|
+
*
|
|
111
|
+
* LiteLLM's `team not allowed to access model. This team can only access
|
|
112
|
+
* models=['glm-latest', 'kimi-latest', ...]` is the canonical example —
|
|
113
|
+
* the list is parsed off the error body so callers / fallback orchestrators
|
|
114
|
+
* can choose a whitelisted alternative without scraping strings.
|
|
115
|
+
*/
|
|
116
|
+
export declare class ModelAccessDeniedError extends ProviderError {
|
|
117
|
+
readonly requestedModel: string | undefined;
|
|
118
|
+
readonly allowedModels: string[] | undefined;
|
|
119
|
+
readonly code: "MODEL_ACCESS_DENIED";
|
|
120
|
+
constructor(message: string, options?: {
|
|
121
|
+
provider?: string;
|
|
122
|
+
requestedModel?: string;
|
|
123
|
+
allowedModels?: string[];
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Parse the `allowed_models` array out of a provider error message body.
|
|
128
|
+
* Currently targets the LiteLLM team-whitelist response shape:
|
|
129
|
+
*
|
|
130
|
+
* "team not allowed to access model. This team can only access
|
|
131
|
+
* models=['glm-latest', 'kimi-latest', 'open-large']"
|
|
132
|
+
*
|
|
133
|
+
* Implementation note: deliberately uses `indexOf`/`slice` instead of a
|
|
134
|
+
* single `/models\s*=\s*\[([^\]]*)\]/` regex. CodeQL flagged the latter
|
|
135
|
+
* as `js/polynomial-redos` because the `[^\]]*` greedy quantifier on
|
|
136
|
+
* library-supplied input can be exploited by a crafted long string. The
|
|
137
|
+
* indexOf/slice path is O(n) with no backtracking and we additionally
|
|
138
|
+
* cap the input length.
|
|
139
|
+
*
|
|
140
|
+
* Returns undefined when no list is found.
|
|
141
|
+
*/
|
|
142
|
+
export declare function parseAllowedModels(message: string): string[] | undefined;
|
|
143
|
+
/**
|
|
144
|
+
* Returns true when `message` looks like a model-access-denied response
|
|
145
|
+
* (LiteLLM "team not allowed", generic "not allowed to access model",
|
|
146
|
+
* or "team can only access models=[...]").
|
|
147
|
+
*/
|
|
148
|
+
export declare function isModelAccessDeniedMessage(message: string): boolean;
|
package/dist/types/errors.js
CHANGED
|
@@ -165,3 +165,97 @@ export class ModelAccessError extends BaseError {
|
|
|
165
165
|
this.requiredTier = requiredTier;
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Curator P1-1: thrown when a provider rejects a request because the
|
|
170
|
+
* caller's team / API key is not whitelisted for the requested model.
|
|
171
|
+
*
|
|
172
|
+
* LiteLLM's `team not allowed to access model. This team can only access
|
|
173
|
+
* models=['glm-latest', 'kimi-latest', ...]` is the canonical example —
|
|
174
|
+
* the list is parsed off the error body so callers / fallback orchestrators
|
|
175
|
+
* can choose a whitelisted alternative without scraping strings.
|
|
176
|
+
*/
|
|
177
|
+
export class ModelAccessDeniedError extends ProviderError {
|
|
178
|
+
requestedModel;
|
|
179
|
+
allowedModels;
|
|
180
|
+
code = "MODEL_ACCESS_DENIED";
|
|
181
|
+
constructor(message, options = {}) {
|
|
182
|
+
super(message, options.provider);
|
|
183
|
+
this.name = "ModelAccessDeniedError";
|
|
184
|
+
this.requestedModel = options.requestedModel;
|
|
185
|
+
this.allowedModels = options.allowedModels;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** Maximum body length we'll attempt to parse. Real provider error
|
|
189
|
+
* bodies are well under 10 KB; longer inputs are either truncated
|
|
190
|
+
* log output or a deliberate ReDoS attempt. */
|
|
191
|
+
const MAX_ALLOWED_MODELS_INPUT = 10_000;
|
|
192
|
+
/**
|
|
193
|
+
* Parse the `allowed_models` array out of a provider error message body.
|
|
194
|
+
* Currently targets the LiteLLM team-whitelist response shape:
|
|
195
|
+
*
|
|
196
|
+
* "team not allowed to access model. This team can only access
|
|
197
|
+
* models=['glm-latest', 'kimi-latest', 'open-large']"
|
|
198
|
+
*
|
|
199
|
+
* Implementation note: deliberately uses `indexOf`/`slice` instead of a
|
|
200
|
+
* single `/models\s*=\s*\[([^\]]*)\]/` regex. CodeQL flagged the latter
|
|
201
|
+
* as `js/polynomial-redos` because the `[^\]]*` greedy quantifier on
|
|
202
|
+
* library-supplied input can be exploited by a crafted long string. The
|
|
203
|
+
* indexOf/slice path is O(n) with no backtracking and we additionally
|
|
204
|
+
* cap the input length.
|
|
205
|
+
*
|
|
206
|
+
* Returns undefined when no list is found.
|
|
207
|
+
*/
|
|
208
|
+
export function parseAllowedModels(message) {
|
|
209
|
+
if (typeof message !== "string" || message.length === 0) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
if (message.length > MAX_ALLOWED_MODELS_INPUT) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
// Locate `models` keyword case-insensitively, then walk forward to
|
|
216
|
+
// confirm `=` and `[` markers — no regex backtracking.
|
|
217
|
+
const lower = message.toLowerCase();
|
|
218
|
+
let idx = lower.indexOf("models", 0);
|
|
219
|
+
while (idx !== -1) {
|
|
220
|
+
let cursor = idx + "models".length;
|
|
221
|
+
// Skip whitespace
|
|
222
|
+
while (cursor < message.length && /\s/.test(message[cursor])) {
|
|
223
|
+
cursor++;
|
|
224
|
+
}
|
|
225
|
+
if (message[cursor] !== "=") {
|
|
226
|
+
idx = lower.indexOf("models", idx + 1);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
cursor++;
|
|
230
|
+
while (cursor < message.length && /\s/.test(message[cursor])) {
|
|
231
|
+
cursor++;
|
|
232
|
+
}
|
|
233
|
+
if (message[cursor] !== "[") {
|
|
234
|
+
idx = lower.indexOf("models", idx + 1);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const open = cursor;
|
|
238
|
+
const close = message.indexOf("]", open + 1);
|
|
239
|
+
if (close === -1) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
const inside = message.slice(open + 1, close);
|
|
243
|
+
const items = inside
|
|
244
|
+
.split(",")
|
|
245
|
+
.map((s) => s.trim().replace(/^['"]|['"]$/g, ""))
|
|
246
|
+
.filter((s) => s.length > 0);
|
|
247
|
+
return items.length > 0 ? items : undefined;
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Returns true when `message` looks like a model-access-denied response
|
|
253
|
+
* (LiteLLM "team not allowed", generic "not allowed to access model",
|
|
254
|
+
* or "team can only access models=[...]").
|
|
255
|
+
*/
|
|
256
|
+
export function isModelAccessDeniedMessage(message) {
|
|
257
|
+
const lower = message.toLowerCase();
|
|
258
|
+
return ((lower.includes("team") && lower.includes("not allowed")) ||
|
|
259
|
+
lower.includes("team can only access") ||
|
|
260
|
+
/not\s+allowed\s+to\s+access\s+(this\s+)?model/i.test(message));
|
|
261
|
+
}
|
package/dist/types/generate.d.ts
CHANGED
|
@@ -447,6 +447,19 @@ export type GenerateOptions = {
|
|
|
447
447
|
* Unset providers fall through to instance credentials, then environment variables.
|
|
448
448
|
*/
|
|
449
449
|
credentials?: NeurolinkCredentials;
|
|
450
|
+
/**
|
|
451
|
+
* Curator P2-3: per-call fallback callback. Overrides any
|
|
452
|
+
* instance-level `providerFallback` set on `new NeuroLink({...})`.
|
|
453
|
+
*/
|
|
454
|
+
providerFallback?: (error: unknown) => Promise<{
|
|
455
|
+
provider?: string;
|
|
456
|
+
model?: string;
|
|
457
|
+
} | null>;
|
|
458
|
+
/**
|
|
459
|
+
* Curator P2-3: per-call ordered model chain. Overrides any
|
|
460
|
+
* instance-level `modelChain`. Tried in order on model-access-denied.
|
|
461
|
+
*/
|
|
462
|
+
modelChain?: string[];
|
|
450
463
|
/**
|
|
451
464
|
* Per-call memory control.
|
|
452
465
|
*
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -445,6 +445,19 @@ export type StreamOptions = {
|
|
|
445
445
|
* Unset providers fall through to instance credentials, then environment variables.
|
|
446
446
|
*/
|
|
447
447
|
credentials?: NeurolinkCredentials;
|
|
448
|
+
/**
|
|
449
|
+
* Curator P2-3: per-call fallback callback. Overrides any
|
|
450
|
+
* instance-level `providerFallback` set on `new NeuroLink({...})`.
|
|
451
|
+
*/
|
|
452
|
+
providerFallback?: (error: unknown) => Promise<{
|
|
453
|
+
provider?: string;
|
|
454
|
+
model?: string;
|
|
455
|
+
} | null>;
|
|
456
|
+
/**
|
|
457
|
+
* Curator P2-3: per-call ordered model chain. Overrides any
|
|
458
|
+
* instance-level `modelChain`. Tried in order on model-access-denied.
|
|
459
|
+
*/
|
|
460
|
+
modelChain?: string[];
|
|
448
461
|
/**
|
|
449
462
|
* Per-call memory control.
|
|
450
463
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.59.0",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
|
|
6
6
|
"author": {
|