@cortexkit/anthropic-auth-core 1.0.0 → 1.1.1
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 +22 -0
- package/dist/accounts.js +117 -39
- package/dist/auth.d.ts +16 -0
- package/dist/auth.js +37 -0
- package/dist/cch.js +3 -1
- package/dist/claude-code.d.ts +19 -0
- package/dist/claude-code.js +258 -0
- package/dist/constants.d.ts +8 -2
- package/dist/constants.js +17 -2
- package/dist/fast.d.ts +21 -0
- package/dist/fast.js +66 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/relay.d.ts +2 -1
- package/dist/relay.js +74 -6
- 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, relay, dump, and request-signing helpers used by CortexKit's OpenCode and Pi integrations.
|
|
3
|
+
Shared Anthropic OAuth, quota, cache, 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
|
@@ -20,6 +20,9 @@ export type OAuthAccount = {
|
|
|
20
20
|
export type AccountOperationError = {
|
|
21
21
|
message: string;
|
|
22
22
|
checkedAt: number;
|
|
23
|
+
nextRetryAt?: number;
|
|
24
|
+
retryCount?: number;
|
|
25
|
+
tokenHash?: string;
|
|
23
26
|
};
|
|
24
27
|
export type AccountQuotaWindow = {
|
|
25
28
|
usedPercent: number;
|
|
@@ -38,6 +41,10 @@ export type AccountStorage = {
|
|
|
38
41
|
enabled?: boolean;
|
|
39
42
|
intervalMinutes?: number;
|
|
40
43
|
refreshBeforeExpiryMinutes?: number;
|
|
44
|
+
mainLastRefreshError?: AccountOperationError;
|
|
45
|
+
mainRefreshLeaseId?: string;
|
|
46
|
+
mainRefreshLeaseUntil?: number;
|
|
47
|
+
mainRefreshLeaseTokenHash?: string;
|
|
41
48
|
};
|
|
42
49
|
quota?: {
|
|
43
50
|
enabled?: boolean;
|
|
@@ -52,6 +59,9 @@ export type AccountStorage = {
|
|
|
52
59
|
dump?: {
|
|
53
60
|
enabled?: boolean;
|
|
54
61
|
};
|
|
62
|
+
claudeFast?: {
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
};
|
|
55
65
|
relay?: {
|
|
56
66
|
enabled?: boolean;
|
|
57
67
|
url?: string;
|
|
@@ -80,7 +90,19 @@ export declare function setCache1hPersistentEnabled(enabled: boolean, mode?: Cac
|
|
|
80
90
|
export declare function setCache1hPersistentMode(mode: Cache1hMode, path?: string): Promise<AccountStorage>;
|
|
81
91
|
export declare function isDumpPersistentlyEnabled(storage: AccountStorage | null): boolean;
|
|
82
92
|
export declare function setDumpPersistentEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
|
|
93
|
+
export declare function isFastModePersistentlyEnabled(storage: AccountStorage | null): boolean;
|
|
94
|
+
export declare function setFastModePersistentEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
|
|
83
95
|
export declare function shouldFallbackStatus(status: number, storage: AccountStorage | null): boolean;
|
|
96
|
+
export declare function getRefreshIntervalMs(storage: AccountStorage | null): number;
|
|
97
|
+
export declare function hashRefreshToken(refreshToken: string): string;
|
|
98
|
+
export declare function buildRefreshOperationError(input: {
|
|
99
|
+
error: unknown;
|
|
100
|
+
now: number;
|
|
101
|
+
refreshToken: string;
|
|
102
|
+
previous?: AccountOperationError;
|
|
103
|
+
}): AccountOperationError;
|
|
104
|
+
export declare function refreshBackoffActive(error: AccountOperationError | undefined, refreshToken: string | undefined, now: number): boolean;
|
|
105
|
+
export declare function formatRefreshBackoffMessage(error: AccountOperationError, now: number): string;
|
|
84
106
|
export declare function getQuotaCheckIntervalMs(storage: AccountStorage | null): number;
|
|
85
107
|
export declare function quotaSnapshotPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null): boolean;
|
|
86
108
|
export declare function getQuotaNextRefreshAt(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, now: number): number;
|
package/dist/accounts.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import { ClaudeOAuthRefreshError, refreshClaudeOAuthToken } from "./auth.js";
|
|
6
|
+
import { CACHE_1H_MODES, CLAUDE_CODE_VERSION, DEFAULT_CACHE_1H_MODE, } from "./constants.js";
|
|
6
7
|
export const ACCOUNT_FILE_NAME = 'anthropic-auth.json';
|
|
7
8
|
export const QUOTA_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
8
9
|
const DEFAULT_FALLBACK_ON = [401, 403, 429];
|
|
9
10
|
const DEFAULT_REFRESH_BEFORE_EXPIRY_MINUTES = 30;
|
|
11
|
+
const DEFAULT_REFRESH_INTERVAL_MINUTES = 10;
|
|
12
|
+
const MIN_REFRESH_RETRY_DELAY_MS = 5 * 60_000;
|
|
13
|
+
const MAX_REFRESH_RETRY_DELAY_MS = 60 * 60_000;
|
|
14
|
+
const NON_TRANSIENT_REFRESH_RETRY_DELAY_MS = 24 * 60 * 60_000;
|
|
10
15
|
const DEFAULT_QUOTA_CHECK_INTERVAL_MINUTES = 5;
|
|
11
16
|
const DEFAULT_MINIMUM_REMAINING = {
|
|
12
17
|
five_hour: 0,
|
|
@@ -62,7 +67,15 @@ function normalizeOperationError(value) {
|
|
|
62
67
|
const checkedAt = Number(value.checkedAt);
|
|
63
68
|
if (!Number.isFinite(checkedAt))
|
|
64
69
|
return undefined;
|
|
65
|
-
|
|
70
|
+
const nextRetryAt = Number(value.nextRetryAt);
|
|
71
|
+
const retryCount = Number(value.retryCount);
|
|
72
|
+
return {
|
|
73
|
+
message: value.message,
|
|
74
|
+
checkedAt,
|
|
75
|
+
nextRetryAt: Number.isFinite(nextRetryAt) ? nextRetryAt : undefined,
|
|
76
|
+
retryCount: Number.isFinite(retryCount) ? retryCount : undefined,
|
|
77
|
+
tokenHash: typeof value.tokenHash === 'string' ? value.tokenHash : undefined,
|
|
78
|
+
};
|
|
66
79
|
}
|
|
67
80
|
function normalizeQuota(value) {
|
|
68
81
|
if (!isRecord(value))
|
|
@@ -102,6 +115,7 @@ function normalizeStorage(value) {
|
|
|
102
115
|
quota: isRecord(value.quota) ? value.quota : undefined,
|
|
103
116
|
claudeCache: isRecord(value.claudeCache) ? value.claudeCache : undefined,
|
|
104
117
|
dump: isRecord(value.dump) ? value.dump : undefined,
|
|
118
|
+
claudeFast: isRecord(value.claudeFast) ? value.claudeFast : undefined,
|
|
105
119
|
relay: isRecord(value.relay) ? value.relay : undefined,
|
|
106
120
|
accounts: value.accounts
|
|
107
121
|
.map(normalizeAccount)
|
|
@@ -199,6 +213,22 @@ export async function setDumpPersistentEnabled(enabled, path = getAccountStorage
|
|
|
199
213
|
await saveAccounts(storage, path);
|
|
200
214
|
return storage;
|
|
201
215
|
}
|
|
216
|
+
export function isFastModePersistentlyEnabled(storage) {
|
|
217
|
+
return storage?.claudeFast?.enabled === true;
|
|
218
|
+
}
|
|
219
|
+
export async function setFastModePersistentEnabled(enabled, path = getAccountStoragePath()) {
|
|
220
|
+
const storage = (await loadAccounts(path)) ?? {
|
|
221
|
+
version: 1,
|
|
222
|
+
main: { type: 'opencode', provider: 'anthropic' },
|
|
223
|
+
accounts: [],
|
|
224
|
+
};
|
|
225
|
+
storage.claudeFast = {
|
|
226
|
+
...(storage.claudeFast ?? {}),
|
|
227
|
+
enabled,
|
|
228
|
+
};
|
|
229
|
+
await saveAccounts(storage, path);
|
|
230
|
+
return storage;
|
|
231
|
+
}
|
|
202
232
|
function getFallbackStatuses(storage) {
|
|
203
233
|
return storage?.fallbackOn?.length ? storage.fallbackOn : DEFAULT_FALLBACK_ON;
|
|
204
234
|
}
|
|
@@ -227,6 +257,50 @@ function refreshBeforeExpiryMs(storage) {
|
|
|
227
257
|
DEFAULT_REFRESH_BEFORE_EXPIRY_MINUTES;
|
|
228
258
|
return Math.max(0, minutes) * 60_000;
|
|
229
259
|
}
|
|
260
|
+
export function getRefreshIntervalMs(storage) {
|
|
261
|
+
const minutes = storage?.refresh?.intervalMinutes ?? DEFAULT_REFRESH_INTERVAL_MINUTES;
|
|
262
|
+
return Math.max(1, minutes) * 60_000;
|
|
263
|
+
}
|
|
264
|
+
export function hashRefreshToken(refreshToken) {
|
|
265
|
+
return createHash('sha256').update(refreshToken).digest('hex');
|
|
266
|
+
}
|
|
267
|
+
function isTransientRefreshError(error) {
|
|
268
|
+
if (error instanceof ClaudeOAuthRefreshError) {
|
|
269
|
+
return error.status === 429 || error.status >= 500;
|
|
270
|
+
}
|
|
271
|
+
if (!(error instanceof Error))
|
|
272
|
+
return false;
|
|
273
|
+
return (error.message.includes('fetch failed') ||
|
|
274
|
+
('code' in error &&
|
|
275
|
+
(error.code === 'ECONNRESET' ||
|
|
276
|
+
error.code === 'ECONNREFUSED' ||
|
|
277
|
+
error.code === 'ETIMEDOUT' ||
|
|
278
|
+
error.code === 'UND_ERR_CONNECT_TIMEOUT')));
|
|
279
|
+
}
|
|
280
|
+
export function buildRefreshOperationError(input) {
|
|
281
|
+
const retryCount = (input.previous?.retryCount ?? 0) + 1;
|
|
282
|
+
const delay = isTransientRefreshError(input.error)
|
|
283
|
+
? Math.min(MAX_REFRESH_RETRY_DELAY_MS, MIN_REFRESH_RETRY_DELAY_MS * 2 ** Math.min(retryCount - 1, 6))
|
|
284
|
+
: NON_TRANSIENT_REFRESH_RETRY_DELAY_MS;
|
|
285
|
+
return {
|
|
286
|
+
message: formatErrorMessage(input.error),
|
|
287
|
+
checkedAt: input.now,
|
|
288
|
+
nextRetryAt: input.now + delay,
|
|
289
|
+
retryCount,
|
|
290
|
+
tokenHash: hashRefreshToken(input.refreshToken),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
export function refreshBackoffActive(error, refreshToken, now) {
|
|
294
|
+
if (!error?.nextRetryAt || error.nextRetryAt <= now)
|
|
295
|
+
return false;
|
|
296
|
+
if (!refreshToken)
|
|
297
|
+
return true;
|
|
298
|
+
return error.tokenHash === hashRefreshToken(refreshToken);
|
|
299
|
+
}
|
|
300
|
+
export function formatRefreshBackoffMessage(error, now) {
|
|
301
|
+
const seconds = Math.max(1, Math.ceil(((error.nextRetryAt ?? now) - now) / 1000));
|
|
302
|
+
return `Claude OAuth refresh is backed off for ${seconds}s after: ${error.message}`;
|
|
303
|
+
}
|
|
230
304
|
export function getQuotaCheckIntervalMs(storage) {
|
|
231
305
|
const minutes = storage?.quota?.checkIntervalMinutes ?? DEFAULT_QUOTA_CHECK_INTERVAL_MINUTES;
|
|
232
306
|
return Math.max(1, minutes) * 60_000;
|
|
@@ -335,16 +409,21 @@ function formatErrorMessage(error) {
|
|
|
335
409
|
return error instanceof Error ? error.message : String(error);
|
|
336
410
|
}
|
|
337
411
|
function recordRefreshError(account, error, now) {
|
|
338
|
-
account.lastRefreshError = {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
412
|
+
account.lastRefreshError = buildRefreshOperationError({
|
|
413
|
+
error,
|
|
414
|
+
now,
|
|
415
|
+
refreshToken: account.refresh,
|
|
416
|
+
previous: account.lastRefreshError,
|
|
417
|
+
});
|
|
342
418
|
}
|
|
343
419
|
function recordQuotaRefreshError(account, error, now) {
|
|
344
420
|
account.lastQuotaRefreshError = {
|
|
345
421
|
message: formatErrorMessage(error),
|
|
346
422
|
checkedAt: now,
|
|
347
423
|
};
|
|
424
|
+
if (error instanceof ClaudeOAuthRefreshError) {
|
|
425
|
+
recordRefreshError(account, error, now);
|
|
426
|
+
}
|
|
348
427
|
}
|
|
349
428
|
export class FallbackAccountManager {
|
|
350
429
|
now;
|
|
@@ -365,22 +444,18 @@ export class FallbackAccountManager {
|
|
|
365
444
|
await saveAccounts(storage, this.configPath);
|
|
366
445
|
}
|
|
367
446
|
startBackgroundRefresh() {
|
|
368
|
-
|
|
369
|
-
|
|
447
|
+
const run = async () => {
|
|
448
|
+
await this.refreshDueAccounts();
|
|
449
|
+
await this.refreshQuotaForDueAccounts();
|
|
450
|
+
};
|
|
451
|
+
void run().catch(() => { });
|
|
370
452
|
if (!this.refreshTimer) {
|
|
371
453
|
this.refreshTimer = setInterval(() => {
|
|
372
|
-
void
|
|
454
|
+
void run().catch(() => { });
|
|
373
455
|
}, BACKGROUND_TICK_MS);
|
|
374
456
|
if ('unref' in this.refreshTimer)
|
|
375
457
|
this.refreshTimer.unref();
|
|
376
458
|
}
|
|
377
|
-
if (!this.quotaTimer) {
|
|
378
|
-
this.quotaTimer = setInterval(() => {
|
|
379
|
-
void this.refreshQuotaForDueAccounts().catch(() => { });
|
|
380
|
-
}, BACKGROUND_TICK_MS);
|
|
381
|
-
if ('unref' in this.quotaTimer)
|
|
382
|
-
this.quotaTimer.unref();
|
|
383
|
-
}
|
|
384
459
|
}
|
|
385
460
|
stopBackgroundRefresh() {
|
|
386
461
|
if (this.refreshTimer)
|
|
@@ -402,6 +477,11 @@ export class FallbackAccountManager {
|
|
|
402
477
|
try {
|
|
403
478
|
let next = account;
|
|
404
479
|
if (tokenNeedsRefresh(next, storage, this.now())) {
|
|
480
|
+
const refreshError = next.lastRefreshError;
|
|
481
|
+
if (refreshError &&
|
|
482
|
+
refreshBackoffActive(refreshError, next.refresh, this.now())) {
|
|
483
|
+
throw new Error(formatRefreshBackoffMessage(refreshError, this.now()));
|
|
484
|
+
}
|
|
405
485
|
next = await this.refreshAccount(next, storage);
|
|
406
486
|
changed = true;
|
|
407
487
|
}
|
|
@@ -444,6 +524,9 @@ export class FallbackAccountManager {
|
|
|
444
524
|
continue;
|
|
445
525
|
if (!tokenNeedsRefresh(account, storage, this.now()))
|
|
446
526
|
continue;
|
|
527
|
+
if (refreshBackoffActive(account.lastRefreshError, account.refresh, this.now())) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
447
530
|
try {
|
|
448
531
|
await this.refreshAccount(account, storage);
|
|
449
532
|
changed = true;
|
|
@@ -469,6 +552,9 @@ export class FallbackAccountManager {
|
|
|
469
552
|
let next = account;
|
|
470
553
|
try {
|
|
471
554
|
if (tokenNeedsRefresh(next, storage, this.now())) {
|
|
555
|
+
if (refreshBackoffActive(next.lastRefreshError, next.refresh, this.now())) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
472
558
|
next = await this.refreshAccount(next, storage);
|
|
473
559
|
changed = true;
|
|
474
560
|
}
|
|
@@ -499,6 +585,11 @@ export class FallbackAccountManager {
|
|
|
499
585
|
let next = account;
|
|
500
586
|
try {
|
|
501
587
|
if (tokenNeedsRefresh(next, storage, this.now())) {
|
|
588
|
+
const refreshError = next.lastRefreshError;
|
|
589
|
+
if (refreshError &&
|
|
590
|
+
refreshBackoffActive(refreshError, next.refresh, this.now())) {
|
|
591
|
+
throw new Error(formatRefreshBackoffMessage(refreshError, this.now()));
|
|
592
|
+
}
|
|
502
593
|
next = await this.refreshAccount(next, storage);
|
|
503
594
|
changed = true;
|
|
504
595
|
}
|
|
@@ -545,29 +636,16 @@ export class FallbackAccountManager {
|
|
|
545
636
|
}
|
|
546
637
|
const sourceAccount = latestAccount ?? account;
|
|
547
638
|
const refreshToken = sourceAccount.refresh;
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
Accept: 'application/json, text/plain, */*',
|
|
553
|
-
'User-Agent': 'axios/1.13.6',
|
|
554
|
-
},
|
|
555
|
-
body: JSON.stringify({
|
|
556
|
-
grant_type: 'refresh_token',
|
|
557
|
-
refresh_token: refreshToken,
|
|
558
|
-
client_id: CLIENT_ID,
|
|
559
|
-
}),
|
|
639
|
+
const refreshed = await refreshClaudeOAuthToken({
|
|
640
|
+
refreshToken,
|
|
641
|
+
fetchImpl: this.fetchImpl,
|
|
642
|
+
now: this.now,
|
|
560
643
|
});
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const refreshedAt = this.now();
|
|
567
|
-
sourceAccount.access = json.access_token;
|
|
568
|
-
sourceAccount.refresh = json.refresh_token ?? refreshToken;
|
|
569
|
-
sourceAccount.expires = refreshedAt + json.expires_in * 1000;
|
|
570
|
-
sourceAccount.lastRefreshedAt = refreshedAt;
|
|
644
|
+
sourceAccount.access = refreshed.access;
|
|
645
|
+
sourceAccount.refresh = refreshed.refresh;
|
|
646
|
+
sourceAccount.expires = refreshed.expires;
|
|
647
|
+
sourceAccount.lastRefreshedAt =
|
|
648
|
+
refreshed.expires - refreshed.expiresIn * 1000;
|
|
571
649
|
sourceAccount.lastRefreshError = undefined;
|
|
572
650
|
updateStoredAccount(storage, sourceAccount);
|
|
573
651
|
return sourceAccount;
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
export declare class ClaudeOAuthRefreshError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly body: string;
|
|
4
|
+
constructor(status: number, body: string);
|
|
5
|
+
}
|
|
6
|
+
export type ClaudeOAuthRefreshResult = {
|
|
7
|
+
access: string;
|
|
8
|
+
refresh: string;
|
|
9
|
+
expires: number;
|
|
10
|
+
expiresIn: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function refreshClaudeOAuthToken(input: {
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
fetchImpl?: typeof fetch;
|
|
15
|
+
now?: () => number;
|
|
16
|
+
}): Promise<ClaudeOAuthRefreshResult>;
|
|
1
17
|
export type AuthorizationResult = {
|
|
2
18
|
url: string;
|
|
3
19
|
redirectUri: string;
|
package/dist/auth.js
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
import { AUTHORIZE_URLS, CLIENT_ID, CODE_CALLBACK_URL, OAUTH_SCOPES, TOKEN_URL, } from "./constants.js";
|
|
2
2
|
import { generatePKCE } from "./pkce.js";
|
|
3
|
+
export class ClaudeOAuthRefreshError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
body;
|
|
6
|
+
constructor(status, body) {
|
|
7
|
+
super(`Claude OAuth refresh failed: ${status} — ${body}`);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.body = body;
|
|
10
|
+
this.name = 'ClaudeOAuthRefreshError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function refreshClaudeOAuthToken(input) {
|
|
14
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
15
|
+
const response = await fetchImpl(TOKEN_URL, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
Accept: 'application/json',
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
grant_type: 'refresh_token',
|
|
23
|
+
refresh_token: input.refreshToken,
|
|
24
|
+
client_id: CLIENT_ID,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
const body = await response.text().catch(() => '');
|
|
29
|
+
throw new ClaudeOAuthRefreshError(response.status, body);
|
|
30
|
+
}
|
|
31
|
+
const json = (await response.json());
|
|
32
|
+
const refreshedAt = input.now?.() ?? Date.now();
|
|
33
|
+
return {
|
|
34
|
+
access: json.access_token,
|
|
35
|
+
refresh: json.refresh_token ?? input.refreshToken,
|
|
36
|
+
expires: refreshedAt + json.expires_in * 1000,
|
|
37
|
+
expiresIn: json.expires_in,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
3
40
|
function generateState() {
|
|
4
41
|
return crypto.randomUUID().replace(/-/g, '');
|
|
5
42
|
}
|
package/dist/cch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import xxhashInit from 'xxhash-wasm';
|
|
3
|
-
import { CLAUDE_CODE_VERSION } from "./constants.js";
|
|
3
|
+
import { CLAUDE_CODE_BUILD_HASH, CLAUDE_CODE_VERSION } from "./constants.js";
|
|
4
4
|
const CCH_SEED = 0x6e52736ac806831en;
|
|
5
5
|
export const CCH_PATTERN = /\bcch=([0-9a-f]{5});/;
|
|
6
6
|
let xxhashPromise = null;
|
|
@@ -58,6 +58,8 @@ export async function signRequestBody(bodyString) {
|
|
|
58
58
|
* busting prompt-cache prefixes on every turn.
|
|
59
59
|
*/
|
|
60
60
|
export function computeVersionSuffix(version = CLAUDE_CODE_VERSION, date = new Date()) {
|
|
61
|
+
if (version === CLAUDE_CODE_VERSION)
|
|
62
|
+
return CLAUDE_CODE_BUILD_HASH;
|
|
61
63
|
const dayStamp = date.toISOString().slice(0, 10);
|
|
62
64
|
return createHash('sha256')
|
|
63
65
|
.update(`${dayStamp}${version}`)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type ClaudeCodeIdentity = {
|
|
2
|
+
deviceId: string;
|
|
3
|
+
accountUuid?: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function getClaudeCodeIdentity(seed: string): ClaudeCodeIdentity;
|
|
7
|
+
export declare function resolveClaudeCodeIdentity(accessToken: string, model?: string): Promise<ClaudeCodeIdentity>;
|
|
8
|
+
export declare function buildClaudeCodeMetadataUserId(identity: ClaudeCodeIdentity): string | null;
|
|
9
|
+
export declare function applyClaudeCodeMetadata(body: Record<string, unknown>, identity: ClaudeCodeIdentity): boolean;
|
|
10
|
+
export declare const CLAUDE_CODE_FULL_AGENT_BETAS: readonly ["claude-code-20250219", "oauth-2025-04-20", "interleaved-thinking-2025-05-14", "context-management-2025-06-27", "prompt-caching-scope-2026-01-05", "advisor-tool-2026-03-01", "advanced-tool-use-2025-11-20", "context-1m-2025-08-07", "effort-2025-11-24", "extended-cache-ttl-2025-04-11", "cache-diagnosis-2026-04-07"];
|
|
11
|
+
export declare function selectClaudeCodeBetas(body?: Record<string, unknown> | null, extraBetas?: string[]): string;
|
|
12
|
+
export declare function applyClaudeCodeHeaders(headers: Headers, accessToken: string, options?: {
|
|
13
|
+
body?: Record<string, unknown> | null;
|
|
14
|
+
identity?: ClaudeCodeIdentity;
|
|
15
|
+
extraBetas?: string[];
|
|
16
|
+
}): Headers;
|
|
17
|
+
export declare function orderClaudeCodeBody<T extends Record<string, unknown>>(body: T): T;
|
|
18
|
+
export declare function claudeCodeBuildHash(): string;
|
|
19
|
+
export declare function claudeCodeEntrypoint(): string;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { CLAUDE_CODE_BUILD_HASH, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_STAINLESS_PACKAGE_VERSION, CLAUDE_CODE_STAINLESS_RUNTIME_VERSION, CLAUDE_CODE_VERSION, FAST_MODE_BETA, USER_AGENT, } from "./constants.js";
|
|
3
|
+
const IDENTITY_CACHE_LIMIT = 1_000;
|
|
4
|
+
const identityCache = new Map();
|
|
5
|
+
function setBounded(map, key, value) {
|
|
6
|
+
if (!map.has(key) && map.size >= IDENTITY_CACHE_LIMIT) {
|
|
7
|
+
const oldest = map.keys().next().value;
|
|
8
|
+
if (oldest !== undefined)
|
|
9
|
+
map.delete(oldest);
|
|
10
|
+
}
|
|
11
|
+
map.set(key, value);
|
|
12
|
+
}
|
|
13
|
+
export function getClaudeCodeIdentity(seed) {
|
|
14
|
+
const cacheKey = seed || 'anonymous';
|
|
15
|
+
const cached = identityCache.get(cacheKey);
|
|
16
|
+
if (cached)
|
|
17
|
+
return cached;
|
|
18
|
+
const identity = {
|
|
19
|
+
deviceId: randomBytes(32).toString('hex'),
|
|
20
|
+
sessionId: randomUUID(),
|
|
21
|
+
};
|
|
22
|
+
setBounded(identityCache, cacheKey, identity);
|
|
23
|
+
return identity;
|
|
24
|
+
}
|
|
25
|
+
const bootstrapFetches = new Map();
|
|
26
|
+
async function fetchClaudeCodeAccountUuid(accessToken, model) {
|
|
27
|
+
if (!accessToken.startsWith('sk-ant-oat'))
|
|
28
|
+
return null;
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL('https://api.anthropic.com/api/claude_cli/bootstrap');
|
|
33
|
+
url.searchParams.set('entrypoint', CLAUDE_CODE_ENTRYPOINT);
|
|
34
|
+
if (model)
|
|
35
|
+
url.searchParams.set('model', model);
|
|
36
|
+
const response = await fetch(url, {
|
|
37
|
+
method: 'GET',
|
|
38
|
+
headers: {
|
|
39
|
+
accept: 'application/json, text/plain, */*',
|
|
40
|
+
authorization: `Bearer ${accessToken}`,
|
|
41
|
+
'content-type': 'application/json',
|
|
42
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
43
|
+
'user-agent': `claude-code/${CLAUDE_CODE_VERSION}`,
|
|
44
|
+
},
|
|
45
|
+
signal: controller.signal,
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok)
|
|
48
|
+
return null;
|
|
49
|
+
const data = (await response.json().catch(() => null));
|
|
50
|
+
const accountUuid = data?.oauth_account?.account_uuid;
|
|
51
|
+
return typeof accountUuid === 'string' && accountUuid ? accountUuid : null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function resolveClaudeCodeIdentity(accessToken, model) {
|
|
61
|
+
const identity = getClaudeCodeIdentity(accessToken);
|
|
62
|
+
if (!accessToken.startsWith('sk-ant-oat'))
|
|
63
|
+
return identity;
|
|
64
|
+
let fetchPromise = bootstrapFetches.get(accessToken);
|
|
65
|
+
if (!fetchPromise) {
|
|
66
|
+
fetchPromise = fetchClaudeCodeAccountUuid(accessToken, model);
|
|
67
|
+
bootstrapFetches.set(accessToken, fetchPromise);
|
|
68
|
+
}
|
|
69
|
+
const accountUuid = await fetchPromise;
|
|
70
|
+
if (!accountUuid) {
|
|
71
|
+
bootstrapFetches.delete(accessToken);
|
|
72
|
+
return identity;
|
|
73
|
+
}
|
|
74
|
+
const accountCacheKey = `account:${accountUuid}`;
|
|
75
|
+
const accountIdentity = identityCache.get(accountCacheKey);
|
|
76
|
+
if (accountIdentity) {
|
|
77
|
+
setBounded(identityCache, accessToken, accountIdentity);
|
|
78
|
+
return accountIdentity;
|
|
79
|
+
}
|
|
80
|
+
identity.accountUuid = accountUuid;
|
|
81
|
+
setBounded(identityCache, accountCacheKey, identity);
|
|
82
|
+
return identity;
|
|
83
|
+
}
|
|
84
|
+
function isRecord(value) {
|
|
85
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
86
|
+
}
|
|
87
|
+
export function buildClaudeCodeMetadataUserId(identity) {
|
|
88
|
+
if (!identity.accountUuid)
|
|
89
|
+
return null;
|
|
90
|
+
return JSON.stringify({
|
|
91
|
+
device_id: identity.deviceId,
|
|
92
|
+
account_uuid: identity.accountUuid,
|
|
93
|
+
session_id: identity.sessionId,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export function applyClaudeCodeMetadata(body, identity) {
|
|
97
|
+
const userId = buildClaudeCodeMetadataUserId(identity);
|
|
98
|
+
if (!userId) {
|
|
99
|
+
if (isRecord(body.metadata))
|
|
100
|
+
delete body.metadata.user_id;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (!isRecord(body.metadata))
|
|
104
|
+
body.metadata = {};
|
|
105
|
+
const metadata = body.metadata;
|
|
106
|
+
if (isRecord(metadata))
|
|
107
|
+
metadata.user_id = userId;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
export const CLAUDE_CODE_FULL_AGENT_BETAS = [
|
|
111
|
+
'claude-code-20250219',
|
|
112
|
+
'oauth-2025-04-20',
|
|
113
|
+
'interleaved-thinking-2025-05-14',
|
|
114
|
+
'context-management-2025-06-27',
|
|
115
|
+
'prompt-caching-scope-2026-01-05',
|
|
116
|
+
'advisor-tool-2026-03-01',
|
|
117
|
+
'advanced-tool-use-2025-11-20',
|
|
118
|
+
'context-1m-2025-08-07',
|
|
119
|
+
'effort-2025-11-24',
|
|
120
|
+
'extended-cache-ttl-2025-04-11',
|
|
121
|
+
'cache-diagnosis-2026-04-07',
|
|
122
|
+
];
|
|
123
|
+
const CLAUDE_CODE_STRUCTURED_OUTPUT_BETAS = [
|
|
124
|
+
'oauth-2025-04-20',
|
|
125
|
+
'interleaved-thinking-2025-05-14',
|
|
126
|
+
'context-management-2025-06-27',
|
|
127
|
+
'prompt-caching-scope-2026-01-05',
|
|
128
|
+
'advisor-tool-2026-03-01',
|
|
129
|
+
'structured-outputs-2025-12-15',
|
|
130
|
+
'cache-diagnosis-2026-04-07',
|
|
131
|
+
];
|
|
132
|
+
const CLAUDE_CODE_BASE_BETAS = [
|
|
133
|
+
'oauth-2025-04-20',
|
|
134
|
+
'interleaved-thinking-2025-05-14',
|
|
135
|
+
'context-management-2025-06-27',
|
|
136
|
+
'prompt-caching-scope-2026-01-05',
|
|
137
|
+
'advisor-tool-2026-03-01',
|
|
138
|
+
'cache-diagnosis-2026-04-07',
|
|
139
|
+
];
|
|
140
|
+
function hasStructuredOutput(body) {
|
|
141
|
+
const outputConfig = body.output_config;
|
|
142
|
+
const format = outputConfig?.format;
|
|
143
|
+
return format?.type === 'json_schema';
|
|
144
|
+
}
|
|
145
|
+
function hasFullAgentShape(body) {
|
|
146
|
+
return (Array.isArray(body.tools) &&
|
|
147
|
+
body.tools.length > 0 &&
|
|
148
|
+
Array.isArray(body.system) &&
|
|
149
|
+
isRecord(body.thinking) &&
|
|
150
|
+
isRecord(body.context_management) &&
|
|
151
|
+
isRecord(body.output_config) &&
|
|
152
|
+
isRecord(body.diagnostics));
|
|
153
|
+
}
|
|
154
|
+
export function selectClaudeCodeBetas(body, extraBetas = []) {
|
|
155
|
+
const selected = body
|
|
156
|
+
? hasFullAgentShape(body)
|
|
157
|
+
? [...CLAUDE_CODE_FULL_AGENT_BETAS]
|
|
158
|
+
: hasStructuredOutput(body)
|
|
159
|
+
? [...CLAUDE_CODE_STRUCTURED_OUTPUT_BETAS]
|
|
160
|
+
: [...CLAUDE_CODE_BASE_BETAS]
|
|
161
|
+
: [...CLAUDE_CODE_BASE_BETAS];
|
|
162
|
+
if (body?.speed === 'fast')
|
|
163
|
+
selected.push(FAST_MODE_BETA);
|
|
164
|
+
for (const beta of extraBetas) {
|
|
165
|
+
const trimmed = beta.trim();
|
|
166
|
+
if (trimmed)
|
|
167
|
+
selected.push(trimmed);
|
|
168
|
+
}
|
|
169
|
+
return [...new Set(selected)].join(',');
|
|
170
|
+
}
|
|
171
|
+
function stainlessOS() {
|
|
172
|
+
switch (process.platform) {
|
|
173
|
+
case 'darwin':
|
|
174
|
+
return 'MacOS';
|
|
175
|
+
case 'win32':
|
|
176
|
+
return 'Windows';
|
|
177
|
+
case 'linux':
|
|
178
|
+
return 'Linux';
|
|
179
|
+
case 'freebsd':
|
|
180
|
+
return 'FreeBSD';
|
|
181
|
+
default:
|
|
182
|
+
return 'Unknown';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function stainlessArch() {
|
|
186
|
+
switch (process.arch) {
|
|
187
|
+
case 'arm64':
|
|
188
|
+
return 'arm64';
|
|
189
|
+
case 'x64':
|
|
190
|
+
return 'x64';
|
|
191
|
+
case 'ia32':
|
|
192
|
+
return 'x32';
|
|
193
|
+
default:
|
|
194
|
+
return process.arch;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export function applyClaudeCodeHeaders(headers, accessToken, options = {}) {
|
|
198
|
+
const identity = options.identity ?? getClaudeCodeIdentity(accessToken);
|
|
199
|
+
const incomingBetas = (headers.get('anthropic-beta') ?? '')
|
|
200
|
+
.split(',')
|
|
201
|
+
.map((beta) => beta.trim())
|
|
202
|
+
.filter(Boolean);
|
|
203
|
+
const extraBetas = [...incomingBetas, ...(options.extraBetas ?? [])];
|
|
204
|
+
headers.set('accept', 'application/json');
|
|
205
|
+
headers.set('authorization', `Bearer ${accessToken}`);
|
|
206
|
+
headers.set('content-type', 'application/json');
|
|
207
|
+
headers.set('user-agent', USER_AGENT);
|
|
208
|
+
headers.set('anthropic-beta', selectClaudeCodeBetas(options.body, extraBetas));
|
|
209
|
+
headers.set('anthropic-dangerous-direct-browser-access', 'true');
|
|
210
|
+
headers.set('anthropic-version', '2023-06-01');
|
|
211
|
+
headers.set('x-app', 'cli');
|
|
212
|
+
headers.set('x-client-request-id', randomUUID());
|
|
213
|
+
headers.set('x-claude-code-session-id', identity.sessionId);
|
|
214
|
+
headers.set('x-stainless-arch', stainlessArch());
|
|
215
|
+
headers.set('x-stainless-lang', 'js');
|
|
216
|
+
headers.set('x-stainless-os', stainlessOS());
|
|
217
|
+
headers.set('x-stainless-package-version', CLAUDE_CODE_STAINLESS_PACKAGE_VERSION);
|
|
218
|
+
headers.set('x-stainless-retry-count', '0');
|
|
219
|
+
headers.set('x-stainless-runtime', 'node');
|
|
220
|
+
headers.set('x-stainless-runtime-version', CLAUDE_CODE_STAINLESS_RUNTIME_VERSION);
|
|
221
|
+
headers.set('x-stainless-timeout', '600');
|
|
222
|
+
headers.delete('x-api-key');
|
|
223
|
+
return headers;
|
|
224
|
+
}
|
|
225
|
+
const BODY_FIELD_ORDER = [
|
|
226
|
+
'model',
|
|
227
|
+
'messages',
|
|
228
|
+
'system',
|
|
229
|
+
'tools',
|
|
230
|
+
'tool_choice',
|
|
231
|
+
'metadata',
|
|
232
|
+
'max_tokens',
|
|
233
|
+
'temperature',
|
|
234
|
+
'thinking',
|
|
235
|
+
'context_management',
|
|
236
|
+
'output_config',
|
|
237
|
+
'diagnostics',
|
|
238
|
+
'stream',
|
|
239
|
+
'speed',
|
|
240
|
+
];
|
|
241
|
+
export function orderClaudeCodeBody(body) {
|
|
242
|
+
const ordered = {};
|
|
243
|
+
for (const key of BODY_FIELD_ORDER) {
|
|
244
|
+
if (Object.hasOwn(body, key))
|
|
245
|
+
ordered[key] = body[key];
|
|
246
|
+
}
|
|
247
|
+
for (const [key, value] of Object.entries(body)) {
|
|
248
|
+
if (!Object.hasOwn(ordered, key))
|
|
249
|
+
ordered[key] = value;
|
|
250
|
+
}
|
|
251
|
+
return ordered;
|
|
252
|
+
}
|
|
253
|
+
export function claudeCodeBuildHash() {
|
|
254
|
+
return CLAUDE_CODE_BUILD_HASH;
|
|
255
|
+
}
|
|
256
|
+
export function claudeCodeEntrypoint() {
|
|
257
|
+
return CLAUDE_CODE_ENTRYPOINT;
|
|
258
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -8,13 +8,19 @@ export declare const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
|
8
8
|
export declare const OAUTH_SCOPES: string[];
|
|
9
9
|
export declare const TOOL_PREFIX = "mcp_";
|
|
10
10
|
export declare const REQUIRED_BETAS: string[];
|
|
11
|
+
export declare const FAST_MODE_BETA = "fast-mode-2026-02-01";
|
|
12
|
+
export declare function mergeAnthropicBetas(existing: string | null | undefined, betas: string[]): string;
|
|
13
|
+
export declare function isFastModeSupportedModel(model: unknown): boolean;
|
|
11
14
|
export declare const OPENCODE_IDENTITY_PREFIX = "You are OpenCode";
|
|
12
15
|
export declare const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
13
16
|
export declare const CCH_SALT = "59cf53e54c78";
|
|
14
17
|
export declare const CCH_POSITIONS: number[];
|
|
15
|
-
export declare const CLAUDE_CODE_VERSION = "2.1.
|
|
18
|
+
export declare const CLAUDE_CODE_VERSION = "2.1.141";
|
|
19
|
+
export declare const CLAUDE_CODE_BUILD_HASH = "67b";
|
|
16
20
|
export declare const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
|
|
17
|
-
export declare const
|
|
21
|
+
export declare const CLAUDE_CODE_STAINLESS_PACKAGE_VERSION = "0.94.0";
|
|
22
|
+
export declare const CLAUDE_CODE_STAINLESS_RUNTIME_VERSION = "v24.3.0";
|
|
23
|
+
export declare const USER_AGENT = "claude-cli/2.1.141 (external, sdk-cli)";
|
|
18
24
|
export declare const CACHE_1H_MODES: readonly ["explicit", "automatic", "hybrid"];
|
|
19
25
|
export type Cache1hMode = (typeof CACHE_1H_MODES)[number];
|
|
20
26
|
export declare const DEFAULT_CACHE_1H_MODE: Cache1hMode;
|
package/dist/constants.js
CHANGED
|
@@ -18,13 +18,28 @@ export const REQUIRED_BETAS = [
|
|
|
18
18
|
'oauth-2025-04-20',
|
|
19
19
|
'interleaved-thinking-2025-05-14',
|
|
20
20
|
];
|
|
21
|
+
export const FAST_MODE_BETA = 'fast-mode-2026-02-01';
|
|
22
|
+
export function mergeAnthropicBetas(existing, betas) {
|
|
23
|
+
const incoming = (existing ?? '')
|
|
24
|
+
.split(',')
|
|
25
|
+
.map((beta) => beta.trim())
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
return [...new Set([...incoming, ...betas])].join(',');
|
|
28
|
+
}
|
|
29
|
+
export function isFastModeSupportedModel(model) {
|
|
30
|
+
return (typeof model === 'string' &&
|
|
31
|
+
(model.startsWith('claude-opus-4-6') || model.startsWith('claude-opus-4-7')));
|
|
32
|
+
}
|
|
21
33
|
export const OPENCODE_IDENTITY_PREFIX = 'You are OpenCode';
|
|
22
34
|
export const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
23
35
|
export const CCH_SALT = '59cf53e54c78';
|
|
24
36
|
export const CCH_POSITIONS = [4, 7, 20];
|
|
25
|
-
export const CLAUDE_CODE_VERSION = '2.1.
|
|
37
|
+
export const CLAUDE_CODE_VERSION = '2.1.141';
|
|
38
|
+
export const CLAUDE_CODE_BUILD_HASH = '67b';
|
|
26
39
|
export const CLAUDE_CODE_ENTRYPOINT = 'sdk-cli';
|
|
27
|
-
export const
|
|
40
|
+
export const CLAUDE_CODE_STAINLESS_PACKAGE_VERSION = '0.94.0';
|
|
41
|
+
export const CLAUDE_CODE_STAINLESS_RUNTIME_VERSION = 'v24.3.0';
|
|
42
|
+
export const USER_AGENT = 'claude-cli/2.1.141 (external, sdk-cli)';
|
|
28
43
|
export const CACHE_1H_MODES = ['explicit', 'automatic', 'hybrid'];
|
|
29
44
|
export const DEFAULT_CACHE_1H_MODE = 'explicit';
|
|
30
45
|
/**
|
package/dist/fast.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const CLAUDE_FAST_COMMAND_NAME = "claude-fast";
|
|
2
|
+
export type FastModeCommandAction = {
|
|
3
|
+
type: 'status';
|
|
4
|
+
} | {
|
|
5
|
+
type: 'enable';
|
|
6
|
+
} | {
|
|
7
|
+
type: 'disable';
|
|
8
|
+
} | {
|
|
9
|
+
type: 'usage';
|
|
10
|
+
};
|
|
11
|
+
export declare function isFastModeEnabled(): boolean;
|
|
12
|
+
export declare function setFastModeEnabled(enabled: boolean): void;
|
|
13
|
+
export declare function resetFastModeState(): void;
|
|
14
|
+
export declare function parseFastModeCommandAction(argumentsText: string): FastModeCommandAction;
|
|
15
|
+
export declare function buildFastModeStatusSummary(input?: {
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
}): string;
|
|
18
|
+
export declare function executeFastModeCommand(input: {
|
|
19
|
+
argumentsText: string;
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
}): string;
|
package/dist/fast.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const CLAUDE_FAST_COMMAND_NAME = 'claude-fast';
|
|
2
|
+
const FAST_STATUS_TITLE = '## Claude Fast Mode Status';
|
|
3
|
+
const FAST_ENABLED_TITLE = '## Claude Fast Mode Enabled';
|
|
4
|
+
const FAST_DISABLED_TITLE = '## Claude Fast Mode Disabled';
|
|
5
|
+
const FAST_USAGE_TITLE = '## Claude Fast Mode Usage';
|
|
6
|
+
const FAST_USAGE = 'Usage: `/claude-fast`, `/claude-fast on`, or `/claude-fast off`.';
|
|
7
|
+
let fastModeEnabled = false;
|
|
8
|
+
export function isFastModeEnabled() {
|
|
9
|
+
return fastModeEnabled;
|
|
10
|
+
}
|
|
11
|
+
export function setFastModeEnabled(enabled) {
|
|
12
|
+
fastModeEnabled = enabled;
|
|
13
|
+
}
|
|
14
|
+
export function resetFastModeState() {
|
|
15
|
+
fastModeEnabled = false;
|
|
16
|
+
}
|
|
17
|
+
export function parseFastModeCommandAction(argumentsText) {
|
|
18
|
+
const normalized = argumentsText.trim().split(/\s+/).filter(Boolean);
|
|
19
|
+
if (normalized.length === 0)
|
|
20
|
+
return { type: 'status' };
|
|
21
|
+
if (normalized.length === 1 && normalized[0] === 'on')
|
|
22
|
+
return { type: 'enable' };
|
|
23
|
+
if (normalized.length === 1 && normalized[0] === 'off')
|
|
24
|
+
return { type: 'disable' };
|
|
25
|
+
return { type: 'usage' };
|
|
26
|
+
}
|
|
27
|
+
export function buildFastModeStatusSummary(input) {
|
|
28
|
+
const enabled = input?.enabled ?? fastModeEnabled;
|
|
29
|
+
return [
|
|
30
|
+
FAST_STATUS_TITLE,
|
|
31
|
+
'',
|
|
32
|
+
`- Enabled: ${enabled ? 'enabled' : 'disabled'}`,
|
|
33
|
+
'- Persisted: ~/.config/opencode/anthropic-auth.json',
|
|
34
|
+
'- Scope: adds Anthropic fast mode to supported Opus requests',
|
|
35
|
+
'- Supported models: claude-opus-4-6 and claude-opus-4-7',
|
|
36
|
+
'- Request changes: adds `speed: "fast"` and the `fast-mode-2026-02-01` beta header',
|
|
37
|
+
'- Note: fast and standard speeds do not share prompt-cache prefixes',
|
|
38
|
+
].join('\n');
|
|
39
|
+
}
|
|
40
|
+
export function executeFastModeCommand(input) {
|
|
41
|
+
const action = parseFastModeCommandAction(input.argumentsText);
|
|
42
|
+
const enabled = input.enabled ?? fastModeEnabled;
|
|
43
|
+
if (action.type === 'status')
|
|
44
|
+
return buildFastModeStatusSummary({ enabled });
|
|
45
|
+
if (action.type === 'enable') {
|
|
46
|
+
return [
|
|
47
|
+
FAST_ENABLED_TITLE,
|
|
48
|
+
'',
|
|
49
|
+
buildFastModeStatusSummary({ enabled: true }),
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
if (action.type === 'disable') {
|
|
53
|
+
return [
|
|
54
|
+
FAST_DISABLED_TITLE,
|
|
55
|
+
'',
|
|
56
|
+
buildFastModeStatusSummary({ enabled: false }),
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
59
|
+
return [
|
|
60
|
+
FAST_USAGE_TITLE,
|
|
61
|
+
'',
|
|
62
|
+
FAST_USAGE,
|
|
63
|
+
'',
|
|
64
|
+
buildFastModeStatusSummary({ enabled }),
|
|
65
|
+
].join('\n');
|
|
66
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ export * from './accounts.ts';
|
|
|
2
2
|
export * from './auth.ts';
|
|
3
3
|
export * from './cache1h.ts';
|
|
4
4
|
export * from './cch.ts';
|
|
5
|
+
export * from './claude-code.ts';
|
|
5
6
|
export * from './constants.ts';
|
|
6
7
|
export * from './dump.ts';
|
|
8
|
+
export * from './fast.ts';
|
|
7
9
|
export * from './logger.ts';
|
|
8
10
|
export * from './pkce.ts';
|
|
9
11
|
export * from './quotas.ts';
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,10 @@ export * from "./accounts.js";
|
|
|
2
2
|
export * from "./auth.js";
|
|
3
3
|
export * from "./cache1h.js";
|
|
4
4
|
export * from "./cch.js";
|
|
5
|
+
export * from "./claude-code.js";
|
|
5
6
|
export * from "./constants.js";
|
|
6
7
|
export * from "./dump.js";
|
|
8
|
+
export * from "./fast.js";
|
|
7
9
|
export * from "./logger.js";
|
|
8
10
|
export * from "./pkce.js";
|
|
9
11
|
export * from "./quotas.js";
|
package/dist/relay.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export declare function sendViaRelay(options: {
|
|
|
22
22
|
headers: Headers;
|
|
23
23
|
body: RequestInit['body'] | null | undefined;
|
|
24
24
|
fallback: () => Promise<Response>;
|
|
25
|
+
affinity?: string | null;
|
|
25
26
|
optimisticResponse?: boolean;
|
|
26
27
|
}): Promise<Response>;
|
|
27
|
-
export declare const WORKER_SCRIPT = "\nasync function hashBody(body) {\n const bytes = new TextEncoder().encode(body)\n const digest = await crypto.subtle.digest('SHA-256', bytes)\n const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('')\n return 'sha256:' + hex\n}\n\nfunction applyPatch(base, patch) {\n return base.slice(0, patch.start) + patch.insert + base.slice(patch.start + patch.deleteCount)\n}\n\nfunction applyPatchSet(base, patch) {\n if (!Array.isArray(patch)) return applyPatch(base, patch)\n if (patch.length === 0) return base\n\n let result = ''\n let cursor = 0\n for (const item of patch) {\n if (item.start < cursor) return { error: 'overlapping patch', status: 400 }\n result += base.slice(cursor, item.start)\n result += item.insert\n cursor = item.start + item.deleteCount\n }\n return result + base.slice(cursor)\n}\n\nasync function readState(env, affinity) {\n const raw = await env.RELAY_STATE.get('session:' + affinity)\n return raw ? JSON.parse(raw) : null\n}\n\nasync function writeState(env, affinity, state) {\n await env.RELAY_STATE.put('session:' + affinity, JSON.stringify(state), { expirationTtl: 86400 })\n}\n\nasync function resolveBody(env, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n const state = await readState(env, payload.affinity)\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nasync function prepareUpstream(env, payload) {\n if ((payload.protocol !== 1 && payload.protocol !== 2) || payload.type !== 'request' || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = await resolveBody(env, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n\n if (typeof body !== 'string' || (await hashBody(body)) !== payload.next_hash) {\n return { error: 'hash mismatch', status: 409 }\n }\n\n await writeState(env, payload.affinity, { body, hash: payload.next_hash, revision: payload.revision })\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'relay',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body }\n}\n\nfunction resolveBodyFromState(state, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nfunction deferWorkerTask(task) {\n return new Promise((resolve) => setTimeout(resolve, 0)).then(task)\n}\n\nfunction checkpointWebSocketState(env, payload, body, nextState) {\n return deferWorkerTask(async () => {\n try {\n if ((await hashBody(body)) !== payload.next_hash) {\n throw new Error('hash mismatch')\n }\n await writeState(env, payload.affinity, nextState)\n } catch (error) {\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n event: 'checkpoint_write_failed',\n affinity: String(payload.affinity).slice(0, 12),\n message: error instanceof Error ? error.message : String(error),\n }))\n }\n })\n}\n\nfunction prepareWebSocketUpstream(env, state, payload) {\n if (payload.protocol !== 2 || payload.type !== 'request' || !payload.id || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = resolveBodyFromState(state, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n if (typeof body !== 'string') return { error: 'invalid body', status: 400 }\n\n const nextState = { body, hash: payload.next_hash, revision: payload.revision }\n const checkpoint = checkpointWebSocketState(env, payload, body, nextState)\n const logAccepted = () => console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body, state: nextState, checkpoint, logAccepted }\n}\n\nfunction headersToObject(headers) {\n const result = {}\n for (const [key, value] of headers.entries()) result[key] = value\n return result\n}\n\nasync function handleRelayPayload(env, payload) {\n const prepared = await prepareUpstream(env, payload)\n if (prepared.error) return prepared\n const upstream = await fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: prepared.body,\n })\n return { upstream }\n}\n\nasync function handleWebSocket(socket, env, ctx, payload, getState, setState) {\n const heartbeat = setInterval(() => {\n try {\n socket.send(JSON.stringify({ protocol: 2, type: 'keepalive' }))\n } catch {\n clearInterval(heartbeat)\n }\n }, 5000)\n try {\n const result = prepareWebSocketUpstream(env, getState(), payload)\n if (result.error) {\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: result.status, message: result.error }))\n return\n }\n\n setState(result.state)\n socket.send(JSON.stringify({ protocol: 2, type: 'accepted', id: payload.id, hash: result.state.hash, revision: result.state.revision }))\n ctx?.waitUntil?.(deferWorkerTask(result.logAccepted))\n\n const upstreamPromise = fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: result.body,\n })\n ctx?.waitUntil?.(result.checkpoint)\n const upstream = await upstreamPromise\n socket.send(JSON.stringify({\n protocol: 2,\n type: 'response_start',\n id: payload.id,\n status: upstream.status,\n statusText: upstream.statusText,\n headers: headersToObject(upstream.headers),\n }))\n\n const reader = upstream.body?.getReader()\n if (reader) {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n socket.send(value)\n }\n }\n socket.send(JSON.stringify({ protocol: 2, type: 'done', id: payload.id }))\n } catch (error) {\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: 500, message: error instanceof Error ? error.message : String(error) }))\n } finally {\n clearInterval(heartbeat)\n }\n}\n\nexport default {\n async fetch(request, env, ctx) {\n if (request.headers.get('Upgrade') === 'websocket') {\n const url = new URL(request.url)\n const token = url.searchParams.get('token')\n const affinity = url.searchParams.get('affinity')\n if (token !== env.RELAY_TOKEN) return new Response('unauthorized', { status: 401 })\n if (!affinity) return new Response('missing affinity', { status: 400 })\n const pair = new WebSocketPair()\n const client = pair[0]\n const server = pair[1]\n server.binaryType = 'arraybuffer'\n let state = null\n let ready = false\n server.accept()\n const loadState = readState(env, affinity).then((loadedState) => {\n state = loadedState\n ready = true\n server.send(JSON.stringify({\n protocol: 2,\n type: 'ready',\n state: state ? { hash: state.hash, revision: state.revision } : null,\n }))\n }).catch((error) => {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 500, message: error instanceof Error ? error.message : String(error) }))\n server.close(1011, 'state load failed')\n })\n ctx?.waitUntil?.(loadState)\n if (!ctx?.waitUntil) void loadState\n let busy = false\n server.addEventListener('message', (event) => {\n if (!ready) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 425, message: 'relay state is not ready' }))\n return\n }\n if (busy) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 429, message: 'request already in flight' }))\n return\n }\n let payload\n try {\n payload = JSON.parse(event.data)\n } catch {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 400, message: 'invalid JSON payload' }))\n return\n }\n payload.affinity = affinity\n busy = true\n const run = handleWebSocket(server, env, ctx, payload, () => state, (nextState) => { state = nextState }).finally(() => { busy = false })\n ctx?.waitUntil?.(run)\n if (!ctx?.waitUntil) void run\n })\n return new Response(null, { status: 101, webSocket: client })\n }\n\n if (request.method === 'GET') return new Response('ok')\n if (request.method !== 'POST') return new Response('method not allowed', { status: 405 })\n if (request.headers.get('x-relay-token') !== env.RELAY_TOKEN) {\n return new Response('unauthorized', { status: 401 })\n }\n\n const payload = await request.json()\n const result = await handleRelayPayload(env, payload)\n if (result.error) return Response.json({ error: result.error }, { status: result.status })\n\n const upstream = result.upstream\n return new Response(upstream.body, {\n status: upstream.status,\n statusText: upstream.statusText,\n headers: upstream.headers,\n })\n },\n}\n";
|
|
28
|
+
export declare const WORKER_SCRIPT = "\nasync function hashBody(body) {\n const bytes = new TextEncoder().encode(body)\n const digest = await crypto.subtle.digest('SHA-256', bytes)\n const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('')\n return 'sha256:' + hex\n}\n\nfunction applyPatch(base, patch) {\n return base.slice(0, patch.start) + patch.insert + base.slice(patch.start + patch.deleteCount)\n}\n\nfunction applyPatchSet(base, patch) {\n if (!Array.isArray(patch)) return applyPatch(base, patch)\n if (patch.length === 0) return base\n\n let result = ''\n let cursor = 0\n for (const item of patch) {\n if (item.start < cursor) return { error: 'overlapping patch', status: 400 }\n result += base.slice(cursor, item.start)\n result += item.insert\n cursor = item.start + item.deleteCount\n }\n return result + base.slice(cursor)\n}\n\nasync function readState(env, affinity) {\n const raw = await env.RELAY_STATE.get('session:' + affinity)\n return raw ? JSON.parse(raw) : null\n}\n\nasync function writeState(env, affinity, state) {\n await env.RELAY_STATE.put('session:' + affinity, JSON.stringify(state), { expirationTtl: 86400 })\n}\n\nasync function resolveBody(env, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n const state = await readState(env, payload.affinity)\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nasync function prepareUpstream(env, payload) {\n if ((payload.protocol !== 1 && payload.protocol !== 2) || payload.type !== 'request' || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = await resolveBody(env, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n\n if (typeof body !== 'string' || (await hashBody(body)) !== payload.next_hash) {\n return { error: 'hash mismatch', status: 409 }\n }\n\n await writeState(env, payload.affinity, { body, hash: payload.next_hash, revision: payload.revision })\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'relay',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body }\n}\n\nfunction resolveBodyFromState(state, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nfunction deferWorkerTask(task) {\n return new Promise((resolve) => setTimeout(resolve, 0)).then(task)\n}\n\nfunction checkpointWebSocketState(env, payload, body, nextState) {\n return deferWorkerTask(async () => {\n try {\n if ((await hashBody(body)) !== payload.next_hash) {\n throw new Error('hash mismatch')\n }\n await writeState(env, payload.affinity, nextState)\n } catch (error) {\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n event: 'checkpoint_write_failed',\n affinity: String(payload.affinity).slice(0, 12),\n message: error instanceof Error ? error.message : String(error),\n }))\n }\n })\n}\n\nasync function prepareWebSocketUpstream(env, state, payload) {\n if (payload.protocol !== 2 || payload.type !== 'request' || !payload.id || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = resolveBodyFromState(state, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n if (typeof body !== 'string') return { error: 'invalid body', status: 400 }\n if ((await hashBody(body)) !== payload.next_hash) {\n return { error: 'hash mismatch', status: 409 }\n }\n\n const nextState = { body, hash: payload.next_hash, revision: payload.revision }\n const checkpoint = checkpointWebSocketState(env, payload, body, nextState)\n const logAccepted = () => console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body, state: nextState, checkpoint, logAccepted }\n}\n\nfunction headersToObject(headers) {\n const result = {}\n for (const [key, value] of headers.entries()) result[key] = value\n return result\n}\n\nasync function handleRelayPayload(env, payload) {\n const prepared = await prepareUpstream(env, payload)\n if (prepared.error) return prepared\n const upstream = await fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: prepared.body,\n })\n return { upstream }\n}\n\nasync function handleWebSocket(socket, env, ctx, payload, getState, setState) {\n const heartbeat = setInterval(() => {\n try {\n socket.send(JSON.stringify({ protocol: 2, type: 'keepalive' }))\n } catch {\n clearInterval(heartbeat)\n }\n }, 5000)\n try {\n const result = await prepareWebSocketUpstream(env, getState(), payload)\n if (result.error) {\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: result.status, message: result.error }))\n return\n }\n\n setState(result.state)\n socket.send(JSON.stringify({ protocol: 2, type: 'accepted', id: payload.id, hash: result.state.hash, revision: result.state.revision }))\n ctx?.waitUntil?.(deferWorkerTask(result.logAccepted))\n\n const upstreamPromise = fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: result.body,\n })\n ctx?.waitUntil?.(result.checkpoint)\n const upstream = await upstreamPromise\n socket.send(JSON.stringify({\n protocol: 2,\n type: 'response_start',\n id: payload.id,\n status: upstream.status,\n statusText: upstream.statusText,\n headers: headersToObject(upstream.headers),\n }))\n\n const reader = upstream.body?.getReader()\n if (reader) {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n socket.send(value)\n }\n }\n socket.send(JSON.stringify({ protocol: 2, type: 'done', id: payload.id }))\n } catch (error) {\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: 500, message: error instanceof Error ? error.message : String(error) }))\n } finally {\n clearInterval(heartbeat)\n }\n}\n\nexport default {\n async fetch(request, env, ctx) {\n if (request.headers.get('Upgrade') === 'websocket') {\n const url = new URL(request.url)\n const token = url.searchParams.get('token')\n const affinity = url.searchParams.get('affinity')\n if (token !== env.RELAY_TOKEN) return new Response('unauthorized', { status: 401 })\n if (!affinity) return new Response('missing affinity', { status: 400 })\n const pair = new WebSocketPair()\n const client = pair[0]\n const server = pair[1]\n server.binaryType = 'arraybuffer'\n let state = null\n let ready = false\n server.accept()\n const loadState = readState(env, affinity).then((loadedState) => {\n state = loadedState\n ready = true\n server.send(JSON.stringify({\n protocol: 2,\n type: 'ready',\n state: state ? { hash: state.hash, revision: state.revision } : null,\n }))\n }).catch((error) => {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 500, message: error instanceof Error ? error.message : String(error) }))\n server.close(1011, 'state load failed')\n })\n ctx?.waitUntil?.(loadState)\n if (!ctx?.waitUntil) void loadState\n let busy = false\n server.addEventListener('message', (event) => {\n if (!ready) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 425, message: 'relay state is not ready' }))\n return\n }\n if (busy) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 429, message: 'request already in flight' }))\n return\n }\n let payload\n try {\n payload = JSON.parse(event.data)\n } catch {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 400, message: 'invalid JSON payload' }))\n return\n }\n payload.affinity = affinity\n busy = true\n const run = handleWebSocket(server, env, ctx, payload, () => state, (nextState) => { state = nextState }).finally(() => { busy = false })\n ctx?.waitUntil?.(run)\n if (!ctx?.waitUntil) void run\n })\n return new Response(null, { status: 101, webSocket: client })\n }\n\n if (request.method === 'GET') return new Response('ok')\n if (request.method !== 'POST') return new Response('method not allowed', { status: 405 })\n if (request.headers.get('x-relay-token') !== env.RELAY_TOKEN) {\n return new Response('unauthorized', { status: 401 })\n }\n\n const payload = await request.json()\n const result = await handleRelayPayload(env, payload)\n if (result.error) return Response.json({ error: result.error }, { status: result.status })\n\n const upstream = result.upstream\n return new Response(upstream.body, {\n status: upstream.status,\n statusText: upstream.statusText,\n headers: upstream.headers,\n })\n },\n}\n";
|
package/dist/relay.js
CHANGED
|
@@ -107,7 +107,7 @@ function isRelayableAnthropicRequest(input, body) {
|
|
|
107
107
|
return false;
|
|
108
108
|
try {
|
|
109
109
|
const url = input instanceof Request ? new URL(input.url) : new URL(input.toString());
|
|
110
|
-
return url.pathname === '/v1/messages';
|
|
110
|
+
return url.pathname === '/v1/messages' || url.pathname === '/messages';
|
|
111
111
|
}
|
|
112
112
|
catch {
|
|
113
113
|
return false;
|
|
@@ -115,8 +115,11 @@ function isRelayableAnthropicRequest(input, body) {
|
|
|
115
115
|
}
|
|
116
116
|
function jsonHeaders(headers) {
|
|
117
117
|
const result = {};
|
|
118
|
-
for (const [key, value] of headers.entries())
|
|
118
|
+
for (const [key, value] of headers.entries()) {
|
|
119
|
+
if (key === 'x-session-affinity' || key === 'x-opencode-session')
|
|
120
|
+
continue;
|
|
119
121
|
result[key] = value;
|
|
122
|
+
}
|
|
120
123
|
return result;
|
|
121
124
|
}
|
|
122
125
|
async function postRelay(config, payload) {
|
|
@@ -363,6 +366,10 @@ class PersistentRelaySession {
|
|
|
363
366
|
this.connecting = undefined;
|
|
364
367
|
}
|
|
365
368
|
if (this.pending) {
|
|
369
|
+
if (this.pending.retryingBeforeResponse)
|
|
370
|
+
return;
|
|
371
|
+
if (this.retryPendingBeforeResponse(this.pending, 'closed before response'))
|
|
372
|
+
return;
|
|
366
373
|
const error = new Error('relay websocket closed before response');
|
|
367
374
|
this.failPending(error);
|
|
368
375
|
}
|
|
@@ -397,6 +404,8 @@ class PersistentRelaySession {
|
|
|
397
404
|
rejectDone,
|
|
398
405
|
sentAt,
|
|
399
406
|
optimisticResponse,
|
|
407
|
+
retryAttempts: 0,
|
|
408
|
+
retryingBeforeResponse: false,
|
|
400
409
|
};
|
|
401
410
|
this.pending = pending;
|
|
402
411
|
this.resetPendingTimeout(pending);
|
|
@@ -415,10 +424,60 @@ class PersistentRelaySession {
|
|
|
415
424
|
pending.timeout = setTimeout(() => {
|
|
416
425
|
if (this.pending !== pending)
|
|
417
426
|
return;
|
|
427
|
+
if (this.retryPendingBeforeResponse(pending, 'response timed out'))
|
|
428
|
+
return;
|
|
418
429
|
this.failPending(new Error('relay websocket response timed out'));
|
|
419
430
|
this.socket?.close();
|
|
420
431
|
}, 45_000);
|
|
421
432
|
}
|
|
433
|
+
retryPendingBeforeResponse(pending, reason) {
|
|
434
|
+
if (pending.responseStartedAt != null)
|
|
435
|
+
return false;
|
|
436
|
+
if (pending.streamDone)
|
|
437
|
+
return false;
|
|
438
|
+
if (pending.retryAttempts >= 1)
|
|
439
|
+
return false;
|
|
440
|
+
pending.retryAttempts += 1;
|
|
441
|
+
pending.retryingBeforeResponse = true;
|
|
442
|
+
clearTimeout(pending.timeout);
|
|
443
|
+
const attempt = pending.retryAttempts;
|
|
444
|
+
const originalId = pending.payload.id;
|
|
445
|
+
relayLog(`websocket ${reason}; reconnecting and retrying full_sync session=${shortAffinity(this.affinity)} request=${originalId} retry=${attempt}`);
|
|
446
|
+
this.socket?.close();
|
|
447
|
+
this.socket = undefined;
|
|
448
|
+
this.serverState = undefined;
|
|
449
|
+
this.connecting = undefined;
|
|
450
|
+
void (async () => {
|
|
451
|
+
try {
|
|
452
|
+
await this.ensureConnected();
|
|
453
|
+
if (this.pending !== pending)
|
|
454
|
+
return;
|
|
455
|
+
const retryPayload = createFullSyncPayload(pending.payload, pending.bodyText);
|
|
456
|
+
retryPayload.protocol = 2;
|
|
457
|
+
retryPayload.id = createRequestId();
|
|
458
|
+
retryPayload.revision = (this.serverState?.revision ?? 0) + 1;
|
|
459
|
+
pending.payload = retryPayload;
|
|
460
|
+
pending.accepted = false;
|
|
461
|
+
pending.acceptedAt = undefined;
|
|
462
|
+
pending.responseStartedAt = undefined;
|
|
463
|
+
pending.sentAt = perfNowMs();
|
|
464
|
+
pending.retryingBeforeResponse = false;
|
|
465
|
+
const socket = this.socket;
|
|
466
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
467
|
+
throw new Error('relay websocket is not connected');
|
|
468
|
+
}
|
|
469
|
+
this.resetPendingTimeout(pending);
|
|
470
|
+
relayLog(`perf websocket retry_send session=${shortAffinity(this.affinity)} request=${retryPayload.id} previous=${originalId} mode=full_sync retry=${attempt} bodyBytes=${pending.bodyText.length} relayBytes=${JSON.stringify(retryPayload).length}`);
|
|
471
|
+
socket.send(JSON.stringify(retryPayload));
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
pending.retryingBeforeResponse = false;
|
|
475
|
+
if (this.pending === pending)
|
|
476
|
+
this.failPending(error);
|
|
477
|
+
}
|
|
478
|
+
})();
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
422
481
|
handleBinaryChunk(data) {
|
|
423
482
|
const pending = this.pending;
|
|
424
483
|
if (!pending?.responseStarted)
|
|
@@ -485,6 +544,10 @@ class PersistentRelaySession {
|
|
|
485
544
|
return;
|
|
486
545
|
}
|
|
487
546
|
if (message.type === 'error') {
|
|
547
|
+
if (message.status === 409 &&
|
|
548
|
+
this.retryPendingBeforeResponse(pending, message.message || 'state mismatch')) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
488
551
|
const error = message.status === 409
|
|
489
552
|
? new RelayStateMismatchError(message.message || 'state mismatch')
|
|
490
553
|
: new Error(message.message || 'relay websocket error');
|
|
@@ -529,10 +592,12 @@ function getPersistentRelaySession(config, affinity) {
|
|
|
529
592
|
return session;
|
|
530
593
|
}
|
|
531
594
|
export async function sendViaRelay(options) {
|
|
532
|
-
const { config, input, init, headers, body, fallback, optimisticResponse } = options;
|
|
595
|
+
const { config, input, init, headers, body, fallback, affinity: explicitAffinity, optimisticResponse, } = options;
|
|
533
596
|
if (!config || !isRelayableAnthropicRequest(input, body))
|
|
534
597
|
return fallback();
|
|
535
|
-
const affinity =
|
|
598
|
+
const affinity = explicitAffinity ||
|
|
599
|
+
headers.get('x-session-affinity') ||
|
|
600
|
+
headers.get('x-opencode-session');
|
|
536
601
|
if (!affinity) {
|
|
537
602
|
relayLog('skipping relay: missing x-session-affinity header');
|
|
538
603
|
return fallback();
|
|
@@ -700,7 +765,7 @@ function checkpointWebSocketState(env, payload, body, nextState) {
|
|
|
700
765
|
})
|
|
701
766
|
}
|
|
702
767
|
|
|
703
|
-
function prepareWebSocketUpstream(env, state, payload) {
|
|
768
|
+
async function prepareWebSocketUpstream(env, state, payload) {
|
|
704
769
|
if (payload.protocol !== 2 || payload.type !== 'request' || !payload.id || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {
|
|
705
770
|
return { error: 'invalid payload', status: 400 }
|
|
706
771
|
}
|
|
@@ -708,6 +773,9 @@ function prepareWebSocketUpstream(env, state, payload) {
|
|
|
708
773
|
const body = resolveBodyFromState(state, payload)
|
|
709
774
|
if (body && typeof body === 'object' && 'error' in body) return body
|
|
710
775
|
if (typeof body !== 'string') return { error: 'invalid body', status: 400 }
|
|
776
|
+
if ((await hashBody(body)) !== payload.next_hash) {
|
|
777
|
+
return { error: 'hash mismatch', status: 409 }
|
|
778
|
+
}
|
|
711
779
|
|
|
712
780
|
const nextState = { body, hash: payload.next_hash, revision: payload.revision }
|
|
713
781
|
const checkpoint = checkpointWebSocketState(env, payload, body, nextState)
|
|
@@ -749,7 +817,7 @@ async function handleWebSocket(socket, env, ctx, payload, getState, setState) {
|
|
|
749
817
|
}
|
|
750
818
|
}, 5000)
|
|
751
819
|
try {
|
|
752
|
-
const result = prepareWebSocketUpstream(env, getState(), payload)
|
|
820
|
+
const result = await prepareWebSocketUpstream(env, getState(), payload)
|
|
753
821
|
if (result.error) {
|
|
754
822
|
socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: result.status, message: result.error }))
|
|
755
823
|
return
|