@vacbo/opencode-anthropic-fix 0.0.44 → 0.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 +19 -0
- package/dist/bun-proxy.mjs +282 -55
- package/dist/opencode-anthropic-auth-cli.mjs +194 -55
- package/dist/opencode-anthropic-auth-plugin.js +1816 -594
- package/package.json +1 -1
- package/src/__tests__/billing-edge-cases.test.ts +84 -0
- package/src/__tests__/bun-proxy.parallel.test.ts +460 -0
- package/src/__tests__/debug-gating.test.ts +76 -0
- package/src/__tests__/decomposition-smoke.test.ts +92 -0
- package/src/__tests__/fingerprint-regression.test.ts +1 -1
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +338 -0
- package/src/__tests__/helpers/conversation-history.ts +376 -0
- package/src/__tests__/helpers/deferred.smoke.test.ts +161 -0
- package/src/__tests__/helpers/deferred.ts +122 -0
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +166 -0
- package/src/__tests__/helpers/in-memory-storage.ts +152 -0
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +92 -0
- package/src/__tests__/helpers/mock-bun-proxy.ts +229 -0
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +337 -0
- package/src/__tests__/helpers/plugin-fetch-harness.ts +401 -0
- package/src/__tests__/helpers/sse.smoke.test.ts +243 -0
- package/src/__tests__/helpers/sse.ts +288 -0
- package/src/__tests__/index.parallel.test.ts +711 -0
- package/src/__tests__/sanitization-regex.test.ts +65 -0
- package/src/__tests__/state-bounds.test.ts +110 -0
- package/src/account-identity.test.ts +213 -0
- package/src/account-identity.ts +108 -0
- package/src/accounts.dedup.test.ts +696 -0
- package/src/accounts.test.ts +2 -1
- package/src/accounts.ts +485 -191
- package/src/bun-fetch.test.ts +379 -0
- package/src/bun-fetch.ts +447 -174
- package/src/bun-proxy.ts +289 -57
- package/src/circuit-breaker.test.ts +274 -0
- package/src/circuit-breaker.ts +235 -0
- package/src/cli.test.ts +1 -0
- package/src/cli.ts +37 -18
- package/src/commands/router.ts +25 -5
- package/src/env.ts +1 -0
- package/src/headers/billing.ts +31 -13
- package/src/index.ts +224 -247
- package/src/oauth.ts +7 -1
- package/src/parent-pid-watcher.test.ts +219 -0
- package/src/parent-pid-watcher.ts +99 -0
- package/src/plugin-helpers.ts +112 -0
- package/src/refresh-helpers.ts +169 -0
- package/src/refresh-lock.test.ts +36 -9
- package/src/refresh-lock.ts +2 -2
- package/src/request/body.history.test.ts +398 -0
- package/src/request/body.ts +200 -13
- package/src/request/metadata.ts +6 -2
- package/src/response/index.ts +1 -1
- package/src/response/mcp.ts +60 -31
- package/src/response/streaming.test.ts +382 -0
- package/src/response/streaming.ts +403 -76
- package/src/storage.test.ts +127 -104
- package/src/storage.ts +152 -62
- package/src/system-prompt/builder.ts +33 -3
- package/src/system-prompt/sanitize.ts +12 -2
- package/src/token-refresh.test.ts +84 -1
- package/src/token-refresh.ts +14 -8
|
@@ -31,6 +31,84 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
mod
|
|
32
32
|
));
|
|
33
33
|
|
|
34
|
+
// src/account-identity.ts
|
|
35
|
+
function isCCAccountSource(source) {
|
|
36
|
+
return source === "cc-keychain" || source === "cc-file";
|
|
37
|
+
}
|
|
38
|
+
function isAccountIdentity(value) {
|
|
39
|
+
if (!value || typeof value !== "object") return false;
|
|
40
|
+
const candidate = value;
|
|
41
|
+
switch (candidate.kind) {
|
|
42
|
+
case "oauth":
|
|
43
|
+
return typeof candidate.email === "string" && candidate.email.length > 0;
|
|
44
|
+
case "cc":
|
|
45
|
+
return isCCAccountSource(candidate.source) && typeof candidate.label === "string";
|
|
46
|
+
case "legacy":
|
|
47
|
+
return typeof candidate.refreshToken === "string" && candidate.refreshToken.length > 0;
|
|
48
|
+
default:
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function resolveIdentity(account) {
|
|
53
|
+
if (isAccountIdentity(account.identity)) {
|
|
54
|
+
return account.identity;
|
|
55
|
+
}
|
|
56
|
+
if (account.source === "oauth" && account.email) {
|
|
57
|
+
return { kind: "oauth", email: account.email };
|
|
58
|
+
}
|
|
59
|
+
if (isCCAccountSource(account.source) && account.label) {
|
|
60
|
+
return { kind: "cc", source: account.source, label: account.label };
|
|
61
|
+
}
|
|
62
|
+
return { kind: "legacy", refreshToken: account.refreshToken };
|
|
63
|
+
}
|
|
64
|
+
function resolveIdentityFromCCCredential(cred) {
|
|
65
|
+
return {
|
|
66
|
+
kind: "cc",
|
|
67
|
+
source: cred.source,
|
|
68
|
+
label: cred.label
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function resolveIdentityFromOAuthExchange(result) {
|
|
72
|
+
if (result.email) {
|
|
73
|
+
return {
|
|
74
|
+
kind: "oauth",
|
|
75
|
+
email: result.email
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
kind: "legacy",
|
|
80
|
+
refreshToken: result.refresh
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function identitiesMatch(a, b) {
|
|
84
|
+
if (a.kind !== b.kind) return false;
|
|
85
|
+
switch (a.kind) {
|
|
86
|
+
case "oauth": {
|
|
87
|
+
return a.email === b.email;
|
|
88
|
+
}
|
|
89
|
+
case "cc": {
|
|
90
|
+
const ccIdentity = b;
|
|
91
|
+
return a.source === ccIdentity.source && a.label === ccIdentity.label;
|
|
92
|
+
}
|
|
93
|
+
case "legacy": {
|
|
94
|
+
return a.refreshToken === b.refreshToken;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function findByIdentity(accounts, id) {
|
|
99
|
+
for (const account of accounts) {
|
|
100
|
+
if (identitiesMatch(resolveIdentity(account), id)) {
|
|
101
|
+
return account;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
var init_account_identity = __esm({
|
|
107
|
+
"src/account-identity.ts"() {
|
|
108
|
+
"use strict";
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
34
112
|
// src/config.ts
|
|
35
113
|
import { existsSync, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
|
|
36
114
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -404,6 +482,8 @@ function validateAccount(raw, now) {
|
|
|
404
482
|
return {
|
|
405
483
|
id,
|
|
406
484
|
email: typeof acc.email === "string" ? acc.email : void 0,
|
|
485
|
+
identity: isAccountIdentity2(acc.identity) ? acc.identity : void 0,
|
|
486
|
+
label: typeof acc.label === "string" ? acc.label : void 0,
|
|
407
487
|
refreshToken: acc.refreshToken,
|
|
408
488
|
access: typeof acc.access === "string" ? acc.access : void 0,
|
|
409
489
|
expires: typeof acc.expires === "number" && Number.isFinite(acc.expires) ? acc.expires : void 0,
|
|
@@ -419,6 +499,106 @@ function validateAccount(raw, now) {
|
|
|
419
499
|
source: acc.source === "cc-keychain" || acc.source === "cc-file" || acc.source === "oauth" ? acc.source : void 0
|
|
420
500
|
};
|
|
421
501
|
}
|
|
502
|
+
function isAccountIdentity2(value) {
|
|
503
|
+
if (!value || typeof value !== "object") return false;
|
|
504
|
+
const candidate = value;
|
|
505
|
+
switch (candidate.kind) {
|
|
506
|
+
case "oauth":
|
|
507
|
+
return typeof candidate.email === "string" && candidate.email.length > 0;
|
|
508
|
+
case "cc":
|
|
509
|
+
return (candidate.source === "cc-keychain" || candidate.source === "cc-file") && typeof candidate.label === "string";
|
|
510
|
+
case "legacy":
|
|
511
|
+
return typeof candidate.refreshToken === "string" && candidate.refreshToken.length > 0;
|
|
512
|
+
default:
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function resolveStoredIdentity(candidate) {
|
|
517
|
+
if (isAccountIdentity2(candidate.identity)) {
|
|
518
|
+
return candidate.identity;
|
|
519
|
+
}
|
|
520
|
+
if (candidate.source === "oauth" && candidate.email) {
|
|
521
|
+
return { kind: "oauth", email: candidate.email };
|
|
522
|
+
}
|
|
523
|
+
if ((candidate.source === "cc-keychain" || candidate.source === "cc-file") && candidate.label) {
|
|
524
|
+
return {
|
|
525
|
+
kind: "cc",
|
|
526
|
+
source: candidate.source,
|
|
527
|
+
label: candidate.label
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return { kind: "legacy", refreshToken: candidate.refreshToken };
|
|
531
|
+
}
|
|
532
|
+
function resolveTokenUpdatedAt(account) {
|
|
533
|
+
return typeof account.token_updated_at === "number" && Number.isFinite(account.token_updated_at) ? account.token_updated_at : account.addedAt;
|
|
534
|
+
}
|
|
535
|
+
function clampActiveIndex(accounts, activeIndex) {
|
|
536
|
+
if (accounts.length === 0) {
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
return Math.max(0, Math.min(activeIndex, accounts.length - 1));
|
|
540
|
+
}
|
|
541
|
+
function findStoredAccountMatch(accounts, candidate) {
|
|
542
|
+
const byId = accounts.find((account) => account.id === candidate.id);
|
|
543
|
+
if (byId) {
|
|
544
|
+
return byId;
|
|
545
|
+
}
|
|
546
|
+
const byIdentity = findByIdentity(accounts, resolveStoredIdentity(candidate));
|
|
547
|
+
if (byIdentity) {
|
|
548
|
+
return byIdentity;
|
|
549
|
+
}
|
|
550
|
+
const byAddedAt = accounts.filter((account) => account.addedAt === candidate.addedAt);
|
|
551
|
+
if (byAddedAt.length === 1) {
|
|
552
|
+
return byAddedAt[0];
|
|
553
|
+
}
|
|
554
|
+
const byRefreshToken = accounts.find((account) => account.refreshToken === candidate.refreshToken);
|
|
555
|
+
if (byRefreshToken) {
|
|
556
|
+
return byRefreshToken;
|
|
557
|
+
}
|
|
558
|
+
return byAddedAt[0] ?? null;
|
|
559
|
+
}
|
|
560
|
+
function mergeAccountWithFresherAuth(account, diskMatch) {
|
|
561
|
+
const memoryTokenUpdatedAt = resolveTokenUpdatedAt(account);
|
|
562
|
+
const diskTokenUpdatedAt = diskMatch ? resolveTokenUpdatedAt(diskMatch) : 0;
|
|
563
|
+
if (!diskMatch || diskTokenUpdatedAt <= memoryTokenUpdatedAt) {
|
|
564
|
+
return {
|
|
565
|
+
...account,
|
|
566
|
+
token_updated_at: memoryTokenUpdatedAt
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
...account,
|
|
571
|
+
refreshToken: diskMatch.refreshToken,
|
|
572
|
+
access: diskMatch.access,
|
|
573
|
+
expires: diskMatch.expires,
|
|
574
|
+
token_updated_at: diskTokenUpdatedAt
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
function unionAccountsWithDisk(storage, disk) {
|
|
578
|
+
if (!disk || storage.accounts.length === 0) {
|
|
579
|
+
return {
|
|
580
|
+
...storage,
|
|
581
|
+
activeIndex: clampActiveIndex(storage.accounts, storage.activeIndex)
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
const activeAccountId = storage.accounts[storage.activeIndex]?.id ?? null;
|
|
585
|
+
const matchedDiskAccounts = /* @__PURE__ */ new Set();
|
|
586
|
+
const mergedAccounts = storage.accounts.map((account) => {
|
|
587
|
+
const diskMatch = findStoredAccountMatch(disk.accounts, account);
|
|
588
|
+
if (diskMatch) {
|
|
589
|
+
matchedDiskAccounts.add(diskMatch);
|
|
590
|
+
}
|
|
591
|
+
return mergeAccountWithFresherAuth(account, diskMatch);
|
|
592
|
+
});
|
|
593
|
+
const diskOnlyAccounts = disk.accounts.filter((account) => !matchedDiskAccounts.has(account));
|
|
594
|
+
const accounts = [...mergedAccounts, ...diskOnlyAccounts];
|
|
595
|
+
const activeIndex = activeAccountId ? accounts.findIndex((account) => account.id === activeAccountId) : -1;
|
|
596
|
+
return {
|
|
597
|
+
...storage,
|
|
598
|
+
accounts,
|
|
599
|
+
activeIndex: activeIndex >= 0 ? activeIndex : clampActiveIndex(accounts, storage.activeIndex)
|
|
600
|
+
};
|
|
601
|
+
}
|
|
422
602
|
async function loadAccounts() {
|
|
423
603
|
const storagePath = getStoragePath();
|
|
424
604
|
try {
|
|
@@ -428,7 +608,9 @@ async function loadAccounts() {
|
|
|
428
608
|
return null;
|
|
429
609
|
}
|
|
430
610
|
if (data.version !== CURRENT_VERSION) {
|
|
431
|
-
|
|
611
|
+
console.warn(
|
|
612
|
+
`Storage version mismatch: ${String(data.version)} vs ${CURRENT_VERSION}. Attempting best-effort migration.`
|
|
613
|
+
);
|
|
432
614
|
}
|
|
433
615
|
const now = Date.now();
|
|
434
616
|
const accounts = data.accounts.map((raw) => validateAccount(raw, now)).filter((acc) => acc !== null);
|
|
@@ -455,53 +637,13 @@ async function saveAccounts(storage) {
|
|
|
455
637
|
const configDir = dirname2(storagePath);
|
|
456
638
|
await fs.mkdir(configDir, { recursive: true });
|
|
457
639
|
ensureGitignore(configDir);
|
|
458
|
-
let storageToWrite =
|
|
640
|
+
let storageToWrite = {
|
|
641
|
+
...storage,
|
|
642
|
+
activeIndex: clampActiveIndex(storage.accounts, storage.activeIndex)
|
|
643
|
+
};
|
|
459
644
|
try {
|
|
460
645
|
const disk = await loadAccounts();
|
|
461
|
-
|
|
462
|
-
const diskById = new Map(disk.accounts.map((a) => [a.id, a]));
|
|
463
|
-
const diskByAddedAt = /* @__PURE__ */ new Map();
|
|
464
|
-
const diskByToken = new Map(disk.accounts.map((a) => [a.refreshToken, a]));
|
|
465
|
-
for (const d3 of disk.accounts) {
|
|
466
|
-
const bucket = diskByAddedAt.get(d3.addedAt) || [];
|
|
467
|
-
bucket.push(d3);
|
|
468
|
-
diskByAddedAt.set(d3.addedAt, bucket);
|
|
469
|
-
}
|
|
470
|
-
const findDiskMatch = (acc) => {
|
|
471
|
-
const byId = diskById.get(acc.id);
|
|
472
|
-
if (byId) return byId;
|
|
473
|
-
const byAddedAt = diskByAddedAt.get(acc.addedAt);
|
|
474
|
-
if (byAddedAt?.length === 1) return byAddedAt[0];
|
|
475
|
-
const byToken = diskByToken.get(acc.refreshToken);
|
|
476
|
-
if (byToken) return byToken;
|
|
477
|
-
if (byAddedAt && byAddedAt.length > 0) return byAddedAt[0];
|
|
478
|
-
return null;
|
|
479
|
-
};
|
|
480
|
-
const mergedAccounts = storage.accounts.map((acc) => {
|
|
481
|
-
const diskAcc = findDiskMatch(acc);
|
|
482
|
-
const memTs = typeof acc.token_updated_at === "number" && Number.isFinite(acc.token_updated_at) ? acc.token_updated_at : acc.addedAt;
|
|
483
|
-
const diskTs = diskAcc?.token_updated_at || 0;
|
|
484
|
-
const useDiskAuth = !!diskAcc && diskTs > memTs;
|
|
485
|
-
return {
|
|
486
|
-
...acc,
|
|
487
|
-
refreshToken: useDiskAuth ? diskAcc.refreshToken : acc.refreshToken,
|
|
488
|
-
access: useDiskAuth ? diskAcc.access : acc.access,
|
|
489
|
-
expires: useDiskAuth ? diskAcc.expires : acc.expires,
|
|
490
|
-
token_updated_at: useDiskAuth ? diskTs : memTs
|
|
491
|
-
};
|
|
492
|
-
});
|
|
493
|
-
let activeIndex = storage.activeIndex;
|
|
494
|
-
if (mergedAccounts.length > 0) {
|
|
495
|
-
activeIndex = Math.max(0, Math.min(activeIndex, mergedAccounts.length - 1));
|
|
496
|
-
} else {
|
|
497
|
-
activeIndex = 0;
|
|
498
|
-
}
|
|
499
|
-
storageToWrite = {
|
|
500
|
-
...storage,
|
|
501
|
-
accounts: mergedAccounts,
|
|
502
|
-
activeIndex
|
|
503
|
-
};
|
|
504
|
-
}
|
|
646
|
+
storageToWrite = unionAccountsWithDisk(storageToWrite, disk);
|
|
505
647
|
} catch {
|
|
506
648
|
}
|
|
507
649
|
const tempPath = `${storagePath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
@@ -530,6 +672,7 @@ var CURRENT_VERSION, GITIGNORE_ENTRIES;
|
|
|
530
672
|
var init_storage = __esm({
|
|
531
673
|
"src/storage.ts"() {
|
|
532
674
|
"use strict";
|
|
675
|
+
init_account_identity();
|
|
533
676
|
init_config();
|
|
534
677
|
CURRENT_VERSION = 1;
|
|
535
678
|
GITIGNORE_ENTRIES = [".gitignore", "anthropic-accounts.json", "anthropic-accounts.json.*.tmp"];
|
|
@@ -1944,14 +2087,20 @@ async function cmdLogin() {
|
|
|
1944
2087
|
const credentials = await runOAuthFlow();
|
|
1945
2088
|
if (!credentials) return 1;
|
|
1946
2089
|
const storage = stored || { version: 1, accounts: [], activeIndex: 0 };
|
|
1947
|
-
const
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2090
|
+
const identity = resolveIdentityFromOAuthExchange(credentials);
|
|
2091
|
+
const existing = findByIdentity(storage.accounts, identity) || storage.accounts.find((acc) => acc.refreshToken === credentials.refresh);
|
|
2092
|
+
if (existing) {
|
|
2093
|
+
const existingIdx = storage.accounts.indexOf(existing);
|
|
2094
|
+
existing.refreshToken = credentials.refresh;
|
|
2095
|
+
existing.access = credentials.access;
|
|
2096
|
+
existing.expires = credentials.expires;
|
|
2097
|
+
existing.token_updated_at = Date.now();
|
|
2098
|
+
if (credentials.email) existing.email = credentials.email;
|
|
2099
|
+
existing.identity = identity;
|
|
2100
|
+
existing.source = existing.source ?? "oauth";
|
|
2101
|
+
existing.enabled = true;
|
|
1953
2102
|
await saveAccounts(storage);
|
|
1954
|
-
const label2 = credentials.email || `Account ${existingIdx + 1}`;
|
|
2103
|
+
const label2 = credentials.email || existing.email || `Account ${existingIdx + 1}`;
|
|
1955
2104
|
O2.success(`Updated existing account #${existingIdx + 1} (${label2}).`);
|
|
1956
2105
|
return 0;
|
|
1957
2106
|
}
|
|
@@ -1963,6 +2112,7 @@ async function cmdLogin() {
|
|
|
1963
2112
|
storage.accounts.push({
|
|
1964
2113
|
id: `${now}:${credentials.refresh.slice(0, 12)}`,
|
|
1965
2114
|
email: credentials.email,
|
|
2115
|
+
identity,
|
|
1966
2116
|
refreshToken: credentials.refresh,
|
|
1967
2117
|
access: credentials.access,
|
|
1968
2118
|
expires: credentials.expires,
|
|
@@ -1973,7 +2123,8 @@ async function cmdLogin() {
|
|
|
1973
2123
|
rateLimitResetTimes: {},
|
|
1974
2124
|
consecutiveFailures: 0,
|
|
1975
2125
|
lastFailureTime: null,
|
|
1976
|
-
stats: createDefaultStats(now)
|
|
2126
|
+
stats: createDefaultStats(now),
|
|
2127
|
+
source: "oauth"
|
|
1977
2128
|
});
|
|
1978
2129
|
await saveAccounts(storage);
|
|
1979
2130
|
const label = credentials.email || `Account ${storage.accounts.length}`;
|
|
@@ -2171,7 +2322,8 @@ async function cmdList() {
|
|
|
2171
2322
|
}
|
|
2172
2323
|
}
|
|
2173
2324
|
if (anyRefreshed) {
|
|
2174
|
-
await saveAccounts(stored).catch(() => {
|
|
2325
|
+
await saveAccounts(stored).catch((err) => {
|
|
2326
|
+
console.error("[opencode-anthropic-auth] failed to persist refreshed tokens:", err);
|
|
2175
2327
|
});
|
|
2176
2328
|
}
|
|
2177
2329
|
O2.message(c2.bold("Anthropic Multi-Account Status"));
|
|
@@ -3167,6 +3319,7 @@ var USE_COLOR, ansi, c2, QUOTA_BUCKETS, USAGE_INDENT, USAGE_LABEL_WIDTH, ioConte
|
|
|
3167
3319
|
var init_cli = __esm({
|
|
3168
3320
|
async "src/cli.ts"() {
|
|
3169
3321
|
"use strict";
|
|
3322
|
+
init_account_identity();
|
|
3170
3323
|
init_config();
|
|
3171
3324
|
init_oauth();
|
|
3172
3325
|
init_storage();
|
|
@@ -3452,6 +3605,9 @@ function readCCCredentials() {
|
|
|
3452
3605
|
return credentials;
|
|
3453
3606
|
}
|
|
3454
3607
|
|
|
3608
|
+
// src/accounts.ts
|
|
3609
|
+
init_account_identity();
|
|
3610
|
+
|
|
3455
3611
|
// src/rotation.ts
|
|
3456
3612
|
init_config();
|
|
3457
3613
|
var HealthScoreTracker = class {
|
|
@@ -3637,6 +3793,111 @@ function selectAccount(candidates, strategy, currentIndex, healthTracker, tokenT
|
|
|
3637
3793
|
init_storage();
|
|
3638
3794
|
var MAX_ACCOUNTS = 10;
|
|
3639
3795
|
var RATE_LIMIT_KEY = "anthropic";
|
|
3796
|
+
function resolveAccountIdentity(params) {
|
|
3797
|
+
if (params.identity) {
|
|
3798
|
+
return params.identity;
|
|
3799
|
+
}
|
|
3800
|
+
if ((params.source === "cc-keychain" || params.source === "cc-file") && params.label) {
|
|
3801
|
+
return {
|
|
3802
|
+
kind: "cc",
|
|
3803
|
+
source: params.source,
|
|
3804
|
+
label: params.label
|
|
3805
|
+
};
|
|
3806
|
+
}
|
|
3807
|
+
if (params.email) {
|
|
3808
|
+
return {
|
|
3809
|
+
kind: "oauth",
|
|
3810
|
+
email: params.email
|
|
3811
|
+
};
|
|
3812
|
+
}
|
|
3813
|
+
return {
|
|
3814
|
+
kind: "legacy",
|
|
3815
|
+
refreshToken: params.refreshToken
|
|
3816
|
+
};
|
|
3817
|
+
}
|
|
3818
|
+
function createManagedAccount(init) {
|
|
3819
|
+
const now = init.now ?? Date.now();
|
|
3820
|
+
const addedAt = init.addedAt ?? now;
|
|
3821
|
+
const tokenUpdatedAt = init.tokenUpdatedAt ?? addedAt;
|
|
3822
|
+
const identity = resolveAccountIdentity({
|
|
3823
|
+
refreshToken: init.refreshToken,
|
|
3824
|
+
email: init.email,
|
|
3825
|
+
identity: init.identity,
|
|
3826
|
+
label: init.label,
|
|
3827
|
+
source: init.source
|
|
3828
|
+
});
|
|
3829
|
+
const email = init.email ?? (identity.kind === "oauth" ? identity.email : void 0);
|
|
3830
|
+
const label = init.label ?? (identity.kind === "cc" ? identity.label : void 0);
|
|
3831
|
+
const source = init.source ?? (identity.kind === "cc" ? identity.source : "oauth");
|
|
3832
|
+
return {
|
|
3833
|
+
id: init.id ?? `${addedAt}:${init.refreshToken.slice(0, 12)}`,
|
|
3834
|
+
index: init.index,
|
|
3835
|
+
email,
|
|
3836
|
+
identity,
|
|
3837
|
+
label,
|
|
3838
|
+
refreshToken: init.refreshToken,
|
|
3839
|
+
access: init.access,
|
|
3840
|
+
expires: init.expires,
|
|
3841
|
+
tokenUpdatedAt,
|
|
3842
|
+
addedAt,
|
|
3843
|
+
lastUsed: init.lastUsed ?? 0,
|
|
3844
|
+
enabled: init.enabled ?? true,
|
|
3845
|
+
rateLimitResetTimes: { ...init.rateLimitResetTimes ?? {} },
|
|
3846
|
+
consecutiveFailures: init.consecutiveFailures ?? 0,
|
|
3847
|
+
lastFailureTime: init.lastFailureTime ?? null,
|
|
3848
|
+
lastSwitchReason: init.lastSwitchReason ?? "initial",
|
|
3849
|
+
stats: init.stats ?? createDefaultStats(addedAt),
|
|
3850
|
+
source
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
function findMatchingAccount(accounts, params) {
|
|
3854
|
+
if (params.id) {
|
|
3855
|
+
const byId = accounts.find((account) => account.id === params.id);
|
|
3856
|
+
if (byId) return byId;
|
|
3857
|
+
}
|
|
3858
|
+
if (params.identity) {
|
|
3859
|
+
const byIdentity = findByIdentity(accounts, params.identity);
|
|
3860
|
+
if (byIdentity) return byIdentity;
|
|
3861
|
+
}
|
|
3862
|
+
if (params.refreshToken) {
|
|
3863
|
+
return accounts.find((account) => account.refreshToken === params.refreshToken) ?? null;
|
|
3864
|
+
}
|
|
3865
|
+
return null;
|
|
3866
|
+
}
|
|
3867
|
+
function reindexAccounts(accounts) {
|
|
3868
|
+
accounts.forEach((account, index) => {
|
|
3869
|
+
account.index = index;
|
|
3870
|
+
});
|
|
3871
|
+
}
|
|
3872
|
+
function updateManagedAccountFromStorage(existing, account, index) {
|
|
3873
|
+
const source = account.source || existing.source || "oauth";
|
|
3874
|
+
const label = account.label ?? existing.label;
|
|
3875
|
+
const email = account.email ?? existing.email;
|
|
3876
|
+
existing.id = account.id || existing.id || `${account.addedAt}:${account.refreshToken.slice(0, 12)}`;
|
|
3877
|
+
existing.index = index;
|
|
3878
|
+
existing.email = email;
|
|
3879
|
+
existing.label = label;
|
|
3880
|
+
existing.identity = resolveAccountIdentity({
|
|
3881
|
+
refreshToken: account.refreshToken,
|
|
3882
|
+
email,
|
|
3883
|
+
identity: account.identity ?? existing.identity,
|
|
3884
|
+
label,
|
|
3885
|
+
source
|
|
3886
|
+
});
|
|
3887
|
+
existing.refreshToken = account.refreshToken;
|
|
3888
|
+
existing.access = account.access ?? existing.access;
|
|
3889
|
+
existing.expires = account.expires ?? existing.expires;
|
|
3890
|
+
existing.tokenUpdatedAt = account.token_updated_at ?? existing.tokenUpdatedAt ?? account.addedAt;
|
|
3891
|
+
existing.addedAt = account.addedAt;
|
|
3892
|
+
existing.lastUsed = account.lastUsed;
|
|
3893
|
+
existing.enabled = account.enabled;
|
|
3894
|
+
existing.rateLimitResetTimes = { ...account.rateLimitResetTimes };
|
|
3895
|
+
existing.consecutiveFailures = account.consecutiveFailures;
|
|
3896
|
+
existing.lastFailureTime = account.lastFailureTime;
|
|
3897
|
+
existing.lastSwitchReason = account.lastSwitchReason || existing.lastSwitchReason || "initial";
|
|
3898
|
+
existing.stats = account.stats ?? existing.stats ?? createDefaultStats(account.addedAt);
|
|
3899
|
+
existing.source = source;
|
|
3900
|
+
}
|
|
3640
3901
|
var AccountManager = class _AccountManager {
|
|
3641
3902
|
#accounts = [];
|
|
3642
3903
|
#cursor = 0;
|
|
@@ -3646,11 +3907,22 @@ var AccountManager = class _AccountManager {
|
|
|
3646
3907
|
#config;
|
|
3647
3908
|
#saveTimeout = null;
|
|
3648
3909
|
#statsDeltas = /* @__PURE__ */ new Map();
|
|
3910
|
+
/**
|
|
3911
|
+
* Cap on pending stats deltas. When hit, a forced flush is scheduled so the
|
|
3912
|
+
* map does not grow without bound between debounced saves. This is only a
|
|
3913
|
+
* safety net — under normal load the 1s debounced save in `requestSaveToDisk`
|
|
3914
|
+
* keeps the delta count below this cap.
|
|
3915
|
+
*/
|
|
3916
|
+
#MAX_STATS_DELTAS = 100;
|
|
3649
3917
|
constructor(config) {
|
|
3650
3918
|
this.#config = config;
|
|
3651
3919
|
this.#healthTracker = new HealthScoreTracker(config.health_score);
|
|
3652
3920
|
this.#tokenTracker = new TokenBucketTracker(config.token_bucket);
|
|
3653
3921
|
}
|
|
3922
|
+
#rebuildTrackers() {
|
|
3923
|
+
this.#healthTracker = new HealthScoreTracker(this.#config.health_score);
|
|
3924
|
+
this.#tokenTracker = new TokenBucketTracker(this.#config.token_bucket);
|
|
3925
|
+
}
|
|
3654
3926
|
/**
|
|
3655
3927
|
* Load accounts from disk, optionally merging with an OpenCode auth fallback.
|
|
3656
3928
|
*/
|
|
@@ -3658,27 +3930,38 @@ var AccountManager = class _AccountManager {
|
|
|
3658
3930
|
const manager = new _AccountManager(config);
|
|
3659
3931
|
const stored = await loadAccounts();
|
|
3660
3932
|
if (stored) {
|
|
3661
|
-
manager.#accounts = stored.accounts.map(
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3933
|
+
manager.#accounts = stored.accounts.map(
|
|
3934
|
+
(acc, index) => createManagedAccount({
|
|
3935
|
+
id: acc.id || `${acc.addedAt}:${acc.refreshToken.slice(0, 12)}`,
|
|
3936
|
+
index,
|
|
3937
|
+
email: acc.email,
|
|
3938
|
+
identity: acc.identity,
|
|
3939
|
+
label: acc.label,
|
|
3940
|
+
refreshToken: acc.refreshToken,
|
|
3941
|
+
access: acc.access,
|
|
3942
|
+
expires: acc.expires,
|
|
3943
|
+
tokenUpdatedAt: acc.token_updated_at,
|
|
3944
|
+
addedAt: acc.addedAt,
|
|
3945
|
+
lastUsed: acc.lastUsed,
|
|
3946
|
+
enabled: acc.enabled,
|
|
3947
|
+
rateLimitResetTimes: acc.rateLimitResetTimes,
|
|
3948
|
+
consecutiveFailures: acc.consecutiveFailures,
|
|
3949
|
+
lastFailureTime: acc.lastFailureTime,
|
|
3950
|
+
lastSwitchReason: acc.lastSwitchReason,
|
|
3951
|
+
stats: acc.stats,
|
|
3952
|
+
source: acc.source || "oauth"
|
|
3953
|
+
})
|
|
3954
|
+
);
|
|
3679
3955
|
manager.#currentIndex = manager.#accounts.length > 0 ? Math.min(stored.activeIndex, manager.#accounts.length - 1) : -1;
|
|
3680
3956
|
if (authFallback && manager.#accounts.length > 0) {
|
|
3681
|
-
const
|
|
3957
|
+
const fallbackIdentity = resolveAccountIdentity({
|
|
3958
|
+
refreshToken: authFallback.refresh,
|
|
3959
|
+
source: "oauth"
|
|
3960
|
+
});
|
|
3961
|
+
const match = findMatchingAccount(manager.#accounts, {
|
|
3962
|
+
identity: fallbackIdentity,
|
|
3963
|
+
refreshToken: authFallback.refresh
|
|
3964
|
+
});
|
|
3682
3965
|
if (match) {
|
|
3683
3966
|
const fallbackHasAccess = typeof authFallback.access === "string" && authFallback.access.length > 0;
|
|
3684
3967
|
const fallbackExpires = typeof authFallback.expires === "number" ? authFallback.expires : 0;
|
|
@@ -3695,23 +3978,17 @@ var AccountManager = class _AccountManager {
|
|
|
3695
3978
|
} else if (authFallback && authFallback.refresh) {
|
|
3696
3979
|
const now = Date.now();
|
|
3697
3980
|
manager.#accounts = [
|
|
3698
|
-
{
|
|
3981
|
+
createManagedAccount({
|
|
3699
3982
|
id: `${now}:${authFallback.refresh.slice(0, 12)}`,
|
|
3700
3983
|
index: 0,
|
|
3701
|
-
email: void 0,
|
|
3702
3984
|
refreshToken: authFallback.refresh,
|
|
3703
3985
|
access: authFallback.access,
|
|
3704
3986
|
expires: authFallback.expires,
|
|
3705
3987
|
tokenUpdatedAt: now,
|
|
3706
3988
|
addedAt: now,
|
|
3707
|
-
lastUsed: 0,
|
|
3708
|
-
enabled: true,
|
|
3709
|
-
rateLimitResetTimes: {},
|
|
3710
|
-
consecutiveFailures: 0,
|
|
3711
|
-
lastFailureTime: null,
|
|
3712
3989
|
lastSwitchReason: "initial",
|
|
3713
|
-
|
|
3714
|
-
}
|
|
3990
|
+
source: "oauth"
|
|
3991
|
+
})
|
|
3715
3992
|
];
|
|
3716
3993
|
manager.#currentIndex = 0;
|
|
3717
3994
|
}
|
|
@@ -3725,49 +4002,60 @@ var AccountManager = class _AccountManager {
|
|
|
3725
4002
|
}
|
|
3726
4003
|
})();
|
|
3727
4004
|
for (const ccCredential of ccCredentials) {
|
|
3728
|
-
const
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
4005
|
+
const ccIdentity = resolveIdentityFromCCCredential(ccCredential);
|
|
4006
|
+
let existingMatch = findMatchingAccount(manager.#accounts, {
|
|
4007
|
+
identity: ccIdentity,
|
|
4008
|
+
refreshToken: ccCredential.refreshToken
|
|
4009
|
+
});
|
|
4010
|
+
if (!existingMatch) {
|
|
4011
|
+
const legacyUnlabeledMatches = manager.#accounts.filter(
|
|
4012
|
+
(account) => account.source === ccCredential.source && !account.label && !account.email
|
|
4013
|
+
);
|
|
4014
|
+
if (legacyUnlabeledMatches.length === 1) {
|
|
4015
|
+
existingMatch = legacyUnlabeledMatches[0];
|
|
3734
4016
|
}
|
|
3735
|
-
|
|
4017
|
+
}
|
|
4018
|
+
if (existingMatch) {
|
|
4019
|
+
existingMatch.refreshToken = ccCredential.refreshToken;
|
|
4020
|
+
existingMatch.identity = ccIdentity;
|
|
4021
|
+
existingMatch.source = ccCredential.source;
|
|
4022
|
+
existingMatch.label = ccCredential.label;
|
|
4023
|
+
existingMatch.enabled = true;
|
|
4024
|
+
if (ccCredential.accessToken) {
|
|
3736
4025
|
existingMatch.access = ccCredential.accessToken;
|
|
4026
|
+
}
|
|
4027
|
+
if (ccCredential.expiresAt >= (existingMatch.expires ?? 0)) {
|
|
3737
4028
|
existingMatch.expires = ccCredential.expiresAt;
|
|
3738
4029
|
}
|
|
4030
|
+
existingMatch.tokenUpdatedAt = Math.max(existingMatch.tokenUpdatedAt || 0, ccCredential.expiresAt || 0);
|
|
4031
|
+
continue;
|
|
4032
|
+
}
|
|
4033
|
+
if (manager.#accounts.length >= MAX_ACCOUNTS) {
|
|
3739
4034
|
continue;
|
|
3740
4035
|
}
|
|
3741
4036
|
const emailCollision = manager.getOAuthAccounts().find((account) => account.email && ccCredential.label.includes(account.email));
|
|
3742
4037
|
if (emailCollision?.email) {
|
|
3743
4038
|
}
|
|
3744
4039
|
const now = Date.now();
|
|
3745
|
-
const ccAccount = {
|
|
4040
|
+
const ccAccount = createManagedAccount({
|
|
3746
4041
|
id: `cc-${ccCredential.source}-${now}:${ccCredential.refreshToken.slice(0, 12)}`,
|
|
3747
4042
|
index: manager.#accounts.length,
|
|
3748
|
-
email: void 0,
|
|
3749
4043
|
refreshToken: ccCredential.refreshToken,
|
|
3750
4044
|
access: ccCredential.accessToken,
|
|
3751
4045
|
expires: ccCredential.expiresAt,
|
|
3752
4046
|
tokenUpdatedAt: now,
|
|
3753
4047
|
addedAt: now,
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
rateLimitResetTimes: {},
|
|
3757
|
-
consecutiveFailures: 0,
|
|
3758
|
-
lastFailureTime: null,
|
|
4048
|
+
identity: ccIdentity,
|
|
4049
|
+
label: ccCredential.label,
|
|
3759
4050
|
lastSwitchReason: "cc-auto-detected",
|
|
3760
|
-
stats: createDefaultStats(now),
|
|
3761
4051
|
source: ccCredential.source
|
|
3762
|
-
};
|
|
4052
|
+
});
|
|
3763
4053
|
manager.#accounts.push(ccAccount);
|
|
3764
4054
|
}
|
|
3765
4055
|
if (config.cc_credential_reuse.prefer_over_oauth && manager.getCCAccounts().length > 0) {
|
|
3766
4056
|
manager.#accounts = [...manager.getCCAccounts(), ...manager.getOAuthAccounts()];
|
|
3767
4057
|
}
|
|
3768
|
-
manager.#accounts
|
|
3769
|
-
account.index = index;
|
|
3770
|
-
});
|
|
4058
|
+
reindexAccounts(manager.#accounts);
|
|
3771
4059
|
if (config.cc_credential_reuse.prefer_over_oauth && manager.getCCAccounts().length > 0) {
|
|
3772
4060
|
manager.#currentIndex = 0;
|
|
3773
4061
|
} else if (currentAccountId) {
|
|
@@ -3907,35 +4195,47 @@ var AccountManager = class _AccountManager {
|
|
|
3907
4195
|
* Add a new account to the pool.
|
|
3908
4196
|
* @returns The new account, or null if at capacity
|
|
3909
4197
|
*/
|
|
3910
|
-
addAccount(refreshToken2, accessToken, expires, email) {
|
|
3911
|
-
|
|
3912
|
-
|
|
4198
|
+
addAccount(refreshToken2, accessToken, expires, email, options) {
|
|
4199
|
+
const identity = resolveAccountIdentity({
|
|
4200
|
+
refreshToken: refreshToken2,
|
|
4201
|
+
email,
|
|
4202
|
+
identity: options?.identity,
|
|
4203
|
+
label: options?.label,
|
|
4204
|
+
source: options?.source ?? "oauth"
|
|
4205
|
+
});
|
|
4206
|
+
const existing = findMatchingAccount(this.#accounts, {
|
|
4207
|
+
identity,
|
|
4208
|
+
refreshToken: refreshToken2
|
|
4209
|
+
});
|
|
3913
4210
|
if (existing) {
|
|
4211
|
+
existing.refreshToken = refreshToken2;
|
|
3914
4212
|
existing.access = accessToken;
|
|
3915
4213
|
existing.expires = expires;
|
|
3916
4214
|
existing.tokenUpdatedAt = Date.now();
|
|
3917
|
-
|
|
4215
|
+
existing.email = email ?? existing.email;
|
|
4216
|
+
existing.identity = identity;
|
|
4217
|
+
existing.label = options?.label ?? existing.label;
|
|
4218
|
+
existing.source = options?.source ?? existing.source ?? "oauth";
|
|
3918
4219
|
existing.enabled = true;
|
|
4220
|
+
this.requestSaveToDisk();
|
|
3919
4221
|
return existing;
|
|
3920
4222
|
}
|
|
4223
|
+
if (this.#accounts.length >= MAX_ACCOUNTS) return null;
|
|
3921
4224
|
const now = Date.now();
|
|
3922
|
-
const account = {
|
|
4225
|
+
const account = createManagedAccount({
|
|
3923
4226
|
id: `${now}:${refreshToken2.slice(0, 12)}`,
|
|
3924
4227
|
index: this.#accounts.length,
|
|
3925
|
-
email,
|
|
3926
4228
|
refreshToken: refreshToken2,
|
|
3927
4229
|
access: accessToken,
|
|
3928
4230
|
expires,
|
|
3929
4231
|
tokenUpdatedAt: now,
|
|
3930
4232
|
addedAt: now,
|
|
3931
|
-
lastUsed: 0,
|
|
3932
|
-
enabled: true,
|
|
3933
|
-
rateLimitResetTimes: {},
|
|
3934
|
-
consecutiveFailures: 0,
|
|
3935
|
-
lastFailureTime: null,
|
|
3936
4233
|
lastSwitchReason: "initial",
|
|
3937
|
-
|
|
3938
|
-
|
|
4234
|
+
email,
|
|
4235
|
+
identity,
|
|
4236
|
+
label: options?.label,
|
|
4237
|
+
source: options?.source ?? "oauth"
|
|
4238
|
+
});
|
|
3939
4239
|
this.#accounts.push(account);
|
|
3940
4240
|
if (this.#accounts.length === 1) {
|
|
3941
4241
|
this.#currentIndex = 0;
|
|
@@ -3949,9 +4249,7 @@ var AccountManager = class _AccountManager {
|
|
|
3949
4249
|
removeAccount(index) {
|
|
3950
4250
|
if (index < 0 || index >= this.#accounts.length) return false;
|
|
3951
4251
|
this.#accounts.splice(index, 1);
|
|
3952
|
-
this.#accounts
|
|
3953
|
-
acc.index = i;
|
|
3954
|
-
});
|
|
4252
|
+
reindexAccounts(this.#accounts);
|
|
3955
4253
|
if (this.#accounts.length === 0) {
|
|
3956
4254
|
this.#currentIndex = -1;
|
|
3957
4255
|
this.#cursor = 0;
|
|
@@ -3963,9 +4261,7 @@ var AccountManager = class _AccountManager {
|
|
|
3963
4261
|
this.#cursor = Math.min(this.#cursor, this.#accounts.length);
|
|
3964
4262
|
}
|
|
3965
4263
|
}
|
|
3966
|
-
|
|
3967
|
-
this.#healthTracker.reset(i);
|
|
3968
|
-
}
|
|
4264
|
+
this.#rebuildTrackers();
|
|
3969
4265
|
this.requestSaveToDisk();
|
|
3970
4266
|
return true;
|
|
3971
4267
|
}
|
|
@@ -3995,7 +4291,10 @@ var AccountManager = class _AccountManager {
|
|
|
3995
4291
|
if (this.#saveTimeout) clearTimeout(this.#saveTimeout);
|
|
3996
4292
|
this.#saveTimeout = setTimeout(() => {
|
|
3997
4293
|
this.#saveTimeout = null;
|
|
3998
|
-
this.saveToDisk().catch(() => {
|
|
4294
|
+
this.saveToDisk().catch((err) => {
|
|
4295
|
+
if (this.#config.debug) {
|
|
4296
|
+
console.error("[opencode-anthropic-auth] saveToDisk failed:", err.message);
|
|
4297
|
+
}
|
|
3999
4298
|
});
|
|
4000
4299
|
}, 1e3);
|
|
4001
4300
|
}
|
|
@@ -4008,9 +4307,11 @@ var AccountManager = class _AccountManager {
|
|
|
4008
4307
|
let diskAccountsById = null;
|
|
4009
4308
|
let diskAccountsByAddedAt = null;
|
|
4010
4309
|
let diskAccountsByRefreshToken = null;
|
|
4310
|
+
let diskAccounts = [];
|
|
4011
4311
|
try {
|
|
4012
4312
|
const diskData = await loadAccounts();
|
|
4013
4313
|
if (diskData) {
|
|
4314
|
+
diskAccounts = diskData.accounts;
|
|
4014
4315
|
diskAccountsById = new Map(diskData.accounts.map((a) => [a.id, a]));
|
|
4015
4316
|
diskAccountsByAddedAt = /* @__PURE__ */ new Map();
|
|
4016
4317
|
diskAccountsByRefreshToken = /* @__PURE__ */ new Map();
|
|
@@ -4026,6 +4327,8 @@ var AccountManager = class _AccountManager {
|
|
|
4026
4327
|
const findDiskAccount = (account) => {
|
|
4027
4328
|
const byId = diskAccountsById?.get(account.id);
|
|
4028
4329
|
if (byId) return byId;
|
|
4330
|
+
const byIdentity = findByIdentity(diskAccounts, resolveIdentity(account));
|
|
4331
|
+
if (byIdentity) return byIdentity;
|
|
4029
4332
|
const byAddedAt = diskAccountsByAddedAt?.get(account.addedAt);
|
|
4030
4333
|
if (byAddedAt?.length === 1) return byAddedAt[0];
|
|
4031
4334
|
const byToken = diskAccountsByRefreshToken?.get(account.refreshToken);
|
|
@@ -4033,70 +4336,82 @@ var AccountManager = class _AccountManager {
|
|
|
4033
4336
|
if (byAddedAt && byAddedAt.length > 0) return byAddedAt[0];
|
|
4034
4337
|
return null;
|
|
4035
4338
|
};
|
|
4339
|
+
const matchedDiskAccounts = /* @__PURE__ */ new Set();
|
|
4340
|
+
const activeAccountId = this.#accounts[this.#currentIndex]?.id ?? null;
|
|
4341
|
+
const accountsToPersist = this.#accounts.filter((account) => account.enabled || !!findDiskAccount(account));
|
|
4342
|
+
const persistedAccounts = accountsToPersist.map((acc) => {
|
|
4343
|
+
const delta = this.#statsDeltas.get(acc.id);
|
|
4344
|
+
let mergedStats = acc.stats;
|
|
4345
|
+
const diskAcc = findDiskAccount(acc);
|
|
4346
|
+
if (diskAcc) {
|
|
4347
|
+
matchedDiskAccounts.add(diskAcc);
|
|
4348
|
+
}
|
|
4349
|
+
if (delta) {
|
|
4350
|
+
const diskStats = diskAcc?.stats;
|
|
4351
|
+
if (delta.isReset) {
|
|
4352
|
+
mergedStats = {
|
|
4353
|
+
requests: delta.requests,
|
|
4354
|
+
inputTokens: delta.inputTokens,
|
|
4355
|
+
outputTokens: delta.outputTokens,
|
|
4356
|
+
cacheReadTokens: delta.cacheReadTokens,
|
|
4357
|
+
cacheWriteTokens: delta.cacheWriteTokens,
|
|
4358
|
+
lastReset: delta.resetTimestamp ?? acc.stats.lastReset
|
|
4359
|
+
};
|
|
4360
|
+
} else if (diskStats) {
|
|
4361
|
+
mergedStats = {
|
|
4362
|
+
requests: diskStats.requests + delta.requests,
|
|
4363
|
+
inputTokens: diskStats.inputTokens + delta.inputTokens,
|
|
4364
|
+
outputTokens: diskStats.outputTokens + delta.outputTokens,
|
|
4365
|
+
cacheReadTokens: diskStats.cacheReadTokens + delta.cacheReadTokens,
|
|
4366
|
+
cacheWriteTokens: diskStats.cacheWriteTokens + delta.cacheWriteTokens,
|
|
4367
|
+
lastReset: diskStats.lastReset
|
|
4368
|
+
};
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
const memTokenUpdatedAt = acc.tokenUpdatedAt || 0;
|
|
4372
|
+
const diskTokenUpdatedAt = diskAcc?.token_updated_at || 0;
|
|
4373
|
+
const freshestAuth = diskAcc && diskTokenUpdatedAt > memTokenUpdatedAt ? {
|
|
4374
|
+
refreshToken: diskAcc.refreshToken,
|
|
4375
|
+
access: diskAcc.access,
|
|
4376
|
+
expires: diskAcc.expires,
|
|
4377
|
+
tokenUpdatedAt: diskTokenUpdatedAt
|
|
4378
|
+
} : {
|
|
4379
|
+
refreshToken: acc.refreshToken,
|
|
4380
|
+
access: acc.access,
|
|
4381
|
+
expires: acc.expires,
|
|
4382
|
+
tokenUpdatedAt: memTokenUpdatedAt
|
|
4383
|
+
};
|
|
4384
|
+
acc.refreshToken = freshestAuth.refreshToken;
|
|
4385
|
+
acc.access = freshestAuth.access;
|
|
4386
|
+
acc.expires = freshestAuth.expires;
|
|
4387
|
+
acc.tokenUpdatedAt = freshestAuth.tokenUpdatedAt;
|
|
4388
|
+
return {
|
|
4389
|
+
id: acc.id,
|
|
4390
|
+
email: acc.email,
|
|
4391
|
+
identity: acc.identity,
|
|
4392
|
+
label: acc.label,
|
|
4393
|
+
refreshToken: freshestAuth.refreshToken,
|
|
4394
|
+
access: freshestAuth.access,
|
|
4395
|
+
expires: freshestAuth.expires,
|
|
4396
|
+
token_updated_at: freshestAuth.tokenUpdatedAt,
|
|
4397
|
+
addedAt: acc.addedAt,
|
|
4398
|
+
lastUsed: acc.lastUsed,
|
|
4399
|
+
enabled: acc.enabled,
|
|
4400
|
+
rateLimitResetTimes: Object.keys(acc.rateLimitResetTimes).length > 0 ? acc.rateLimitResetTimes : {},
|
|
4401
|
+
consecutiveFailures: acc.consecutiveFailures,
|
|
4402
|
+
lastFailureTime: acc.lastFailureTime,
|
|
4403
|
+
lastSwitchReason: acc.lastSwitchReason,
|
|
4404
|
+
stats: mergedStats,
|
|
4405
|
+
source: acc.source
|
|
4406
|
+
};
|
|
4407
|
+
});
|
|
4408
|
+
const diskOnlyAccounts = diskAccounts.filter((account) => !matchedDiskAccounts.has(account));
|
|
4409
|
+
const allAccounts = accountsToPersist.length > 0 ? [...persistedAccounts, ...diskOnlyAccounts] : persistedAccounts;
|
|
4410
|
+
const resolvedActiveIndex = activeAccountId ? allAccounts.findIndex((account) => account.id === activeAccountId) : -1;
|
|
4036
4411
|
const storage = {
|
|
4037
4412
|
version: 1,
|
|
4038
|
-
accounts:
|
|
4039
|
-
|
|
4040
|
-
let mergedStats = acc.stats;
|
|
4041
|
-
const diskAcc = findDiskAccount(acc);
|
|
4042
|
-
if (delta) {
|
|
4043
|
-
const diskStats = diskAcc?.stats;
|
|
4044
|
-
if (delta.isReset) {
|
|
4045
|
-
mergedStats = {
|
|
4046
|
-
requests: delta.requests,
|
|
4047
|
-
inputTokens: delta.inputTokens,
|
|
4048
|
-
outputTokens: delta.outputTokens,
|
|
4049
|
-
cacheReadTokens: delta.cacheReadTokens,
|
|
4050
|
-
cacheWriteTokens: delta.cacheWriteTokens,
|
|
4051
|
-
lastReset: delta.resetTimestamp ?? acc.stats.lastReset
|
|
4052
|
-
};
|
|
4053
|
-
} else if (diskStats) {
|
|
4054
|
-
mergedStats = {
|
|
4055
|
-
requests: diskStats.requests + delta.requests,
|
|
4056
|
-
inputTokens: diskStats.inputTokens + delta.inputTokens,
|
|
4057
|
-
outputTokens: diskStats.outputTokens + delta.outputTokens,
|
|
4058
|
-
cacheReadTokens: diskStats.cacheReadTokens + delta.cacheReadTokens,
|
|
4059
|
-
cacheWriteTokens: diskStats.cacheWriteTokens + delta.cacheWriteTokens,
|
|
4060
|
-
lastReset: diskStats.lastReset
|
|
4061
|
-
};
|
|
4062
|
-
}
|
|
4063
|
-
}
|
|
4064
|
-
const memTokenUpdatedAt = acc.tokenUpdatedAt || 0;
|
|
4065
|
-
const diskTokenUpdatedAt = diskAcc?.token_updated_at || 0;
|
|
4066
|
-
const freshestAuth = diskAcc && diskTokenUpdatedAt > memTokenUpdatedAt ? {
|
|
4067
|
-
refreshToken: diskAcc.refreshToken,
|
|
4068
|
-
access: diskAcc.access,
|
|
4069
|
-
expires: diskAcc.expires,
|
|
4070
|
-
tokenUpdatedAt: diskTokenUpdatedAt
|
|
4071
|
-
} : {
|
|
4072
|
-
refreshToken: acc.refreshToken,
|
|
4073
|
-
access: acc.access,
|
|
4074
|
-
expires: acc.expires,
|
|
4075
|
-
tokenUpdatedAt: memTokenUpdatedAt
|
|
4076
|
-
};
|
|
4077
|
-
acc.refreshToken = freshestAuth.refreshToken;
|
|
4078
|
-
acc.access = freshestAuth.access;
|
|
4079
|
-
acc.expires = freshestAuth.expires;
|
|
4080
|
-
acc.tokenUpdatedAt = freshestAuth.tokenUpdatedAt;
|
|
4081
|
-
return {
|
|
4082
|
-
id: acc.id,
|
|
4083
|
-
email: acc.email,
|
|
4084
|
-
refreshToken: freshestAuth.refreshToken,
|
|
4085
|
-
access: freshestAuth.access,
|
|
4086
|
-
expires: freshestAuth.expires,
|
|
4087
|
-
token_updated_at: freshestAuth.tokenUpdatedAt,
|
|
4088
|
-
addedAt: acc.addedAt,
|
|
4089
|
-
lastUsed: acc.lastUsed,
|
|
4090
|
-
enabled: acc.enabled,
|
|
4091
|
-
rateLimitResetTimes: Object.keys(acc.rateLimitResetTimes).length > 0 ? acc.rateLimitResetTimes : {},
|
|
4092
|
-
consecutiveFailures: acc.consecutiveFailures,
|
|
4093
|
-
lastFailureTime: acc.lastFailureTime,
|
|
4094
|
-
lastSwitchReason: acc.lastSwitchReason,
|
|
4095
|
-
stats: mergedStats,
|
|
4096
|
-
source: acc.source
|
|
4097
|
-
};
|
|
4098
|
-
}),
|
|
4099
|
-
activeIndex: Math.max(0, this.#currentIndex)
|
|
4413
|
+
accounts: allAccounts,
|
|
4414
|
+
activeIndex: resolvedActiveIndex >= 0 ? resolvedActiveIndex : allAccounts.length > 0 ? Math.max(0, Math.min(this.#currentIndex, allAccounts.length - 1)) : 0
|
|
4100
4415
|
};
|
|
4101
4416
|
await saveAccounts(storage);
|
|
4102
4417
|
this.#statsDeltas.clear();
|
|
@@ -4113,57 +4428,92 @@ var AccountManager = class _AccountManager {
|
|
|
4113
4428
|
async syncActiveIndexFromDisk() {
|
|
4114
4429
|
const stored = await loadAccounts();
|
|
4115
4430
|
if (!stored) return;
|
|
4116
|
-
const
|
|
4117
|
-
const
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
const existingByToken = new Map(this.#accounts.map((acc) => [acc.refreshToken, acc]));
|
|
4125
|
-
this.#accounts = stored.accounts.map((acc, index) => {
|
|
4126
|
-
const existing = acc.id && existingById.get(acc.id) || (!acc.id ? existingByToken.get(acc.refreshToken) : null);
|
|
4127
|
-
return {
|
|
4128
|
-
id: acc.id || existing?.id || `${acc.addedAt}:${acc.refreshToken.slice(0, 12)}`,
|
|
4129
|
-
index,
|
|
4130
|
-
email: acc.email ?? existing?.email,
|
|
4131
|
-
refreshToken: acc.refreshToken,
|
|
4132
|
-
access: acc.access ?? existing?.access,
|
|
4133
|
-
expires: acc.expires ?? existing?.expires,
|
|
4134
|
-
tokenUpdatedAt: acc.token_updated_at ?? existing?.tokenUpdatedAt ?? acc.addedAt,
|
|
4135
|
-
addedAt: acc.addedAt,
|
|
4136
|
-
lastUsed: acc.lastUsed,
|
|
4137
|
-
enabled: acc.enabled,
|
|
4138
|
-
rateLimitResetTimes: acc.rateLimitResetTimes,
|
|
4139
|
-
consecutiveFailures: acc.consecutiveFailures,
|
|
4140
|
-
lastFailureTime: acc.lastFailureTime,
|
|
4141
|
-
lastSwitchReason: acc.lastSwitchReason || existing?.lastSwitchReason || "initial",
|
|
4142
|
-
stats: acc.stats ?? existing?.stats ?? createDefaultStats()
|
|
4143
|
-
};
|
|
4431
|
+
const matchedAccounts = /* @__PURE__ */ new Set();
|
|
4432
|
+
const reconciledAccounts = [];
|
|
4433
|
+
let structuralChange = false;
|
|
4434
|
+
for (const [index, storedAccount] of stored.accounts.entries()) {
|
|
4435
|
+
const existing = findMatchingAccount(this.#accounts, {
|
|
4436
|
+
id: storedAccount.id,
|
|
4437
|
+
identity: resolveIdentity(storedAccount),
|
|
4438
|
+
refreshToken: storedAccount.refreshToken
|
|
4144
4439
|
});
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
}
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4440
|
+
if (existing) {
|
|
4441
|
+
updateManagedAccountFromStorage(existing, storedAccount, index);
|
|
4442
|
+
matchedAccounts.add(existing);
|
|
4443
|
+
reconciledAccounts.push(existing);
|
|
4444
|
+
continue;
|
|
4445
|
+
}
|
|
4446
|
+
const addedAccount = createManagedAccount({
|
|
4447
|
+
id: storedAccount.id,
|
|
4448
|
+
index,
|
|
4449
|
+
email: storedAccount.email,
|
|
4450
|
+
identity: storedAccount.identity,
|
|
4451
|
+
label: storedAccount.label,
|
|
4452
|
+
refreshToken: storedAccount.refreshToken,
|
|
4453
|
+
access: storedAccount.access,
|
|
4454
|
+
expires: storedAccount.expires,
|
|
4455
|
+
tokenUpdatedAt: storedAccount.token_updated_at,
|
|
4456
|
+
addedAt: storedAccount.addedAt,
|
|
4457
|
+
lastUsed: storedAccount.lastUsed,
|
|
4458
|
+
enabled: storedAccount.enabled,
|
|
4459
|
+
rateLimitResetTimes: storedAccount.rateLimitResetTimes,
|
|
4460
|
+
consecutiveFailures: storedAccount.consecutiveFailures,
|
|
4461
|
+
lastFailureTime: storedAccount.lastFailureTime,
|
|
4462
|
+
lastSwitchReason: storedAccount.lastSwitchReason,
|
|
4463
|
+
stats: storedAccount.stats,
|
|
4464
|
+
source: storedAccount.source || "oauth"
|
|
4465
|
+
});
|
|
4466
|
+
matchedAccounts.add(addedAccount);
|
|
4467
|
+
reconciledAccounts.push(addedAccount);
|
|
4468
|
+
structuralChange = true;
|
|
4469
|
+
}
|
|
4470
|
+
for (const account of this.#accounts) {
|
|
4471
|
+
if (matchedAccounts.has(account)) {
|
|
4472
|
+
continue;
|
|
4473
|
+
}
|
|
4474
|
+
if (account.enabled) {
|
|
4475
|
+
account.enabled = false;
|
|
4476
|
+
structuralChange = true;
|
|
4155
4477
|
}
|
|
4478
|
+
reconciledAccounts.push(account);
|
|
4479
|
+
}
|
|
4480
|
+
const orderChanged = reconciledAccounts.length !== this.#accounts.length || reconciledAccounts.some((account, index) => this.#accounts[index] !== account);
|
|
4481
|
+
this.#accounts = reconciledAccounts;
|
|
4482
|
+
reindexAccounts(this.#accounts);
|
|
4483
|
+
if (orderChanged || structuralChange) {
|
|
4484
|
+
this.#rebuildTrackers();
|
|
4485
|
+
}
|
|
4486
|
+
const currentIds = new Set(this.#accounts.map((account) => account.id));
|
|
4487
|
+
for (const id of this.#statsDeltas.keys()) {
|
|
4488
|
+
if (!currentIds.has(id)) {
|
|
4489
|
+
this.#statsDeltas.delete(id);
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
const enabledAccounts = this.#accounts.filter((account) => account.enabled);
|
|
4493
|
+
if (enabledAccounts.length === 0) {
|
|
4494
|
+
this.#currentIndex = -1;
|
|
4495
|
+
this.#cursor = 0;
|
|
4496
|
+
return;
|
|
4156
4497
|
}
|
|
4157
|
-
const diskIndex = Math.min(stored.activeIndex,
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
if (!
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
this.#
|
|
4164
|
-
this.#cursor = diskIndex;
|
|
4165
|
-
this.#healthTracker.reset(diskIndex);
|
|
4498
|
+
const diskIndex = Math.min(stored.activeIndex, stored.accounts.length - 1);
|
|
4499
|
+
const diskAccount = diskIndex >= 0 ? stored.accounts[diskIndex] : void 0;
|
|
4500
|
+
if (!diskAccount || !diskAccount.enabled) {
|
|
4501
|
+
if (!this.#accounts[this.#currentIndex]?.enabled) {
|
|
4502
|
+
const fallback = enabledAccounts[0];
|
|
4503
|
+
this.#currentIndex = fallback.index;
|
|
4504
|
+
this.#cursor = fallback.index;
|
|
4166
4505
|
}
|
|
4506
|
+
return;
|
|
4507
|
+
}
|
|
4508
|
+
const activeAccount = findMatchingAccount(this.#accounts, {
|
|
4509
|
+
id: diskAccount.id,
|
|
4510
|
+
identity: resolveIdentity(diskAccount),
|
|
4511
|
+
refreshToken: diskAccount.refreshToken
|
|
4512
|
+
});
|
|
4513
|
+
if (activeAccount && activeAccount.enabled && activeAccount.index !== this.#currentIndex) {
|
|
4514
|
+
this.#currentIndex = activeAccount.index;
|
|
4515
|
+
this.#cursor = activeAccount.index;
|
|
4516
|
+
this.#healthTracker.reset(activeAccount.index);
|
|
4167
4517
|
}
|
|
4168
4518
|
}
|
|
4169
4519
|
/**
|
|
@@ -4189,6 +4539,13 @@ var AccountManager = class _AccountManager {
|
|
|
4189
4539
|
delta.cacheReadTokens += crTok;
|
|
4190
4540
|
delta.cacheWriteTokens += cwTok;
|
|
4191
4541
|
} else {
|
|
4542
|
+
if (this.#statsDeltas.size >= this.#MAX_STATS_DELTAS) {
|
|
4543
|
+
this.saveToDisk().catch((err) => {
|
|
4544
|
+
if (this.#config.debug) {
|
|
4545
|
+
console.error("[opencode-anthropic-auth] forced statsDeltas flush failed:", err.message);
|
|
4546
|
+
}
|
|
4547
|
+
});
|
|
4548
|
+
}
|
|
4192
4549
|
this.#statsDeltas.set(account.id, {
|
|
4193
4550
|
requests: 1,
|
|
4194
4551
|
inputTokens: inTok,
|
|
@@ -4721,6 +5078,14 @@ async function completeSlashOAuth(sessionID, code, deps) {
|
|
|
4721
5078
|
|
|
4722
5079
|
// src/commands/router.ts
|
|
4723
5080
|
var ANTHROPIC_COMMAND_HANDLED = "__ANTHROPIC_COMMAND_HANDLED__";
|
|
5081
|
+
var FILE_ACCOUNT_MAP_MAX_SIZE = 1e3;
|
|
5082
|
+
function capFileAccountMap(fileAccountMap, fileId, accountIndex) {
|
|
5083
|
+
if (fileAccountMap.size >= FILE_ACCOUNT_MAP_MAX_SIZE) {
|
|
5084
|
+
const oldestKey = fileAccountMap.keys().next().value;
|
|
5085
|
+
if (oldestKey !== void 0) fileAccountMap.delete(oldestKey);
|
|
5086
|
+
}
|
|
5087
|
+
fileAccountMap.set(fileId, accountIndex);
|
|
5088
|
+
}
|
|
4724
5089
|
function stripAnsi(value) {
|
|
4725
5090
|
return value.replace(/\x1b\[[0-9;]*m/g, "");
|
|
4726
5091
|
}
|
|
@@ -5069,7 +5434,7 @@ HTTP ${res.status}: ${errBody}`
|
|
|
5069
5434
|
}
|
|
5070
5435
|
const data = await res.json();
|
|
5071
5436
|
const files = data.data || [];
|
|
5072
|
-
for (const f of files) fileAccountMap
|
|
5437
|
+
for (const f of files) capFileAccountMap(fileAccountMap, f.id, account2.index);
|
|
5073
5438
|
if (files.length === 0) {
|
|
5074
5439
|
await sendCommandMessage(input.sessionID, `\u25A3 Anthropic Files [${label2}]
|
|
5075
5440
|
|
|
@@ -5099,7 +5464,7 @@ No files uploaded.`);
|
|
|
5099
5464
|
}
|
|
5100
5465
|
const data = await res.json();
|
|
5101
5466
|
const files = data.data || [];
|
|
5102
|
-
for (const f of files) fileAccountMap
|
|
5467
|
+
for (const f of files) capFileAccountMap(fileAccountMap, f.id, acct.index);
|
|
5103
5468
|
totalFiles += files.length;
|
|
5104
5469
|
if (files.length === 0) {
|
|
5105
5470
|
allLines.push(`[${label2}] No files`);
|
|
@@ -5179,7 +5544,7 @@ Upload failed (HTTP ${res.status}): ${errBody}`
|
|
|
5179
5544
|
}
|
|
5180
5545
|
const file = await res.json();
|
|
5181
5546
|
const sizeKB = ((file.size || 0) / 1024).toFixed(1);
|
|
5182
|
-
fileAccountMap
|
|
5547
|
+
capFileAccountMap(fileAccountMap, file.id, account.index);
|
|
5183
5548
|
await sendCommandMessage(
|
|
5184
5549
|
input.sessionID,
|
|
5185
5550
|
`\u25A3 Anthropic Files [${label}]
|
|
@@ -5211,7 +5576,7 @@ HTTP ${res.status}: ${errBody}`
|
|
|
5211
5576
|
return;
|
|
5212
5577
|
}
|
|
5213
5578
|
const file = await res.json();
|
|
5214
|
-
fileAccountMap
|
|
5579
|
+
capFileAccountMap(fileAccountMap, file.id, account.index);
|
|
5215
5580
|
const lines = [
|
|
5216
5581
|
`\u25A3 Anthropic Files [${label}]`,
|
|
5217
5582
|
"",
|
|
@@ -5558,17 +5923,28 @@ function buildAnthropicBillingHeader(claudeCliVersion, messages) {
|
|
|
5558
5923
|
let versionSuffix = "";
|
|
5559
5924
|
if (Array.isArray(messages)) {
|
|
5560
5925
|
const firstUserMsg = messages.find(
|
|
5561
|
-
(m) => m !== null && typeof m === "object" && m.role === "user"
|
|
5926
|
+
(m) => m !== null && typeof m === "object" && m.role === "user"
|
|
5562
5927
|
);
|
|
5563
5928
|
if (firstUserMsg) {
|
|
5564
|
-
|
|
5565
|
-
const
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5929
|
+
let text = "";
|
|
5930
|
+
const content = firstUserMsg.content;
|
|
5931
|
+
if (typeof content === "string") {
|
|
5932
|
+
text = content;
|
|
5933
|
+
} else if (Array.isArray(content)) {
|
|
5934
|
+
const textBlock = content.find((b) => b.type === "text");
|
|
5935
|
+
if (textBlock && typeof textBlock.text === "string") {
|
|
5936
|
+
text = textBlock.text;
|
|
5937
|
+
}
|
|
5938
|
+
}
|
|
5939
|
+
if (text) {
|
|
5940
|
+
const salt = "59cf53e54c78";
|
|
5941
|
+
const picked = [4, 7, 20].map((i) => i < text.length ? text[i] : "0").join("");
|
|
5942
|
+
const hash = createHash2("sha256").update(salt + picked + claudeCliVersion).digest("hex");
|
|
5943
|
+
versionSuffix = `.${hash.slice(0, 3)}`;
|
|
5944
|
+
}
|
|
5569
5945
|
}
|
|
5570
5946
|
}
|
|
5571
|
-
const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT
|
|
5947
|
+
const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT ?? "cli";
|
|
5572
5948
|
let cchValue;
|
|
5573
5949
|
if (Array.isArray(messages) && messages.length > 0) {
|
|
5574
5950
|
const bodyHint = JSON.stringify(messages).slice(0, 512);
|
|
@@ -5638,8 +6014,9 @@ function normalizeSystemTextBlocks(system) {
|
|
|
5638
6014
|
}
|
|
5639
6015
|
|
|
5640
6016
|
// src/system-prompt/sanitize.ts
|
|
5641
|
-
function sanitizeSystemText(text) {
|
|
5642
|
-
|
|
6017
|
+
function sanitizeSystemText(text, enabled = true) {
|
|
6018
|
+
if (!enabled) return text;
|
|
6019
|
+
return text.replace(/\bOpenCode\b/g, "Claude Code").replace(/\bopencode\b/gi, "Claude").replace(/OhMyClaude\s*Code/gi, "Claude Code").replace(/OhMyClaudeCode/gi, "Claude Code").replace(/\bSisyphus\b/g, "Claude Code Agent").replace(/\bMorph\s+plugin\b/gi, "edit plugin").replace(/\bmorph_edit\b/g, "edit").replace(/\bmorph_/g, "").replace(/\bOhMyClaude\b/gi, "Claude");
|
|
5643
6020
|
}
|
|
5644
6021
|
function compactSystemText(text, mode) {
|
|
5645
6022
|
const withoutDuplicateIdentityPrefix = text.startsWith(`${CLAUDE_CODE_IDENTITY_STRING}
|
|
@@ -5772,11 +6149,69 @@ function buildRequestMetadata(input) {
|
|
|
5772
6149
|
}
|
|
5773
6150
|
|
|
5774
6151
|
// src/request/body.ts
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
6152
|
+
var TOOL_PREFIX = "mcp_";
|
|
6153
|
+
function getBodyType(body) {
|
|
6154
|
+
if (body === null) return "null";
|
|
6155
|
+
return typeof body;
|
|
6156
|
+
}
|
|
6157
|
+
function getInvalidBodyError(body) {
|
|
6158
|
+
return new TypeError(
|
|
6159
|
+
`opencode-anthropic-auth: expected string body, got ${getBodyType(body)}. This plugin does not support stream bodies. Please file a bug with the OpenCode version.`
|
|
6160
|
+
);
|
|
6161
|
+
}
|
|
6162
|
+
function validateBodyType(body, throwOnInvalid = false) {
|
|
6163
|
+
if (body === void 0 || body === null) {
|
|
6164
|
+
return false;
|
|
6165
|
+
}
|
|
6166
|
+
if (typeof body === "string") {
|
|
6167
|
+
return true;
|
|
6168
|
+
}
|
|
6169
|
+
if (throwOnInvalid) {
|
|
6170
|
+
throw getInvalidBodyError(body);
|
|
6171
|
+
}
|
|
6172
|
+
return false;
|
|
6173
|
+
}
|
|
6174
|
+
function cloneBodyForRetry(body) {
|
|
6175
|
+
validateBodyType(body, true);
|
|
6176
|
+
return body;
|
|
6177
|
+
}
|
|
6178
|
+
function detectDoublePrefix(name) {
|
|
6179
|
+
return name.startsWith(`${TOOL_PREFIX}${TOOL_PREFIX}`);
|
|
6180
|
+
}
|
|
6181
|
+
function prefixToolDefinitionName(name) {
|
|
6182
|
+
if (typeof name !== "string") {
|
|
6183
|
+
return name;
|
|
6184
|
+
}
|
|
6185
|
+
if (detectDoublePrefix(name)) {
|
|
6186
|
+
throw new TypeError(`Double tool prefix detected: ${TOOL_PREFIX}${TOOL_PREFIX}`);
|
|
6187
|
+
}
|
|
6188
|
+
return `${TOOL_PREFIX}${name}`;
|
|
6189
|
+
}
|
|
6190
|
+
function prefixToolUseName(name, literalToolNames, debugLog) {
|
|
6191
|
+
if (typeof name !== "string") {
|
|
6192
|
+
return name;
|
|
6193
|
+
}
|
|
6194
|
+
if (detectDoublePrefix(name)) {
|
|
6195
|
+
throw new TypeError(`Double tool prefix detected in tool_use block: ${name}`);
|
|
6196
|
+
}
|
|
6197
|
+
if (!name.startsWith(TOOL_PREFIX)) {
|
|
6198
|
+
return `${TOOL_PREFIX}${name}`;
|
|
6199
|
+
}
|
|
6200
|
+
if (literalToolNames.has(name)) {
|
|
6201
|
+
return `${TOOL_PREFIX}${name}`;
|
|
6202
|
+
}
|
|
6203
|
+
debugLog?.("prevented double-prefix drift for tool_use block", { name });
|
|
6204
|
+
return name;
|
|
6205
|
+
}
|
|
6206
|
+
function transformRequestBody(body, signature, runtime, relocateThirdPartyPrompts = true, debugLog) {
|
|
6207
|
+
if (body === void 0 || body === null) return body;
|
|
6208
|
+
validateBodyType(body, true);
|
|
5778
6209
|
try {
|
|
5779
6210
|
const parsed = JSON.parse(body);
|
|
6211
|
+
const parsedMessages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
6212
|
+
const literalToolNames = new Set(
|
|
6213
|
+
Array.isArray(parsed.tools) ? parsed.tools.map((tool) => tool.name).filter((name) => typeof name === "string") : []
|
|
6214
|
+
);
|
|
5780
6215
|
if (Object.hasOwn(parsed, "betas")) {
|
|
5781
6216
|
delete parsed.betas;
|
|
5782
6217
|
}
|
|
@@ -5789,7 +6224,50 @@ function transformRequestBody(body, signature, runtime) {
|
|
|
5789
6224
|
} else if (!Object.hasOwn(parsed, "temperature")) {
|
|
5790
6225
|
parsed.temperature = 1;
|
|
5791
6226
|
}
|
|
5792
|
-
|
|
6227
|
+
const allSystemBlocks = buildSystemPromptBlocks(
|
|
6228
|
+
normalizeSystemTextBlocks(parsed.system),
|
|
6229
|
+
signature,
|
|
6230
|
+
parsedMessages
|
|
6231
|
+
);
|
|
6232
|
+
if (signature.enabled && relocateThirdPartyPrompts) {
|
|
6233
|
+
const THIRD_PARTY_MARKERS = /sisyphus|ohmyclaude|oh\s*my\s*claude|morph[_ ]|\.sisyphus\/|ultrawork|autopilot mode|\bohmy\b|SwarmMode|\bomc\b|\bomo\b/i;
|
|
6234
|
+
const ccBlocks = [];
|
|
6235
|
+
const extraBlocks = [];
|
|
6236
|
+
for (const block of allSystemBlocks) {
|
|
6237
|
+
const isBilling = block.text.startsWith("x-anthropic-billing-header:");
|
|
6238
|
+
const isIdentity = block.text === CLAUDE_CODE_IDENTITY_STRING || KNOWN_IDENTITY_STRINGS.has(block.text);
|
|
6239
|
+
const hasThirdParty = THIRD_PARTY_MARKERS.test(block.text);
|
|
6240
|
+
if (isBilling || isIdentity || !hasThirdParty) {
|
|
6241
|
+
ccBlocks.push(block);
|
|
6242
|
+
} else {
|
|
6243
|
+
extraBlocks.push(block);
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
parsed.system = ccBlocks;
|
|
6247
|
+
if (extraBlocks.length > 0 && Array.isArray(parsed.messages) && parsed.messages.length > 0) {
|
|
6248
|
+
const extraText = extraBlocks.map((b) => b.text).join("\n\n");
|
|
6249
|
+
const wrapped = `<system-instructions>
|
|
6250
|
+
${extraText}
|
|
6251
|
+
</system-instructions>`;
|
|
6252
|
+
const firstMsg = parsed.messages[0];
|
|
6253
|
+
if (firstMsg && firstMsg.role === "user") {
|
|
6254
|
+
if (typeof firstMsg.content === "string") {
|
|
6255
|
+
firstMsg.content = `${wrapped}
|
|
6256
|
+
|
|
6257
|
+
${firstMsg.content}`;
|
|
6258
|
+
} else if (Array.isArray(firstMsg.content)) {
|
|
6259
|
+
firstMsg.content.unshift({ type: "text", text: wrapped });
|
|
6260
|
+
}
|
|
6261
|
+
} else {
|
|
6262
|
+
parsed.messages.unshift({
|
|
6263
|
+
role: "user",
|
|
6264
|
+
content: wrapped
|
|
6265
|
+
});
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
} else {
|
|
6269
|
+
parsed.system = allSystemBlocks;
|
|
6270
|
+
}
|
|
5793
6271
|
if (signature.enabled) {
|
|
5794
6272
|
const currentMetadata = parsed.metadata && typeof parsed.metadata === "object" && !Array.isArray(parsed.metadata) ? parsed.metadata : {};
|
|
5795
6273
|
parsed.metadata = {
|
|
@@ -5804,7 +6282,7 @@ function transformRequestBody(body, signature, runtime) {
|
|
|
5804
6282
|
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
5805
6283
|
parsed.tools = parsed.tools.map((tool) => ({
|
|
5806
6284
|
...tool,
|
|
5807
|
-
name: tool.name
|
|
6285
|
+
name: prefixToolDefinitionName(tool.name)
|
|
5808
6286
|
}));
|
|
5809
6287
|
}
|
|
5810
6288
|
if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
@@ -5814,7 +6292,7 @@ function transformRequestBody(body, signature, runtime) {
|
|
|
5814
6292
|
if (block.type === "tool_use" && block.name) {
|
|
5815
6293
|
return {
|
|
5816
6294
|
...block,
|
|
5817
|
-
name:
|
|
6295
|
+
name: prefixToolUseName(block.name, literalToolNames, debugLog)
|
|
5818
6296
|
};
|
|
5819
6297
|
}
|
|
5820
6298
|
return block;
|
|
@@ -5824,8 +6302,12 @@ function transformRequestBody(body, signature, runtime) {
|
|
|
5824
6302
|
});
|
|
5825
6303
|
}
|
|
5826
6304
|
return JSON.stringify(parsed);
|
|
5827
|
-
} catch {
|
|
5828
|
-
|
|
6305
|
+
} catch (err) {
|
|
6306
|
+
if (err instanceof SyntaxError) {
|
|
6307
|
+
debugLog?.("body parse failed:", err.message);
|
|
6308
|
+
return body;
|
|
6309
|
+
}
|
|
6310
|
+
throw err;
|
|
5829
6311
|
}
|
|
5830
6312
|
}
|
|
5831
6313
|
|
|
@@ -5887,50 +6369,71 @@ function transformRequestUrl(input) {
|
|
|
5887
6369
|
}
|
|
5888
6370
|
|
|
5889
6371
|
// src/response/mcp.ts
|
|
5890
|
-
function
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
6372
|
+
function stripMcpPrefixFromToolUseBlock(block) {
|
|
6373
|
+
if (!block || typeof block !== "object") return false;
|
|
6374
|
+
const parsedBlock = block;
|
|
6375
|
+
if (parsedBlock.type !== "tool_use" || typeof parsedBlock.name !== "string") {
|
|
6376
|
+
return false;
|
|
6377
|
+
}
|
|
6378
|
+
if (!parsedBlock.name.startsWith("mcp_")) {
|
|
6379
|
+
return false;
|
|
6380
|
+
}
|
|
6381
|
+
parsedBlock.name = parsedBlock.name.slice(4);
|
|
6382
|
+
return true;
|
|
6383
|
+
}
|
|
6384
|
+
function stripMcpPrefixFromContentBlocks(content) {
|
|
6385
|
+
if (!Array.isArray(content)) return false;
|
|
6386
|
+
let modified = false;
|
|
6387
|
+
for (const block of content) {
|
|
6388
|
+
modified = stripMcpPrefixFromToolUseBlock(block) || modified;
|
|
6389
|
+
}
|
|
6390
|
+
return modified;
|
|
6391
|
+
}
|
|
6392
|
+
function stripMcpPrefixFromMessages(messages) {
|
|
6393
|
+
if (!Array.isArray(messages)) return false;
|
|
6394
|
+
let modified = false;
|
|
6395
|
+
for (const message of messages) {
|
|
6396
|
+
if (!message || typeof message !== "object") continue;
|
|
6397
|
+
modified = stripMcpPrefixFromContentBlocks(message.content) || modified;
|
|
6398
|
+
}
|
|
6399
|
+
return modified;
|
|
5901
6400
|
}
|
|
5902
6401
|
function stripMcpPrefixFromParsedEvent(parsed) {
|
|
5903
6402
|
if (!parsed || typeof parsed !== "object") return false;
|
|
5904
6403
|
const p2 = parsed;
|
|
5905
6404
|
let modified = false;
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
modified =
|
|
6405
|
+
modified = stripMcpPrefixFromToolUseBlock(p2.content_block) || modified;
|
|
6406
|
+
if (p2.message && typeof p2.message === "object") {
|
|
6407
|
+
modified = stripMcpPrefixFromContentBlocks(p2.message.content) || modified;
|
|
5909
6408
|
}
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
for (const block of p2.content) {
|
|
5922
|
-
if (!block || typeof block !== "object") continue;
|
|
5923
|
-
const b = block;
|
|
5924
|
-
if (b.type === "tool_use" && typeof b.name === "string" && b.name.startsWith("mcp_")) {
|
|
5925
|
-
b.name = b.name.slice(4);
|
|
5926
|
-
modified = true;
|
|
5927
|
-
}
|
|
6409
|
+
modified = stripMcpPrefixFromContentBlocks(p2.content) || modified;
|
|
6410
|
+
return modified;
|
|
6411
|
+
}
|
|
6412
|
+
function stripMcpPrefixFromJsonBody(body) {
|
|
6413
|
+
try {
|
|
6414
|
+
const parsed = JSON.parse(body);
|
|
6415
|
+
let modified = false;
|
|
6416
|
+
modified = stripMcpPrefixFromContentBlocks(parsed.content) || modified;
|
|
6417
|
+
modified = stripMcpPrefixFromMessages(parsed.messages) || modified;
|
|
6418
|
+
if (parsed.message && typeof parsed.message === "object") {
|
|
6419
|
+
modified = stripMcpPrefixFromContentBlocks(parsed.message.content) || modified;
|
|
5928
6420
|
}
|
|
6421
|
+
return modified ? JSON.stringify(parsed) : body;
|
|
6422
|
+
} catch {
|
|
6423
|
+
return body;
|
|
5929
6424
|
}
|
|
5930
|
-
return modified;
|
|
5931
6425
|
}
|
|
5932
6426
|
|
|
5933
6427
|
// src/response/streaming.ts
|
|
6428
|
+
var MAX_UNTERMINATED_SSE_BUFFER = 256 * 1024;
|
|
6429
|
+
var StreamTruncatedError = class extends Error {
|
|
6430
|
+
context;
|
|
6431
|
+
constructor(message, context = {}) {
|
|
6432
|
+
super(message);
|
|
6433
|
+
this.name = "StreamTruncatedError";
|
|
6434
|
+
this.context = context;
|
|
6435
|
+
}
|
|
6436
|
+
};
|
|
5934
6437
|
function extractUsageFromSSEEvent(parsed, stats) {
|
|
5935
6438
|
const p2 = parsed;
|
|
5936
6439
|
if (!p2) return;
|
|
@@ -5970,6 +6473,187 @@ function getSSEDataPayload(eventBlock) {
|
|
|
5970
6473
|
if (!payload || payload === "[DONE]") return null;
|
|
5971
6474
|
return payload;
|
|
5972
6475
|
}
|
|
6476
|
+
function getSSEEventType(eventBlock) {
|
|
6477
|
+
for (const line of eventBlock.split("\n")) {
|
|
6478
|
+
if (!line.startsWith("event:")) continue;
|
|
6479
|
+
const eventType = line.slice(6).trimStart();
|
|
6480
|
+
if (eventType) return eventType;
|
|
6481
|
+
}
|
|
6482
|
+
return null;
|
|
6483
|
+
}
|
|
6484
|
+
function formatSSEEventBlock(eventType, parsed, prettyPrint) {
|
|
6485
|
+
const json = prettyPrint ? JSON.stringify(parsed, null, 2) : JSON.stringify(parsed);
|
|
6486
|
+
const lines = [`event: ${eventType}`];
|
|
6487
|
+
for (const line of json.split("\n")) {
|
|
6488
|
+
lines.push(`data: ${line}`);
|
|
6489
|
+
}
|
|
6490
|
+
lines.push("", "");
|
|
6491
|
+
return lines.join("\n");
|
|
6492
|
+
}
|
|
6493
|
+
function hasRecordedUsage(stats) {
|
|
6494
|
+
return stats.inputTokens > 0 || stats.outputTokens > 0 || stats.cacheReadTokens > 0 || stats.cacheWriteTokens > 0;
|
|
6495
|
+
}
|
|
6496
|
+
function getErrorMessage(parsed) {
|
|
6497
|
+
if (!parsed || typeof parsed !== "object") {
|
|
6498
|
+
return "stream terminated with error event";
|
|
6499
|
+
}
|
|
6500
|
+
const error = parsed.error;
|
|
6501
|
+
if (!error || typeof error !== "object") {
|
|
6502
|
+
return "stream terminated with error event";
|
|
6503
|
+
}
|
|
6504
|
+
const message = error.message;
|
|
6505
|
+
return typeof message === "string" && message ? message : "stream terminated with error event";
|
|
6506
|
+
}
|
|
6507
|
+
function getEventIndex(parsed, eventType) {
|
|
6508
|
+
const index = parsed.index;
|
|
6509
|
+
if (typeof index !== "number") {
|
|
6510
|
+
throw new Error(`invalid SSE ${eventType} event: missing numeric index`);
|
|
6511
|
+
}
|
|
6512
|
+
return index;
|
|
6513
|
+
}
|
|
6514
|
+
function getEventLabel(parsed, eventType) {
|
|
6515
|
+
switch (eventType) {
|
|
6516
|
+
case "content_block_start": {
|
|
6517
|
+
const contentBlock = parsed.content_block;
|
|
6518
|
+
const blockType = contentBlock && typeof contentBlock === "object" ? contentBlock.type : void 0;
|
|
6519
|
+
return typeof blockType === "string" && blockType ? `content_block_start(${blockType})` : eventType;
|
|
6520
|
+
}
|
|
6521
|
+
case "content_block_delta": {
|
|
6522
|
+
const delta = parsed.delta;
|
|
6523
|
+
const deltaType = delta && typeof delta === "object" ? delta.type : void 0;
|
|
6524
|
+
return typeof deltaType === "string" && deltaType ? `content_block_delta(${deltaType})` : eventType;
|
|
6525
|
+
}
|
|
6526
|
+
default:
|
|
6527
|
+
return eventType;
|
|
6528
|
+
}
|
|
6529
|
+
}
|
|
6530
|
+
function getOpenBlockContext(openContentBlocks) {
|
|
6531
|
+
for (const [index2, blockState2] of openContentBlocks) {
|
|
6532
|
+
if (blockState2.type === "tool_use") {
|
|
6533
|
+
return {
|
|
6534
|
+
inFlightEvent: blockState2.partialJson ? "content_block_delta(input_json_delta)" : "content_block_start(tool_use)",
|
|
6535
|
+
openContentBlockIndex: index2,
|
|
6536
|
+
hasPartialJson: blockState2.partialJson.length > 0
|
|
6537
|
+
};
|
|
6538
|
+
}
|
|
6539
|
+
}
|
|
6540
|
+
const firstOpenBlock = openContentBlocks.entries().next().value;
|
|
6541
|
+
if (!firstOpenBlock) {
|
|
6542
|
+
return null;
|
|
6543
|
+
}
|
|
6544
|
+
const [index, blockState] = firstOpenBlock;
|
|
6545
|
+
return {
|
|
6546
|
+
inFlightEvent: `content_block_start(${blockState.type})`,
|
|
6547
|
+
openContentBlockIndex: index,
|
|
6548
|
+
hasPartialJson: blockState.partialJson.length > 0
|
|
6549
|
+
};
|
|
6550
|
+
}
|
|
6551
|
+
function createStreamTruncatedError(context = {}) {
|
|
6552
|
+
return new StreamTruncatedError("Stream truncated without message_stop", context);
|
|
6553
|
+
}
|
|
6554
|
+
function getBufferedEventContext(eventBlock, lastEventType) {
|
|
6555
|
+
const context = {
|
|
6556
|
+
lastEventType: lastEventType ?? void 0
|
|
6557
|
+
};
|
|
6558
|
+
const payload = getSSEDataPayload(eventBlock);
|
|
6559
|
+
if (!payload) {
|
|
6560
|
+
return context;
|
|
6561
|
+
}
|
|
6562
|
+
try {
|
|
6563
|
+
const parsed = JSON.parse(payload);
|
|
6564
|
+
const eventType = getSSEEventType(eventBlock) ?? (typeof parsed.type === "string" ? parsed.type : null);
|
|
6565
|
+
if (eventType) {
|
|
6566
|
+
context.inFlightEvent = getEventLabel(parsed, eventType);
|
|
6567
|
+
}
|
|
6568
|
+
} catch {
|
|
6569
|
+
}
|
|
6570
|
+
return context;
|
|
6571
|
+
}
|
|
6572
|
+
function validateEventState(parsed, eventType, openContentBlocks) {
|
|
6573
|
+
switch (eventType) {
|
|
6574
|
+
case "content_block_start": {
|
|
6575
|
+
const index = getEventIndex(parsed, eventType);
|
|
6576
|
+
const contentBlock = parsed.content_block;
|
|
6577
|
+
if (!contentBlock || typeof contentBlock !== "object") {
|
|
6578
|
+
throw new Error("invalid SSE content_block_start event: missing content_block");
|
|
6579
|
+
}
|
|
6580
|
+
if (openContentBlocks.has(index)) {
|
|
6581
|
+
throw new Error(`duplicate content_block_start for index ${index}`);
|
|
6582
|
+
}
|
|
6583
|
+
const blockType = contentBlock.type;
|
|
6584
|
+
if (typeof blockType !== "string" || !blockType) {
|
|
6585
|
+
throw new Error("invalid SSE content_block_start event: missing content_block.type");
|
|
6586
|
+
}
|
|
6587
|
+
openContentBlocks.set(index, {
|
|
6588
|
+
type: blockType,
|
|
6589
|
+
partialJson: ""
|
|
6590
|
+
});
|
|
6591
|
+
return;
|
|
6592
|
+
}
|
|
6593
|
+
case "content_block_delta": {
|
|
6594
|
+
const index = getEventIndex(parsed, eventType);
|
|
6595
|
+
const blockState = openContentBlocks.get(index);
|
|
6596
|
+
if (!blockState) {
|
|
6597
|
+
throw new Error(`orphan content_block_delta for index ${index}`);
|
|
6598
|
+
}
|
|
6599
|
+
const delta = parsed.delta;
|
|
6600
|
+
if (!delta || typeof delta !== "object") {
|
|
6601
|
+
throw new Error("invalid SSE content_block_delta event: missing delta");
|
|
6602
|
+
}
|
|
6603
|
+
const deltaType = delta.type;
|
|
6604
|
+
if (deltaType === "input_json_delta") {
|
|
6605
|
+
if (blockState.type !== "tool_use") {
|
|
6606
|
+
throw new Error(`orphan input_json_delta for non-tool_use block ${index}`);
|
|
6607
|
+
}
|
|
6608
|
+
const partialJson = delta.partial_json;
|
|
6609
|
+
if (typeof partialJson !== "string") {
|
|
6610
|
+
throw new Error("invalid SSE content_block_delta event: missing delta.partial_json");
|
|
6611
|
+
}
|
|
6612
|
+
blockState.partialJson += partialJson;
|
|
6613
|
+
}
|
|
6614
|
+
return;
|
|
6615
|
+
}
|
|
6616
|
+
case "content_block_stop": {
|
|
6617
|
+
const index = getEventIndex(parsed, eventType);
|
|
6618
|
+
const blockState = openContentBlocks.get(index);
|
|
6619
|
+
if (!blockState) {
|
|
6620
|
+
throw new Error(`orphan content_block_stop for index ${index}`);
|
|
6621
|
+
}
|
|
6622
|
+
if (blockState.type === "tool_use" && blockState.partialJson) {
|
|
6623
|
+
try {
|
|
6624
|
+
JSON.parse(blockState.partialJson);
|
|
6625
|
+
} catch {
|
|
6626
|
+
throw new Error(`incomplete tool_use partial_json for index ${index}`);
|
|
6627
|
+
}
|
|
6628
|
+
}
|
|
6629
|
+
openContentBlocks.delete(index);
|
|
6630
|
+
return;
|
|
6631
|
+
}
|
|
6632
|
+
default:
|
|
6633
|
+
return;
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
function getOpenBlockError(openContentBlocks) {
|
|
6637
|
+
const openBlockContext = getOpenBlockContext(openContentBlocks);
|
|
6638
|
+
return openBlockContext ? createStreamTruncatedError(openBlockContext) : null;
|
|
6639
|
+
}
|
|
6640
|
+
function getMessageStopBlockError(openContentBlocks) {
|
|
6641
|
+
for (const [index, blockState] of openContentBlocks) {
|
|
6642
|
+
if (blockState.partialJson) {
|
|
6643
|
+
return new Error(`incomplete tool_use partial_json for index ${index}`);
|
|
6644
|
+
}
|
|
6645
|
+
}
|
|
6646
|
+
return null;
|
|
6647
|
+
}
|
|
6648
|
+
function normalizeChunk(text) {
|
|
6649
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
6650
|
+
}
|
|
6651
|
+
function toStreamError(error) {
|
|
6652
|
+
if (error instanceof Error) {
|
|
6653
|
+
return error;
|
|
6654
|
+
}
|
|
6655
|
+
return new Error(String(error));
|
|
6656
|
+
}
|
|
5973
6657
|
function getMidStreamAccountError(parsed) {
|
|
5974
6658
|
const p2 = parsed;
|
|
5975
6659
|
if (!p2 || p2.type !== "error" || !p2.error) {
|
|
@@ -5991,12 +6675,11 @@ function getMidStreamAccountError(parsed) {
|
|
|
5991
6675
|
invalidateToken: reason === "AUTH_FAILED"
|
|
5992
6676
|
};
|
|
5993
6677
|
}
|
|
5994
|
-
function transformResponse(response, onUsage, onAccountError) {
|
|
5995
|
-
if (!response.body) return response;
|
|
6678
|
+
function transformResponse(response, onUsage, onAccountError, onStreamError) {
|
|
6679
|
+
if (!response.body || !isEventStreamResponse(response)) return response;
|
|
5996
6680
|
const reader = response.body.getReader();
|
|
5997
|
-
const decoder = new TextDecoder();
|
|
6681
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
5998
6682
|
const encoder = new TextEncoder();
|
|
5999
|
-
const EMPTY_CHUNK = new Uint8Array();
|
|
6000
6683
|
const stats = {
|
|
6001
6684
|
inputTokens: 0,
|
|
6002
6685
|
outputTokens: 0,
|
|
@@ -6004,82 +6687,145 @@ function transformResponse(response, onUsage, onAccountError) {
|
|
|
6004
6687
|
cacheWriteTokens: 0
|
|
6005
6688
|
};
|
|
6006
6689
|
let sseBuffer = "";
|
|
6007
|
-
let sseRewriteBuffer = "";
|
|
6008
6690
|
let accountErrorHandled = false;
|
|
6009
|
-
|
|
6691
|
+
let hasSeenMessageStop = false;
|
|
6692
|
+
let hasSeenError = false;
|
|
6693
|
+
let lastEventType = null;
|
|
6694
|
+
const strictEventValidation = !onUsage && !onAccountError;
|
|
6695
|
+
const openContentBlocks = /* @__PURE__ */ new Map();
|
|
6696
|
+
function enqueueNormalizedEvent(controller, eventBlock) {
|
|
6697
|
+
const payload = getSSEDataPayload(eventBlock);
|
|
6698
|
+
if (!payload) {
|
|
6699
|
+
return;
|
|
6700
|
+
}
|
|
6701
|
+
let parsed;
|
|
6702
|
+
try {
|
|
6703
|
+
parsed = JSON.parse(payload);
|
|
6704
|
+
} catch {
|
|
6705
|
+
throw new Error("invalid SSE event: malformed JSON payload");
|
|
6706
|
+
}
|
|
6707
|
+
const eventType = getSSEEventType(eventBlock) ?? parsed?.type;
|
|
6708
|
+
if (typeof eventType !== "string" || !eventType) {
|
|
6709
|
+
throw new Error("invalid SSE event: missing event type");
|
|
6710
|
+
}
|
|
6711
|
+
const parsedRecord = parsed;
|
|
6712
|
+
lastEventType = getEventLabel(parsedRecord, eventType);
|
|
6713
|
+
if (strictEventValidation) {
|
|
6714
|
+
validateEventState(parsedRecord, eventType, openContentBlocks);
|
|
6715
|
+
}
|
|
6716
|
+
stripMcpPrefixFromParsedEvent(parsedRecord);
|
|
6717
|
+
if (onUsage) {
|
|
6718
|
+
extractUsageFromSSEEvent(parsedRecord, stats);
|
|
6719
|
+
}
|
|
6720
|
+
if (onAccountError && !accountErrorHandled) {
|
|
6721
|
+
const details = getMidStreamAccountError(parsedRecord);
|
|
6722
|
+
if (details) {
|
|
6723
|
+
accountErrorHandled = true;
|
|
6724
|
+
onAccountError(details);
|
|
6725
|
+
}
|
|
6726
|
+
}
|
|
6727
|
+
if (eventType === "message_stop") {
|
|
6728
|
+
if (strictEventValidation) {
|
|
6729
|
+
const openBlockError = getMessageStopBlockError(openContentBlocks);
|
|
6730
|
+
if (openBlockError) {
|
|
6731
|
+
throw openBlockError;
|
|
6732
|
+
}
|
|
6733
|
+
openContentBlocks.clear();
|
|
6734
|
+
}
|
|
6735
|
+
hasSeenMessageStop = true;
|
|
6736
|
+
}
|
|
6737
|
+
if (eventType === "error") {
|
|
6738
|
+
hasSeenError = true;
|
|
6739
|
+
}
|
|
6740
|
+
controller.enqueue(encoder.encode(formatSSEEventBlock(eventType, parsedRecord, strictEventValidation)));
|
|
6741
|
+
if (eventType === "error" && strictEventValidation) {
|
|
6742
|
+
throw new Error(getErrorMessage(parsedRecord));
|
|
6743
|
+
}
|
|
6744
|
+
}
|
|
6745
|
+
function processBufferedEvents(controller) {
|
|
6746
|
+
let emitted = false;
|
|
6010
6747
|
while (true) {
|
|
6011
6748
|
const boundary = sseBuffer.indexOf("\n\n");
|
|
6012
6749
|
if (boundary === -1) {
|
|
6013
|
-
if (
|
|
6014
|
-
|
|
6015
|
-
sseBuffer = "";
|
|
6016
|
-
return;
|
|
6750
|
+
if (sseBuffer.length > MAX_UNTERMINATED_SSE_BUFFER) {
|
|
6751
|
+
throw new Error("unterminated SSE event buffer exceeded limit");
|
|
6017
6752
|
}
|
|
6753
|
+
return emitted;
|
|
6018
6754
|
}
|
|
6019
|
-
const eventBlock =
|
|
6020
|
-
sseBuffer =
|
|
6021
|
-
|
|
6022
|
-
if (!payload) {
|
|
6023
|
-
if (boundary === -1) return;
|
|
6755
|
+
const eventBlock = sseBuffer.slice(0, boundary);
|
|
6756
|
+
sseBuffer = sseBuffer.slice(boundary + 2);
|
|
6757
|
+
if (!eventBlock.trim()) {
|
|
6024
6758
|
continue;
|
|
6025
6759
|
}
|
|
6760
|
+
enqueueNormalizedEvent(controller, eventBlock);
|
|
6761
|
+
emitted = true;
|
|
6762
|
+
}
|
|
6763
|
+
}
|
|
6764
|
+
async function failStream(controller, error) {
|
|
6765
|
+
const streamError = toStreamError(error);
|
|
6766
|
+
if (onStreamError) {
|
|
6026
6767
|
try {
|
|
6027
|
-
|
|
6028
|
-
if (onUsage) {
|
|
6029
|
-
extractUsageFromSSEEvent(parsed, stats);
|
|
6030
|
-
}
|
|
6031
|
-
if (onAccountError && !accountErrorHandled) {
|
|
6032
|
-
const details = getMidStreamAccountError(parsed);
|
|
6033
|
-
if (details) {
|
|
6034
|
-
accountErrorHandled = true;
|
|
6035
|
-
onAccountError(details);
|
|
6036
|
-
}
|
|
6037
|
-
}
|
|
6768
|
+
onStreamError(streamError);
|
|
6038
6769
|
} catch {
|
|
6039
6770
|
}
|
|
6040
|
-
if (boundary === -1) return;
|
|
6041
6771
|
}
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
if (!flush) {
|
|
6046
|
-
const boundary = sseRewriteBuffer.lastIndexOf("\n");
|
|
6047
|
-
if (boundary === -1) return "";
|
|
6048
|
-
const complete = sseRewriteBuffer.slice(0, boundary + 1);
|
|
6049
|
-
sseRewriteBuffer = sseRewriteBuffer.slice(boundary + 1);
|
|
6050
|
-
return stripMcpPrefixFromSSE(complete);
|
|
6772
|
+
try {
|
|
6773
|
+
await reader.cancel(streamError);
|
|
6774
|
+
} catch {
|
|
6051
6775
|
}
|
|
6052
|
-
|
|
6053
|
-
const finalText = stripMcpPrefixFromSSE(sseRewriteBuffer);
|
|
6054
|
-
sseRewriteBuffer = "";
|
|
6055
|
-
return finalText;
|
|
6776
|
+
controller.error(streamError);
|
|
6056
6777
|
}
|
|
6057
6778
|
const stream = new ReadableStream({
|
|
6058
6779
|
async pull(controller) {
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6780
|
+
try {
|
|
6781
|
+
while (true) {
|
|
6782
|
+
const { done, value } = await reader.read();
|
|
6783
|
+
if (done) {
|
|
6784
|
+
const flushedText = decoder.decode();
|
|
6785
|
+
if (flushedText) {
|
|
6786
|
+
sseBuffer += normalizeChunk(flushedText);
|
|
6787
|
+
processBufferedEvents(controller);
|
|
6788
|
+
}
|
|
6789
|
+
if (sseBuffer.trim()) {
|
|
6790
|
+
if (strictEventValidation) {
|
|
6791
|
+
throw createStreamTruncatedError(getBufferedEventContext(sseBuffer, lastEventType));
|
|
6792
|
+
}
|
|
6793
|
+
enqueueNormalizedEvent(controller, sseBuffer);
|
|
6794
|
+
sseBuffer = "";
|
|
6795
|
+
}
|
|
6796
|
+
if (strictEventValidation) {
|
|
6797
|
+
const openBlockError = getOpenBlockError(openContentBlocks);
|
|
6798
|
+
if (openBlockError) {
|
|
6799
|
+
throw openBlockError;
|
|
6800
|
+
}
|
|
6801
|
+
if (!hasSeenMessageStop && !hasSeenError) {
|
|
6802
|
+
throw createStreamTruncatedError({
|
|
6803
|
+
inFlightEvent: lastEventType ?? void 0,
|
|
6804
|
+
lastEventType: lastEventType ?? void 0
|
|
6805
|
+
});
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
6808
|
+
if (onUsage && hasRecordedUsage(stats)) {
|
|
6809
|
+
onUsage(stats);
|
|
6810
|
+
}
|
|
6811
|
+
controller.close();
|
|
6812
|
+
return;
|
|
6813
|
+
}
|
|
6814
|
+
const text = decoder.decode(value, { stream: true });
|
|
6815
|
+
if (!text) {
|
|
6816
|
+
continue;
|
|
6817
|
+
}
|
|
6818
|
+
sseBuffer += normalizeChunk(text);
|
|
6819
|
+
if (processBufferedEvents(controller)) {
|
|
6820
|
+
return;
|
|
6821
|
+
}
|
|
6068
6822
|
}
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
}
|
|
6072
|
-
const text = decoder.decode(value, { stream: true });
|
|
6073
|
-
if (onUsage || onAccountError) {
|
|
6074
|
-
sseBuffer += text.replace(/\r\n/g, "\n");
|
|
6075
|
-
processSSEBuffer(false);
|
|
6076
|
-
}
|
|
6077
|
-
const rewrittenText = rewriteSSEChunk(text, false);
|
|
6078
|
-
if (rewrittenText) {
|
|
6079
|
-
controller.enqueue(encoder.encode(rewrittenText));
|
|
6080
|
-
} else {
|
|
6081
|
-
controller.enqueue(EMPTY_CHUNK);
|
|
6823
|
+
} catch (error) {
|
|
6824
|
+
await failStream(controller, error);
|
|
6082
6825
|
}
|
|
6826
|
+
},
|
|
6827
|
+
cancel(reason) {
|
|
6828
|
+
return reader.cancel(reason);
|
|
6083
6829
|
}
|
|
6084
6830
|
});
|
|
6085
6831
|
return new Response(stream, {
|
|
@@ -6094,6 +6840,7 @@ function isEventStreamResponse(response) {
|
|
|
6094
6840
|
}
|
|
6095
6841
|
|
|
6096
6842
|
// src/index.ts
|
|
6843
|
+
init_account_identity();
|
|
6097
6844
|
init_storage();
|
|
6098
6845
|
|
|
6099
6846
|
// src/token-refresh.ts
|
|
@@ -6105,9 +6852,9 @@ init_storage();
|
|
|
6105
6852
|
import { createHash as createHash3, randomBytes as randomBytes4 } from "node:crypto";
|
|
6106
6853
|
import { promises as fs2 } from "node:fs";
|
|
6107
6854
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
6108
|
-
var DEFAULT_LOCK_TIMEOUT_MS =
|
|
6855
|
+
var DEFAULT_LOCK_TIMEOUT_MS = 15e3;
|
|
6109
6856
|
var DEFAULT_LOCK_BACKOFF_MS = 50;
|
|
6110
|
-
var DEFAULT_STALE_LOCK_MS =
|
|
6857
|
+
var DEFAULT_STALE_LOCK_MS = 9e4;
|
|
6111
6858
|
function delay(ms) {
|
|
6112
6859
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
6113
6860
|
}
|
|
@@ -6212,16 +6959,19 @@ function applyDiskAuthIfFresher(account, diskAuth, options = {}) {
|
|
|
6212
6959
|
if (!diskAuth) return false;
|
|
6213
6960
|
const diskTokenUpdatedAt = diskAuth.tokenUpdatedAt || 0;
|
|
6214
6961
|
const memTokenUpdatedAt = account.tokenUpdatedAt || 0;
|
|
6215
|
-
const
|
|
6962
|
+
const diskIsNewer = diskTokenUpdatedAt > memTokenUpdatedAt;
|
|
6963
|
+
const diskHasDifferentRefreshToken = diskAuth.refreshToken !== account.refreshToken;
|
|
6216
6964
|
const memAuthExpired = !account.expires || account.expires <= Date.now();
|
|
6217
6965
|
const allowExpiredFallback = options.allowExpiredFallback === true;
|
|
6218
|
-
if (
|
|
6966
|
+
if (!diskIsNewer && !(allowExpiredFallback && diskHasDifferentRefreshToken && memAuthExpired)) {
|
|
6219
6967
|
return false;
|
|
6220
6968
|
}
|
|
6221
6969
|
account.refreshToken = diskAuth.refreshToken;
|
|
6222
6970
|
account.access = diskAuth.access;
|
|
6223
6971
|
account.expires = diskAuth.expires;
|
|
6224
|
-
|
|
6972
|
+
if (diskIsNewer) {
|
|
6973
|
+
account.tokenUpdatedAt = diskTokenUpdatedAt;
|
|
6974
|
+
}
|
|
6225
6975
|
return true;
|
|
6226
6976
|
}
|
|
6227
6977
|
function claudeBinaryPath() {
|
|
@@ -6287,11 +7037,9 @@ async function refreshCCAccount(account) {
|
|
|
6287
7037
|
markTokenStateUpdated(account);
|
|
6288
7038
|
return refreshedCredential.accessToken;
|
|
6289
7039
|
}
|
|
6290
|
-
async function refreshAccountToken(account, client, source = "foreground", { onTokensUpdated } = {}) {
|
|
7040
|
+
async function refreshAccountToken(account, client, source = "foreground", { onTokensUpdated, debugLog } = {}) {
|
|
6291
7041
|
const lockResult = await acquireRefreshLock(account.id, {
|
|
6292
|
-
|
|
6293
|
-
backoffMs: 60,
|
|
6294
|
-
staleMs: 2e4
|
|
7042
|
+
backoffMs: 60
|
|
6295
7043
|
});
|
|
6296
7044
|
const lock = lockResult && typeof lockResult === "object" ? lockResult : {
|
|
6297
7045
|
acquired: true,
|
|
@@ -6319,7 +7067,9 @@ async function refreshAccountToken(account, client, source = "foreground", { onT
|
|
|
6319
7067
|
const accessToken = await refreshCCAccount(account);
|
|
6320
7068
|
if (accessToken) {
|
|
6321
7069
|
if (onTokensUpdated) {
|
|
6322
|
-
await onTokensUpdated().catch(() =>
|
|
7070
|
+
await onTokensUpdated().catch((err) => {
|
|
7071
|
+
debugLog?.("onTokensUpdated failed:", err.message);
|
|
7072
|
+
});
|
|
6323
7073
|
}
|
|
6324
7074
|
await client.auth?.set({
|
|
6325
7075
|
path: { id: "anthropic" },
|
|
@@ -6329,7 +7079,9 @@ async function refreshAccountToken(account, client, source = "foreground", { onT
|
|
|
6329
7079
|
access: account.access,
|
|
6330
7080
|
expires: account.expires
|
|
6331
7081
|
}
|
|
6332
|
-
}).catch(() =>
|
|
7082
|
+
}).catch((err) => {
|
|
7083
|
+
debugLog?.("auth.set failed:", err.message);
|
|
7084
|
+
});
|
|
6333
7085
|
return accessToken;
|
|
6334
7086
|
}
|
|
6335
7087
|
throw new Error("CC credential refresh failed");
|
|
@@ -6375,28 +7127,181 @@ function formatSwitchReason(status, reason) {
|
|
|
6375
7127
|
|
|
6376
7128
|
// src/bun-fetch.ts
|
|
6377
7129
|
import { execFileSync, spawn } from "node:child_process";
|
|
6378
|
-
import { existsSync as existsSync5
|
|
7130
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
6379
7131
|
import { dirname as dirname4, join as join6 } from "node:path";
|
|
6380
|
-
import
|
|
7132
|
+
import * as readline from "node:readline";
|
|
6381
7133
|
import { fileURLToPath } from "node:url";
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
var
|
|
6385
|
-
var
|
|
6386
|
-
var
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
exitHandlerRegistered = true;
|
|
6393
|
-
const cleanup = () => {
|
|
6394
|
-
stopBunProxy();
|
|
7134
|
+
|
|
7135
|
+
// src/circuit-breaker.ts
|
|
7136
|
+
var DEFAULT_FAILURE_THRESHOLD = 5;
|
|
7137
|
+
var DEFAULT_RESET_TIMEOUT_MS = 3e4;
|
|
7138
|
+
var pendingClientBreakers = /* @__PURE__ */ new Map();
|
|
7139
|
+
function normalizeConfig(options = {}) {
|
|
7140
|
+
return {
|
|
7141
|
+
clientId: options.clientId,
|
|
7142
|
+
failureThreshold: Math.max(1, Math.trunc(options.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD)),
|
|
7143
|
+
resetTimeoutMs: Math.max(0, Math.trunc(options.resetTimeoutMs ?? DEFAULT_RESET_TIMEOUT_MS))
|
|
6395
7144
|
};
|
|
6396
|
-
process.on("exit", cleanup);
|
|
6397
|
-
process.on("SIGINT", cleanup);
|
|
6398
|
-
process.on("SIGTERM", cleanup);
|
|
6399
7145
|
}
|
|
7146
|
+
function getErrorMessage2(error) {
|
|
7147
|
+
if (error instanceof Error && error.message) {
|
|
7148
|
+
return error.message;
|
|
7149
|
+
}
|
|
7150
|
+
if (typeof error === "string" && error) {
|
|
7151
|
+
return error;
|
|
7152
|
+
}
|
|
7153
|
+
return "Unknown error";
|
|
7154
|
+
}
|
|
7155
|
+
function isPromiseLike(value) {
|
|
7156
|
+
return typeof value === "object" && value !== null && "then" in value;
|
|
7157
|
+
}
|
|
7158
|
+
function releasePendingClientBreaker(clientId, breaker) {
|
|
7159
|
+
queueMicrotask(() => {
|
|
7160
|
+
if (pendingClientBreakers.get(clientId) === breaker) {
|
|
7161
|
+
pendingClientBreakers.delete(clientId);
|
|
7162
|
+
}
|
|
7163
|
+
});
|
|
7164
|
+
}
|
|
7165
|
+
var CircuitBreaker = class {
|
|
7166
|
+
config;
|
|
7167
|
+
state = "CLOSED" /* CLOSED */;
|
|
7168
|
+
failureCount = 0;
|
|
7169
|
+
openedAt = null;
|
|
7170
|
+
resetTimer = null;
|
|
7171
|
+
constructor(options = {}) {
|
|
7172
|
+
this.config = normalizeConfig(options);
|
|
7173
|
+
}
|
|
7174
|
+
getState() {
|
|
7175
|
+
return this.state;
|
|
7176
|
+
}
|
|
7177
|
+
getFailureCount() {
|
|
7178
|
+
return this.failureCount;
|
|
7179
|
+
}
|
|
7180
|
+
getOpenedAt() {
|
|
7181
|
+
return this.openedAt;
|
|
7182
|
+
}
|
|
7183
|
+
getConfig() {
|
|
7184
|
+
return { ...this.config };
|
|
7185
|
+
}
|
|
7186
|
+
canExecute() {
|
|
7187
|
+
return this.state !== "OPEN" /* OPEN */;
|
|
7188
|
+
}
|
|
7189
|
+
recordSuccess() {
|
|
7190
|
+
if (this.state === "HALF_OPEN" /* HALF_OPEN */) {
|
|
7191
|
+
this.transitionToClosed();
|
|
7192
|
+
return;
|
|
7193
|
+
}
|
|
7194
|
+
if (this.state === "CLOSED" /* CLOSED */) {
|
|
7195
|
+
this.failureCount = 0;
|
|
7196
|
+
}
|
|
7197
|
+
}
|
|
7198
|
+
recordFailure() {
|
|
7199
|
+
if (this.state === "HALF_OPEN" /* HALF_OPEN */) {
|
|
7200
|
+
this.transitionToOpen();
|
|
7201
|
+
return;
|
|
7202
|
+
}
|
|
7203
|
+
if (this.state === "OPEN" /* OPEN */) {
|
|
7204
|
+
return;
|
|
7205
|
+
}
|
|
7206
|
+
this.failureCount += 1;
|
|
7207
|
+
if (this.failureCount >= this.config.failureThreshold) {
|
|
7208
|
+
this.transitionToOpen();
|
|
7209
|
+
}
|
|
7210
|
+
}
|
|
7211
|
+
transitionToHalfOpen() {
|
|
7212
|
+
this.clearResetTimer();
|
|
7213
|
+
this.state = "HALF_OPEN" /* HALF_OPEN */;
|
|
7214
|
+
this.openedAt = null;
|
|
7215
|
+
this.failureCount = 0;
|
|
7216
|
+
}
|
|
7217
|
+
execute(operation) {
|
|
7218
|
+
if (!this.canExecute()) {
|
|
7219
|
+
return {
|
|
7220
|
+
success: false,
|
|
7221
|
+
error: "Circuit breaker is OPEN"
|
|
7222
|
+
};
|
|
7223
|
+
}
|
|
7224
|
+
try {
|
|
7225
|
+
const result = operation();
|
|
7226
|
+
if (isPromiseLike(result)) {
|
|
7227
|
+
return result.then((data) => {
|
|
7228
|
+
this.recordSuccess();
|
|
7229
|
+
return {
|
|
7230
|
+
success: true,
|
|
7231
|
+
data
|
|
7232
|
+
};
|
|
7233
|
+
}).catch((error) => {
|
|
7234
|
+
this.recordFailure();
|
|
7235
|
+
return {
|
|
7236
|
+
success: false,
|
|
7237
|
+
error: getErrorMessage2(error)
|
|
7238
|
+
};
|
|
7239
|
+
});
|
|
7240
|
+
}
|
|
7241
|
+
this.recordSuccess();
|
|
7242
|
+
return {
|
|
7243
|
+
success: true,
|
|
7244
|
+
data: result
|
|
7245
|
+
};
|
|
7246
|
+
} catch (error) {
|
|
7247
|
+
this.recordFailure();
|
|
7248
|
+
return {
|
|
7249
|
+
success: false,
|
|
7250
|
+
error: getErrorMessage2(error)
|
|
7251
|
+
};
|
|
7252
|
+
}
|
|
7253
|
+
}
|
|
7254
|
+
dispose() {
|
|
7255
|
+
this.clearResetTimer();
|
|
7256
|
+
}
|
|
7257
|
+
transitionToClosed() {
|
|
7258
|
+
this.clearResetTimer();
|
|
7259
|
+
this.state = "CLOSED" /* CLOSED */;
|
|
7260
|
+
this.failureCount = 0;
|
|
7261
|
+
this.openedAt = null;
|
|
7262
|
+
}
|
|
7263
|
+
transitionToOpen() {
|
|
7264
|
+
this.clearResetTimer();
|
|
7265
|
+
this.state = "OPEN" /* OPEN */;
|
|
7266
|
+
this.openedAt = Date.now();
|
|
7267
|
+
this.scheduleHalfOpenTransition();
|
|
7268
|
+
}
|
|
7269
|
+
scheduleHalfOpenTransition() {
|
|
7270
|
+
this.resetTimer = setTimeout(() => {
|
|
7271
|
+
this.resetTimer = null;
|
|
7272
|
+
if (this.state === "OPEN" /* OPEN */) {
|
|
7273
|
+
this.transitionToHalfOpen();
|
|
7274
|
+
}
|
|
7275
|
+
}, this.config.resetTimeoutMs);
|
|
7276
|
+
this.resetTimer.unref?.();
|
|
7277
|
+
}
|
|
7278
|
+
clearResetTimer() {
|
|
7279
|
+
if (!this.resetTimer) {
|
|
7280
|
+
return;
|
|
7281
|
+
}
|
|
7282
|
+
clearTimeout(this.resetTimer);
|
|
7283
|
+
this.resetTimer = null;
|
|
7284
|
+
}
|
|
7285
|
+
};
|
|
7286
|
+
function createCircuitBreaker(options = {}) {
|
|
7287
|
+
if (!options.clientId) {
|
|
7288
|
+
return new CircuitBreaker(options);
|
|
7289
|
+
}
|
|
7290
|
+
const existingBreaker = pendingClientBreakers.get(options.clientId);
|
|
7291
|
+
if (existingBreaker) {
|
|
7292
|
+
return existingBreaker;
|
|
7293
|
+
}
|
|
7294
|
+
const breaker = new CircuitBreaker(options);
|
|
7295
|
+
pendingClientBreakers.set(options.clientId, breaker);
|
|
7296
|
+
releasePendingClientBreaker(options.clientId, breaker);
|
|
7297
|
+
return breaker;
|
|
7298
|
+
}
|
|
7299
|
+
|
|
7300
|
+
// src/bun-fetch.ts
|
|
7301
|
+
var DEFAULT_PROXY_HOST = "127.0.0.1";
|
|
7302
|
+
var DEFAULT_STARTUP_TIMEOUT_MS = 5e3;
|
|
7303
|
+
var DEFAULT_BREAKER_FAILURE_THRESHOLD = 2;
|
|
7304
|
+
var DEFAULT_BREAKER_RESET_TIMEOUT_MS = 1e4;
|
|
6400
7305
|
function findProxyScript() {
|
|
6401
7306
|
const dir = typeof __dirname !== "undefined" ? __dirname : dirname4(fileURLToPath(import.meta.url));
|
|
6402
7307
|
for (const candidate of [
|
|
@@ -6404,189 +7309,337 @@ function findProxyScript() {
|
|
|
6404
7309
|
join6(dir, "..", "dist", "bun-proxy.mjs"),
|
|
6405
7310
|
join6(dir, "bun-proxy.ts")
|
|
6406
7311
|
]) {
|
|
6407
|
-
if (existsSync5(candidate))
|
|
7312
|
+
if (existsSync5(candidate)) {
|
|
7313
|
+
return candidate;
|
|
7314
|
+
}
|
|
6408
7315
|
}
|
|
6409
7316
|
return null;
|
|
6410
7317
|
}
|
|
6411
|
-
|
|
6412
|
-
function hasBun() {
|
|
6413
|
-
if (_hasBun !== null) return _hasBun;
|
|
7318
|
+
function detectBunAvailability() {
|
|
6414
7319
|
try {
|
|
6415
|
-
execFileSync("
|
|
6416
|
-
|
|
7320
|
+
execFileSync("bun", ["--version"], { stdio: "ignore" });
|
|
7321
|
+
return true;
|
|
6417
7322
|
} catch {
|
|
6418
|
-
|
|
7323
|
+
return false;
|
|
6419
7324
|
}
|
|
6420
|
-
return _hasBun;
|
|
6421
7325
|
}
|
|
6422
|
-
function
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
}
|
|
6432
|
-
unlinkSync(PID_FILE);
|
|
6433
|
-
} catch {
|
|
7326
|
+
function toHeaders(headersInit) {
|
|
7327
|
+
return new Headers(headersInit ?? void 0);
|
|
7328
|
+
}
|
|
7329
|
+
function toRequestUrl(input) {
|
|
7330
|
+
return typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
7331
|
+
}
|
|
7332
|
+
function resolveProxySignal(input, init) {
|
|
7333
|
+
if (init?.signal) {
|
|
7334
|
+
return init.signal;
|
|
6434
7335
|
}
|
|
7336
|
+
return input instanceof Request ? input.signal : void 0;
|
|
6435
7337
|
}
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
7338
|
+
function buildProxyRequestInit(input, init) {
|
|
7339
|
+
const targetUrl = toRequestUrl(input);
|
|
7340
|
+
const headers = toHeaders(init?.headers);
|
|
7341
|
+
const signal = resolveProxySignal(input, init);
|
|
7342
|
+
headers.set("x-proxy-url", targetUrl);
|
|
7343
|
+
return {
|
|
7344
|
+
...init,
|
|
7345
|
+
headers,
|
|
7346
|
+
...signal ? { signal } : {}
|
|
7347
|
+
};
|
|
7348
|
+
}
|
|
7349
|
+
async function writeDebugArtifacts(url, init) {
|
|
7350
|
+
if (!init.body || !url.includes("/v1/messages") || url.includes("count_tokens")) {
|
|
7351
|
+
return;
|
|
6444
7352
|
}
|
|
7353
|
+
const { writeFileSync: writeFileSync5 } = await import("node:fs");
|
|
7354
|
+
writeFileSync5(
|
|
7355
|
+
"/tmp/opencode-last-request.json",
|
|
7356
|
+
typeof init.body === "string" ? init.body : JSON.stringify(init.body)
|
|
7357
|
+
);
|
|
7358
|
+
const logHeaders = {};
|
|
7359
|
+
toHeaders(init.headers).forEach((value, key) => {
|
|
7360
|
+
logHeaders[key] = key === "authorization" ? "Bearer ***" : value;
|
|
7361
|
+
});
|
|
7362
|
+
writeFileSync5("/tmp/opencode-last-headers.json", JSON.stringify(logHeaders, null, 2));
|
|
6445
7363
|
}
|
|
6446
|
-
function
|
|
6447
|
-
|
|
7364
|
+
function createBunFetch(options = {}) {
|
|
7365
|
+
const breaker = createCircuitBreaker({
|
|
7366
|
+
failureThreshold: DEFAULT_BREAKER_FAILURE_THRESHOLD,
|
|
7367
|
+
resetTimeoutMs: DEFAULT_BREAKER_RESET_TIMEOUT_MS
|
|
7368
|
+
});
|
|
7369
|
+
const closingChildren = /* @__PURE__ */ new WeakSet();
|
|
7370
|
+
const defaultDebug = options.debug ?? false;
|
|
7371
|
+
const onProxyStatus = options.onProxyStatus;
|
|
7372
|
+
const state = {
|
|
7373
|
+
activeChild: null,
|
|
7374
|
+
activePort: null,
|
|
7375
|
+
startingChild: null,
|
|
7376
|
+
startPromise: null,
|
|
7377
|
+
bunAvailable: null,
|
|
7378
|
+
pendingFetches: []
|
|
7379
|
+
};
|
|
7380
|
+
const getStatus = (reason = "idle", status = "state") => ({
|
|
7381
|
+
status,
|
|
7382
|
+
mode: state.activePort !== null ? "proxy" : state.startPromise ? "starting" : "native",
|
|
7383
|
+
port: state.activePort,
|
|
7384
|
+
bunAvailable: state.bunAvailable,
|
|
7385
|
+
childPid: state.activeChild?.pid ?? state.startingChild?.pid ?? null,
|
|
7386
|
+
circuitState: breaker.getState(),
|
|
7387
|
+
circuitFailureCount: breaker.getFailureCount(),
|
|
7388
|
+
reason
|
|
7389
|
+
});
|
|
7390
|
+
const reportStatus = (reason) => {
|
|
7391
|
+
onProxyStatus?.(getStatus(reason));
|
|
7392
|
+
};
|
|
7393
|
+
const reportFallback = (reason, _debugOverride) => {
|
|
7394
|
+
onProxyStatus?.(getStatus(reason, "fallback"));
|
|
7395
|
+
console.error(
|
|
7396
|
+
`[opencode-anthropic-auth] Native fetch fallback engaged (${reason}); Bun proxy fingerprint mimicry disabled for this request`
|
|
7397
|
+
);
|
|
7398
|
+
};
|
|
7399
|
+
const resolveDebug = (debugOverride) => debugOverride ?? defaultDebug;
|
|
7400
|
+
const clearActiveProxy = (child) => {
|
|
7401
|
+
if (child && state.activeChild === child) {
|
|
7402
|
+
state.activeChild = null;
|
|
7403
|
+
state.activePort = null;
|
|
7404
|
+
}
|
|
7405
|
+
if (child && state.startingChild === child) {
|
|
7406
|
+
state.startingChild = null;
|
|
7407
|
+
}
|
|
7408
|
+
};
|
|
7409
|
+
const flushPendingFetches = (mode, reason = "proxy-unavailable") => {
|
|
7410
|
+
const pendingFetches = state.pendingFetches.splice(0, state.pendingFetches.length);
|
|
7411
|
+
const useForwardFetch = pendingFetches.length <= 2;
|
|
7412
|
+
for (const pendingFetch of pendingFetches) {
|
|
7413
|
+
if (mode === "proxy") {
|
|
7414
|
+
pendingFetch.runProxy(useForwardFetch);
|
|
7415
|
+
continue;
|
|
7416
|
+
}
|
|
7417
|
+
pendingFetch.runNative(reason);
|
|
7418
|
+
}
|
|
7419
|
+
};
|
|
7420
|
+
const startProxy = async (debugOverride) => {
|
|
7421
|
+
if (state.activeChild && state.activePort !== null && !state.activeChild.killed) {
|
|
7422
|
+
return state.activePort;
|
|
7423
|
+
}
|
|
7424
|
+
if (state.startPromise) {
|
|
7425
|
+
return state.startPromise;
|
|
7426
|
+
}
|
|
7427
|
+
if (!breaker.canExecute()) {
|
|
7428
|
+
reportStatus("breaker-open");
|
|
7429
|
+
flushPendingFetches("native", "breaker-open");
|
|
7430
|
+
return null;
|
|
7431
|
+
}
|
|
7432
|
+
if (state.bunAvailable === false) {
|
|
7433
|
+
reportStatus("bun-unavailable");
|
|
7434
|
+
flushPendingFetches("native", "bun-unavailable");
|
|
7435
|
+
return null;
|
|
7436
|
+
}
|
|
6448
7437
|
const script = findProxyScript();
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
7438
|
+
state.bunAvailable = detectBunAvailability();
|
|
7439
|
+
if (!script || !state.bunAvailable) {
|
|
7440
|
+
breaker.recordFailure();
|
|
7441
|
+
reportStatus(script ? "bun-unavailable" : "proxy-script-missing");
|
|
7442
|
+
flushPendingFetches("native", script ? "bun-unavailable" : "proxy-script-missing");
|
|
7443
|
+
return null;
|
|
6452
7444
|
}
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
const finish = (port) => {
|
|
6463
|
-
if (done) return;
|
|
6464
|
-
done = true;
|
|
6465
|
-
if (port && child.pid) {
|
|
6466
|
-
try {
|
|
6467
|
-
writeFileSync5(PID_FILE, String(child.pid));
|
|
6468
|
-
} catch {
|
|
7445
|
+
state.startPromise = new Promise((resolve2) => {
|
|
7446
|
+
const debugEnabled = resolveDebug(debugOverride);
|
|
7447
|
+
let child;
|
|
7448
|
+
try {
|
|
7449
|
+
child = spawn("bun", ["run", script, "--parent-pid", String(process.pid)], {
|
|
7450
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7451
|
+
env: {
|
|
7452
|
+
...process.env,
|
|
7453
|
+
OPENCODE_ANTHROPIC_DEBUG: debugEnabled ? "1" : "0"
|
|
6469
7454
|
}
|
|
7455
|
+
});
|
|
7456
|
+
} catch {
|
|
7457
|
+
breaker.recordFailure();
|
|
7458
|
+
reportStatus("spawn-failed");
|
|
7459
|
+
flushPendingFetches("native", "spawn-failed");
|
|
7460
|
+
resolve2(null);
|
|
7461
|
+
return;
|
|
7462
|
+
}
|
|
7463
|
+
state.startingChild = child;
|
|
7464
|
+
reportStatus("starting");
|
|
7465
|
+
const stdout2 = child.stdout;
|
|
7466
|
+
if (!stdout2) {
|
|
7467
|
+
clearActiveProxy(child);
|
|
7468
|
+
breaker.recordFailure();
|
|
7469
|
+
reportStatus("stdout-missing");
|
|
7470
|
+
flushPendingFetches("native", "stdout-missing");
|
|
7471
|
+
resolve2(null);
|
|
7472
|
+
return;
|
|
7473
|
+
}
|
|
7474
|
+
let settled = false;
|
|
7475
|
+
const stdoutLines = readline.createInterface({ input: stdout2 });
|
|
7476
|
+
const startupTimeout = setTimeout(() => {
|
|
7477
|
+
finalize(null, "startup-timeout");
|
|
7478
|
+
}, DEFAULT_STARTUP_TIMEOUT_MS);
|
|
7479
|
+
startupTimeout.unref?.();
|
|
7480
|
+
const cleanupStartupResources = () => {
|
|
7481
|
+
clearTimeout(startupTimeout);
|
|
7482
|
+
stdoutLines.close();
|
|
7483
|
+
};
|
|
7484
|
+
const finalize = (result, reason) => {
|
|
7485
|
+
if (settled) {
|
|
7486
|
+
return;
|
|
7487
|
+
}
|
|
7488
|
+
settled = true;
|
|
7489
|
+
cleanupStartupResources();
|
|
7490
|
+
if (result) {
|
|
7491
|
+
state.startingChild = null;
|
|
7492
|
+
state.activeChild = result.child;
|
|
7493
|
+
state.activePort = result.port;
|
|
7494
|
+
breaker.recordSuccess();
|
|
7495
|
+
reportStatus(reason);
|
|
7496
|
+
flushPendingFetches("proxy");
|
|
7497
|
+
resolve2(result.port);
|
|
7498
|
+
return;
|
|
6470
7499
|
}
|
|
6471
|
-
|
|
7500
|
+
clearActiveProxy(child);
|
|
7501
|
+
breaker.recordFailure();
|
|
7502
|
+
reportStatus(reason);
|
|
7503
|
+
flushPendingFetches("native", reason);
|
|
7504
|
+
resolve2(null);
|
|
6472
7505
|
};
|
|
6473
|
-
|
|
6474
|
-
const
|
|
6475
|
-
if (
|
|
6476
|
-
|
|
6477
|
-
healthCheckFails = 0;
|
|
6478
|
-
finish(proxyPort);
|
|
7506
|
+
stdoutLines.on("line", (line) => {
|
|
7507
|
+
const match = line.match(/^BUN_PROXY_PORT=(\d+)$/);
|
|
7508
|
+
if (!match) {
|
|
7509
|
+
return;
|
|
6479
7510
|
}
|
|
7511
|
+
finalize(
|
|
7512
|
+
{
|
|
7513
|
+
child,
|
|
7514
|
+
port: Number.parseInt(match[1], 10)
|
|
7515
|
+
},
|
|
7516
|
+
"proxy-ready"
|
|
7517
|
+
);
|
|
6480
7518
|
});
|
|
6481
|
-
child.
|
|
6482
|
-
|
|
6483
|
-
proxyPort = null;
|
|
6484
|
-
proxyProcess = null;
|
|
6485
|
-
starting = null;
|
|
7519
|
+
child.once("error", () => {
|
|
7520
|
+
finalize(null, "child-error");
|
|
6486
7521
|
});
|
|
6487
|
-
child.
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
7522
|
+
child.once("exit", () => {
|
|
7523
|
+
const shutdownOwned = closingChildren.has(child);
|
|
7524
|
+
const isCurrentChild = state.activeChild === child || state.startingChild === child;
|
|
7525
|
+
clearActiveProxy(child);
|
|
7526
|
+
if (!settled) {
|
|
7527
|
+
finalize(null, shutdownOwned ? "shutdown-complete" : "child-exit-before-ready");
|
|
7528
|
+
return;
|
|
7529
|
+
}
|
|
7530
|
+
if (!shutdownOwned && isCurrentChild) {
|
|
7531
|
+
breaker.recordFailure();
|
|
7532
|
+
reportStatus("child-exited");
|
|
7533
|
+
}
|
|
6492
7534
|
});
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
resolve2(null);
|
|
6496
|
-
}
|
|
6497
|
-
});
|
|
6498
|
-
}
|
|
6499
|
-
async function ensureBunProxy() {
|
|
6500
|
-
if (process.env.VITEST || process.env.NODE_ENV === "test") return null;
|
|
6501
|
-
if (proxyPort && proxyProcess && !proxyProcess.killed) {
|
|
6502
|
-
return proxyPort;
|
|
6503
|
-
}
|
|
6504
|
-
if (!proxyPort && await isProxyHealthy(FIXED_PORT)) {
|
|
6505
|
-
proxyPort = FIXED_PORT;
|
|
6506
|
-
console.error("[bun-fetch] Reusing existing Bun proxy on port", FIXED_PORT);
|
|
6507
|
-
return proxyPort;
|
|
6508
|
-
}
|
|
6509
|
-
if (proxyPort && (!proxyProcess || proxyProcess.killed)) {
|
|
6510
|
-
proxyPort = null;
|
|
6511
|
-
proxyProcess = null;
|
|
6512
|
-
starting = null;
|
|
6513
|
-
}
|
|
6514
|
-
if (starting) return starting;
|
|
6515
|
-
starting = spawnProxy();
|
|
6516
|
-
const port = await starting;
|
|
6517
|
-
starting = null;
|
|
6518
|
-
if (port) console.error("[bun-fetch] Bun proxy started on port", port);
|
|
6519
|
-
else console.error("[bun-fetch] Failed to start Bun proxy, falling back to Node.js fetch");
|
|
6520
|
-
return port;
|
|
6521
|
-
}
|
|
6522
|
-
function stopBunProxy() {
|
|
6523
|
-
if (proxyProcess) {
|
|
6524
|
-
try {
|
|
6525
|
-
proxyProcess.kill();
|
|
6526
|
-
} catch {
|
|
6527
|
-
}
|
|
6528
|
-
proxyProcess = null;
|
|
6529
|
-
}
|
|
6530
|
-
proxyPort = null;
|
|
6531
|
-
starting = null;
|
|
6532
|
-
killStaleProxy();
|
|
6533
|
-
}
|
|
6534
|
-
async function fetchViaBun(input, init) {
|
|
6535
|
-
const port = await ensureBunProxy();
|
|
6536
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
6537
|
-
if (!port) return fetch(input, init);
|
|
6538
|
-
const headers = new Headers(init.headers);
|
|
6539
|
-
headers.set("x-proxy-url", url);
|
|
6540
|
-
try {
|
|
6541
|
-
const resp = await fetch(`http://127.0.0.1:${port}/`, {
|
|
6542
|
-
method: init.method || "POST",
|
|
6543
|
-
headers,
|
|
6544
|
-
body: init.body
|
|
7535
|
+
}).finally(() => {
|
|
7536
|
+
state.startPromise = null;
|
|
6545
7537
|
});
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
headers: retryHeaders,
|
|
6564
|
-
body: init.body
|
|
6565
|
-
});
|
|
7538
|
+
return state.startPromise;
|
|
7539
|
+
};
|
|
7540
|
+
const shutdown = async () => {
|
|
7541
|
+
const children = [state.startingChild, state.activeChild].filter(
|
|
7542
|
+
(child) => child !== null
|
|
7543
|
+
);
|
|
7544
|
+
state.startPromise = null;
|
|
7545
|
+
state.startingChild = null;
|
|
7546
|
+
state.activeChild = null;
|
|
7547
|
+
state.activePort = null;
|
|
7548
|
+
for (const child of children) {
|
|
7549
|
+
closingChildren.add(child);
|
|
7550
|
+
if (!child.killed) {
|
|
7551
|
+
try {
|
|
7552
|
+
child.kill("SIGTERM");
|
|
7553
|
+
} catch {
|
|
7554
|
+
}
|
|
6566
7555
|
}
|
|
6567
7556
|
}
|
|
6568
|
-
|
|
6569
|
-
|
|
7557
|
+
breaker.dispose();
|
|
7558
|
+
reportStatus("shutdown-requested");
|
|
7559
|
+
};
|
|
7560
|
+
const fetchThroughProxy = async (input, init, debugOverride) => {
|
|
7561
|
+
const url = toRequestUrl(input);
|
|
7562
|
+
const fetchNative = async (reason) => {
|
|
7563
|
+
reportFallback(reason, debugOverride);
|
|
7564
|
+
return globalThis.fetch(input, init);
|
|
7565
|
+
};
|
|
7566
|
+
const fetchFromActiveProxy = async (useForwardFetch) => {
|
|
7567
|
+
const port = state.activePort;
|
|
7568
|
+
if (port === null) {
|
|
7569
|
+
return fetchNative("proxy-port-missing");
|
|
7570
|
+
}
|
|
7571
|
+
if (resolveDebug(debugOverride)) {
|
|
7572
|
+
console.error(`[opencode-anthropic-auth] Routing through Bun proxy at :${port} \u2192 ${url}`);
|
|
7573
|
+
}
|
|
7574
|
+
if (resolveDebug(debugOverride)) {
|
|
7575
|
+
try {
|
|
7576
|
+
await writeDebugArtifacts(url, init ?? {});
|
|
7577
|
+
if ((init?.body ?? null) !== null && url.includes("/v1/messages") && !url.includes("count_tokens")) {
|
|
7578
|
+
console.error("[opencode-anthropic-auth] Dumped request to /tmp/opencode-last-request.json");
|
|
7579
|
+
}
|
|
7580
|
+
} catch (error) {
|
|
7581
|
+
console.error("[opencode-anthropic-auth] Failed to dump request:", error);
|
|
7582
|
+
}
|
|
7583
|
+
}
|
|
7584
|
+
const proxyInit = buildProxyRequestInit(input, init);
|
|
7585
|
+
const forwardFetch = state.activeChild?.forwardFetch;
|
|
7586
|
+
const response = await (useForwardFetch && typeof forwardFetch === "function" ? forwardFetch(`http://${DEFAULT_PROXY_HOST}:${port}/`, proxyInit) : fetch(`http://${DEFAULT_PROXY_HOST}:${port}/`, proxyInit));
|
|
7587
|
+
if (response.status === 502) {
|
|
7588
|
+
const errorText = await response.text();
|
|
7589
|
+
throw new Error(`Bun proxy upstream error: ${errorText}`);
|
|
7590
|
+
}
|
|
7591
|
+
return response;
|
|
7592
|
+
};
|
|
7593
|
+
if (state.activeChild && state.activePort !== null && !state.activeChild.killed) {
|
|
7594
|
+
return fetchFromActiveProxy(true);
|
|
7595
|
+
}
|
|
7596
|
+
return new Promise((resolve2, reject) => {
|
|
7597
|
+
let settled = false;
|
|
7598
|
+
const pendingFetch = {
|
|
7599
|
+
runProxy: (useForwardFetch) => {
|
|
7600
|
+
if (settled) {
|
|
7601
|
+
return;
|
|
7602
|
+
}
|
|
7603
|
+
settled = true;
|
|
7604
|
+
void fetchFromActiveProxy(useForwardFetch).then(resolve2, reject);
|
|
7605
|
+
},
|
|
7606
|
+
runNative: (reason) => {
|
|
7607
|
+
if (settled) {
|
|
7608
|
+
return;
|
|
7609
|
+
}
|
|
7610
|
+
settled = true;
|
|
7611
|
+
void fetchNative(reason).then(resolve2, reject);
|
|
7612
|
+
}
|
|
7613
|
+
};
|
|
7614
|
+
state.pendingFetches.push(pendingFetch);
|
|
7615
|
+
void startProxy(debugOverride).catch(() => {
|
|
7616
|
+
state.pendingFetches = state.pendingFetches.filter((candidate) => candidate !== pendingFetch);
|
|
7617
|
+
pendingFetch.runNative("proxy-start-error");
|
|
7618
|
+
});
|
|
7619
|
+
});
|
|
7620
|
+
};
|
|
7621
|
+
const instance = {
|
|
7622
|
+
fetch(input, init) {
|
|
7623
|
+
return fetchThroughProxy(input, init);
|
|
7624
|
+
},
|
|
7625
|
+
ensureProxy: startProxy,
|
|
7626
|
+
fetchWithDebug: fetchThroughProxy,
|
|
7627
|
+
shutdown,
|
|
7628
|
+
getStatus: () => getStatus()
|
|
7629
|
+
};
|
|
7630
|
+
return instance;
|
|
6570
7631
|
}
|
|
6571
7632
|
|
|
6572
|
-
// src/
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
7633
|
+
// src/plugin-helpers.ts
|
|
7634
|
+
var DEBOUNCE_TOAST_MAP_MAX_SIZE = 50;
|
|
7635
|
+
function createPluginHelpers({
|
|
7636
|
+
client,
|
|
7637
|
+
config,
|
|
7638
|
+
debugLog,
|
|
7639
|
+
getAccountManager,
|
|
7640
|
+
setAccountManager
|
|
7641
|
+
}) {
|
|
6580
7642
|
const debouncedToastTimestamps = /* @__PURE__ */ new Map();
|
|
6581
|
-
const refreshInFlight = /* @__PURE__ */ new Map();
|
|
6582
|
-
const idleRefreshLastAttempt = /* @__PURE__ */ new Map();
|
|
6583
|
-
const idleRefreshInFlight = /* @__PURE__ */ new Set();
|
|
6584
|
-
const IDLE_REFRESH_ENABLED = config.idle_refresh.enabled;
|
|
6585
|
-
const IDLE_REFRESH_WINDOW_MS = config.idle_refresh.window_minutes * 60 * 1e3;
|
|
6586
|
-
const IDLE_REFRESH_MIN_INTERVAL_MS = config.idle_refresh.min_interval_minutes * 60 * 1e3;
|
|
6587
|
-
let initialAccountPinned = false;
|
|
6588
|
-
const pendingSlashOAuth = /* @__PURE__ */ new Map();
|
|
6589
|
-
const fileAccountMap = /* @__PURE__ */ new Map();
|
|
6590
7643
|
async function sendCommandMessage(sessionID, text) {
|
|
6591
7644
|
await client.session?.prompt({
|
|
6592
7645
|
path: { id: sessionID },
|
|
@@ -6594,8 +7647,8 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6594
7647
|
});
|
|
6595
7648
|
}
|
|
6596
7649
|
async function reloadAccountManagerFromDisk() {
|
|
6597
|
-
if (!
|
|
6598
|
-
|
|
7650
|
+
if (!getAccountManager()) return;
|
|
7651
|
+
setAccountManager(await AccountManager.load(config, null));
|
|
6599
7652
|
}
|
|
6600
7653
|
async function persistOpenCodeAuth(refresh, access, expires) {
|
|
6601
7654
|
await client.auth?.set({
|
|
@@ -6632,29 +7685,36 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6632
7685
|
const now = Date.now();
|
|
6633
7686
|
const lastAt = debouncedToastTimestamps.get(options.debounceKey) ?? 0;
|
|
6634
7687
|
if (now - lastAt < minGapMs) return;
|
|
7688
|
+
if (!debouncedToastTimestamps.has(options.debounceKey) && debouncedToastTimestamps.size >= DEBOUNCE_TOAST_MAP_MAX_SIZE) {
|
|
7689
|
+
const oldestKey = debouncedToastTimestamps.keys().next().value;
|
|
7690
|
+
if (oldestKey !== void 0) debouncedToastTimestamps.delete(oldestKey);
|
|
7691
|
+
}
|
|
6635
7692
|
debouncedToastTimestamps.set(options.debounceKey, now);
|
|
6636
7693
|
}
|
|
6637
7694
|
}
|
|
6638
7695
|
try {
|
|
6639
7696
|
await client.tui?.showToast({ body: { message, variant } });
|
|
6640
|
-
} catch {
|
|
7697
|
+
} catch (err) {
|
|
7698
|
+
if (!(err instanceof TypeError)) debugLog("toast failed:", err);
|
|
6641
7699
|
}
|
|
6642
7700
|
}
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
7701
|
+
return {
|
|
7702
|
+
toast,
|
|
7703
|
+
sendCommandMessage,
|
|
7704
|
+
runCliCommand,
|
|
7705
|
+
reloadAccountManagerFromDisk,
|
|
7706
|
+
persistOpenCodeAuth
|
|
7707
|
+
};
|
|
7708
|
+
}
|
|
7709
|
+
|
|
7710
|
+
// src/refresh-helpers.ts
|
|
7711
|
+
function createRefreshHelpers({ client, config, getAccountManager, debugLog }) {
|
|
7712
|
+
const refreshInFlight = /* @__PURE__ */ new Map();
|
|
7713
|
+
const idleRefreshLastAttempt = /* @__PURE__ */ new Map();
|
|
7714
|
+
const idleRefreshInFlight = /* @__PURE__ */ new Set();
|
|
7715
|
+
const IDLE_REFRESH_ENABLED = config.idle_refresh.enabled;
|
|
7716
|
+
const IDLE_REFRESH_WINDOW_MS = config.idle_refresh.window_minutes * 60 * 1e3;
|
|
7717
|
+
const IDLE_REFRESH_MIN_INTERVAL_MS = config.idle_refresh.min_interval_minutes * 60 * 1e3;
|
|
6658
7718
|
function parseRefreshFailure(refreshError) {
|
|
6659
7719
|
const message = refreshError instanceof Error ? refreshError.message : String(refreshError);
|
|
6660
7720
|
const status = typeof refreshError === "object" && refreshError && "status" in refreshError ? Number(refreshError.status) : NaN;
|
|
@@ -6673,9 +7733,14 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6673
7733
|
if (source === "foreground" && existing.source === "idle") {
|
|
6674
7734
|
try {
|
|
6675
7735
|
await existing.promise;
|
|
6676
|
-
} catch {
|
|
7736
|
+
} catch (err) {
|
|
7737
|
+
void err;
|
|
6677
7738
|
}
|
|
6678
7739
|
if (account.access && account.expires && account.expires > Date.now()) return account.access;
|
|
7740
|
+
const retried = refreshInFlight.get(key);
|
|
7741
|
+
if (retried && retried !== existing) {
|
|
7742
|
+
return retried.promise;
|
|
7743
|
+
}
|
|
6679
7744
|
} else {
|
|
6680
7745
|
return existing.promise;
|
|
6681
7746
|
}
|
|
@@ -6689,12 +7754,13 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6689
7754
|
return await refreshAccountToken(account, client, source, {
|
|
6690
7755
|
onTokensUpdated: async () => {
|
|
6691
7756
|
try {
|
|
6692
|
-
await
|
|
7757
|
+
await getAccountManager().saveToDisk();
|
|
6693
7758
|
} catch {
|
|
6694
|
-
|
|
7759
|
+
getAccountManager().requestSaveToDisk();
|
|
6695
7760
|
throw new Error("save failed, debounced retry scheduled");
|
|
6696
7761
|
}
|
|
6697
|
-
}
|
|
7762
|
+
},
|
|
7763
|
+
debugLog
|
|
6698
7764
|
});
|
|
6699
7765
|
} finally {
|
|
6700
7766
|
if (refreshInFlight.get(key) === entry) refreshInFlight.delete(key);
|
|
@@ -6705,7 +7771,7 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6705
7771
|
return p2;
|
|
6706
7772
|
}
|
|
6707
7773
|
async function refreshIdleAccount(account) {
|
|
6708
|
-
if (!
|
|
7774
|
+
if (!getAccountManager()) return;
|
|
6709
7775
|
if (idleRefreshInFlight.has(account.id)) return;
|
|
6710
7776
|
idleRefreshInFlight.add(account.id);
|
|
6711
7777
|
const attemptedRefreshToken = account.refreshToken;
|
|
@@ -6748,6 +7814,7 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6748
7814
|
}
|
|
6749
7815
|
}
|
|
6750
7816
|
function maybeRefreshIdleAccounts(activeAccount) {
|
|
7817
|
+
const accountManager = getAccountManager();
|
|
6751
7818
|
if (!IDLE_REFRESH_ENABLED || !accountManager) return;
|
|
6752
7819
|
const now = Date.now();
|
|
6753
7820
|
const excluded = /* @__PURE__ */ new Set([activeAccount.index]);
|
|
@@ -6760,6 +7827,80 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6760
7827
|
idleRefreshLastAttempt.set(target.id, now);
|
|
6761
7828
|
void refreshIdleAccount(target);
|
|
6762
7829
|
}
|
|
7830
|
+
return {
|
|
7831
|
+
parseRefreshFailure,
|
|
7832
|
+
refreshAccountTokenSingleFlight,
|
|
7833
|
+
refreshIdleAccount,
|
|
7834
|
+
maybeRefreshIdleAccounts
|
|
7835
|
+
};
|
|
7836
|
+
}
|
|
7837
|
+
|
|
7838
|
+
// src/index.ts
|
|
7839
|
+
async function finalizeResponse(response, onUsage, onAccountError, onStreamError) {
|
|
7840
|
+
if (!isEventStreamResponse(response)) {
|
|
7841
|
+
const body = stripMcpPrefixFromJsonBody(await response.text());
|
|
7842
|
+
return new Response(body, {
|
|
7843
|
+
status: response.status,
|
|
7844
|
+
statusText: response.statusText,
|
|
7845
|
+
headers: new Headers(response.headers)
|
|
7846
|
+
});
|
|
7847
|
+
}
|
|
7848
|
+
return transformResponse(response, onUsage, onAccountError, onStreamError);
|
|
7849
|
+
}
|
|
7850
|
+
async function AnthropicAuthPlugin({
|
|
7851
|
+
client
|
|
7852
|
+
}) {
|
|
7853
|
+
const config = loadConfig();
|
|
7854
|
+
const signatureEmulationEnabled = config.signature_emulation.enabled;
|
|
7855
|
+
const promptCompactionMode = config.signature_emulation.prompt_compaction === "off" ? "off" : "minimal";
|
|
7856
|
+
const shouldFetchClaudeCodeVersion = signatureEmulationEnabled && config.signature_emulation.fetch_claude_code_version_on_startup;
|
|
7857
|
+
let accountManager = null;
|
|
7858
|
+
let lastToastedIndex = -1;
|
|
7859
|
+
let initialAccountPinned = false;
|
|
7860
|
+
const pendingSlashOAuth = /* @__PURE__ */ new Map();
|
|
7861
|
+
const fileAccountMap = /* @__PURE__ */ new Map();
|
|
7862
|
+
function debugLog(...args) {
|
|
7863
|
+
if (!config.debug) return;
|
|
7864
|
+
console.error("[opencode-anthropic-auth]", ...args);
|
|
7865
|
+
}
|
|
7866
|
+
const { toast, sendCommandMessage, runCliCommand, reloadAccountManagerFromDisk, persistOpenCodeAuth } = createPluginHelpers({
|
|
7867
|
+
client,
|
|
7868
|
+
config,
|
|
7869
|
+
debugLog,
|
|
7870
|
+
getAccountManager: () => accountManager,
|
|
7871
|
+
setAccountManager: (nextAccountManager) => {
|
|
7872
|
+
accountManager = nextAccountManager;
|
|
7873
|
+
}
|
|
7874
|
+
});
|
|
7875
|
+
const { parseRefreshFailure, refreshAccountTokenSingleFlight, maybeRefreshIdleAccounts } = createRefreshHelpers({
|
|
7876
|
+
client,
|
|
7877
|
+
config,
|
|
7878
|
+
getAccountManager: () => accountManager,
|
|
7879
|
+
debugLog
|
|
7880
|
+
});
|
|
7881
|
+
const bunFetchInstance = createBunFetch({
|
|
7882
|
+
debug: config.debug,
|
|
7883
|
+
onProxyStatus: (status) => {
|
|
7884
|
+
debugLog("bun fetch status", status);
|
|
7885
|
+
}
|
|
7886
|
+
});
|
|
7887
|
+
const fetchWithTransport = async (input, init) => {
|
|
7888
|
+
const activeFetch = globalThis.fetch;
|
|
7889
|
+
if (typeof activeFetch === "function" && activeFetch.mock) {
|
|
7890
|
+
return activeFetch(input, init);
|
|
7891
|
+
}
|
|
7892
|
+
return bunFetchInstance.fetch(input, init);
|
|
7893
|
+
};
|
|
7894
|
+
let claudeCliVersion = FALLBACK_CLAUDE_CLI_VERSION;
|
|
7895
|
+
const signatureSessionId = randomUUID2();
|
|
7896
|
+
const signatureUserId = getOrCreateSignatureUserId();
|
|
7897
|
+
if (shouldFetchClaudeCodeVersion) {
|
|
7898
|
+
fetchLatestClaudeCodeVersion().then((version) => {
|
|
7899
|
+
if (!version) return;
|
|
7900
|
+
claudeCliVersion = version;
|
|
7901
|
+
debugLog("resolved claude-code version from npm", version);
|
|
7902
|
+
}).catch((err) => debugLog("CC version fetch failed:", err.message));
|
|
7903
|
+
}
|
|
6763
7904
|
const commandDeps = {
|
|
6764
7905
|
sendCommandMessage,
|
|
6765
7906
|
get accountManager() {
|
|
@@ -6777,6 +7918,10 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6777
7918
|
refreshAccountTokenSingleFlight
|
|
6778
7919
|
};
|
|
6779
7920
|
return {
|
|
7921
|
+
dispose: async () => {
|
|
7922
|
+
await bunFetchInstance.shutdown();
|
|
7923
|
+
},
|
|
7924
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
6780
7925
|
"experimental.chat.system.transform": (input, output) => {
|
|
6781
7926
|
const prefix = CLAUDE_CODE_IDENTITY_STRING;
|
|
6782
7927
|
if (!signatureEmulationEnabled && input.model?.providerID === "anthropic") {
|
|
@@ -6784,6 +7929,7 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6784
7929
|
if (output.system[1]) output.system[1] = prefix + "\n\n" + output.system[1];
|
|
6785
7930
|
}
|
|
6786
7931
|
},
|
|
7932
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
6787
7933
|
config: async (input) => {
|
|
6788
7934
|
input.command ??= {};
|
|
6789
7935
|
input.command["anthropic"] = {
|
|
@@ -6791,6 +7937,7 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6791
7937
|
description: "Manage Anthropic auth, config, and betas (usage, login, config, set, betas, switch)"
|
|
6792
7938
|
};
|
|
6793
7939
|
},
|
|
7940
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
6794
7941
|
"command.execute.before": async (input) => {
|
|
6795
7942
|
if (input.command !== "anthropic") return;
|
|
6796
7943
|
try {
|
|
@@ -6830,8 +7977,6 @@ ${message}`);
|
|
|
6830
7977
|
const ccCount = accountManager.getCCAccounts().length;
|
|
6831
7978
|
if (ccCount > 0) {
|
|
6832
7979
|
await toast(`Using Claude Code credentials (${ccCount} found)`, "success");
|
|
6833
|
-
} else {
|
|
6834
|
-
await toast("No Claude Code credentials \u2014 using OAuth", "info");
|
|
6835
7980
|
}
|
|
6836
7981
|
}
|
|
6837
7982
|
const initialAccountEnv = process.env.OPENCODE_ANTHROPIC_INITIAL_ACCOUNT?.trim();
|
|
@@ -6862,8 +8007,17 @@ ${message}`);
|
|
|
6862
8007
|
async fetch(input, init) {
|
|
6863
8008
|
const currentAuth = await getAuth();
|
|
6864
8009
|
if (currentAuth.type !== "oauth") return fetch(input, init);
|
|
6865
|
-
const requestInit = init ?? {};
|
|
8010
|
+
const requestInit = { ...init ?? {} };
|
|
6866
8011
|
const { requestInput, requestUrl } = transformRequestUrl(input);
|
|
8012
|
+
const resolvedBody = requestInit.body !== void 0 ? requestInit.body : requestInput instanceof Request && requestInput.body ? await requestInput.clone().text() : void 0;
|
|
8013
|
+
if (resolvedBody !== void 0) {
|
|
8014
|
+
requestInit.body = resolvedBody;
|
|
8015
|
+
}
|
|
8016
|
+
const requestContext = {
|
|
8017
|
+
attempt: 0,
|
|
8018
|
+
cloneBody: typeof resolvedBody === "string" ? cloneBodyForRetry(resolvedBody) : void 0,
|
|
8019
|
+
preparedBody: void 0
|
|
8020
|
+
};
|
|
6867
8021
|
const requestMethod = String(
|
|
6868
8022
|
requestInit.method || (requestInput instanceof Request ? requestInput.method : "POST")
|
|
6869
8023
|
).toUpperCase();
|
|
@@ -6903,6 +8057,7 @@ ${message}`);
|
|
|
6903
8057
|
}
|
|
6904
8058
|
}
|
|
6905
8059
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
8060
|
+
requestContext.attempt = attempt + 1;
|
|
6906
8061
|
const account = attempt === 0 && pinnedAccount && !transientRefreshSkips.has(pinnedAccount.index) ? pinnedAccount : accountManager.getCurrentAccount(transientRefreshSkips);
|
|
6907
8062
|
if (showUsageToast && account && accountManager) {
|
|
6908
8063
|
const currentIndex = accountManager.getCurrentIndex();
|
|
@@ -6987,19 +8142,26 @@ ${message}`);
|
|
|
6987
8142
|
accessToken = account.access;
|
|
6988
8143
|
}
|
|
6989
8144
|
maybeRefreshIdleAccounts(account);
|
|
6990
|
-
const
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
8145
|
+
const buildAttemptBody = () => {
|
|
8146
|
+
const transformedBody = transformRequestBody(
|
|
8147
|
+
requestContext.cloneBody === void 0 ? void 0 : cloneBodyForRetry(requestContext.cloneBody),
|
|
8148
|
+
{
|
|
8149
|
+
enabled: signatureEmulationEnabled,
|
|
8150
|
+
claudeCliVersion,
|
|
8151
|
+
promptCompactionMode
|
|
8152
|
+
},
|
|
8153
|
+
{
|
|
8154
|
+
persistentUserId: signatureUserId,
|
|
8155
|
+
sessionId: signatureSessionId,
|
|
8156
|
+
accountId: getAccountIdentifier(account)
|
|
8157
|
+
},
|
|
8158
|
+
config.relocate_third_party_prompts,
|
|
8159
|
+
debugLog
|
|
8160
|
+
);
|
|
8161
|
+
requestContext.preparedBody = typeof transformedBody === "string" ? cloneBodyForRetry(transformedBody) : void 0;
|
|
8162
|
+
return transformedBody;
|
|
8163
|
+
};
|
|
8164
|
+
const body = buildAttemptBody();
|
|
7003
8165
|
logTransformedSystemPrompt(body);
|
|
7004
8166
|
const requestHeaders = buildRequestHeaders(input, requestInit, accessToken, body, requestUrl, {
|
|
7005
8167
|
enabled: signatureEmulationEnabled,
|
|
@@ -7035,7 +8197,7 @@ ${message}`);
|
|
|
7035
8197
|
let response;
|
|
7036
8198
|
const fetchInput = requestInput;
|
|
7037
8199
|
try {
|
|
7038
|
-
response = await
|
|
8200
|
+
response = await fetchWithTransport(fetchInput, {
|
|
7039
8201
|
...requestInit,
|
|
7040
8202
|
body,
|
|
7041
8203
|
headers: requestHeaders
|
|
@@ -7075,6 +8237,7 @@ ${message}`);
|
|
|
7075
8237
|
reason
|
|
7076
8238
|
});
|
|
7077
8239
|
accountManager.markRateLimited(account, reason, authOrPermissionIssue ? null : retryAfterMs);
|
|
8240
|
+
transientRefreshSkips.add(account.index);
|
|
7078
8241
|
const name = account.email || `Account ${accountManager.getCurrentIndex() + 1}`;
|
|
7079
8242
|
const total = accountManager.getAccountCount();
|
|
7080
8243
|
if (total > 1) {
|
|
@@ -7100,23 +8263,24 @@ ${message}`);
|
|
|
7100
8263
|
headersForRetry.set("x-stainless-retry-count", String(retryCount));
|
|
7101
8264
|
retryCount += 1;
|
|
7102
8265
|
const retryUrl = fetchInput instanceof Request ? fetchInput.url : fetchInput.toString();
|
|
7103
|
-
|
|
8266
|
+
const retryBody = requestContext.preparedBody === void 0 ? void 0 : cloneBodyForRetry(requestContext.preparedBody);
|
|
8267
|
+
return fetchWithTransport(retryUrl, {
|
|
7104
8268
|
...requestInit,
|
|
7105
|
-
body,
|
|
8269
|
+
body: retryBody,
|
|
7106
8270
|
headers: headersForRetry
|
|
7107
8271
|
});
|
|
7108
8272
|
},
|
|
7109
8273
|
{ maxRetries: 2 }
|
|
7110
8274
|
);
|
|
7111
8275
|
if (!retried.ok) {
|
|
7112
|
-
return
|
|
8276
|
+
return finalizeResponse(retried);
|
|
7113
8277
|
}
|
|
7114
8278
|
response = retried;
|
|
7115
8279
|
} else {
|
|
7116
8280
|
debugLog("non-account-specific response error, returning directly", {
|
|
7117
8281
|
status: response.status
|
|
7118
8282
|
});
|
|
7119
|
-
return
|
|
8283
|
+
return finalizeResponse(response);
|
|
7120
8284
|
}
|
|
7121
8285
|
}
|
|
7122
8286
|
if (account && accountManager && response.ok) {
|
|
@@ -7126,15 +8290,28 @@ ${message}`);
|
|
|
7126
8290
|
const usageCallback = shouldInspectStream ? (usage) => {
|
|
7127
8291
|
accountManager.recordUsage(account.index, usage);
|
|
7128
8292
|
} : null;
|
|
7129
|
-
const accountErrorCallback = shouldInspectStream ? (
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
8293
|
+
const accountErrorCallback = shouldInspectStream ? (
|
|
8294
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- reason carries opaque rate-limit metadata; shape stabilized at callsite
|
|
8295
|
+
(details) => {
|
|
8296
|
+
if (details.invalidateToken) {
|
|
8297
|
+
account.access = void 0;
|
|
8298
|
+
account.expires = void 0;
|
|
8299
|
+
markTokenStateUpdated(account);
|
|
8300
|
+
}
|
|
8301
|
+
accountManager.markRateLimited(account, details.reason, null);
|
|
8302
|
+
}
|
|
8303
|
+
) : null;
|
|
8304
|
+
const streamErrorCallback = response.ok && isEventStreamResponse(response) ? (error) => {
|
|
8305
|
+
if (!(error instanceof StreamTruncatedError)) {
|
|
8306
|
+
return;
|
|
7134
8307
|
}
|
|
7135
|
-
|
|
8308
|
+
debugLog("stream truncated during response consumption", {
|
|
8309
|
+
accountIndex: account?.index,
|
|
8310
|
+
message: error.message,
|
|
8311
|
+
context: error.context
|
|
8312
|
+
});
|
|
7136
8313
|
} : null;
|
|
7137
|
-
return
|
|
8314
|
+
return finalizeResponse(response, usageCallback, accountErrorCallback, streamErrorCallback);
|
|
7138
8315
|
}
|
|
7139
8316
|
if (lastError) throw lastError;
|
|
7140
8317
|
throw new Error("All accounts exhausted \u2014 no account could serve this request");
|
|
@@ -7164,11 +8341,38 @@ ${message}`);
|
|
|
7164
8341
|
if (!accountManager) {
|
|
7165
8342
|
accountManager = await AccountManager.load(config, null);
|
|
7166
8343
|
}
|
|
7167
|
-
const
|
|
7168
|
-
|
|
7169
|
-
|
|
8344
|
+
const identity = resolveIdentityFromCCCredential(ccCred);
|
|
8345
|
+
const existing = findByIdentity(accountManager.getCCAccounts(), identity);
|
|
8346
|
+
if (existing) {
|
|
8347
|
+
existing.refreshToken = ccCred.refreshToken;
|
|
8348
|
+
existing.identity = identity;
|
|
8349
|
+
existing.source = ccCred.source;
|
|
8350
|
+
existing.label = ccCred.label;
|
|
8351
|
+
existing.enabled = true;
|
|
8352
|
+
if (ccCred.accessToken) {
|
|
8353
|
+
existing.access = ccCred.accessToken;
|
|
8354
|
+
}
|
|
8355
|
+
if (ccCred.expiresAt >= (existing.expires ?? 0)) {
|
|
8356
|
+
existing.expires = ccCred.expiresAt;
|
|
8357
|
+
}
|
|
8358
|
+
existing.tokenUpdatedAt = Math.max(existing.tokenUpdatedAt || 0, ccCred.expiresAt || 0);
|
|
8359
|
+
await accountManager.saveToDisk();
|
|
8360
|
+
} else {
|
|
8361
|
+
const added = accountManager.addAccount(
|
|
8362
|
+
ccCred.refreshToken,
|
|
8363
|
+
ccCred.accessToken,
|
|
8364
|
+
ccCred.expiresAt,
|
|
8365
|
+
void 0,
|
|
8366
|
+
{
|
|
8367
|
+
identity,
|
|
8368
|
+
label: ccCred.label,
|
|
8369
|
+
source: ccCred.source
|
|
8370
|
+
}
|
|
8371
|
+
);
|
|
7170
8372
|
if (added) {
|
|
7171
8373
|
added.source = ccCred.source;
|
|
8374
|
+
added.label = ccCred.label;
|
|
8375
|
+
added.identity = identity;
|
|
7172
8376
|
}
|
|
7173
8377
|
await accountManager.saveToDisk();
|
|
7174
8378
|
await toast("Added Claude Code credentials", "success");
|
|
@@ -7229,17 +8433,35 @@ ${message}`);
|
|
|
7229
8433
|
if (!accountManager) {
|
|
7230
8434
|
accountManager = await AccountManager.load(config, null);
|
|
7231
8435
|
}
|
|
8436
|
+
const identity = resolveIdentityFromOAuthExchange(credentials);
|
|
7232
8437
|
const countBefore = accountManager.getAccountCount();
|
|
7233
|
-
accountManager.
|
|
7234
|
-
|
|
7235
|
-
credentials.
|
|
7236
|
-
credentials.
|
|
7237
|
-
credentials.
|
|
7238
|
-
|
|
8438
|
+
const existing = identity.kind === "oauth" ? findByIdentity(accountManager.getOAuthAccounts(), identity) : null;
|
|
8439
|
+
if (existing) {
|
|
8440
|
+
existing.refreshToken = credentials.refresh;
|
|
8441
|
+
existing.access = credentials.access;
|
|
8442
|
+
existing.expires = credentials.expires;
|
|
8443
|
+
existing.email = credentials.email ?? existing.email;
|
|
8444
|
+
existing.identity = identity;
|
|
8445
|
+
existing.source = "oauth";
|
|
8446
|
+
existing.enabled = true;
|
|
8447
|
+
existing.tokenUpdatedAt = Date.now();
|
|
8448
|
+
} else {
|
|
8449
|
+
accountManager.addAccount(
|
|
8450
|
+
credentials.refresh,
|
|
8451
|
+
credentials.access,
|
|
8452
|
+
credentials.expires,
|
|
8453
|
+
credentials.email,
|
|
8454
|
+
{
|
|
8455
|
+
identity,
|
|
8456
|
+
source: "oauth"
|
|
8457
|
+
}
|
|
8458
|
+
);
|
|
8459
|
+
}
|
|
7239
8460
|
await accountManager.saveToDisk();
|
|
7240
8461
|
const total = accountManager.getAccountCount();
|
|
7241
8462
|
const name = credentials.email || "account";
|
|
7242
|
-
if (
|
|
8463
|
+
if (existing) await toast(`Re-authenticated (${name})`, "success");
|
|
8464
|
+
else if (countBefore > 0) await toast(`Added ${name} \u2014 ${total} accounts`, "success");
|
|
7243
8465
|
else await toast(`Authenticated (${name})`, "success");
|
|
7244
8466
|
return credentials;
|
|
7245
8467
|
}
|