@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9
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 +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
package/src/token-refresh.ts
CHANGED
|
@@ -15,10 +15,10 @@ import { loadAccounts } from "./storage.js";
|
|
|
15
15
|
export type { ManagedAccount };
|
|
16
16
|
|
|
17
17
|
export interface DiskAuth {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
refreshToken: string;
|
|
19
|
+
access?: string;
|
|
20
|
+
expires?: number;
|
|
21
|
+
tokenUpdatedAt: number;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -26,27 +26,27 @@ export interface DiskAuth {
|
|
|
26
26
|
* Another instance may have rotated tokens since we loaded into memory.
|
|
27
27
|
*/
|
|
28
28
|
export async function readDiskAccountAuth(accountId: string): Promise<DiskAuth | null> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
try {
|
|
30
|
+
const diskData = await loadAccounts();
|
|
31
|
+
if (!diskData) return null;
|
|
32
|
+
const diskAccount = diskData.accounts.find((a) => a.id === accountId);
|
|
33
|
+
if (!diskAccount) return null;
|
|
34
|
+
return {
|
|
35
|
+
refreshToken: diskAccount.refreshToken,
|
|
36
|
+
access: diskAccount.access,
|
|
37
|
+
expires: diskAccount.expires,
|
|
38
|
+
tokenUpdatedAt: diskAccount.token_updated_at,
|
|
39
|
+
};
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Stamp the account's tokenUpdatedAt to the current time.
|
|
47
47
|
*/
|
|
48
48
|
export function markTokenStateUpdated(account: ManagedAccount, now = Date.now()): void {
|
|
49
|
-
|
|
49
|
+
account.tokenUpdatedAt = now;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
@@ -54,128 +54,128 @@ export function markTokenStateUpdated(account: ManagedAccount, now = Date.now())
|
|
|
54
54
|
* Returns true if the account was updated from disk.
|
|
55
55
|
*/
|
|
56
56
|
export function applyDiskAuthIfFresher(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
account: ManagedAccount,
|
|
58
|
+
diskAuth: DiskAuth | null,
|
|
59
|
+
options: { allowExpiredFallback?: boolean } = {},
|
|
60
60
|
): boolean {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
61
|
+
if (!diskAuth) return false;
|
|
62
|
+
const diskTokenUpdatedAt = diskAuth.tokenUpdatedAt || 0;
|
|
63
|
+
const memTokenUpdatedAt = account.tokenUpdatedAt || 0;
|
|
64
|
+
const diskIsNewer = diskTokenUpdatedAt > memTokenUpdatedAt;
|
|
65
|
+
const diskHasDifferentRefreshToken = diskAuth.refreshToken !== account.refreshToken;
|
|
66
|
+
const memAuthExpired = !account.expires || account.expires <= Date.now();
|
|
67
|
+
const allowExpiredFallback = options.allowExpiredFallback === true;
|
|
68
|
+
if (!diskIsNewer && !(allowExpiredFallback && diskHasDifferentRefreshToken && memAuthExpired)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
account.refreshToken = diskAuth.refreshToken;
|
|
72
|
+
account.access = diskAuth.access;
|
|
73
|
+
account.expires = diskAuth.expires;
|
|
74
|
+
if (diskIsNewer) {
|
|
75
|
+
account.tokenUpdatedAt = diskTokenUpdatedAt;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export interface RefreshAccountTokenOptions {
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
onTokensUpdated?: () => Promise<void>;
|
|
82
|
+
debugLog?: (...args: unknown[]) => void;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export interface OpenCodeClient {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
86
|
+
auth?: {
|
|
87
|
+
set(params: {
|
|
88
|
+
path: { id: string };
|
|
89
|
+
body: {
|
|
90
|
+
type: string;
|
|
91
|
+
refresh: string;
|
|
92
|
+
access?: string;
|
|
93
|
+
expires?: number;
|
|
94
|
+
};
|
|
95
|
+
}): Promise<unknown>;
|
|
96
|
+
};
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function claudeBinaryPath(): string | null {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
try {
|
|
101
|
+
return execSync("which claude", { encoding: "utf-8", timeout: 5000 }).trim();
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function isFreshCCCredential(credential: CCCredential | null): boolean {
|
|
108
|
-
|
|
108
|
+
return Boolean(credential && credential.expiresAt > Date.now() + FOREGROUND_EXPIRY_BUFFER_MS);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function selectCCCredential(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
account: ManagedAccount,
|
|
113
|
+
credentials: CCCredential[],
|
|
114
|
+
preferredLabel?: string,
|
|
115
115
|
): CCCredential | null {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
if (credentials.length === 0) return null;
|
|
117
|
+
if (preferredLabel) {
|
|
118
|
+
const byLabel = credentials.find((credential) => credential.label === preferredLabel);
|
|
119
|
+
if (byLabel) return byLabel;
|
|
120
|
+
}
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
const byRefreshToken = credentials.find((credential) => credential.refreshToken === account.refreshToken);
|
|
123
|
+
if (byRefreshToken) return byRefreshToken;
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
if (account.access) {
|
|
126
|
+
const byAccessToken = credentials.find((credential) => credential.accessToken === account.access);
|
|
127
|
+
if (byAccessToken) return byAccessToken;
|
|
128
|
+
}
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
return credentials.length === 1 ? credentials[0] : null;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
function readCredentialForAccount(account: ManagedAccount, preferredLabel?: string): CCCredential | null {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
if (account.source === "cc-file") {
|
|
135
|
+
return readCCCredentialsFromFile();
|
|
136
|
+
}
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
if (account.source !== "cc-keychain") {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
const keychainCredentials = readCCCredentials().filter((credential) => credential.source === "cc-keychain");
|
|
143
|
+
return selectCCCredential(account, keychainCredentials, preferredLabel);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
async function refreshCCAccount(account: ManagedAccount): Promise<string | null> {
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
const initialCredential = readCredentialForAccount(account);
|
|
148
|
+
if (!initialCredential) return null;
|
|
149
|
+
|
|
150
|
+
if (isFreshCCCredential(initialCredential)) {
|
|
151
|
+
account.access = initialCredential.accessToken;
|
|
152
|
+
account.refreshToken = initialCredential.refreshToken;
|
|
153
|
+
account.expires = initialCredential.expiresAt;
|
|
154
|
+
markTokenStateUpdated(account);
|
|
155
|
+
return initialCredential.accessToken;
|
|
156
|
+
}
|
|
149
157
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
account.refreshToken = initialCredential.refreshToken;
|
|
153
|
-
account.expires = initialCredential.expiresAt;
|
|
154
|
-
markTokenStateUpdated(account);
|
|
155
|
-
return initialCredential.accessToken;
|
|
156
|
-
}
|
|
158
|
+
const claudePath = claudeBinaryPath();
|
|
159
|
+
if (!claudePath) return null;
|
|
157
160
|
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
try {
|
|
162
|
+
execSync(`${claudePath} -p . --model haiku`, {
|
|
163
|
+
encoding: "utf-8",
|
|
164
|
+
timeout: 60000,
|
|
165
|
+
});
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
160
169
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const refreshedCredential = readCredentialForAccount(account, initialCredential.label);
|
|
171
|
-
if (!isFreshCCCredential(refreshedCredential)) return null;
|
|
172
|
-
if (!refreshedCredential) return null;
|
|
173
|
-
|
|
174
|
-
account.access = refreshedCredential.accessToken;
|
|
175
|
-
account.refreshToken = refreshedCredential.refreshToken;
|
|
176
|
-
account.expires = refreshedCredential.expiresAt;
|
|
177
|
-
markTokenStateUpdated(account);
|
|
178
|
-
return refreshedCredential.accessToken;
|
|
170
|
+
const refreshedCredential = readCredentialForAccount(account, initialCredential.label);
|
|
171
|
+
if (!isFreshCCCredential(refreshedCredential)) return null;
|
|
172
|
+
if (!refreshedCredential) return null;
|
|
173
|
+
|
|
174
|
+
account.access = refreshedCredential.accessToken;
|
|
175
|
+
account.refreshToken = refreshedCredential.refreshToken;
|
|
176
|
+
account.expires = refreshedCredential.expiresAt;
|
|
177
|
+
markTokenStateUpdated(account);
|
|
178
|
+
return refreshedCredential.accessToken;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/**
|
|
@@ -189,125 +189,130 @@ async function refreshCCAccount(account: ManagedAccount): Promise<string | null>
|
|
|
189
189
|
* @throws If refresh fails
|
|
190
190
|
*/
|
|
191
191
|
export async function refreshAccountToken(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
account: ManagedAccount,
|
|
193
|
+
client: OpenCodeClient,
|
|
194
|
+
source: "foreground" | "idle" = "foreground",
|
|
195
|
+
{ onTokensUpdated, debugLog }: RefreshAccountTokenOptions = {},
|
|
196
196
|
): Promise<string> {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
});
|
|
200
|
-
const lock =
|
|
201
|
-
lockResult && typeof lockResult === "object"
|
|
202
|
-
? lockResult
|
|
203
|
-
: {
|
|
204
|
-
acquired: true,
|
|
205
|
-
lockPath: null,
|
|
206
|
-
owner: null,
|
|
207
|
-
lockInode: null,
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
if (!lock.acquired) {
|
|
211
|
-
const diskAuth = await readDiskAccountAuth(account.id);
|
|
212
|
-
const adopted = applyDiskAuthIfFresher(account, diskAuth, {
|
|
213
|
-
allowExpiredFallback: true,
|
|
197
|
+
const lockResult = await acquireRefreshLock(account.id, {
|
|
198
|
+
backoffMs: 60,
|
|
214
199
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
200
|
+
const lock =
|
|
201
|
+
lockResult && typeof lockResult === "object"
|
|
202
|
+
? lockResult
|
|
203
|
+
: {
|
|
204
|
+
acquired: true,
|
|
205
|
+
lockPath: null,
|
|
206
|
+
owner: null,
|
|
207
|
+
lockInode: null,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (!lock.acquired) {
|
|
211
|
+
const diskAuth = await readDiskAccountAuth(account.id);
|
|
212
|
+
const adopted = applyDiskAuthIfFresher(account, diskAuth, {
|
|
213
|
+
allowExpiredFallback: true,
|
|
214
|
+
});
|
|
215
|
+
if (
|
|
216
|
+
adopted &&
|
|
217
|
+
account.access &&
|
|
218
|
+
account.expires &&
|
|
219
|
+
account.expires > Date.now() + FOREGROUND_EXPIRY_BUFFER_MS
|
|
220
|
+
) {
|
|
221
|
+
return account.access;
|
|
222
|
+
}
|
|
223
|
+
throw new Error("Refresh lock busy");
|
|
232
224
|
}
|
|
233
225
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
226
|
+
try {
|
|
227
|
+
const diskAuthBeforeRefresh = await readDiskAccountAuth(account.id);
|
|
228
|
+
const adopted = applyDiskAuthIfFresher(account, diskAuthBeforeRefresh);
|
|
229
|
+
if (
|
|
230
|
+
source === "foreground" &&
|
|
231
|
+
adopted &&
|
|
232
|
+
account.access &&
|
|
233
|
+
account.expires &&
|
|
234
|
+
account.expires > Date.now() + FOREGROUND_EXPIRY_BUFFER_MS
|
|
235
|
+
) {
|
|
236
|
+
return account.access;
|
|
241
237
|
}
|
|
242
238
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
239
|
+
if (account.source === "cc-keychain" || account.source === "cc-file") {
|
|
240
|
+
const accessToken = await refreshCCAccount(account);
|
|
241
|
+
if (accessToken) {
|
|
242
|
+
if (onTokensUpdated) {
|
|
243
|
+
await onTokensUpdated().catch((err) => {
|
|
244
|
+
debugLog?.("onTokensUpdated failed:", (err as Error).message);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await client.auth
|
|
249
|
+
?.set({
|
|
250
|
+
path: { id: "anthropic" },
|
|
251
|
+
body: {
|
|
252
|
+
type: "oauth",
|
|
253
|
+
refresh: account.refreshToken,
|
|
254
|
+
access: account.access,
|
|
255
|
+
expires: account.expires,
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
.catch((err) => {
|
|
259
|
+
debugLog?.("auth.set failed:", (err as Error).message);
|
|
260
|
+
});
|
|
261
|
+
return accessToken;
|
|
262
|
+
}
|
|
263
|
+
throw new Error("CC credential refresh failed");
|
|
264
|
+
}
|
|
260
265
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
266
|
+
const json = await refreshToken(account.refreshToken, {
|
|
267
|
+
signal: AbortSignal.timeout(10_000),
|
|
268
|
+
});
|
|
264
269
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
270
|
+
account.access = json.access_token;
|
|
271
|
+
account.expires = Date.now() + json.expires_in * 1000;
|
|
272
|
+
if (json.refresh_token) {
|
|
273
|
+
account.refreshToken = json.refresh_token;
|
|
274
|
+
}
|
|
275
|
+
markTokenStateUpdated(account);
|
|
271
276
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
277
|
+
// Persist new tokens to disk BEFORE releasing the cross-process lock.
|
|
278
|
+
// This is critical: if we release the lock first, another process can
|
|
279
|
+
// acquire it and read the old (now-rotated) refresh token from disk,
|
|
280
|
+
// leading to an invalid_grant failure.
|
|
281
|
+
if (onTokensUpdated) {
|
|
282
|
+
try {
|
|
283
|
+
await onTokensUpdated();
|
|
284
|
+
} catch {
|
|
285
|
+
// Best-effort: in-memory tokens remain valid for this process.
|
|
286
|
+
}
|
|
287
|
+
}
|
|
283
288
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
289
|
+
// Also persist to OpenCode's auth.json for compatibility.
|
|
290
|
+
try {
|
|
291
|
+
await client.auth?.set({
|
|
292
|
+
path: { id: "anthropic" },
|
|
293
|
+
body: {
|
|
294
|
+
type: "oauth",
|
|
295
|
+
refresh: account.refreshToken,
|
|
296
|
+
access: account.access,
|
|
297
|
+
expires: account.expires,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
} catch {
|
|
301
|
+
// Ignore persistence errors; in-memory tokens remain valid for this request.
|
|
302
|
+
}
|
|
298
303
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
304
|
+
return json.access_token;
|
|
305
|
+
} finally {
|
|
306
|
+
await releaseRefreshLock(lock);
|
|
307
|
+
}
|
|
303
308
|
}
|
|
304
309
|
|
|
305
310
|
/**
|
|
306
311
|
* Build user-facing switch reason text for account-specific errors.
|
|
307
312
|
*/
|
|
308
313
|
export function formatSwitchReason(status: number, reason: RateLimitReason): string {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
314
|
+
if (reason === "AUTH_FAILED") return "auth failed";
|
|
315
|
+
if (status === 403 && reason === "QUOTA_EXHAUSTED") return "permission denied";
|
|
316
|
+
if (reason === "QUOTA_EXHAUSTED") return "quota exhausted";
|
|
317
|
+
return "rate-limited";
|
|
313
318
|
}
|
package/src/types.ts
CHANGED
|
@@ -11,49 +11,49 @@ export type PromptCompactionMode = "minimal" | "off";
|
|
|
11
11
|
export type AccountSelectionStrategy = "round-robin" | "sequential" | string;
|
|
12
12
|
|
|
13
13
|
export interface UsageStats {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
inputTokens: number;
|
|
15
|
+
outputTokens: number;
|
|
16
|
+
cacheReadTokens: number;
|
|
17
|
+
cacheWriteTokens: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface SystemBlock {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
type: string;
|
|
22
|
+
text: string;
|
|
23
|
+
cache_control?: { type: string; scope?: string; ttl?: string };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface SignatureConfig {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
claudeCliVersion: string;
|
|
29
|
+
promptCompactionMode: PromptCompactionMode;
|
|
30
|
+
/**
|
|
31
|
+
* When true, runs the legacy regex-based sanitization on system prompt text
|
|
32
|
+
* (rewrites OpenCode/Sisyphus/Morph identifiers). Defaults to false because
|
|
33
|
+
* the plugin's primary defense is now to relocate non-CC blocks into a
|
|
34
|
+
* user-message <system-instructions> wrapper. Opt in via the
|
|
35
|
+
* `sanitize_system_prompt` config field for double-belt-and-suspenders.
|
|
36
|
+
*/
|
|
37
|
+
sanitizeSystemPrompt?: boolean;
|
|
38
|
+
strategy?: AccountSelectionStrategy;
|
|
39
|
+
customBetas?: string[];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export interface RuntimeContext {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
persistentUserId: string;
|
|
44
|
+
sessionId: string;
|
|
45
|
+
accountId: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export interface RequestBodyMetadata {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
model: string;
|
|
50
|
+
tools: unknown[];
|
|
51
|
+
messages: unknown[];
|
|
52
|
+
hasFileReferences: boolean;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export interface RequestMetadata {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
user_id: string;
|
|
57
|
+
organization_uuid?: string;
|
|
58
|
+
user_email?: string;
|
|
59
59
|
}
|