@juspay/neurolink 9.54.1 → 9.54.3

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 (52) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +288 -288
  3. package/dist/cli/factories/commandFactory.js +43 -4
  4. package/dist/cli/utils/abortHandler.d.ts +22 -0
  5. package/dist/cli/utils/abortHandler.js +53 -0
  6. package/dist/core/baseProvider.d.ts +7 -1
  7. package/dist/core/baseProvider.js +19 -0
  8. package/dist/lib/core/baseProvider.d.ts +7 -1
  9. package/dist/lib/core/baseProvider.js +19 -0
  10. package/dist/lib/neurolink.js +17 -1
  11. package/dist/lib/providers/anthropic.js +1 -0
  12. package/dist/lib/providers/anthropicBaseProvider.js +1 -0
  13. package/dist/lib/providers/azureOpenai.js +1 -0
  14. package/dist/lib/providers/googleAiStudio.js +1 -0
  15. package/dist/lib/providers/googleVertex.d.ts +14 -0
  16. package/dist/lib/providers/googleVertex.js +51 -12
  17. package/dist/lib/providers/huggingFace.js +1 -0
  18. package/dist/lib/providers/litellm.js +1 -0
  19. package/dist/lib/providers/mistral.js +1 -0
  20. package/dist/lib/providers/openAI.js +1 -0
  21. package/dist/lib/providers/openRouter.js +1 -0
  22. package/dist/lib/providers/openaiCompatible.js +1 -0
  23. package/dist/lib/proxy/routingPolicy.d.ts +27 -17
  24. package/dist/lib/proxy/routingPolicy.js +53 -209
  25. package/dist/lib/server/routes/claudeProxyRoutes.js +35 -73
  26. package/dist/lib/types/proxyTypes.d.ts +9 -50
  27. package/dist/lib/types/streamTypes.d.ts +6 -0
  28. package/dist/lib/utils/messageBuilder.js +39 -6
  29. package/dist/lib/utils/toolCallRepair.d.ts +21 -0
  30. package/dist/lib/utils/toolCallRepair.js +298 -0
  31. package/dist/neurolink.js +17 -1
  32. package/dist/providers/anthropic.js +1 -0
  33. package/dist/providers/anthropicBaseProvider.js +1 -0
  34. package/dist/providers/azureOpenai.js +1 -0
  35. package/dist/providers/googleAiStudio.js +1 -0
  36. package/dist/providers/googleVertex.d.ts +14 -0
  37. package/dist/providers/googleVertex.js +51 -12
  38. package/dist/providers/huggingFace.js +1 -0
  39. package/dist/providers/litellm.js +1 -0
  40. package/dist/providers/mistral.js +1 -0
  41. package/dist/providers/openAI.js +1 -0
  42. package/dist/providers/openRouter.js +1 -0
  43. package/dist/providers/openaiCompatible.js +1 -0
  44. package/dist/proxy/routingPolicy.d.ts +27 -17
  45. package/dist/proxy/routingPolicy.js +53 -209
  46. package/dist/server/routes/claudeProxyRoutes.js +35 -73
  47. package/dist/types/proxyTypes.d.ts +9 -50
  48. package/dist/types/streamTypes.d.ts +6 -0
  49. package/dist/utils/messageBuilder.js +39 -6
  50. package/dist/utils/toolCallRepair.d.ts +21 -0
  51. package/dist/utils/toolCallRepair.js +297 -0
  52. package/package.json +1 -1
@@ -252,6 +252,7 @@ export class OpenRouterProvider extends BaseProvider {
252
252
  }),
253
253
  abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
254
254
  experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
