@vacbo/opencode-anthropic-fix 0.0.45 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +1801 -613
- package/package.json +4 -4
- 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 -191
- 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 +11 -5
- 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
|
@@ -4,16 +4,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __getProtoOf = Object.getPrototypeOf;
|
|
6
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
-
}) : x)(function(x) {
|
|
10
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
-
});
|
|
13
7
|
var __esm = (fn, res) => function __init() {
|
|
14
8
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
9
|
};
|
|
16
|
-
var __commonJS = (cb, mod) => function
|
|
10
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
17
11
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
18
12
|
};
|
|
19
13
|
var __export = (target, all) => {
|
|
@@ -37,6 +31,84 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
37
31
|
mod
|
|
38
32
|
));
|
|
39
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
|
+
|
|
40
112
|
// src/config.ts
|
|
41
113
|
import { existsSync, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
|
|
42
114
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -410,6 +482,8 @@ function validateAccount(raw, now) {
|
|
|
410
482
|
return {
|
|
411
483
|
id,
|
|
412
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,
|
|
413
487
|
refreshToken: acc.refreshToken,
|
|
414
488
|
access: typeof acc.access === "string" ? acc.access : void 0,
|
|
415
489
|
expires: typeof acc.expires === "number" && Number.isFinite(acc.expires) ? acc.expires : void 0,
|
|
@@ -425,6 +499,106 @@ function validateAccount(raw, now) {
|
|
|
425
499
|
source: acc.source === "cc-keychain" || acc.source === "cc-file" || acc.source === "oauth" ? acc.source : void 0
|
|
426
500
|
};
|
|
427
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
|
+
}
|
|
428
602
|
async function loadAccounts() {
|
|
429
603
|
const storagePath = getStoragePath();
|
|
430
604
|
try {
|
|
@@ -434,7 +608,9 @@ async function loadAccounts() {
|
|
|
434
608
|
return null;
|
|
435
609
|
}
|
|
436
610
|
if (data.version !== CURRENT_VERSION) {
|
|
437
|
-
|
|
611
|
+
console.warn(
|
|
612
|
+
`Storage version mismatch: ${String(data.version)} vs ${CURRENT_VERSION}. Attempting best-effort migration.`
|
|
613
|
+
);
|
|
438
614
|
}
|
|
439
615
|
const now = Date.now();
|
|
440
616
|
const accounts = data.accounts.map((raw) => validateAccount(raw, now)).filter((acc) => acc !== null);
|
|
@@ -461,53 +637,13 @@ async function saveAccounts(storage) {
|
|
|
461
637
|
const configDir = dirname2(storagePath);
|
|
462
638
|
await fs.mkdir(configDir, { recursive: true });
|
|
463
639
|
ensureGitignore(configDir);
|
|
464
|
-
let storageToWrite =
|
|
640
|
+
let storageToWrite = {
|
|
641
|
+
...storage,
|
|
642
|
+
activeIndex: clampActiveIndex(storage.accounts, storage.activeIndex)
|
|
643
|
+
};
|
|
465
644
|
try {
|
|
466
645
|
const disk = await loadAccounts();
|
|
467
|
-
|
|
468
|
-
const diskById = new Map(disk.accounts.map((a) => [a.id, a]));
|
|
469
|
-
const diskByAddedAt = /* @__PURE__ */ new Map();
|
|
470
|
-
const diskByToken = new Map(disk.accounts.map((a) => [a.refreshToken, a]));
|
|
471
|
-
for (const d3 of disk.accounts) {
|
|
472
|
-
const bucket = diskByAddedAt.get(d3.addedAt) || [];
|
|
473
|
-
bucket.push(d3);
|
|
474
|
-
diskByAddedAt.set(d3.addedAt, bucket);
|
|
475
|
-
}
|
|
476
|
-
const findDiskMatch = (acc) => {
|
|
477
|
-
const byId = diskById.get(acc.id);
|
|
478
|
-
if (byId) return byId;
|
|
479
|
-
const byAddedAt = diskByAddedAt.get(acc.addedAt);
|
|
480
|
-
if (byAddedAt?.length === 1) return byAddedAt[0];
|
|
481
|
-
const byToken = diskByToken.get(acc.refreshToken);
|
|
482
|
-
if (byToken) return byToken;
|
|
483
|
-
if (byAddedAt && byAddedAt.length > 0) return byAddedAt[0];
|
|
484
|
-
return null;
|
|
485
|
-
};
|
|
486
|
-
const mergedAccounts = storage.accounts.map((acc) => {
|
|
487
|
-
const diskAcc = findDiskMatch(acc);
|
|
488
|
-
const memTs = typeof acc.token_updated_at === "number" && Number.isFinite(acc.token_updated_at) ? acc.token_updated_at : acc.addedAt;
|
|
489
|
-
const diskTs = diskAcc?.token_updated_at || 0;
|
|
490
|
-
const useDiskAuth = !!diskAcc && diskTs > memTs;
|
|
491
|
-
return {
|
|
492
|
-
...acc,
|
|
493
|
-
refreshToken: useDiskAuth ? diskAcc.refreshToken : acc.refreshToken,
|
|
494
|
-
access: useDiskAuth ? diskAcc.access : acc.access,
|
|
495
|
-
expires: useDiskAuth ? diskAcc.expires : acc.expires,
|
|
496
|
-
token_updated_at: useDiskAuth ? diskTs : memTs
|
|
497
|
-
};
|
|
498
|
-
});
|
|
499
|
-
let activeIndex = storage.activeIndex;
|
|
500
|
-
if (mergedAccounts.length > 0) {
|
|
501
|
-
activeIndex = Math.max(0, Math.min(activeIndex, mergedAccounts.length - 1));
|
|
502
|
-
} else {
|
|
503
|
-
activeIndex = 0;
|
|
504
|
-
}
|
|
505
|
-
storageToWrite = {
|
|
506
|
-
...storage,
|
|
507
|
-
accounts: mergedAccounts,
|
|
508
|
-
activeIndex
|
|
509
|
-
};
|
|
510
|
-
}
|
|
646
|
+
storageToWrite = unionAccountsWithDisk(storageToWrite, disk);
|
|
511
647
|
} catch {
|
|
512
648
|
}
|
|
513
649
|
const tempPath = `${storagePath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
@@ -536,6 +672,7 @@ var CURRENT_VERSION, GITIGNORE_ENTRIES;
|
|
|
536
672
|
var init_storage = __esm({
|
|
537
673
|
"src/storage.ts"() {
|
|
538
674
|
"use strict";
|
|
675
|
+
init_account_identity();
|
|
539
676
|
init_config();
|
|
540
677
|
CURRENT_VERSION = 1;
|
|
541
678
|
GITIGNORE_ENTRIES = [".gitignore", "anthropic-accounts.json", "anthropic-accounts.json.*.tmp"];
|
|
@@ -1950,14 +2087,20 @@ async function cmdLogin() {
|
|
|
1950
2087
|
const credentials = await runOAuthFlow();
|
|
1951
2088
|
if (!credentials) return 1;
|
|
1952
2089
|
const storage = stored || { version: 1, accounts: [], activeIndex: 0 };
|
|
1953
|
-
const
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
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;
|
|
1959
2102
|
await saveAccounts(storage);
|
|
1960
|
-
const label2 = credentials.email || `Account ${existingIdx + 1}`;
|
|
2103
|
+
const label2 = credentials.email || existing.email || `Account ${existingIdx + 1}`;
|
|
1961
2104
|
O2.success(`Updated existing account #${existingIdx + 1} (${label2}).`);
|
|
1962
2105
|
return 0;
|
|
1963
2106
|
}
|
|
@@ -1969,6 +2112,7 @@ async function cmdLogin() {
|
|
|
1969
2112
|
storage.accounts.push({
|
|
1970
2113
|
id: `${now}:${credentials.refresh.slice(0, 12)}`,
|
|
1971
2114
|
email: credentials.email,
|
|
2115
|
+
identity,
|
|
1972
2116
|
refreshToken: credentials.refresh,
|
|
1973
2117
|
access: credentials.access,
|
|
1974
2118
|
expires: credentials.expires,
|
|
@@ -1979,7 +2123,8 @@ async function cmdLogin() {
|
|
|
1979
2123
|
rateLimitResetTimes: {},
|
|
1980
2124
|
consecutiveFailures: 0,
|
|
1981
2125
|
lastFailureTime: null,
|
|
1982
|
-
stats: createDefaultStats(now)
|
|
2126
|
+
stats: createDefaultStats(now),
|
|
2127
|
+
source: "oauth"
|
|
1983
2128
|
});
|
|
1984
2129
|
await saveAccounts(storage);
|
|
1985
2130
|
const label = credentials.email || `Account ${storage.accounts.length}`;
|
|
@@ -2177,7 +2322,8 @@ async function cmdList() {
|
|
|
2177
2322
|
}
|
|
2178
2323
|
}
|
|
2179
2324
|
if (anyRefreshed) {
|
|
2180
|
-
await saveAccounts(stored).catch(() => {
|
|
2325
|
+
await saveAccounts(stored).catch((err) => {
|
|
2326
|
+
console.error("[opencode-anthropic-auth] failed to persist refreshed tokens:", err);
|
|
2181
2327
|
});
|
|
2182
2328
|
}
|
|
2183
2329
|
O2.message(c2.bold("Anthropic Multi-Account Status"));
|
|
@@ -3173,6 +3319,7 @@ var USE_COLOR, ansi, c2, QUOTA_BUCKETS, USAGE_INDENT, USAGE_LABEL_WIDTH, ioConte
|
|
|
3173
3319
|
var init_cli = __esm({
|
|
3174
3320
|
async "src/cli.ts"() {
|
|
3175
3321
|
"use strict";
|
|
3322
|
+
init_account_identity();
|
|
3176
3323
|
init_config();
|
|
3177
3324
|
init_oauth();
|
|
3178
3325
|
init_storage();
|
|
@@ -3458,6 +3605,9 @@ function readCCCredentials() {
|
|
|
3458
3605
|
return credentials;
|
|
3459
3606
|
}
|
|
3460
3607
|
|
|
3608
|
+
// src/accounts.ts
|
|
3609
|
+
init_account_identity();
|
|
3610
|
+
|
|
3461
3611
|
// src/rotation.ts
|
|
3462
3612
|
init_config();
|
|
3463
3613
|
var HealthScoreTracker = class {
|
|
@@ -3643,6 +3793,111 @@ function selectAccount(candidates, strategy, currentIndex, healthTracker, tokenT
|
|
|
3643
3793
|
init_storage();
|
|
3644
3794
|
var MAX_ACCOUNTS = 10;
|
|
3645
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
|
+
}
|
|
3646
3901
|
var AccountManager = class _AccountManager {
|
|
3647
3902
|
#accounts = [];
|
|
3648
3903
|
#cursor = 0;
|
|
@@ -3652,11 +3907,22 @@ var AccountManager = class _AccountManager {
|
|
|
3652
3907
|
#config;
|
|
3653
3908
|
#saveTimeout = null;
|
|
3654
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;
|
|
3655
3917
|
constructor(config) {
|
|
3656
3918
|
this.#config = config;
|
|
3657
3919
|
this.#healthTracker = new HealthScoreTracker(config.health_score);
|
|
3658
3920
|
this.#tokenTracker = new TokenBucketTracker(config.token_bucket);
|
|
3659
3921
|
}
|
|
3922
|
+
#rebuildTrackers() {
|
|
3923
|
+
this.#healthTracker = new HealthScoreTracker(this.#config.health_score);
|
|
3924
|
+
this.#tokenTracker = new TokenBucketTracker(this.#config.token_bucket);
|
|
3925
|
+
}
|
|
3660
3926
|
/**
|
|
3661
3927
|
* Load accounts from disk, optionally merging with an OpenCode auth fallback.
|
|
3662
3928
|
*/
|
|
@@ -3664,27 +3930,38 @@ var AccountManager = class _AccountManager {
|
|
|
3664
3930
|
const manager = new _AccountManager(config);
|
|
3665
3931
|
const stored = await loadAccounts();
|
|
3666
3932
|
if (stored) {
|
|
3667
|
-
manager.#accounts = stored.accounts.map(
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
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
|
+
);
|
|
3685
3955
|
manager.#currentIndex = manager.#accounts.length > 0 ? Math.min(stored.activeIndex, manager.#accounts.length - 1) : -1;
|
|
3686
3956
|
if (authFallback && manager.#accounts.length > 0) {
|
|
3687
|
-
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
|
+
});
|
|
3688
3965
|
if (match) {
|
|
3689
3966
|
const fallbackHasAccess = typeof authFallback.access === "string" && authFallback.access.length > 0;
|
|
3690
3967
|
const fallbackExpires = typeof authFallback.expires === "number" ? authFallback.expires : 0;
|
|
@@ -3701,23 +3978,17 @@ var AccountManager = class _AccountManager {
|
|
|
3701
3978
|
} else if (authFallback && authFallback.refresh) {
|
|
3702
3979
|
const now = Date.now();
|
|
3703
3980
|
manager.#accounts = [
|
|
3704
|
-
{
|
|
3981
|
+
createManagedAccount({
|
|
3705
3982
|
id: `${now}:${authFallback.refresh.slice(0, 12)}`,
|
|
3706
3983
|
index: 0,
|
|
3707
|
-
email: void 0,
|
|
3708
3984
|
refreshToken: authFallback.refresh,
|
|
3709
3985
|
access: authFallback.access,
|
|
3710
3986
|
expires: authFallback.expires,
|
|
3711
3987
|
tokenUpdatedAt: now,
|
|
3712
3988
|
addedAt: now,
|
|
3713
|
-
lastUsed: 0,
|
|
3714
|
-
enabled: true,
|
|
3715
|
-
rateLimitResetTimes: {},
|
|
3716
|
-
consecutiveFailures: 0,
|
|
3717
|
-
lastFailureTime: null,
|
|
3718
3989
|
lastSwitchReason: "initial",
|
|
3719
|
-
|
|
3720
|
-
}
|
|
3990
|
+
source: "oauth"
|
|
3991
|
+
})
|
|
3721
3992
|
];
|
|
3722
3993
|
manager.#currentIndex = 0;
|
|
3723
3994
|
}
|
|
@@ -3731,49 +4002,60 @@ var AccountManager = class _AccountManager {
|
|
|
3731
4002
|
}
|
|
3732
4003
|
})();
|
|
3733
4004
|
for (const ccCredential of ccCredentials) {
|
|
3734
|
-
const
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
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];
|
|
3740
4016
|
}
|
|
3741
|
-
|
|
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) {
|
|
3742
4025
|
existingMatch.access = ccCredential.accessToken;
|
|
4026
|
+
}
|
|
4027
|
+
if (ccCredential.expiresAt >= (existingMatch.expires ?? 0)) {
|
|
3743
4028
|
existingMatch.expires = ccCredential.expiresAt;
|
|
3744
4029
|
}
|
|
4030
|
+
existingMatch.tokenUpdatedAt = Math.max(existingMatch.tokenUpdatedAt || 0, ccCredential.expiresAt || 0);
|
|
4031
|
+
continue;
|
|
4032
|
+
}
|
|
4033
|
+
if (manager.#accounts.length >= MAX_ACCOUNTS) {
|
|
3745
4034
|
continue;
|
|
3746
4035
|
}
|
|
3747
4036
|
const emailCollision = manager.getOAuthAccounts().find((account) => account.email && ccCredential.label.includes(account.email));
|
|
3748
4037
|
if (emailCollision?.email) {
|
|
3749
4038
|
}
|
|
3750
4039
|
const now = Date.now();
|
|
3751
|
-
const ccAccount = {
|
|
4040
|
+
const ccAccount = createManagedAccount({
|
|
3752
4041
|
id: `cc-${ccCredential.source}-${now}:${ccCredential.refreshToken.slice(0, 12)}`,
|
|
3753
4042
|
index: manager.#accounts.length,
|
|
3754
|
-
email: void 0,
|
|
3755
4043
|
refreshToken: ccCredential.refreshToken,
|
|
3756
4044
|
access: ccCredential.accessToken,
|
|
3757
4045
|
expires: ccCredential.expiresAt,
|
|
3758
4046
|
tokenUpdatedAt: now,
|
|
3759
4047
|
addedAt: now,
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
rateLimitResetTimes: {},
|
|
3763
|
-
consecutiveFailures: 0,
|
|
3764
|
-
lastFailureTime: null,
|
|
4048
|
+
identity: ccIdentity,
|
|
4049
|
+
label: ccCredential.label,
|
|
3765
4050
|
lastSwitchReason: "cc-auto-detected",
|
|
3766
|
-
stats: createDefaultStats(now),
|
|
3767
4051
|
source: ccCredential.source
|
|
3768
|
-
};
|
|
4052
|
+
});
|
|
3769
4053
|
manager.#accounts.push(ccAccount);
|
|
3770
4054
|
}
|
|
3771
4055
|
if (config.cc_credential_reuse.prefer_over_oauth && manager.getCCAccounts().length > 0) {
|
|
3772
4056
|
manager.#accounts = [...manager.getCCAccounts(), ...manager.getOAuthAccounts()];
|
|
3773
4057
|
}
|
|
3774
|
-
manager.#accounts
|
|
3775
|
-
account.index = index;
|
|
3776
|
-
});
|
|
4058
|
+
reindexAccounts(manager.#accounts);
|
|
3777
4059
|
if (config.cc_credential_reuse.prefer_over_oauth && manager.getCCAccounts().length > 0) {
|
|
3778
4060
|
manager.#currentIndex = 0;
|
|
3779
4061
|
} else if (currentAccountId) {
|
|
@@ -3913,35 +4195,47 @@ var AccountManager = class _AccountManager {
|
|
|
3913
4195
|
* Add a new account to the pool.
|
|
3914
4196
|
* @returns The new account, or null if at capacity
|
|
3915
4197
|
*/
|
|
3916
|
-
addAccount(refreshToken2, accessToken, expires, email) {
|
|
3917
|
-
|
|
3918
|
-
|
|
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
|
+
});
|
|
3919
4210
|
if (existing) {
|
|
4211
|
+
existing.refreshToken = refreshToken2;
|
|
3920
4212
|
existing.access = accessToken;
|
|
3921
4213
|
existing.expires = expires;
|
|
3922
4214
|
existing.tokenUpdatedAt = Date.now();
|
|
3923
|
-
|
|
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";
|
|
3924
4219
|
existing.enabled = true;
|
|
4220
|
+
this.requestSaveToDisk();
|
|
3925
4221
|
return existing;
|
|
3926
4222
|
}
|
|
4223
|
+
if (this.#accounts.length >= MAX_ACCOUNTS) return null;
|
|
3927
4224
|
const now = Date.now();
|
|
3928
|
-
const account = {
|
|
4225
|
+
const account = createManagedAccount({
|
|
3929
4226
|
id: `${now}:${refreshToken2.slice(0, 12)}`,
|
|
3930
4227
|
index: this.#accounts.length,
|
|
3931
|
-
email,
|
|
3932
4228
|
refreshToken: refreshToken2,
|
|
3933
4229
|
access: accessToken,
|
|
3934
4230
|
expires,
|
|
3935
4231
|
tokenUpdatedAt: now,
|
|
3936
4232
|
addedAt: now,
|
|
3937
|
-
lastUsed: 0,
|
|
3938
|
-
enabled: true,
|
|
3939
|
-
rateLimitResetTimes: {},
|
|
3940
|
-
consecutiveFailures: 0,
|
|
3941
|
-
lastFailureTime: null,
|
|
3942
4233
|
lastSwitchReason: "initial",
|
|
3943
|
-
|
|
3944
|
-
|
|
4234
|
+
email,
|
|
4235
|
+
identity,
|
|
4236
|
+
label: options?.label,
|
|
4237
|
+
source: options?.source ?? "oauth"
|
|
4238
|
+
});
|
|
3945
4239
|
this.#accounts.push(account);
|
|
3946
4240
|
if (this.#accounts.length === 1) {
|
|
3947
4241
|
this.#currentIndex = 0;
|
|
@@ -3955,9 +4249,7 @@ var AccountManager = class _AccountManager {
|
|
|
3955
4249
|
removeAccount(index) {
|
|
3956
4250
|
if (index < 0 || index >= this.#accounts.length) return false;
|
|
3957
4251
|
this.#accounts.splice(index, 1);
|
|
3958
|
-
this.#accounts
|
|
3959
|
-
acc.index = i;
|
|
3960
|
-
});
|
|
4252
|
+
reindexAccounts(this.#accounts);
|
|
3961
4253
|
if (this.#accounts.length === 0) {
|
|
3962
4254
|
this.#currentIndex = -1;
|
|
3963
4255
|
this.#cursor = 0;
|
|
@@ -3969,9 +4261,7 @@ var AccountManager = class _AccountManager {
|
|
|
3969
4261
|
this.#cursor = Math.min(this.#cursor, this.#accounts.length);
|
|
3970
4262
|
}
|
|
3971
4263
|
}
|
|
3972
|
-
|
|
3973
|
-
this.#healthTracker.reset(i);
|
|
3974
|
-
}
|
|
4264
|
+
this.#rebuildTrackers();
|
|
3975
4265
|
this.requestSaveToDisk();
|
|
3976
4266
|
return true;
|
|
3977
4267
|
}
|
|
@@ -4001,7 +4291,10 @@ var AccountManager = class _AccountManager {
|
|
|
4001
4291
|
if (this.#saveTimeout) clearTimeout(this.#saveTimeout);
|
|
4002
4292
|
this.#saveTimeout = setTimeout(() => {
|
|
4003
4293
|
this.#saveTimeout = null;
|
|
4004
|
-
this.saveToDisk().catch(() => {
|
|
4294
|
+
this.saveToDisk().catch((err) => {
|
|
4295
|
+
if (this.#config.debug) {
|
|
4296
|
+
console.error("[opencode-anthropic-auth] saveToDisk failed:", err.message);
|
|
4297
|
+
}
|
|
4005
4298
|
});
|
|
4006
4299
|
}, 1e3);
|
|
4007
4300
|
}
|
|
@@ -4014,9 +4307,11 @@ var AccountManager = class _AccountManager {
|
|
|
4014
4307
|
let diskAccountsById = null;
|
|
4015
4308
|
let diskAccountsByAddedAt = null;
|
|
4016
4309
|
let diskAccountsByRefreshToken = null;
|
|
4310
|
+
let diskAccounts = [];
|
|
4017
4311
|
try {
|
|
4018
4312
|
const diskData = await loadAccounts();
|
|
4019
4313
|
if (diskData) {
|
|
4314
|
+
diskAccounts = diskData.accounts;
|
|
4020
4315
|
diskAccountsById = new Map(diskData.accounts.map((a) => [a.id, a]));
|
|
4021
4316
|
diskAccountsByAddedAt = /* @__PURE__ */ new Map();
|
|
4022
4317
|
diskAccountsByRefreshToken = /* @__PURE__ */ new Map();
|
|
@@ -4032,6 +4327,8 @@ var AccountManager = class _AccountManager {
|
|
|
4032
4327
|
const findDiskAccount = (account) => {
|
|
4033
4328
|
const byId = diskAccountsById?.get(account.id);
|
|
4034
4329
|
if (byId) return byId;
|
|
4330
|
+
const byIdentity = findByIdentity(diskAccounts, resolveIdentity(account));
|
|
4331
|
+
if (byIdentity) return byIdentity;
|
|
4035
4332
|
const byAddedAt = diskAccountsByAddedAt?.get(account.addedAt);
|
|
4036
4333
|
if (byAddedAt?.length === 1) return byAddedAt[0];
|
|
4037
4334
|
const byToken = diskAccountsByRefreshToken?.get(account.refreshToken);
|
|
@@ -4039,70 +4336,82 @@ var AccountManager = class _AccountManager {
|
|
|
4039
4336
|
if (byAddedAt && byAddedAt.length > 0) return byAddedAt[0];
|
|
4040
4337
|
return null;
|
|
4041
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;
|
|
4042
4411
|
const storage = {
|
|
4043
4412
|
version: 1,
|
|
4044
|
-
accounts:
|
|
4045
|
-
|
|
4046
|
-
let mergedStats = acc.stats;
|
|
4047
|
-
const diskAcc = findDiskAccount(acc);
|
|
4048
|
-
if (delta) {
|
|
4049
|
-
const diskStats = diskAcc?.stats;
|
|
4050
|
-
if (delta.isReset) {
|
|
4051
|
-
mergedStats = {
|
|
4052
|
-
requests: delta.requests,
|
|
4053
|
-
inputTokens: delta.inputTokens,
|
|
4054
|
-
outputTokens: delta.outputTokens,
|
|
4055
|
-
cacheReadTokens: delta.cacheReadTokens,
|
|
4056
|
-
cacheWriteTokens: delta.cacheWriteTokens,
|
|
4057
|
-
lastReset: delta.resetTimestamp ?? acc.stats.lastReset
|
|
4058
|
-
};
|
|
4059
|
-
} else if (diskStats) {
|
|
4060
|
-
mergedStats = {
|
|
4061
|
-
requests: diskStats.requests + delta.requests,
|
|
4062
|
-
inputTokens: diskStats.inputTokens + delta.inputTokens,
|
|
4063
|
-
outputTokens: diskStats.outputTokens + delta.outputTokens,
|
|
4064
|
-
cacheReadTokens: diskStats.cacheReadTokens + delta.cacheReadTokens,
|
|
4065
|
-
cacheWriteTokens: diskStats.cacheWriteTokens + delta.cacheWriteTokens,
|
|
4066
|
-
lastReset: diskStats.lastReset
|
|
4067
|
-
};
|
|
4068
|
-
}
|
|
4069
|
-
}
|
|
4070
|
-
const memTokenUpdatedAt = acc.tokenUpdatedAt || 0;
|
|
4071
|
-
const diskTokenUpdatedAt = diskAcc?.token_updated_at || 0;
|
|
4072
|
-
const freshestAuth = diskAcc && diskTokenUpdatedAt > memTokenUpdatedAt ? {
|
|
4073
|
-
refreshToken: diskAcc.refreshToken,
|
|
4074
|
-
access: diskAcc.access,
|
|
4075
|
-
expires: diskAcc.expires,
|
|
4076
|
-
tokenUpdatedAt: diskTokenUpdatedAt
|
|
4077
|
-
} : {
|
|
4078
|
-
refreshToken: acc.refreshToken,
|
|
4079
|
-
access: acc.access,
|
|
4080
|
-
expires: acc.expires,
|
|
4081
|
-
tokenUpdatedAt: memTokenUpdatedAt
|
|
4082
|
-
};
|
|
4083
|
-
acc.refreshToken = freshestAuth.refreshToken;
|
|
4084
|
-
acc.access = freshestAuth.access;
|
|
4085
|
-
acc.expires = freshestAuth.expires;
|
|
4086
|
-
acc.tokenUpdatedAt = freshestAuth.tokenUpdatedAt;
|
|
4087
|
-
return {
|
|
4088
|
-
id: acc.id,
|
|
4089
|
-
email: acc.email,
|
|
4090
|
-
refreshToken: freshestAuth.refreshToken,
|
|
4091
|
-
access: freshestAuth.access,
|
|
4092
|
-
expires: freshestAuth.expires,
|
|
4093
|
-
token_updated_at: freshestAuth.tokenUpdatedAt,
|
|
4094
|
-
addedAt: acc.addedAt,
|
|
4095
|
-
lastUsed: acc.lastUsed,
|
|
4096
|
-
enabled: acc.enabled,
|
|
4097
|
-
rateLimitResetTimes: Object.keys(acc.rateLimitResetTimes).length > 0 ? acc.rateLimitResetTimes : {},
|
|
4098
|
-
consecutiveFailures: acc.consecutiveFailures,
|
|
4099
|
-
lastFailureTime: acc.lastFailureTime,
|
|
4100
|
-
lastSwitchReason: acc.lastSwitchReason,
|
|
4101
|
-
stats: mergedStats,
|
|
4102
|
-
source: acc.source
|
|
4103
|
-
};
|
|
4104
|
-
}),
|
|
4105
|
-
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
|
|
4106
4415
|
};
|
|
4107
4416
|
await saveAccounts(storage);
|
|
4108
4417
|
this.#statsDeltas.clear();
|
|
@@ -4119,57 +4428,92 @@ var AccountManager = class _AccountManager {
|
|
|
4119
4428
|
async syncActiveIndexFromDisk() {
|
|
4120
4429
|
const stored = await loadAccounts();
|
|
4121
4430
|
if (!stored) return;
|
|
4122
|
-
const
|
|
4123
|
-
const
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
const existingByToken = new Map(this.#accounts.map((acc) => [acc.refreshToken, acc]));
|
|
4131
|
-
this.#accounts = stored.accounts.map((acc, index) => {
|
|
4132
|
-
const existing = acc.id && existingById.get(acc.id) || (!acc.id ? existingByToken.get(acc.refreshToken) : null);
|
|
4133
|
-
return {
|
|
4134
|
-
id: acc.id || existing?.id || `${acc.addedAt}:${acc.refreshToken.slice(0, 12)}`,
|
|
4135
|
-
index,
|
|
4136
|
-
email: acc.email ?? existing?.email,
|
|
4137
|
-
refreshToken: acc.refreshToken,
|
|
4138
|
-
access: acc.access ?? existing?.access,
|
|
4139
|
-
expires: acc.expires ?? existing?.expires,
|
|
4140
|
-
tokenUpdatedAt: acc.token_updated_at ?? existing?.tokenUpdatedAt ?? acc.addedAt,
|
|
4141
|
-
addedAt: acc.addedAt,
|
|
4142
|
-
lastUsed: acc.lastUsed,
|
|
4143
|
-
enabled: acc.enabled,
|
|
4144
|
-
rateLimitResetTimes: acc.rateLimitResetTimes,
|
|
4145
|
-
consecutiveFailures: acc.consecutiveFailures,
|
|
4146
|
-
lastFailureTime: acc.lastFailureTime,
|
|
4147
|
-
lastSwitchReason: acc.lastSwitchReason || existing?.lastSwitchReason || "initial",
|
|
4148
|
-
stats: acc.stats ?? existing?.stats ?? createDefaultStats()
|
|
4149
|
-
};
|
|
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
|
|
4150
4439
|
});
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
}
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
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;
|
|
4161
4473
|
}
|
|
4474
|
+
if (account.enabled) {
|
|
4475
|
+
account.enabled = false;
|
|
4476
|
+
structuralChange = true;
|
|
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;
|
|
4162
4497
|
}
|
|
4163
|
-
const diskIndex = Math.min(stored.activeIndex,
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
if (!
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
this.#
|
|
4170
|
-
this.#cursor = diskIndex;
|
|
4171
|
-
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;
|
|
4172
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);
|
|
4173
4517
|
}
|
|
4174
4518
|
}
|
|
4175
4519
|
/**
|
|
@@ -4195,6 +4539,13 @@ var AccountManager = class _AccountManager {
|
|
|
4195
4539
|
delta.cacheReadTokens += crTok;
|
|
4196
4540
|
delta.cacheWriteTokens += cwTok;
|
|
4197
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
|
+
}
|
|
4198
4549
|
this.#statsDeltas.set(account.id, {
|
|
4199
4550
|
requests: 1,
|
|
4200
4551
|
inputTokens: inTok,
|
|
@@ -4727,6 +5078,14 @@ async function completeSlashOAuth(sessionID, code, deps) {
|
|
|
4727
5078
|
|
|
4728
5079
|
// src/commands/router.ts
|
|
4729
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
|
+
}
|
|
4730
5089
|
function stripAnsi(value) {
|
|
4731
5090
|
return value.replace(/\x1b\[[0-9;]*m/g, "");
|
|
4732
5091
|
}
|
|
@@ -5075,7 +5434,7 @@ HTTP ${res.status}: ${errBody}`
|
|
|
5075
5434
|
}
|
|
5076
5435
|
const data = await res.json();
|
|
5077
5436
|
const files = data.data || [];
|
|
5078
|
-
for (const f of files) fileAccountMap
|
|
5437
|
+
for (const f of files) capFileAccountMap(fileAccountMap, f.id, account2.index);
|
|
5079
5438
|
if (files.length === 0) {
|
|
5080
5439
|
await sendCommandMessage(input.sessionID, `\u25A3 Anthropic Files [${label2}]
|
|
5081
5440
|
|
|
@@ -5105,7 +5464,7 @@ No files uploaded.`);
|
|
|
5105
5464
|
}
|
|
5106
5465
|
const data = await res.json();
|
|
5107
5466
|
const files = data.data || [];
|
|
5108
|
-
for (const f of files) fileAccountMap
|
|
5467
|
+
for (const f of files) capFileAccountMap(fileAccountMap, f.id, acct.index);
|
|
5109
5468
|
totalFiles += files.length;
|
|
5110
5469
|
if (files.length === 0) {
|
|
5111
5470
|
allLines.push(`[${label2}] No files`);
|
|
@@ -5185,7 +5544,7 @@ Upload failed (HTTP ${res.status}): ${errBody}`
|
|
|
5185
5544
|
}
|
|
5186
5545
|
const file = await res.json();
|
|
5187
5546
|
const sizeKB = ((file.size || 0) / 1024).toFixed(1);
|
|
5188
|
-
fileAccountMap
|
|
5547
|
+
capFileAccountMap(fileAccountMap, file.id, account.index);
|
|
5189
5548
|
await sendCommandMessage(
|
|
5190
5549
|
input.sessionID,
|
|
5191
5550
|
`\u25A3 Anthropic Files [${label}]
|
|
@@ -5217,7 +5576,7 @@ HTTP ${res.status}: ${errBody}`
|
|
|
5217
5576
|
return;
|
|
5218
5577
|
}
|
|
5219
5578
|
const file = await res.json();
|
|
5220
|
-
fileAccountMap
|
|
5579
|
+
capFileAccountMap(fileAccountMap, file.id, account.index);
|
|
5221
5580
|
const lines = [
|
|
5222
5581
|
`\u25A3 Anthropic Files [${label}]`,
|
|
5223
5582
|
"",
|
|
@@ -5585,7 +5944,7 @@ function buildAnthropicBillingHeader(claudeCliVersion, messages) {
|
|
|
5585
5944
|
}
|
|
5586
5945
|
}
|
|
5587
5946
|
}
|
|
5588
|
-
const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT
|
|
5947
|
+
const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT ?? "cli";
|
|
5589
5948
|
let cchValue;
|
|
5590
5949
|
if (Array.isArray(messages) && messages.length > 0) {
|
|
5591
5950
|
const bodyHint = JSON.stringify(messages).slice(0, 512);
|
|
@@ -5655,8 +6014,9 @@ function normalizeSystemTextBlocks(system) {
|
|
|
5655
6014
|
}
|
|
5656
6015
|
|
|
5657
6016
|
// src/system-prompt/sanitize.ts
|
|
5658
|
-
function sanitizeSystemText(text) {
|
|
5659
|
-
|
|
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");
|
|
5660
6020
|
}
|
|
5661
6021
|
function compactSystemText(text, mode) {
|
|
5662
6022
|
const withoutDuplicateIdentityPrefix = text.startsWith(`${CLAUDE_CODE_IDENTITY_STRING}
|
|
@@ -5789,11 +6149,69 @@ function buildRequestMetadata(input) {
|
|
|
5789
6149
|
}
|
|
5790
6150
|
|
|
5791
6151
|
// src/request/body.ts
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
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);
|
|
5795
6209
|
try {
|
|
5796
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
|
+
);
|
|
5797
6215
|
if (Object.hasOwn(parsed, "betas")) {
|
|
5798
6216
|
delete parsed.betas;
|
|
5799
6217
|
}
|
|
@@ -5806,8 +6224,51 @@ function transformRequestBody(body, signature, runtime) {
|
|
|
5806
6224
|
} else if (!Object.hasOwn(parsed, "temperature")) {
|
|
5807
6225
|
parsed.temperature = 1;
|
|
5808
6226
|
}
|
|
5809
|
-
|
|
5810
|
-
|
|
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
|
+
}
|
|
6271
|
+
if (signature.enabled) {
|
|
5811
6272
|
const currentMetadata = parsed.metadata && typeof parsed.metadata === "object" && !Array.isArray(parsed.metadata) ? parsed.metadata : {};
|
|
5812
6273
|
parsed.metadata = {
|
|
5813
6274
|
...currentMetadata,
|
|
@@ -5821,7 +6282,7 @@ function transformRequestBody(body, signature, runtime) {
|
|
|
5821
6282
|
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
5822
6283
|
parsed.tools = parsed.tools.map((tool) => ({
|
|
5823
6284
|
...tool,
|
|
5824
|
-
name: tool.name
|
|
6285
|
+
name: prefixToolDefinitionName(tool.name)
|
|
5825
6286
|
}));
|
|
5826
6287
|
}
|
|
5827
6288
|
if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
@@ -5831,7 +6292,7 @@ function transformRequestBody(body, signature, runtime) {
|
|
|
5831
6292
|
if (block.type === "tool_use" && block.name) {
|
|
5832
6293
|
return {
|
|
5833
6294
|
...block,
|
|
5834
|
-
name:
|
|
6295
|
+
name: prefixToolUseName(block.name, literalToolNames, debugLog)
|
|
5835
6296
|
};
|
|
5836
6297
|
}
|
|
5837
6298
|
return block;
|
|
@@ -5841,8 +6302,12 @@ function transformRequestBody(body, signature, runtime) {
|
|
|
5841
6302
|
});
|
|
5842
6303
|
}
|
|
5843
6304
|
return JSON.stringify(parsed);
|
|
5844
|
-
} catch {
|
|
5845
|
-
|
|
6305
|
+
} catch (err) {
|
|
6306
|
+
if (err instanceof SyntaxError) {
|
|
6307
|
+
debugLog?.("body parse failed:", err.message);
|
|
6308
|
+
return body;
|
|
6309
|
+
}
|
|
6310
|
+
throw err;
|
|
5846
6311
|
}
|
|
5847
6312
|
}
|
|
5848
6313
|
|
|
@@ -5904,50 +6369,71 @@ function transformRequestUrl(input) {
|
|
|
5904
6369
|
}
|
|
5905
6370
|
|
|
5906
6371
|
// src/response/mcp.ts
|
|
5907
|
-
function
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
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;
|
|
5918
6400
|
}
|
|
5919
6401
|
function stripMcpPrefixFromParsedEvent(parsed) {
|
|
5920
6402
|
if (!parsed || typeof parsed !== "object") return false;
|
|
5921
6403
|
const p2 = parsed;
|
|
5922
6404
|
let modified = false;
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
modified =
|
|
6405
|
+
modified = stripMcpPrefixFromToolUseBlock(p2.content_block) || modified;
|
|
6406
|
+
if (p2.message && typeof p2.message === "object") {
|
|
6407
|
+
modified = stripMcpPrefixFromContentBlocks(p2.message.content) || modified;
|
|
5926
6408
|
}
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
for (const block of p2.content) {
|
|
5939
|
-
if (!block || typeof block !== "object") continue;
|
|
5940
|
-
const b = block;
|
|
5941
|
-
if (b.type === "tool_use" && typeof b.name === "string" && b.name.startsWith("mcp_")) {
|
|
5942
|
-
b.name = b.name.slice(4);
|
|
5943
|
-
modified = true;
|
|
5944
|
-
}
|
|
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;
|
|
5945
6420
|
}
|
|
6421
|
+
return modified ? JSON.stringify(parsed) : body;
|
|
6422
|
+
} catch {
|
|
6423
|
+
return body;
|
|
5946
6424
|
}
|
|
5947
|
-
return modified;
|
|
5948
6425
|
}
|
|
5949
6426
|
|
|
5950
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
|
+
};
|
|
5951
6437
|
function extractUsageFromSSEEvent(parsed, stats) {
|
|
5952
6438
|
const p2 = parsed;
|
|
5953
6439
|
if (!p2) return;
|
|
@@ -5987,6 +6473,187 @@ function getSSEDataPayload(eventBlock) {
|
|
|
5987
6473
|
if (!payload || payload === "[DONE]") return null;
|
|
5988
6474
|
return payload;
|
|
5989
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
|
+
}
|
|
5990
6657
|
function getMidStreamAccountError(parsed) {
|
|
5991
6658
|
const p2 = parsed;
|
|
5992
6659
|
if (!p2 || p2.type !== "error" || !p2.error) {
|
|
@@ -6008,12 +6675,11 @@ function getMidStreamAccountError(parsed) {
|
|
|
6008
6675
|
invalidateToken: reason === "AUTH_FAILED"
|
|
6009
6676
|
};
|
|
6010
6677
|
}
|
|
6011
|
-
function transformResponse(response, onUsage, onAccountError) {
|
|
6012
|
-
if (!response.body) return response;
|
|
6678
|
+
function transformResponse(response, onUsage, onAccountError, onStreamError) {
|
|
6679
|
+
if (!response.body || !isEventStreamResponse(response)) return response;
|
|
6013
6680
|
const reader = response.body.getReader();
|
|
6014
|
-
const decoder = new TextDecoder();
|
|
6681
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
6015
6682
|
const encoder = new TextEncoder();
|
|
6016
|
-
const EMPTY_CHUNK = new Uint8Array();
|
|
6017
6683
|
const stats = {
|
|
6018
6684
|
inputTokens: 0,
|
|
6019
6685
|
outputTokens: 0,
|
|
@@ -6021,82 +6687,145 @@ function transformResponse(response, onUsage, onAccountError) {
|
|
|
6021
6687
|
cacheWriteTokens: 0
|
|
6022
6688
|
};
|
|
6023
6689
|
let sseBuffer = "";
|
|
6024
|
-
let sseRewriteBuffer = "";
|
|
6025
6690
|
let accountErrorHandled = false;
|
|
6026
|
-
|
|
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;
|
|
6027
6747
|
while (true) {
|
|
6028
6748
|
const boundary = sseBuffer.indexOf("\n\n");
|
|
6029
6749
|
if (boundary === -1) {
|
|
6030
|
-
if (
|
|
6031
|
-
|
|
6032
|
-
sseBuffer = "";
|
|
6033
|
-
return;
|
|
6750
|
+
if (sseBuffer.length > MAX_UNTERMINATED_SSE_BUFFER) {
|
|
6751
|
+
throw new Error("unterminated SSE event buffer exceeded limit");
|
|
6034
6752
|
}
|
|
6753
|
+
return emitted;
|
|
6035
6754
|
}
|
|
6036
|
-
const eventBlock =
|
|
6037
|
-
sseBuffer =
|
|
6038
|
-
|
|
6039
|
-
if (!payload) {
|
|
6040
|
-
if (boundary === -1) return;
|
|
6755
|
+
const eventBlock = sseBuffer.slice(0, boundary);
|
|
6756
|
+
sseBuffer = sseBuffer.slice(boundary + 2);
|
|
6757
|
+
if (!eventBlock.trim()) {
|
|
6041
6758
|
continue;
|
|
6042
6759
|
}
|
|
6760
|
+
enqueueNormalizedEvent(controller, eventBlock);
|
|
6761
|
+
emitted = true;
|
|
6762
|
+
}
|
|
6763
|
+
}
|
|
6764
|
+
async function failStream(controller, error) {
|
|
6765
|
+
const streamError = toStreamError(error);
|
|
6766
|
+
if (onStreamError) {
|
|
6043
6767
|
try {
|
|
6044
|
-
|
|
6045
|
-
if (onUsage) {
|
|
6046
|
-
extractUsageFromSSEEvent(parsed, stats);
|
|
6047
|
-
}
|
|
6048
|
-
if (onAccountError && !accountErrorHandled) {
|
|
6049
|
-
const details = getMidStreamAccountError(parsed);
|
|
6050
|
-
if (details) {
|
|
6051
|
-
accountErrorHandled = true;
|
|
6052
|
-
onAccountError(details);
|
|
6053
|
-
}
|
|
6054
|
-
}
|
|
6768
|
+
onStreamError(streamError);
|
|
6055
6769
|
} catch {
|
|
6056
6770
|
}
|
|
6057
|
-
if (boundary === -1) return;
|
|
6058
6771
|
}
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
if (!flush) {
|
|
6063
|
-
const boundary = sseRewriteBuffer.lastIndexOf("\n");
|
|
6064
|
-
if (boundary === -1) return "";
|
|
6065
|
-
const complete = sseRewriteBuffer.slice(0, boundary + 1);
|
|
6066
|
-
sseRewriteBuffer = sseRewriteBuffer.slice(boundary + 1);
|
|
6067
|
-
return stripMcpPrefixFromSSE(complete);
|
|
6772
|
+
try {
|
|
6773
|
+
await reader.cancel(streamError);
|
|
6774
|
+
} catch {
|
|
6068
6775
|
}
|
|
6069
|
-
|
|
6070
|
-
const finalText = stripMcpPrefixFromSSE(sseRewriteBuffer);
|
|
6071
|
-
sseRewriteBuffer = "";
|
|
6072
|
-
return finalText;
|
|
6776
|
+
controller.error(streamError);
|
|
6073
6777
|
}
|
|
6074
6778
|
const stream = new ReadableStream({
|
|
6075
6779
|
async pull(controller) {
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
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
|
+
}
|
|
6085
6822
|
}
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
}
|
|
6089
|
-
const text = decoder.decode(value, { stream: true });
|
|
6090
|
-
if (onUsage || onAccountError) {
|
|
6091
|
-
sseBuffer += text.replace(/\r\n/g, "\n");
|
|
6092
|
-
processSSEBuffer(false);
|
|
6093
|
-
}
|
|
6094
|
-
const rewrittenText = rewriteSSEChunk(text, false);
|
|
6095
|
-
if (rewrittenText) {
|
|
6096
|
-
controller.enqueue(encoder.encode(rewrittenText));
|
|
6097
|
-
} else {
|
|
6098
|
-
controller.enqueue(EMPTY_CHUNK);
|
|
6823
|
+
} catch (error) {
|
|
6824
|
+
await failStream(controller, error);
|
|
6099
6825
|
}
|
|
6826
|
+
},
|
|
6827
|
+
cancel(reason) {
|
|
6828
|
+
return reader.cancel(reason);
|
|
6100
6829
|
}
|
|
6101
6830
|
});
|
|
6102
6831
|
return new Response(stream, {
|
|
@@ -6111,6 +6840,7 @@ function isEventStreamResponse(response) {
|
|
|
6111
6840
|
}
|
|
6112
6841
|
|
|
6113
6842
|
// src/index.ts
|
|
6843
|
+
init_account_identity();
|
|
6114
6844
|
init_storage();
|
|
6115
6845
|
|
|
6116
6846
|
// src/token-refresh.ts
|
|
@@ -6122,9 +6852,9 @@ init_storage();
|
|
|
6122
6852
|
import { createHash as createHash3, randomBytes as randomBytes4 } from "node:crypto";
|
|
6123
6853
|
import { promises as fs2 } from "node:fs";
|
|
6124
6854
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
6125
|
-
var DEFAULT_LOCK_TIMEOUT_MS =
|
|
6855
|
+
var DEFAULT_LOCK_TIMEOUT_MS = 15e3;
|
|
6126
6856
|
var DEFAULT_LOCK_BACKOFF_MS = 50;
|
|
6127
|
-
var DEFAULT_STALE_LOCK_MS =
|
|
6857
|
+
var DEFAULT_STALE_LOCK_MS = 9e4;
|
|
6128
6858
|
function delay(ms) {
|
|
6129
6859
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
6130
6860
|
}
|
|
@@ -6229,16 +6959,19 @@ function applyDiskAuthIfFresher(account, diskAuth, options = {}) {
|
|
|
6229
6959
|
if (!diskAuth) return false;
|
|
6230
6960
|
const diskTokenUpdatedAt = diskAuth.tokenUpdatedAt || 0;
|
|
6231
6961
|
const memTokenUpdatedAt = account.tokenUpdatedAt || 0;
|
|
6232
|
-
const
|
|
6962
|
+
const diskIsNewer = diskTokenUpdatedAt > memTokenUpdatedAt;
|
|
6963
|
+
const diskHasDifferentRefreshToken = diskAuth.refreshToken !== account.refreshToken;
|
|
6233
6964
|
const memAuthExpired = !account.expires || account.expires <= Date.now();
|
|
6234
6965
|
const allowExpiredFallback = options.allowExpiredFallback === true;
|
|
6235
|
-
if (
|
|
6966
|
+
if (!diskIsNewer && !(allowExpiredFallback && diskHasDifferentRefreshToken && memAuthExpired)) {
|
|
6236
6967
|
return false;
|
|
6237
6968
|
}
|
|
6238
6969
|
account.refreshToken = diskAuth.refreshToken;
|
|
6239
6970
|
account.access = diskAuth.access;
|
|
6240
6971
|
account.expires = diskAuth.expires;
|
|
6241
|
-
|
|
6972
|
+
if (diskIsNewer) {
|
|
6973
|
+
account.tokenUpdatedAt = diskTokenUpdatedAt;
|
|
6974
|
+
}
|
|
6242
6975
|
return true;
|
|
6243
6976
|
}
|
|
6244
6977
|
function claudeBinaryPath() {
|
|
@@ -6304,11 +7037,9 @@ async function refreshCCAccount(account) {
|
|
|
6304
7037
|
markTokenStateUpdated(account);
|
|
6305
7038
|
return refreshedCredential.accessToken;
|
|
6306
7039
|
}
|
|
6307
|
-
async function refreshAccountToken(account, client, source = "foreground", { onTokensUpdated } = {}) {
|
|
7040
|
+
async function refreshAccountToken(account, client, source = "foreground", { onTokensUpdated, debugLog } = {}) {
|
|
6308
7041
|
const lockResult = await acquireRefreshLock(account.id, {
|
|
6309
|
-
|
|
6310
|
-
backoffMs: 60,
|
|
6311
|
-
staleMs: 2e4
|
|
7042
|
+
backoffMs: 60
|
|
6312
7043
|
});
|
|
6313
7044
|
const lock = lockResult && typeof lockResult === "object" ? lockResult : {
|
|
6314
7045
|
acquired: true,
|
|
@@ -6336,7 +7067,9 @@ async function refreshAccountToken(account, client, source = "foreground", { onT
|
|
|
6336
7067
|
const accessToken = await refreshCCAccount(account);
|
|
6337
7068
|
if (accessToken) {
|
|
6338
7069
|
if (onTokensUpdated) {
|
|
6339
|
-
await onTokensUpdated().catch(() =>
|
|
7070
|
+
await onTokensUpdated().catch((err) => {
|
|
7071
|
+
debugLog?.("onTokensUpdated failed:", err.message);
|
|
7072
|
+
});
|
|
6340
7073
|
}
|
|
6341
7074
|
await client.auth?.set({
|
|
6342
7075
|
path: { id: "anthropic" },
|
|
@@ -6346,7 +7079,9 @@ async function refreshAccountToken(account, client, source = "foreground", { onT
|
|
|
6346
7079
|
access: account.access,
|
|
6347
7080
|
expires: account.expires
|
|
6348
7081
|
}
|
|
6349
|
-
}).catch(() =>
|
|
7082
|
+
}).catch((err) => {
|
|
7083
|
+
debugLog?.("auth.set failed:", err.message);
|
|
7084
|
+
});
|
|
6350
7085
|
return accessToken;
|
|
6351
7086
|
}
|
|
6352
7087
|
throw new Error("CC credential refresh failed");
|
|
@@ -6392,28 +7127,181 @@ function formatSwitchReason(status, reason) {
|
|
|
6392
7127
|
|
|
6393
7128
|
// src/bun-fetch.ts
|
|
6394
7129
|
import { execFileSync, spawn } from "node:child_process";
|
|
6395
|
-
import { existsSync as existsSync5
|
|
7130
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
6396
7131
|
import { dirname as dirname4, join as join6 } from "node:path";
|
|
6397
|
-
import
|
|
7132
|
+
import * as readline from "node:readline";
|
|
6398
7133
|
import { fileURLToPath } from "node:url";
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
var
|
|
6402
|
-
var
|
|
6403
|
-
var
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
exitHandlerRegistered = true;
|
|
6410
|
-
const cleanup = () => {
|
|
6411
|
-
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))
|
|
6412
7144
|
};
|
|
6413
|
-
process.on("exit", cleanup);
|
|
6414
|
-
process.on("SIGINT", cleanup);
|
|
6415
|
-
process.on("SIGTERM", cleanup);
|
|
6416
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;
|
|
6417
7305
|
function findProxyScript() {
|
|
6418
7306
|
const dir = typeof __dirname !== "undefined" ? __dirname : dirname4(fileURLToPath(import.meta.url));
|
|
6419
7307
|
for (const candidate of [
|
|
@@ -6421,206 +7309,337 @@ function findProxyScript() {
|
|
|
6421
7309
|
join6(dir, "..", "dist", "bun-proxy.mjs"),
|
|
6422
7310
|
join6(dir, "bun-proxy.ts")
|
|
6423
7311
|
]) {
|
|
6424
|
-
if (existsSync5(candidate))
|
|
7312
|
+
if (existsSync5(candidate)) {
|
|
7313
|
+
return candidate;
|
|
7314
|
+
}
|
|
6425
7315
|
}
|
|
6426
7316
|
return null;
|
|
6427
7317
|
}
|
|
6428
|
-
|
|
6429
|
-
function hasBun() {
|
|
6430
|
-
if (_hasBun !== null) return _hasBun;
|
|
7318
|
+
function detectBunAvailability() {
|
|
6431
7319
|
try {
|
|
6432
|
-
execFileSync("
|
|
6433
|
-
|
|
7320
|
+
execFileSync("bun", ["--version"], { stdio: "ignore" });
|
|
7321
|
+
return true;
|
|
6434
7322
|
} catch {
|
|
6435
|
-
|
|
7323
|
+
return false;
|
|
6436
7324
|
}
|
|
6437
|
-
return _hasBun;
|
|
6438
7325
|
}
|
|
6439
|
-
function
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
}
|
|
6449
|
-
unlinkSync(PID_FILE);
|
|
6450
|
-
} 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;
|
|
6451
7335
|
}
|
|
7336
|
+
return input instanceof Request ? input.signal : void 0;
|
|
6452
7337
|
}
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
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;
|
|
6461
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));
|
|
6462
7363
|
}
|
|
6463
|
-
function
|
|
6464
|
-
|
|
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
|
+
}
|
|
6465
7437
|
const script = findProxyScript();
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
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;
|
|
6469
7444
|
}
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
const finish = (port) => {
|
|
6480
|
-
if (done) return;
|
|
6481
|
-
done = true;
|
|
6482
|
-
if (port && child.pid) {
|
|
6483
|
-
try {
|
|
6484
|
-
writeFileSync5(PID_FILE, String(child.pid));
|
|
6485
|
-
} 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"
|
|
6486
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;
|
|
6487
7487
|
}
|
|
6488
|
-
|
|
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;
|
|
7499
|
+
}
|
|
7500
|
+
clearActiveProxy(child);
|
|
7501
|
+
breaker.recordFailure();
|
|
7502
|
+
reportStatus(reason);
|
|
7503
|
+
flushPendingFetches("native", reason);
|
|
7504
|
+
resolve2(null);
|
|
6489
7505
|
};
|
|
6490
|
-
|
|
6491
|
-
const
|
|
6492
|
-
if (
|
|
6493
|
-
|
|
6494
|
-
healthCheckFails = 0;
|
|
6495
|
-
finish(proxyPort);
|
|
7506
|
+
stdoutLines.on("line", (line) => {
|
|
7507
|
+
const match = line.match(/^BUN_PROXY_PORT=(\d+)$/);
|
|
7508
|
+
if (!match) {
|
|
7509
|
+
return;
|
|
6496
7510
|
}
|
|
7511
|
+
finalize(
|
|
7512
|
+
{
|
|
7513
|
+
child,
|
|
7514
|
+
port: Number.parseInt(match[1], 10)
|
|
7515
|
+
},
|
|
7516
|
+
"proxy-ready"
|
|
7517
|
+
);
|
|
6497
7518
|
});
|
|
6498
|
-
child.
|
|
6499
|
-
|
|
6500
|
-
proxyPort = null;
|
|
6501
|
-
proxyProcess = null;
|
|
6502
|
-
starting = null;
|
|
6503
|
-
});
|
|
6504
|
-
child.on("exit", () => {
|
|
6505
|
-
proxyPort = null;
|
|
6506
|
-
proxyProcess = null;
|
|
6507
|
-
starting = null;
|
|
6508
|
-
finish(null);
|
|
7519
|
+
child.once("error", () => {
|
|
7520
|
+
finalize(null, "child-error");
|
|
6509
7521
|
});
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
proxyPort = FIXED_PORT;
|
|
6523
|
-
console.error("[bun-fetch] Reusing existing Bun proxy on port", FIXED_PORT);
|
|
6524
|
-
return proxyPort;
|
|
6525
|
-
}
|
|
6526
|
-
if (proxyPort && (!proxyProcess || proxyProcess.killed)) {
|
|
6527
|
-
proxyPort = null;
|
|
6528
|
-
proxyProcess = null;
|
|
6529
|
-
starting = null;
|
|
6530
|
-
}
|
|
6531
|
-
if (starting) return starting;
|
|
6532
|
-
starting = spawnProxy();
|
|
6533
|
-
const port = await starting;
|
|
6534
|
-
starting = null;
|
|
6535
|
-
if (port) console.error("[bun-fetch] Bun proxy started on port", port);
|
|
6536
|
-
else console.error("[bun-fetch] Failed to start Bun proxy, falling back to Node.js fetch");
|
|
6537
|
-
return port;
|
|
6538
|
-
}
|
|
6539
|
-
function stopBunProxy() {
|
|
6540
|
-
if (proxyProcess) {
|
|
6541
|
-
try {
|
|
6542
|
-
proxyProcess.kill();
|
|
6543
|
-
} catch {
|
|
6544
|
-
}
|
|
6545
|
-
proxyProcess = null;
|
|
6546
|
-
}
|
|
6547
|
-
proxyPort = null;
|
|
6548
|
-
starting = null;
|
|
6549
|
-
killStaleProxy();
|
|
6550
|
-
}
|
|
6551
|
-
async function fetchViaBun(input, init) {
|
|
6552
|
-
const port = await ensureBunProxy();
|
|
6553
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
6554
|
-
if (!port) {
|
|
6555
|
-
console.error("[bun-fetch] WARNING: No proxy available, using Node.js fetch (will route to extra usage!)");
|
|
6556
|
-
return fetch(input, init);
|
|
6557
|
-
}
|
|
6558
|
-
console.error(`[bun-fetch] Routing through Bun proxy at :${port} \u2192 ${url}`);
|
|
6559
|
-
if (init.body && url.includes("/v1/messages") && !url.includes("count_tokens")) {
|
|
6560
|
-
try {
|
|
6561
|
-
const { writeFileSync: writeFileSync6 } = __require("node:fs");
|
|
6562
|
-
writeFileSync6("/tmp/opencode-last-request.json", typeof init.body === "string" ? init.body : JSON.stringify(init.body));
|
|
6563
|
-
const hdrs = {};
|
|
6564
|
-
init.headers.forEach((v, k) => {
|
|
6565
|
-
hdrs[k] = k === "authorization" ? "Bearer ***" : v;
|
|
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
|
+
}
|
|
6566
7534
|
});
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
} catch {
|
|
6570
|
-
}
|
|
6571
|
-
}
|
|
6572
|
-
const headers = new Headers(init.headers);
|
|
6573
|
-
headers.set("x-proxy-url", url);
|
|
6574
|
-
try {
|
|
6575
|
-
const resp = await fetch(`http://127.0.0.1:${port}/`, {
|
|
6576
|
-
method: init.method || "POST",
|
|
6577
|
-
headers,
|
|
6578
|
-
body: init.body
|
|
7535
|
+
}).finally(() => {
|
|
7536
|
+
state.startPromise = null;
|
|
6579
7537
|
});
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
headers: retryHeaders,
|
|
6598
|
-
body: init.body
|
|
6599
|
-
});
|
|
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
|
+
}
|
|
6600
7555
|
}
|
|
6601
7556
|
}
|
|
6602
|
-
|
|
6603
|
-
|
|
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;
|
|
6604
7631
|
}
|
|
6605
7632
|
|
|
6606
|
-
// src/
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
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
|
+
}) {
|
|
6614
7642
|
const debouncedToastTimestamps = /* @__PURE__ */ new Map();
|
|
6615
|
-
const refreshInFlight = /* @__PURE__ */ new Map();
|
|
6616
|
-
const idleRefreshLastAttempt = /* @__PURE__ */ new Map();
|
|
6617
|
-
const idleRefreshInFlight = /* @__PURE__ */ new Set();
|
|
6618
|
-
const IDLE_REFRESH_ENABLED = config.idle_refresh.enabled;
|
|
6619
|
-
const IDLE_REFRESH_WINDOW_MS = config.idle_refresh.window_minutes * 60 * 1e3;
|
|
6620
|
-
const IDLE_REFRESH_MIN_INTERVAL_MS = config.idle_refresh.min_interval_minutes * 60 * 1e3;
|
|
6621
|
-
let initialAccountPinned = false;
|
|
6622
|
-
const pendingSlashOAuth = /* @__PURE__ */ new Map();
|
|
6623
|
-
const fileAccountMap = /* @__PURE__ */ new Map();
|
|
6624
7643
|
async function sendCommandMessage(sessionID, text) {
|
|
6625
7644
|
await client.session?.prompt({
|
|
6626
7645
|
path: { id: sessionID },
|
|
@@ -6628,8 +7647,8 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6628
7647
|
});
|
|
6629
7648
|
}
|
|
6630
7649
|
async function reloadAccountManagerFromDisk() {
|
|
6631
|
-
if (!
|
|
6632
|
-
|
|
7650
|
+
if (!getAccountManager()) return;
|
|
7651
|
+
setAccountManager(await AccountManager.load(config, null));
|
|
6633
7652
|
}
|
|
6634
7653
|
async function persistOpenCodeAuth(refresh, access, expires) {
|
|
6635
7654
|
await client.auth?.set({
|
|
@@ -6666,29 +7685,36 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6666
7685
|
const now = Date.now();
|
|
6667
7686
|
const lastAt = debouncedToastTimestamps.get(options.debounceKey) ?? 0;
|
|
6668
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
|
+
}
|
|
6669
7692
|
debouncedToastTimestamps.set(options.debounceKey, now);
|
|
6670
7693
|
}
|
|
6671
7694
|
}
|
|
6672
7695
|
try {
|
|
6673
7696
|
await client.tui?.showToast({ body: { message, variant } });
|
|
6674
|
-
} catch {
|
|
7697
|
+
} catch (err) {
|
|
7698
|
+
if (!(err instanceof TypeError)) debugLog("toast failed:", err);
|
|
6675
7699
|
}
|
|
6676
7700
|
}
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
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;
|
|
6692
7718
|
function parseRefreshFailure(refreshError) {
|
|
6693
7719
|
const message = refreshError instanceof Error ? refreshError.message : String(refreshError);
|
|
6694
7720
|
const status = typeof refreshError === "object" && refreshError && "status" in refreshError ? Number(refreshError.status) : NaN;
|
|
@@ -6707,9 +7733,14 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6707
7733
|
if (source === "foreground" && existing.source === "idle") {
|
|
6708
7734
|
try {
|
|
6709
7735
|
await existing.promise;
|
|
6710
|
-
} catch {
|
|
7736
|
+
} catch (err) {
|
|
7737
|
+
void err;
|
|
6711
7738
|
}
|
|
6712
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
|
+
}
|
|
6713
7744
|
} else {
|
|
6714
7745
|
return existing.promise;
|
|
6715
7746
|
}
|
|
@@ -6723,12 +7754,13 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6723
7754
|
return await refreshAccountToken(account, client, source, {
|
|
6724
7755
|
onTokensUpdated: async () => {
|
|
6725
7756
|
try {
|
|
6726
|
-
await
|
|
7757
|
+
await getAccountManager().saveToDisk();
|
|
6727
7758
|
} catch {
|
|
6728
|
-
|
|
7759
|
+
getAccountManager().requestSaveToDisk();
|
|
6729
7760
|
throw new Error("save failed, debounced retry scheduled");
|
|
6730
7761
|
}
|
|
6731
|
-
}
|
|
7762
|
+
},
|
|
7763
|
+
debugLog
|
|
6732
7764
|
});
|
|
6733
7765
|
} finally {
|
|
6734
7766
|
if (refreshInFlight.get(key) === entry) refreshInFlight.delete(key);
|
|
@@ -6739,7 +7771,7 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6739
7771
|
return p2;
|
|
6740
7772
|
}
|
|
6741
7773
|
async function refreshIdleAccount(account) {
|
|
6742
|
-
if (!
|
|
7774
|
+
if (!getAccountManager()) return;
|
|
6743
7775
|
if (idleRefreshInFlight.has(account.id)) return;
|
|
6744
7776
|
idleRefreshInFlight.add(account.id);
|
|
6745
7777
|
const attemptedRefreshToken = account.refreshToken;
|
|
@@ -6782,6 +7814,7 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6782
7814
|
}
|
|
6783
7815
|
}
|
|
6784
7816
|
function maybeRefreshIdleAccounts(activeAccount) {
|
|
7817
|
+
const accountManager = getAccountManager();
|
|
6785
7818
|
if (!IDLE_REFRESH_ENABLED || !accountManager) return;
|
|
6786
7819
|
const now = Date.now();
|
|
6787
7820
|
const excluded = /* @__PURE__ */ new Set([activeAccount.index]);
|
|
@@ -6794,6 +7827,80 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6794
7827
|
idleRefreshLastAttempt.set(target.id, now);
|
|
6795
7828
|
void refreshIdleAccount(target);
|
|
6796
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
|
+
}
|
|
6797
7904
|
const commandDeps = {
|
|
6798
7905
|
sendCommandMessage,
|
|
6799
7906
|
get accountManager() {
|
|
@@ -6811,6 +7918,10 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6811
7918
|
refreshAccountTokenSingleFlight
|
|
6812
7919
|
};
|
|
6813
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
|
|
6814
7925
|
"experimental.chat.system.transform": (input, output) => {
|
|
6815
7926
|
const prefix = CLAUDE_CODE_IDENTITY_STRING;
|
|
6816
7927
|
if (!signatureEmulationEnabled && input.model?.providerID === "anthropic") {
|
|
@@ -6818,6 +7929,7 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6818
7929
|
if (output.system[1]) output.system[1] = prefix + "\n\n" + output.system[1];
|
|
6819
7930
|
}
|
|
6820
7931
|
},
|
|
7932
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
6821
7933
|
config: async (input) => {
|
|
6822
7934
|
input.command ??= {};
|
|
6823
7935
|
input.command["anthropic"] = {
|
|
@@ -6825,6 +7937,7 @@ async function AnthropicAuthPlugin({ client }) {
|
|
|
6825
7937
|
description: "Manage Anthropic auth, config, and betas (usage, login, config, set, betas, switch)"
|
|
6826
7938
|
};
|
|
6827
7939
|
},
|
|
7940
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
6828
7941
|
"command.execute.before": async (input) => {
|
|
6829
7942
|
if (input.command !== "anthropic") return;
|
|
6830
7943
|
try {
|
|
@@ -6864,8 +7977,6 @@ ${message}`);
|
|
|
6864
7977
|
const ccCount = accountManager.getCCAccounts().length;
|
|
6865
7978
|
if (ccCount > 0) {
|
|
6866
7979
|
await toast(`Using Claude Code credentials (${ccCount} found)`, "success");
|
|
6867
|
-
} else {
|
|
6868
|
-
await toast("No Claude Code credentials \u2014 using OAuth", "info");
|
|
6869
7980
|
}
|
|
6870
7981
|
}
|
|
6871
7982
|
const initialAccountEnv = process.env.OPENCODE_ANTHROPIC_INITIAL_ACCOUNT?.trim();
|
|
@@ -6896,8 +8007,17 @@ ${message}`);
|
|
|
6896
8007
|
async fetch(input, init) {
|
|
6897
8008
|
const currentAuth = await getAuth();
|
|
6898
8009
|
if (currentAuth.type !== "oauth") return fetch(input, init);
|
|
6899
|
-
const requestInit = init ?? {};
|
|
8010
|
+
const requestInit = { ...init ?? {} };
|
|
6900
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
|
+
};
|
|
6901
8021
|
const requestMethod = String(
|
|
6902
8022
|
requestInit.method || (requestInput instanceof Request ? requestInput.method : "POST")
|
|
6903
8023
|
).toUpperCase();
|
|
@@ -6937,6 +8057,7 @@ ${message}`);
|
|
|
6937
8057
|
}
|
|
6938
8058
|
}
|
|
6939
8059
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
8060
|
+
requestContext.attempt = attempt + 1;
|
|
6940
8061
|
const account = attempt === 0 && pinnedAccount && !transientRefreshSkips.has(pinnedAccount.index) ? pinnedAccount : accountManager.getCurrentAccount(transientRefreshSkips);
|
|
6941
8062
|
if (showUsageToast && account && accountManager) {
|
|
6942
8063
|
const currentIndex = accountManager.getCurrentIndex();
|
|
@@ -7021,19 +8142,26 @@ ${message}`);
|
|
|
7021
8142
|
accessToken = account.access;
|
|
7022
8143
|
}
|
|
7023
8144
|
maybeRefreshIdleAccounts(account);
|
|
7024
|
-
const
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
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();
|
|
7037
8165
|
logTransformedSystemPrompt(body);
|
|
7038
8166
|
const requestHeaders = buildRequestHeaders(input, requestInit, accessToken, body, requestUrl, {
|
|
7039
8167
|
enabled: signatureEmulationEnabled,
|
|
@@ -7069,7 +8197,7 @@ ${message}`);
|
|
|
7069
8197
|
let response;
|
|
7070
8198
|
const fetchInput = requestInput;
|
|
7071
8199
|
try {
|
|
7072
|
-
response = await
|
|
8200
|
+
response = await fetchWithTransport(fetchInput, {
|
|
7073
8201
|
...requestInit,
|
|
7074
8202
|
body,
|
|
7075
8203
|
headers: requestHeaders
|
|
@@ -7109,6 +8237,7 @@ ${message}`);
|
|
|
7109
8237
|
reason
|
|
7110
8238
|
});
|
|
7111
8239
|
accountManager.markRateLimited(account, reason, authOrPermissionIssue ? null : retryAfterMs);
|
|
8240
|
+
transientRefreshSkips.add(account.index);
|
|
7112
8241
|
const name = account.email || `Account ${accountManager.getCurrentIndex() + 1}`;
|
|
7113
8242
|
const total = accountManager.getAccountCount();
|
|
7114
8243
|
if (total > 1) {
|
|
@@ -7134,23 +8263,24 @@ ${message}`);
|
|
|
7134
8263
|
headersForRetry.set("x-stainless-retry-count", String(retryCount));
|
|
7135
8264
|
retryCount += 1;
|
|
7136
8265
|
const retryUrl = fetchInput instanceof Request ? fetchInput.url : fetchInput.toString();
|
|
7137
|
-
|
|
8266
|
+
const retryBody = requestContext.preparedBody === void 0 ? void 0 : cloneBodyForRetry(requestContext.preparedBody);
|
|
8267
|
+
return fetchWithTransport(retryUrl, {
|
|
7138
8268
|
...requestInit,
|
|
7139
|
-
body,
|
|
8269
|
+
body: retryBody,
|
|
7140
8270
|
headers: headersForRetry
|
|
7141
8271
|
});
|
|
7142
8272
|
},
|
|
7143
8273
|
{ maxRetries: 2 }
|
|
7144
8274
|
);
|
|
7145
8275
|
if (!retried.ok) {
|
|
7146
|
-
return
|
|
8276
|
+
return finalizeResponse(retried);
|
|
7147
8277
|
}
|
|
7148
8278
|
response = retried;
|
|
7149
8279
|
} else {
|
|
7150
8280
|
debugLog("non-account-specific response error, returning directly", {
|
|
7151
8281
|
status: response.status
|
|
7152
8282
|
});
|
|
7153
|
-
return
|
|
8283
|
+
return finalizeResponse(response);
|
|
7154
8284
|
}
|
|
7155
8285
|
}
|
|
7156
8286
|
if (account && accountManager && response.ok) {
|
|
@@ -7160,15 +8290,28 @@ ${message}`);
|
|
|
7160
8290
|
const usageCallback = shouldInspectStream ? (usage) => {
|
|
7161
8291
|
accountManager.recordUsage(account.index, usage);
|
|
7162
8292
|
} : null;
|
|
7163
|
-
const accountErrorCallback = shouldInspectStream ? (
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
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;
|
|
7168
8307
|
}
|
|
7169
|
-
|
|
8308
|
+
debugLog("stream truncated during response consumption", {
|
|
8309
|
+
accountIndex: account?.index,
|
|
8310
|
+
message: error.message,
|
|
8311
|
+
context: error.context
|
|
8312
|
+
});
|
|
7170
8313
|
} : null;
|
|
7171
|
-
return
|
|
8314
|
+
return finalizeResponse(response, usageCallback, accountErrorCallback, streamErrorCallback);
|
|
7172
8315
|
}
|
|
7173
8316
|
if (lastError) throw lastError;
|
|
7174
8317
|
throw new Error("All accounts exhausted \u2014 no account could serve this request");
|
|
@@ -7198,11 +8341,38 @@ ${message}`);
|
|
|
7198
8341
|
if (!accountManager) {
|
|
7199
8342
|
accountManager = await AccountManager.load(config, null);
|
|
7200
8343
|
}
|
|
7201
|
-
const
|
|
7202
|
-
|
|
7203
|
-
|
|
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
|
+
);
|
|
7204
8372
|
if (added) {
|
|
7205
8373
|
added.source = ccCred.source;
|
|
8374
|
+
added.label = ccCred.label;
|
|
8375
|
+
added.identity = identity;
|
|
7206
8376
|
}
|
|
7207
8377
|
await accountManager.saveToDisk();
|
|
7208
8378
|
await toast("Added Claude Code credentials", "success");
|
|
@@ -7263,17 +8433,35 @@ ${message}`);
|
|
|
7263
8433
|
if (!accountManager) {
|
|
7264
8434
|
accountManager = await AccountManager.load(config, null);
|
|
7265
8435
|
}
|
|
8436
|
+
const identity = resolveIdentityFromOAuthExchange(credentials);
|
|
7266
8437
|
const countBefore = accountManager.getAccountCount();
|
|
7267
|
-
accountManager.
|
|
7268
|
-
|
|
7269
|
-
credentials.
|
|
7270
|
-
credentials.
|
|
7271
|
-
credentials.
|
|
7272
|
-
|
|
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
|
+
}
|
|
7273
8460
|
await accountManager.saveToDisk();
|
|
7274
8461
|
const total = accountManager.getAccountCount();
|
|
7275
8462
|
const name = credentials.email || "account";
|
|
7276
|
-
if (
|
|
8463
|
+
if (existing) await toast(`Re-authenticated (${name})`, "success");
|
|
8464
|
+
else if (countBefore > 0) await toast(`Added ${name} \u2014 ${total} accounts`, "success");
|
|
7277
8465
|
else await toast(`Authenticated (${name})`, "success");
|
|
7278
8466
|
return credentials;
|
|
7279
8467
|
}
|