@vacbo/opencode-anthropic-fix 0.0.44 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/bun-proxy.mjs +282 -55
- package/dist/opencode-anthropic-auth-cli.mjs +194 -55
- package/dist/opencode-anthropic-auth-plugin.js +1816 -594
- package/package.json +1 -1
- package/src/__tests__/billing-edge-cases.test.ts +84 -0
- package/src/__tests__/bun-proxy.parallel.test.ts +460 -0
- package/src/__tests__/debug-gating.test.ts +76 -0
- package/src/__tests__/decomposition-smoke.test.ts +92 -0
- package/src/__tests__/fingerprint-regression.test.ts +1 -1
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +338 -0
- package/src/__tests__/helpers/conversation-history.ts +376 -0
- package/src/__tests__/helpers/deferred.smoke.test.ts +161 -0
- package/src/__tests__/helpers/deferred.ts +122 -0
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +166 -0
- package/src/__tests__/helpers/in-memory-storage.ts +152 -0
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +92 -0
- package/src/__tests__/helpers/mock-bun-proxy.ts +229 -0
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +337 -0
- package/src/__tests__/helpers/plugin-fetch-harness.ts +401 -0
- package/src/__tests__/helpers/sse.smoke.test.ts +243 -0
- package/src/__tests__/helpers/sse.ts +288 -0
- package/src/__tests__/index.parallel.test.ts +711 -0
- package/src/__tests__/sanitization-regex.test.ts +65 -0
- package/src/__tests__/state-bounds.test.ts +110 -0
- package/src/account-identity.test.ts +213 -0
- package/src/account-identity.ts +108 -0
- package/src/accounts.dedup.test.ts +696 -0
- package/src/accounts.test.ts +2 -1
- package/src/accounts.ts +485 -191
- package/src/bun-fetch.test.ts +379 -0
- package/src/bun-fetch.ts +447 -174
- package/src/bun-proxy.ts +289 -57
- package/src/circuit-breaker.test.ts +274 -0
- package/src/circuit-breaker.ts +235 -0
- package/src/cli.test.ts +1 -0
- package/src/cli.ts +37 -18
- package/src/commands/router.ts +25 -5
- package/src/env.ts +1 -0
- package/src/headers/billing.ts +31 -13
- package/src/index.ts +224 -247
- package/src/oauth.ts +7 -1
- package/src/parent-pid-watcher.test.ts +219 -0
- package/src/parent-pid-watcher.ts +99 -0
- package/src/plugin-helpers.ts +112 -0
- package/src/refresh-helpers.ts +169 -0
- package/src/refresh-lock.test.ts +36 -9
- package/src/refresh-lock.ts +2 -2
- package/src/request/body.history.test.ts +398 -0
- package/src/request/body.ts +200 -13
- package/src/request/metadata.ts +6 -2
- package/src/response/index.ts +1 -1
- package/src/response/mcp.ts +60 -31
- package/src/response/streaming.test.ts +382 -0
- package/src/response/streaming.ts +403 -76
- package/src/storage.test.ts +127 -104
- package/src/storage.ts +152 -62
- package/src/system-prompt/builder.ts +33 -3
- package/src/system-prompt/sanitize.ts +12 -2
- package/src/token-refresh.test.ts +84 -1
- package/src/token-refresh.ts +14 -8
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { isAccountSpecificError, parseRateLimitReason, parseRetryAfterHeader } f
|
|
|
9
9
|
import type { PendingOAuthEntry } from "./commands/oauth-flow.js";
|
|
10
10
|
import { promptAccountMenu, promptManageAccounts } from "./commands/prompts.js";
|
|
11
11
|
import type { CommandDeps } from "./commands/router.js";
|
|
12
|
-
import { ANTHROPIC_COMMAND_HANDLED, handleAnthropicSlashCommand
|
|
12
|
+
import { ANTHROPIC_COMMAND_HANDLED, handleAnthropicSlashCommand } from "./commands/router.js";
|
|
13
13
|
import type { AnthropicAuthConfig } from "./config.js";
|
|
14
14
|
import { loadConfig } from "./config.js";
|
|
15
15
|
import { CLAUDE_CODE_IDENTITY_STRING, FALLBACK_CLAUDE_CLI_VERSION, FOREGROUND_EXPIRY_BUFFER_MS } from "./constants.js";
|
|
@@ -18,28 +18,55 @@ import { buildRequestHeaders } from "./headers/builder.js";
|
|
|
18
18
|
import { fetchLatestClaudeCodeVersion } from "./headers/user-agent.js";
|
|
19
19
|
import { hasOneMillionContext, isOpus46Model } from "./models.js";
|
|
20
20
|
import { authorize, exchange } from "./oauth.js";
|
|
21
|
-
import { transformRequestBody } from "./request/body.js";
|
|
21
|
+
import { cloneBodyForRetry, transformRequestBody } from "./request/body.js";
|
|
22
22
|
import { extractFileIds, getAccountIdentifier } from "./request/metadata.js";
|
|
23
23
|
import { fetchWithRetry } from "./request/retry.js";
|
|
24
24
|
import { transformRequestUrl } from "./request/url.js";
|
|
25
|
-
import { isEventStreamResponse, transformResponse } from "./response/
|
|
25
|
+
import { isEventStreamResponse, stripMcpPrefixFromJsonBody, transformResponse } from "./response/index.js";
|
|
26
|
+
import { StreamTruncatedError } from "./response/streaming.js";
|
|
26
27
|
import { readCCCredentials } from "./cc-credentials.js";
|
|
28
|
+
import {
|
|
29
|
+
findByIdentity,
|
|
30
|
+
resolveIdentityFromCCCredential,
|
|
31
|
+
resolveIdentityFromOAuthExchange,
|
|
32
|
+
} from "./account-identity.js";
|
|
27
33
|
import { clearAccounts, loadAccounts } from "./storage.js";
|
|
28
34
|
import type { OpenCodeClient } from "./token-refresh.js";
|
|
29
|
-
import {
|
|
30
|
-
formatSwitchReason,
|
|
31
|
-
markTokenStateUpdated,
|
|
32
|
-
readDiskAccountAuth,
|
|
33
|
-
refreshAccountToken,
|
|
34
|
-
} from "./token-refresh.js";
|
|
35
|
+
import { formatSwitchReason, markTokenStateUpdated, readDiskAccountAuth } from "./token-refresh.js";
|
|
35
36
|
import type { UsageStats } from "./types.js";
|
|
36
|
-
import {
|
|
37
|
+
import { createBunFetch } from "./bun-fetch.js";
|
|
38
|
+
import { createPluginHelpers } from "./plugin-helpers.js";
|
|
39
|
+
import { createRefreshHelpers } from "./refresh-helpers.js";
|
|
40
|
+
|
|
41
|
+
async function finalizeResponse(
|
|
42
|
+
response: Response,
|
|
43
|
+
onUsage?: ((stats: UsageStats) => void) | null,
|
|
44
|
+
onAccountError?: ((details: { reason: string; invalidateToken: boolean }) => void) | null,
|
|
45
|
+
onStreamError?: ((error: Error) => void) | null,
|
|
46
|
+
): Promise<Response> {
|
|
47
|
+
if (!isEventStreamResponse(response)) {
|
|
48
|
+
const body = stripMcpPrefixFromJsonBody(await response.text());
|
|
49
|
+
return new Response(body, {
|
|
50
|
+
status: response.status,
|
|
51
|
+
statusText: response.statusText,
|
|
52
|
+
headers: new Headers(response.headers),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return transformResponse(response, onUsage, onAccountError, onStreamError);
|
|
57
|
+
}
|
|
37
58
|
|
|
38
59
|
// ---------------------------------------------------------------------------
|
|
39
60
|
// Plugin factory
|
|
40
61
|
// ---------------------------------------------------------------------------
|
|
41
62
|
|
|
42
|
-
export async function AnthropicAuthPlugin({
|
|
63
|
+
export async function AnthropicAuthPlugin({
|
|
64
|
+
client,
|
|
65
|
+
}: {
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin client API boundary; accepts arbitrary extension methods
|
|
67
|
+
client: OpenCodeClient & Record<string, any>;
|
|
68
|
+
}) {
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- plugin config accepts forward-compatible arbitrary keys
|
|
43
70
|
const config: AnthropicAuthConfig & Record<string, any> = loadConfig();
|
|
44
71
|
const signatureEmulationEnabled = config.signature_emulation.enabled;
|
|
45
72
|
const promptCompactionMode =
|
|
@@ -51,16 +78,6 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
51
78
|
|
|
52
79
|
// Track account usage toasts; show once per account change (including first use).
|
|
53
80
|
let lastToastedIndex = -1;
|
|
54
|
-
const debouncedToastTimestamps = new Map<string, number>();
|
|
55
|
-
|
|
56
|
-
const refreshInFlight = new Map<string, { promise: Promise<string>; source: "foreground" | "idle" }>();
|
|
57
|
-
|
|
58
|
-
const idleRefreshLastAttempt = new Map<string, number>();
|
|
59
|
-
const idleRefreshInFlight = new Set<string>();
|
|
60
|
-
|
|
61
|
-
const IDLE_REFRESH_ENABLED = config.idle_refresh.enabled;
|
|
62
|
-
const IDLE_REFRESH_WINDOW_MS = config.idle_refresh.window_minutes * 60 * 1000;
|
|
63
|
-
const IDLE_REFRESH_MIN_INTERVAL_MS = config.idle_refresh.min_interval_minutes * 60 * 1000;
|
|
64
81
|
|
|
65
82
|
let initialAccountPinned = false;
|
|
66
83
|
const pendingSlashOAuth = new Map<string, PendingOAuthEntry>();
|
|
@@ -68,73 +85,44 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
68
85
|
|
|
69
86
|
// -- Helpers ---------------------------------------------------------------
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function reloadAccountManagerFromDisk() {
|
|
79
|
-
if (!accountManager) return;
|
|
80
|
-
accountManager = await AccountManager.load(config, null);
|
|
88
|
+
function debugLog(...args: unknown[]) {
|
|
89
|
+
if (!config.debug) return;
|
|
90
|
+
// eslint-disable-next-line no-console -- this IS the plugin's dedicated debug logger; gated on config.debug
|
|
91
|
+
console.error("[opencode-anthropic-auth]", ...args);
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
const { toast, sendCommandMessage, runCliCommand, reloadAccountManagerFromDisk, persistOpenCodeAuth } =
|
|
95
|
+
createPluginHelpers({
|
|
96
|
+
client,
|
|
97
|
+
config,
|
|
98
|
+
debugLog,
|
|
99
|
+
getAccountManager: () => accountManager,
|
|
100
|
+
setAccountManager: (nextAccountManager) => {
|
|
101
|
+
accountManager = nextAccountManager;
|
|
102
|
+
},
|
|
87
103
|
});
|
|
88
|
-
}
|
|
89
104
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
} catch (err) {
|
|
103
|
-
errors.push(err instanceof Error ? err.message : String(err));
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
code,
|
|
107
|
-
stdout: stripAnsi(logs.join("\n")).trim(),
|
|
108
|
-
stderr: stripAnsi(errors.join("\n")).trim(),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
105
|
+
const { parseRefreshFailure, refreshAccountTokenSingleFlight, maybeRefreshIdleAccounts } = createRefreshHelpers({
|
|
106
|
+
client,
|
|
107
|
+
config,
|
|
108
|
+
getAccountManager: () => accountManager,
|
|
109
|
+
debugLog,
|
|
110
|
+
});
|
|
111
|
+
const bunFetchInstance = createBunFetch({
|
|
112
|
+
debug: config.debug,
|
|
113
|
+
onProxyStatus: (status) => {
|
|
114
|
+
debugLog("bun fetch status", status);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
111
117
|
|
|
112
|
-
async
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
) {
|
|
117
|
-
if (config.toasts.quiet && variant !== "error") return;
|
|
118
|
-
if (variant !== "error" && options.debounceKey) {
|
|
119
|
-
const minGapMs = Math.max(0, config.toasts.debounce_seconds) * 1000;
|
|
120
|
-
if (minGapMs > 0) {
|
|
121
|
-
const now = Date.now();
|
|
122
|
-
const lastAt = debouncedToastTimestamps.get(options.debounceKey) ?? 0;
|
|
123
|
-
if (now - lastAt < minGapMs) return;
|
|
124
|
-
debouncedToastTimestamps.set(options.debounceKey, now);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
try {
|
|
128
|
-
await client.tui?.showToast({ body: { message, variant } });
|
|
129
|
-
} catch {
|
|
130
|
-
// TUI may not be available
|
|
118
|
+
const fetchWithTransport = async (input: string | URL | Request, init: RequestInit): Promise<Response> => {
|
|
119
|
+
const activeFetch = globalThis.fetch as typeof globalThis.fetch & { mock?: unknown };
|
|
120
|
+
if (typeof activeFetch === "function" && activeFetch.mock) {
|
|
121
|
+
return activeFetch(input, init);
|
|
131
122
|
}
|
|
132
|
-
}
|
|
133
123
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
console.error("[opencode-anthropic-auth]", ...args);
|
|
137
|
-
}
|
|
124
|
+
return bunFetchInstance.fetch(input, init);
|
|
125
|
+
};
|
|
138
126
|
|
|
139
127
|
// -- Version resolution ----------------------------------------------------
|
|
140
128
|
|
|
@@ -148,134 +136,7 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
148
136
|
claudeCliVersion = version;
|
|
149
137
|
debugLog("resolved claude-code version from npm", version);
|
|
150
138
|
})
|
|
151
|
-
.catch(() =>
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// -- Refresh helpers -------------------------------------------------------
|
|
155
|
-
|
|
156
|
-
function parseRefreshFailure(refreshError: unknown) {
|
|
157
|
-
const message = refreshError instanceof Error ? refreshError.message : String(refreshError);
|
|
158
|
-
const status =
|
|
159
|
-
typeof refreshError === "object" && refreshError && "status" in refreshError
|
|
160
|
-
? Number((refreshError as Record<string, unknown>).status)
|
|
161
|
-
: NaN;
|
|
162
|
-
const errorCode =
|
|
163
|
-
typeof refreshError === "object" && refreshError && ("errorCode" in refreshError || "code" in refreshError)
|
|
164
|
-
? String(
|
|
165
|
-
(refreshError as Record<string, unknown>).errorCode || (refreshError as Record<string, unknown>).code || "",
|
|
166
|
-
)
|
|
167
|
-
: "";
|
|
168
|
-
const msgLower = message.toLowerCase();
|
|
169
|
-
const isInvalidGrant =
|
|
170
|
-
errorCode === "invalid_grant" || errorCode === "invalid_request" || msgLower.includes("invalid_grant");
|
|
171
|
-
const isTerminalStatus = status === 400 || status === 401 || status === 403;
|
|
172
|
-
return { message, status, errorCode, isInvalidGrant, isTerminalStatus };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function refreshAccountTokenSingleFlight(
|
|
176
|
-
account: ManagedAccount,
|
|
177
|
-
source: "foreground" | "idle" = "foreground",
|
|
178
|
-
): Promise<string> {
|
|
179
|
-
const key = account.id;
|
|
180
|
-
const existing = refreshInFlight.get(key);
|
|
181
|
-
if (existing) {
|
|
182
|
-
if (source === "foreground" && existing.source === "idle") {
|
|
183
|
-
try {
|
|
184
|
-
await existing.promise;
|
|
185
|
-
} catch {
|
|
186
|
-
/* ignore idle failure */
|
|
187
|
-
}
|
|
188
|
-
if (account.access && account.expires && account.expires > Date.now()) return account.access;
|
|
189
|
-
} else {
|
|
190
|
-
return existing.promise;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const entry: { promise: Promise<string>; source: "foreground" | "idle" } = {
|
|
195
|
-
source,
|
|
196
|
-
promise: Promise.resolve(""),
|
|
197
|
-
};
|
|
198
|
-
const p = (async () => {
|
|
199
|
-
try {
|
|
200
|
-
return await refreshAccountToken(account, client, source, {
|
|
201
|
-
onTokensUpdated: async () => {
|
|
202
|
-
try {
|
|
203
|
-
await accountManager!.saveToDisk();
|
|
204
|
-
} catch {
|
|
205
|
-
accountManager!.requestSaveToDisk();
|
|
206
|
-
throw new Error("save failed, debounced retry scheduled");
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
} finally {
|
|
211
|
-
if (refreshInFlight.get(key) === entry) refreshInFlight.delete(key);
|
|
212
|
-
}
|
|
213
|
-
})();
|
|
214
|
-
entry.promise = p;
|
|
215
|
-
refreshInFlight.set(key, entry);
|
|
216
|
-
return p;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function refreshIdleAccount(account: ManagedAccount) {
|
|
220
|
-
if (!accountManager) return;
|
|
221
|
-
if (idleRefreshInFlight.has(account.id)) return;
|
|
222
|
-
idleRefreshInFlight.add(account.id);
|
|
223
|
-
const attemptedRefreshToken = account.refreshToken;
|
|
224
|
-
try {
|
|
225
|
-
try {
|
|
226
|
-
await refreshAccountTokenSingleFlight(account, "idle");
|
|
227
|
-
return;
|
|
228
|
-
} catch (err) {
|
|
229
|
-
let details = parseRefreshFailure(err);
|
|
230
|
-
if (!(details.isInvalidGrant || details.isTerminalStatus)) {
|
|
231
|
-
debugLog("idle refresh skipped after transient failure", {
|
|
232
|
-
accountIndex: account.index,
|
|
233
|
-
status: details.status,
|
|
234
|
-
errorCode: details.errorCode,
|
|
235
|
-
message: details.message,
|
|
236
|
-
});
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
const diskAuth = await readDiskAccountAuth(account.id);
|
|
240
|
-
const retryToken = diskAuth?.refreshToken;
|
|
241
|
-
if (retryToken && retryToken !== attemptedRefreshToken && account.refreshToken === attemptedRefreshToken) {
|
|
242
|
-
account.refreshToken = retryToken;
|
|
243
|
-
if (diskAuth?.tokenUpdatedAt) account.tokenUpdatedAt = diskAuth.tokenUpdatedAt;
|
|
244
|
-
else markTokenStateUpdated(account);
|
|
245
|
-
}
|
|
246
|
-
try {
|
|
247
|
-
await refreshAccountTokenSingleFlight(account, "idle");
|
|
248
|
-
} catch (retryErr) {
|
|
249
|
-
details = parseRefreshFailure(retryErr);
|
|
250
|
-
debugLog("idle refresh retry failed", {
|
|
251
|
-
accountIndex: account.index,
|
|
252
|
-
status: details.status,
|
|
253
|
-
errorCode: details.errorCode,
|
|
254
|
-
message: details.message,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
} finally {
|
|
259
|
-
idleRefreshInFlight.delete(account.id);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function maybeRefreshIdleAccounts(activeAccount: ManagedAccount) {
|
|
264
|
-
if (!IDLE_REFRESH_ENABLED || !accountManager) return;
|
|
265
|
-
const now = Date.now();
|
|
266
|
-
const excluded = new Set([activeAccount.index]);
|
|
267
|
-
const candidates = accountManager
|
|
268
|
-
.getEnabledAccounts(excluded)
|
|
269
|
-
.filter((acc) => !acc.expires || acc.expires <= now + IDLE_REFRESH_WINDOW_MS)
|
|
270
|
-
.filter((acc) => {
|
|
271
|
-
const last = idleRefreshLastAttempt.get(acc.id) ?? 0;
|
|
272
|
-
return now - last >= IDLE_REFRESH_MIN_INTERVAL_MS;
|
|
273
|
-
})
|
|
274
|
-
.sort((a, b) => (a.expires ?? 0) - (b.expires ?? 0));
|
|
275
|
-
const target = candidates[0];
|
|
276
|
-
if (!target) return;
|
|
277
|
-
idleRefreshLastAttempt.set(target.id, now);
|
|
278
|
-
void refreshIdleAccount(target);
|
|
139
|
+
.catch((err) => debugLog("CC version fetch failed:", (err as Error).message));
|
|
279
140
|
}
|
|
280
141
|
|
|
281
142
|
// -- Command deps ----------------------------------------------------------
|
|
@@ -300,6 +161,11 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
300
161
|
// -- Plugin return ---------------------------------------------------------
|
|
301
162
|
|
|
302
163
|
return {
|
|
164
|
+
dispose: async () => {
|
|
165
|
+
await bunFetchInstance.shutdown();
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
303
169
|
"experimental.chat.system.transform": (input: Record<string, any>, output: Record<string, any>) => {
|
|
304
170
|
const prefix = CLAUDE_CODE_IDENTITY_STRING;
|
|
305
171
|
if (!signatureEmulationEnabled && input.model?.providerID === "anthropic") {
|
|
@@ -308,6 +174,7 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
308
174
|
}
|
|
309
175
|
},
|
|
310
176
|
|
|
177
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
311
178
|
config: async (input: Record<string, any>) => {
|
|
312
179
|
input.command ??= {};
|
|
313
180
|
input.command["anthropic"] = {
|
|
@@ -316,9 +183,11 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
316
183
|
};
|
|
317
184
|
},
|
|
318
185
|
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
319
187
|
"command.execute.before": async (input: Record<string, any>) => {
|
|
320
188
|
if (input.command !== "anthropic") return;
|
|
321
189
|
try {
|
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- command router accepts the Record-shaped input from OpenCode
|
|
322
191
|
await handleAnthropicSlashCommand(input as any, commandDeps);
|
|
323
192
|
} catch (err) {
|
|
324
193
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -329,10 +198,16 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
329
198
|
|
|
330
199
|
auth: {
|
|
331
200
|
provider: "anthropic",
|
|
332
|
-
async loader(
|
|
201
|
+
async loader(
|
|
202
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode auth loader API boundary
|
|
203
|
+
getAuth: () => Promise<Record<string, any>>,
|
|
204
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode auth loader API boundary
|
|
205
|
+
provider: Record<string, any>,
|
|
206
|
+
) {
|
|
333
207
|
const auth = await getAuth();
|
|
334
208
|
if (auth.type === "oauth") {
|
|
335
209
|
// Zero out cost for max plan and optionally override context limits.
|
|
210
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- model objects carry provider-specific metadata
|
|
336
211
|
for (const model of Object.values(provider.models) as Record<string, any>[]) {
|
|
337
212
|
model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } };
|
|
338
213
|
if (
|
|
@@ -362,8 +237,6 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
362
237
|
const ccCount = accountManager.getCCAccounts().length;
|
|
363
238
|
if (ccCount > 0) {
|
|
364
239
|
await toast(`Using Claude Code credentials (${ccCount} found)`, "success");
|
|
365
|
-
} else {
|
|
366
|
-
await toast("No Claude Code credentials — using OAuth", "info");
|
|
367
240
|
}
|
|
368
241
|
}
|
|
369
242
|
|
|
@@ -395,12 +268,37 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
395
268
|
|
|
396
269
|
return {
|
|
397
270
|
apiKey: "",
|
|
398
|
-
async fetch(
|
|
271
|
+
async fetch(
|
|
272
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- fetch input varies (string | URL | Request) across call sites
|
|
273
|
+
input: any,
|
|
274
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- fetch init is OpenCode-shaped RequestInit-plus
|
|
275
|
+
init: any,
|
|
276
|
+
) {
|
|
399
277
|
const currentAuth = await getAuth();
|
|
400
278
|
if (currentAuth.type !== "oauth") return fetch(input, init);
|
|
401
279
|
|
|
402
|
-
const requestInit = init ?? {};
|
|
280
|
+
const requestInit = { ...(init ?? {}) };
|
|
403
281
|
const { requestInput, requestUrl } = transformRequestUrl(input);
|
|
282
|
+
const resolvedBody =
|
|
283
|
+
requestInit.body !== undefined
|
|
284
|
+
? requestInit.body
|
|
285
|
+
: requestInput instanceof Request && requestInput.body
|
|
286
|
+
? await requestInput.clone().text()
|
|
287
|
+
: undefined;
|
|
288
|
+
if (resolvedBody !== undefined) {
|
|
289
|
+
requestInit.body = resolvedBody;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const requestContext: {
|
|
293
|
+
attempt: number;
|
|
294
|
+
cloneBody: string | undefined;
|
|
295
|
+
preparedBody: string | undefined;
|
|
296
|
+
} = {
|
|
297
|
+
attempt: 0,
|
|
298
|
+
cloneBody: typeof resolvedBody === "string" ? cloneBodyForRetry(resolvedBody) : undefined,
|
|
299
|
+
preparedBody: undefined,
|
|
300
|
+
};
|
|
301
|
+
|
|
404
302
|
const requestMethod = String(
|
|
405
303
|
requestInit.method || (requestInput instanceof Request ? requestInput.method : "POST"),
|
|
406
304
|
).toUpperCase();
|
|
@@ -447,6 +345,8 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
447
345
|
}
|
|
448
346
|
|
|
449
347
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
348
|
+
requestContext.attempt = attempt + 1;
|
|
349
|
+
|
|
450
350
|
const account =
|
|
451
351
|
attempt === 0 && pinnedAccount && !transientRefreshSkips.has(pinnedAccount.index)
|
|
452
352
|
? pinnedAccount
|
|
@@ -549,19 +449,30 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
549
449
|
|
|
550
450
|
maybeRefreshIdleAccounts(account);
|
|
551
451
|
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
452
|
+
const buildAttemptBody = () => {
|
|
453
|
+
const transformedBody = transformRequestBody(
|
|
454
|
+
requestContext.cloneBody === undefined ? undefined : cloneBodyForRetry(requestContext.cloneBody),
|
|
455
|
+
{
|
|
456
|
+
enabled: signatureEmulationEnabled,
|
|
457
|
+
claudeCliVersion,
|
|
458
|
+
promptCompactionMode,
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
persistentUserId: signatureUserId,
|
|
462
|
+
sessionId: signatureSessionId,
|
|
463
|
+
accountId: getAccountIdentifier(account),
|
|
464
|
+
},
|
|
465
|
+
config.relocate_third_party_prompts,
|
|
466
|
+
debugLog,
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
requestContext.preparedBody =
|
|
470
|
+
typeof transformedBody === "string" ? cloneBodyForRetry(transformedBody) : undefined;
|
|
471
|
+
|
|
472
|
+
return transformedBody;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const body = buildAttemptBody();
|
|
565
476
|
logTransformedSystemPrompt(body);
|
|
566
477
|
|
|
567
478
|
const requestHeaders = buildRequestHeaders(input, requestInit, accessToken!, body, requestUrl, {
|
|
@@ -605,7 +516,7 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
605
516
|
let response: Response;
|
|
606
517
|
const fetchInput = requestInput as string | URL | Request;
|
|
607
518
|
try {
|
|
608
|
-
response = await
|
|
519
|
+
response = await fetchWithTransport(fetchInput, {
|
|
609
520
|
...requestInit,
|
|
610
521
|
body,
|
|
611
522
|
headers: requestHeaders,
|
|
@@ -649,6 +560,7 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
649
560
|
reason,
|
|
650
561
|
});
|
|
651
562
|
accountManager.markRateLimited(account, reason, authOrPermissionIssue ? null : retryAfterMs);
|
|
563
|
+
transientRefreshSkips.add(account.index);
|
|
652
564
|
const name = account.email || `Account ${accountManager.getCurrentIndex() + 1}`;
|
|
653
565
|
const total = accountManager.getAccountCount();
|
|
654
566
|
if (total > 1) {
|
|
@@ -677,9 +589,13 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
677
589
|
headersForRetry.set("x-stainless-retry-count", String(retryCount));
|
|
678
590
|
retryCount += 1;
|
|
679
591
|
const retryUrl = fetchInput instanceof Request ? fetchInput.url : fetchInput.toString();
|
|
680
|
-
|
|
592
|
+
const retryBody =
|
|
593
|
+
requestContext.preparedBody === undefined
|
|
594
|
+
? undefined
|
|
595
|
+
: cloneBodyForRetry(requestContext.preparedBody);
|
|
596
|
+
return fetchWithTransport(retryUrl, {
|
|
681
597
|
...requestInit,
|
|
682
|
-
body,
|
|
598
|
+
body: retryBody,
|
|
683
599
|
headers: headersForRetry,
|
|
684
600
|
});
|
|
685
601
|
},
|
|
@@ -687,7 +603,7 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
687
603
|
);
|
|
688
604
|
|
|
689
605
|
if (!retried.ok) {
|
|
690
|
-
return
|
|
606
|
+
return finalizeResponse(retried);
|
|
691
607
|
}
|
|
692
608
|
|
|
693
609
|
response = retried;
|
|
@@ -695,7 +611,7 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
695
611
|
debugLog("non-account-specific response error, returning directly", {
|
|
696
612
|
status: response.status,
|
|
697
613
|
});
|
|
698
|
-
return
|
|
614
|
+
return finalizeResponse(response);
|
|
699
615
|
}
|
|
700
616
|
}
|
|
701
617
|
|
|
@@ -711,7 +627,8 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
711
627
|
}
|
|
712
628
|
: null;
|
|
713
629
|
const accountErrorCallback = shouldInspectStream
|
|
714
|
-
?
|
|
630
|
+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any -- reason carries opaque rate-limit metadata; shape stabilized at callsite
|
|
631
|
+
(details: { reason: any; invalidateToken: boolean }) => {
|
|
715
632
|
if (details.invalidateToken) {
|
|
716
633
|
account.access = undefined;
|
|
717
634
|
account.expires = undefined;
|
|
@@ -720,8 +637,22 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
720
637
|
accountManager!.markRateLimited(account, details.reason, null);
|
|
721
638
|
}
|
|
722
639
|
: null;
|
|
640
|
+
const streamErrorCallback =
|
|
641
|
+
response.ok && isEventStreamResponse(response)
|
|
642
|
+
? (error: Error) => {
|
|
643
|
+
if (!(error instanceof StreamTruncatedError)) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
723
646
|
|
|
724
|
-
|
|
647
|
+
debugLog("stream truncated during response consumption", {
|
|
648
|
+
accountIndex: account?.index,
|
|
649
|
+
message: error.message,
|
|
650
|
+
context: error.context,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
: null;
|
|
654
|
+
|
|
655
|
+
return finalizeResponse(response, usageCallback, accountErrorCallback, streamErrorCallback);
|
|
725
656
|
}
|
|
726
657
|
|
|
727
658
|
if (lastError) throw lastError;
|
|
@@ -759,14 +690,39 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
759
690
|
accountManager = await AccountManager.load(config, null);
|
|
760
691
|
}
|
|
761
692
|
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
693
|
+
const identity = resolveIdentityFromCCCredential(ccCred);
|
|
694
|
+
const existing = findByIdentity(accountManager.getCCAccounts(), identity);
|
|
695
|
+
|
|
696
|
+
if (existing) {
|
|
697
|
+
existing.refreshToken = ccCred.refreshToken;
|
|
698
|
+
existing.identity = identity;
|
|
699
|
+
existing.source = ccCred.source;
|
|
700
|
+
existing.label = ccCred.label;
|
|
701
|
+
existing.enabled = true;
|
|
702
|
+
if (ccCred.accessToken) {
|
|
703
|
+
existing.access = ccCred.accessToken;
|
|
704
|
+
}
|
|
705
|
+
if (ccCred.expiresAt >= (existing.expires ?? 0)) {
|
|
706
|
+
existing.expires = ccCred.expiresAt;
|
|
707
|
+
}
|
|
708
|
+
existing.tokenUpdatedAt = Math.max(existing.tokenUpdatedAt || 0, ccCred.expiresAt || 0);
|
|
709
|
+
await accountManager.saveToDisk();
|
|
710
|
+
} else {
|
|
711
|
+
const added = accountManager.addAccount(
|
|
712
|
+
ccCred.refreshToken,
|
|
713
|
+
ccCred.accessToken,
|
|
714
|
+
ccCred.expiresAt,
|
|
715
|
+
undefined,
|
|
716
|
+
{
|
|
717
|
+
identity,
|
|
718
|
+
label: ccCred.label,
|
|
719
|
+
source: ccCred.source,
|
|
720
|
+
},
|
|
721
|
+
);
|
|
768
722
|
if (added) {
|
|
769
723
|
added.source = ccCred.source;
|
|
724
|
+
added.label = ccCred.label;
|
|
725
|
+
added.identity = identity;
|
|
770
726
|
}
|
|
771
727
|
await accountManager.saveToDisk();
|
|
772
728
|
await toast("Added Claude Code credentials", "success");
|
|
@@ -829,17 +785,38 @@ export async function AnthropicAuthPlugin({ client }: { client: OpenCodeClient &
|
|
|
829
785
|
if (!accountManager) {
|
|
830
786
|
accountManager = await AccountManager.load(config, null);
|
|
831
787
|
}
|
|
788
|
+
const identity = resolveIdentityFromOAuthExchange(credentials);
|
|
832
789
|
const countBefore = accountManager.getAccountCount();
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
credentials.
|
|
838
|
-
|
|
790
|
+
const existing =
|
|
791
|
+
identity.kind === "oauth" ? findByIdentity(accountManager.getOAuthAccounts(), identity) : null;
|
|
792
|
+
|
|
793
|
+
if (existing) {
|
|
794
|
+
existing.refreshToken = credentials.refresh;
|
|
795
|
+
existing.access = credentials.access;
|
|
796
|
+
existing.expires = credentials.expires;
|
|
797
|
+
existing.email = credentials.email ?? existing.email;
|
|
798
|
+
existing.identity = identity;
|
|
799
|
+
existing.source = "oauth";
|
|
800
|
+
existing.enabled = true;
|
|
801
|
+
existing.tokenUpdatedAt = Date.now();
|
|
802
|
+
} else {
|
|
803
|
+
accountManager.addAccount(
|
|
804
|
+
credentials.refresh,
|
|
805
|
+
credentials.access,
|
|
806
|
+
credentials.expires,
|
|
807
|
+
credentials.email,
|
|
808
|
+
{
|
|
809
|
+
identity,
|
|
810
|
+
source: "oauth",
|
|
811
|
+
},
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
839
815
|
await accountManager.saveToDisk();
|
|
840
816
|
const total = accountManager.getAccountCount();
|
|
841
817
|
const name = credentials.email || "account";
|
|
842
|
-
if (
|
|
818
|
+
if (existing) await toast(`Re-authenticated (${name})`, "success");
|
|
819
|
+
else if (countBefore > 0) await toast(`Added ${name} — ${total} accounts`, "success");
|
|
843
820
|
else await toast(`Authenticated (${name})`, "success");
|
|
844
821
|
return credentials;
|
|
845
822
|
},
|