255
+ experimental_repairToolCall: this.getToolCallRepairFn(options),
255
256
  onError: (event) => {
256
257
  const error = event.error;
257
258
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -195,6 +195,7 @@ export class OpenAICompatibleProvider extends BaseProvider {
195
195
  stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
196
196
  abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
197
197
  experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
198
+ experimental_repairToolCall: this.getToolCallRepairFn(options),
198
199
  onStepFinish: (event) => {
199
200
  this.handleToolExecutionStorage([...event.toolCalls], [...event.toolResults], options, new Date()).catch((error) => {
200
201
  logger.warn("[OpenAiCompatibleProvider] Failed to store tool executions", {
@@ -1,33 +1,43 @@
1
- import type { ClaudeProxyModelTier, ClaudeProxyRequestClass, ClaudeProxyRequestProfile, CooldownScope, CooldownSkippedAccount, FallbackEligibilityDecision, FallbackEntry, ParsedClaudeRequest, ProxyTranslationAttempt, ProxyTranslationPlan, RuntimeAccountState } from "../types/index.js";
2
- export type { ClaudeProxyModelTier, ClaudeProxyRequestClass, ClaudeProxyRequestProfile, CooldownScope, CooldownSkippedAccount, FallbackEligibilityDecision, ProxyTranslationAttempt, ProxyTranslationPlan, };
1
+ import type { ClaudeProxyModelTier, CooldownSkippedAccount, FallbackEntry, ParsedClaudeRequest, ProxyTranslationAttempt, ProxyTranslationPlan, RuntimeAccountState } from "../types/index.js";
2
+ export type { ClaudeProxyModelTier, ProxyTranslationAttempt, ProxyTranslationPlan, };
3
3
  export declare function inferClaudeProxyModelTier(modelName: string): ClaudeProxyModelTier;
4
- export declare function classifyClaudeProxyRequest(requestedModel: string, parsed: ParsedClaudeRequest): ClaudeProxyRequestProfile;
5
- export declare function getRequestClassCooldownKey(profile: ClaudeProxyRequestProfile): string;
6
- export declare function getModelTierCooldownKey(profile: ClaudeProxyRequestProfile): string;
7
- export declare function evaluateFallbackEligibility(profile: ClaudeProxyRequestProfile, candidate: {
8
- provider?: string;
9
- model?: string;
10
- }): FallbackEligibilityDecision;
4
+ /**
5
+ * Build a translation plan for a Claude-compatible proxy request.
6
+ * The plan lists the primary provider followed by eligible fallback targets.
7
+ * All configured fallback entries are always eligible — no contract-based gating.
8
+ * When no fallback chain is configured, an "auto-provider" entry is appended.
9
+ */
11
10
  export declare function buildProxyTranslationPlan(primary: {
12
11
  provider: string;
13
12
  model?: string;
14
- }, fallbackChain: FallbackEntry[], requestedModel: string, parsed: ParsedClaudeRequest): ProxyTranslationPlan;
15
- export declare function summarizeSkippedFallbacks(plan: Pick<ProxyTranslationPlan, "profile" | "skipped">): string | null;
16
- export declare function getActiveCooldownScope(state: RuntimeAccountState, profile: ClaudeProxyRequestProfile, now?: number): CooldownScope | null;
13
+ }, fallbackChain: FallbackEntry[], requestedModel: string, _parsed: ParsedClaudeRequest): ProxyTranslationPlan;
14
+ /**
15
+ * Check whether an account is currently cooling down.
16
+ * Returns the cooldown timestamp if active, null otherwise.
17
+ */
18
+ export declare function getAccountCooldownUntil(state: RuntimeAccountState, now?: number): number | null;
19
+ /**
20
+ * Partition accounts into eligible (no cooldown) and skipped (cooling down).
21
+ */
17
22
  export declare function partitionAccountsByCooldown<T extends {
18
23
  key: string;
19
- }>(accounts: T[], getState: (account: T) => RuntimeAccountState, profile: ClaudeProxyRequestProfile, now?: number): {
24
+ }>(accounts: T[], getState: (account: T) => RuntimeAccountState, now?: number): {
20
25
  eligible: T[];
21
26
  skipped: CooldownSkippedAccount<T>[];
22
27
  };
23
- export declare function applyRateLimitCooldownScope(args: {
28
+ /**
29
+ * Apply a rate-limit cooldown to an account.
30
+ * Uses simple exponential backoff with a floor and cap.
31
+ */
32
+ export declare function applyRateLimitCooldown(args: {
24
33
  state: RuntimeAccountState;
25
- profile: ClaudeProxyRequestProfile;
26
34
  retryAfterMs?: number;
27
35
  now?: number;
28
36
  capMs: number;
29
37
  }): {
30
38
  backoffMs: number;
31
- requestClassKey: string;
32
- modelTierKey: string;
33
39
  };
40
+ /**
41
+ * Clear cooldown state for an account after a successful request.
42
+ */
43
+ export declare function clearAccountCooldown(state: RuntimeAccountState): void;
@@ -1,9 +1,4 @@
1
- const STREAMING_CONVERSATIONAL_TOOL_THRESHOLD = 4;
2
- const STRONG_TOOL_FIDELITY_THRESHOLD = 8;
3
- const HIGH_TOOL_COUNT_THRESHOLD = 24;
4
1
  const DEFAULT_COOLDOWN_FLOOR_MS = 1_000;
5
- const HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS = 10_000;
6
- const HIGH_FIDELITY_COOLDOWN_FLOOR_MS = 300_000;
7
2
  export function inferClaudeProxyModelTier(modelName) {
8
3
  const normalized = modelName.toLowerCase();
9
4
  if (normalized.includes("opus")) {
@@ -17,101 +12,13 @@ export function inferClaudeProxyModelTier(modelName) {
17
12
  }
18
13
  return "other";
19
14
  }
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);
15
+ /**
16
+ * Build a translation plan for a Claude-compatible proxy request.
17
+ * The plan lists the primary provider followed by eligible fallback targets.
18
+ * All configured fallback entries are always eligible — no contract-based gating.
19
+ * When no fallback chain is configured, an "auto-provider" entry is appended.
20
+ */
21
+ export function buildProxyTranslationPlan(primary, fallbackChain, requestedModel, _parsed) {
115
22
  const attempts = [
116
23
  {
117
24
  provider: primary.provider,
@@ -119,141 +26,78 @@ export function buildProxyTranslationPlan(primary, fallbackChain, requestedModel
119
26
  label: `${primary.provider}/${primary.model ?? "unknown"}`,
120
27
  },
121
28
  ];
122
- const skipped = [];
123
29
  for (const fallback of fallbackChain) {
124
30
  if (fallback.provider === primary.provider &&
125
31
  fallback.model === primary.model) {
126
32
  continue;
127
33
  }
128
- const decision = evaluateFallbackEligibility(profile, fallback);
129
- if (!decision.eligible) {
130
- skipped.push(decision);
131
- continue;
132
- }
133
34
  attempts.push({
134
35
  provider: fallback.provider,
135
36
  model: fallback.model,
136
37
  label: `${fallback.provider}/${fallback.model}`,
137
38
  });
138
39
  }
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
- }
40
+ // Append auto-provider when no configured fallback chain exists,
41
+ // or when all configured entries were deduped (same as primary).
42
+ if (fallbackChain.length === 0 || attempts.length === 1) {
43
+ attempts.push({ label: "auto-provider" });
147
44
  }
148
45
  return {
149
- profile,
46
+ requestedModel,
47
+ modelTier: inferClaudeProxyModelTier(requestedModel),
150
48
  attempts,
151
- skipped,
49
+ skipped: [],
152
50
  };
153
51
  }
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
- };
52
+ // ---------------------------------------------------------------------------
53
+ // Simple per-account cooldown
54
+ // ---------------------------------------------------------------------------
55
+ /**
56
+ * Check whether an account is currently cooling down.
57
+ * Returns the cooldown timestamp if active, null otherwise.
58
+ */
59
+ export function getAccountCooldownUntil(state, now = Date.now()) {
60
+ if (state.coolingUntil && state.coolingUntil > now) {
61
+ return state.coolingUntil;
198
62
  }
199
- return longest;
63
+ return null;
200
64
  }
201
- export function partitionAccountsByCooldown(accounts, getState, profile, now = Date.now()) {
65
+ /**
66
+ * Partition accounts into eligible (no cooldown) and skipped (cooling down).
67
+ */
68
+ export function partitionAccountsByCooldown(accounts, getState, now = Date.now()) {
202
69
  const eligible = [];
203
70
  const skipped = [];
204
71
  for (const account of accounts) {
205
- const cooldown = getActiveCooldownScope(getState(account), profile, now);
206
- if (cooldown) {
207
- skipped.push({ account, cooldown });
72
+ const state = getState(account);
73
+ const until = getAccountCooldownUntil(state, now);
74
+ if (until !== null) {
75
+ skipped.push({
76
+ account,
77
+ cooldown: { until, backoffLevel: state.backoffLevel },
78
+ });
208
79
  continue;
209
80
  }
210
81
  eligible.push(account);
211
82
  }
212
- return {
213
- eligible,
214
- skipped,
215
- };
83
+ return { eligible, skipped };
216
84
  }
217
- export function applyRateLimitCooldownScope(args) {
85
+ /**
86
+ * Apply a rate-limit cooldown to an account.
87
+ * Uses simple exponential backoff with a floor and cap.
88
+ */
89
+ export function applyRateLimitCooldown(args) {
218
90
  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
- // High-tool-count-non-stream gets its own (lower) floor so that requests
225
- // recover faster once proper OAuth betas are forwarded. Check it first
226
- // because every >=24-tool request also satisfies requiresStrongToolFidelity
227
- // (threshold 8), which would otherwise shadow this branch.
228
- const floorMs = args.profile.isHighToolCountNonStream
229
- ? HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS
230
- : args.profile.modelTier === "opus" ||
231
- args.profile.requiresStrongToolFidelity
232
- ? HIGH_FIDELITY_COOLDOWN_FLOOR_MS
233
- : DEFAULT_COOLDOWN_FLOOR_MS;
234
- const baseCooldownMs = Math.max(args.retryAfterMs ?? 0, floorMs);
235
- const backoffMs = Math.min(baseCooldownMs * 2 ** scopedBackoffLevel, args.capMs);
236
- const until = now + backoffMs;
237
- args.state.requestClassCooldowns = {
238
- ...(args.state.requestClassCooldowns ?? {}),
239
- [requestClassKey]: Math.max(args.state.requestClassCooldowns?.[requestClassKey] ?? 0, until),
240
- };
241
- args.state.modelTierCooldowns = {
242
- ...(args.state.modelTierCooldowns ?? {}),
243
- [modelTierKey]: Math.max(args.state.modelTierCooldowns?.[modelTierKey] ?? 0, until),
244
- };
245
- args.state.requestClassBackoffLevels = {
246
- ...rcBackoffLevels,
247
- [requestClassKey]: (rcBackoffLevels[requestClassKey] ?? 0) + 1,
248
- };
249
- args.state.modelTierBackoffLevels = {
250
- ...mtBackoffLevels,
251
- [modelTierKey]: (mtBackoffLevels[modelTierKey] ?? 0) + 1,
252
- };
91
+ const baseCooldownMs = Math.max(args.retryAfterMs ?? 0, DEFAULT_COOLDOWN_FLOOR_MS);
92
+ const backoffMs = Math.min(baseCooldownMs * 2 ** args.state.backoffLevel, args.capMs);
93
+ args.state.coolingUntil = now + backoffMs;
253
94
  args.state.backoffLevel += 1;
254
- return {
255
- backoffMs,
256
- requestClassKey,
257
- modelTierKey,
258
- };
95
+ return { backoffMs };
96
+ }
97
+ /**
98
+ * Clear cooldown state for an account after a successful request.
99
+ */
100
+ export function clearAccountCooldown(state) {
101
+ state.coolingUntil = undefined;
102
+ state.backoffLevel = 0;
259
103
  }