@juspay/neurolink 12.12.0 → 12.12.2
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 +374 -374
- package/dist/core/nativeGenerateLoop.js +40 -2
- package/dist/providers/openaiChatCompletionsBase.d.ts +0 -3
- package/dist/providers/openaiChatCompletionsBase.js +42 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -1
- package/dist/server/routes/claudeProxyRoutes.js +82 -18
- package/dist/types/proxy.d.ts +7 -1
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* share this: the v3 result shape (`content` parts, `finishReason`, `usage`) is
|
|
14
14
|
* the same across Anthropic, the OpenAI-compatible family and SageMaker.
|
|
15
15
|
*/
|
|
16
|
+
import { logger } from "../utils/logger.js";
|
|
16
17
|
import { guardToolExecutor } from "./toolExecutionGuards.js";
|
|
17
18
|
/**
|
|
18
19
|
* Narrow a model handle to the delegating shape this loop drives.
|
|
@@ -104,10 +105,19 @@ export async function runNativeGenerateLoop(args, toolExecutionSummaries) {
|
|
|
104
105
|
let steps = 0;
|
|
105
106
|
// One bounded recovery re-ask per turn; see the empty-tool-calls branch.
|
|
106
107
|
let reasked = false;
|
|
108
|
+
// True only while the NEXT step is the recovery re-ask. `reasked` stays set
|
|
109
|
+
// for the rest of the turn, so it cannot distinguish "this step is the
|
|
110
|
+
// re-ask" from "the re-ask already happened" — and degrading a later,
|
|
111
|
+
// unrelated failure would swallow a real error.
|
|
112
|
+
let reaskPending = false;
|
|
113
|
+
// The turn as it stood before the re-ask. The re-ask is a bonus request on
|
|
114
|
+
// top of a call that already produced a result, so if it fails the honest
|
|
115
|
+
// answer is that result — not a thrown turn.
|
|
116
|
+
let preReask;
|
|
107
117
|
const hasTools = Boolean(args.tools && args.tools.length > 0);
|
|
108
118
|
for (let step = 0; step < args.maxSteps; step++) {
|
|
109
119
|
steps = step + 1;
|
|
110
|
-
const
|
|
120
|
+
const runThisStep = () => args.runStep(() => args.doGenerate({
|
|
111
121
|
prompt: args.conversation,
|
|
112
122
|
...(args.tools && args.tools.length > 0 ? { tools: args.tools } : {}),
|
|
113
123
|
// The v3 call option is an OBJECT — `{ type: "none" }`. Passing the
|
|
@@ -120,7 +130,9 @@ export async function runNativeGenerateLoop(args, toolExecutionSummaries) {
|
|
|
120
130
|
: args.toolChoice !== undefined
|
|
121
131
|
? { toolChoice: args.toolChoice }
|
|
122
132
|
: {}),
|
|
123
|
-
...(args.responseFormat
|
|
133
|
+
...(args.responseFormat
|
|
134
|
+
? { responseFormat: args.responseFormat }
|
|
135
|
+
: {}),
|
|
124
136
|
...(args.providerOptions
|
|
125
137
|
? { providerOptions: args.providerOptions }
|
|
126
138
|
: {}),
|
|
@@ -132,6 +144,30 @@ export async function runNativeGenerateLoop(args, toolExecutionSummaries) {
|
|
|
132
144
|
: {}),
|
|
133
145
|
...(args.abortSignal ? { abortSignal: args.abortSignal } : {}),
|
|
134
146
|
}));
|
|
147
|
+
let res;
|
|
148
|
+
try {
|
|
149
|
+
res = await runThisStep();
|
|
150
|
+
}
|
|
151
|
+
catch (stepError) {
|
|
152
|
+
// Ported from GenerationHandler.recoverEmptyToolCallsFinish's catch: a
|
|
153
|
+
// failed re-ask hands back the turn that already succeeded rather than
|
|
154
|
+
// turning a degraded turn into a thrown one. The native port made the
|
|
155
|
+
// re-ask a `continue`, so the failure surfaced from the NEXT step's
|
|
156
|
+
// doGenerate and propagated instead.
|
|
157
|
+
if (reaskPending && preReask) {
|
|
158
|
+
logger.warn("toolChoice: none re-ask failed; returning the original result", {
|
|
159
|
+
error: stepError instanceof Error
|
|
160
|
+
? stepError.message
|
|
161
|
+
: String(stepError),
|
|
162
|
+
});
|
|
163
|
+
text = preReask.text;
|
|
164
|
+
finishReason = preReask.finishReason;
|
|
165
|
+
rawFinishReason = preReask.rawFinishReason;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
throw stepError;
|
|
169
|
+
}
|
|
170
|
+
reaskPending = false;
|
|
135
171
|
const parts = asParts(res.content);
|
|
136
172
|
// Each step REPLACES the text rather than appending: the final step's
|
|
137
173
|
// answer is the turn's answer, matching what generateText reported.
|
|
@@ -177,6 +213,8 @@ export async function runNativeGenerateLoop(args, toolExecutionSummaries) {
|
|
|
177
213
|
step + 1 < args.maxSteps;
|
|
178
214
|
if (emptyToolCallsFinish) {
|
|
179
215
|
reasked = true;
|
|
216
|
+
reaskPending = true;
|
|
217
|
+
preReask = { text, finishReason, rawFinishReason };
|
|
180
218
|
args.conversation.push({ role: "assistant", content: parts });
|
|
181
219
|
continue;
|
|
182
220
|
}
|
|
@@ -20,9 +20,6 @@
|
|
|
20
20
|
import type { AIProviderName } from "../constants/enums.js";
|
|
21
21
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
22
22
|
import type { LanguageModel, OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatResponseFormat, OpenAICompatStreamLifecycleListeners, Schema, EnhancedGenerateResult, TextGenerationOptions, ValidationSchema, StreamOptions, StreamResult, ZodUnknownSchema } from "../types/index.js";
|
|
23
|
-
/**
|
|
24
|
-
* Abstract HTTP+SSE provider for OpenAI chat-completions-shaped endpoints.
|
|
25
|
-
*/
|
|
26
23
|
export declare abstract class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
27
24
|
protected config: {
|
|
28
25
|
baseURL: string;
|
|
@@ -36,6 +36,7 @@ import { resolveRequestKind } from "../core/resolveRequestKind.js";
|
|
|
36
36
|
import { appendJsonSchemaInstruction, hasNativeDoGenerate, runNativeGenerateLoop, } from "../core/nativeGenerateLoop.js";
|
|
37
37
|
import { resolveToolExecutionRecords } from "../core/toolExecutionRecorder.js";
|
|
38
38
|
import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
|
|
39
|
+
import { coerceJsonToSchema, schemaAccepts } from "../utils/json/coerce.js";
|
|
39
40
|
import { resolveToolChoice } from "../utils/toolChoice.js";
|
|
40
41
|
import { transformToolExecutions } from "../utils/transformationUtils.js";
|
|
41
42
|
import { withProviderRetry } from "../utils/providerRetry.js";
|
|
@@ -52,6 +53,19 @@ const WINDOW_FIT_MARGIN_TOKENS = 512;
|
|
|
52
53
|
/**
|
|
53
54
|
* Abstract HTTP+SSE provider for OpenAI chat-completions-shaped endpoints.
|
|
54
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* Did the model's text yield an object the caller's schema accepts?
|
|
58
|
+
*
|
|
59
|
+
* This is the trigger for the prompt-side structured-output fallback. It asks
|
|
60
|
+
* the question the ai-package's structured-output parser used to ask by
|
|
61
|
+
* throwing: did the native `response_format` attempt actually produce the
|
|
62
|
+
* object. A schema we cannot validate with accepts everything, so an unknown
|
|
63
|
+
* schema never forces a pointless second request.
|
|
64
|
+
*/
|
|
65
|
+
const yieldsSchemaValidObject = (text, schema) => {
|
|
66
|
+
const coerced = coerceJsonToSchema(text, schema);
|
|
67
|
+
return coerced !== null && schemaAccepts(schema, coerced.structuredData);
|
|
68
|
+
};
|
|
55
69
|
export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
56
70
|
config;
|
|
57
71
|
resolvedModel;
|
|
@@ -855,14 +869,42 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
855
869
|
logger.warn(`[${this.providerName}] provider rejected response_format — retrying with the schema in the system prompt`, { provider: this.providerName, model: modelId });
|
|
856
870
|
loop = await runLoop(appendJsonSchemaInstruction(conversation, responseFormat.schema), undefined);
|
|
857
871
|
}
|
|
872
|
+
// The vendor can also IGNORE `response_format` and answer in prose without
|
|
873
|
+
// erroring at all — GMI Cloud's MiniMax endpoint does exactly that, and it
|
|
874
|
+
// is the case the fallback was written for. On the ai-package path the
|
|
875
|
+
// structured-output parser threw on the unparseable answer, so the catch
|
|
876
|
+
// above was reached; the native loop has no such parser, so the silent
|
|
877
|
+
// case sailed through and handed the caller prose. Same recovery, keyed on
|
|
878
|
+
// the result rather than on an exception.
|
|
879
|
+
if (responseFormat !== undefined &&
|
|
880
|
+
options.schema !== undefined &&
|
|
881
|
+
!yieldsSchemaValidObject(loop.text, options.schema)) {
|
|
882
|
+
logger.warn(`[${this.providerName}] response_format did not yield a schema-valid object — retrying with the schema in the system prompt`, { provider: this.providerName, model: modelId });
|
|
883
|
+
loop = await runLoop(appendJsonSchemaInstruction(conversation, responseFormat.schema), undefined);
|
|
884
|
+
}
|
|
858
885
|
const { text, finishReason, toolsUsed } = loop;
|
|
859
886
|
const inputTokens = loop.inputTokens;
|
|
860
887
|
const outputTokens = loop.outputTokens;
|
|
888
|
+
// stopReason / stepsUsed parity with the other native loops (Vertex
|
|
889
|
+
// Gemini / Claude / Bedrock) and with the ai-package path this replaced.
|
|
890
|
+
// Without them a consumer cannot tell a completed turn from one the step
|
|
891
|
+
// cap truncated: the turn that ends on a `tool-calls` finish with the
|
|
892
|
+
// budget spent is exactly the case the caller configured `maxSteps` to
|
|
893
|
+
// bound, and reporting it as a plain completion hides that.
|
|
894
|
+
const stepsUsed = loop.steps;
|
|
895
|
+
const stopReason = stepsUsed >= (options.maxSteps || DEFAULT_MAX_STEPS) &&
|
|
896
|
+
finishReason === "tool-calls"
|
|
897
|
+
? "step-cap"
|
|
898
|
+
: finishReason === "error"
|
|
899
|
+
? "provider-error"
|
|
900
|
+
: "completed";
|
|
861
901
|
const enhanced = {
|
|
862
902
|
content: text,
|
|
863
903
|
provider: this.providerName,
|
|
864
904
|
model: modelId,
|
|
865
905
|
finishReason,
|
|
906
|
+
stopReason,
|
|
907
|
+
stepsUsed,
|
|
866
908
|
usage: {
|
|
867
909
|
input: inputTokens,
|
|
868
910
|
output: outputTokens,
|
|
@@ -55,7 +55,7 @@ declare function resetEpochToMs(resetEpoch: number | undefined, now: number): nu
|
|
|
55
55
|
* burst / acceleration limit) is transient: honor retry-after as a floor,
|
|
56
56
|
* allow a couple of jittered same-account retries, then a short cooldown.
|
|
57
57
|
*/
|
|
58
|
-
declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: number, now: number, unifiedStatus?: string | undefined, policy?: ProxyOveragePolicy): AccountCooldownPlan;
|
|
58
|
+
declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: number, now: number, unifiedStatus?: string | undefined, policy?: ProxyOveragePolicy, requestedModel?: string): AccountCooldownPlan;
|
|
59
59
|
/**
|
|
60
60
|
* Reconcile quota-backed cooldowns against a fresh provider observation. A
|
|
61
61
|
* rejected window parks the account until its reset; an allowed observation for
|
|
@@ -553,6 +553,45 @@ function clampCooldownUntil(untilMs, now, reason) {
|
|
|
553
553
|
const ceiling = Math.min(MAX_COOLDOWN_MS, (reason && MAX_COOLDOWN_MS_BY_REASON[reason]) ?? MAX_COOLDOWN_MS);
|
|
554
554
|
return Math.min(Math.max(untilMs, now + MIN_COOLDOWN_MS), now + ceiling);
|
|
555
555
|
}
|
|
556
|
+
function isAllowedQuotaStatus(status) {
|
|
557
|
+
return status?.trim().toLowerCase() === "allowed";
|
|
558
|
+
}
|
|
559
|
+
function isScopedWindowExhausted(window, now) {
|
|
560
|
+
if (!window || resetEpochToMs(window.resetsAt, now) === undefined) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
return (window.status?.trim().toLowerCase() === "rejected" ||
|
|
564
|
+
(window.used ?? 0) >= 1);
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Anthropic represents some model-specific limits through a rejected top-level
|
|
568
|
+
* unified status. The scope window is the authoritative discriminator: only
|
|
569
|
+
* treat that response as model-scoped when both account-wide windows remain
|
|
570
|
+
* explicitly allowed and the requested model's window is exhausted.
|
|
571
|
+
*/
|
|
572
|
+
function getScopedOnlyExhaustion(quota, requestedModel, now, policy = overagePolicy) {
|
|
573
|
+
if (!quota ||
|
|
574
|
+
!requestedModel ||
|
|
575
|
+
!isAllowedQuotaStatus(quota.sessionStatus) ||
|
|
576
|
+
!isAllowedQuotaStatus(quota.weeklyStatus) ||
|
|
577
|
+
isOverageUsable(quota, policy)) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
const scopedWindow = matchScopedQuotaWindow(quota, requestedModel, now);
|
|
581
|
+
return isScopedWindowExhausted(scopedWindow, now) ? scopedWindow : null;
|
|
582
|
+
}
|
|
583
|
+
function hasScopedOnlyExhaustion(quota, now, policy = overagePolicy) {
|
|
584
|
+
if (!isAllowedQuotaStatus(quota.sessionStatus) ||
|
|
585
|
+
!isAllowedQuotaStatus(quota.weeklyStatus) ||
|
|
586
|
+
isOverageUsable(quota, policy)) {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
return (quota.windows ?? []).some((window) => window.kind === "weekly_scoped" &&
|
|
590
|
+
typeof window.scopeModel === "string" &&
|
|
591
|
+
now - scopedWindowObservedAt(quota, window) <=
|
|
592
|
+
QUOTA_SNAPSHOT_FRESHNESS_MS &&
|
|
593
|
+
isScopedWindowExhausted(window, now));
|
|
594
|
+
}
|
|
556
595
|
/**
|
|
557
596
|
* Decide how to cool an account after a genuine (non-anti-abuse) 429.
|
|
558
597
|
*
|
|
@@ -567,13 +606,14 @@ function clampCooldownUntil(untilMs, now, reason) {
|
|
|
567
606
|
* burst / acceleration limit) is transient: honor retry-after as a floor,
|
|
568
607
|
* allow a couple of jittered same-account retries, then a short cooldown.
|
|
569
608
|
*/
|
|
570
|
-
function planCooldownFor429(quota, retryAfterMs, now, unifiedStatus = quota?.unifiedStatus, policy = overagePolicy) {
|
|
609
|
+
function planCooldownFor429(quota, retryAfterMs, now, unifiedStatus = quota?.unifiedStatus, policy = overagePolicy, requestedModel) {
|
|
571
610
|
// Weekly exhaustion takes precedence — it's the longest, hardest ceiling.
|
|
572
611
|
if (quota && quota.weeklyStatus === "rejected") {
|
|
573
612
|
const reset = resetEpochToMs(quota.weeklyResetAt, now) ??
|
|
574
613
|
(retryAfterMs > 0 ? now + retryAfterMs : now + DEFAULT_COOLING_PERIOD_MS);
|
|
575
614
|
return {
|
|
576
615
|
reason: "weekly",
|
|
616
|
+
scope: "account",
|
|
577
617
|
coolingUntil: clampCooldownUntil(reset, now, "weekly"),
|
|
578
618
|
rotateImmediately: true,
|
|
579
619
|
};
|
|
@@ -584,10 +624,25 @@ function planCooldownFor429(quota, retryAfterMs, now, unifiedStatus = quota?.uni
|
|
|
584
624
|
(retryAfterMs > 0 ? now + retryAfterMs : now + DEFAULT_COOLING_PERIOD_MS);
|
|
585
625
|
return {
|
|
586
626
|
reason: "session",
|
|
627
|
+
scope: "account",
|
|
587
628
|
coolingUntil: clampCooldownUntil(reset, now, "session"),
|
|
588
629
|
rotateImmediately: true,
|
|
589
630
|
};
|
|
590
631
|
}
|
|
632
|
+
const scopedOnlyExhaustion = getScopedOnlyExhaustion(quota, requestedModel, now, policy);
|
|
633
|
+
if (scopedOnlyExhaustion) {
|
|
634
|
+
const reset = resetEpochToMs(scopedOnlyExhaustion.resetsAt, now) ??
|
|
635
|
+
(retryAfterMs > 0 ? now + retryAfterMs : now + DEFAULT_HARD_COOLDOWN_MS);
|
|
636
|
+
return {
|
|
637
|
+
// Keep the provider's top-level classification for logs, but do not
|
|
638
|
+
// persist it as an account cooldown. The scoped quota window gates only
|
|
639
|
+
// this model on subsequent requests.
|
|
640
|
+
reason: "unified",
|
|
641
|
+
scope: "model",
|
|
642
|
+
coolingUntil: reset,
|
|
643
|
+
rotateImmediately: true,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
591
646
|
// Anthropic may reject the authoritative top-level unified limit while both
|
|
592
647
|
// 5h and 7d sub-window statuses still say "allowed". Treating this as a
|
|
593
648
|
// transient burst retries a known-exhausted account and delays failover.
|
|
@@ -595,6 +650,7 @@ function planCooldownFor429(quota, retryAfterMs, now, unifiedStatus = quota?.uni
|
|
|
595
650
|
const reset = retryAfterMs > 0 ? now + retryAfterMs : now + DEFAULT_HARD_COOLDOWN_MS;
|
|
596
651
|
return {
|
|
597
652
|
reason: "unified",
|
|
653
|
+
scope: "account",
|
|
598
654
|
coolingUntil: clampCooldownUntil(reset, now, "unified"),
|
|
599
655
|
rotateImmediately: true,
|
|
600
656
|
};
|
|
@@ -605,6 +661,7 @@ function planCooldownFor429(quota, retryAfterMs, now, unifiedStatus = quota?.uni
|
|
|
605
661
|
const base = retryAfterMs > 0 ? retryAfterMs : DEFAULT_COOLING_PERIOD_MS;
|
|
606
662
|
return {
|
|
607
663
|
reason: "transient",
|
|
664
|
+
scope: "account",
|
|
608
665
|
coolingUntil: now +
|
|
609
666
|
Math.max(MIN_COOLDOWN_MS, Math.min(base, TRANSIENT_MAX_COOLDOWN_MS)),
|
|
610
667
|
rotateImmediately: false,
|
|
@@ -622,6 +679,7 @@ function minutesUntil(untilMs, now) {
|
|
|
622
679
|
*/
|
|
623
680
|
function reconcileCooldownFromQuota(state, quota, now, policy = overagePolicy) {
|
|
624
681
|
const overageAvailable = isOverageUsable(quota, policy);
|
|
682
|
+
const scopedOnlyExhaustion = hasScopedOnlyExhaustion(quota, now, policy);
|
|
625
683
|
let until;
|
|
626
684
|
let reason;
|
|
627
685
|
if (quota.weeklyStatus === "rejected") {
|
|
@@ -646,7 +704,8 @@ function reconcileCooldownFromQuota(state, quota, now, policy = overagePolicy) {
|
|
|
646
704
|
}
|
|
647
705
|
else if (until === undefined &&
|
|
648
706
|
quota.unifiedStatus === "rejected" &&
|
|
649
|
-
!overageAvailable
|
|
707
|
+
!overageAvailable &&
|
|
708
|
+
!scopedOnlyExhaustion) {
|
|
650
709
|
until = now + DEFAULT_HARD_COOLDOWN_MS;
|
|
651
710
|
reason = "unified";
|
|
652
711
|
}
|
|
@@ -656,7 +715,7 @@ function reconcileCooldownFromQuota(state, quota, now, policy = overagePolicy) {
|
|
|
656
715
|
(state.coolingReason === "session" &&
|
|
657
716
|
quota.sessionStatus === "allowed") ||
|
|
658
717
|
(state.coolingReason === "unified" &&
|
|
659
|
-
quota.unifiedStatus === "allowed"));
|
|
718
|
+
(quota.unifiedStatus === "allowed" || scopedOnlyExhaustion)));
|
|
660
719
|
if (recoveredQuotaCooldown && state.coolingUntil) {
|
|
661
720
|
const previousCoolingUntil = state.coolingUntil;
|
|
662
721
|
const previousCoolingReason = state.coolingReason;
|
|
@@ -4348,13 +4407,14 @@ async function handleAnthropicStreamingSuccessResponse(args) {
|
|
|
4348
4407
|
const quota = parseQuotaHeaders(responseHeaders, { model: body.model });
|
|
4349
4408
|
const now = Date.now();
|
|
4350
4409
|
if (isRateLimit) {
|
|
4351
|
-
const cooldownPlan = planCooldownFor429(quota, parseRetryAfterMs(responseHeaders["retry-after"] ?? null), now, getUnifiedRateLimitStatus(responseHeaders));
|
|
4410
|
+
const cooldownPlan = planCooldownFor429(quota, parseRetryAfterMs(responseHeaders["retry-after"] ?? null), now, getUnifiedRateLimitStatus(responseHeaders), overagePolicy, typeof body.model === "string" ? body.model : undefined);
|
|
4352
4411
|
accountState.quota = quota
|
|
4353
4412
|
? mergeQuotaSnapshot(accountState.quota, quota)
|
|
4354
4413
|
: accountState.quota;
|
|
4355
4414
|
const rateLimitKind = cooldownPlan.reason === "transient" ? "transient" : "quota";
|
|
4356
|
-
if (
|
|
4357
|
-
|
|
4415
|
+
if (cooldownPlan.scope === "account" &&
|
|
4416
|
+
(!accountState.coolingUntil ||
|
|
4417
|
+
cooldownPlan.coolingUntil > accountState.coolingUntil)) {
|
|
4358
4418
|
accountState.coolingUntil = cooldownPlan.coolingUntil;
|
|
4359
4419
|
accountState.coolingReason = cooldownPlan.reason;
|
|
4360
4420
|
await saveAccountCooldown(account.key, cooldownPlan.coolingUntil, cooldownPlan.reason).catch(() => {
|
|
@@ -5132,7 +5192,7 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
5132
5192
|
if (retryQuota429) {
|
|
5133
5193
|
accountState.quota = mergeQuotaSnapshot(accountState.quota, retryQuota429);
|
|
5134
5194
|
}
|
|
5135
|
-
const retryPlan = planCooldownFor429(retryQuota429, parseRetryAfterMs(retryRespHeaders["retry-after"] ?? null), nowRetry, getUnifiedRateLimitStatus(retryRespHeaders));
|
|
5195
|
+
const retryPlan = planCooldownFor429(retryQuota429, parseRetryAfterMs(retryRespHeaders["retry-after"] ?? null), nowRetry, getUnifiedRateLimitStatus(retryRespHeaders), overagePolicy, typeof body.model === "string" ? body.model : undefined);
|
|
5136
5196
|
const rateLimitKind = retryPlan.reason === "transient" ? "transient" : "quota";
|
|
5137
5197
|
recordAttemptError(account.label, account.type, retryStatus, rateLimitKind);
|
|
5138
5198
|
retryLogAttempt(429, "rate_limit_error", retryBody, {
|
|
@@ -5140,8 +5200,9 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
5140
5200
|
rateLimitKind,
|
|
5141
5201
|
cooldownReason: retryPlan.reason,
|
|
5142
5202
|
});
|
|
5143
|
-
if (
|
|
5144
|
-
|
|
5203
|
+
if (retryPlan.scope === "account" &&
|
|
5204
|
+
(!accountState.coolingUntil ||
|
|
5205
|
+
retryPlan.coolingUntil > accountState.coolingUntil)) {
|
|
5145
5206
|
accountState.coolingUntil = retryPlan.coolingUntil;
|
|
5146
5207
|
accountState.coolingReason = retryPlan.reason;
|
|
5147
5208
|
}
|
|
@@ -5150,9 +5211,11 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
5150
5211
|
// Non-fatal: routing already has the in-memory snapshot.
|
|
5151
5212
|
});
|
|
5152
5213
|
}
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5214
|
+
if (retryPlan.scope === "account") {
|
|
5215
|
+
await saveAccountCooldown(account.key, accountState.coolingUntil ?? retryPlan.coolingUntil, accountState.coolingReason ?? retryPlan.reason).catch(() => {
|
|
5216
|
+
// Non-fatal: routing already has the in-memory cooldown.
|
|
5217
|
+
});
|
|
5218
|
+
}
|
|
5156
5219
|
advancePrimaryIfCurrent(account.key, enabledAccounts.length, orderedAccounts[0]?.key);
|
|
5157
5220
|
break;
|
|
5158
5221
|
}
|
|
@@ -6149,10 +6212,10 @@ async function fetchAnthropicAccountResponse(args) {
|
|
|
6149
6212
|
const now = Date.now();
|
|
6150
6213
|
const quota = parseQuotaHeaders(errRespHeaders, { model: requestedModel });
|
|
6151
6214
|
const unifiedStatus = getUnifiedRateLimitStatus(errRespHeaders);
|
|
6152
|
-
const cooldownPlan = planCooldownFor429(quota, retryAfterMs, now, unifiedStatus);
|
|
6215
|
+
const cooldownPlan = planCooldownFor429(quota, retryAfterMs, now, unifiedStatus, overagePolicy, requestedModel);
|
|
6153
6216
|
const rateLimitKind = cooldownPlan.reason === "transient" ? "transient" : "quota";
|
|
6154
6217
|
recordAttemptError(account.label, account.type, 429, rateLimitKind);
|
|
6155
|
-
logger.always(`[proxy] ← 429 account=${account.label} reason=${cooldownPlan.reason} ` +
|
|
6218
|
+
logger.always(`[proxy] ← 429 account=${account.label} reason=${cooldownPlan.reason} scope=${cooldownPlan.scope} ` +
|
|
6156
6219
|
`retry-after=${retryAfterMs}ms 5h-status=${errRespHeaders["anthropic-ratelimit-unified-5h-status"] ?? "unknown"} ` +
|
|
6157
6220
|
`7d-status=${errRespHeaders["anthropic-ratelimit-unified-7d-status"] ?? "unknown"} ` +
|
|
6158
6221
|
`unified-status=${unifiedStatus ?? "unknown"} ` +
|
|
@@ -6507,14 +6570,15 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
6507
6570
|
// Publish the cooldown before retrying so requests arriving behind
|
|
6508
6571
|
// this one skip the throttled account instead of joining the burst.
|
|
6509
6572
|
let cooldownExtended = false;
|
|
6510
|
-
if (
|
|
6511
|
-
|
|
6573
|
+
if (plan.scope === "account" &&
|
|
6574
|
+
(!accountState.coolingUntil ||
|
|
6575
|
+
plan.coolingUntil > accountState.coolingUntil)) {
|
|
6512
6576
|
accountState.coolingUntil = plan.coolingUntil;
|
|
6513
6577
|
accountState.coolingReason = plan.reason;
|
|
6514
6578
|
cooldownExtended = true;
|
|
6515
6579
|
}
|
|
6516
|
-
if (cooldownExtended) {
|
|
6517
|
-
await saveAccountCooldown(account.key, accountState.coolingUntil, accountState.coolingReason ?? plan.reason).catch(() => {
|
|
6580
|
+
if (cooldownExtended && plan.scope === "account") {
|
|
6581
|
+
await saveAccountCooldown(account.key, accountState.coolingUntil ?? plan.coolingUntil, accountState.coolingReason ?? plan.reason).catch(() => {
|
|
6518
6582
|
// Non-fatal: routing already has the in-memory cooldown.
|
|
6519
6583
|
});
|
|
6520
6584
|
}
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -856,7 +856,13 @@ export type PreparedAnthropicAccountAttempt = {
|
|
|
856
856
|
export type RateLimitCoolingReason = Exclude<AccountCoolingReason, "auth">;
|
|
857
857
|
export type AccountCooldownPlan = {
|
|
858
858
|
reason: RateLimitCoolingReason;
|
|
859
|
-
/**
|
|
859
|
+
/**
|
|
860
|
+
* Whether this limit applies to every request on the account or only to the
|
|
861
|
+
* requested model. Model scope must never be persisted as an account
|
|
862
|
+
* cooldown; the quota window itself remains the routing evidence.
|
|
863
|
+
*/
|
|
864
|
+
scope: "account" | "model";
|
|
865
|
+
/** Epoch-ms until which the limiting window is expected to recover. */
|
|
860
866
|
coolingUntil: number;
|
|
861
867
|
/** When true (unified/5h/7d rejected), rotate immediately — retrying the
|
|
862
868
|
* same account is futile until its window resets. When false (transient
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.12.
|
|
3
|
+
"version": "12.12.2",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|