@juspay/neurolink 9.86.0 → 9.86.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 +12 -0
- package/dist/browser/neurolink.min.js +370 -370
- package/dist/lib/providers/anthropic.js +52 -3
- package/dist/lib/providers/openaiChatCompletionsClient.d.ts +3 -11
- package/dist/lib/proxy/accountQuota.d.ts +5 -0
- package/dist/lib/proxy/accountQuota.js +8 -1
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +22 -6
- package/dist/lib/server/routes/claudeProxyRoutes.js +52 -8
- package/dist/lib/types/common.d.ts +17 -0
- package/dist/lib/utils/anthropicCacheBreakpoints.d.ts +29 -1
- package/dist/lib/utils/anthropicCacheBreakpoints.js +54 -1
- package/dist/lib/utils/tokenUtils.d.ts +7 -2
- package/dist/lib/utils/tokenUtils.js +19 -2
- package/dist/providers/anthropic.js +52 -3
- package/dist/providers/openaiChatCompletionsClient.d.ts +3 -11
- package/dist/proxy/accountQuota.d.ts +5 -0
- package/dist/proxy/accountQuota.js +8 -1
- package/dist/server/routes/claudeProxyRoutes.d.ts +22 -6
- package/dist/server/routes/claudeProxyRoutes.js +52 -8
- package/dist/types/common.d.ts +17 -0
- package/dist/utils/anthropicCacheBreakpoints.d.ts +29 -1
- package/dist/utils/anthropicCacheBreakpoints.js +54 -1
- package/dist/utils/tokenUtils.d.ts +7 -2
- package/dist/utils/tokenUtils.js +19 -2
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
|
13
13
|
import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
|
|
14
14
|
import { logger } from "../utils/logger.js";
|
|
15
15
|
import { redactUrlCredentials } from "../utils/logSanitize.js";
|
|
16
|
+
import { ANTHROPIC_MAX_CACHE_BREAKPOINTS, applyAnthropicHistoryCacheBreakpoints, countAnthropicCacheMarkers, } from "../utils/anthropicCacheBreakpoints.js";
|
|
16
17
|
import { calculateCost } from "../utils/pricing.js";
|
|
17
18
|
import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
|
|
18
19
|
import { composeAbortSignals, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../utils/timeout.js";
|
|
@@ -1123,9 +1124,23 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1123
1124
|
}
|
|
1124
1125
|
// Extended thinking passthrough (providerOptions.anthropic.thinking).
|
|
1125
1126
|
const thinking = options.providerOptions?.anthropic?.thinking;
|
|
1127
|
+
// Prompt-cache parity with the native Vertex+Claude path: upstream
|
|
1128
|
+
// layers mark only the stable prefix (system via MessageBuilder,
|
|
1129
|
+
// last tool via GenerationHandler) — the growing conversation
|
|
1130
|
+
// history has no breakpoint, so on every turn it falls after the
|
|
1131
|
+
// last marker and is re-billed as fresh input. Add rolling history
|
|
1132
|
+
// breakpoints in whatever budget remains under Anthropic's
|
|
1133
|
+
// four-marker ceiling; pre-existing markers are counted so the
|
|
1134
|
+
// request can never exceed the cap.
|
|
1135
|
+
const cacheMarkersUsed = countAnthropicCacheMarkers({
|
|
1136
|
+
system,
|
|
1137
|
+
tools,
|
|
1138
|
+
messages: messages,
|
|
1139
|
+
});
|
|
1140
|
+
const cachedMessages = applyAnthropicHistoryCacheBreakpoints(messages, ANTHROPIC_MAX_CACHE_BREAKPOINTS - cacheMarkersUsed);
|
|
1126
1141
|
const params = {
|
|
1127
1142
|
model: modelId,
|
|
1128
|
-
messages,
|
|
1143
|
+
messages: cachedMessages,
|
|
1129
1144
|
max_tokens: resolveClaudeMaxTokens(modelId, options.maxOutputTokens),
|
|
1130
1145
|
...(system ? { system } : {}),
|
|
1131
1146
|
...(options.temperature !== undefined &&
|
|
@@ -1345,10 +1360,19 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1345
1360
|
.then((usage) => {
|
|
1346
1361
|
streamSpan.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens || 0);
|
|
1347
1362
|
streamSpan.setAttribute("gen_ai.usage.output_tokens", usage.completionTokens || 0);
|
|
1363
|
+
if (usage.cacheReadTokens) {
|
|
1364
|
+
streamSpan.setAttribute("gen_ai.usage.cached_input_tokens", usage.cacheReadTokens);
|
|
1365
|
+
}
|
|
1348
1366
|
const cost = calculateCost(this.providerName, this.modelName, {
|
|
1349
1367
|
input: usage.promptTokens || 0,
|
|
1350
1368
|
output: usage.completionTokens || 0,
|
|
1351
1369
|
total: usage.totalTokens || 0,
|
|
1370
|
+
...(usage.cacheReadTokens
|
|
1371
|
+
? { cacheReadTokens: usage.cacheReadTokens }
|
|
1372
|
+
: {}),
|
|
1373
|
+
...(usage.cacheCreationTokens
|
|
1374
|
+
? { cacheCreationTokens: usage.cacheCreationTokens }
|
|
1375
|
+
: {}),
|
|
1352
1376
|
});
|
|
1353
1377
|
if (cost && cost > 0) {
|
|
1354
1378
|
streamSpan.setAttribute("neurolink.cost", cost);
|
|
@@ -1372,11 +1396,26 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1372
1396
|
const conversation = payload.messages.slice();
|
|
1373
1397
|
let totalInput = 0;
|
|
1374
1398
|
let totalOutput = 0;
|
|
1399
|
+
let totalCacheRead = 0;
|
|
1400
|
+
let totalCacheWrite = 0;
|
|
1375
1401
|
let lastStop = null;
|
|
1376
1402
|
for (let step = 0; step < maxSteps; step++) {
|
|
1403
|
+
// Prompt-cache parity with the native Vertex+Claude path — rolling
|
|
1404
|
+
// history breakpoints, re-applied per step so the stable prefix
|
|
1405
|
+
// stays byte-identical while the breakpoint follows the growing
|
|
1406
|
+
// tail. Budget respects markers upstream layers already placed
|
|
1407
|
+
// (system / last tool / message blocks) so the request never
|
|
1408
|
+
// exceeds Anthropic's four-marker cap. Pure: `conversation` itself
|
|
1409
|
+
// is never mutated, so re-counting per step stays stable.
|
|
1410
|
+
const cacheMarkersUsed = countAnthropicCacheMarkers({
|
|
1411
|
+
system: payload.system,
|
|
1412
|
+
tools: anthropicTools,
|
|
1413
|
+
messages: conversation,
|
|
1414
|
+
});
|
|
1415
|
+
const cachedConversation = applyAnthropicHistoryCacheBreakpoints(conversation, ANTHROPIC_MAX_CACHE_BREAKPOINTS - cacheMarkersUsed);
|
|
1377
1416
|
const params = {
|
|
1378
1417
|
model: modelId,
|
|
1379
|
-
messages:
|
|
1418
|
+
messages: cachedConversation,
|
|
1380
1419
|
max_tokens: resolveClaudeMaxTokens(modelId, options.maxTokens),
|
|
1381
1420
|
stream: true,
|
|
1382
1421
|
...(payload.system ? { system: payload.system } : {}),
|
|
@@ -1406,6 +1445,12 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1406
1445
|
if (event.type === "message_start") {
|
|
1407
1446
|
totalInput += event.message.usage.input_tokens ?? 0;
|
|
1408
1447
|
totalOutput += event.message.usage.output_tokens ?? 0;
|
|
1448
|
+
// Anthropic reports cache reads/writes SEPARATELY from
|
|
1449
|
+
// input_tokens on the same message_start event — without these
|
|
1450
|
+
// the streaming path silently drops all cache accounting.
|
|
1451
|
+
totalCacheRead += event.message.usage.cache_read_input_tokens ?? 0;
|
|
1452
|
+
totalCacheWrite +=
|
|
1453
|
+
event.message.usage.cache_creation_input_tokens ?? 0;
|
|
1409
1454
|
}
|
|
1410
1455
|
else if (event.type === "content_block_start") {
|
|
1411
1456
|
blockTypes.set(event.index, event.content_block.type);
|
|
@@ -1578,7 +1623,11 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1578
1623
|
resolveUsage({
|
|
1579
1624
|
promptTokens: totalInput,
|
|
1580
1625
|
completionTokens: totalOutput,
|
|
1581
|
-
totalTokens: totalInput + totalOutput,
|
|
1626
|
+
totalTokens: totalInput + totalCacheRead + totalCacheWrite + totalOutput,
|
|
1627
|
+
...(totalCacheRead > 0 ? { cacheReadTokens: totalCacheRead } : {}),
|
|
1628
|
+
...(totalCacheWrite > 0
|
|
1629
|
+
? { cacheCreationTokens: totalCacheWrite }
|
|
1630
|
+
: {}),
|
|
1582
1631
|
});
|
|
1583
1632
|
resolveFinish(lastStop ?? "stop");
|
|
1584
1633
|
};
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Nothing here imports from "ai" or "@ai-sdk/*". The whole point of this
|
|
14
14
|
* module is to be the native replacement for the AI SDK's OpenAI wrapper.
|
|
15
15
|
*/
|
|
16
|
-
import type { OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatMessage, OpenAICompatMessageContent, OpenAICompatResponseFormat, OpenAICompatSSEResult, OpenAICompatStreamChunk, OpenAICompatToolChoiceWire, OpenAICompatV3CallToolChoice, OpenAICompatV3CallTools, Tool } from "../types/index.js";
|
|
16
|
+
import type { OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatMessage, OpenAICompatMessageContent, OpenAICompatResponseFormat, OpenAICompatSSEResult, OpenAICompatStreamChunk, OpenAICompatToolChoiceWire, OpenAICompatV3CallToolChoice, OpenAICompatV3CallTools, DeferredUsage, Tool } from "../types/index.js";
|
|
17
17
|
export declare const stripTrailingSlash: (s: string) => string;
|
|
18
18
|
export declare const safeStringify: (value: unknown) => string;
|
|
19
19
|
export declare const stringifyToolInput: (input: unknown) => string;
|
|
@@ -38,17 +38,9 @@ export declare const buildBody: (args: OpenAICompatBuildBodyArgs) => OpenAICompa
|
|
|
38
38
|
export declare const parseSSEStream: (body: ReadableStream<Uint8Array>, onTextDelta: (delta: string) => void, onReasoningDelta?: (delta: string) => void) => Promise<OpenAICompatSSEResult>;
|
|
39
39
|
export declare const buildAPIError: (url: string, body: OpenAICompatChatRequest, res: Response) => Promise<Error>;
|
|
40
40
|
export declare const createDeferredAnalytics: () => {
|
|
41
|
-
usagePromise: Promise<
|
|
42
|
-
promptTokens: number;
|
|
43
|
-
completionTokens: number;
|
|
44
|
-
totalTokens: number;
|
|
45
|
-
}>;
|
|
41
|
+
usagePromise: Promise<DeferredUsage>;
|
|
46
42
|
finishPromise: Promise<string>;
|
|
47
|
-
resolveUsage: (u:
|
|
48
|
-
promptTokens: number;
|
|
49
|
-
completionTokens: number;
|
|
50
|
-
totalTokens: number;
|
|
51
|
-
}) => void;
|
|
43
|
+
resolveUsage: (u: DeferredUsage) => void;
|
|
52
44
|
resolveFinish: (reason: string) => void;
|
|
53
45
|
};
|
|
54
46
|
export declare const createChunkQueue: () => {
|
|
@@ -35,5 +35,10 @@ export declare function loadAccountQuota(accountKey: string): Promise<AccountQuo
|
|
|
35
35
|
* Update quota for a single account.
|
|
36
36
|
* Updates in-memory cache immediately (non-blocking),
|
|
37
37
|
* then debounces the disk write to every 5 seconds.
|
|
38
|
+
*
|
|
39
|
+
* Loads the persisted file into the cache before the first write so a save
|
|
40
|
+
* after a process restart merges with existing entries instead of rewriting
|
|
41
|
+
* the file with only the accounts used since boot (which silently erased
|
|
42
|
+
* other accounts' snapshots and blinded quota-aware routing to them).
|
|
38
43
|
*/
|
|
39
44
|
export declare function saveAccountQuota(accountKey: string, quota: AccountQuota): Promise<void>;
|
|
@@ -174,10 +174,17 @@ export async function loadAccountQuota(accountKey) {
|
|
|
174
174
|
* Update quota for a single account.
|
|
175
175
|
* Updates in-memory cache immediately (non-blocking),
|
|
176
176
|
* then debounces the disk write to every 5 seconds.
|
|
177
|
+
*
|
|
178
|
+
* Loads the persisted file into the cache before the first write so a save
|
|
179
|
+
* after a process restart merges with existing entries instead of rewriting
|
|
180
|
+
* the file with only the accounts used since boot (which silently erased
|
|
181
|
+
* other accounts' snapshots and blinded quota-aware routing to them).
|
|
177
182
|
*/
|
|
178
183
|
export async function saveAccountQuota(accountKey, quota) {
|
|
184
|
+
if (!cacheLoaded) {
|
|
185
|
+
await loadAccountQuotas();
|
|
186
|
+
}
|
|
179
187
|
memoryCache[accountKey] = quota;
|
|
180
|
-
cacheLoaded = true;
|
|
181
188
|
dirty = true;
|
|
182
189
|
scheduleFlush();
|
|
183
190
|
}
|
|
@@ -44,18 +44,32 @@ declare function resetEpochToMs(resetEpoch: number | undefined, now: number): nu
|
|
|
44
44
|
* allow a couple of jittered same-account retries, then a short cooldown.
|
|
45
45
|
*/
|
|
46
46
|
declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: number, now: number): AccountCooldownPlan;
|
|
47
|
+
/**
|
|
48
|
+
* Seed each account's runtime quota from the persisted snapshots in
|
|
49
|
+
* ~/.neurolink/account-quotas.json (keyed by label). Runtime state is
|
|
50
|
+
* in-memory only, so without this the quota-aware ordering is blind after a
|
|
51
|
+
* proxy restart: all accounts tie, selection falls back to token-store
|
|
52
|
+
* enumeration order, and the first account served becomes self-reinforcing
|
|
53
|
+
* (it alone has data) — starving the others regardless of their resets.
|
|
54
|
+
* Never overwrites fresher in-memory quota; stale disk snapshots degrade
|
|
55
|
+
* gracefully because past reset timestamps are ignored by resetEpochToMs.
|
|
56
|
+
*/
|
|
57
|
+
declare function seedRuntimeQuotasFromDisk(accounts: ProxyPassthroughAccount[]): Promise<void>;
|
|
47
58
|
/**
|
|
48
59
|
* Order accounts to MAXIMIZE quota utilization (fill-first, smart order):
|
|
49
60
|
* spend the account whose window refreshes SOONEST first, so its about-to-reset
|
|
50
61
|
* allowance isn't wasted, then move to accounts with longer-dated resets.
|
|
51
62
|
*
|
|
52
63
|
* Priority among usable accounts:
|
|
53
|
-
* 1.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
64
|
+
* 1. no quota data yet — probe first: one request reveals its windows and
|
|
65
|
+
* self-corrects the ordering. (Ranking unknowns last would starve them
|
|
66
|
+
* forever: never picked → never observed → never comparable.)
|
|
67
|
+
* 2. soonest WEEKLY (7d) reset — the scarce, use-it-or-lose-it ceiling
|
|
68
|
+
* 3. soonest SESSION (5h) reset
|
|
69
|
+
* 4. highest weekly utilization — finish off the one closest to done
|
|
70
|
+
* 5. configured primary account, then insertion order
|
|
71
|
+
* Cooling/rejected accounts sort last, soonest-back-to-service first, as
|
|
72
|
+
* last resort.
|
|
59
73
|
*/
|
|
60
74
|
declare function orderAccountsByQuota(accounts: ProxyPassthroughAccount[], now: number): ProxyPassthroughAccount[];
|
|
61
75
|
/**
|
|
@@ -95,6 +109,8 @@ export declare const __testHooks: {
|
|
|
95
109
|
planCooldownFor429: typeof planCooldownFor429;
|
|
96
110
|
orderAccountsByQuota: typeof orderAccountsByQuota;
|
|
97
111
|
resetEpochToMs: typeof resetEpochToMs;
|
|
112
|
+
seedRuntimeQuotasFromDisk: typeof seedRuntimeQuotasFromDisk;
|
|
113
|
+
getAccountRuntimeState: (key: string) => RuntimeAccountState | undefined;
|
|
98
114
|
setConfiguredPrimaryAccountKey: (key: string | undefined) => void;
|
|
99
115
|
getConfiguredPrimaryAccountKey: () => string | undefined;
|
|
100
116
|
setPrimaryAccountIndex: (index: number) => void;
|
|
@@ -13,7 +13,7 @@ import { access, readFile } from "node:fs/promises";
|
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { buildStableClaudeCodeBillingHeader, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_OAUTH_BETAS, getOrCreateClaudeCodeIdentity, parseClaudeCodeUserId, } from "../../auth/anthropicOAuth.js";
|
|
16
|
-
import { parseQuotaHeaders, saveAccountQuota, } from "../../proxy/accountQuota.js";
|
|
16
|
+
import { loadAccountQuotas, parseQuotaHeaders, saveAccountQuota, } from "../../proxy/accountQuota.js";
|
|
17
17
|
import { buildClaudeError, ClaudeStreamSerializer, generateToolUseId, parseClaudeRequest, serializeClaudeResponse, } from "../../proxy/claudeFormat.js";
|
|
18
18
|
import { buildAnthropicModelsListResponse, buildTranslationOptions, extractText, extractToolArgs, extractUsageFromStreamResult, handleTranslatedJsonRequest, handleTranslatedStreamRequest, hasTranslatedOutput, } from "../../proxy/proxyTranslationEngine.js";
|
|
19
19
|
import { tracers } from "../../telemetry/tracers.js";
|
|
@@ -242,6 +242,30 @@ function maybeCoolFromQuota(state, quota, now) {
|
|
|
242
242
|
logger.always(`[proxy] proactively cooling account (${reason}) ~${minutesUntil(clamped, now)}m from success-response quota (status rejected)`);
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Seed each account's runtime quota from the persisted snapshots in
|
|
247
|
+
* ~/.neurolink/account-quotas.json (keyed by label). Runtime state is
|
|
248
|
+
* in-memory only, so without this the quota-aware ordering is blind after a
|
|
249
|
+
* proxy restart: all accounts tie, selection falls back to token-store
|
|
250
|
+
* enumeration order, and the first account served becomes self-reinforcing
|
|
251
|
+
* (it alone has data) — starving the others regardless of their resets.
|
|
252
|
+
* Never overwrites fresher in-memory quota; stale disk snapshots degrade
|
|
253
|
+
* gracefully because past reset timestamps are ignored by resetEpochToMs.
|
|
254
|
+
*/
|
|
255
|
+
async function seedRuntimeQuotasFromDisk(accounts) {
|
|
256
|
+
try {
|
|
257
|
+
const persisted = await loadAccountQuotas();
|
|
258
|
+
for (const account of accounts) {
|
|
259
|
+
const state = getOrCreateRuntimeState(account.key);
|
|
260
|
+
if (!state.quota && persisted[account.label]) {
|
|
261
|
+
state.quota = persisted[account.label];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// Non-fatal: seeding is best-effort; ordering falls back to probe-first.
|
|
267
|
+
}
|
|
268
|
+
}
|
|
245
269
|
/** Quota-aware selection is on by default; disable with
|
|
246
270
|
* NEUROLINK_PROXY_QUOTA_ROUTING=off|false|0. Only affects the fill-first
|
|
247
271
|
* strategy (round-robin keeps strict rotation). */
|
|
@@ -260,6 +284,7 @@ function accountSortMetrics(accountKey, now) {
|
|
|
260
284
|
const sessionRejected = q?.sessionStatus === "rejected" && sessionReset !== undefined;
|
|
261
285
|
return {
|
|
262
286
|
usable: !coolingActive && !weeklyRejected && !sessionRejected,
|
|
287
|
+
hasQuota: !!q,
|
|
263
288
|
coolingUntil: st?.coolingUntil ?? 0,
|
|
264
289
|
weeklyReset: weeklyReset ?? Number.POSITIVE_INFINITY,
|
|
265
290
|
sessionReset: sessionReset ?? Number.POSITIVE_INFINITY,
|
|
@@ -272,14 +297,18 @@ function accountSortMetrics(accountKey, now) {
|
|
|
272
297
|
* allowance isn't wasted, then move to accounts with longer-dated resets.
|
|
273
298
|
*
|
|
274
299
|
* Priority among usable accounts:
|
|
275
|
-
* 1.
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
300
|
+
* 1. no quota data yet — probe first: one request reveals its windows and
|
|
301
|
+
* self-corrects the ordering. (Ranking unknowns last would starve them
|
|
302
|
+
* forever: never picked → never observed → never comparable.)
|
|
303
|
+
* 2. soonest WEEKLY (7d) reset — the scarce, use-it-or-lose-it ceiling
|
|
304
|
+
* 3. soonest SESSION (5h) reset
|
|
305
|
+
* 4. highest weekly utilization — finish off the one closest to done
|
|
306
|
+
* 5. configured primary account, then insertion order
|
|
307
|
+
* Cooling/rejected accounts sort last, soonest-back-to-service first, as
|
|
308
|
+
* last resort.
|
|
281
309
|
*/
|
|
282
310
|
function orderAccountsByQuota(accounts, now) {
|
|
311
|
+
const primaryKey = configuredPrimaryAccountKey;
|
|
283
312
|
return [...accounts].sort((a, b) => {
|
|
284
313
|
const ma = accountSortMetrics(a.key, now);
|
|
285
314
|
const mb = accountSortMetrics(b.key, now);
|
|
@@ -291,13 +320,22 @@ function orderAccountsByQuota(accounts, now) {
|
|
|
291
320
|
const bu = mb.coolingUntil || Number.POSITIVE_INFINITY;
|
|
292
321
|
return au - bu;
|
|
293
322
|
}
|
|
323
|
+
if (ma.hasQuota !== mb.hasQuota) {
|
|
324
|
+
return ma.hasQuota ? 1 : -1;
|
|
325
|
+
}
|
|
294
326
|
if (ma.weeklyReset !== mb.weeklyReset) {
|
|
295
327
|
return ma.weeklyReset - mb.weeklyReset;
|
|
296
328
|
}
|
|
297
329
|
if (ma.sessionReset !== mb.sessionReset) {
|
|
298
330
|
return ma.sessionReset - mb.sessionReset;
|
|
299
331
|
}
|
|
300
|
-
|
|
332
|
+
if (ma.weeklyUsed !== mb.weeklyUsed) {
|
|
333
|
+
return mb.weeklyUsed - ma.weeklyUsed;
|
|
334
|
+
}
|
|
335
|
+
if (primaryKey && (a.key === primaryKey) !== (b.key === primaryKey)) {
|
|
336
|
+
return a.key === primaryKey ? -1 : 1;
|
|
337
|
+
}
|
|
338
|
+
return 0;
|
|
301
339
|
});
|
|
302
340
|
}
|
|
303
341
|
// ---------------------------------------------------------------------------
|
|
@@ -1280,6 +1318,7 @@ async function loadClaudeProxyAccounts(args) {
|
|
|
1280
1318
|
state.lastToken = account.token;
|
|
1281
1319
|
state.lastRefreshToken = account.refreshToken;
|
|
1282
1320
|
}
|
|
1321
|
+
await seedRuntimeQuotasFromDisk(accounts);
|
|
1283
1322
|
const enabledAccounts = accounts.filter((account) => {
|
|
1284
1323
|
return !getOrCreateRuntimeState(account.key).permanentlyDisabled;
|
|
1285
1324
|
});
|
|
@@ -3777,6 +3816,11 @@ export const __testHooks = {
|
|
|
3777
3816
|
planCooldownFor429,
|
|
3778
3817
|
orderAccountsByQuota,
|
|
3779
3818
|
resetEpochToMs,
|
|
3819
|
+
seedRuntimeQuotasFromDisk,
|
|
3820
|
+
getAccountRuntimeState: (key) => {
|
|
3821
|
+
const state = accountRuntimeState.get(key);
|
|
3822
|
+
return state ? { ...state } : undefined;
|
|
3823
|
+
},
|
|
3780
3824
|
setConfiguredPrimaryAccountKey: (key) => {
|
|
3781
3825
|
configuredPrimaryAccountKey = key;
|
|
3782
3826
|
},
|
|
@@ -223,6 +223,11 @@ export type RawUsageObject = {
|
|
|
223
223
|
cacheReadInputTokens?: number;
|
|
224
224
|
cacheCreationTokens?: number;
|
|
225
225
|
cacheReadTokens?: number;
|
|
226
|
+
cachedInputTokens?: number;
|
|
227
|
+
inputTokenDetails?: {
|
|
228
|
+
cacheReadTokens?: number;
|
|
229
|
+
cacheWriteTokens?: number;
|
|
230
|
+
};
|
|
226
231
|
prompt_tokens_details?: {
|
|
227
232
|
cached_tokens?: number;
|
|
228
233
|
};
|
|
@@ -232,6 +237,18 @@ export type RawUsageObject = {
|
|
|
232
237
|
thinkingTokens?: number;
|
|
233
238
|
usage?: RawUsageObject;
|
|
234
239
|
};
|
|
240
|
+
/**
|
|
241
|
+
* Aggregated usage resolved by a provider's deferred-analytics pair after a
|
|
242
|
+
* multi-step stream loop ends. The cache fields are optional — only providers
|
|
243
|
+
* with prompt caching (Anthropic) populate them.
|
|
244
|
+
*/
|
|
245
|
+
export type DeferredUsage = {
|
|
246
|
+
promptTokens: number;
|
|
247
|
+
completionTokens: number;
|
|
248
|
+
totalTokens: number;
|
|
249
|
+
cacheReadTokens?: number;
|
|
250
|
+
cacheCreationTokens?: number;
|
|
251
|
+
};
|
|
235
252
|
/**
|
|
236
253
|
* Options for token extraction from raw usage objects.
|
|
237
254
|
*/
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput } from "../types/index.js";
|
|
1
|
+
import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput, VertexAnthropicMessage } from "../types/index.js";
|
|
2
|
+
/** Anthropic allows at most four `cache_control` breakpoints per request. */
|
|
3
|
+
export declare const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4;
|
|
2
4
|
/**
|
|
3
5
|
* Annotate a native Vertex+Claude request with prompt-cache breakpoints.
|
|
4
6
|
*
|
|
@@ -13,3 +15,29 @@ import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput } from "../t
|
|
|
13
15
|
* Pure: the inputs are cloned, never mutated.
|
|
14
16
|
*/
|
|
15
17
|
export declare function applyVertexAnthropicCacheBreakpoints(input: VertexAnthropicCacheInput): VertexAnthropicCacheOutput;
|
|
18
|
+
/**
|
|
19
|
+
* Count the `cache_control` markers already present on a request. The direct
|
|
20
|
+
* Anthropic path (AI-SDK pipeline) arrives with markers the upstream layers
|
|
21
|
+
* placed — MessageBuilder tags the system prompt, GenerationHandler tags the
|
|
22
|
+
* last tool definition, and message content blocks may carry translated
|
|
23
|
+
* AI-SDK markers. Anthropic rejects requests with more than four markers, so
|
|
24
|
+
* any additional history breakpoints must fit in the remaining budget.
|
|
25
|
+
*/
|
|
26
|
+
export declare function countAnthropicCacheMarkers(input: {
|
|
27
|
+
system?: string | ReadonlyArray<{
|
|
28
|
+
cache_control?: unknown;
|
|
29
|
+
}> | undefined;
|
|
30
|
+
tools?: ReadonlyArray<{
|
|
31
|
+
cache_control?: unknown;
|
|
32
|
+
}> | undefined;
|
|
33
|
+
messages: ReadonlyArray<VertexAnthropicMessage>;
|
|
34
|
+
}): number;
|
|
35
|
+
/**
|
|
36
|
+
* Rolling history breakpoints for request paths whose stable-prefix markers
|
|
37
|
+
* are managed upstream (direct Anthropic: system via MessageBuilder, last
|
|
38
|
+
* tool via GenerationHandler). Marks the last content block of up to `budget`
|
|
39
|
+
* tail messages; a tail block that already carries a marker is left as-is
|
|
40
|
+
* without consuming budget (it already serves as that breakpoint). Pure —
|
|
41
|
+
* the input array is cloned, never mutated.
|
|
42
|
+
*/
|
|
43
|
+
export declare function applyAnthropicHistoryCacheBreakpoints(input: ReadonlyArray<VertexAnthropicMessage>, budget: number): VertexAnthropicMessage[];
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
const EPHEMERAL = { type: "ephemeral" };
|
|
18
18
|
/** Anthropic allows at most four `cache_control` breakpoints per request. */
|
|
19
|
-
const
|
|
19
|
+
export const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4;
|
|
20
|
+
const MAX_BREAKPOINTS = ANTHROPIC_MAX_CACHE_BREAKPOINTS;
|
|
20
21
|
/**
|
|
21
22
|
* Annotate a native Vertex+Claude request with prompt-cache breakpoints.
|
|
22
23
|
*
|
|
@@ -60,6 +61,58 @@ export function applyVertexAnthropicCacheBreakpoints(input) {
|
|
|
60
61
|
}
|
|
61
62
|
return { system, tools, messages };
|
|
62
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Count the `cache_control` markers already present on a request. The direct
|
|
66
|
+
* Anthropic path (AI-SDK pipeline) arrives with markers the upstream layers
|
|
67
|
+
* placed — MessageBuilder tags the system prompt, GenerationHandler tags the
|
|
68
|
+
* last tool definition, and message content blocks may carry translated
|
|
69
|
+
* AI-SDK markers. Anthropic rejects requests with more than four markers, so
|
|
70
|
+
* any additional history breakpoints must fit in the remaining budget.
|
|
71
|
+
*/
|
|
72
|
+
export function countAnthropicCacheMarkers(input) {
|
|
73
|
+
let count = 0;
|
|
74
|
+
if (Array.isArray(input.system)) {
|
|
75
|
+
count += input.system.filter((b) => b.cache_control).length;
|
|
76
|
+
}
|
|
77
|
+
if (input.tools) {
|
|
78
|
+
count += input.tools.filter((t) => t.cache_control).length;
|
|
79
|
+
}
|
|
80
|
+
for (const message of input.messages) {
|
|
81
|
+
if (Array.isArray(message.content)) {
|
|
82
|
+
count += message.content.filter((b) => b.cache_control).length;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return count;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Rolling history breakpoints for request paths whose stable-prefix markers
|
|
89
|
+
* are managed upstream (direct Anthropic: system via MessageBuilder, last
|
|
90
|
+
* tool via GenerationHandler). Marks the last content block of up to `budget`
|
|
91
|
+
* tail messages; a tail block that already carries a marker is left as-is
|
|
92
|
+
* without consuming budget (it already serves as that breakpoint). Pure —
|
|
93
|
+
* the input array is cloned, never mutated.
|
|
94
|
+
*/
|
|
95
|
+
export function applyAnthropicHistoryCacheBreakpoints(input, budget) {
|
|
96
|
+
const messages = input.map((m) => ({ ...m }));
|
|
97
|
+
let remaining = Math.max(0, Math.min(budget, MAX_BREAKPOINTS));
|
|
98
|
+
for (let i = messages.length - 1; i >= 0 && remaining > 0; i--) {
|
|
99
|
+
if (lastContentBlockHasMarker(messages[i])) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (markLastContentBlock(messages, i)) {
|
|
103
|
+
remaining--;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return messages;
|
|
107
|
+
}
|
|
108
|
+
/** True when the last content block of a message already carries `cache_control`. */
|
|
109
|
+
function lastContentBlockHasMarker(message) {
|
|
110
|
+
if (!Array.isArray(message.content) || message.content.length === 0) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const last = message.content[message.content.length - 1];
|
|
114
|
+
return !!last.cache_control;
|
|
115
|
+
}
|
|
63
116
|
/**
|
|
64
117
|
* Place a cache breakpoint on the last content block of `messages[i]`.
|
|
65
118
|
* Anthropic attaches `cache_control` to a content block, not the message
|
|
@@ -30,12 +30,17 @@ export declare function extractTotalTokens(usage: RawUsageObject, input: number,
|
|
|
30
30
|
export declare function extractReasoningTokens(usage: RawUsageObject): number | undefined;
|
|
31
31
|
/**
|
|
32
32
|
* Extract cache creation token count from various provider formats
|
|
33
|
-
* Supports: cacheCreationInputTokens, cacheCreationTokens
|
|
33
|
+
* Supports: cacheCreationInputTokens, cacheCreationTokens, and the ai@6
|
|
34
|
+
* normalized `inputTokenDetails.cacheWriteTokens` (the only shape the native
|
|
35
|
+
* direct-Anthropic path reports through generateText).
|
|
34
36
|
*/
|
|
35
37
|
export declare function extractCacheCreationTokens(usage: RawUsageObject): number | undefined;
|
|
36
38
|
/**
|
|
37
39
|
* Extract cache read token count from various provider formats
|
|
38
|
-
* Supports: cacheReadInputTokens, cacheReadTokens
|
|
40
|
+
* Supports: cacheReadInputTokens, cacheReadTokens, and the ai@6 normalized
|
|
41
|
+
* shapes — flat `cachedInputTokens` and nested
|
|
42
|
+
* `inputTokenDetails.cacheReadTokens` (the only shapes the native
|
|
43
|
+
* direct-Anthropic path reports through generateText).
|
|
39
44
|
*/
|
|
40
45
|
export declare function extractCacheReadTokens(usage: RawUsageObject): number | undefined;
|
|
41
46
|
/**
|
|
@@ -74,7 +74,9 @@ export function extractReasoningTokens(usage) {
|
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
76
|
* Extract cache creation token count from various provider formats
|
|
77
|
-
* Supports: cacheCreationInputTokens, cacheCreationTokens
|
|
77
|
+
* Supports: cacheCreationInputTokens, cacheCreationTokens, and the ai@6
|
|
78
|
+
* normalized `inputTokenDetails.cacheWriteTokens` (the only shape the native
|
|
79
|
+
* direct-Anthropic path reports through generateText).
|
|
78
80
|
*/
|
|
79
81
|
export function extractCacheCreationTokens(usage) {
|
|
80
82
|
if (typeof usage.cacheCreationInputTokens === "number" &&
|
|
@@ -85,11 +87,18 @@ export function extractCacheCreationTokens(usage) {
|
|
|
85
87
|
usage.cacheCreationTokens > 0) {
|
|
86
88
|
return usage.cacheCreationTokens;
|
|
87
89
|
}
|
|
90
|
+
if (typeof usage.inputTokenDetails?.cacheWriteTokens === "number" &&
|
|
91
|
+
usage.inputTokenDetails.cacheWriteTokens > 0) {
|
|
92
|
+
return usage.inputTokenDetails.cacheWriteTokens;
|
|
93
|
+
}
|
|
88
94
|
return undefined;
|
|
89
95
|
}
|
|
90
96
|
/**
|
|
91
97
|
* Extract cache read token count from various provider formats
|
|
92
|
-
* Supports: cacheReadInputTokens, cacheReadTokens
|
|
98
|
+
* Supports: cacheReadInputTokens, cacheReadTokens, and the ai@6 normalized
|
|
99
|
+
* shapes — flat `cachedInputTokens` and nested
|
|
100
|
+
* `inputTokenDetails.cacheReadTokens` (the only shapes the native
|
|
101
|
+
* direct-Anthropic path reports through generateText).
|
|
93
102
|
*/
|
|
94
103
|
export function extractCacheReadTokens(usage) {
|
|
95
104
|
if (typeof usage.cacheReadInputTokens === "number" &&
|
|
@@ -99,6 +108,14 @@ export function extractCacheReadTokens(usage) {
|
|
|
99
108
|
if (typeof usage.cacheReadTokens === "number" && usage.cacheReadTokens > 0) {
|
|
100
109
|
return usage.cacheReadTokens;
|
|
101
110
|
}
|
|
111
|
+
if (typeof usage.cachedInputTokens === "number" &&
|
|
112
|
+
usage.cachedInputTokens > 0) {
|
|
113
|
+
return usage.cachedInputTokens;
|
|
114
|
+
}
|
|
115
|
+
if (typeof usage.inputTokenDetails?.cacheReadTokens === "number" &&
|
|
116
|
+
usage.inputTokenDetails.cacheReadTokens > 0) {
|
|
117
|
+
return usage.inputTokenDetails.cacheReadTokens;
|
|
118
|
+
}
|
|
102
119
|
return undefined;
|
|
103
120
|
}
|
|
104
121
|
/**
|
|
@@ -13,6 +13,7 @@ import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
|
13
13
|
import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
|
|
14
14
|
import { logger } from "../utils/logger.js";
|
|
15
15
|
import { redactUrlCredentials } from "../utils/logSanitize.js";
|
|
16
|
+
import { ANTHROPIC_MAX_CACHE_BREAKPOINTS, applyAnthropicHistoryCacheBreakpoints, countAnthropicCacheMarkers, } from "../utils/anthropicCacheBreakpoints.js";
|
|
16
17
|
import { calculateCost } from "../utils/pricing.js";
|
|
17
18
|
import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
|
|
18
19
|
import { composeAbortSignals, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../utils/timeout.js";
|
|
@@ -1123,9 +1124,23 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1123
1124
|
}
|
|
1124
1125
|
// Extended thinking passthrough (providerOptions.anthropic.thinking).
|
|
1125
1126
|
const thinking = options.providerOptions?.anthropic?.thinking;
|
|
1127
|
+
// Prompt-cache parity with the native Vertex+Claude path: upstream
|
|
1128
|
+
// layers mark only the stable prefix (system via MessageBuilder,
|
|
1129
|
+
// last tool via GenerationHandler) — the growing conversation
|
|
1130
|
+
// history has no breakpoint, so on every turn it falls after the
|
|
1131
|
+
// last marker and is re-billed as fresh input. Add rolling history
|
|
1132
|
+
// breakpoints in whatever budget remains under Anthropic's
|
|
1133
|
+
// four-marker ceiling; pre-existing markers are counted so the
|
|
1134
|
+
// request can never exceed the cap.
|
|
1135
|
+
const cacheMarkersUsed = countAnthropicCacheMarkers({
|
|
1136
|
+
system,
|
|
1137
|
+
tools,
|
|
1138
|
+
messages: messages,
|
|
1139
|
+
});
|
|
1140
|
+
const cachedMessages = applyAnthropicHistoryCacheBreakpoints(messages, ANTHROPIC_MAX_CACHE_BREAKPOINTS - cacheMarkersUsed);
|
|
1126
1141
|
const params = {
|
|
1127
1142
|
model: modelId,
|
|
1128
|
-
messages,
|
|
1143
|
+
messages: cachedMessages,
|
|
1129
1144
|
max_tokens: resolveClaudeMaxTokens(modelId, options.maxOutputTokens),
|
|
1130
1145
|
...(system ? { system } : {}),
|
|
1131
1146
|
...(options.temperature !== undefined &&
|
|
@@ -1345,10 +1360,19 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1345
1360
|
.then((usage) => {
|
|
1346
1361
|
streamSpan.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens || 0);
|
|
1347
1362
|
streamSpan.setAttribute("gen_ai.usage.output_tokens", usage.completionTokens || 0);
|
|
1363
|
+
if (usage.cacheReadTokens) {
|
|
1364
|
+
streamSpan.setAttribute("gen_ai.usage.cached_input_tokens", usage.cacheReadTokens);
|
|
1365
|
+
}
|
|
1348
1366
|
const cost = calculateCost(this.providerName, this.modelName, {
|
|
1349
1367
|
input: usage.promptTokens || 0,
|
|
1350
1368
|
output: usage.completionTokens || 0,
|
|
1351
1369
|
total: usage.totalTokens || 0,
|
|
1370
|
+
...(usage.cacheReadTokens
|
|
1371
|
+
? { cacheReadTokens: usage.cacheReadTokens }
|
|
1372
|
+
: {}),
|
|
1373
|
+
...(usage.cacheCreationTokens
|
|
1374
|
+
? { cacheCreationTokens: usage.cacheCreationTokens }
|
|
1375
|
+
: {}),
|
|
1352
1376
|
});
|
|
1353
1377
|
if (cost && cost > 0) {
|
|
1354
1378
|
streamSpan.setAttribute("neurolink.cost", cost);
|
|
@@ -1372,11 +1396,26 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1372
1396
|
const conversation = payload.messages.slice();
|
|
1373
1397
|
let totalInput = 0;
|
|
1374
1398
|
let totalOutput = 0;
|
|
1399
|
+
let totalCacheRead = 0;
|
|
1400
|
+
let totalCacheWrite = 0;
|
|
1375
1401
|
let lastStop = null;
|
|
1376
1402
|
for (let step = 0; step < maxSteps; step++) {
|
|
1403
|
+
// Prompt-cache parity with the native Vertex+Claude path — rolling
|
|
1404
|
+
// history breakpoints, re-applied per step so the stable prefix
|
|
1405
|
+
// stays byte-identical while the breakpoint follows the growing
|
|
1406
|
+
// tail. Budget respects markers upstream layers already placed
|
|
1407
|
+
// (system / last tool / message blocks) so the request never
|
|
1408
|
+
// exceeds Anthropic's four-marker cap. Pure: `conversation` itself
|
|
1409
|
+
// is never mutated, so re-counting per step stays stable.
|
|
1410
|
+
const cacheMarkersUsed = countAnthropicCacheMarkers({
|
|
1411
|
+
system: payload.system,
|
|
1412
|
+
tools: anthropicTools,
|
|
1413
|
+
messages: conversation,
|
|
1414
|
+
});
|
|
1415
|
+
const cachedConversation = applyAnthropicHistoryCacheBreakpoints(conversation, ANTHROPIC_MAX_CACHE_BREAKPOINTS - cacheMarkersUsed);
|
|
1377
1416
|
const params = {
|
|
1378
1417
|
model: modelId,
|
|
1379
|
-
messages:
|
|
1418
|
+
messages: cachedConversation,
|
|
1380
1419
|
max_tokens: resolveClaudeMaxTokens(modelId, options.maxTokens),
|
|
1381
1420
|
stream: true,
|
|
1382
1421
|
...(payload.system ? { system: payload.system } : {}),
|
|
@@ -1406,6 +1445,12 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1406
1445
|
if (event.type === "message_start") {
|
|
1407
1446
|
totalInput += event.message.usage.input_tokens ?? 0;
|
|
1408
1447
|
totalOutput += event.message.usage.output_tokens ?? 0;
|
|
1448
|
+
// Anthropic reports cache reads/writes SEPARATELY from
|
|
1449
|
+
// input_tokens on the same message_start event — without these
|
|
1450
|
+
// the streaming path silently drops all cache accounting.
|
|
1451
|
+
totalCacheRead += event.message.usage.cache_read_input_tokens ?? 0;
|
|
1452
|
+
totalCacheWrite +=
|
|
1453
|
+
event.message.usage.cache_creation_input_tokens ?? 0;
|
|
1409
1454
|
}
|
|
1410
1455
|
else if (event.type === "content_block_start") {
|
|
1411
1456
|
blockTypes.set(event.index, event.content_block.type);
|
|
@@ -1578,7 +1623,11 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1578
1623
|
resolveUsage({
|
|
1579
1624
|
promptTokens: totalInput,
|
|
1580
1625
|
completionTokens: totalOutput,
|
|
1581
|
-
totalTokens: totalInput + totalOutput,
|
|
1626
|
+
totalTokens: totalInput + totalCacheRead + totalCacheWrite + totalOutput,
|
|
1627
|
+
...(totalCacheRead > 0 ? { cacheReadTokens: totalCacheRead } : {}),
|
|
1628
|
+
...(totalCacheWrite > 0
|
|
1629
|
+
? { cacheCreationTokens: totalCacheWrite }
|
|
1630
|
+
: {}),
|
|
1582
1631
|
});
|
|
1583
1632
|
resolveFinish(lastStop ?? "stop");
|
|
1584
1633
|
};
|