@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.
Files changed (107) hide show
  1. package/README.md +88 -88
  2. package/dist/opencode-anthropic-auth-cli.mjs +804 -507
  3. package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
  4. package/package.json +67 -59
  5. package/src/__tests__/billing-edge-cases.test.ts +59 -59
  6. package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
  7. package/src/__tests__/cc-comparison.test.ts +87 -87
  8. package/src/__tests__/cc-credentials.test.ts +254 -250
  9. package/src/__tests__/cch-drift-checker.test.ts +51 -51
  10. package/src/__tests__/cch-native-style.test.ts +56 -56
  11. package/src/__tests__/debug-gating.test.ts +42 -42
  12. package/src/__tests__/decomposition-smoke.test.ts +68 -68
  13. package/src/__tests__/fingerprint-regression.test.ts +575 -566
  14. package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
  15. package/src/__tests__/helpers/conversation-history.ts +119 -119
  16. package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
  17. package/src/__tests__/helpers/deferred.ts +69 -69
  18. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
  19. package/src/__tests__/helpers/in-memory-storage.ts +88 -88
  20. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
  21. package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
  22. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
  23. package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
  24. package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
  25. package/src/__tests__/helpers/sse.ts +209 -209
  26. package/src/__tests__/index.parallel.test.ts +605 -595
  27. package/src/__tests__/sanitization-regex.test.ts +112 -112
  28. package/src/__tests__/state-bounds.test.ts +90 -90
  29. package/src/account-identity.test.ts +197 -192
  30. package/src/account-identity.ts +69 -67
  31. package/src/account-state.test.ts +86 -86
  32. package/src/account-state.ts +25 -25
  33. package/src/accounts/matching.test.ts +335 -0
  34. package/src/accounts/matching.ts +167 -0
  35. package/src/accounts/persistence.test.ts +345 -0
  36. package/src/accounts/persistence.ts +432 -0
  37. package/src/accounts/repair.test.ts +276 -0
  38. package/src/accounts/repair.ts +407 -0
  39. package/src/accounts.dedup.test.ts +621 -621
  40. package/src/accounts.test.ts +933 -929
  41. package/src/accounts.ts +633 -989
  42. package/src/backoff.test.ts +345 -345
  43. package/src/backoff.ts +219 -219
  44. package/src/betas.ts +124 -124
  45. package/src/bun-fetch.test.ts +345 -342
  46. package/src/bun-fetch.ts +424 -424
  47. package/src/bun-proxy.test.ts +25 -25
  48. package/src/bun-proxy.ts +209 -209
  49. package/src/cc-credentials.ts +111 -111
  50. package/src/circuit-breaker.test.ts +184 -184
  51. package/src/circuit-breaker.ts +169 -169
  52. package/src/cli/commands/auth.ts +963 -0
  53. package/src/cli/commands/config.ts +547 -0
  54. package/src/cli/formatting.test.ts +406 -0
  55. package/src/cli/formatting.ts +219 -0
  56. package/src/cli.ts +255 -2022
  57. package/src/commands/handlers/betas.ts +100 -0
  58. package/src/commands/handlers/config.ts +99 -0
  59. package/src/commands/handlers/files.ts +375 -0
  60. package/src/commands/oauth-flow.ts +181 -166
  61. package/src/commands/prompts.ts +61 -61
  62. package/src/commands/router.test.ts +421 -0
  63. package/src/commands/router.ts +143 -635
  64. package/src/config.test.ts +482 -482
  65. package/src/config.ts +412 -404
  66. package/src/constants.ts +48 -48
  67. package/src/drift/cch-constants.ts +95 -95
  68. package/src/env.ts +111 -105
  69. package/src/headers/billing.ts +33 -33
  70. package/src/headers/builder.ts +130 -130
  71. package/src/headers/cch.ts +75 -75
  72. package/src/headers/stainless.ts +25 -25
  73. package/src/headers/user-agent.ts +23 -23
  74. package/src/index.ts +436 -828
  75. package/src/models.ts +27 -27
  76. package/src/oauth.test.ts +102 -102
  77. package/src/oauth.ts +178 -178
  78. package/src/parent-pid-watcher.test.ts +148 -148
  79. package/src/parent-pid-watcher.ts +69 -69
  80. package/src/plugin-helpers.ts +82 -82
  81. package/src/refresh-helpers.ts +145 -139
  82. package/src/refresh-lock.test.ts +94 -94
  83. package/src/refresh-lock.ts +93 -93
  84. package/src/request/body.history.test.ts +579 -571
  85. package/src/request/body.ts +255 -255
  86. package/src/request/metadata.ts +65 -65
  87. package/src/request/retry.test.ts +156 -156
  88. package/src/request/retry.ts +67 -67
  89. package/src/request/url.ts +21 -21
  90. package/src/request-orchestration-helpers.ts +648 -0
  91. package/src/response/index.ts +5 -5
  92. package/src/response/mcp.ts +58 -58
  93. package/src/response/streaming.test.ts +313 -311
  94. package/src/response/streaming.ts +412 -410
  95. package/src/rotation.test.ts +304 -301
  96. package/src/rotation.ts +205 -205
  97. package/src/storage.test.ts +547 -547
  98. package/src/storage.ts +315 -291
  99. package/src/system-prompt/builder.ts +38 -38
  100. package/src/system-prompt/index.ts +5 -5
  101. package/src/system-prompt/normalize.ts +60 -60
  102. package/src/system-prompt/sanitize.ts +30 -30
  103. package/src/thinking.ts +21 -20
  104. package/src/token-refresh.test.ts +265 -265
  105. package/src/token-refresh.ts +219 -214
  106. package/src/types.ts +30 -30
  107. package/dist/bun-proxy.mjs +0 -291
