@cortexkit/anthropic-auth-core 1.1.1 → 1.1.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/README.md +1 -1
- package/dist/accounts.d.ts +7 -0
- package/dist/accounts.js +63 -2
- package/dist/cachekeep.d.ts +92 -0
- package/dist/cachekeep.js +297 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @cortexkit/anthropic-auth-core
|
|
2
2
|
|
|
3
|
-
Shared Anthropic OAuth, quota, cache, fast mode, relay, dump, and request-signing helpers used by CortexKit's OpenCode and Pi integrations.
|
|
3
|
+
Shared Anthropic OAuth, quota, cache, cache-keepalive, fast mode, relay, dump, and request-signing helpers used by CortexKit's OpenCode and Pi integrations.
|
|
4
4
|
|
|
5
5
|
User-facing packages:
|
|
6
6
|
|
package/dist/accounts.d.ts
CHANGED
|
@@ -62,6 +62,11 @@ export type AccountStorage = {
|
|
|
62
62
|
claudeFast?: {
|
|
63
63
|
enabled?: boolean;
|
|
64
64
|
};
|
|
65
|
+
cacheKeep?: {
|
|
66
|
+
enabled?: boolean;
|
|
67
|
+
startHour?: number;
|
|
68
|
+
endHour?: number;
|
|
69
|
+
};
|
|
65
70
|
relay?: {
|
|
66
71
|
enabled?: boolean;
|
|
67
72
|
url?: string;
|
|
@@ -92,6 +97,8 @@ export declare function isDumpPersistentlyEnabled(storage: AccountStorage | null
|
|
|
92
97
|
export declare function setDumpPersistentEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
|
|
93
98
|
export declare function isFastModePersistentlyEnabled(storage: AccountStorage | null): boolean;
|
|
94
99
|
export declare function setFastModePersistentEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
|
|
100
|
+
export declare function setCacheKeepPersistentWindow(startHour: number, endHour: number, path?: string): Promise<AccountStorage>;
|
|
101
|
+
export declare function setCacheKeepPersistentEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
|
|
95
102
|
export declare function shouldFallbackStatus(status: number, storage: AccountStorage | null): boolean;
|
|
96
103
|
export declare function getRefreshIntervalMs(storage: AccountStorage | null): number;
|
|
97
104
|
export declare function hashRefreshToken(refreshToken: string): string;
|
package/dist/accounts.js
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { ClaudeOAuthRefreshError, refreshClaudeOAuthToken } from "./auth.js";
|
|
6
6
|
import { CACHE_1H_MODES, CLAUDE_CODE_VERSION, DEFAULT_CACHE_1H_MODE, } from "./constants.js";
|
|
7
|
+
import { log } from "./logger.js";
|
|
7
8
|
export const ACCOUNT_FILE_NAME = 'anthropic-auth.json';
|
|
8
9
|
export const QUOTA_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
9
10
|
const DEFAULT_FALLBACK_ON = [401, 403, 429];
|
|
@@ -116,6 +117,7 @@ function normalizeStorage(value) {
|
|
|
116
117
|
claudeCache: isRecord(value.claudeCache) ? value.claudeCache : undefined,
|
|
117
118
|
dump: isRecord(value.dump) ? value.dump : undefined,
|
|
118
119
|
claudeFast: isRecord(value.claudeFast) ? value.claudeFast : undefined,
|
|
120
|
+
cacheKeep: isRecord(value.cacheKeep) ? value.cacheKeep : undefined,
|
|
119
121
|
relay: isRecord(value.relay) ? value.relay : undefined,
|
|
120
122
|
accounts: value.accounts
|
|
121
123
|
.map(normalizeAccount)
|
|
@@ -229,6 +231,33 @@ export async function setFastModePersistentEnabled(enabled, path = getAccountSto
|
|
|
229
231
|
await saveAccounts(storage, path);
|
|
230
232
|
return storage;
|
|
231
233
|
}
|
|
234
|
+
export async function setCacheKeepPersistentWindow(startHour, endHour, path = getAccountStoragePath()) {
|
|
235
|
+
const storage = (await loadAccounts(path)) ?? {
|
|
236
|
+
version: 1,
|
|
237
|
+
main: { type: 'opencode', provider: 'anthropic' },
|
|
238
|
+
accounts: [],
|
|
239
|
+
};
|
|
240
|
+
storage.cacheKeep = {
|
|
241
|
+
enabled: true,
|
|
242
|
+
startHour,
|
|
243
|
+
endHour,
|
|
244
|
+
};
|
|
245
|
+
await saveAccounts(storage, path);
|
|
246
|
+
return storage;
|
|
247
|
+
}
|
|
248
|
+
export async function setCacheKeepPersistentEnabled(enabled, path = getAccountStoragePath()) {
|
|
249
|
+
const storage = (await loadAccounts(path)) ?? {
|
|
250
|
+
version: 1,
|
|
251
|
+
main: { type: 'opencode', provider: 'anthropic' },
|
|
252
|
+
accounts: [],
|
|
253
|
+
};
|
|
254
|
+
storage.cacheKeep = {
|
|
255
|
+
...(storage.cacheKeep ?? {}),
|
|
256
|
+
enabled,
|
|
257
|
+
};
|
|
258
|
+
await saveAccounts(storage, path);
|
|
259
|
+
return storage;
|
|
260
|
+
}
|
|
232
261
|
function getFallbackStatuses(storage) {
|
|
233
262
|
return storage?.fallbackOn?.length ? storage.fallbackOn : DEFAULT_FALLBACK_ON;
|
|
234
263
|
}
|
|
@@ -278,7 +307,11 @@ function isTransientRefreshError(error) {
|
|
|
278
307
|
error.code === 'UND_ERR_CONNECT_TIMEOUT')));
|
|
279
308
|
}
|
|
280
309
|
export function buildRefreshOperationError(input) {
|
|
281
|
-
const
|
|
310
|
+
const tokenHash = hashRefreshToken(input.refreshToken);
|
|
311
|
+
const previousRetryCount = input.previous?.tokenHash === tokenHash
|
|
312
|
+
? (input.previous.retryCount ?? 0)
|
|
313
|
+
: 0;
|
|
314
|
+
const retryCount = previousRetryCount + 1;
|
|
282
315
|
const delay = isTransientRefreshError(input.error)
|
|
283
316
|
? Math.min(MAX_REFRESH_RETRY_DELAY_MS, MIN_REFRESH_RETRY_DELAY_MS * 2 ** Math.min(retryCount - 1, 6))
|
|
284
317
|
: NON_TRANSIENT_REFRESH_RETRY_DELAY_MS;
|
|
@@ -287,7 +320,7 @@ export function buildRefreshOperationError(input) {
|
|
|
287
320
|
checkedAt: input.now,
|
|
288
321
|
nextRetryAt: input.now + delay,
|
|
289
322
|
retryCount,
|
|
290
|
-
tokenHash
|
|
323
|
+
tokenHash,
|
|
291
324
|
};
|
|
292
325
|
}
|
|
293
326
|
export function refreshBackoffActive(error, refreshToken, now) {
|
|
@@ -525,13 +558,28 @@ export class FallbackAccountManager {
|
|
|
525
558
|
if (!tokenNeedsRefresh(account, storage, this.now()))
|
|
526
559
|
continue;
|
|
527
560
|
if (refreshBackoffActive(account.lastRefreshError, account.refresh, this.now())) {
|
|
561
|
+
log('[refresh] fallback oauth skipped backoff', {
|
|
562
|
+
accountId: account.id,
|
|
563
|
+
nextRetryAt: account.lastRefreshError?.nextRetryAt,
|
|
564
|
+
retryCount: account.lastRefreshError?.retryCount,
|
|
565
|
+
});
|
|
528
566
|
continue;
|
|
529
567
|
}
|
|
530
568
|
try {
|
|
569
|
+
log('[refresh] fallback oauth background due', {
|
|
570
|
+
accountId: account.id,
|
|
571
|
+
expiresInMs: account.expires
|
|
572
|
+
? account.expires - this.now()
|
|
573
|
+
: undefined,
|
|
574
|
+
});
|
|
531
575
|
await this.refreshAccount(account, storage);
|
|
532
576
|
changed = true;
|
|
533
577
|
}
|
|
534
578
|
catch (error) {
|
|
579
|
+
log('[refresh] fallback oauth background failed', {
|
|
580
|
+
accountId: account.id,
|
|
581
|
+
error: error instanceof Error ? error.message : String(error),
|
|
582
|
+
});
|
|
535
583
|
recordRefreshError(account, error, this.now());
|
|
536
584
|
updateStoredAccount(storage, account);
|
|
537
585
|
changed = true;
|
|
@@ -636,6 +684,13 @@ export class FallbackAccountManager {
|
|
|
636
684
|
}
|
|
637
685
|
const sourceAccount = latestAccount ?? account;
|
|
638
686
|
const refreshToken = sourceAccount.refresh;
|
|
687
|
+
log('[refresh] fallback oauth refresh request start', {
|
|
688
|
+
accountId: sourceAccount.id,
|
|
689
|
+
force: options.force === true,
|
|
690
|
+
expiresInMs: sourceAccount.expires
|
|
691
|
+
? sourceAccount.expires - this.now()
|
|
692
|
+
: undefined,
|
|
693
|
+
});
|
|
639
694
|
const refreshed = await refreshClaudeOAuthToken({
|
|
640
695
|
refreshToken,
|
|
641
696
|
fetchImpl: this.fetchImpl,
|
|
@@ -648,6 +703,12 @@ export class FallbackAccountManager {
|
|
|
648
703
|
refreshed.expires - refreshed.expiresIn * 1000;
|
|
649
704
|
sourceAccount.lastRefreshError = undefined;
|
|
650
705
|
updateStoredAccount(storage, sourceAccount);
|
|
706
|
+
log('[refresh] fallback oauth refresh succeeded', {
|
|
707
|
+
accountId: sourceAccount.id,
|
|
708
|
+
expiresInMs: sourceAccount.expires
|
|
709
|
+
? sourceAccount.expires - this.now()
|
|
710
|
+
: undefined,
|
|
711
|
+
});
|
|
651
712
|
return sourceAccount;
|
|
652
713
|
}
|
|
653
714
|
async refreshAccountQuota(account, storage) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { AccountStorage } from './accounts.ts';
|
|
2
|
+
export declare const CLAUDE_CACHE_KEEP_COMMAND_NAME = "claude-cachekeep";
|
|
3
|
+
export declare const CACHE_KEEP_TTL_MS: number;
|
|
4
|
+
export declare const CACHE_KEEP_PREWARM_LEAD_MS: number;
|
|
5
|
+
export declare const CACHE_KEEP_TICK_MS = 60000;
|
|
6
|
+
export declare const CACHE_KEEP_EXTENDED_TTL_BETA = "extended-cache-ttl-2025-04-11";
|
|
7
|
+
export type CacheKeepWindow = {
|
|
8
|
+
startHour: number;
|
|
9
|
+
endHour: number;
|
|
10
|
+
};
|
|
11
|
+
export type CacheKeepCommandAction = {
|
|
12
|
+
type: 'status';
|
|
13
|
+
} | {
|
|
14
|
+
type: 'disable';
|
|
15
|
+
} | {
|
|
16
|
+
type: 'window';
|
|
17
|
+
startHour: number;
|
|
18
|
+
endHour: number;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'usage';
|
|
21
|
+
};
|
|
22
|
+
export type CacheKeepStatus = {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
window?: CacheKeepWindow;
|
|
25
|
+
trackedSessions?: number;
|
|
26
|
+
nextPrewarmAt?: number;
|
|
27
|
+
hybridActive?: boolean;
|
|
28
|
+
};
|
|
29
|
+
export declare function parseCacheKeepCommandAction(input: string): CacheKeepCommandAction;
|
|
30
|
+
export declare function getCacheKeepWindow(storage: AccountStorage | null): CacheKeepWindow | undefined;
|
|
31
|
+
export declare function isCacheKeepPersistentlyEnabled(storage: AccountStorage | null): boolean;
|
|
32
|
+
export declare function isCacheKeepHybridActive(storage: AccountStorage | null): boolean;
|
|
33
|
+
export declare function isWithinCacheKeepWindow(window: CacheKeepWindow | undefined, now?: Date): boolean;
|
|
34
|
+
export declare function localDayKey(now?: Date): string;
|
|
35
|
+
export declare function buildCacheKeepStatusSummary(status: CacheKeepStatus): string;
|
|
36
|
+
export declare function executeCacheKeepCommand(input: {
|
|
37
|
+
argumentsText: string;
|
|
38
|
+
enabled?: boolean;
|
|
39
|
+
window?: CacheKeepWindow;
|
|
40
|
+
trackedSessions?: number;
|
|
41
|
+
nextPrewarmAt?: number;
|
|
42
|
+
hybridActive?: boolean;
|
|
43
|
+
}): string;
|
|
44
|
+
export declare function buildCacheKeepPrewarmBody(bodyText: string): Promise<{
|
|
45
|
+
ok: true;
|
|
46
|
+
bodyText: string;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: string;
|
|
50
|
+
}>;
|
|
51
|
+
export type CacheKeepTarget = {
|
|
52
|
+
id: string;
|
|
53
|
+
url: string;
|
|
54
|
+
headers: Record<string, string>;
|
|
55
|
+
bodyText: string;
|
|
56
|
+
cacheExpiresAt: number;
|
|
57
|
+
dayKey: string;
|
|
58
|
+
};
|
|
59
|
+
export declare class CacheKeepManager {
|
|
60
|
+
private readonly options;
|
|
61
|
+
private readonly targets;
|
|
62
|
+
private timer;
|
|
63
|
+
constructor(options: {
|
|
64
|
+
loadStorage: () => Promise<AccountStorage | null>;
|
|
65
|
+
fetchImpl?: typeof fetch;
|
|
66
|
+
now?: () => number;
|
|
67
|
+
prepareHeaders?: (headers: Headers, target: CacheKeepTarget) => Promise<Headers> | Headers;
|
|
68
|
+
log?: (message: string, data?: Record<string, unknown>) => void;
|
|
69
|
+
});
|
|
70
|
+
start(): void;
|
|
71
|
+
stop(): void;
|
|
72
|
+
stats(window?: CacheKeepWindow, now?: number): {
|
|
73
|
+
trackedSessions: number;
|
|
74
|
+
nextPrewarmAt: number | undefined;
|
|
75
|
+
};
|
|
76
|
+
track(input: {
|
|
77
|
+
sessionId?: string | null;
|
|
78
|
+
url: string;
|
|
79
|
+
headers: Headers;
|
|
80
|
+
bodyText: string;
|
|
81
|
+
storage: AccountStorage | null;
|
|
82
|
+
cacheMode: string;
|
|
83
|
+
}): {
|
|
84
|
+
tracked: boolean;
|
|
85
|
+
reason: string;
|
|
86
|
+
} | {
|
|
87
|
+
tracked: boolean;
|
|
88
|
+
reason?: undefined;
|
|
89
|
+
};
|
|
90
|
+
tick(): Promise<void>;
|
|
91
|
+
private prewarm;
|
|
92
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { CCH_PATTERN, signRequestBody } from "./cch.js";
|
|
2
|
+
import { orderClaudeCodeBody } from "./claude-code.js";
|
|
3
|
+
export const CLAUDE_CACHE_KEEP_COMMAND_NAME = 'claude-cachekeep';
|
|
4
|
+
export const CACHE_KEEP_TTL_MS = 60 * 60_000;
|
|
5
|
+
export const CACHE_KEEP_PREWARM_LEAD_MS = 5 * 60_000;
|
|
6
|
+
export const CACHE_KEEP_TICK_MS = 60_000;
|
|
7
|
+
export const CACHE_KEEP_EXTENDED_TTL_BETA = 'extended-cache-ttl-2025-04-11';
|
|
8
|
+
const STATUS_TITLE = '## Claude Cache Keep Status';
|
|
9
|
+
const ENABLED_TITLE = '## Claude Cache Keep Enabled';
|
|
10
|
+
const DISABLED_TITLE = '## Claude Cache Keep Disabled';
|
|
11
|
+
const USAGE_TITLE = '## Claude Cache Keep Usage';
|
|
12
|
+
const USAGE = 'Usage: `/claude-cachekeep`, `/claude-cachekeep off`, or `/claude-cachekeep HH-HH`.';
|
|
13
|
+
function padHour(hour) {
|
|
14
|
+
return String(hour).padStart(2, '0');
|
|
15
|
+
}
|
|
16
|
+
export function parseCacheKeepCommandAction(input) {
|
|
17
|
+
const trimmed = input.trim();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
return { type: 'status' };
|
|
20
|
+
if (trimmed === 'off')
|
|
21
|
+
return { type: 'disable' };
|
|
22
|
+
const match = /^(\d{1,2})-(\d{1,2})$/.exec(trimmed);
|
|
23
|
+
if (!match)
|
|
24
|
+
return { type: 'usage' };
|
|
25
|
+
const startHour = Number(match[1]);
|
|
26
|
+
const endHour = Number(match[2]);
|
|
27
|
+
if (!Number.isInteger(startHour) ||
|
|
28
|
+
!Number.isInteger(endHour) ||
|
|
29
|
+
startHour < 0 ||
|
|
30
|
+
startHour > 23 ||
|
|
31
|
+
endHour < 0 ||
|
|
32
|
+
endHour > 23 ||
|
|
33
|
+
startHour === endHour) {
|
|
34
|
+
return { type: 'usage' };
|
|
35
|
+
}
|
|
36
|
+
return { type: 'window', startHour, endHour };
|
|
37
|
+
}
|
|
38
|
+
export function getCacheKeepWindow(storage) {
|
|
39
|
+
const startHour = Number(storage?.cacheKeep?.startHour);
|
|
40
|
+
const endHour = Number(storage?.cacheKeep?.endHour);
|
|
41
|
+
if (!Number.isInteger(startHour) ||
|
|
42
|
+
!Number.isInteger(endHour) ||
|
|
43
|
+
startHour < 0 ||
|
|
44
|
+
startHour > 23 ||
|
|
45
|
+
endHour < 0 ||
|
|
46
|
+
endHour > 23 ||
|
|
47
|
+
startHour === endHour) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return { startHour, endHour };
|
|
51
|
+
}
|
|
52
|
+
export function isCacheKeepPersistentlyEnabled(storage) {
|
|
53
|
+
return storage?.cacheKeep?.enabled === true && !!getCacheKeepWindow(storage);
|
|
54
|
+
}
|
|
55
|
+
export function isCacheKeepHybridActive(storage) {
|
|
56
|
+
return (isCacheKeepPersistentlyEnabled(storage) &&
|
|
57
|
+
storage?.claudeCache?.enabled === true &&
|
|
58
|
+
storage.claudeCache.mode === 'hybrid');
|
|
59
|
+
}
|
|
60
|
+
export function isWithinCacheKeepWindow(window, now = new Date()) {
|
|
61
|
+
if (!window)
|
|
62
|
+
return false;
|
|
63
|
+
const hour = now.getHours();
|
|
64
|
+
if (window.startHour < window.endHour) {
|
|
65
|
+
return hour >= window.startHour && hour < window.endHour;
|
|
66
|
+
}
|
|
67
|
+
return hour >= window.startHour || hour < window.endHour;
|
|
68
|
+
}
|
|
69
|
+
export function localDayKey(now = new Date()) {
|
|
70
|
+
return `${now.getFullYear()}-${padHour(now.getMonth() + 1)}-${padHour(now.getDate())}`;
|
|
71
|
+
}
|
|
72
|
+
function localWindowKey(window, now = new Date()) {
|
|
73
|
+
if (window &&
|
|
74
|
+
window.startHour > window.endHour &&
|
|
75
|
+
now.getHours() < window.endHour) {
|
|
76
|
+
const previous = new Date(now);
|
|
77
|
+
previous.setDate(previous.getDate() - 1);
|
|
78
|
+
return localDayKey(previous);
|
|
79
|
+
}
|
|
80
|
+
return localDayKey(now);
|
|
81
|
+
}
|
|
82
|
+
export function buildCacheKeepStatusSummary(status) {
|
|
83
|
+
const window = status.window
|
|
84
|
+
? `${padHour(status.window.startHour)}-${padHour(status.window.endHour)}`
|
|
85
|
+
: 'not configured';
|
|
86
|
+
const lines = [
|
|
87
|
+
`Cache keep: ${status.enabled ? 'enabled' : 'disabled'}`,
|
|
88
|
+
`Window: ${window}`,
|
|
89
|
+
'Mode requirement: `/claude-cache mode hybrid` must be active.',
|
|
90
|
+
];
|
|
91
|
+
if (typeof status.hybridActive === 'boolean') {
|
|
92
|
+
lines.push(`Hybrid active: ${status.hybridActive ? 'yes' : 'no'}`);
|
|
93
|
+
}
|
|
94
|
+
if (typeof status.trackedSessions === 'number') {
|
|
95
|
+
lines.push(`Tracked sessions: ${status.trackedSessions}`);
|
|
96
|
+
}
|
|
97
|
+
if (status.nextPrewarmAt) {
|
|
98
|
+
lines.push(`Next prewarm: ${new Date(status.nextPrewarmAt).toLocaleString()}`);
|
|
99
|
+
}
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|
|
102
|
+
export function executeCacheKeepCommand(input) {
|
|
103
|
+
const action = parseCacheKeepCommandAction(input.argumentsText);
|
|
104
|
+
const status = {
|
|
105
|
+
enabled: input.enabled ?? false,
|
|
106
|
+
window: input.window,
|
|
107
|
+
trackedSessions: input.trackedSessions,
|
|
108
|
+
nextPrewarmAt: input.nextPrewarmAt,
|
|
109
|
+
hybridActive: input.hybridActive,
|
|
110
|
+
};
|
|
111
|
+
if (action.type === 'status') {
|
|
112
|
+
return [STATUS_TITLE, '', buildCacheKeepStatusSummary(status)].join('\n');
|
|
113
|
+
}
|
|
114
|
+
if (action.type === 'disable') {
|
|
115
|
+
return [
|
|
116
|
+
DISABLED_TITLE,
|
|
117
|
+
'',
|
|
118
|
+
buildCacheKeepStatusSummary({ ...status, enabled: false }),
|
|
119
|
+
].join('\n');
|
|
120
|
+
}
|
|
121
|
+
if (action.type === 'window') {
|
|
122
|
+
return [
|
|
123
|
+
ENABLED_TITLE,
|
|
124
|
+
'',
|
|
125
|
+
buildCacheKeepStatusSummary({
|
|
126
|
+
...status,
|
|
127
|
+
enabled: true,
|
|
128
|
+
window: { startHour: action.startHour, endHour: action.endHour },
|
|
129
|
+
}),
|
|
130
|
+
].join('\n');
|
|
131
|
+
}
|
|
132
|
+
return [USAGE_TITLE, '', USAGE, '', buildCacheKeepStatusSummary(status)].join('\n');
|
|
133
|
+
}
|
|
134
|
+
function hasExplicitCacheControl(value) {
|
|
135
|
+
if (!value || typeof value !== 'object')
|
|
136
|
+
return false;
|
|
137
|
+
if (Array.isArray(value))
|
|
138
|
+
return value.some(hasExplicitCacheControl);
|
|
139
|
+
const record = value;
|
|
140
|
+
const cacheControl = record.cache_control;
|
|
141
|
+
if (cacheControl &&
|
|
142
|
+
typeof cacheControl === 'object' &&
|
|
143
|
+
cacheControl.type === 'ephemeral') {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
return Object.values(record).some(hasExplicitCacheControl);
|
|
147
|
+
}
|
|
148
|
+
export async function buildCacheKeepPrewarmBody(bodyText) {
|
|
149
|
+
let body;
|
|
150
|
+
try {
|
|
151
|
+
body = JSON.parse(bodyText);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return { ok: false, reason: 'body is not valid JSON' };
|
|
155
|
+
}
|
|
156
|
+
if (!hasExplicitCacheControl(body)) {
|
|
157
|
+
return { ok: false, reason: 'body has no explicit cache breakpoints' };
|
|
158
|
+
}
|
|
159
|
+
const warm = structuredClone(body);
|
|
160
|
+
warm.max_tokens = 0;
|
|
161
|
+
delete warm.stream;
|
|
162
|
+
const thinking = warm.thinking;
|
|
163
|
+
if (thinking?.type === 'enabled')
|
|
164
|
+
delete warm.thinking;
|
|
165
|
+
const outputConfig = warm.output_config;
|
|
166
|
+
if (outputConfig?.format)
|
|
167
|
+
delete warm.output_config;
|
|
168
|
+
const toolChoice = warm.tool_choice;
|
|
169
|
+
if (toolChoice?.type === 'tool' || toolChoice?.type === 'any') {
|
|
170
|
+
delete warm.tool_choice;
|
|
171
|
+
}
|
|
172
|
+
const unsignedBodyText = JSON.stringify(orderClaudeCodeBody(warm)).replace(CCH_PATTERN, 'cch=00000;');
|
|
173
|
+
const signedBodyText = await signRequestBody(unsignedBodyText);
|
|
174
|
+
return { ok: true, bodyText: signedBodyText };
|
|
175
|
+
}
|
|
176
|
+
export class CacheKeepManager {
|
|
177
|
+
options;
|
|
178
|
+
targets = new Map();
|
|
179
|
+
timer = null;
|
|
180
|
+
constructor(options) {
|
|
181
|
+
this.options = options;
|
|
182
|
+
}
|
|
183
|
+
start() {
|
|
184
|
+
if (this.timer)
|
|
185
|
+
return;
|
|
186
|
+
this.timer = setInterval(() => {
|
|
187
|
+
void this.tick().catch((error) => {
|
|
188
|
+
this.options.log?.('[cachekeep] tick failed', {
|
|
189
|
+
error: error instanceof Error ? error.message : String(error),
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}, CACHE_KEEP_TICK_MS);
|
|
193
|
+
if ('unref' in this.timer)
|
|
194
|
+
this.timer.unref();
|
|
195
|
+
}
|
|
196
|
+
stop() {
|
|
197
|
+
if (this.timer)
|
|
198
|
+
clearInterval(this.timer);
|
|
199
|
+
this.timer = null;
|
|
200
|
+
}
|
|
201
|
+
stats(window, now = this.options.now?.() ?? Date.now()) {
|
|
202
|
+
const today = localWindowKey(window, new Date(now));
|
|
203
|
+
const targets = [...this.targets.values()].filter((target) => target.dayKey === today);
|
|
204
|
+
const nextPrewarmAt = targets.length
|
|
205
|
+
? Math.min(...targets.map((target) => target.cacheExpiresAt - CACHE_KEEP_PREWARM_LEAD_MS))
|
|
206
|
+
: undefined;
|
|
207
|
+
return { trackedSessions: targets.length, nextPrewarmAt };
|
|
208
|
+
}
|
|
209
|
+
track(input) {
|
|
210
|
+
if (!input.sessionId)
|
|
211
|
+
return { tracked: false, reason: 'missing session id' };
|
|
212
|
+
if (!isCacheKeepPersistentlyEnabled(input.storage)) {
|
|
213
|
+
return { tracked: false, reason: 'cachekeep disabled' };
|
|
214
|
+
}
|
|
215
|
+
if (input.cacheMode !== 'hybrid') {
|
|
216
|
+
return { tracked: false, reason: 'cache mode is not hybrid' };
|
|
217
|
+
}
|
|
218
|
+
const now = this.options.now?.() ?? Date.now();
|
|
219
|
+
const window = getCacheKeepWindow(input.storage);
|
|
220
|
+
if (!isWithinCacheKeepWindow(window, new Date(now))) {
|
|
221
|
+
return { tracked: false, reason: 'outside configured window' };
|
|
222
|
+
}
|
|
223
|
+
const headers = {};
|
|
224
|
+
input.headers.forEach((value, key) => {
|
|
225
|
+
headers[key] = value;
|
|
226
|
+
});
|
|
227
|
+
this.targets.set(input.sessionId, {
|
|
228
|
+
id: input.sessionId,
|
|
229
|
+
url: input.url,
|
|
230
|
+
headers,
|
|
231
|
+
bodyText: input.bodyText,
|
|
232
|
+
cacheExpiresAt: now + CACHE_KEEP_TTL_MS,
|
|
233
|
+
dayKey: localWindowKey(window, new Date(now)),
|
|
234
|
+
});
|
|
235
|
+
this.start();
|
|
236
|
+
return { tracked: true };
|
|
237
|
+
}
|
|
238
|
+
async tick() {
|
|
239
|
+
const storage = await this.options.loadStorage();
|
|
240
|
+
const window = getCacheKeepWindow(storage);
|
|
241
|
+
const now = this.options.now?.() ?? Date.now();
|
|
242
|
+
const today = localWindowKey(window, new Date(now));
|
|
243
|
+
for (const [id, target] of this.targets) {
|
|
244
|
+
if (target.dayKey !== today)
|
|
245
|
+
this.targets.delete(id);
|
|
246
|
+
}
|
|
247
|
+
if (!this.targets.size) {
|
|
248
|
+
this.stop();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!isCacheKeepHybridActive(storage))
|
|
252
|
+
return;
|
|
253
|
+
if (!isWithinCacheKeepWindow(window, new Date(now)))
|
|
254
|
+
return;
|
|
255
|
+
const dueAt = now + CACHE_KEEP_PREWARM_LEAD_MS;
|
|
256
|
+
for (const target of this.targets.values()) {
|
|
257
|
+
if (target.cacheExpiresAt > dueAt)
|
|
258
|
+
continue;
|
|
259
|
+
await this.prewarm(target, now);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async prewarm(target, now) {
|
|
263
|
+
const prewarm = await buildCacheKeepPrewarmBody(target.bodyText);
|
|
264
|
+
if (!prewarm.ok) {
|
|
265
|
+
this.options.log?.('[cachekeep] prewarm skipped', {
|
|
266
|
+
session: target.id,
|
|
267
|
+
reason: prewarm.reason,
|
|
268
|
+
});
|
|
269
|
+
this.targets.delete(target.id);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const fetchImpl = this.options.fetchImpl ?? fetch;
|
|
273
|
+
const prewarmTarget = { ...target, bodyText: prewarm.bodyText };
|
|
274
|
+
const headers = this.options.prepareHeaders
|
|
275
|
+
? await this.options.prepareHeaders(new Headers(target.headers), prewarmTarget)
|
|
276
|
+
: new Headers(target.headers);
|
|
277
|
+
headers.delete('content-length');
|
|
278
|
+
headers.delete('transfer-encoding');
|
|
279
|
+
const response = await fetchImpl(target.url, {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers,
|
|
282
|
+
body: prewarm.bodyText,
|
|
283
|
+
});
|
|
284
|
+
if (!response.ok) {
|
|
285
|
+
this.options.log?.('[cachekeep] prewarm failed', {
|
|
286
|
+
session: target.id,
|
|
287
|
+
status: response.status,
|
|
288
|
+
body: await response.text().catch(() => ''),
|
|
289
|
+
});
|
|
290
|
+
target.cacheExpiresAt = now + CACHE_KEEP_PREWARM_LEAD_MS + 5 * 60_000;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
await response.body?.cancel().catch(() => { });
|
|
294
|
+
target.cacheExpiresAt = now + CACHE_KEEP_TTL_MS;
|
|
295
|
+
this.options.log?.('[cachekeep] prewarm succeeded', { session: target.id });
|
|
296
|
+
}
|
|
297
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED