@juspay/neurolink 9.42.1 → 9.44.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 +300 -300
- package/dist/cli/commands/mcp.js +15 -3
- package/dist/cli/commands/proxy.js +29 -6
- package/dist/core/baseProvider.js +12 -3
- package/dist/core/factory.js +4 -4
- package/dist/core/modules/ToolsManager.d.ts +1 -0
- package/dist/core/modules/ToolsManager.js +40 -42
- package/dist/core/toolEvents.d.ts +3 -0
- package/dist/core/toolEvents.js +7 -0
- package/dist/evaluation/scorers/scorerRegistry.js +3 -2
- package/dist/lib/core/baseProvider.js +12 -3
- package/dist/lib/core/factory.js +4 -4
- package/dist/lib/core/modules/ToolsManager.d.ts +1 -0
- package/dist/lib/core/modules/ToolsManager.js +40 -42
- package/dist/lib/core/toolEvents.d.ts +3 -0
- package/dist/lib/core/toolEvents.js +8 -0
- package/dist/lib/evaluation/scorers/scorerRegistry.js +3 -2
- package/dist/lib/neurolink.js +33 -19
- package/dist/lib/providers/googleNativeGemini3.d.ts +4 -0
- package/dist/lib/providers/googleNativeGemini3.js +39 -1
- package/dist/lib/providers/googleVertex.js +10 -2
- package/dist/lib/proxy/claudeFormat.js +2 -1
- package/dist/lib/proxy/proxyHealth.d.ts +17 -0
- package/dist/lib/proxy/proxyHealth.js +55 -0
- package/dist/lib/proxy/requestLogger.js +8 -3
- package/dist/lib/proxy/routingPolicy.d.ts +33 -0
- package/dist/lib/proxy/routingPolicy.js +255 -0
- package/dist/lib/proxy/snapshotPersistence.d.ts +2 -0
- package/dist/lib/proxy/snapshotPersistence.js +41 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +1 -9
- package/dist/lib/server/routes/claudeProxyRoutes.js +304 -219
- package/dist/lib/tasks/store/redisTaskStore.js +34 -16
- package/dist/lib/types/cli.d.ts +4 -0
- package/dist/lib/types/proxyTypes.d.ts +87 -0
- package/dist/lib/types/tools.d.ts +18 -0
- package/dist/lib/utils/schemaConversion.d.ts +1 -0
- package/dist/lib/utils/schemaConversion.js +3 -0
- package/dist/neurolink.js +33 -19
- package/dist/providers/googleNativeGemini3.d.ts +4 -0
- package/dist/providers/googleNativeGemini3.js +39 -1
- package/dist/providers/googleVertex.js +10 -2
- package/dist/proxy/claudeFormat.js +2 -1
- package/dist/proxy/proxyHealth.d.ts +17 -0
- package/dist/proxy/proxyHealth.js +54 -0
- package/dist/proxy/requestLogger.js +8 -3
- package/dist/proxy/routingPolicy.d.ts +33 -0
- package/dist/proxy/routingPolicy.js +254 -0
- package/dist/proxy/snapshotPersistence.d.ts +2 -0
- package/dist/proxy/snapshotPersistence.js +40 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -9
- package/dist/server/routes/claudeProxyRoutes.js +304 -219
- package/dist/tasks/store/redisTaskStore.js +34 -16
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/proxyTypes.d.ts +87 -0
- package/dist/types/tools.d.ts +18 -0
- package/dist/utils/schemaConversion.d.ts +1 -0
- package/dist/utils/schemaConversion.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
const STREAMING_CONVERSATIONAL_TOOL_THRESHOLD = 4;
|
|
2
|
+
const STRONG_TOOL_FIDELITY_THRESHOLD = 8;
|
|
3
|
+
const HIGH_TOOL_COUNT_THRESHOLD = 24;
|
|
4
|
+
const DEFAULT_COOLDOWN_FLOOR_MS = 1_000;
|
|
5
|
+
const HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS = 120_000;
|
|
6
|
+
const HIGH_FIDELITY_COOLDOWN_FLOOR_MS = 300_000;
|
|
7
|
+
export function inferClaudeProxyModelTier(modelName) {
|
|
8
|
+
const normalized = modelName.toLowerCase();
|
|
9
|
+
if (normalized.includes("opus")) {
|
|
10
|
+
return "opus";
|
|
11
|
+
}
|
|
12
|
+
if (normalized.includes("sonnet")) {
|
|
13
|
+
return "sonnet";
|
|
14
|
+
}
|
|
15
|
+
if (normalized.includes("haiku")) {
|
|
16
|
+
return "haiku";
|
|
17
|
+
}
|
|
18
|
+
return "other";
|
|
19
|
+
}
|
|
20
|
+
function detectToolHistory(parsed) {
|
|
21
|
+
return parsed.conversationMessages.some((message) => {
|
|
22
|
+
return (message.content.includes("[tool_use:") ||
|
|
23
|
+
message.content.includes("[tool_result:"));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function classifyClaudeProxyRequest(requestedModel, parsed) {
|
|
27
|
+
const toolCount = Object.keys(parsed.tools).length;
|
|
28
|
+
const hasImages = parsed.images.length > 0;
|
|
29
|
+
const hasThinking = !!parsed.thinkingConfig?.enabled;
|
|
30
|
+
const hasToolHistory = detectToolHistory(parsed);
|
|
31
|
+
const requiresSpecificTool = !!parsed.toolChoiceName;
|
|
32
|
+
const requiresToolUse = parsed.toolChoice === "required" || requiresSpecificTool || hasToolHistory;
|
|
33
|
+
const requiresStrongToolFidelity = toolCount >= STRONG_TOOL_FIDELITY_THRESHOLD ||
|
|
34
|
+
requiresSpecificTool ||
|
|
35
|
+
hasToolHistory;
|
|
36
|
+
const isHighToolCountNonStream = !parsed.stream && toolCount >= HIGH_TOOL_COUNT_THRESHOLD;
|
|
37
|
+
const isStreamingConversational = parsed.stream &&
|
|
38
|
+
!hasImages &&
|
|
39
|
+
toolCount <= STREAMING_CONVERSATIONAL_TOOL_THRESHOLD &&
|
|
40
|
+
!requiresStrongToolFidelity;
|
|
41
|
+
const classes = [];
|
|
42
|
+
if (hasImages) {
|
|
43
|
+
classes.push("multimodal");
|
|
44
|
+
}
|
|
45
|
+
if (isHighToolCountNonStream) {
|
|
46
|
+
classes.push("high-tool-count-non-stream-structured");
|
|
47
|
+
}
|
|
48
|
+
if (requiresStrongToolFidelity) {
|
|
49
|
+
classes.push("strong-tool-fidelity");
|
|
50
|
+
}
|
|
51
|
+
if (isStreamingConversational) {
|
|
52
|
+
classes.push("streaming-conversational");
|
|
53
|
+
}
|
|
54
|
+
if (classes.length === 0) {
|
|
55
|
+
classes.push("standard");
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
requestedModel,
|
|
59
|
+
modelTier: inferClaudeProxyModelTier(requestedModel),
|
|
60
|
+
primaryClass: classes[0],
|
|
61
|
+
classes,
|
|
62
|
+
stream: parsed.stream,
|
|
63
|
+
toolCount,
|
|
64
|
+
hasImages,
|
|
65
|
+
hasThinking,
|
|
66
|
+
hasToolHistory,
|
|
67
|
+
requiresToolUse,
|
|
68
|
+
requiresSpecificTool,
|
|
69
|
+
requiresStrongToolFidelity,
|
|
70
|
+
isHighToolCountNonStream,
|
|
71
|
+
isStreamingConversational,
|
|
72
|
+
isMultimodal: hasImages,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function getRequestClassCooldownKey(profile) {
|
|
76
|
+
return `${profile.primaryClass}:${profile.requestedModel.toLowerCase()}`;
|
|
77
|
+
}
|
|
78
|
+
export function getModelTierCooldownKey(profile) {
|
|
79
|
+
return profile.modelTier;
|
|
80
|
+
}
|
|
81
|
+
function getQualityGuardReason(profile, provider, _model) {
|
|
82
|
+
// Only gate auto-provider fallback (no explicit provider).
|
|
83
|
+
// Configured fallback-chain entries are always allowed through —
|
|
84
|
+
// let them attempt the request and fail naturally if the provider
|
|
85
|
+
// cannot handle it.
|
|
86
|
+
if (!provider) {
|
|
87
|
+
if (profile.modelTier === "opus" ||
|
|
88
|
+
profile.requiresStrongToolFidelity ||
|
|
89
|
+
profile.isHighToolCountNonStream) {
|
|
90
|
+
return "auto-provider fallback is disabled for requests that require contract preservation";
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
export function evaluateFallbackEligibility(profile, candidate) {
|
|
97
|
+
const policyBlockReason = getQualityGuardReason(profile, candidate.provider, candidate.model);
|
|
98
|
+
if (policyBlockReason) {
|
|
99
|
+
return {
|
|
100
|
+
provider: candidate.provider,
|
|
101
|
+
model: candidate.model,
|
|
102
|
+
eligible: false,
|
|
103
|
+
reason: policyBlockReason,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
provider: candidate.provider,
|
|
108
|
+
model: candidate.model,
|
|
109
|
+
eligible: true,
|
|
110
|
+
reason: "eligible",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function buildProxyTranslationPlan(primary, fallbackChain, requestedModel, parsed) {
|
|
114
|
+
const profile = classifyClaudeProxyRequest(requestedModel, parsed);
|
|
115
|
+
const attempts = [
|
|
116
|
+
{
|
|
117
|
+
provider: primary.provider,
|
|
118
|
+
model: primary.model,
|
|
119
|
+
label: `${primary.provider}/${primary.model ?? "unknown"}`,
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
const skipped = [];
|
|
123
|
+
for (const fallback of fallbackChain) {
|
|
124
|
+
if (fallback.provider === primary.provider &&
|
|
125
|
+
fallback.model === primary.model) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const decision = evaluateFallbackEligibility(profile, fallback);
|
|
129
|
+
if (!decision.eligible) {
|
|
130
|
+
skipped.push(decision);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
attempts.push({
|
|
134
|
+
provider: fallback.provider,
|
|
135
|
+
model: fallback.model,
|
|
136
|
+
label: `${fallback.provider}/${fallback.model}`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (fallbackChain.length === 0) {
|
|
140
|
+
const autoDecision = evaluateFallbackEligibility(profile, {});
|
|
141
|
+
if (autoDecision.eligible) {
|
|
142
|
+
attempts.push({ label: "auto-provider" });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
skipped.push(autoDecision);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
profile,
|
|
150
|
+
attempts,
|
|
151
|
+
skipped,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function summarizeSkippedFallbacks(plan) {
|
|
155
|
+
if (plan.skipped.length === 0) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const summary = plan.skipped
|
|
159
|
+
.map((decision) => {
|
|
160
|
+
const label = decision.provider
|
|
161
|
+
? `${decision.provider}/${decision.model ?? "unknown"}`
|
|
162
|
+
: "auto-provider";
|
|
163
|
+
return `${label}: ${decision.reason}`;
|
|
164
|
+
})
|
|
165
|
+
.join("; ");
|
|
166
|
+
return `Fallback policy preserved the requested ${plan.profile.primaryClass} contract by skipping ineligible targets. ${summary}`;
|
|
167
|
+
}
|
|
168
|
+
export function getActiveCooldownScope(state, profile, now = Date.now()) {
|
|
169
|
+
let longest = null;
|
|
170
|
+
const requestClassKey = getRequestClassCooldownKey(profile);
|
|
171
|
+
const requestClassUntil = state.requestClassCooldowns?.[requestClassKey] ?? undefined;
|
|
172
|
+
if (requestClassUntil && requestClassUntil > now) {
|
|
173
|
+
longest = {
|
|
174
|
+
scope: "request_class",
|
|
175
|
+
key: requestClassKey,
|
|
176
|
+
until: requestClassUntil,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const modelTierKey = getModelTierCooldownKey(profile);
|
|
180
|
+
const modelTierUntil = state.modelTierCooldowns?.[modelTierKey] ?? undefined;
|
|
181
|
+
if (modelTierUntil &&
|
|
182
|
+
modelTierUntil > now &&
|
|
183
|
+
modelTierUntil > (longest?.until ?? 0)) {
|
|
184
|
+
longest = {
|
|
185
|
+
scope: "model_tier",
|
|
186
|
+
key: modelTierKey,
|
|
187
|
+
until: modelTierUntil,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (state.coolingUntil &&
|
|
191
|
+
state.coolingUntil > now &&
|
|
192
|
+
state.coolingUntil > (longest?.until ?? 0)) {
|
|
193
|
+
longest = {
|
|
194
|
+
scope: "generic",
|
|
195
|
+
key: "generic",
|
|
196
|
+
until: state.coolingUntil,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return longest;
|
|
200
|
+
}
|
|
201
|
+
export function partitionAccountsByCooldown(accounts, getState, profile, now = Date.now()) {
|
|
202
|
+
const eligible = [];
|
|
203
|
+
const skipped = [];
|
|
204
|
+
for (const account of accounts) {
|
|
205
|
+
const cooldown = getActiveCooldownScope(getState(account), profile, now);
|
|
206
|
+
if (cooldown) {
|
|
207
|
+
skipped.push({ account, cooldown });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
eligible.push(account);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
eligible,
|
|
214
|
+
skipped,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function applyRateLimitCooldownScope(args) {
|
|
218
|
+
const now = args.now ?? Date.now();
|
|
219
|
+
const requestClassKey = getRequestClassCooldownKey(args.profile);
|
|
220
|
+
const modelTierKey = getModelTierCooldownKey(args.profile);
|
|
221
|
+
const rcBackoffLevels = args.state.requestClassBackoffLevels ?? {};
|
|
222
|
+
const mtBackoffLevels = args.state.modelTierBackoffLevels ?? {};
|
|
223
|
+
const scopedBackoffLevel = Math.max(rcBackoffLevels[requestClassKey] ?? 0, mtBackoffLevels[modelTierKey] ?? 0);
|
|
224
|
+
const floorMs = args.profile.modelTier === "opus" || args.profile.requiresStrongToolFidelity
|
|
225
|
+
? HIGH_FIDELITY_COOLDOWN_FLOOR_MS
|
|
226
|
+
: args.profile.isHighToolCountNonStream
|
|
227
|
+
? HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS
|
|
228
|
+
: DEFAULT_COOLDOWN_FLOOR_MS;
|
|
229
|
+
const baseCooldownMs = Math.max(args.retryAfterMs ?? 0, floorMs);
|
|
230
|
+
const backoffMs = Math.min(baseCooldownMs * 2 ** scopedBackoffLevel, args.capMs);
|
|
231
|
+
const until = now + backoffMs;
|
|
232
|
+
args.state.requestClassCooldowns = {
|
|
233
|
+
...(args.state.requestClassCooldowns ?? {}),
|
|
234
|
+
[requestClassKey]: Math.max(args.state.requestClassCooldowns?.[requestClassKey] ?? 0, until),
|
|
235
|
+
};
|
|
236
|
+
args.state.modelTierCooldowns = {
|
|
237
|
+
...(args.state.modelTierCooldowns ?? {}),
|
|
238
|
+
[modelTierKey]: Math.max(args.state.modelTierCooldowns?.[modelTierKey] ?? 0, until),
|
|
239
|
+
};
|
|
240
|
+
args.state.requestClassBackoffLevels = {
|
|
241
|
+
...rcBackoffLevels,
|
|
242
|
+
[requestClassKey]: (rcBackoffLevels[requestClassKey] ?? 0) + 1,
|
|
243
|
+
};
|
|
244
|
+
args.state.modelTierBackoffLevels = {
|
|
245
|
+
...mtBackoffLevels,
|
|
246
|
+
[modelTierKey]: (mtBackoffLevels[modelTierKey] ?? 0) + 1,
|
|
247
|
+
};
|
|
248
|
+
args.state.backoffLevel += 1;
|
|
249
|
+
return {
|
|
250
|
+
backoffMs,
|
|
251
|
+
requestClassKey,
|
|
252
|
+
modelTierKey,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
const writeLocks = new Map();
|
|
5
|
+
async function writeSnapshotFile(targetPath, payload, mode) {
|
|
6
|
+
const dir = dirname(targetPath);
|
|
7
|
+
const baseName = basename(targetPath);
|
|
8
|
+
await mkdir(dir, { recursive: true });
|
|
9
|
+
const tempPath = join(dir, `.${baseName}.${process.pid}.${randomUUID()}.tmp`);
|
|
10
|
+
try {
|
|
11
|
+
await writeFile(tempPath, payload, { mode });
|
|
12
|
+
await rename(tempPath, targetPath);
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
await rm(tempPath, { force: true }).catch(() => {
|
|
16
|
+
// Best-effort cleanup only.
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function writeJsonSnapshotAtomically(targetPath, data, mode = 0o600) {
|
|
21
|
+
const payload = JSON.stringify(data, null, 2);
|
|
22
|
+
const previous = writeLocks.get(targetPath) ?? Promise.resolve();
|
|
23
|
+
const next = previous
|
|
24
|
+
.catch(() => {
|
|
25
|
+
// Preserve the queue even if a previous write failed.
|
|
26
|
+
})
|
|
27
|
+
.then(() => writeSnapshotFile(targetPath, payload, mode));
|
|
28
|
+
writeLocks.set(targetPath, next);
|
|
29
|
+
try {
|
|
30
|
+
await next;
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
if (writeLocks.get(targetPath) === next) {
|
|
34
|
+
writeLocks.delete(targetPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function clearSnapshotWriteLocksForTests() {
|
|
39
|
+
writeLocks.clear();
|
|
40
|
+
}
|
|
@@ -12,11 +12,6 @@
|
|
|
12
12
|
import type { ModelRouter } from "../../proxy/modelRouter.js";
|
|
13
13
|
import type { ParsedClaudeRequest } from "../../types/index.js";
|
|
14
14
|
import type { RouteGroup } from "../types.js";
|
|
15
|
-
type ProxyTranslationAttempt = {
|
|
16
|
-
provider?: string;
|
|
17
|
-
model?: string;
|
|
18
|
-
label: string;
|
|
19
|
-
};
|
|
20
15
|
/**
|
|
21
16
|
* Create Claude-compatible proxy routes.
|
|
22
17
|
*
|
|
@@ -28,6 +23,7 @@ type ProxyTranslationAttempt = {
|
|
|
28
23
|
* @returns RouteGroup with Claude-compatible endpoints.
|
|
29
24
|
*/
|
|
30
25
|
export declare function createClaudeProxyRoutes(modelRouter?: ModelRouter, basePath?: string, accountStrategy?: "round-robin" | "fill-first", passthroughMode?: boolean): RouteGroup;
|
|
26
|
+
export declare function getTransientSameAccountRetryDelayMs(retryNumber: number): number;
|
|
31
27
|
type ParsedClaudeError = {
|
|
32
28
|
errorType?: string;
|
|
33
29
|
message?: string;
|
|
@@ -44,10 +40,6 @@ export declare function buildProxyFallbackOptions(parsed: ParsedClaudeRequest, o
|
|
|
44
40
|
provider?: string;
|
|
45
41
|
model?: string;
|
|
46
42
|
}): Record<string, unknown>;
|
|
47
|
-
export declare function buildProxyTranslationAttempts(primary: {
|
|
48
|
-
provider: string;
|
|
49
|
-
model?: string;
|
|
50
|
-
}, modelRouter?: ModelRouter, parsed?: Pick<ParsedClaudeRequest, "images" | "thinkingConfig">): ProxyTranslationAttempt[];
|
|
51
43
|
/**
|
|
52
44
|
* Detect transient upstream failures that should trigger account/provider failover.
|
|
53
45
|
*
|