@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/index.ts
CHANGED
|
@@ -5,891 +5,499 @@
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import type { ManagedAccount } from "./accounts.js";
|
|
7
7
|
import { AccountManager } from "./accounts.js";
|
|
8
|
-
import { isAccountSpecificError, parseRateLimitReason, parseRetryAfterHeader } from "./backoff.js";
|
|
9
8
|
import type { PendingOAuthEntry } from "./commands/oauth-flow.js";
|
|
10
9
|
import { promptAccountMenu, promptManageAccounts } from "./commands/prompts.js";
|
|
11
10
|
import type { CommandDeps } from "./commands/router.js";
|
|
12
11
|
import { ANTHROPIC_COMMAND_HANDLED, handleAnthropicSlashCommand } from "./commands/router.js";
|
|
13
12
|
import type { AnthropicAuthConfig } from "./config.js";
|
|
14
13
|
import { loadConfig } from "./config.js";
|
|
15
|
-
import { CLAUDE_CODE_IDENTITY_STRING, FALLBACK_CLAUDE_CLI_VERSION
|
|
16
|
-
import { getOrCreateSignatureUserId, isTruthyEnv
|
|
17
|
-
import { buildRequestHeaders } from "./headers/builder.js";
|
|
14
|
+
import { CLAUDE_CODE_IDENTITY_STRING, FALLBACK_CLAUDE_CLI_VERSION } from "./constants.js";
|
|
15
|
+
import { getOrCreateSignatureUserId, isTruthyEnv } from "./env.js";
|
|
18
16
|
import { fetchLatestClaudeCodeVersion } from "./headers/user-agent.js";
|
|
19
17
|
import { hasOneMillionContext, isOpus46Model } from "./models.js";
|
|
20
18
|
import { authorize, exchange } from "./oauth.js";
|
|
21
|
-
import { cloneBodyForRetry, transformRequestBody } from "./request/body.js";
|
|
22
|
-
import { extractFileIds, getAccountIdentifier } from "./request/metadata.js";
|
|
23
|
-
import { fetchWithRetry } from "./request/retry.js";
|
|
24
|
-
import { transformRequestUrl } from "./request/url.js";
|
|
25
|
-
import { isEventStreamResponse, stripMcpPrefixFromJsonBody, transformResponse } from "./response/index.js";
|
|
26
|
-
import { StreamTruncatedError } from "./response/streaming.js";
|
|
27
19
|
import { readCCCredentials } from "./cc-credentials.js";
|
|
28
20
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
findByIdentity,
|
|
22
|
+
resolveIdentityFromCCCredential,
|
|
23
|
+
resolveIdentityFromOAuthExchange,
|
|
32
24
|
} from "./account-identity.js";
|
|
33
25
|
import { clearAccounts, loadAccounts } from "./storage.js";
|
|
34
26
|
import type { OpenCodeClient } from "./token-refresh.js";
|
|
35
|
-
import { formatSwitchReason, markTokenStateUpdated, readDiskAccountAuth } from "./token-refresh.js";
|
|
36
|
-
import type { UsageStats } from "./types.js";
|
|
37
27
|
import { createBunFetch } from "./bun-fetch.js";
|
|
38
28
|
import { createPluginHelpers } from "./plugin-helpers.js";
|
|
39
29
|
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
|
-
}
|
|
30
|
+
import { createRequestOrchestrationHelpers } from "./request-orchestration-helpers.js";
|
|
58
31
|
|
|
59
32
|
// ---------------------------------------------------------------------------
|
|
60
33
|
// Plugin factory
|
|
61
34
|
// ---------------------------------------------------------------------------
|
|
62
35
|
|
|
63
36
|
export async function AnthropicAuthPlugin({
|
|
64
|
-
|
|
37
|
+
client,
|
|
65
38
|
}: {
|
|
66
|
-
|
|
67
|
-
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin client API boundary; accepts arbitrary extension methods
|
|
40
|
+
client: OpenCodeClient & Record<string, any>;
|
|
68
41
|
}) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- plugin config accepts forward-compatible arbitrary keys
|
|
43
|
+
const config: AnthropicAuthConfig & Record<string, any> = loadConfig();
|
|
44
|
+
const signatureEmulationEnabled = config.signature_emulation.enabled;
|
|
45
|
+
const promptCompactionMode =
|
|
46
|
+
config.signature_emulation.prompt_compaction === "off" ? ("off" as const) : ("minimal" as const);
|
|
47
|
+
const signatureSanitizeSystemPrompt = config.signature_emulation.sanitize_system_prompt === true;
|
|
48
|
+
const shouldFetchClaudeCodeVersion =
|
|
49
|
+
signatureEmulationEnabled && config.signature_emulation.fetch_claude_code_version_on_startup;
|
|
50
|
+
|
|
51
|
+
let accountManager: AccountManager | null = null;
|
|
52
|
+
|
|
53
|
+
// Track account usage toasts; show once per account change (including first use).
|
|
54
|
+
let lastToastedIndex = -1;
|
|
55
|
+
|
|
56
|
+
let initialAccountPinned = false;
|
|
57
|
+
const pendingSlashOAuth = new Map<string, PendingOAuthEntry>();
|
|
58
|
+
const fileAccountMap = new Map<string, number>();
|
|
59
|
+
|
|
60
|
+
// -- Helpers ---------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
function debugLog(...args: unknown[]) {
|
|
63
|
+
if (!config.debug) return;
|
|
64
|
+
// eslint-disable-next-line no-console -- this IS the plugin's dedicated debug logger; gated on config.debug
|
|
65
|
+
console.error("[opencode-anthropic-auth]", ...args);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { toast, sendCommandMessage, runCliCommand, reloadAccountManagerFromDisk, persistOpenCodeAuth } =
|
|
69
|
+
createPluginHelpers({
|
|
70
|
+
client,
|
|
71
|
+
config,
|
|
72
|
+
debugLog,
|
|
73
|
+
getAccountManager: () => accountManager,
|
|
74
|
+
setAccountManager: (nextAccountManager) => {
|
|
75
|
+
accountManager = nextAccountManager;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const { parseRefreshFailure, refreshAccountTokenSingleFlight, maybeRefreshIdleAccounts } = createRefreshHelpers({
|
|
80
|
+
client,
|
|
81
|
+
config,
|
|
82
|
+
getAccountManager: () => accountManager,
|
|
83
|
+
debugLog,
|
|
84
|
+
});
|
|
85
|
+
const bunFetchInstance = createBunFetch({
|
|
86
|
+
debug: config.debug,
|
|
87
|
+
onProxyStatus: (status) => {
|
|
88
|
+
debugLog("bun fetch status", status);
|
|
89
|
+
},
|
|
104
90
|
});
|
|
105
91
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
92
|
+
const fetchWithTransport = async (input: string | URL | Request, init: RequestInit): Promise<Response> => {
|
|
93
|
+
const activeFetch = globalThis.fetch as typeof globalThis.fetch & { mock?: unknown };
|
|
94
|
+
if (typeof activeFetch === "function" && activeFetch.mock) {
|
|
95
|
+
return activeFetch(input, init);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return bunFetchInstance.fetch(input, init);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// -- Version resolution ----------------------------------------------------
|
|
102
|
+
|
|
103
|
+
let claudeCliVersion = FALLBACK_CLAUDE_CLI_VERSION;
|
|
104
|
+
const signatureSessionId = randomUUID();
|
|
105
|
+
const signatureUserId = getOrCreateSignatureUserId();
|
|
106
|
+
if (shouldFetchClaudeCodeVersion) {
|
|
107
|
+
fetchLatestClaudeCodeVersion()
|
|
108
|
+
.then((version) => {
|
|
109
|
+
if (!version) return;
|
|
110
|
+
claudeCliVersion = version;
|
|
111
|
+
debugLog("resolved claude-code version from npm", version);
|
|
112
|
+
})
|
|
113
|
+
.catch((err) => debugLog("CC version fetch failed:", (err as Error).message));
|
|
123
114
|
}
|
|
124
115
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
get accountManager() {
|
|
148
|
-
return accountManager;
|
|
149
|
-
},
|
|
150
|
-
runCliCommand,
|
|
151
|
-
config,
|
|
152
|
-
fileAccountMap,
|
|
153
|
-
get initialAccountPinned() {
|
|
154
|
-
return initialAccountPinned;
|
|
155
|
-
},
|
|
156
|
-
pendingSlashOAuth,
|
|
157
|
-
reloadAccountManagerFromDisk,
|
|
158
|
-
persistOpenCodeAuth,
|
|
159
|
-
refreshAccountTokenSingleFlight,
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
// -- Plugin return ---------------------------------------------------------
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
dispose: async () => {
|
|
166
|
-
await bunFetchInstance.shutdown();
|
|
167
|
-
},
|
|
168
|
-
|
|
169
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
170
|
-
"experimental.chat.system.transform": (input: Record<string, any>, output: Record<string, any>) => {
|
|
171
|
-
const prefix = CLAUDE_CODE_IDENTITY_STRING;
|
|
172
|
-
if (!signatureEmulationEnabled && input.model?.providerID === "anthropic") {
|
|
173
|
-
output.system.unshift(prefix);
|
|
174
|
-
if (output.system[1]) output.system[1] = prefix + "\n\n" + output.system[1];
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
|
|
178
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
179
|
-
config: async (input: Record<string, any>) => {
|
|
180
|
-
input.command ??= {};
|
|
181
|
-
input.command["anthropic"] = {
|
|
182
|
-
template: "/anthropic",
|
|
183
|
-
description: "Manage Anthropic auth, config, and betas (usage, login, config, set, betas, switch)",
|
|
184
|
-
};
|
|
185
|
-
},
|
|
186
|
-
|
|
187
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
188
|
-
"command.execute.before": async (input: Record<string, any>) => {
|
|
189
|
-
if (input.command !== "anthropic") return;
|
|
190
|
-
try {
|
|
191
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- command router accepts the Record-shaped input from OpenCode
|
|
192
|
-
await handleAnthropicSlashCommand(input as any, commandDeps);
|
|
193
|
-
} catch (err) {
|
|
194
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
195
|
-
await sendCommandMessage(input.sessionID, `▣ Anthropic (error)\n\n${message}`);
|
|
196
|
-
}
|
|
197
|
-
throw new Error(ANTHROPIC_COMMAND_HANDLED);
|
|
198
|
-
},
|
|
199
|
-
|
|
200
|
-
auth: {
|
|
201
|
-
provider: "anthropic",
|
|
202
|
-
async loader(
|
|
203
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode auth loader API boundary
|
|
204
|
-
getAuth: () => Promise<Record<string, any>>,
|
|
205
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode auth loader API boundary
|
|
206
|
-
provider: Record<string, any>,
|
|
207
|
-
) {
|
|
208
|
-
const auth = await getAuth();
|
|
209
|
-
if (auth.type === "oauth") {
|
|
210
|
-
// Zero out cost for max plan and optionally override context limits.
|
|
211
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- model objects carry provider-specific metadata
|
|
212
|
-
for (const model of Object.values(provider.models) as Record<string, any>[]) {
|
|
213
|
-
model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } };
|
|
214
|
-
if (
|
|
215
|
-
config.override_model_limits.enabled &&
|
|
216
|
-
!isTruthyEnv(process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT) &&
|
|
217
|
-
(hasOneMillionContext(model.id) || isOpus46Model(model.id))
|
|
218
|
-
) {
|
|
219
|
-
model.limit = {
|
|
220
|
-
...(model.limit ?? {}),
|
|
221
|
-
context: config.override_model_limits.context,
|
|
222
|
-
...(config.override_model_limits.output > 0 ? { output: config.override_model_limits.output } : {}),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Initialize AccountManager from disk + OpenCode auth fallback
|
|
228
|
-
accountManager = await AccountManager.load(config, {
|
|
229
|
-
refresh: auth.refresh,
|
|
230
|
-
access: auth.access,
|
|
231
|
-
expires: auth.expires,
|
|
232
|
-
});
|
|
233
|
-
if (accountManager.getAccountCount() > 0) {
|
|
234
|
-
await accountManager.saveToDisk();
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (config.cc_credential_reuse?.enabled && config.cc_credential_reuse?.auto_detect) {
|
|
238
|
-
const ccCount = accountManager.getCCAccounts().length;
|
|
239
|
-
if (ccCount > 0) {
|
|
240
|
-
await toast(`Using Claude Code credentials (${ccCount} found)`, "success");
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// OPENCODE_ANTHROPIC_INITIAL_ACCOUNT: pin to a specific account
|
|
245
|
-
const initialAccountEnv = process.env.OPENCODE_ANTHROPIC_INITIAL_ACCOUNT?.trim();
|
|
246
|
-
if (initialAccountEnv && accountManager.getAccountCount() > 1) {
|
|
247
|
-
const accounts = accountManager.getEnabledAccounts();
|
|
248
|
-
let target: ManagedAccount | null = null;
|
|
249
|
-
const asIndex = parseInt(initialAccountEnv, 10);
|
|
250
|
-
if (!isNaN(asIndex) && asIndex >= 1 && asIndex <= accounts.length) {
|
|
251
|
-
target = accounts[asIndex - 1];
|
|
252
|
-
}
|
|
253
|
-
if (!target) {
|
|
254
|
-
target =
|
|
255
|
-
accounts.find((a) => a.email && a.email.toLowerCase() === initialAccountEnv.toLowerCase()) ?? null;
|
|
256
|
-
}
|
|
257
|
-
if (target && accountManager.forceCurrentIndex(target.index)) {
|
|
258
|
-
config.account_selection_strategy = "sticky";
|
|
259
|
-
initialAccountPinned = true;
|
|
260
|
-
debugLog("OPENCODE_ANTHROPIC_INITIAL_ACCOUNT: pinned to account", {
|
|
261
|
-
index: target.index + 1,
|
|
262
|
-
email: target.email,
|
|
263
|
-
strategy: "sticky (overridden)",
|
|
264
|
-
});
|
|
265
|
-
} else {
|
|
266
|
-
debugLog("OPENCODE_ANTHROPIC_INITIAL_ACCOUNT: could not resolve account", initialAccountEnv);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
apiKey: "",
|
|
272
|
-
async fetch(
|
|
273
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- fetch input varies (string | URL | Request) across call sites
|
|
274
|
-
input: any,
|
|
275
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- fetch init is OpenCode-shaped RequestInit-plus
|
|
276
|
-
init: any,
|
|
277
|
-
) {
|
|
278
|
-
const currentAuth = await getAuth();
|
|
279
|
-
if (currentAuth.type !== "oauth") return fetch(input, init);
|
|
280
|
-
|
|
281
|
-
const requestInit = { ...(init ?? {}) };
|
|
282
|
-
const { requestInput, requestUrl } = transformRequestUrl(input);
|
|
283
|
-
const resolvedBody =
|
|
284
|
-
requestInit.body !== undefined
|
|
285
|
-
? requestInit.body
|
|
286
|
-
: requestInput instanceof Request && requestInput.body
|
|
287
|
-
? await requestInput.clone().text()
|
|
288
|
-
: undefined;
|
|
289
|
-
if (resolvedBody !== undefined) {
|
|
290
|
-
requestInit.body = resolvedBody;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const requestContext: {
|
|
294
|
-
attempt: number;
|
|
295
|
-
cloneBody: string | undefined;
|
|
296
|
-
preparedBody: string | undefined;
|
|
297
|
-
} = {
|
|
298
|
-
attempt: 0,
|
|
299
|
-
cloneBody: typeof resolvedBody === "string" ? cloneBodyForRetry(resolvedBody) : undefined,
|
|
300
|
-
preparedBody: undefined,
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
const requestMethod = String(
|
|
304
|
-
requestInit.method || (requestInput instanceof Request ? requestInput.method : "POST"),
|
|
305
|
-
).toUpperCase();
|
|
306
|
-
let showUsageToast: boolean;
|
|
307
|
-
try {
|
|
308
|
-
showUsageToast = new URL(requestUrl!).pathname === "/v1/messages" && requestMethod === "POST";
|
|
309
|
-
} catch {
|
|
310
|
-
showUsageToast = false;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
let lastError: unknown = null;
|
|
314
|
-
const transientRefreshSkips = new Set<number>();
|
|
315
|
-
|
|
316
|
-
if (accountManager && !initialAccountPinned) {
|
|
317
|
-
await accountManager.syncActiveIndexFromDisk();
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const maxAttempts = accountManager!.getTotalAccountCount();
|
|
321
|
-
|
|
322
|
-
// File-ID account pinning
|
|
323
|
-
let pinnedAccount: ManagedAccount | null = null;
|
|
324
|
-
if (typeof requestInit.body === "string" && fileAccountMap.size > 0) {
|
|
325
|
-
try {
|
|
326
|
-
const bodyObj = JSON.parse(requestInit.body);
|
|
327
|
-
const fileIds = extractFileIds(bodyObj);
|
|
328
|
-
for (const fid of fileIds) {
|
|
329
|
-
const pinnedIndex = fileAccountMap.get(fid);
|
|
330
|
-
if (pinnedIndex !== undefined) {
|
|
331
|
-
const candidates = accountManager!.getEnabledAccounts();
|
|
332
|
-
pinnedAccount = candidates.find((a) => a.index === pinnedIndex) ?? null;
|
|
333
|
-
if (pinnedAccount) {
|
|
334
|
-
debugLog("file-id pinning: routing to account", {
|
|
335
|
-
fileId: fid,
|
|
336
|
-
accountIndex: pinnedIndex,
|
|
337
|
-
email: pinnedAccount.email,
|
|
338
|
-
});
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
} catch {
|
|
344
|
-
/* Non-JSON body */
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
349
|
-
requestContext.attempt = attempt + 1;
|
|
350
|
-
|
|
351
|
-
const account =
|
|
352
|
-
attempt === 0 && pinnedAccount && !transientRefreshSkips.has(pinnedAccount.index)
|
|
353
|
-
? pinnedAccount
|
|
354
|
-
: accountManager!.getCurrentAccount(transientRefreshSkips);
|
|
355
|
-
|
|
356
|
-
// Toast account usage
|
|
357
|
-
if (showUsageToast && account && accountManager) {
|
|
358
|
-
const currentIndex = accountManager.getCurrentIndex();
|
|
359
|
-
if (currentIndex !== lastToastedIndex) {
|
|
360
|
-
const name = account.email || `Account ${currentIndex + 1}`;
|
|
361
|
-
const total = accountManager.getAccountCount();
|
|
362
|
-
const msg = total > 1 ? `Claude: ${name} (${currentIndex + 1}/${total})` : `Claude: ${name}`;
|
|
363
|
-
await toast(msg, "info", { debounceKey: "account-usage" });
|
|
364
|
-
lastToastedIndex = currentIndex;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
116
|
+
const { executeOAuthFetch } = createRequestOrchestrationHelpers({
|
|
117
|
+
config,
|
|
118
|
+
debugLog,
|
|
119
|
+
toast,
|
|
120
|
+
getAccountManager: () => accountManager,
|
|
121
|
+
getClaudeCliVersion: () => claudeCliVersion,
|
|
122
|
+
getInitialAccountPinned: () => initialAccountPinned,
|
|
123
|
+
getLastToastedIndex: () => lastToastedIndex,
|
|
124
|
+
setLastToastedIndex: (index) => {
|
|
125
|
+
lastToastedIndex = index;
|
|
126
|
+
},
|
|
127
|
+
fileAccountMap,
|
|
128
|
+
fetchWithTransport,
|
|
129
|
+
parseRefreshFailure,
|
|
130
|
+
refreshAccountTokenSingleFlight,
|
|
131
|
+
maybeRefreshIdleAccounts,
|
|
132
|
+
signatureEmulationEnabled,
|
|
133
|
+
promptCompactionMode,
|
|
134
|
+
signatureSanitizeSystemPrompt,
|
|
135
|
+
signatureSessionId,
|
|
136
|
+
signatureUserId,
|
|
137
|
+
});
|
|
367
138
|
|
|
368
|
-
|
|
369
|
-
const enabledCount = accountManager!.getAccountCount();
|
|
370
|
-
if (enabledCount === 0) {
|
|
371
|
-
throw new Error(
|
|
372
|
-
"No enabled Anthropic accounts available. Enable one with 'opencode-anthropic-auth enable <N>'.",
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
throw new Error("No available Anthropic account for request.");
|
|
376
|
-
}
|
|
139
|
+
// -- Command deps ----------------------------------------------------------
|
|
377
140
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
account.refreshToken === attemptedRefreshToken
|
|
395
|
-
) {
|
|
396
|
-
debugLog("refresh token on disk differs from in-memory, retrying with disk token", {
|
|
397
|
-
accountIndex: account.index,
|
|
398
|
-
});
|
|
399
|
-
account.refreshToken = retryToken;
|
|
400
|
-
if (diskAuth?.tokenUpdatedAt) account.tokenUpdatedAt = diskAuth.tokenUpdatedAt;
|
|
401
|
-
else markTokenStateUpdated(account);
|
|
402
|
-
} else if (retryToken && retryToken !== attemptedRefreshToken) {
|
|
403
|
-
debugLog("skipping disk token adoption because in-memory token already changed", {
|
|
404
|
-
accountIndex: account.index,
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
try {
|
|
408
|
-
accessToken = await refreshAccountTokenSingleFlight(account);
|
|
409
|
-
} catch (retryErr) {
|
|
410
|
-
finalError = retryErr;
|
|
411
|
-
details = parseRefreshFailure(retryErr);
|
|
412
|
-
debugLog("retry refresh failed", {
|
|
413
|
-
accountIndex: account.index,
|
|
414
|
-
status: details.status,
|
|
415
|
-
errorCode: details.errorCode,
|
|
416
|
-
message: details.message,
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
}
|
|
141
|
+
const commandDeps: CommandDeps = {
|
|
142
|
+
sendCommandMessage,
|
|
143
|
+
get accountManager() {
|
|
144
|
+
return accountManager;
|
|
145
|
+
},
|
|
146
|
+
runCliCommand,
|
|
147
|
+
config,
|
|
148
|
+
fileAccountMap,
|
|
149
|
+
get initialAccountPinned() {
|
|
150
|
+
return initialAccountPinned;
|
|
151
|
+
},
|
|
152
|
+
pendingSlashOAuth,
|
|
153
|
+
reloadAccountManagerFromDisk,
|
|
154
|
+
persistOpenCodeAuth,
|
|
155
|
+
refreshAccountTokenSingleFlight,
|
|
156
|
+
};
|
|
420
157
|
|
|
421
|
-
|
|
422
|
-
accountManager!.markFailure(account);
|
|
423
|
-
if (details.isInvalidGrant || details.isTerminalStatus) {
|
|
424
|
-
const name = account.email || `Account ${accountManager!.getCurrentIndex() + 1}`;
|
|
425
|
-
debugLog("disabling account after terminal refresh failure", {
|
|
426
|
-
accountIndex: account.index,
|
|
427
|
-
status: details.status,
|
|
428
|
-
errorCode: details.errorCode,
|
|
429
|
-
message: details.message,
|
|
430
|
-
});
|
|
431
|
-
account.enabled = false;
|
|
432
|
-
accountManager!.requestSaveToDisk();
|
|
433
|
-
const statusLabel = Number.isFinite(details.status)
|
|
434
|
-
? `HTTP ${details.status}`
|
|
435
|
-
: "unknown status";
|
|
436
|
-
await toast(
|
|
437
|
-
`Disabled ${name} (token refresh failed: ${details.errorCode || statusLabel})`,
|
|
438
|
-
"error",
|
|
439
|
-
);
|
|
440
|
-
} else {
|
|
441
|
-
transientRefreshSkips.add(account.index);
|
|
442
|
-
}
|
|
443
|
-
lastError = finalError;
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
} else {
|
|
448
|
-
accessToken = account.access;
|
|
449
|
-
}
|
|
158
|
+
// -- Plugin return ---------------------------------------------------------
|
|
450
159
|
|
|
451
|
-
|
|
160
|
+
return {
|
|
161
|
+
dispose: async () => {
|
|
162
|
+
await bunFetchInstance.shutdown();
|
|
163
|
+
},
|
|
452
164
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
},
|
|
462
|
-
{
|
|
463
|
-
persistentUserId: signatureUserId,
|
|
464
|
-
sessionId: signatureSessionId,
|
|
465
|
-
accountId: getAccountIdentifier(account),
|
|
466
|
-
},
|
|
467
|
-
config.relocate_third_party_prompts,
|
|
468
|
-
debugLog,
|
|
469
|
-
);
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
166
|
+
"experimental.chat.system.transform": (input: Record<string, any>, output: Record<string, any>) => {
|
|
167
|
+
const prefix = CLAUDE_CODE_IDENTITY_STRING;
|
|
168
|
+
if (!signatureEmulationEnabled && input.model?.providerID === "anthropic") {
|
|
169
|
+
output.system.unshift(prefix);
|
|
170
|
+
if (output.system[1]) output.system[1] = prefix + "\n\n" + output.system[1];
|
|
171
|
+
}
|
|
172
|
+
},
|
|
470
173
|
|
|
471
|
-
|
|
472
|
-
|
|
174
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
175
|
+
config: async (input: Record<string, any>) => {
|
|
176
|
+
input.command ??= {};
|
|
177
|
+
input.command["anthropic"] = {
|
|
178
|
+
template: "/anthropic",
|
|
179
|
+
description: "Manage Anthropic auth, config, and betas (usage, login, config, set, betas, switch)",
|
|
180
|
+
};
|
|
181
|
+
},
|
|
473
182
|
|
|
474
|
-
|
|
183
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin hook API boundary
|
|
184
|
+
"command.execute.before": async (input: Record<string, any>) => {
|
|
185
|
+
if (input.command !== "anthropic") return;
|
|
186
|
+
try {
|
|
187
|
+
const slashInput = {
|
|
188
|
+
command: String(input.command),
|
|
189
|
+
arguments: typeof input.arguments === "string" ? input.arguments : undefined,
|
|
190
|
+
sessionID: String(input.sessionID),
|
|
475
191
|
};
|
|
192
|
+
await handleAnthropicSlashCommand(slashInput, commandDeps);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
195
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic (error)\n\n${message}`);
|
|
196
|
+
}
|
|
197
|
+
throw new Error(ANTHROPIC_COMMAND_HANDLED);
|
|
198
|
+
},
|
|
476
199
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
200
|
+
auth: {
|
|
201
|
+
provider: "anthropic",
|
|
202
|
+
async loader(
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode auth loader API boundary
|
|
204
|
+
getAuth: () => Promise<Record<string, any>>,
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode auth loader API boundary
|
|
206
|
+
provider: Record<string, any>,
|
|
207
|
+
) {
|
|
208
|
+
const auth = await getAuth();
|
|
209
|
+
if (auth.type === "oauth") {
|
|
210
|
+
// Zero out cost for max plan and optionally override context limits.
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- model objects carry provider-specific metadata
|
|
212
|
+
for (const model of Object.values(provider.models) as Record<string, any>[]) {
|
|
213
|
+
model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } };
|
|
214
|
+
if (
|
|
215
|
+
config.override_model_limits.enabled &&
|
|
216
|
+
!isTruthyEnv(process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT) &&
|
|
217
|
+
(hasOneMillionContext(model.id) || isOpus46Model(model.id))
|
|
218
|
+
) {
|
|
219
|
+
model.limit = {
|
|
220
|
+
...(model.limit ?? {}),
|
|
221
|
+
context: config.override_model_limits.context,
|
|
222
|
+
...(config.override_model_limits.output > 0
|
|
223
|
+
? { output: config.override_model_limits.output }
|
|
224
|
+
: {}),
|
|
225
|
+
};
|
|
503
226
|
}
|
|
504
|
-
return undefined;
|
|
505
|
-
})()
|
|
506
|
-
: undefined;
|
|
507
|
-
|
|
508
|
-
debugLog("fingerprint snapshot", {
|
|
509
|
-
billingHeader: billingBlock ?? "(not in system prompt)",
|
|
510
|
-
userAgent: requestHeaders.get("user-agent"),
|
|
511
|
-
anthropicBeta: requestHeaders.get("anthropic-beta"),
|
|
512
|
-
stainlessPackageVersion: requestHeaders.get("x-stainless-package-version"),
|
|
513
|
-
xApp: requestHeaders.get("x-app"),
|
|
514
|
-
claudeCliVersion,
|
|
515
|
-
signatureEnabled: signatureEmulationEnabled,
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
let response: Response;
|
|
520
|
-
const fetchInput = requestInput as string | URL | Request;
|
|
521
|
-
const buildTransportRequestInit = (
|
|
522
|
-
headers: Headers,
|
|
523
|
-
requestBody: RequestInit["body"],
|
|
524
|
-
forceFreshConnection: boolean,
|
|
525
|
-
): RequestInit => {
|
|
526
|
-
const requestHeadersForTransport = new Headers(headers);
|
|
527
|
-
if (forceFreshConnection) {
|
|
528
|
-
requestHeadersForTransport.set("connection", "close");
|
|
529
|
-
requestHeadersForTransport.set("x-proxy-disable-keepalive", "true");
|
|
530
|
-
} else {
|
|
531
|
-
requestHeadersForTransport.delete("connection");
|
|
532
|
-
requestHeadersForTransport.delete("x-proxy-disable-keepalive");
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return {
|
|
536
|
-
...requestInit,
|
|
537
|
-
body: requestBody,
|
|
538
|
-
headers: requestHeadersForTransport,
|
|
539
|
-
...(forceFreshConnection ? { keepalive: false } : {}),
|
|
540
|
-
};
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
try {
|
|
544
|
-
response = await fetchWithRetry(
|
|
545
|
-
async ({ forceFreshConnection }) =>
|
|
546
|
-
fetchWithTransport(
|
|
547
|
-
fetchInput,
|
|
548
|
-
buildTransportRequestInit(requestHeaders, body, forceFreshConnection),
|
|
549
|
-
),
|
|
550
|
-
{
|
|
551
|
-
maxRetries: 2,
|
|
552
|
-
shouldRetryResponse: () => false,
|
|
553
|
-
},
|
|
554
|
-
);
|
|
555
|
-
} catch (err) {
|
|
556
|
-
const fetchError = err instanceof Error ? err : new Error(String(err));
|
|
557
|
-
if (accountManager && account) {
|
|
558
|
-
accountManager.markFailure(account);
|
|
559
|
-
transientRefreshSkips.add(account.index);
|
|
560
|
-
lastError = fetchError;
|
|
561
|
-
debugLog("request fetch threw, trying next account", {
|
|
562
|
-
accountIndex: account.index,
|
|
563
|
-
message: fetchError.message,
|
|
564
|
-
});
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
|
-
throw fetchError;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// On error, check if account-specific or service-wide
|
|
571
|
-
if (!response.ok && accountManager && account) {
|
|
572
|
-
let errorBody: string | null = null;
|
|
573
|
-
try {
|
|
574
|
-
errorBody = await response.clone().text();
|
|
575
|
-
} catch {
|
|
576
|
-
/* ignore */
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
if (isAccountSpecificError(response.status, errorBody)) {
|
|
580
|
-
const reason = parseRateLimitReason(response.status, errorBody);
|
|
581
|
-
const retryAfterMs = parseRetryAfterHeader(response);
|
|
582
|
-
const authOrPermissionIssue = reason === "AUTH_FAILED";
|
|
583
|
-
if (reason === "AUTH_FAILED") {
|
|
584
|
-
account.access = undefined;
|
|
585
|
-
account.expires = undefined;
|
|
586
|
-
markTokenStateUpdated(account);
|
|
587
|
-
}
|
|
588
|
-
debugLog("account-specific error, switching account", {
|
|
589
|
-
accountIndex: account.index,
|
|
590
|
-
status: response.status,
|
|
591
|
-
reason,
|
|
592
|
-
});
|
|
593
|
-
accountManager.markRateLimited(account, reason, authOrPermissionIssue ? null : retryAfterMs);
|
|
594
|
-
transientRefreshSkips.add(account.index);
|
|
595
|
-
const name = account.email || `Account ${accountManager.getCurrentIndex() + 1}`;
|
|
596
|
-
const total = accountManager.getAccountCount();
|
|
597
|
-
if (total > 1) {
|
|
598
|
-
const switchReason = formatSwitchReason(response.status, reason);
|
|
599
|
-
await toast(`${name} ${switchReason}, switching account`, "warning", {
|
|
600
|
-
debounceKey: "account-switch",
|
|
601
|
-
});
|
|
602
227
|
}
|
|
603
|
-
continue;
|
|
604
|
-
}
|
|
605
228
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
229
|
+
// Initialize AccountManager from disk + OpenCode auth fallback
|
|
230
|
+
accountManager = await AccountManager.load(config, {
|
|
231
|
+
refresh: auth.refresh,
|
|
232
|
+
access: auth.access,
|
|
233
|
+
expires: auth.expires,
|
|
609
234
|
});
|
|
235
|
+
if (accountManager.getAccountCount() > 0) {
|
|
236
|
+
await accountManager.saveToDisk();
|
|
237
|
+
}
|
|
610
238
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
retryCount += 1;
|
|
616
|
-
return response;
|
|
239
|
+
if (config.cc_credential_reuse?.enabled && config.cc_credential_reuse?.auto_detect) {
|
|
240
|
+
const ccCount = accountManager.getCCAccounts().length;
|
|
241
|
+
if (ccCount > 0) {
|
|
242
|
+
await toast(`Using Claude Code credentials (${ccCount} found)`, "success");
|
|
617
243
|
}
|
|
244
|
+
}
|
|
618
245
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
246
|
+
// OPENCODE_ANTHROPIC_INITIAL_ACCOUNT: pin to a specific account
|
|
247
|
+
const initialAccountEnv = process.env.OPENCODE_ANTHROPIC_INITIAL_ACCOUNT?.trim();
|
|
248
|
+
if (initialAccountEnv && accountManager.getAccountCount() > 1) {
|
|
249
|
+
const accounts = accountManager.getEnabledAccounts();
|
|
250
|
+
let target: ManagedAccount | null = null;
|
|
251
|
+
const asIndex = parseInt(initialAccountEnv, 10);
|
|
252
|
+
if (!isNaN(asIndex) && asIndex >= 1 && asIndex <= accounts.length) {
|
|
253
|
+
target = accounts[asIndex - 1];
|
|
254
|
+
}
|
|
255
|
+
if (!target) {
|
|
256
|
+
target =
|
|
257
|
+
accounts.find(
|
|
258
|
+
(a) => a.email && a.email.toLowerCase() === initialAccountEnv.toLowerCase(),
|
|
259
|
+
) ?? null;
|
|
260
|
+
}
|
|
261
|
+
if (target && accountManager.forceCurrentIndex(target.index)) {
|
|
262
|
+
config.account_selection_strategy = "sticky";
|
|
263
|
+
initialAccountPinned = true;
|
|
264
|
+
debugLog("OPENCODE_ANTHROPIC_INITIAL_ACCOUNT: pinned to account", {
|
|
265
|
+
index: target.index + 1,
|
|
266
|
+
email: target.email,
|
|
267
|
+
strategy: "sticky (overridden)",
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
debugLog(
|
|
271
|
+
"OPENCODE_ANTHROPIC_INITIAL_ACCOUNT: could not resolve account",
|
|
272
|
+
initialAccountEnv,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
637
275
|
}
|
|
638
276
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
277
|
+
return {
|
|
278
|
+
apiKey: "",
|
|
279
|
+
async fetch(
|
|
280
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- fetch input varies (string | URL | Request) across call sites
|
|
281
|
+
input: any,
|
|
282
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- fetch init is OpenCode-shaped RequestInit-plus
|
|
283
|
+
init: any,
|
|
284
|
+
) {
|
|
285
|
+
const currentAuth = await getAuth();
|
|
286
|
+
if (currentAuth.type !== "oauth") return fetch(input, init);
|
|
287
|
+
return executeOAuthFetch(input, init);
|
|
288
|
+
},
|
|
289
|
+
};
|
|
646
290
|
}
|
|
647
291
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
accountManager.markSuccess(account);
|
|
651
|
-
}
|
|
292
|
+
return {};
|
|
293
|
+
},
|
|
652
294
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
response.ok && isEventStreamResponse(response)
|
|
672
|
-
? (error: Error) => {
|
|
673
|
-
if (!(error instanceof StreamTruncatedError)) {
|
|
674
|
-
return;
|
|
295
|
+
methods: [
|
|
296
|
+
{
|
|
297
|
+
label: "Claude Code Credentials (auto-detected)",
|
|
298
|
+
type: "oauth" as const,
|
|
299
|
+
authorize: async () => {
|
|
300
|
+
const ccCredentials = readCCCredentials();
|
|
301
|
+
|
|
302
|
+
if (ccCredentials.length === 0) {
|
|
303
|
+
return {
|
|
304
|
+
url: "about:blank",
|
|
305
|
+
instructions:
|
|
306
|
+
"No Claude Code credentials found. Please install Claude Code and run 'claude login' first, then return here to use those credentials.",
|
|
307
|
+
method: "code" as const,
|
|
308
|
+
callback: async () => ({
|
|
309
|
+
type: "failed" as const,
|
|
310
|
+
reason: "Claude Code not installed or not authenticated",
|
|
311
|
+
}),
|
|
312
|
+
};
|
|
675
313
|
}
|
|
676
314
|
|
|
677
|
-
|
|
678
|
-
accountIndex: account?.index,
|
|
679
|
-
message: error.message,
|
|
680
|
-
context: error.context,
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
: null;
|
|
684
|
-
|
|
685
|
-
return finalizeResponse(response, usageCallback, accountErrorCallback, streamErrorCallback);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
if (lastError) throw lastError;
|
|
689
|
-
throw new Error("All accounts exhausted — no account could serve this request");
|
|
690
|
-
},
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
return {};
|
|
695
|
-
},
|
|
696
|
-
|
|
697
|
-
methods: [
|
|
698
|
-
{
|
|
699
|
-
label: "Claude Code Credentials (auto-detected)",
|
|
700
|
-
type: "oauth" as const,
|
|
701
|
-
authorize: async () => {
|
|
702
|
-
const ccCredentials = readCCCredentials();
|
|
703
|
-
|
|
704
|
-
if (ccCredentials.length === 0) {
|
|
705
|
-
return {
|
|
706
|
-
url: "about:blank",
|
|
707
|
-
instructions:
|
|
708
|
-
"No Claude Code credentials found. Please install Claude Code and run 'claude login' first, then return here to use those credentials.",
|
|
709
|
-
method: "code" as const,
|
|
710
|
-
callback: async () => ({
|
|
711
|
-
type: "failed" as const,
|
|
712
|
-
reason: "Claude Code not installed or not authenticated",
|
|
713
|
-
}),
|
|
714
|
-
};
|
|
715
|
-
}
|
|
315
|
+
const ccCred = ccCredentials[0];
|
|
716
316
|
|
|
717
|
-
|
|
317
|
+
if (!accountManager) {
|
|
318
|
+
accountManager = await AccountManager.load(config, null);
|
|
319
|
+
}
|
|
718
320
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
321
|
+
const identity = resolveIdentityFromCCCredential(ccCred);
|
|
322
|
+
const existing = findByIdentity(accountManager.getCCAccounts(), identity);
|
|
323
|
+
|
|
324
|
+
if (existing) {
|
|
325
|
+
existing.refreshToken = ccCred.refreshToken;
|
|
326
|
+
existing.identity = identity;
|
|
327
|
+
existing.source = ccCred.source;
|
|
328
|
+
existing.label = ccCred.label;
|
|
329
|
+
existing.enabled = true;
|
|
330
|
+
if (ccCred.accessToken) {
|
|
331
|
+
existing.access = ccCred.accessToken;
|
|
332
|
+
}
|
|
333
|
+
if (ccCred.expiresAt >= (existing.expires ?? 0)) {
|
|
334
|
+
existing.expires = ccCred.expiresAt;
|
|
335
|
+
}
|
|
336
|
+
existing.tokenUpdatedAt = Math.max(existing.tokenUpdatedAt || 0, ccCred.expiresAt || 0);
|
|
337
|
+
await accountManager.saveToDisk();
|
|
338
|
+
} else {
|
|
339
|
+
const added = accountManager.addAccount(
|
|
340
|
+
ccCred.refreshToken,
|
|
341
|
+
ccCred.accessToken,
|
|
342
|
+
ccCred.expiresAt,
|
|
343
|
+
undefined,
|
|
344
|
+
{
|
|
345
|
+
identity,
|
|
346
|
+
label: ccCred.label,
|
|
347
|
+
source: ccCred.source,
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
if (added) {
|
|
351
|
+
added.source = ccCred.source;
|
|
352
|
+
added.label = ccCred.label;
|
|
353
|
+
added.identity = identity;
|
|
354
|
+
}
|
|
355
|
+
await accountManager.saveToDisk();
|
|
356
|
+
await toast("Added Claude Code credentials", "success");
|
|
357
|
+
}
|
|
722
358
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
existing.label = ccCred.label;
|
|
731
|
-
existing.enabled = true;
|
|
732
|
-
if (ccCred.accessToken) {
|
|
733
|
-
existing.access = ccCred.accessToken;
|
|
734
|
-
}
|
|
735
|
-
if (ccCred.expiresAt >= (existing.expires ?? 0)) {
|
|
736
|
-
existing.expires = ccCred.expiresAt;
|
|
737
|
-
}
|
|
738
|
-
existing.tokenUpdatedAt = Math.max(existing.tokenUpdatedAt || 0, ccCred.expiresAt || 0);
|
|
739
|
-
await accountManager.saveToDisk();
|
|
740
|
-
} else {
|
|
741
|
-
const added = accountManager.addAccount(
|
|
742
|
-
ccCred.refreshToken,
|
|
743
|
-
ccCred.accessToken,
|
|
744
|
-
ccCred.expiresAt,
|
|
745
|
-
undefined,
|
|
746
|
-
{
|
|
747
|
-
identity,
|
|
748
|
-
label: ccCred.label,
|
|
749
|
-
source: ccCred.source,
|
|
359
|
+
return {
|
|
360
|
+
type: "success" as const,
|
|
361
|
+
refresh: ccCred.refreshToken,
|
|
362
|
+
access: ccCred.accessToken,
|
|
363
|
+
expires: ccCred.expiresAt,
|
|
364
|
+
};
|
|
365
|
+
},
|
|
750
366
|
},
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
callback: async () => ({ type: "failed" as const }),
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
if (action === "manage") {
|
|
785
|
-
await promptManageAccounts(accountManager);
|
|
786
|
-
await accountManager.saveToDisk();
|
|
787
|
-
return {
|
|
788
|
-
url: "about:blank",
|
|
789
|
-
instructions: "Account management complete. Re-run auth to add accounts.",
|
|
790
|
-
method: "code" as const,
|
|
791
|
-
callback: async () => ({ type: "failed" as const }),
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
if (action === "fresh") {
|
|
795
|
-
await clearAccounts();
|
|
796
|
-
accountManager.clearAll();
|
|
797
|
-
}
|
|
798
|
-
}
|
|
367
|
+
{
|
|
368
|
+
label: "Claude Pro/Max (multi-account)",
|
|
369
|
+
type: "oauth" as const,
|
|
370
|
+
authorize: async () => {
|
|
371
|
+
const stored = await loadAccounts();
|
|
372
|
+
if (stored && stored.accounts.length > 0 && accountManager) {
|
|
373
|
+
const action = await promptAccountMenu(accountManager);
|
|
374
|
+
if (action === "cancel") {
|
|
375
|
+
return {
|
|
376
|
+
url: "about:blank",
|
|
377
|
+
instructions: "Cancelled.",
|
|
378
|
+
method: "code" as const,
|
|
379
|
+
callback: async () => ({ type: "failed" as const }),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (action === "manage") {
|
|
383
|
+
await promptManageAccounts(accountManager);
|
|
384
|
+
await accountManager.saveToDisk();
|
|
385
|
+
return {
|
|
386
|
+
url: "about:blank",
|
|
387
|
+
instructions: "Account management complete. Re-run auth to add accounts.",
|
|
388
|
+
method: "code" as const,
|
|
389
|
+
callback: async () => ({ type: "failed" as const }),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (action === "fresh") {
|
|
393
|
+
await clearAccounts();
|
|
394
|
+
accountManager.clearAll();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
799
397
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
398
|
+
const { url, verifier, state } = await authorize("max");
|
|
399
|
+
return {
|
|
400
|
+
url,
|
|
401
|
+
instructions: "Paste the authorization code here: ",
|
|
402
|
+
method: "code" as const,
|
|
403
|
+
callback: async (code: string) => {
|
|
404
|
+
const parts = code.split("#");
|
|
405
|
+
if (state && parts[1] && parts[1] !== state) {
|
|
406
|
+
return {
|
|
407
|
+
type: "failed" as const,
|
|
408
|
+
reason: "OAuth state mismatch",
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const credentials = await exchange(code, verifier);
|
|
412
|
+
if (credentials.type === "failed") return credentials;
|
|
413
|
+
if (!accountManager) {
|
|
414
|
+
accountManager = await AccountManager.load(config, null);
|
|
415
|
+
}
|
|
416
|
+
const identity = resolveIdentityFromOAuthExchange(credentials);
|
|
417
|
+
const countBefore = accountManager.getAccountCount();
|
|
418
|
+
const candidate =
|
|
419
|
+
identity.kind === "oauth"
|
|
420
|
+
? findByIdentity(accountManager.getOAuthAccounts(), identity)
|
|
421
|
+
: null;
|
|
422
|
+
// Refuse to reshape a row that was born as a CC import. The id
|
|
423
|
+
// prefix is the only durable proof-of-origin; reshaping it to
|
|
424
|
+
// oauth would re-introduce the dedup bug.
|
|
425
|
+
const existing =
|
|
426
|
+
candidate && /^cc-(cc-keychain|cc-file)-\d+:/.test(candidate.id) ? null : candidate;
|
|
427
|
+
|
|
428
|
+
if (existing) {
|
|
429
|
+
existing.refreshToken = credentials.refresh;
|
|
430
|
+
existing.access = credentials.access;
|
|
431
|
+
existing.expires = credentials.expires;
|
|
432
|
+
existing.email = credentials.email ?? existing.email;
|
|
433
|
+
existing.identity = identity;
|
|
434
|
+
existing.source = "oauth";
|
|
435
|
+
existing.enabled = true;
|
|
436
|
+
existing.tokenUpdatedAt = Date.now();
|
|
437
|
+
} else {
|
|
438
|
+
accountManager.addAccount(
|
|
439
|
+
credentials.refresh,
|
|
440
|
+
credentials.access,
|
|
441
|
+
credentials.expires,
|
|
442
|
+
credentials.email,
|
|
443
|
+
{
|
|
444
|
+
identity,
|
|
445
|
+
source: "oauth",
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
await accountManager.saveToDisk();
|
|
451
|
+
const total = accountManager.getAccountCount();
|
|
452
|
+
const name = credentials.email || "account";
|
|
453
|
+
if (existing) await toast(`Re-authenticated (${name})`, "success");
|
|
454
|
+
else if (countBefore > 0) await toast(`Added ${name} — ${total} accounts`, "success");
|
|
455
|
+
else await toast(`Authenticated (${name})`, "success");
|
|
456
|
+
return credentials;
|
|
457
|
+
},
|
|
458
|
+
};
|
|
841
459
|
},
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
},
|
|
884
|
-
};
|
|
885
|
-
},
|
|
886
|
-
},
|
|
887
|
-
{
|
|
888
|
-
provider: "anthropic",
|
|
889
|
-
label: "Manually enter API Key",
|
|
890
|
-
type: "api" as const,
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
label: "Create an API Key",
|
|
463
|
+
type: "oauth" as const,
|
|
464
|
+
authorize: async () => {
|
|
465
|
+
const { url, verifier, state } = await authorize("console");
|
|
466
|
+
return {
|
|
467
|
+
url,
|
|
468
|
+
instructions: "Paste the authorization code here: ",
|
|
469
|
+
method: "code" as const,
|
|
470
|
+
callback: async (code: string) => {
|
|
471
|
+
const parts = code.split("#");
|
|
472
|
+
if (state && parts[1] && parts[1] !== state) {
|
|
473
|
+
return {
|
|
474
|
+
type: "failed" as const,
|
|
475
|
+
reason: "OAuth state mismatch",
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const credentials = await exchange(code, verifier);
|
|
479
|
+
if (credentials.type === "failed") return credentials;
|
|
480
|
+
const result = (await fetch(
|
|
481
|
+
"https://api.anthropic.com/api/oauth/claude_cli/create_api_key",
|
|
482
|
+
{
|
|
483
|
+
method: "POST",
|
|
484
|
+
headers: {
|
|
485
|
+
"Content-Type": "application/json",
|
|
486
|
+
authorization: `Bearer ${credentials.access}`,
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
).then((r) => r.json())) as { raw_key: string };
|
|
490
|
+
return { type: "success" as const, key: result.raw_key };
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
provider: "anthropic",
|
|
497
|
+
label: "Manually enter API Key",
|
|
498
|
+
type: "api" as const,
|
|
499
|
+
},
|
|
500
|
+
],
|
|
891
501
|
},
|
|
892
|
-
|
|
893
|
-
},
|
|
894
|
-
};
|
|
502
|
+
};
|
|
895
503
|
}
|