opencode-anthropic-multi-account 0.2.4 → 0.2.5
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/dist/index.js +239 -11
- package/dist/pi-ai-adapter.d.ts +2 -0
- package/dist/token-node-request.d.ts +10 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2194,9 +2194,100 @@ var TOOL_PREFIX = ANTHROPIC_OAUTH_ADAPTER.toolPrefix;
|
|
|
2194
2194
|
var ACCOUNTS_FILENAME2 = ANTHROPIC_OAUTH_ADAPTER.accountStorageFilename;
|
|
2195
2195
|
var PLAN_LABELS = ANTHROPIC_OAUTH_ADAPTER.planLabels;
|
|
2196
2196
|
var TOKEN_EXPIRY_BUFFER_MS = 6e4;
|
|
2197
|
+
var TOKEN_REFRESH_TIMEOUT_MS = 3e4;
|
|
2197
2198
|
|
|
2198
2199
|
// src/pi-ai-adapter.ts
|
|
2199
|
-
import {
|
|
2200
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2201
|
+
import * as piAiOauth from "@mariozechner/pi-ai/oauth";
|
|
2202
|
+
|
|
2203
|
+
// src/token-node-request.ts
|
|
2204
|
+
import * as childProcess from "node:child_process";
|
|
2205
|
+
function buildNodeTokenRequestScript() {
|
|
2206
|
+
return `
|
|
2207
|
+
const https = require("node:https");
|
|
2208
|
+
const endpoint = process.env.ANTHROPIC_REFRESH_ENDPOINT;
|
|
2209
|
+
const timeoutMs = Number(process.env.ANTHROPIC_REFRESH_TIMEOUT_MS || "30000");
|
|
2210
|
+
const payload = process.env.ANTHROPIC_REFRESH_REQUEST_BODY || "";
|
|
2211
|
+
|
|
2212
|
+
function printSuccess(body) {
|
|
2213
|
+
console.log(JSON.stringify({ ok: true, body }));
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
function printFailure(error) {
|
|
2217
|
+
console.log(JSON.stringify({ ok: false, ...error }));
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
const request = https.request(endpoint, {
|
|
2221
|
+
method: "POST",
|
|
2222
|
+
headers: {
|
|
2223
|
+
"Content-Type": "application/json",
|
|
2224
|
+
Accept: "application/json",
|
|
2225
|
+
"Content-Length": Buffer.byteLength(payload).toString(),
|
|
2226
|
+
},
|
|
2227
|
+
}, (response) => {
|
|
2228
|
+
let body = "";
|
|
2229
|
+
response.setEncoding("utf8");
|
|
2230
|
+
response.on("data", (chunk) => {
|
|
2231
|
+
body += chunk;
|
|
2232
|
+
});
|
|
2233
|
+
response.on("end", () => {
|
|
2234
|
+
const status = response.statusCode ?? 0;
|
|
2235
|
+
if (status < 200 || status >= 300) {
|
|
2236
|
+
printFailure({ status, body });
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
printSuccess(body);
|
|
2241
|
+
});
|
|
2242
|
+
});
|
|
2243
|
+
|
|
2244
|
+
request.setTimeout(timeoutMs, () => {
|
|
2245
|
+
request.destroy(new Error("Request timed out after " + timeoutMs + "ms"));
|
|
2246
|
+
});
|
|
2247
|
+
|
|
2248
|
+
request.on("error", (error) => {
|
|
2249
|
+
printFailure({ error: error instanceof Error ? error.name + ": " + error.message : String(error) });
|
|
2250
|
+
});
|
|
2251
|
+
|
|
2252
|
+
request.write(payload);
|
|
2253
|
+
request.end();
|
|
2254
|
+
`;
|
|
2255
|
+
}
|
|
2256
|
+
async function defaultRunNodeTokenRequest(options) {
|
|
2257
|
+
const script = buildNodeTokenRequestScript();
|
|
2258
|
+
return await new Promise((resolve, reject) => {
|
|
2259
|
+
childProcess.execFile(
|
|
2260
|
+
options.executable,
|
|
2261
|
+
["-e", script],
|
|
2262
|
+
{
|
|
2263
|
+
timeout: options.timeoutMs + 1e3,
|
|
2264
|
+
maxBuffer: 1024 * 1024,
|
|
2265
|
+
env: {
|
|
2266
|
+
...process.env,
|
|
2267
|
+
ANTHROPIC_REFRESH_ENDPOINT: options.endpoint,
|
|
2268
|
+
ANTHROPIC_REFRESH_REQUEST_BODY: options.body,
|
|
2269
|
+
ANTHROPIC_REFRESH_TIMEOUT_MS: String(options.timeoutMs)
|
|
2270
|
+
}
|
|
2271
|
+
},
|
|
2272
|
+
(error, stdout, stderr) => {
|
|
2273
|
+
const trimmedStdout = stdout.trim();
|
|
2274
|
+
if (error) {
|
|
2275
|
+
reject(new Error(stderr.trim() || error.message));
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
if (!trimmedStdout) {
|
|
2279
|
+
reject(new Error("Empty response from Node refresh helper"));
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
resolve(trimmedStdout);
|
|
2283
|
+
}
|
|
2284
|
+
);
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
var nodeTokenRequestRunner = defaultRunNodeTokenRequest;
|
|
2288
|
+
async function runNodeTokenRequest(options) {
|
|
2289
|
+
return await nodeTokenRequestRunner(options);
|
|
2290
|
+
}
|
|
2200
2291
|
|
|
2201
2292
|
// src/config.ts
|
|
2202
2293
|
initCoreConfig("claude-multiauth.json");
|
|
@@ -2378,19 +2469,140 @@ function fromPiAiCredentials(creds) {
|
|
|
2378
2469
|
expiresAt: creds.expires
|
|
2379
2470
|
};
|
|
2380
2471
|
}
|
|
2472
|
+
var ANTHROPIC_REFRESH_ENDPOINT = "https://platform.claude.com/v1/oauth/token";
|
|
2473
|
+
var REFRESH_NODE_EXECUTABLE = process.env.OPENCODE_REFRESH_NODE_EXECUTABLE || "node";
|
|
2474
|
+
var tokenProxyContext = new AsyncLocalStorage();
|
|
2475
|
+
var tokenProxyInstalled = false;
|
|
2476
|
+
var tokenProxyOriginalFetch = null;
|
|
2477
|
+
var refreshEndpointUrl = new URL(ANTHROPIC_REFRESH_ENDPOINT);
|
|
2478
|
+
function buildRefreshRequestError(details) {
|
|
2479
|
+
return new Error(`Anthropic token refresh request failed. url=${ANTHROPIC_REFRESH_ENDPOINT}; details=${details}`);
|
|
2480
|
+
}
|
|
2481
|
+
function buildRefreshInvalidJsonError(body, details) {
|
|
2482
|
+
return new Error(`Anthropic token refresh returned invalid JSON. url=${ANTHROPIC_REFRESH_ENDPOINT}; body=${body}; details=${details}`);
|
|
2483
|
+
}
|
|
2484
|
+
function getRequestUrlString(input) {
|
|
2485
|
+
if (typeof input === "string") return input;
|
|
2486
|
+
if (input instanceof URL) return input.toString();
|
|
2487
|
+
return input.url;
|
|
2488
|
+
}
|
|
2489
|
+
function isAnthropicTokenEndpoint(input) {
|
|
2490
|
+
const rawUrl = getRequestUrlString(input);
|
|
2491
|
+
try {
|
|
2492
|
+
const url = new URL(rawUrl);
|
|
2493
|
+
return url.origin === refreshEndpointUrl.origin && url.pathname === refreshEndpointUrl.pathname;
|
|
2494
|
+
} catch {
|
|
2495
|
+
return rawUrl === ANTHROPIC_REFRESH_ENDPOINT;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
function getRequestBodySource(input, init) {
|
|
2499
|
+
if (init?.body !== void 0) {
|
|
2500
|
+
return init.body;
|
|
2501
|
+
}
|
|
2502
|
+
if (input instanceof Request) {
|
|
2503
|
+
return input.body;
|
|
2504
|
+
}
|
|
2505
|
+
return void 0;
|
|
2506
|
+
}
|
|
2507
|
+
function stringifyBinaryBody(body) {
|
|
2508
|
+
if (body instanceof ArrayBuffer) {
|
|
2509
|
+
return Buffer.from(body).toString("utf8");
|
|
2510
|
+
}
|
|
2511
|
+
return Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString("utf8");
|
|
2512
|
+
}
|
|
2513
|
+
async function getRequestBody(input, init) {
|
|
2514
|
+
const body = getRequestBodySource(input, init);
|
|
2515
|
+
if (typeof body === "string") return body;
|
|
2516
|
+
if (body instanceof URLSearchParams) return body.toString();
|
|
2517
|
+
if (body instanceof Uint8Array || body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
|
|
2518
|
+
return stringifyBinaryBody(body);
|
|
2519
|
+
}
|
|
2520
|
+
if (typeof Blob !== "undefined" && body instanceof Blob) return await body.text();
|
|
2521
|
+
if (body instanceof ReadableStream) return await new Response(body).text();
|
|
2522
|
+
if (input instanceof Request && init?.body === void 0) return await input.clone().text();
|
|
2523
|
+
if (body == null) return "";
|
|
2524
|
+
throw buildRefreshRequestError(`Unsupported token request body type: ${Object.prototype.toString.call(body)}`);
|
|
2525
|
+
}
|
|
2526
|
+
function getRequestMethod(input, init) {
|
|
2527
|
+
return init?.method ?? (input instanceof Request ? input.method : "GET");
|
|
2528
|
+
}
|
|
2529
|
+
function shouldProxyTokenRequest(input) {
|
|
2530
|
+
return tokenProxyContext.getStore() === true && isAnthropicTokenEndpoint(input);
|
|
2531
|
+
}
|
|
2532
|
+
async function postAnthropicTokenViaNode(body) {
|
|
2533
|
+
let output;
|
|
2534
|
+
try {
|
|
2535
|
+
output = await runNodeTokenRequest({
|
|
2536
|
+
body,
|
|
2537
|
+
endpoint: ANTHROPIC_REFRESH_ENDPOINT,
|
|
2538
|
+
executable: REFRESH_NODE_EXECUTABLE,
|
|
2539
|
+
timeoutMs: TOKEN_REFRESH_TIMEOUT_MS
|
|
2540
|
+
});
|
|
2541
|
+
} catch (error) {
|
|
2542
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
2543
|
+
throw buildRefreshRequestError(details);
|
|
2544
|
+
}
|
|
2545
|
+
let parsed;
|
|
2546
|
+
try {
|
|
2547
|
+
parsed = JSON.parse(output);
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
const details = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
2550
|
+
throw buildRefreshInvalidJsonError(output, details);
|
|
2551
|
+
}
|
|
2552
|
+
const result = parsed;
|
|
2553
|
+
if (!result.ok) {
|
|
2554
|
+
if (result.error) {
|
|
2555
|
+
throw buildRefreshRequestError(result.error);
|
|
2556
|
+
}
|
|
2557
|
+
throw buildRefreshRequestError(`Error: HTTP request failed. status=${result.status ?? 0}; url=${ANTHROPIC_REFRESH_ENDPOINT}; body=${result.body ?? ""}`);
|
|
2558
|
+
}
|
|
2559
|
+
return new Response(result.body ?? "", {
|
|
2560
|
+
status: 200,
|
|
2561
|
+
headers: {
|
|
2562
|
+
"content-type": "application/json"
|
|
2563
|
+
}
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
function createAnthropicTokenProxyFetch(originalFetch) {
|
|
2567
|
+
return (async (input, init) => {
|
|
2568
|
+
if (!shouldProxyTokenRequest(input)) {
|
|
2569
|
+
return originalFetch(input, init);
|
|
2570
|
+
}
|
|
2571
|
+
const method = getRequestMethod(input, init).toUpperCase();
|
|
2572
|
+
if (method !== "POST") {
|
|
2573
|
+
throw buildRefreshRequestError(`Unsupported token endpoint method: ${method}`);
|
|
2574
|
+
}
|
|
2575
|
+
return await postAnthropicTokenViaNode(await getRequestBody(input, init));
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
function ensureAnthropicTokenProxyFetchInstalled() {
|
|
2579
|
+
if (tokenProxyInstalled) return;
|
|
2580
|
+
tokenProxyOriginalFetch = globalThis.fetch;
|
|
2581
|
+
globalThis.fetch = createAnthropicTokenProxyFetch(tokenProxyOriginalFetch);
|
|
2582
|
+
tokenProxyInstalled = true;
|
|
2583
|
+
}
|
|
2584
|
+
async function withAnthropicTokenProxyFetch(operation) {
|
|
2585
|
+
ensureAnthropicTokenProxyFetchInstalled();
|
|
2586
|
+
return await tokenProxyContext.run(true, operation);
|
|
2587
|
+
}
|
|
2588
|
+
async function fetchProfileWithSingleRetry(accessToken) {
|
|
2589
|
+
let profileResult = await fetchProfile(accessToken);
|
|
2590
|
+
if (profileResult.ok) {
|
|
2591
|
+
return profileResult;
|
|
2592
|
+
}
|
|
2593
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2594
|
+
profileResult = await fetchProfile(accessToken);
|
|
2595
|
+
return profileResult;
|
|
2596
|
+
}
|
|
2381
2597
|
async function loginWithPiAi(callbacks) {
|
|
2382
|
-
const piCreds = await loginAnthropic({
|
|
2598
|
+
const piCreds = await withAnthropicTokenProxyFetch(() => piAiOauth.loginAnthropic({
|
|
2383
2599
|
onAuth: callbacks.onAuth,
|
|
2384
2600
|
onPrompt: callbacks.onPrompt,
|
|
2385
2601
|
onProgress: callbacks.onProgress,
|
|
2386
2602
|
onManualCodeInput: callbacks.onManualCodeInput
|
|
2387
|
-
});
|
|
2603
|
+
}));
|
|
2388
2604
|
const base = fromPiAiCredentials(piCreds);
|
|
2389
|
-
|
|
2390
|
-
if (!profileResult.ok) {
|
|
2391
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
2392
|
-
profileResult = await fetchProfile(piCreds.access);
|
|
2393
|
-
}
|
|
2605
|
+
const profileResult = await fetchProfileWithSingleRetry(piCreds.access);
|
|
2394
2606
|
const profileData = profileResult.ok ? profileResult.data : void 0;
|
|
2395
2607
|
return {
|
|
2396
2608
|
...base,
|
|
@@ -2401,7 +2613,7 @@ async function loginWithPiAi(callbacks) {
|
|
|
2401
2613
|
};
|
|
2402
2614
|
}
|
|
2403
2615
|
async function refreshWithPiAi(currentRefreshToken) {
|
|
2404
|
-
const piCreds = await refreshAnthropicToken(currentRefreshToken);
|
|
2616
|
+
const piCreds = await withAnthropicTokenProxyFetch(() => piAiOauth.refreshAnthropicToken(currentRefreshToken));
|
|
2405
2617
|
return {
|
|
2406
2618
|
accessToken: piCreds.access,
|
|
2407
2619
|
refreshToken: piCreds.refresh,
|
|
@@ -2412,6 +2624,13 @@ var PI_AI_ADAPTER_SERVICE = ANTHROPIC_OAUTH_ADAPTER.serviceLogName;
|
|
|
2412
2624
|
|
|
2413
2625
|
// src/token.ts
|
|
2414
2626
|
var PERMANENT_FAILURE_HTTP_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
|
|
2627
|
+
var PERMANENT_FAILURE_MESSAGE_PATTERNS = [
|
|
2628
|
+
/\binvalid_grant\b/i,
|
|
2629
|
+
/\binvalid_scope\b/i,
|
|
2630
|
+
/\bunauthorized_client\b/i,
|
|
2631
|
+
/\brefresh token\b.*\b(invalid|expired|revoked|no longer valid)\b/i,
|
|
2632
|
+
/\bauth(?:entication)?(?:[_\s-]+)?invalid\b/i
|
|
2633
|
+
];
|
|
2415
2634
|
var refreshMutexByAccountId = /* @__PURE__ */ new Map();
|
|
2416
2635
|
function isTokenExpired(account) {
|
|
2417
2636
|
if (!account.accessToken || !account.expiresAt) return true;
|
|
@@ -2428,7 +2647,9 @@ async function refreshToken(currentRefreshToken, accountId, client) {
|
|
|
2428
2647
|
} catch (error) {
|
|
2429
2648
|
const message = error instanceof Error ? error.message : String(error);
|
|
2430
2649
|
const statusMatch = message.match(/\b(400|401|403)\b/);
|
|
2431
|
-
const
|
|
2650
|
+
const hasPermanentStatus = statusMatch !== null && PERMANENT_FAILURE_HTTP_STATUSES.has(Number(statusMatch[1]));
|
|
2651
|
+
const hasPermanentMessage = PERMANENT_FAILURE_MESSAGE_PATTERNS.some((pattern) => pattern.test(message));
|
|
2652
|
+
const isPermanent = hasPermanentStatus || hasPermanentMessage;
|
|
2432
2653
|
await client.app.log({
|
|
2433
2654
|
body: {
|
|
2434
2655
|
service: ANTHROPIC_OAUTH_ADAPTER.serviceLogName,
|
|
@@ -3039,7 +3260,14 @@ async function checkAccountQuota(manager, account, client) {
|
|
|
3039
3260
|
}
|
|
3040
3261
|
const refreshResult = await manager.ensureValidToken(account.uuid, client);
|
|
3041
3262
|
if (!refreshResult.ok) {
|
|
3042
|
-
|
|
3263
|
+
await manager.markAuthFailure(account.uuid, refreshResult);
|
|
3264
|
+
await manager.refresh();
|
|
3265
|
+
const updatedAccount = manager.getAccounts().find((candidate) => candidate.uuid === account.uuid);
|
|
3266
|
+
if (!updatedAccount) {
|
|
3267
|
+
printQuotaError(account, refreshResult.permanent ? "Refresh failed permanently; account removed" : "Failed to refresh token");
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
printQuotaError(updatedAccount, updatedAccount.isAuthDisabled ? `${updatedAccount.authDisabledReason ?? "Auth disabled"} (refresh failed)` : "Failed to refresh token");
|
|
3043
3271
|
return;
|
|
3044
3272
|
}
|
|
3045
3273
|
await manager.refresh();
|
package/dist/pi-ai-adapter.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface LoginWithPiAiCallbacks {
|
|
|
11
11
|
onProgress?: (message: string) => void;
|
|
12
12
|
onManualCodeInput?: () => Promise<string>;
|
|
13
13
|
}
|
|
14
|
+
export declare function withAnthropicTokenProxyFetch<T>(operation: () => Promise<T>): Promise<T>;
|
|
15
|
+
export declare function resetAnthropicTokenProxyStateForTest(): void;
|
|
14
16
|
export declare function loginWithPiAi(callbacks: LoginWithPiAiCallbacks): Promise<Partial<StoredAccount>>;
|
|
15
17
|
export declare function refreshWithPiAi(currentRefreshToken: string): Promise<CredentialRefreshPatch>;
|
|
16
18
|
export declare const PI_AI_ADAPTER_SERVICE: string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface NodeTokenRequestOptions {
|
|
2
|
+
body: string;
|
|
3
|
+
endpoint: string;
|
|
4
|
+
executable: string;
|
|
5
|
+
timeoutMs: number;
|
|
6
|
+
}
|
|
7
|
+
type NodeTokenRequestRunner = (options: NodeTokenRequestOptions) => Promise<string>;
|
|
8
|
+
export declare function runNodeTokenRequest(options: NodeTokenRequestOptions): Promise<string>;
|
|
9
|
+
export declare function setNodeTokenRequestRunnerForTest(runner: NodeTokenRequestRunner | null): void;
|
|
10
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-anthropic-multi-account",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "OpenCode plugin for Anthropic multi-account management with automatic rate limit switching",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"directory": "packages/anthropic-multi-account"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"opencode-multi-account-core": "^0.2.
|
|
42
|
+
"opencode-multi-account-core": "^0.2.5",
|
|
43
43
|
"@mariozechner/pi-ai": "^0.61.0",
|
|
44
44
|
"valibot": "^1.2.0"
|
|
45
45
|
},
|