@@ -15,10 +15,10 @@ import { loadAccounts } from "./storage.js";
15
15
  export type { ManagedAccount };
16
16
 
17
17
  export interface DiskAuth {
18
- refreshToken: string;
19
- access?: string;
20
- expires?: number;
21
- tokenUpdatedAt: number;
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
- 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
- }
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
- account.tokenUpdatedAt = now;
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
- account: ManagedAccount,
58
- diskAuth: DiskAuth | null,
59
- options: { allowExpiredFallback?: boolean } = {},
57
+ account: ManagedAccount,
58
+ diskAuth: DiskAuth | null,
59
+ options: { allowExpiredFallback?: boolean } = {},
60
60
  ): boolean {
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;
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
- onTokensUpdated?: () => Promise<void>;
82
- debugLog?: (...args: unknown[]) => void;
81
+ onTokensUpdated?: () => Promise<void>;
82
+ debugLog?: (...args: unknown[]) => void;
83
83
  }
84
84
 
85
85
  export interface OpenCodeClient {
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
- };
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
- try {
101
- return execSync("which claude", { encoding: "utf-8", timeout: 5000 }).trim();
102
- } catch {
103
- return null;
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
- return Boolean(credential && credential.expiresAt > Date.now() + FOREGROUND_EXPIRY_BUFFER_MS);
108
+ return Boolean(credential && credential.expiresAt > Date.now() + FOREGROUND_EXPIRY_BUFFER_MS);
109
109
  }
110
110
 
111
111
  function selectCCCredential(
112
- account: ManagedAccount,
113
- credentials: CCCredential[],
114
- preferredLabel?: string,
112
+ account: ManagedAccount,
113
+ credentials: CCCredential[],
114
+ preferredLabel?: string,
115
115
  ): CCCredential | null {
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
- }
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
- const byRefreshToken = credentials.find((credential) => credential.refreshToken === account.refreshToken);
123
- if (byRefreshToken) return byRefreshToken;
122
+ const byRefreshToken = credentials.find((credential) => credential.refreshToken === account.refreshToken);
123
+ if (byRefreshToken) return byRefreshToken;
124
124
 
125
- if (account.access) {
126
- const byAccessToken = credentials.find((credential) => credential.accessToken === account.access);
127
- if (byAccessToken) return byAccessToken;
128
- }
125
+ if (account.access) {
126
+ const byAccessToken = credentials.find((credential) => credential.accessToken === account.access);
127
+ if (byAccessToken) return byAccessToken;
128
+ }
129
129
 
130
- return credentials.length === 1 ? credentials[0] : null;
130
+ return credentials.length === 1 ? credentials[0] : null;
131
131
  }
132
132
 
133
133
  function readCredentialForAccount(account: ManagedAccount, preferredLabel?: string): CCCredential | null {
134
- if (account.source === "cc-file") {
135
- return readCCCredentialsFromFile();
136
- }
134
+ if (account.source === "cc-file") {
135
+ return readCCCredentialsFromFile();
136
+ }
137
137
 
138
- if (account.source !== "cc-keychain") {
139
- return null;
140
- }
138
+ if (account.source !== "cc-keychain") {
139
+ return null;
140
+ }
141
141
 
142
- const keychainCredentials = readCCCredentials().filter((credential) => credential.source === "cc-keychain");
143
- return selectCCCredential(account, keychainCredentials, preferredLabel);
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
- const initialCredential = readCredentialForAccount(account);
148
- if (!initialCredential) return null;
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
- 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
- }
158
+ const claudePath = claudeBinaryPath();
159
+ if (!claudePath) return null;
157
160
 
