@cortexkit/anthropic-auth-core 1.7.0 → 1.8.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/dist/accounts.d.ts +42 -7
- package/dist/accounts.js +269 -34
- package/dist/cachekeep.d.ts +6 -0
- package/dist/cachekeep.js +40 -5
- package/dist/claude-code.js +25 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models.d.ts +20 -0
- package/dist/models.js +31 -0
- package/dist/quota-manager.d.ts +1 -0
- package/dist/quota-manager.js +32 -13
- package/dist/quotas.js +2 -1
- package/dist/relay.js +133 -8
- package/package.json +1 -1
package/dist/accounts.d.ts
CHANGED
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
import { type Cache1hMode } from './constants.ts';
|
|
2
2
|
export declare const ACCOUNT_FILE_NAME = "anthropic-auth.json";
|
|
3
|
+
export declare const ACCOUNT_STATE_FILE_NAME = "anthropic-auth-state.json";
|
|
3
4
|
export declare const QUOTA_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
4
5
|
export type QuotaWindowName = 'five_hour' | 'seven_day';
|
|
5
|
-
export type
|
|
6
|
+
export type AccountBase = {
|
|
6
7
|
id: string;
|
|
7
8
|
label?: string;
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
addedAt?: number;
|
|
11
|
+
lastUsed?: number;
|
|
12
|
+
};
|
|
13
|
+
export type OAuthAccount = AccountBase & {
|
|
8
14
|
type: 'oauth';
|
|
9
15
|
access?: string;
|
|
10
16
|
refresh: string;
|
|
11
17
|
expires?: number;
|
|
12
|
-
enabled?: boolean;
|
|
13
|
-
addedAt?: number;
|
|
14
|
-
lastUsed?: number;
|
|
15
18
|
lastRefreshedAt?: number;
|
|
16
19
|
lastRefreshError?: AccountOperationError;
|
|
17
20
|
lastQuotaRefreshError?: AccountOperationError;
|
|
18
21
|
quota?: Partial<Record<QuotaWindowName, AccountQuotaWindow>>;
|
|
19
22
|
};
|
|
23
|
+
export type ApiKeyAccount = AccountBase & {
|
|
24
|
+
type: 'api';
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
baseURL: string;
|
|
27
|
+
authHeader?: 'authorization-bearer' | 'x-api-key';
|
|
28
|
+
};
|
|
29
|
+
export type FallbackAccount = OAuthAccount | ApiKeyAccount;
|
|
30
|
+
export declare function isOAuthAccount(account: FallbackAccount): account is OAuthAccount;
|
|
31
|
+
export declare function isApiKeyAccount(account: FallbackAccount): account is ApiKeyAccount;
|
|
32
|
+
export declare function isValidApiBaseURL(value: string | undefined): boolean;
|
|
20
33
|
export type AccountOperationError = {
|
|
21
34
|
message: string;
|
|
22
35
|
checkedAt: number;
|
|
@@ -94,7 +107,27 @@ export type AccountStorage = {
|
|
|
94
107
|
transport?: 'http' | 'websocket';
|
|
95
108
|
};
|
|
96
109
|
killswitch?: KillswitchConfig;
|
|
97
|
-
accounts:
|
|
110
|
+
accounts: FallbackAccount[];
|
|
111
|
+
};
|
|
112
|
+
export type AccountRuntimeEntry = Partial<Pick<OAuthAccount, 'access' | 'refresh' | 'expires' | 'lastUsed' | 'lastRefreshedAt' | 'lastRefreshError' | 'lastQuotaRefreshError' | 'quota'> & Pick<ApiKeyAccount, 'apiKey' | 'lastUsed'>>;
|
|
113
|
+
export type AccountRuntimeState = {
|
|
114
|
+
version: 1;
|
|
115
|
+
main?: {
|
|
116
|
+
quota?: OAuthQuotaSnapshot;
|
|
117
|
+
quotaCheckedAt?: number;
|
|
118
|
+
quotaToken?: string;
|
|
119
|
+
lastQuotaApiError?: AccountOperationError;
|
|
120
|
+
lastRefreshError?: AccountOperationError;
|
|
121
|
+
refreshLeaseId?: string;
|
|
122
|
+
refreshLeaseUntil?: number;
|
|
123
|
+
refreshLeaseTokenHash?: string;
|
|
124
|
+
};
|
|
125
|
+
accounts?: Record<string, AccountRuntimeEntry>;
|
|
126
|
+
};
|
|
127
|
+
export type AccountStateSaveScope = {
|
|
128
|
+
mainQuota?: boolean;
|
|
129
|
+
mainRefresh?: boolean;
|
|
130
|
+
accounts?: true | string[];
|
|
98
131
|
};
|
|
99
132
|
export type OAuthQuotaSnapshot = Partial<Record<QuotaWindowName, AccountQuotaWindow>>;
|
|
100
133
|
export type AccountManagerOptions = {
|
|
@@ -109,8 +142,10 @@ export type AccountRefreshError = {
|
|
|
109
142
|
message: string;
|
|
110
143
|
};
|
|
111
144
|
export declare function getAccountStoragePath(): string;
|
|
145
|
+
export declare function getAccountStatePath(configPath?: string): string;
|
|
112
146
|
export declare function loadAccounts(path?: string): Promise<AccountStorage | null>;
|
|
113
147
|
export declare function saveAccounts(storage: AccountStorage, path?: string): Promise<void>;
|
|
148
|
+
export declare function saveAccountState(storage: AccountStorage, path?: string, scope?: AccountStateSaveScope): Promise<void>;
|
|
114
149
|
export declare function acquireRefreshFileLock(options: {
|
|
115
150
|
name: string;
|
|
116
151
|
ttlMs: number;
|
|
@@ -202,11 +237,11 @@ export declare class FallbackAccountManager {
|
|
|
202
237
|
*/
|
|
203
238
|
private seedFallbackQuota;
|
|
204
239
|
load(): Promise<AccountStorage | null>;
|
|
205
|
-
save(storage: AccountStorage): Promise<void>;
|
|
240
|
+
save(storage: AccountStorage, accountIds?: string[]): Promise<void>;
|
|
206
241
|
startBackgroundRefresh(): void;
|
|
207
242
|
stopBackgroundRefresh(): void;
|
|
208
243
|
getUsableFallbackAccounts(existingStorage?: AccountStorage | null): Promise<OAuthAccount[]>;
|
|
209
|
-
markUsed(account:
|
|
244
|
+
markUsed(account: FallbackAccount): Promise<void>;
|
|
210
245
|
accountPassesQuotaPolicy(account: OAuthAccount, storage: AccountStorage | null): boolean;
|
|
211
246
|
/**
|
|
212
247
|
* Return the account with its quota overlaid from the unified QuotaManager
|
package/dist/accounts.js
CHANGED
|
@@ -8,7 +8,28 @@ import { log } from "./logger.js";
|
|
|
8
8
|
const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis);
|
|
9
9
|
const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis);
|
|
10
10
|
export const ACCOUNT_FILE_NAME = 'anthropic-auth.json';
|
|
11
|
+
export const ACCOUNT_STATE_FILE_NAME = 'anthropic-auth-state.json';
|
|
11
12
|
export const QUOTA_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
13
|
+
export function isOAuthAccount(account) {
|
|
14
|
+
return account.type === 'oauth';
|
|
15
|
+
}
|
|
16
|
+
export function isApiKeyAccount(account) {
|
|
17
|
+
return account.type === 'api';
|
|
18
|
+
}
|
|
19
|
+
export function isValidApiBaseURL(value) {
|
|
20
|
+
const raw = value?.trim();
|
|
21
|
+
if (!raw)
|
|
22
|
+
return false;
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(raw);
|
|
25
|
+
return ((url.protocol === 'http:' || url.protocol === 'https:') &&
|
|
26
|
+
!url.username &&
|
|
27
|
+
!url.password);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
12
33
|
const DEFAULT_FALLBACK_ON = [401, 403, 429];
|
|
13
34
|
const MIN_REFRESH_BEFORE_EXPIRY_MINUTES = 240;
|
|
14
35
|
const DEFAULT_REFRESH_BEFORE_EXPIRY_MINUTES = MIN_REFRESH_BEFORE_EXPIRY_MINUTES;
|
|
@@ -40,28 +61,55 @@ export function getAccountStoragePath() {
|
|
|
40
61
|
return (process.env.OPENCODE_ANTHROPIC_AUTH_FILE?.trim() ||
|
|
41
62
|
join(getConfigDir(), ACCOUNT_FILE_NAME));
|
|
42
63
|
}
|
|
64
|
+
export function getAccountStatePath(configPath = getAccountStoragePath()) {
|
|
65
|
+
const explicit = process.env.OPENCODE_ANTHROPIC_AUTH_STATE_FILE?.trim();
|
|
66
|
+
if (explicit)
|
|
67
|
+
return explicit;
|
|
68
|
+
return configPath.endsWith(ACCOUNT_FILE_NAME)
|
|
69
|
+
? join(dirname(configPath), ACCOUNT_STATE_FILE_NAME)
|
|
70
|
+
: `${configPath}.state.json`;
|
|
71
|
+
}
|
|
43
72
|
function isRecord(value) {
|
|
44
73
|
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
45
74
|
}
|
|
75
|
+
function normalizeAccountBase(value) {
|
|
76
|
+
return {
|
|
77
|
+
id: typeof value.id === 'string' && value.id.trim()
|
|
78
|
+
? value.id.trim()
|
|
79
|
+
: randomUUID(),
|
|
80
|
+
label: typeof value.label === 'string' ? value.label : undefined,
|
|
81
|
+
enabled: typeof value.enabled === 'boolean' ? value.enabled : undefined,
|
|
82
|
+
addedAt: typeof value.addedAt === 'number' ? value.addedAt : undefined,
|
|
83
|
+
lastUsed: typeof value.lastUsed === 'number' ? value.lastUsed : undefined,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
46
86
|
function normalizeAccount(value) {
|
|
47
87
|
if (!isRecord(value))
|
|
48
88
|
return null;
|
|
89
|
+
if (value.type === 'api') {
|
|
90
|
+
const baseURL = typeof value.baseURL === 'string' ? value.baseURL.trim() : '';
|
|
91
|
+
const apiKey = typeof value.apiKey === 'string' ? value.apiKey.trim() : '';
|
|
92
|
+
if (!isValidApiBaseURL(baseURL))
|
|
93
|
+
return null;
|
|
94
|
+
const authHeader = value.authHeader === 'x-api-key' ? 'x-api-key' : 'authorization-bearer';
|
|
95
|
+
return {
|
|
96
|
+
...normalizeAccountBase(value),
|
|
97
|
+
type: 'api',
|
|
98
|
+
apiKey: apiKey || undefined,
|
|
99
|
+
baseURL,
|
|
100
|
+
authHeader,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
49
103
|
if (value.type !== 'oauth')
|
|
50
104
|
return null;
|
|
51
105
|
if (typeof value.refresh !== 'string' || !value.refresh.trim())
|
|
52
106
|
return null;
|
|
53
107
|
return {
|
|
54
|
-
|
|
55
|
-
? value.id.trim()
|
|
56
|
-
: randomUUID(),
|
|
57
|
-
label: typeof value.label === 'string' ? value.label : undefined,
|
|
108
|
+
...normalizeAccountBase(value),
|
|
58
109
|
type: 'oauth',
|
|
59
110
|
access: typeof value.access === 'string' ? value.access : undefined,
|
|
60
111
|
refresh: value.refresh,
|
|
61
112
|
expires: typeof value.expires === 'number' ? value.expires : undefined,
|
|
62
|
-
enabled: typeof value.enabled === 'boolean' ? value.enabled : undefined,
|
|
63
|
-
addedAt: typeof value.addedAt === 'number' ? value.addedAt : undefined,
|
|
64
|
-
lastUsed: typeof value.lastUsed === 'number' ? value.lastUsed : undefined,
|
|
65
113
|
lastRefreshedAt: typeof value.lastRefreshedAt === 'number'
|
|
66
114
|
? value.lastRefreshedAt
|
|
67
115
|
: undefined,
|
|
@@ -136,41 +184,226 @@ function normalizeStorage(value) {
|
|
|
136
184
|
.filter((account) => account != null),
|
|
137
185
|
};
|
|
138
186
|
}
|
|
139
|
-
|
|
187
|
+
async function readJsonIfPresent(path) {
|
|
140
188
|
try {
|
|
141
|
-
|
|
142
|
-
return normalizeStorage(JSON.parse(raw));
|
|
189
|
+
return { exists: true, value: JSON.parse(await readFile(path, 'utf8')) };
|
|
143
190
|
}
|
|
144
191
|
catch (error) {
|
|
145
|
-
if (error.code === 'ENOENT')
|
|
146
|
-
return null;
|
|
147
|
-
|
|
192
|
+
if (error.code === 'ENOENT') {
|
|
193
|
+
return { exists: false, value: null };
|
|
194
|
+
}
|
|
195
|
+
return { exists: false, value: null };
|
|
148
196
|
}
|
|
149
197
|
}
|
|
198
|
+
function objectWithDefinedEntries(value) {
|
|
199
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
200
|
+
}
|
|
201
|
+
function mergeConfigAndState(configValue, stateValue) {
|
|
202
|
+
if (!isRecord(configValue))
|
|
203
|
+
return configValue;
|
|
204
|
+
const state = isRecord(stateValue) ? stateValue : {};
|
|
205
|
+
const mainState = isRecord(state.main) ? state.main : undefined;
|
|
206
|
+
const stateAccounts = isRecord(state.accounts) ? state.accounts : {};
|
|
207
|
+
const quotaConfig = isRecord(configValue.quota) ? configValue.quota : {};
|
|
208
|
+
const refreshConfig = isRecord(configValue.refresh) ? configValue.refresh : {};
|
|
209
|
+
const mainQuotaSource = mainState ?? quotaConfig;
|
|
210
|
+
const mainRefreshSource = mainState ?? refreshConfig;
|
|
211
|
+
const accounts = Array.isArray(configValue.accounts)
|
|
212
|
+
? configValue.accounts.map((account) => {
|
|
213
|
+
if (!isRecord(account))
|
|
214
|
+
return account;
|
|
215
|
+
const stateAccount = typeof account.id === 'string' && isRecord(stateAccounts[account.id])
|
|
216
|
+
? stateAccounts[account.id]
|
|
217
|
+
: {};
|
|
218
|
+
return { ...account, ...stateAccount };
|
|
219
|
+
})
|
|
220
|
+
: [];
|
|
221
|
+
return {
|
|
222
|
+
...configValue,
|
|
223
|
+
refresh: objectWithDefinedEntries({
|
|
224
|
+
...refreshConfig,
|
|
225
|
+
mainLastRefreshError: mainRefreshSource.lastRefreshError,
|
|
226
|
+
mainRefreshLeaseId: mainRefreshSource.refreshLeaseId,
|
|
227
|
+
mainRefreshLeaseUntil: mainRefreshSource.refreshLeaseUntil,
|
|
228
|
+
mainRefreshLeaseTokenHash: mainRefreshSource.refreshLeaseTokenHash,
|
|
229
|
+
}),
|
|
230
|
+
quota: objectWithDefinedEntries({
|
|
231
|
+
...quotaConfig,
|
|
232
|
+
mainQuota: mainQuotaSource.quota,
|
|
233
|
+
mainQuotaCheckedAt: mainQuotaSource.quotaCheckedAt,
|
|
234
|
+
mainQuotaToken: mainQuotaSource.quotaToken,
|
|
235
|
+
mainLastQuotaApiError: mainQuotaSource.lastQuotaApiError,
|
|
236
|
+
}),
|
|
237
|
+
accounts,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
export async function loadAccounts(path = getAccountStoragePath()) {
|
|
241
|
+
const config = await readJsonIfPresent(path);
|
|
242
|
+
if (!config.exists)
|
|
243
|
+
return null;
|
|
244
|
+
const state = await readJsonIfPresent(getAccountStatePath(path));
|
|
245
|
+
return normalizeStorage(mergeConfigAndState(config.value, state.value));
|
|
246
|
+
}
|
|
150
247
|
async function loadExistingTopLevelFields(path) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const parsed = JSON.parse(raw);
|
|
154
|
-
return isRecord(parsed) ? parsed : {};
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return {};
|
|
158
|
-
}
|
|
248
|
+
const existing = await readJsonIfPresent(path);
|
|
249
|
+
return isRecord(existing.value) ? existing.value : {};
|
|
159
250
|
}
|
|
160
251
|
function omitUndefinedTopLevel(value) {
|
|
161
252
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
162
253
|
}
|
|
163
|
-
|
|
254
|
+
function accountConfig(account) {
|
|
255
|
+
return objectWithDefinedEntries({
|
|
256
|
+
id: account.id,
|
|
257
|
+
label: account.label,
|
|
258
|
+
type: account.type,
|
|
259
|
+
enabled: account.enabled,
|
|
260
|
+
addedAt: account.addedAt,
|
|
261
|
+
baseURL: account.type === 'api' ? account.baseURL : undefined,
|
|
262
|
+
authHeader: account.type === 'api' ? account.authHeader : undefined,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function accountRuntimeState(account) {
|
|
266
|
+
if (account.type === 'api') {
|
|
267
|
+
return objectWithDefinedEntries({
|
|
268
|
+
apiKey: account.apiKey,
|
|
269
|
+
lastUsed: account.lastUsed,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return objectWithDefinedEntries({
|
|
273
|
+
access: account.access,
|
|
274
|
+
refresh: account.refresh,
|
|
275
|
+
expires: account.expires,
|
|
276
|
+
lastUsed: account.lastUsed,
|
|
277
|
+
lastRefreshedAt: account.lastRefreshedAt,
|
|
278
|
+
lastRefreshError: account.lastRefreshError,
|
|
279
|
+
lastQuotaRefreshError: account.lastQuotaRefreshError,
|
|
280
|
+
quota: account.quota,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function configFromStorage(storage) {
|
|
284
|
+
const refresh = storage.refresh
|
|
285
|
+
? objectWithDefinedEntries({
|
|
286
|
+
enabled: storage.refresh.enabled,
|
|
287
|
+
intervalMinutes: storage.refresh.intervalMinutes,
|
|
288
|
+
refreshBeforeExpiryMinutes: storage.refresh.refreshBeforeExpiryMinutes,
|
|
289
|
+
})
|
|
290
|
+
: undefined;
|
|
291
|
+
const quota = storage.quota
|
|
292
|
+
? objectWithDefinedEntries({
|
|
293
|
+
enabled: storage.quota.enabled,
|
|
294
|
+
checkIntervalMinutes: storage.quota.checkIntervalMinutes,
|
|
295
|
+
refreshEveryNRequests: storage.quota.refreshEveryNRequests,
|
|
296
|
+
minimumRemaining: storage.quota.minimumRemaining,
|
|
297
|
+
failClosedOnUnknownQuota: storage.quota.failClosedOnUnknownQuota,
|
|
298
|
+
showToasts: storage.quota.showToasts,
|
|
299
|
+
})
|
|
300
|
+
: undefined;
|
|
301
|
+
return omitUndefinedTopLevel({
|
|
302
|
+
version: 1,
|
|
303
|
+
main: storage.main,
|
|
304
|
+
routing: storage.routing,
|
|
305
|
+
fallbackOn: storage.fallbackOn,
|
|
306
|
+
refresh,
|
|
307
|
+
quota,
|
|
308
|
+
claudeCache: storage.claudeCache,
|
|
309
|
+
dump: storage.dump,
|
|
310
|
+
claudeFast: storage.claudeFast,
|
|
311
|
+
cacheKeep: storage.cacheKeep,
|
|
312
|
+
relay: storage.relay,
|
|
313
|
+
killswitch: storage.killswitch,
|
|
314
|
+
accounts: storage.accounts.map(accountConfig),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function stateFromStorage(storage) {
|
|
318
|
+
const accounts = Object.fromEntries(storage.accounts.map((account) => [
|
|
319
|
+
account.id,
|
|
320
|
+
accountRuntimeState(account),
|
|
321
|
+
]));
|
|
322
|
+
return {
|
|
323
|
+
version: 1,
|
|
324
|
+
main: objectWithDefinedEntries({
|
|
325
|
+
quota: storage.quota?.mainQuota,
|
|
326
|
+
quotaCheckedAt: storage.quota?.mainQuotaCheckedAt,
|
|
327
|
+
quotaToken: storage.quota?.mainQuotaToken,
|
|
328
|
+
lastQuotaApiError: storage.quota?.mainLastQuotaApiError,
|
|
329
|
+
lastRefreshError: storage.refresh?.mainLastRefreshError,
|
|
330
|
+
refreshLeaseId: storage.refresh?.mainRefreshLeaseId,
|
|
331
|
+
refreshLeaseUntil: storage.refresh?.mainRefreshLeaseUntil,
|
|
332
|
+
refreshLeaseTokenHash: storage.refresh?.mainRefreshLeaseTokenHash,
|
|
333
|
+
}),
|
|
334
|
+
accounts,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
async function writeJsonAtomic(path, value) {
|
|
164
338
|
await mkdir(dirname(path), { recursive: true });
|
|
165
|
-
const existing = await loadExistingTopLevelFields(path);
|
|
166
|
-
const next = { ...existing, ...omitUndefinedTopLevel(storage) };
|
|
167
339
|
const tempPath = `${path}.${randomUUID()}.tmp`;
|
|
168
|
-
await writeFile(tempPath, `${JSON.stringify(
|
|
340
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
169
341
|
encoding: 'utf8',
|
|
170
342
|
mode: 0o600,
|
|
171
343
|
});
|
|
172
344
|
await rename(tempPath, path);
|
|
173
345
|
}
|
|
346
|
+
export async function saveAccounts(storage, path = getAccountStoragePath()) {
|
|
347
|
+
const existing = await loadExistingTopLevelFields(path);
|
|
348
|
+
const nextConfig = { ...existing, ...configFromStorage(storage) };
|
|
349
|
+
await writeJsonAtomic(path, nextConfig);
|
|
350
|
+
await writeJsonAtomic(getAccountStatePath(path), stateFromStorage(storage));
|
|
351
|
+
}
|
|
352
|
+
function applyMainQuotaStatePatch(state, storage) {
|
|
353
|
+
state.main = state.main ?? {};
|
|
354
|
+
state.main.quota = storage.quota?.mainQuota;
|
|
355
|
+
state.main.quotaCheckedAt = storage.quota?.mainQuotaCheckedAt;
|
|
356
|
+
state.main.quotaToken = storage.quota?.mainQuotaToken;
|
|
357
|
+
state.main.lastQuotaApiError = storage.quota?.mainLastQuotaApiError;
|
|
358
|
+
}
|
|
359
|
+
function applyMainRefreshStatePatch(state, storage) {
|
|
360
|
+
state.main = state.main ?? {};
|
|
361
|
+
state.main.lastRefreshError = storage.refresh?.mainLastRefreshError;
|
|
362
|
+
state.main.refreshLeaseId = storage.refresh?.mainRefreshLeaseId;
|
|
363
|
+
state.main.refreshLeaseUntil = storage.refresh?.mainRefreshLeaseUntil;
|
|
364
|
+
state.main.refreshLeaseTokenHash = storage.refresh?.mainRefreshLeaseTokenHash;
|
|
365
|
+
}
|
|
366
|
+
function pruneUndefined(value) {
|
|
367
|
+
if (Array.isArray(value))
|
|
368
|
+
return value.map(pruneUndefined);
|
|
369
|
+
if (!isRecord(value))
|
|
370
|
+
return value;
|
|
371
|
+
return Object.fromEntries(Object.entries(value)
|
|
372
|
+
.filter(([, entry]) => entry !== undefined)
|
|
373
|
+
.map(([key, entry]) => [key, pruneUndefined(entry)]));
|
|
374
|
+
}
|
|
375
|
+
export async function saveAccountState(storage, path = getAccountStoragePath(), scope = {
|
|
376
|
+
mainQuota: true,
|
|
377
|
+
mainRefresh: true,
|
|
378
|
+
accounts: true,
|
|
379
|
+
}) {
|
|
380
|
+
const statePath = getAccountStatePath(path);
|
|
381
|
+
const existing = (await readJsonIfPresent(statePath)).value;
|
|
382
|
+
const next = isRecord(existing)
|
|
383
|
+
? { ...existing, version: 1 }
|
|
384
|
+
: { version: 1 };
|
|
385
|
+
if (scope.mainQuota)
|
|
386
|
+
applyMainQuotaStatePatch(next, storage);
|
|
387
|
+
if (scope.mainRefresh)
|
|
388
|
+
applyMainRefreshStatePatch(next, storage);
|
|
389
|
+
if (scope.accounts) {
|
|
390
|
+
const ids = scope.accounts === true ? null : new Set(scope.accounts);
|
|
391
|
+
next.accounts = { ...(isRecord(next.accounts) ? next.accounts : {}) };
|
|
392
|
+
for (const account of storage.accounts) {
|
|
393
|
+
if (ids && !ids.has(account.id))
|
|
394
|
+
continue;
|
|
395
|
+
next.accounts[account.id] = accountRuntimeState(account);
|
|
396
|
+
}
|
|
397
|
+
if (ids) {
|
|
398
|
+
for (const id of ids) {
|
|
399
|
+
if (!storage.accounts.some((account) => account.id === id)) {
|
|
400
|
+
delete next.accounts[id];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
await writeJsonAtomic(statePath, pruneUndefined(next));
|
|
406
|
+
}
|
|
174
407
|
export async function acquireRefreshFileLock(options) {
|
|
175
408
|
const accountPath = options.path ?? getAccountStoragePath();
|
|
176
409
|
const lockPath = `${accountPath}.${options.name}.lock`;
|
|
@@ -786,8 +1019,10 @@ export class FallbackAccountManager {
|
|
|
786
1019
|
async load() {
|
|
787
1020
|
return loadAccounts(this.configPath);
|
|
788
1021
|
}
|
|
789
|
-
async save(storage) {
|
|
790
|
-
await
|
|
1022
|
+
async save(storage, accountIds) {
|
|
1023
|
+
await saveAccountState(storage, this.configPath, {
|
|
1024
|
+
accounts: accountIds ?? true,
|
|
1025
|
+
});
|
|
791
1026
|
}
|
|
792
1027
|
startBackgroundRefresh() {
|
|
793
1028
|
const run = async () => {
|
|
@@ -818,7 +1053,7 @@ export class FallbackAccountManager {
|
|
|
818
1053
|
const usable = [];
|
|
819
1054
|
let changed = false;
|
|
820
1055
|
for (const account of storage.accounts) {
|
|
821
|
-
if (account.enabled === false)
|
|
1056
|
+
if (account.enabled === false || !isOAuthAccount(account))
|
|
822
1057
|
continue;
|
|
823
1058
|
try {
|
|
824
1059
|
let next = account;
|
|
@@ -898,7 +1133,7 @@ export class FallbackAccountManager {
|
|
|
898
1133
|
return;
|
|
899
1134
|
let changed = false;
|
|
900
1135
|
for (const account of storage.accounts) {
|
|
901
|
-
if (account.enabled === false)
|
|
1136
|
+
if (account.enabled === false || !isOAuthAccount(account))
|
|
902
1137
|
continue;
|
|
903
1138
|
if (!tokenNeedsRefresh(account, storage, this.now()))
|
|
904
1139
|
continue;
|
|
@@ -940,7 +1175,7 @@ export class FallbackAccountManager {
|
|
|
940
1175
|
return;
|
|
941
1176
|
let changed = false;
|
|
942
1177
|
for (const account of storage.accounts) {
|
|
943
|
-
if (account.enabled === false)
|
|
1178
|
+
if (account.enabled === false || !isOAuthAccount(account))
|
|
944
1179
|
continue;
|
|
945
1180
|
let next = account;
|
|
946
1181
|
try {
|
|
@@ -985,7 +1220,7 @@ export class FallbackAccountManager {
|
|
|
985
1220
|
const force = options.force ?? false;
|
|
986
1221
|
let changed = false;
|
|
987
1222
|
for (const account of storage.accounts) {
|
|
988
|
-
if (account.enabled === false)
|
|
1223
|
+
if (account.enabled === false || !isOAuthAccount(account))
|
|
989
1224
|
continue;
|
|
990
1225
|
let next = account;
|
|
991
1226
|
try {
|
|
@@ -1046,7 +1281,7 @@ export class FallbackAccountManager {
|
|
|
1046
1281
|
while (Date.now() < deadline) {
|
|
1047
1282
|
await new Promise((resolve) => setTimeout(resolve, FALLBACK_REFRESH_JOIN_POLL_MS));
|
|
1048
1283
|
const latestStorage = await this.load();
|
|
1049
|
-
const latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id);
|
|
1284
|
+
const latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id && isOAuthAccount(candidate));
|
|
1050
1285
|
if (!latestAccount)
|
|
1051
1286
|
continue;
|
|
1052
1287
|
const changed = latestAccount.access !== previous.access ||
|
|
@@ -1075,7 +1310,7 @@ export class FallbackAccountManager {
|
|
|
1075
1310
|
}
|
|
1076
1311
|
async refreshAccountNow(account, storage, options) {
|
|
1077
1312
|
let latestStorage = await this.load();
|
|
1078
|
-
let latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id);
|
|
1313
|
+
let latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id && isOAuthAccount(candidate));
|
|
1079
1314
|
if (latestAccount &&
|
|
1080
1315
|
!options.force &&
|
|
1081
1316
|
!tokenNeedsRefresh(latestAccount, latestStorage, this.now())) {
|
|
@@ -1101,7 +1336,7 @@ export class FallbackAccountManager {
|
|
|
1101
1336
|
}
|
|
1102
1337
|
try {
|
|
1103
1338
|
latestStorage = await this.load();
|
|
1104
|
-
latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id);
|
|
1339
|
+
latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id && isOAuthAccount(candidate));
|
|
1105
1340
|
if (latestAccount &&
|
|
1106
1341
|
!options.force &&
|
|
1107
1342
|
!tokenNeedsRefresh(latestAccount, latestStorage, this.now())) {
|
package/dist/cachekeep.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export declare const CACHE_KEEP_TTL_MS: number;
|
|
|
4
4
|
export declare const CACHE_KEEP_PREWARM_LEAD_MS: number;
|
|
5
5
|
export declare const CACHE_KEEP_TICK_MS = 60000;
|
|
6
6
|
export declare const CACHE_KEEP_EXTENDED_TTL_BETA = "extended-cache-ttl-2025-04-11";
|
|
7
|
+
export declare const CACHE_KEEP_MAX_TARGETS = 32;
|
|
8
|
+
export declare const CACHE_KEEP_MAX_BODY_BYTES: number;
|
|
9
|
+
export declare const CACHE_KEEP_STALE_TARGET_MS: number;
|
|
7
10
|
export type CacheKeepWindow = {
|
|
8
11
|
startHour: number;
|
|
9
12
|
endHour: number;
|
|
@@ -74,6 +77,9 @@ export declare class CacheKeepManager {
|
|
|
74
77
|
nextPrewarmAt: number | undefined;
|
|
75
78
|
};
|
|
76
79
|
trackedCount(): number;
|
|
80
|
+
private totalBodyBytes;
|
|
81
|
+
private evictOldestTarget;
|
|
82
|
+
private pruneTargets;
|
|
77
83
|
track(input: {
|
|
78
84
|
sessionId?: string | null;
|
|
79
85
|
url: string;
|
package/dist/cachekeep.js
CHANGED
|
@@ -5,6 +5,9 @@ export const CACHE_KEEP_TTL_MS = 60 * 60_000;
|
|
|
5
5
|
export const CACHE_KEEP_PREWARM_LEAD_MS = 5 * 60_000;
|
|
6
6
|
export const CACHE_KEEP_TICK_MS = 60_000;
|
|
7
7
|
export const CACHE_KEEP_EXTENDED_TTL_BETA = 'extended-cache-ttl-2025-04-11';
|
|
8
|
+
export const CACHE_KEEP_MAX_TARGETS = 32;
|
|
9
|
+
export const CACHE_KEEP_MAX_BODY_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
export const CACHE_KEEP_STALE_TARGET_MS = 2 * CACHE_KEEP_TTL_MS;
|
|
8
11
|
const STATUS_TITLE = '## Claude Cache Keep Status';
|
|
9
12
|
const ENABLED_TITLE = '## Claude Cache Keep Enabled';
|
|
10
13
|
const DISABLED_TITLE = '## Claude Cache Keep Disabled';
|
|
@@ -208,6 +211,33 @@ export class CacheKeepManager {
|
|
|
208
211
|
trackedCount() {
|
|
209
212
|
return this.targets.size;
|
|
210
213
|
}
|
|
214
|
+
totalBodyBytes() {
|
|
215
|
+
return [...this.targets.values()].reduce((sum, target) => sum + target.bodyText.length, 0);
|
|
216
|
+
}
|
|
217
|
+
evictOldestTarget() {
|
|
218
|
+
const oldest = this.targets.keys().next().value;
|
|
219
|
+
if (oldest !== undefined)
|
|
220
|
+
this.targets.delete(oldest);
|
|
221
|
+
}
|
|
222
|
+
pruneTargets(now, today) {
|
|
223
|
+
for (const [id, target] of this.targets) {
|
|
224
|
+
if (today && target.dayKey !== today) {
|
|
225
|
+
this.targets.delete(id);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (now - target.cacheExpiresAt > CACHE_KEEP_STALE_TARGET_MS) {
|
|
229
|
+
this.targets.delete(id);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
while (this.targets.size > CACHE_KEEP_MAX_TARGETS)
|
|
233
|
+
this.evictOldestTarget();
|
|
234
|
+
while (this.totalBodyBytes() > CACHE_KEEP_MAX_BODY_BYTES) {
|
|
235
|
+
const before = this.targets.size;
|
|
236
|
+
this.evictOldestTarget();
|
|
237
|
+
if (this.targets.size === before)
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
211
241
|
track(input) {
|
|
212
242
|
if (!input.sessionId)
|
|
213
243
|
return { tracked: false, reason: 'missing session id' };
|
|
@@ -217,11 +247,18 @@ export class CacheKeepManager {
|
|
|
217
247
|
if (input.cacheMode !== 'hybrid') {
|
|
218
248
|
return { tracked: false, reason: 'cache mode is not hybrid' };
|
|
219
249
|
}
|
|
250
|
+
if (input.bodyText.length > CACHE_KEEP_MAX_BODY_BYTES) {
|
|
251
|
+
return { tracked: false, reason: 'body exceeds cachekeep memory budget' };
|
|
252
|
+
}
|
|
220
253
|
const now = this.options.now?.() ?? Date.now();
|
|
221
254
|
const window = getCacheKeepWindow(input.storage);
|
|
222
255
|
if (!isWithinCacheKeepWindow(window, new Date(now))) {
|
|
223
256
|
return { tracked: false, reason: 'outside configured window' };
|
|
224
257
|
}
|
|
258
|
+
const today = localWindowKey(window, new Date(now));
|
|
259
|
+
this.pruneTargets(now, today);
|
|
260
|
+
if (this.targets.has(input.sessionId))
|
|
261
|
+
this.targets.delete(input.sessionId);
|
|
225
262
|
const headers = {};
|
|
226
263
|
input.headers.forEach((value, key) => {
|
|
227
264
|
headers[key] = value;
|
|
@@ -232,8 +269,9 @@ export class CacheKeepManager {
|
|
|
232
269
|
headers,
|
|
233
270
|
bodyText: input.bodyText,
|
|
234
271
|
cacheExpiresAt: now + CACHE_KEEP_TTL_MS,
|
|
235
|
-
dayKey:
|
|
272
|
+
dayKey: today,
|
|
236
273
|
});
|
|
274
|
+
this.pruneTargets(now, today);
|
|
237
275
|
this.start();
|
|
238
276
|
return { tracked: true };
|
|
239
277
|
}
|
|
@@ -242,10 +280,7 @@ export class CacheKeepManager {
|
|
|
242
280
|
const window = getCacheKeepWindow(storage);
|
|
243
281
|
const now = this.options.now?.() ?? Date.now();
|
|
244
282
|
const today = localWindowKey(window, new Date(now));
|
|
245
|
-
|
|
246
|
-
if (target.dayKey !== today)
|
|
247
|
-
this.targets.delete(id);
|
|
248
|
-
}
|
|
283
|
+
this.pruneTargets(now, today);
|
|
249
284
|
if (!this.targets.size) {
|
|
250
285
|
this.stop();
|
|
251
286
|
return;
|
package/dist/claude-code.js
CHANGED
|
@@ -22,7 +22,10 @@ export function getClaudeCodeIdentity(seed) {
|
|
|
22
22
|
setBounded(identityCache, cacheKey, identity);
|
|
23
23
|
return identity;
|
|
24
24
|
}
|
|
25
|
+
const BOOTSTRAP_IDENTITY_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
26
|
+
const BOOTSTRAP_IDENTITY_NEGATIVE_TTL_MS = 5 * 60_000;
|
|
25
27
|
const bootstrapFetches = new Map();
|
|
28
|
+
const bootstrapResults = new Map();
|
|
26
29
|
async function fetchClaudeCodeAccountUuid(accessToken, model) {
|
|
27
30
|
if (!accessToken.startsWith('sk-ant-oat'))
|
|
28
31
|
return null;
|
|
@@ -61,16 +64,35 @@ export async function resolveClaudeCodeIdentity(accessToken, model) {
|
|
|
61
64
|
const identity = getClaudeCodeIdentity(accessToken);
|
|
62
65
|
if (!accessToken.startsWith('sk-ant-oat'))
|
|
63
66
|
return identity;
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const cachedResult = bootstrapResults.get(accessToken);
|
|
69
|
+
if (cachedResult && cachedResult.expiresAt > now) {
|
|
70
|
+
if (!cachedResult.accountUuid)
|
|
71
|
+
return identity;
|
|
72
|
+
const accountCacheKey = `account:${cachedResult.accountUuid}`;
|
|
73
|
+
const accountIdentity = identityCache.get(accountCacheKey);
|
|
74
|
+
if (accountIdentity) {
|
|
75
|
+
setBounded(identityCache, accessToken, accountIdentity);
|
|
76
|
+
return accountIdentity;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
64
79
|
let fetchPromise = bootstrapFetches.get(accessToken);
|
|
65
80
|
if (!fetchPromise) {
|
|
66
81
|
fetchPromise = fetchClaudeCodeAccountUuid(accessToken, model);
|
|
67
82
|
bootstrapFetches.set(accessToken, fetchPromise);
|
|
68
83
|
}
|
|
69
|
-
const accountUuid = await fetchPromise
|
|
70
|
-
if (!accountUuid) {
|
|
84
|
+
const accountUuid = await fetchPromise.finally(() => {
|
|
71
85
|
bootstrapFetches.delete(accessToken);
|
|
86
|
+
});
|
|
87
|
+
setBounded(bootstrapResults, accessToken, {
|
|
88
|
+
accountUuid,
|
|
89
|
+
expiresAt: now +
|
|
90
|
+
(accountUuid
|
|
91
|
+
? BOOTSTRAP_IDENTITY_CACHE_TTL_MS
|
|
92
|
+
: BOOTSTRAP_IDENTITY_NEGATIVE_TTL_MS),
|
|
93
|
+
});
|
|
94
|
+
if (!accountUuid)
|
|
72
95
|
return identity;
|
|
73
|
-
}
|
|
74
96
|
const accountCacheKey = `account:${accountUuid}`;
|
|
75
97
|
const accountIdentity = identityCache.get(accountCacheKey);
|
|
76
98
|
if (accountIdentity) {
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const CLAUDE_FABLE_5_MODEL_ID = "claude-fable-5";
|
|
2
|
+
export declare const CLAUDE_MYTHOS_5_MODEL_ID = "claude-mythos-5";
|
|
3
|
+
export declare const CLAUDE_FABLE_MYTHOS_5_MODEL_IDS: readonly ["claude-fable-5", "claude-mythos-5"];
|
|
4
|
+
export type ClaudeFableMythos5ModelId = (typeof CLAUDE_FABLE_MYTHOS_5_MODEL_IDS)[number];
|
|
5
|
+
export declare const CLAUDE_FABLE_MYTHOS_5_PRICING: {
|
|
6
|
+
readonly input: 10;
|
|
7
|
+
readonly output: 50;
|
|
8
|
+
readonly cacheRead: 1;
|
|
9
|
+
readonly cacheWrite5m: 12.5;
|
|
10
|
+
readonly cacheWrite1h: 20;
|
|
11
|
+
};
|
|
12
|
+
export declare const CLAUDE_FABLE_MYTHOS_5_CONTEXT_WINDOW = 1000000;
|
|
13
|
+
export declare const CLAUDE_FABLE_MYTHOS_5_MAX_OUTPUT_TOKENS = 128000;
|
|
14
|
+
export declare const CLAUDE_FABLE_MYTHOS_5_RELEASE_DATE = "2026-06-09";
|
|
15
|
+
export declare const CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS: Record<ClaudeFableMythos5ModelId, {
|
|
16
|
+
id: ClaudeFableMythos5ModelId;
|
|
17
|
+
name: string;
|
|
18
|
+
limited?: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function isClaudeFableOrMythos5Model(model: unknown): boolean;
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const CLAUDE_FABLE_5_MODEL_ID = 'claude-fable-5';
|
|
2
|
+
export const CLAUDE_MYTHOS_5_MODEL_ID = 'claude-mythos-5';
|
|
3
|
+
export const CLAUDE_FABLE_MYTHOS_5_MODEL_IDS = [
|
|
4
|
+
CLAUDE_FABLE_5_MODEL_ID,
|
|
5
|
+
CLAUDE_MYTHOS_5_MODEL_ID,
|
|
6
|
+
];
|
|
7
|
+
export const CLAUDE_FABLE_MYTHOS_5_PRICING = {
|
|
8
|
+
input: 10,
|
|
9
|
+
output: 50,
|
|
10
|
+
cacheRead: 1,
|
|
11
|
+
cacheWrite5m: 12.5,
|
|
12
|
+
cacheWrite1h: 20,
|
|
13
|
+
};
|
|
14
|
+
export const CLAUDE_FABLE_MYTHOS_5_CONTEXT_WINDOW = 1_000_000;
|
|
15
|
+
export const CLAUDE_FABLE_MYTHOS_5_MAX_OUTPUT_TOKENS = 128_000;
|
|
16
|
+
export const CLAUDE_FABLE_MYTHOS_5_RELEASE_DATE = '2026-06-09';
|
|
17
|
+
export const CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS = {
|
|
18
|
+
[CLAUDE_FABLE_5_MODEL_ID]: {
|
|
19
|
+
id: CLAUDE_FABLE_5_MODEL_ID,
|
|
20
|
+
name: 'Claude Fable 5',
|
|
21
|
+
},
|
|
22
|
+
[CLAUDE_MYTHOS_5_MODEL_ID]: {
|
|
23
|
+
id: CLAUDE_MYTHOS_5_MODEL_ID,
|
|
24
|
+
name: 'Claude Mythos 5',
|
|
25
|
+
limited: true,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
export function isClaudeFableOrMythos5Model(model) {
|
|
29
|
+
return (typeof model === 'string' &&
|
|
30
|
+
CLAUDE_FABLE_MYTHOS_5_MODEL_IDS.some((id) => model === id || model.startsWith(`${id}-`)));
|
|
31
|
+
}
|
package/dist/quota-manager.d.ts
CHANGED
|
@@ -94,6 +94,7 @@ export declare class QuotaManager {
|
|
|
94
94
|
/** Minimum gap between consecutive quota API calls (ms). */
|
|
95
95
|
private static readonly API_CALL_GAP_MS;
|
|
96
96
|
private static fallbackInflightKey;
|
|
97
|
+
private static quotaLockName;
|
|
97
98
|
/**
|
|
98
99
|
* Serialize API calls through a shared gate so only one
|
|
99
100
|
* quota API request runs at a time, with a minimum gap
|
package/dist/quota-manager.js
CHANGED
|
@@ -284,6 +284,10 @@ export class QuotaManager {
|
|
|
284
284
|
static fallbackInflightKey(accountId, accessToken) {
|
|
285
285
|
return JSON.stringify([accountId, tokenFingerprint(accessToken)]);
|
|
286
286
|
}
|
|
287
|
+
static quotaLockName(accountId) {
|
|
288
|
+
const safeId = accountId.replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
289
|
+
return `opencode-fallback-quota-refresh-${safeId || 'account'}`;
|
|
290
|
+
}
|
|
287
291
|
/**
|
|
288
292
|
* Serialize API calls through a shared gate so only one
|
|
289
293
|
* quota API request runs at a time, with a minimum gap
|
|
@@ -367,20 +371,35 @@ export class QuotaManager {
|
|
|
367
371
|
return cached.quota;
|
|
368
372
|
throw new Error('Quota API rate-limited — try again later');
|
|
369
373
|
}
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
now: this.now,
|
|
374
|
+
const fileLock = await acquireRefreshFileLock({
|
|
375
|
+
name: QuotaManager.quotaLockName(accountId),
|
|
376
|
+
ttlMs: 30_000,
|
|
374
377
|
});
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
378
|
+
if (!fileLock) {
|
|
379
|
+
const cached = this.getFallback(accountId, accessToken);
|
|
380
|
+
if (cached)
|
|
381
|
+
return cached.quota;
|
|
382
|
+
throw new Error('Quota refresh is already in progress');
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
const quota = await fetchOAuthQuotaSnapshot({
|
|
386
|
+
accessToken,
|
|
387
|
+
fetchImpl: this.fetchImpl,
|
|
388
|
+
now: this.now,
|
|
389
|
+
});
|
|
390
|
+
const now = this.now();
|
|
391
|
+
this.setFallback(accountId, {
|
|
392
|
+
quota,
|
|
393
|
+
refreshAfter: now + getQuotaCheckIntervalMs(this.storage),
|
|
394
|
+
checkedAt: now,
|
|
395
|
+
}, accessToken);
|
|
396
|
+
this.fallbackApiErrors.delete(accountId);
|
|
397
|
+
this.fallbackErrorTokenFps.delete(accountId);
|
|
398
|
+
return quota;
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
await fileLock.release();
|
|
402
|
+
}
|
|
384
403
|
}
|
|
385
404
|
catch (error) {
|
|
386
405
|
this._handleFallbackFetchError(accountId, accessToken, error);
|
package/dist/quotas.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isOAuthAccount } from "./accounts.js";
|
|
1
2
|
export const CLAUDE_QUOTAS_COMMAND_NAME = 'claude-quota';
|
|
2
3
|
const WINDOW_LABELS = {
|
|
3
4
|
five_hour: '5h',
|
|
@@ -60,7 +61,7 @@ function accountStoredError(account) {
|
|
|
60
61
|
export function buildFallbackQuotaSummaries(storage, errors = new Map()) {
|
|
61
62
|
if (!storage?.accounts.length)
|
|
62
63
|
return [];
|
|
63
|
-
return storage.accounts.map((account) => {
|
|
64
|
+
return storage.accounts.filter(isOAuthAccount).map((account) => {
|
|
64
65
|
const error = errors.get(account.id) ?? accountStoredError(account);
|
|
65
66
|
return {
|
|
66
67
|
name: accountName(account),
|
package/dist/relay.js
CHANGED
|
@@ -21,6 +21,10 @@ class RelayWebSocketConnectionResetError extends Error {
|
|
|
21
21
|
const sessionState = new Map();
|
|
22
22
|
const websocketSessions = new Map();
|
|
23
23
|
const loggedRelayConfigMessages = new Set();
|
|
24
|
+
const RELAY_SESSION_STATE_TTL_MS = 2 * 60 * 60_000;
|
|
25
|
+
const RELAY_WEBSOCKET_IDLE_TTL_MS = 30 * 60_000;
|
|
26
|
+
const MAX_RELAY_SESSION_STATES = 128;
|
|
27
|
+
const MAX_RELAY_WEBSOCKET_SESSIONS = 32;
|
|
24
28
|
let nextRequestId = 0;
|
|
25
29
|
function createRequestId() {
|
|
26
30
|
nextRequestId += 1;
|
|
@@ -136,14 +140,23 @@ function jsonHeaders(headers) {
|
|
|
136
140
|
}
|
|
137
141
|
return result;
|
|
138
142
|
}
|
|
139
|
-
async function postRelay(config, payload) {
|
|
143
|
+
async function postRelay(config, payload, signal) {
|
|
144
|
+
const stringifyStart = perfNowMs();
|
|
145
|
+
const body = JSON.stringify(payload);
|
|
146
|
+
relayPerfLog('http_payload_stringify', {
|
|
147
|
+
session: shortAffinity(payload.affinity),
|
|
148
|
+
ms: formatMs(perfNowMs() - stringifyStart),
|
|
149
|
+
relayBytes: body.length,
|
|
150
|
+
mode: payload.mode,
|
|
151
|
+
});
|
|
140
152
|
return fetch(config.url, {
|
|
141
153
|
method: 'POST',
|
|
142
154
|
headers: {
|
|
143
155
|
'content-type': 'application/json',
|
|
144
156
|
'x-relay-token': config.token,
|
|
145
157
|
},
|
|
146
|
-
body
|
|
158
|
+
body,
|
|
159
|
+
signal: signal ?? undefined,
|
|
147
160
|
});
|
|
148
161
|
}
|
|
149
162
|
function shortAffinity(affinity) {
|
|
@@ -155,6 +168,14 @@ function perfNowMs() {
|
|
|
155
168
|
function formatMs(value) {
|
|
156
169
|
return Math.round(value * 10) / 10;
|
|
157
170
|
}
|
|
171
|
+
function relayPerfLoggingEnabled() {
|
|
172
|
+
return process.env.OPENCODE_ANTHROPIC_AUTH_PERF === '1';
|
|
173
|
+
}
|
|
174
|
+
function relayPerfLog(stage, data) {
|
|
175
|
+
if (!relayPerfLoggingEnabled())
|
|
176
|
+
return;
|
|
177
|
+
relayLog(`[perf] relay ${stage} ${JSON.stringify(data)}`);
|
|
178
|
+
}
|
|
158
179
|
function createRelayPayload(options) {
|
|
159
180
|
const { input, init, headers, bodyText, affinity, previous, nextHash } = options;
|
|
160
181
|
const rewrittenUrl = input instanceof Request ? input.url : input.toString();
|
|
@@ -197,21 +218,25 @@ function createFullSyncPayload(payload, bodyText) {
|
|
|
197
218
|
};
|
|
198
219
|
}
|
|
199
220
|
function updateLocalRelayState(affinity, bodyText, nextHash, revision) {
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
sessionState.delete(affinity);
|
|
200
223
|
sessionState.set(affinity, {
|
|
201
224
|
body: bodyText,
|
|
202
225
|
hash: nextHash,
|
|
203
226
|
revision,
|
|
227
|
+
updatedAt: now,
|
|
204
228
|
});
|
|
229
|
+
cleanupRelaySessionCaches(now);
|
|
205
230
|
}
|
|
206
231
|
async function sendRelayHttp(options) {
|
|
207
|
-
const { config, payload, bodyText, fallback } = options;
|
|
232
|
+
const { config, payload, bodyText, fallback, signal } = options;
|
|
208
233
|
let actualPayload = payload;
|
|
209
|
-
let response = await postRelay(config, actualPayload);
|
|
234
|
+
let response = await postRelay(config, actualPayload, signal);
|
|
210
235
|
if (response.status === 409 && actualPayload.mode === 'patch') {
|
|
211
236
|
relayLog(`state mismatch from relay session=${shortAffinity(actualPayload.affinity)}; retrying full_sync`);
|
|
212
237
|
await response.body?.cancel().catch(() => { });
|
|
213
238
|
actualPayload = createFullSyncPayload(actualPayload, bodyText);
|
|
214
|
-
response = await postRelay(config, actualPayload);
|
|
239
|
+
response = await postRelay(config, actualPayload, signal);
|
|
215
240
|
}
|
|
216
241
|
if (!response.ok && response.status >= 500 && config.fallbackToDirect) {
|
|
217
242
|
relayLog(`relay returned ${response.status}; falling back direct session=${shortAffinity(actualPayload.affinity)}`);
|
|
@@ -279,11 +304,27 @@ class PersistentRelaySession {
|
|
|
279
304
|
queue = Promise.resolve();
|
|
280
305
|
pending;
|
|
281
306
|
serverState;
|
|
307
|
+
lastUsedAt = Date.now();
|
|
282
308
|
constructor(config, affinity) {
|
|
283
309
|
this.config = config;
|
|
284
310
|
this.affinity = affinity;
|
|
285
311
|
}
|
|
312
|
+
touch(now = Date.now()) {
|
|
313
|
+
this.lastUsedAt = now;
|
|
314
|
+
}
|
|
315
|
+
canEvict(now = Date.now()) {
|
|
316
|
+
return (!this.pending &&
|
|
317
|
+
!this.connecting &&
|
|
318
|
+
now - this.lastUsedAt > RELAY_WEBSOCKET_IDLE_TTL_MS);
|
|
319
|
+
}
|
|
320
|
+
dispose() {
|
|
321
|
+
this.socket?.close();
|
|
322
|
+
this.socket = undefined;
|
|
323
|
+
this.serverState = undefined;
|
|
324
|
+
this.connecting = undefined;
|
|
325
|
+
}
|
|
286
326
|
send(payload, bodyText, optimisticResponse = false) {
|
|
327
|
+
this.touch();
|
|
287
328
|
const enqueuedAt = perfNowMs();
|
|
288
329
|
const start = this.queue
|
|
289
330
|
.catch(() => { })
|
|
@@ -638,12 +679,48 @@ class PersistentRelaySession {
|
|
|
638
679
|
pending.rejectDone(error);
|
|
639
680
|
}
|
|
640
681
|
}
|
|
682
|
+
function cleanupRelaySessionCaches(now = Date.now()) {
|
|
683
|
+
for (const [affinity, state] of sessionState) {
|
|
684
|
+
if (now - state.updatedAt > RELAY_SESSION_STATE_TTL_MS) {
|
|
685
|
+
sessionState.delete(affinity);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
while (sessionState.size > MAX_RELAY_SESSION_STATES) {
|
|
689
|
+
const oldest = sessionState.keys().next().value;
|
|
690
|
+
if (oldest === undefined)
|
|
691
|
+
break;
|
|
692
|
+
sessionState.delete(oldest);
|
|
693
|
+
}
|
|
694
|
+
for (const [affinity, session] of websocketSessions) {
|
|
695
|
+
if (session.canEvict(now)) {
|
|
696
|
+
session.dispose();
|
|
697
|
+
websocketSessions.delete(affinity);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
while (websocketSessions.size > MAX_RELAY_WEBSOCKET_SESSIONS) {
|
|
701
|
+
let evicted = false;
|
|
702
|
+
for (const [affinity, session] of websocketSessions) {
|
|
703
|
+
if (!session.canEvict(now))
|
|
704
|
+
continue;
|
|
705
|
+
session.dispose();
|
|
706
|
+
websocketSessions.delete(affinity);
|
|
707
|
+
evicted = true;
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
if (!evicted)
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
641
714
|
function getPersistentRelaySession(config, affinity) {
|
|
715
|
+
cleanupRelaySessionCaches();
|
|
642
716
|
const existing = websocketSessions.get(affinity);
|
|
643
|
-
if (existing)
|
|
717
|
+
if (existing) {
|
|
718
|
+
existing.touch();
|
|
644
719
|
return existing;
|
|
720
|
+
}
|
|
645
721
|
const session = new PersistentRelaySession(config, affinity);
|
|
646
722
|
websocketSessions.set(affinity, session);
|
|
723
|
+
cleanupRelaySessionCaches();
|
|
647
724
|
return session;
|
|
648
725
|
}
|
|
649
726
|
export async function sendViaRelay(options) {
|
|
@@ -658,8 +735,15 @@ export async function sendViaRelay(options) {
|
|
|
658
735
|
return fallback();
|
|
659
736
|
}
|
|
660
737
|
const bodyText = body;
|
|
738
|
+
const hashStart = perfNowMs();
|
|
661
739
|
const nextHash = hashBody(bodyText);
|
|
740
|
+
relayPerfLog('hash_body', {
|
|
741
|
+
session: shortAffinity(affinity),
|
|
742
|
+
ms: formatMs(perfNowMs() - hashStart),
|
|
743
|
+
bodyBytes: bodyText.length,
|
|
744
|
+
});
|
|
662
745
|
const previous = sessionState.get(affinity);
|
|
746
|
+
const payloadStart = perfNowMs();
|
|
663
747
|
const payload = createRelayPayload({
|
|
664
748
|
input,
|
|
665
749
|
init,
|
|
@@ -669,8 +753,16 @@ export async function sendViaRelay(options) {
|
|
|
669
753
|
previous,
|
|
670
754
|
nextHash,
|
|
671
755
|
});
|
|
756
|
+
relayPerfLog('create_payload', {
|
|
757
|
+
session: shortAffinity(affinity),
|
|
758
|
+
ms: formatMs(perfNowMs() - payloadStart),
|
|
759
|
+
mode: payload.mode,
|
|
760
|
+
bodyBytes: bodyText.length,
|
|
761
|
+
previousBytes: previous?.body.length,
|
|
762
|
+
});
|
|
672
763
|
try {
|
|
673
764
|
let result;
|
|
765
|
+
const sendStart = perfNowMs();
|
|
674
766
|
if (config.transport === 'websocket') {
|
|
675
767
|
try {
|
|
676
768
|
const session = getPersistentRelaySession(config, affinity);
|
|
@@ -678,19 +770,47 @@ export async function sendViaRelay(options) {
|
|
|
678
770
|
}
|
|
679
771
|
catch (error) {
|
|
680
772
|
relayLog(`websocket relay failed session=${shortAffinity(affinity)}; trying http relay: ${error instanceof Error ? error.message : String(error)}`);
|
|
681
|
-
result = await sendRelayHttp({
|
|
773
|
+
result = await sendRelayHttp({
|
|
774
|
+
config,
|
|
775
|
+
payload,
|
|
776
|
+
bodyText,
|
|
777
|
+
fallback,
|
|
778
|
+
signal: init?.signal,
|
|
779
|
+
});
|
|
682
780
|
}
|
|
683
781
|
}
|
|
684
782
|
else {
|
|
685
|
-
result = await sendRelayHttp({
|
|
783
|
+
result = await sendRelayHttp({
|
|
784
|
+
config,
|
|
785
|
+
payload,
|
|
786
|
+
bodyText,
|
|
787
|
+
fallback,
|
|
788
|
+
signal: init?.signal,
|
|
789
|
+
});
|
|
686
790
|
}
|
|
791
|
+
relayPerfLog('send', {
|
|
792
|
+
session: shortAffinity(affinity),
|
|
793
|
+
ms: formatMs(perfNowMs() - sendStart),
|
|
794
|
+
transport: result.transport,
|
|
795
|
+
protocol: result.protocol,
|
|
796
|
+
mode: result.payload.mode,
|
|
797
|
+
status: result.response.status,
|
|
798
|
+
usedRelay: result.usedRelay,
|
|
799
|
+
});
|
|
687
800
|
if (!result.usedRelay)
|
|
688
801
|
return result.response;
|
|
689
802
|
if (result.transport === 'http') {
|
|
690
803
|
updateLocalRelayState(affinity, bodyText, result.payload.next_hash, result.payload.revision);
|
|
691
804
|
}
|
|
805
|
+
const payloadStringifyStart = perfNowMs();
|
|
692
806
|
const actualPayloadBytes = JSON.stringify(result.payload).length;
|
|
807
|
+
relayPerfLog('payload_stringify', {
|
|
808
|
+
session: shortAffinity(affinity),
|
|
809
|
+
ms: formatMs(perfNowMs() - payloadStringifyStart),
|
|
810
|
+
relayBytes: actualPayloadBytes,
|
|
811
|
+
});
|
|
693
812
|
relayLog(`used relay transport=${result.transport} protocol=${result.protocol} mode=${result.payload.mode} status=${result.response.status} session=${shortAffinity(affinity)} bodyBytes=${bodyText.length} relayBytes=${actualPayloadBytes}`);
|
|
813
|
+
const dumpStart = perfNowMs();
|
|
694
814
|
await dumpRelayRequest({
|
|
695
815
|
affinity,
|
|
696
816
|
transport: result.transport,
|
|
@@ -702,6 +822,11 @@ export async function sendViaRelay(options) {
|
|
|
702
822
|
payload: result.payload,
|
|
703
823
|
relayBytes: actualPayloadBytes,
|
|
704
824
|
});
|
|
825
|
+
relayPerfLog('dump', {
|
|
826
|
+
session: shortAffinity(affinity),
|
|
827
|
+
ms: formatMs(perfNowMs() - dumpStart),
|
|
828
|
+
enabled: true,
|
|
829
|
+
});
|
|
705
830
|
return result.response;
|
|
706
831
|
}
|
|
707
832
|
catch (error) {
|