@juspay/neurolink 10.9.1 → 10.10.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 +6 -0
- package/dist/browser/neurolink.min.js +379 -379
- package/dist/cli/commands/proxy.js +29 -0
- package/dist/lib/providers/anthropic/client.d.ts +22 -7
- package/dist/lib/providers/anthropic/client.js +83 -58
- package/dist/lib/providers/anthropic/rateLimitCapture.d.ts +82 -0
- package/dist/lib/providers/anthropic/rateLimitCapture.js +375 -0
- package/dist/lib/proxy/quotaHeaders.d.ts +73 -0
- package/dist/lib/proxy/quotaHeaders.js +189 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +132 -17
- package/dist/lib/types/analytics.d.ts +8 -0
- package/dist/lib/types/generate.d.ts +12 -0
- package/dist/lib/types/proxy.d.ts +43 -0
- package/dist/lib/types/subscription.d.ts +77 -0
- package/dist/providers/anthropic/client.d.ts +22 -7
- package/dist/providers/anthropic/client.js +83 -58
- package/dist/providers/anthropic/rateLimitCapture.d.ts +82 -0
- package/dist/providers/anthropic/rateLimitCapture.js +374 -0
- package/dist/proxy/quotaHeaders.d.ts +73 -0
- package/dist/proxy/quotaHeaders.js +188 -0
- package/dist/server/routes/claudeProxyRoutes.js +132 -17
- package/dist/types/analytics.d.ts +8 -0
- package/dist/types/generate.d.ts +12 -0
- package/dist/types/proxy.d.ts +43 -0
- package/dist/types/subscription.d.ts +77 -0
- package/package.json +3 -1
|
@@ -17,6 +17,7 @@ import { buildStableClaudeCodeBillingHeader, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_
|
|
|
17
17
|
import { clearAccountCooldown, loadAccountCooldowns, saveAccountCooldown, } from "../../proxy/accountCooldown.js";
|
|
18
18
|
import { anthropicAccountKeysEqual, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, shouldLoadFallbackCredential, } from "../../proxy/accountSelection.js";
|
|
19
19
|
import { getUnifiedRateLimitStatus, isQuotaOverageAvailable, loadAccountQuotas, parseQuotaHeaders, saveAccountQuota, } from "../../proxy/accountQuota.js";
|
|
20
|
+
import { buildProxyLimitHeaders, summarizePoolHeadroom, } from "../../proxy/quotaHeaders.js";
|
|
20
21
|
import { buildClaudeError, ClaudeStreamSerializer, generateToolUseId, parseClaudeRequest, serializeClaudeResponse, } from "../../proxy/claudeFormat.js";
|
|
21
22
|
import { buildAnthropicModelsListResponse, buildTranslationOptions, extractText, extractToolArgs, extractUsageFromStreamResult, handleTranslatedJsonRequest, handleTranslatedStreamRequest, hasTranslatedOutput, } from "../../proxy/proxyTranslationEngine.js";
|
|
22
23
|
import { tracers } from "../../telemetry/tracers.js";
|
|
@@ -426,6 +427,56 @@ function resetEpochToMs(resetEpoch, now) {
|
|
|
426
427
|
const ms = resetEpoch > 4_102_444_800 ? resetEpoch : resetEpoch * 1000;
|
|
427
428
|
return ms > now ? ms : undefined;
|
|
428
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Publish limit/quota headers for this response onto the request context.
|
|
432
|
+
*
|
|
433
|
+
* Every response path funnels through here so the header contract is defined
|
|
434
|
+
* once. The proxy runtime copies `ctx.responseHeaders` onto JSON and error
|
|
435
|
+
* responses, and merges them into streaming Responses for keys those don't
|
|
436
|
+
* already set — so a single call covers both shapes.
|
|
437
|
+
*/
|
|
438
|
+
function publishLimitHeaders(ctx, args) {
|
|
439
|
+
try {
|
|
440
|
+
const pool = args.poolAccounts
|
|
441
|
+
? summarizePoolHeadroom(args.poolAccounts.map((account) => {
|
|
442
|
+
const state = accountRuntimeState.get(account.key);
|
|
443
|
+
return {
|
|
444
|
+
...(state?.coolingUntil !== undefined
|
|
445
|
+
? { coolingUntil: state.coolingUntil }
|
|
446
|
+
: {}),
|
|
447
|
+
...(state?.quota ? { quota: state.quota } : {}),
|
|
448
|
+
};
|
|
449
|
+
}))
|
|
450
|
+
: undefined;
|
|
451
|
+
const headers = buildProxyLimitHeaders({
|
|
452
|
+
...(args.upstreamHeaders
|
|
453
|
+
? { upstreamHeaders: args.upstreamHeaders }
|
|
454
|
+
: {}),
|
|
455
|
+
context: {
|
|
456
|
+
quota: args.quota ?? null,
|
|
457
|
+
source: args.source,
|
|
458
|
+
...(args.account ? { accountLabel: args.account.label } : {}),
|
|
459
|
+
...((args.accountType ?? args.account?.type)
|
|
460
|
+
? { accountType: args.accountType ?? args.account?.type }
|
|
461
|
+
: {}),
|
|
462
|
+
...(args.servedBy ? { servedBy: args.servedBy } : {}),
|
|
463
|
+
...(args.attempt !== undefined ? { attempt: args.attempt } : {}),
|
|
464
|
+
...(args.accountState?.coolingUntil !== undefined
|
|
465
|
+
? { coolingUntil: args.accountState.coolingUntil }
|
|
466
|
+
: {}),
|
|
467
|
+
...(args.accountState?.coolingReason
|
|
468
|
+
? { coolingReason: args.accountState.coolingReason }
|
|
469
|
+
: {}),
|
|
470
|
+
...(pool ? { pool } : {}),
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
ctx.responseHeaders = { ...(ctx.responseHeaders ?? {}), ...headers };
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
// Diagnostics must never break a response that is otherwise fine.
|
|
477
|
+
logger.debug(`[proxy] failed to publish limit headers: ${error instanceof Error ? error.message : String(error)}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
429
480
|
/** Clamp a cooldown target epoch-ms into [now+MIN, now+MAX]. */
|
|
430
481
|
function clampCooldownUntil(untilMs, now) {
|
|
431
482
|
return Math.min(Math.max(untilMs, now + MIN_COOLDOWN_MS), now + MAX_COOLDOWN_MS);
|
|
@@ -1466,6 +1517,20 @@ async function handleClaudePassthroughRequest(args) {
|
|
|
1466
1517
|
upstreamResponseHeaders[key] = value;
|
|
1467
1518
|
});
|
|
1468
1519
|
tracer?.logUpstreamResponseHeaders(upstreamResponseHeaders);
|
|
1520
|
+
// Passthrough uses the caller's own credentials, so there is no account pool
|
|
1521
|
+
// to report — but the upstream quota headers are still the caller's real
|
|
1522
|
+
// limits. Published before the ok/non-ok split so a 429 carries them too.
|
|
1523
|
+
{
|
|
1524
|
+
const passthroughQuota = parseQuotaHeaders(response.headers);
|
|
1525
|
+
publishLimitHeaders(ctx, {
|
|
1526
|
+
upstreamHeaders: response.headers,
|
|
1527
|
+
quota: passthroughQuota,
|
|
1528
|
+
source: passthroughQuota ? "live" : "none",
|
|
1529
|
+
accountType: "passthrough",
|
|
1530
|
+
servedBy: "anthropic",
|
|
1531
|
+
attempt: 1,
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1469
1534
|
if (!response.ok) {
|
|
1470
1535
|
const errorText = await response.text();
|
|
1471
1536
|
recordAttemptError("passthrough", "passthrough", response.status, response.status === 429 ? "quota" : undefined);
|
|
@@ -1572,7 +1637,7 @@ function trackUpstreamReadableStream(source) {
|
|
|
1572
1637
|
return { stream, outcome: tracker.outcome };
|
|
1573
1638
|
}
|
|
1574
1639
|
async function handleClaudePassthroughStreamResponse(args) {
|
|
1575
|
-
const { bodyStr, response, tracer, requestStartTime, upstreamSpan, upstreamResponseHeaders, logProxyBody, logFinalRequest, } = args;
|
|
1640
|
+
const { ctx, bodyStr, response, tracer, requestStartTime, upstreamSpan, upstreamResponseHeaders, logProxyBody, logFinalRequest, } = args;
|
|
1576
1641
|
const responseHeaders = { ...upstreamResponseHeaders };
|
|
1577
1642
|
const { stream: clientCaptureStream, capture: clientCapture } = createRawStreamCapture();
|
|
1578
1643
|
const responseBody = response.body;
|
|
@@ -1754,7 +1819,13 @@ async function handleClaudePassthroughStreamResponse(args) {
|
|
|
1754
1819
|
const clientStream = streamSource.pipeThrough(clientCaptureStream);
|
|
1755
1820
|
return new Response(clientStream, {
|
|
1756
1821
|
status: response.status,
|
|
1757
|
-
headers
|
|
1822
|
+
// Upstream headers first, then the proxy's own — passthrough already
|
|
1823
|
+
// forwarded everything Anthropic sent, so this only adds the x-neurolink-*
|
|
1824
|
+
// fields the client cannot derive on its own.
|
|
1825
|
+
headers: {
|
|
1826
|
+
...responseHeaders,
|
|
1827
|
+
...(ctx.responseHeaders ?? {}),
|
|
1828
|
+
},
|
|
1758
1829
|
});
|
|
1759
1830
|
}
|
|
1760
1831
|
async function handleClaudePassthroughJsonResponse(args) {
|
|
@@ -2244,6 +2315,14 @@ async function tryConfiguredClaudeFallbackChain(args) {
|
|
|
2244
2315
|
attemptCount: fallbackPlan.attempts.slice(1).length,
|
|
2245
2316
|
reason: "fallback_success",
|
|
2246
2317
|
});
|
|
2318
|
+
// A different provider produced this response — say so, and report no
|
|
2319
|
+
// quota. Emitting the last Anthropic snapshot here would attribute one
|
|
2320
|
+
// provider's capacity to another's output.
|
|
2321
|
+
publishLimitHeaders(ctx, {
|
|
2322
|
+
quota: null,
|
|
2323
|
+
source: "none",
|
|
2324
|
+
servedBy: fallback.provider,
|
|
2325
|
+
});
|
|
2247
2326
|
return { response };
|
|
2248
2327
|
}
|
|
2249
2328
|
catch (fallbackErr) {
|
|
@@ -2320,6 +2399,13 @@ async function tryAutoClaudeFallback(args) {
|
|
|
2320
2399
|
attemptCount: 1,
|
|
2321
2400
|
reason: "fallback_success",
|
|
2322
2401
|
});
|
|
2402
|
+
// See the configured-chain path: never attribute Anthropic quota to a
|
|
2403
|
+
// response another provider produced.
|
|
2404
|
+
publishLimitHeaders(ctx, {
|
|
2405
|
+
quota: null,
|
|
2406
|
+
source: "none",
|
|
2407
|
+
servedBy: "auto-provider",
|
|
2408
|
+
});
|
|
2323
2409
|
return { response };
|
|
2324
2410
|
}
|
|
2325
2411
|
catch (fallbackErr) {
|
|
@@ -2441,7 +2527,7 @@ function buildClaudeAnthropicFailureResponse(args) {
|
|
|
2441
2527
|
});
|
|
2442
2528
|
}
|
|
2443
2529
|
async function handleAnthropicSuccessfulResponse(args) {
|
|
2444
|
-
const { ctx, body, account, accountState, response, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, onStreamTerminal, logFinalRequest, } = args;
|
|
2530
|
+
const { ctx, body, account, accountState, response, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, onStreamTerminal, poolAccounts, logFinalRequest, } = args;
|
|
2445
2531
|
accountState.consecutiveRefreshFailures = 0;
|
|
2446
2532
|
logger.always(`[proxy] ← ${response.status} account=${account.label}`);
|
|
2447
2533
|
const quota = parseQuotaHeaders(response.headers);
|
|
@@ -2470,6 +2556,20 @@ async function handleAnthropicSuccessfulResponse(args) {
|
|
|
2470
2556
|
responseHeaders[key] = value;
|
|
2471
2557
|
});
|
|
2472
2558
|
tracer?.logUpstreamResponseHeaders(responseHeaders);
|
|
2559
|
+
// Surface limits to the client. `quota` is non-null only when this upstream
|
|
2560
|
+
// response actually carried the unified headers; otherwise fall back to the
|
|
2561
|
+
// account's last snapshot and label it as such, so a consumer never mistakes
|
|
2562
|
+
// a carried-over reading for a fresh one.
|
|
2563
|
+
publishLimitHeaders(ctx, {
|
|
2564
|
+
upstreamHeaders: response.headers,
|
|
2565
|
+
quota: quota ?? accountState.quota ?? null,
|
|
2566
|
+
source: quota ? "live" : accountState.quota ? "snapshot" : "none",
|
|
2567
|
+
account,
|
|
2568
|
+
accountState,
|
|
2569
|
+
servedBy: "anthropic",
|
|
2570
|
+
attempt: attemptNumber,
|
|
2571
|
+
...(poolAccounts ? { poolAccounts } : {}),
|
|
2572
|
+
});
|
|
2473
2573
|
if (body.stream) {
|
|
2474
2574
|
return handleAnthropicStreamingSuccessResponse({
|
|
2475
2575
|
ctx,
|
|
@@ -2506,7 +2606,7 @@ async function handleAnthropicSuccessfulResponse(args) {
|
|
|
2506
2606
|
});
|
|
2507
2607
|
}
|
|
2508
2608
|
async function handleAnthropicStreamingSuccessResponse(args) {
|
|
2509
|
-
const { account, accountState, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, onStreamTerminal, logFinalRequest, } = args;
|
|
2609
|
+
const { ctx, account, accountState, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, onStreamTerminal, logFinalRequest, } = args;
|
|
2510
2610
|
if (!response.body) {
|
|
2511
2611
|
recordAttemptError(account.label, account.type, 502);
|
|
2512
2612
|
logAttempt(502, "stream_error", "No response body from upstream");
|
|
@@ -2703,6 +2803,7 @@ async function handleAnthropicStreamingSuccessResponse(args) {
|
|
|
2703
2803
|
},
|
|
2704
2804
|
});
|
|
2705
2805
|
const { response: result, telemetryDone } = attachAnthropicSuccessStreamTelemetry({
|
|
2806
|
+
ctx,
|
|
2706
2807
|
account,
|
|
2707
2808
|
response,
|
|
2708
2809
|
responseHeaders,
|
|
@@ -2742,7 +2843,7 @@ function recordCommittedAnthropicStreamAttemptFailure(outcome, account) {
|
|
|
2742
2843
|
}
|
|
2743
2844
|
}
|
|
2744
2845
|
function attachAnthropicSuccessStreamTelemetry(args) {
|
|
2745
|
-
const { account, response, responseHeaders, remainingStream, streamOutcome, tracer, requestStartTime, attemptNumber, finalBodyStr, upstreamSpan, logProxyBody, logFinalRequest, } = args;
|
|
2846
|
+
const { ctx, account, response, responseHeaders, remainingStream, streamOutcome, tracer, requestStartTime, attemptNumber, finalBodyStr, upstreamSpan, logProxyBody, logFinalRequest, } = args;
|
|
2746
2847
|
const { stream: clientCaptureStream, capture: clientCapture } = createRawStreamCapture();
|
|
2747
2848
|
let streamSource = remainingStream;
|
|
2748
2849
|
let telemetryDone;
|
|
@@ -2952,23 +3053,18 @@ function attachAnthropicSuccessStreamTelemetry(args) {
|
|
|
2952
3053
|
}
|
|
2953
3054
|
}
|
|
2954
3055
|
const clientStream = streamSource.pipeThrough(clientCaptureStream);
|
|
3056
|
+
// Limit headers published on the context are applied here rather than left
|
|
3057
|
+
// to the runtime wrapper: this Response goes straight to the client on every
|
|
3058
|
+
// mount (proxy runtime and the generic server adapters alike), so the
|
|
3059
|
+
// streaming path has to carry them itself. The previous five-name allowlist
|
|
3060
|
+
// forwarded only the legacy counters and dropped the unified subscription
|
|
3061
|
+
// windows entirely.
|
|
2955
3062
|
const clientResponseHeaders = {
|
|
3063
|
+
...(ctx.responseHeaders ?? {}),
|
|
2956
3064
|
"content-type": "text/event-stream",
|
|
2957
3065
|
"cache-control": "no-cache",
|
|
2958
3066
|
connection: "keep-alive",
|
|
2959
3067
|
};
|
|
2960
|
-
for (const headerName of [
|
|
2961
|
-
"retry-after",
|
|
2962
|
-
"anthropic-ratelimit-requests-remaining",
|
|
2963
|
-
"anthropic-ratelimit-requests-limit",
|
|
2964
|
-
"anthropic-ratelimit-tokens-remaining",
|
|
2965
|
-
"anthropic-ratelimit-tokens-limit",
|
|
2966
|
-
]) {
|
|
2967
|
-
const value = response.headers.get(headerName);
|
|
2968
|
-
if (value) {
|
|
2969
|
-
clientResponseHeaders[headerName] = value;
|
|
2970
|
-
}
|
|
2971
|
-
}
|
|
2972
3068
|
return {
|
|
2973
3069
|
response: new Response(clientStream, {
|
|
2974
3070
|
status: response.status,
|
|
@@ -4612,6 +4708,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
4612
4708
|
logProxyBody,
|
|
4613
4709
|
logFinalRequest,
|
|
4614
4710
|
onStreamTerminal: admissionLease.release,
|
|
4711
|
+
poolAccounts: enabledAccounts,
|
|
4615
4712
|
});
|
|
4616
4713
|
if ("retryNextAccount" in successResult) {
|
|
4617
4714
|
if (successResult.failure) {
|
|
@@ -4674,6 +4771,24 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
4674
4771
|
}
|
|
4675
4772
|
loopState.fallbackFailureMessage = fallbackFailureMessage;
|
|
4676
4773
|
}
|
|
4774
|
+
// Terminal failure — usually "every account is rate-limited". This is the
|
|
4775
|
+
// response a caller most needs limit data on, and historically the one that
|
|
4776
|
+
// carried none. No single account served it, so report the account we would
|
|
4777
|
+
// have tried first plus pool headroom, explicitly marked as a snapshot.
|
|
4778
|
+
{
|
|
4779
|
+
const primary = orderedAccounts[0];
|
|
4780
|
+
const primaryState = primary
|
|
4781
|
+
? accountRuntimeState.get(primary.key)
|
|
4782
|
+
: undefined;
|
|
4783
|
+
publishLimitHeaders(ctx, {
|
|
4784
|
+
quota: primaryState?.quota ?? null,
|
|
4785
|
+
source: primaryState?.quota ? "snapshot" : "none",
|
|
4786
|
+
...(primary ? { account: primary } : {}),
|
|
4787
|
+
...(primaryState ? { accountState: primaryState } : {}),
|
|
4788
|
+
attempt: loopState.attemptNumber,
|
|
4789
|
+
poolAccounts: enabledAccounts,
|
|
4790
|
+
});
|
|
4791
|
+
}
|
|
4677
4792
|
return buildClaudeAnthropicFailureResponse({
|
|
4678
4793
|
tracer,
|
|
4679
4794
|
requestStartTime,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Comprehensive usage tracking, performance metrics, and cost analysis types
|
|
4
4
|
*/
|
|
5
5
|
import type { JsonValue, UnknownRecord } from "./common.js";
|
|
6
|
+
import type { ClaudeLimitSnapshot } from "./subscription.js";
|
|
6
7
|
/**
|
|
7
8
|
* Token usage information (consolidated from multiple sources)
|
|
8
9
|
*/
|
|
@@ -45,6 +46,13 @@ export type AnalyticsData = {
|
|
|
45
46
|
elapsedMs?: number;
|
|
46
47
|
/** Verbatim provider finish/stop reason for the terminal model call. */
|
|
47
48
|
rawFinishReason?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Account limit state observed on this request — subscription window
|
|
51
|
+
* headroom, reset times, and (via the NeuroLink Claude proxy) which account
|
|
52
|
+
* served it and how much the pool has left. Present for Anthropic traffic
|
|
53
|
+
* whose response carried rate-limit headers.
|
|
54
|
+
*/
|
|
55
|
+
limits?: ClaudeLimitSnapshot;
|
|
48
56
|
};
|
|
49
57
|
/**
|
|
50
58
|
* Stream Analytics Data - Enhanced for performance tracking
|
|
@@ -3,6 +3,7 @@ import type { RAGConfig } from "./rag.js";
|
|
|
3
3
|
import type { KnowledgeGroundingMetadata, KnowledgeRequestScope } from "./knowledge.js";
|
|
4
4
|
import type { SkillsCallOptions } from "./skills.js";
|
|
5
5
|
import type { AnalyticsData, TokenUsage } from "./analytics.js";
|
|
6
|
+
import type { ClaudeLimitSnapshot } from "./subscription.js";
|
|
6
7
|
import type { JsonValue } from "./common.js";
|
|
7
8
|
import type { Content, ImageWithAltText } from "./content.js";
|
|
8
9
|
import type { ChatMessage, ConversationMemoryConfig } from "./conversation.js";
|
|
@@ -963,6 +964,17 @@ export type GenerateResult = {
|
|
|
963
964
|
message: string;
|
|
964
965
|
}>;
|
|
965
966
|
};
|
|
967
|
+
/**
|
|
968
|
+
* Account limit state for this request, parsed from Anthropic's
|
|
969
|
+
* `anthropic-ratelimit-*` response headers (plus the NeuroLink Claude
|
|
970
|
+
* proxy's `x-neurolink-*` additions when routed through it).
|
|
971
|
+
*
|
|
972
|
+
* Subscription windows report utilization, so headroom is a percentage
|
|
973
|
+
* (`sessionLeftPct`) rather than an absolute count — Anthropic publishes no
|
|
974
|
+
* remaining message or token figure for them. API-key accounts do carry
|
|
975
|
+
* absolute `requestsRemaining` / `tokensRemaining`.
|
|
976
|
+
*/
|
|
977
|
+
limits?: ClaudeLimitSnapshot;
|
|
966
978
|
};
|
|
967
979
|
/**
|
|
968
980
|
* Unified options for both generation and streaming
|
|
@@ -944,6 +944,49 @@ export type AccountQuota = {
|
|
|
944
944
|
/** Epoch ms when we last captured this data */
|
|
945
945
|
lastUpdated: number;
|
|
946
946
|
};
|
|
947
|
+
/**
|
|
948
|
+
* Provenance of the quota numbers attached to a single proxy response.
|
|
949
|
+
* - "live" : parsed from THIS upstream response's headers.
|
|
950
|
+
* - "snapshot" : the last known snapshot for the serving account; the upstream
|
|
951
|
+
* response carried no quota headers.
|
|
952
|
+
* - "none" : no Anthropic account served this request (fallback provider,
|
|
953
|
+
* or a failure before any account was reached).
|
|
954
|
+
*
|
|
955
|
+
* Consumers must not treat "snapshot" as current. Without this distinction a
|
|
956
|
+
* stale reading is indistinguishable from a fresh one.
|
|
957
|
+
*/
|
|
958
|
+
export type ProxyQuotaSource = "live" | "snapshot" | "none";
|
|
959
|
+
/** Aggregate account-pool headroom at the moment a response was produced. */
|
|
960
|
+
export type ProxyPoolHeadroom = {
|
|
961
|
+
/** Accounts eligible to serve a request right now (not cooling/disabled). */
|
|
962
|
+
available: number;
|
|
963
|
+
/** Accounts currently in a cooldown window. */
|
|
964
|
+
cooling: number;
|
|
965
|
+
/** Best session headroom across available accounts, as a percentage 0-100.
|
|
966
|
+
* Undefined when no available account has a quota snapshot. */
|
|
967
|
+
bestSessionLeftPct?: number;
|
|
968
|
+
};
|
|
969
|
+
/**
|
|
970
|
+
* Everything the proxy knows about limits and routing for one response.
|
|
971
|
+
* Consumed by `buildQuotaResponseHeaders` — kept as data (not headers) so the
|
|
972
|
+
* assembly stays pure and testable.
|
|
973
|
+
*/
|
|
974
|
+
export type ProxyQuotaHeaderContext = {
|
|
975
|
+
quota: AccountQuota | null;
|
|
976
|
+
source: ProxyQuotaSource;
|
|
977
|
+
/** Account label that served the request; omitted for fallback/no-account. */
|
|
978
|
+
accountLabel?: string;
|
|
979
|
+
accountType?: string;
|
|
980
|
+
/** Which upstream actually produced the response ("anthropic" or a fallback
|
|
981
|
+
* provider name). Lets a consumer avoid attributing quota to the wrong one. */
|
|
982
|
+
servedBy?: string;
|
|
983
|
+
/** 1-based attempt index within the routing loop. */
|
|
984
|
+
attempt?: number;
|
|
985
|
+
/** Epoch ms until which the serving account is cooling, when applicable. */
|
|
986
|
+
coolingUntil?: number;
|
|
987
|
+
coolingReason?: AccountCoolingReason;
|
|
988
|
+
pool?: ProxyPoolHeadroom;
|
|
989
|
+
};
|
|
947
990
|
/** Why an account is currently cooling. Drives cooldown duration and logging.
|
|
948
991
|
* - "weekly" : 7d unified limit rejected — cool until the weekly reset.
|
|
949
992
|
* - "session" : 5h unified limit rejected — cool until the session reset.
|
|
@@ -87,6 +87,83 @@ export type AnthropicRateLimitInfo = {
|
|
|
87
87
|
* Retry-After header value in seconds (present on 429 responses)
|
|
88
88
|
*/
|
|
89
89
|
retryAfter?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Subscription (OAuth) window utilization, 0.0-1.0 of capacity USED, from
|
|
92
|
+
* `anthropic-ratelimit-unified-5h-utilization`.
|
|
93
|
+
*
|
|
94
|
+
* Anthropic publishes utilization for subscription windows, never an
|
|
95
|
+
* absolute remaining count — there is no message or token figure to report.
|
|
96
|
+
* `sessionLeftPct` below is the derived "how much is left".
|
|
97
|
+
*/
|
|
98
|
+
sessionUtilization?: number;
|
|
99
|
+
/** "allowed" | "throttled" | "rejected" for the 5h window. */
|
|
100
|
+
sessionStatus?: string;
|
|
101
|
+
/** Unix epoch seconds at which the 5h window resets. */
|
|
102
|
+
sessionResetAt?: number;
|
|
103
|
+
/** Whole-percent capacity remaining in the 5h window (100 - utilization). */
|
|
104
|
+
sessionLeftPct?: number;
|
|
105
|
+
/** 7d window utilization, 0.0-1.0 of capacity USED. */
|
|
106
|
+
weeklyUtilization?: number;
|
|
107
|
+
weeklyStatus?: string;
|
|
108
|
+
weeklyResetAt?: number;
|
|
109
|
+
weeklyLeftPct?: number;
|
|
110
|
+
/** Authoritative top-level unified status; can be "rejected" even while both
|
|
111
|
+
* sub-windows still report "allowed". */
|
|
112
|
+
unifiedStatus?: string;
|
|
113
|
+
/** Whether overage is permitted once a window is exhausted. */
|
|
114
|
+
overageStatus?: string;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Per-request limit snapshot as observed by the Anthropic provider.
|
|
118
|
+
*
|
|
119
|
+
* Assembled from response headers on every request — whether the provider is
|
|
120
|
+
* talking directly to Anthropic (both auth methods) or through the NeuroLink
|
|
121
|
+
* Claude proxy. The `account`/`pool`/`servedBy` fields are populated only by
|
|
122
|
+
* the proxy, which is the only party that knows them.
|
|
123
|
+
*/
|
|
124
|
+
export type ClaudeLimitSnapshot = {
|
|
125
|
+
/** Rate-limit figures parsed from `anthropic-ratelimit-*` headers. */
|
|
126
|
+
rateLimit: AnthropicRateLimitInfo;
|
|
127
|
+
/**
|
|
128
|
+
* Provenance of the quota numbers. "snapshot" means the proxy reported a
|
|
129
|
+
* previously captured reading rather than one from this response; "none"
|
|
130
|
+
* means no Anthropic account served the request (e.g. a fallback provider).
|
|
131
|
+
* Absent when talking directly to Anthropic, where any figures are live.
|
|
132
|
+
*/
|
|
133
|
+
quotaSource?: "live" | "snapshot" | "none";
|
|
134
|
+
/** Proxy account label that served the request. */
|
|
135
|
+
account?: string;
|
|
136
|
+
/** "oauth" | "api_key" | "passthrough". */
|
|
137
|
+
accountType?: string;
|
|
138
|
+
/** Upstream that produced the response — "anthropic" or a fallback provider. */
|
|
139
|
+
servedBy?: string;
|
|
140
|
+
/** Epoch ms until which the serving account is cooling. */
|
|
141
|
+
accountCoolingUntil?: number;
|
|
142
|
+
accountCoolingReason?: string;
|
|
143
|
+
/** Proxy account-pool headroom at response time. */
|
|
144
|
+
pool?: {
|
|
145
|
+
available?: number;
|
|
146
|
+
cooling?: number;
|
|
147
|
+
bestSessionLeftPct?: number;
|
|
148
|
+
};
|
|
149
|
+
/** Anthropic request id, for correlating with provider-side logs. */
|
|
150
|
+
requestId?: string;
|
|
151
|
+
/** HTTP status of the response the snapshot came from. */
|
|
152
|
+
status?: number;
|
|
153
|
+
/** Epoch ms when this snapshot was captured. */
|
|
154
|
+
capturedAt: number;
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Per-request capture slot the Anthropic fetch wrapper writes into.
|
|
158
|
+
*
|
|
159
|
+
* Held in AsyncLocalStorage for the duration of a generate/stream call, so
|
|
160
|
+
* concurrent calls on one provider instance cannot see each other's limits.
|
|
161
|
+
* `headers` keeps the raw response header bag so the AI-SDK model adapter can
|
|
162
|
+
* report real response headers.
|
|
163
|
+
*/
|
|
164
|
+
export type ClaudeLimitCaptureSlot = {
|
|
165
|
+
snapshot?: ClaudeLimitSnapshot;
|
|
166
|
+
headers?: Record<string, string>;
|
|
90
167
|
};
|
|
91
168
|
/**
|
|
92
169
|
* Response metadata including rate limit information
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AIProviderName } from "../../constants/enums.js";
|
|
2
2
|
import { BaseProvider } from "../../core/baseProvider.js";
|
|
3
|
-
import type { AnthropicProviderConfig, StreamOptions, StreamResult, ValidationSchema, EnhancedGenerateResult, TextGenerationOptions, AnthropicAuthMethod, AnthropicResponseMetadata, ClaudeSubscriptionTier, ClaudeUsageInfo } from "../../types/index.js";
|
|
3
|
+
import type { AnthropicProviderConfig, StreamOptions, StreamResult, ValidationSchema, EnhancedGenerateResult, TextGenerationOptions, AnthropicAuthMethod, AnthropicRateLimitInfo, AnthropicResponseMetadata, ClaudeSubscriptionTier, ClaudeUsageInfo } from "../../types/index.js";
|
|
4
4
|
import type { LanguageModel } from "../../types/index.js";
|
|
5
5
|
/**
|
|
6
6
|
* Anthropic Provider v2 - BaseProvider Implementation
|
|
@@ -103,12 +103,18 @@ export declare class AnthropicProvider extends BaseProvider {
|
|
|
103
103
|
*/
|
|
104
104
|
getLastResponseMetadata(): AnthropicResponseMetadata | null;
|
|
105
105
|
/**
|
|
106
|
-
* Update response metadata from
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
106
|
+
* Update response metadata from a captured limit snapshot.
|
|
107
|
+
*
|
|
108
|
+
* Takes already-parsed rate-limit info rather than raw headers: parsing now
|
|
109
|
+
* lives in `rateLimitCapture`, which is the only layer that sees the raw
|
|
110
|
+
* response and understands both header families (unified subscription
|
|
111
|
+
* windows and legacy per-tier counters).
|
|
112
|
+
*
|
|
113
|
+
* @param rateLimit - Parsed rate-limit figures
|
|
114
|
+
* @param requestId - Optional Anthropic request ID
|
|
115
|
+
* @param usageUpdate - Optional token counts to fold into usage tracking
|
|
110
116
|
*/
|
|
111
|
-
protected updateResponseMetadata(
|
|
117
|
+
protected updateResponseMetadata(rateLimit: AnthropicRateLimitInfo, requestId?: string, usageUpdate?: {
|
|
112
118
|
inputTokens?: number;
|
|
113
119
|
outputTokens?: number;
|
|
114
120
|
}): void;
|
|
@@ -127,7 +133,16 @@ export declare class AnthropicProvider extends BaseProvider {
|
|
|
127
133
|
* BaseProvider so that expired tokens are renewed automatically.
|
|
128
134
|
*/
|
|
129
135
|
generate(optionsOrPrompt: TextGenerationOptions | string, analysisSchema?: ValidationSchema): Promise<EnhancedGenerateResult | null>;
|
|
130
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Fold a captured snapshot into the provider's usage bookkeeping and log it.
|
|
138
|
+
*
|
|
139
|
+
* `updateResponseMetadata` had no callers before this — the metadata it
|
|
140
|
+
* maintains, and the public `getLastResponseMetadata()` / `getUsageInfo()`
|
|
141
|
+
* that read it, were never populated by anything.
|
|
142
|
+
*/
|
|
143
|
+
private recordLimitSnapshot;
|
|
144
|
+
protected executeStream(options: StreamOptions, analysisSchema?: ValidationSchema): Promise<StreamResult>;
|
|
145
|
+
private executeStreamInCaptureScope;
|
|
131
146
|
isAvailable(): Promise<boolean>;
|
|
132
147
|
getModel(): LanguageModel;
|
|
133
148
|
}
|