158
- const claudePath = claudeBinaryPath();
159
- if (!claudePath) return null;
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
- try {
162
- execSync(`${claudePath} -p . --model haiku`, {
163
- encoding: "utf-8",
164
- timeout: 60000,
165
- });
166
- } catch {
167
- return null;
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
- account: ManagedAccount,
193
- client: OpenCodeClient,
194
- source: "foreground" | "idle" = "foreground",
195
- { onTokensUpdated, debugLog }: RefreshAccountTokenOptions = {},
192
+ account: ManagedAccount,
193
+ client: OpenCodeClient,
194
+ source: "foreground" | "idle" = "foreground",
195
+ { onTokensUpdated, debugLog }: RefreshAccountTokenOptions = {},
196
196
  ): Promise<string> {
197
- const lockResult = await acquireRefreshLock(account.id, {
198
- backoffMs: 60,
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
- if (adopted && account.access && account.expires && account.expires > Date.now() + FOREGROUND_EXPIRY_BUFFER_MS) {
216
- return account.access;
217
- }
218
- throw new Error("Refresh lock busy");
219
- }
220
-
221
- try {
222
- const diskAuthBeforeRefresh = await readDiskAccountAuth(account.id);
223
- const adopted = applyDiskAuthIfFresher(account, diskAuthBeforeRefresh);
224
- if (
225
- source === "foreground" &&
226
- adopted &&
227
- account.access &&
228
- account.expires &&
229
- account.expires > Date.now() + FOREGROUND_EXPIRY_BUFFER_MS
230
- ) {
231
- return account.access;
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
- if (account.source === "cc-keychain" || account.source === "cc-file") {
235
- const accessToken = await refreshCCAccount(account);
236
- if (accessToken) {
237
- if (onTokensUpdated) {
238
- await onTokensUpdated().catch((err) => {
239
- debugLog?.("onTokensUpdated failed:", (err as Error).message);
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
- await client.auth
244
- ?.set({
245
- path: { id: "anthropic" },
246
- body: {
247
- type: "oauth",
248
- refresh: account.refreshToken,
249
- access: account.access,
250
- expires: account.expires,
251
- },
252
- })
253
- .catch((err) => {
254
- debugLog?.("auth.set failed:", (err as Error).message);
255
- });
256
- return accessToken;
257
- }
258
- throw new Error("CC credential refresh failed");
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
- const json = await refreshToken(account.refreshToken, {
262
- signal: AbortSignal.timeout(10_000),
263
- });
266
+ const json = await refreshToken(account.refreshToken, {
267
+ signal: AbortSignal.timeout(10_000),
268
+ });
264
269
 
265
- account.access = json.access_token;
266
- account.expires = Date.now() + json.expires_in * 1000;
267
- if (json.refresh_token) {
268
- account.refreshToken = json.refresh_token;
269
- }
270
- markTokenStateUpdated(account);
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
- // Persist new tokens to disk BEFORE releasing the cross-process lock.
273
- // This is critical: if we release the lock first, another process can
274
- // acquire it and read the old (now-rotated) refresh token from disk,
275
- // leading to an invalid_grant failure.
276
- if (onTokensUpdated) {
277
- try {
278
- await onTokensUpdated();
279
- } catch {
280
- // Best-effort: in-memory tokens remain valid for this process.
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
- // Also persist to OpenCode's auth.json for compatibility.
285
- try {
286
- await client.auth?.set({
287
- path: { id: "anthropic" },
288
- body: {
289
- type: "oauth",
290
- refresh: account.refreshToken,
291
- access: account.access,
292
- expires: account.expires,
293
- },
294
- });
295
- } catch {
296
- // Ignore persistence errors; in-memory tokens remain valid for this request.
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
- return json.access_token;
300
- } finally {
301
- await releaseRefreshLock(lock);
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
- if (reason === "AUTH_FAILED") return "auth failed";
310
- if (status === 403 && reason === "QUOTA_EXHAUSTED") return "permission denied";
311
- if (reason === "QUOTA_EXHAUSTED") return "quota exhausted";
312
- return "rate-limited";
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
- inputTokens: number;
15
- outputTokens: number;
16
- cacheReadTokens: number;
17
- cacheWriteTokens: number;
14
+ inputTokens: number;
15
+ outputTokens: number;
16
+ cacheReadTokens: number;
17
+ cacheWriteTokens: number;
18
18
  }
19
19
 
20
20
  export interface SystemBlock {
21
- type: string;
22
- text: string;
23
- cache_control?: { type: string; scope?: string; ttl?: string };
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
- 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[];
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
- persistentUserId: string;
44
- sessionId: string;
45
- accountId: string;
43
+ persistentUserId: string;
44
+ sessionId: string;
45
+ accountId: string;
46
46
  }
47
47
 
48
48
  export interface RequestBodyMetadata {
49
- model: string;
50
- tools: unknown[];
51
- messages: unknown[];
52
- hasFileReferences: boolean;
49
+ model: string;
50
+ tools: unknown[];
51
+ messages: unknown[];
52
+ hasFileReferences: boolean;
53
53
  }
54
54
 
55
55
  export interface RequestMetadata {
56
- user_id: string;
57
- organization_uuid?: string;
58
- user_email?: string;
56
+ user_id: string;
57
+ organization_uuid?: string;
58
+ user_email?: string;
59
59
  }