opencode-anthropic-fix 0.0.21 → 0.0.22
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 +52 -18
- package/index.mjs +322 -65
- package/lib/accounts.mjs +14 -0
- package/lib/config.mjs +1 -1
- package/lib/config.test.mjs +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -298,7 +298,7 @@ Example workflow:
|
|
|
298
298
|
Upload, list, download, and delete files via the Anthropic Files API. The `files-api-2025-04-14` beta is auto-included in every request.
|
|
299
299
|
|
|
300
300
|
```text
|
|
301
|
-
/anthropic files # list
|
|
301
|
+
/anthropic files # list files across ALL accounts
|
|
302
302
|
/anthropic files upload ./report.pdf # upload a file
|
|
303
303
|
/anthropic files get file_abc123 # get file metadata
|
|
304
304
|
/anthropic files download file_abc123 # download to current directory
|
|
@@ -308,6 +308,16 @@ Upload, list, download, and delete files via the Anthropic Files API. The `files
|
|
|
308
308
|
|
|
309
309
|
Supported formats: PDF, DOCX, TXT, CSV, Excel, Markdown, images (max 350 MB per file). Uploaded files can be referenced by `file_id` in Messages API requests.
|
|
310
310
|
|
|
311
|
+
**Multi-account behavior:** Files on Anthropic are per-account. With multiple accounts:
|
|
312
|
+
|
|
313
|
+
- `/anthropic files list` (no `--account`) queries **all** enabled accounts, labeling each file with its owner email
|
|
314
|
+
- Use `--account <email|index>` to target a specific account for any action:
|
|
315
|
+
```text
|
|
316
|
+
/anthropic files list --account alice@example.com
|
|
317
|
+
/anthropic files upload ./data.csv --account 2
|
|
318
|
+
```
|
|
319
|
+
- **Auto-pinning:** When you upload or list files, the plugin remembers which account owns each `file_id`. If a subsequent Messages API request references that `file_id`, the plugin automatically routes it to the correct account — even with round-robin or hybrid strategies.
|
|
320
|
+
|
|
311
321
|
### OAuth flows from slash command
|
|
312
322
|
|
|
313
323
|
Login and reauth are two-step flows in slash mode:
|
|
@@ -333,11 +343,11 @@ Pending slash OAuth flows expire after 10 minutes. If completion fails with an e
|
|
|
333
343
|
|
|
334
344
|
Control how the plugin picks which account to use for each request.
|
|
335
345
|
|
|
336
|
-
| Strategy
|
|
337
|
-
|
|
|
338
|
-
| **`
|
|
339
|
-
| **`
|
|
340
|
-
| **`hybrid`**
|
|
346
|
+
| Strategy | Behavior | Best For |
|
|
347
|
+
| ---------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
348
|
+
| **`sticky`** (default) | Stay on one account until it fails or is rate-limited | Single account, predictable behavior, full feature compatibility |
|
|
349
|
+
| **`round-robin`** | Rotate through accounts on every request | Spreading load evenly across accounts (see [limitations](#round-robin-limitations)) |
|
|
350
|
+
| **`hybrid`** | Score-based selection with stickiness bias. Considers health, token budget, and freshness | Multiple accounts with varying rate limits |
|
|
341
351
|
|
|
342
352
|
### Change Strategy
|
|
343
353
|
|
|
@@ -352,14 +362,37 @@ export OPENCODE_ANTHROPIC_STRATEGY=hybrid
|
|
|
352
362
|
# Edit ~/.config/opencode/anthropic-auth.json
|
|
353
363
|
```
|
|
354
364
|
|
|
365
|
+
### Round-Robin Limitations
|
|
366
|
+
|
|
367
|
+
Some Anthropic API features maintain server-side per-account state that breaks when requests alternate between accounts:
|
|
368
|
+
|
|
369
|
+
| Feature | Impact | Plugin Mitigation |
|
|
370
|
+
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
371
|
+
| **Files API** (`files-api-2025-04-14`) | `file_id` is per-account; referencing Account A's file from Account B = `file_not_found` | **Auto-pinning**: the plugin tracks which account owns each `file_id` and routes the request accordingly |
|
|
372
|
+
| **Prompt Caching** (`prompt-caching-scope-2026-01-05`) | Cache is per-workspace; alternating accounts means zero cache hits, doubling token costs | **Auto-skipped** in round-robin: the `prompt-caching-scope` beta is excluded from the header |
|
|
373
|
+
| **Code Execution** (`code-execution-2025-08-25`) | Sandbox state is ephemeral per-request; multi-step workflows lose files/state when routed to a different account | **Auto-skipped** in round-robin: the `code-execution` beta is excluded from the header |
|
|
374
|
+
| **Message Batches** (`message-batches-2024-09-24`) | `batch_id` is per-account; polling from wrong account = 404 | No automatic mitigation (not auto-included) |
|
|
375
|
+
|
|
376
|
+
**Recommendation for round-robin users:** If you need prompt caching or code execution, pin each OpenCode session to a single account:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
# Terminal 1 — uses account 1
|
|
380
|
+
OPENCODE_ANTHROPIC_INITIAL_ACCOUNT=1 opencode
|
|
381
|
+
|
|
382
|
+
# Terminal 2 — uses account 2
|
|
383
|
+
OPENCODE_ANTHROPIC_INITIAL_ACCOUNT=2 opencode
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
This automatically overrides the strategy to `sticky` for that session, re-enabling all betas. Other sessions are unaffected.
|
|
387
|
+
|
|
355
388
|
## Configuration
|
|
356
389
|
|
|
357
390
|
Configuration is stored at `~/.config/opencode/anthropic-auth.json`. All settings are optional — defaults work well for most users.
|
|
358
391
|
|
|
359
392
|
```jsonc
|
|
360
393
|
{
|
|
361
|
-
// Account selection strategy: "
|
|
362
|
-
"account_selection_strategy": "
|
|
394
|
+
// Account selection strategy: "sticky" | "round-robin" | "hybrid"
|
|
395
|
+
"account_selection_strategy": "sticky",
|
|
363
396
|
|
|
364
397
|
// Seconds before consecutive failure count resets (60-7200)
|
|
365
398
|
"failure_ttl_seconds": 3600,
|
|
@@ -427,16 +460,17 @@ Configuration is stored at `~/.config/opencode/anthropic-auth.json`. All setting
|
|
|
427
460
|
|
|
428
461
|
### Environment Variables
|
|
429
462
|
|
|
430
|
-
| Variable | Description
|
|
431
|
-
| -------------------------------------------------- |
|
|
432
|
-
| `OPENCODE_ANTHROPIC_STRATEGY` | Override the account selection strategy at runtime.
|
|
433
|
-
| `OPENCODE_ANTHROPIC_DEBUG` | Set to `1` to enable debug logging.
|
|
434
|
-
| `OPENCODE_ANTHROPIC_QUIET` | Set to `1` to suppress non-error toasts (account status, switching).
|
|
435
|
-
| `OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE` | Set to `0` to disable Claude signature emulation (legacy mode).
|
|
436
|
-
| `OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION` | Set to `0` to skip npm version lookup at startup.
|
|
437
|
-
| `OPENCODE_ANTHROPIC_PROMPT_COMPACTION` | Set to `off` to disable default minimal system prompt compaction.
|
|
438
|
-
| `OPENCODE_ANTHROPIC_DEBUG_SYSTEM_PROMPT` | Set to `1` to log the final transformed `system` prompt to stderr (title-generator requests are skipped).
|
|
439
|
-
| `OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS` | Set to `0` to disable context limit overrides for 1M-window models (e.g. when models.dev has been updated).
|
|
463
|
+
| Variable | Description |
|
|
464
|
+
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
465
|
+
| `OPENCODE_ANTHROPIC_STRATEGY` | Override the account selection strategy at runtime. |
|
|
466
|
+
| `OPENCODE_ANTHROPIC_DEBUG` | Set to `1` to enable debug logging. |
|
|
467
|
+
| `OPENCODE_ANTHROPIC_QUIET` | Set to `1` to suppress non-error toasts (account status, switching). |
|
|
468
|
+
| `OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE` | Set to `0` to disable Claude signature emulation (legacy mode). |
|
|
469
|
+
| `OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION` | Set to `0` to skip npm version lookup at startup. |
|
|
470
|
+
| `OPENCODE_ANTHROPIC_PROMPT_COMPACTION` | Set to `off` to disable default minimal system prompt compaction. |
|
|
471
|
+
| `OPENCODE_ANTHROPIC_DEBUG_SYSTEM_PROMPT` | Set to `1` to log the final transformed `system` prompt to stderr (title-generator requests are skipped). |
|
|
472
|
+
| `OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS` | Set to `0` to disable context limit overrides for 1M-window models (e.g. when models.dev has been updated). |
|
|
473
|
+
| `OPENCODE_ANTHROPIC_INITIAL_ACCOUNT` | Pin this session to a specific account (1-based index or email). Overrides strategy to `sticky`. See [Round-Robin Limitations](#round-robin-limitations). |
|
|
440
474
|
|
|
441
475
|
### OAuth-only behavior
|
|
442
476
|
|
package/index.mjs
CHANGED
|
@@ -662,9 +662,10 @@ function buildSystemPromptBlocks(system, signature, messages) {
|
|
|
662
662
|
* @param {string} model
|
|
663
663
|
* @param {"anthropic" | "bedrock" | "vertex" | "foundry"} provider
|
|
664
664
|
* @param {string[]} [customBetas]
|
|
665
|
+
* @param {import('./lib/config.mjs').AccountSelectionStrategy} [strategy]
|
|
665
666
|
* @returns {string}
|
|
666
667
|
*/
|
|
667
|
-
function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provider, customBetas) {
|
|
668
|
+
function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provider, customBetas, strategy) {
|
|
668
669
|
const incomingBetasList = incomingBeta
|
|
669
670
|
.split(",")
|
|
670
671
|
.map((b) => b.trim())
|
|
@@ -679,13 +680,19 @@ function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provide
|
|
|
679
680
|
|
|
680
681
|
const nonInteractive = isNonInteractiveMode();
|
|
681
682
|
const haiku = isHaikuModel(model);
|
|
683
|
+
const isRoundRobin = strategy === "round-robin";
|
|
682
684
|
|
|
683
685
|
if (!haiku) {
|
|
684
686
|
betas.push(CLAUDE_CODE_BETA_FLAG);
|
|
685
|
-
|
|
687
|
+
// Code execution sandbox state is per-account; skip in round-robin to avoid
|
|
688
|
+
// losing sandbox state mid-workflow when requests rotate between accounts.
|
|
689
|
+
if (!isRoundRobin) {
|
|
690
|
+
betas.push("code-execution-2025-08-25");
|
|
691
|
+
}
|
|
686
692
|
}
|
|
687
693
|
|
|
688
694
|
// Files API is model-independent; enables /v1/files endpoint and file_id references.
|
|
695
|
+
// Safe in round-robin because file-ID account pinning routes requests to the correct account.
|
|
689
696
|
betas.push("files-api-2025-04-14");
|
|
690
697
|
|
|
691
698
|
if (isOpus46Model(model)) {
|
|
@@ -723,7 +730,9 @@ function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provide
|
|
|
723
730
|
betas.push("web-search-2025-03-05");
|
|
724
731
|
}
|
|
725
732
|
|
|
726
|
-
|
|
733
|
+
// Prompt caching is per-workspace (since Feb 2026); round-robin across accounts
|
|
734
|
+
// means zero cache hits and doubled token costs. Skip in round-robin.
|
|
735
|
+
if (nonInteractive && !isRoundRobin) {
|
|
727
736
|
betas.push("prompt-caching-scope-2026-01-05");
|
|
728
737
|
}
|
|
729
738
|
|
|
@@ -864,7 +873,7 @@ async function fetchLatestClaudeCodeVersion(timeoutMs = 1200) {
|
|
|
864
873
|
* @param {string} accessToken
|
|
865
874
|
* @param {string | undefined} requestBody
|
|
866
875
|
* @param {URL | null} requestUrl
|
|
867
|
-
* @param {{enabled: boolean, claudeCliVersion: string}} signature
|
|
876
|
+
* @param {{enabled: boolean, claudeCliVersion: string, strategy?: import('./lib/config.mjs').AccountSelectionStrategy}} signature
|
|
868
877
|
* @returns {Headers}
|
|
869
878
|
*/
|
|
870
879
|
function buildRequestHeaders(input, requestInit, accessToken, requestBody, requestUrl, signature) {
|
|
@@ -898,7 +907,14 @@ function buildRequestHeaders(input, requestInit, accessToken, requestBody, reque
|
|
|
898
907
|
const incomingBeta = requestHeaders.get("anthropic-beta") || "";
|
|
899
908
|
const { model, tools, messages } = parseRequestBodyMetadata(requestBody);
|
|
900
909
|
const provider = detectProvider(requestUrl);
|
|
901
|
-
const mergedBetas = buildAnthropicBetaHeader(
|
|
910
|
+
const mergedBetas = buildAnthropicBetaHeader(
|
|
911
|
+
incomingBeta,
|
|
912
|
+
signature.enabled,
|
|
913
|
+
model,
|
|
914
|
+
provider,
|
|
915
|
+
signature.customBetas,
|
|
916
|
+
signature.strategy,
|
|
917
|
+
);
|
|
902
918
|
|
|
903
919
|
const authTokenOverride = process.env.ANTHROPIC_AUTH_TOKEN?.trim();
|
|
904
920
|
const bearerToken = authTokenOverride || accessToken;
|
|
@@ -1566,6 +1582,30 @@ function parseCommandArgs(raw) {
|
|
|
1566
1582
|
return parts;
|
|
1567
1583
|
}
|
|
1568
1584
|
|
|
1585
|
+
/**
|
|
1586
|
+
* Extract all file_id values from a parsed Messages API request body.
|
|
1587
|
+
* Walks messages and system arrays looking for file content blocks.
|
|
1588
|
+
* @param {any} body - Parsed JSON body
|
|
1589
|
+
* @returns {string[]}
|
|
1590
|
+
*/
|
|
1591
|
+
function extractFileIds(body) {
|
|
1592
|
+
const ids = [];
|
|
1593
|
+
if (!body || typeof body !== "object") return ids;
|
|
1594
|
+
function walk(obj) {
|
|
1595
|
+
if (Array.isArray(obj)) {
|
|
1596
|
+
for (const item of obj) walk(item);
|
|
1597
|
+
} else if (obj && typeof obj === "object") {
|
|
1598
|
+
if (obj.source?.file_id) ids.push(obj.source.file_id);
|
|
1599
|
+
for (const val of Object.values(obj)) {
|
|
1600
|
+
if (val && typeof val === "object") walk(val);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
walk(body.messages);
|
|
1605
|
+
walk(body.system);
|
|
1606
|
+
return ids;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1569
1609
|
/**
|
|
1570
1610
|
* @type {import('@opencode-ai/plugin').Plugin}
|
|
1571
1611
|
*/
|
|
@@ -1596,12 +1636,27 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
1596
1636
|
const IDLE_REFRESH_WINDOW_MS = config.idle_refresh.window_minutes * 60 * 1000;
|
|
1597
1637
|
const IDLE_REFRESH_MIN_INTERVAL_MS = config.idle_refresh.min_interval_minutes * 60 * 1000;
|
|
1598
1638
|
|
|
1639
|
+
/**
|
|
1640
|
+
* Whether OPENCODE_ANTHROPIC_INITIAL_ACCOUNT env var pinned this session to a
|
|
1641
|
+
* specific account. When true, syncActiveIndexFromDisk is skipped and strategy
|
|
1642
|
+
* is forced to sticky.
|
|
1643
|
+
*/
|
|
1644
|
+
let initialAccountPinned = false;
|
|
1645
|
+
|
|
1599
1646
|
/**
|
|
1600
1647
|
* Pending slash-command OAuth flows keyed by session ID.
|
|
1601
1648
|
* @type {Map<string, { mode: "login" | "reauth", verifier: string, targetIndex?: number, createdAt: number }>}
|
|
1602
1649
|
*/
|
|
1603
1650
|
const pendingSlashOAuth = new Map();
|
|
1604
1651
|
|
|
1652
|
+
/**
|
|
1653
|
+
* In-memory mapping of file_id → account index for file-ID account pinning.
|
|
1654
|
+
* Populated by /anthropic files commands, consumed by the fetch interceptor
|
|
1655
|
+
* to route Messages API requests referencing file_ids to the correct account.
|
|
1656
|
+
* @type {Map<string, number>}
|
|
1657
|
+
*/
|
|
1658
|
+
const fileAccountMap = new Map();
|
|
1659
|
+
|
|
1605
1660
|
/**
|
|
1606
1661
|
* Send an informational message into the current session.
|
|
1607
1662
|
* @param {string} sessionID
|
|
@@ -1978,6 +2033,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
1978
2033
|
|
|
1979
2034
|
if (!action || action === "list") {
|
|
1980
2035
|
const fresh = loadConfigFresh();
|
|
2036
|
+
const strategy = fresh.account_selection_strategy || config.account_selection_strategy;
|
|
1981
2037
|
const lines = [
|
|
1982
2038
|
"▣ Anthropic Betas",
|
|
1983
2039
|
"",
|
|
@@ -1985,15 +2041,16 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
1985
2041
|
" oauth-2025-04-20, claude-code-20250219,",
|
|
1986
2042
|
" interleaved-thinking-2025-05-14 OR adaptive-thinking-2026-01-28,",
|
|
1987
2043
|
" fine-grained-tool-streaming-2025-05-14,",
|
|
1988
|
-
|
|
1989
|
-
" files-api-2025-04-14",
|
|
2044
|
+
` code-execution-2025-08-25 (non-Haiku${strategy === "round-robin" ? ", skipped in round-robin" : ""}),`,
|
|
2045
|
+
" files-api-2025-04-14,",
|
|
2046
|
+
` prompt-caching-scope-2026-01-05 (non-interactive${strategy === "round-robin" ? ", skipped in round-robin" : ""})`,
|
|
1990
2047
|
"",
|
|
2048
|
+
`Strategy: ${strategy}${initialAccountPinned ? " (pinned via OPENCODE_ANTHROPIC_INITIAL_ACCOUNT)" : ""}`,
|
|
1991
2049
|
`Custom betas: ${fresh.custom_betas.length ? fresh.custom_betas.join(", ") : "(none)"}`,
|
|
1992
2050
|
"",
|
|
1993
2051
|
"Toggleable presets:",
|
|
1994
2052
|
" /anthropic betas add structured-outputs-2025-12-15",
|
|
1995
2053
|
" /anthropic betas add context-management-2025-06-27",
|
|
1996
|
-
" /anthropic betas add prompt-caching-scope-2026-01-05",
|
|
1997
2054
|
" /anthropic betas add tool-examples-2025-10-29",
|
|
1998
2055
|
" /anthropic betas add web-search-2025-03-05",
|
|
1999
2056
|
" /anthropic betas add compact-2026-01-12",
|
|
@@ -2046,12 +2103,23 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2046
2103
|
}
|
|
2047
2104
|
|
|
2048
2105
|
// /anthropic files [list|upload|get|delete|download] — Files API management
|
|
2106
|
+
// Supports --account <email|index> to target a specific account.
|
|
2107
|
+
// Without --account, list aggregates from ALL accounts; other actions use the current account.
|
|
2049
2108
|
if (primary === "files") {
|
|
2050
|
-
|
|
2109
|
+
// Parse --account flag from args
|
|
2110
|
+
let targetAccountId = null;
|
|
2111
|
+
const filteredArgs = [];
|
|
2112
|
+
for (let i = 0; i < args.length; i++) {
|
|
2113
|
+
if (args[i] === "--account" && i + 1 < args.length) {
|
|
2114
|
+
targetAccountId = args[i + 1];
|
|
2115
|
+
i++;
|
|
2116
|
+
} else {
|
|
2117
|
+
filteredArgs.push(args[i]);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
const action = (filteredArgs[1] || "").toLowerCase();
|
|
2051
2121
|
|
|
2052
|
-
|
|
2053
|
-
const account = accountManager?.getCurrentAccount();
|
|
2054
|
-
if (!account) {
|
|
2122
|
+
if (!accountManager || accountManager.getAccountCount() === 0) {
|
|
2055
2123
|
await sendCommandMessage(
|
|
2056
2124
|
input.sessionID,
|
|
2057
2125
|
"▣ Anthropic Files (error)\n\nNo accounts configured. Use /anthropic login first.",
|
|
@@ -2059,54 +2127,152 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2059
2127
|
return;
|
|
2060
2128
|
}
|
|
2061
2129
|
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2130
|
+
/**
|
|
2131
|
+
* Resolve a single account by email or 1-based index.
|
|
2132
|
+
* If identifier is null, falls back to the current account.
|
|
2133
|
+
* @param {string | null} identifier
|
|
2134
|
+
* @returns {{ account: import('./lib/accounts.mjs').ManagedAccount, label: string } | null}
|
|
2135
|
+
*/
|
|
2136
|
+
function resolveTargetAccount(identifier) {
|
|
2137
|
+
const accounts = accountManager.getEnabledAccounts();
|
|
2138
|
+
if (identifier) {
|
|
2139
|
+
// Try by email
|
|
2140
|
+
const byEmail = accounts.find((a) => a.email === identifier);
|
|
2141
|
+
if (byEmail) return { account: byEmail, label: byEmail.email || `Account ${byEmail.index + 1}` };
|
|
2142
|
+
// Try by 1-based index
|
|
2143
|
+
const idx = parseInt(identifier, 10);
|
|
2144
|
+
if (!isNaN(idx) && idx >= 1) {
|
|
2145
|
+
const byIdx = accounts.find((a) => a.index === idx - 1);
|
|
2146
|
+
if (byIdx) return { account: byIdx, label: byIdx.email || `Account ${byIdx.index + 1}` };
|
|
2147
|
+
}
|
|
2148
|
+
return null;
|
|
2149
|
+
}
|
|
2150
|
+
// Default to current
|
|
2151
|
+
const current = accountManager.getCurrentAccount();
|
|
2152
|
+
if (!current) return null;
|
|
2153
|
+
return { account: current, label: current.email || `Account ${current.index + 1}` };
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
/**
|
|
2157
|
+
* Get authenticated headers for a specific account, refreshing token if needed.
|
|
2158
|
+
* @param {import('./lib/accounts.mjs').ManagedAccount} acct
|
|
2159
|
+
*/
|
|
2160
|
+
async function getFilesAuth(acct) {
|
|
2161
|
+
let tok = acct.access;
|
|
2162
|
+
if (!tok || !acct.expires || acct.expires < Date.now()) {
|
|
2163
|
+
tok = await refreshAccountTokenSingleFlight(acct);
|
|
2072
2164
|
}
|
|
2165
|
+
return {
|
|
2166
|
+
authorization: `Bearer ${tok}`,
|
|
2167
|
+
"anthropic-beta": "files-api-2025-04-14",
|
|
2168
|
+
};
|
|
2073
2169
|
}
|
|
2074
2170
|
|
|
2075
2171
|
const apiBase = "https://api.anthropic.com";
|
|
2076
|
-
const authHeaders = {
|
|
2077
|
-
authorization: `Bearer ${token}`,
|
|
2078
|
-
"anthropic-beta": "files-api-2025-04-14",
|
|
2079
|
-
};
|
|
2080
2172
|
|
|
2081
2173
|
try {
|
|
2082
|
-
// /anthropic files list — list
|
|
2174
|
+
// /anthropic files list — list uploaded files
|
|
2083
2175
|
if (!action || action === "list") {
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
const
|
|
2087
|
-
|
|
2176
|
+
if (targetAccountId) {
|
|
2177
|
+
// List for a specific account
|
|
2178
|
+
const resolved = resolveTargetAccount(targetAccountId);
|
|
2179
|
+
if (!resolved) {
|
|
2180
|
+
await sendCommandMessage(
|
|
2181
|
+
input.sessionID,
|
|
2182
|
+
`▣ Anthropic Files (error)\n\nAccount not found: ${targetAccountId}`,
|
|
2183
|
+
);
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
const { account, label } = resolved;
|
|
2187
|
+
const headers = await getFilesAuth(account);
|
|
2188
|
+
const res = await fetch(`${apiBase}/v1/files`, { headers });
|
|
2189
|
+
if (!res.ok) {
|
|
2190
|
+
const errBody = await res.text();
|
|
2191
|
+
await sendCommandMessage(
|
|
2192
|
+
input.sessionID,
|
|
2193
|
+
`▣ Anthropic Files (error) [${label}]\n\nHTTP ${res.status}: ${errBody}`,
|
|
2194
|
+
);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
const data = await res.json();
|
|
2198
|
+
const files = data.data || [];
|
|
2199
|
+
for (const f of files) fileAccountMap.set(f.id, account.index);
|
|
2200
|
+
if (files.length === 0) {
|
|
2201
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Files [${label}]\n\nNo files uploaded.`);
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
const lines = [`▣ Anthropic Files [${label}]`, "", `${files.length} file(s):`, ""];
|
|
2205
|
+
for (const f of files) {
|
|
2206
|
+
const sizeKB = (f.size / 1024).toFixed(1);
|
|
2207
|
+
lines.push(` ${f.id} ${f.filename} (${sizeKB} KB, ${f.purpose})`);
|
|
2208
|
+
}
|
|
2209
|
+
await sendCommandMessage(input.sessionID, lines.join("\n"));
|
|
2088
2210
|
return;
|
|
2089
2211
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2212
|
+
|
|
2213
|
+
// List files from ALL enabled accounts
|
|
2214
|
+
const accounts = accountManager.getEnabledAccounts();
|
|
2215
|
+
const allLines = ["▣ Anthropic Files (all accounts)", ""];
|
|
2216
|
+
let totalFiles = 0;
|
|
2217
|
+
for (const acct of accounts) {
|
|
2218
|
+
const label = acct.email || `Account ${acct.index + 1}`;
|
|
2219
|
+
try {
|
|
2220
|
+
const headers = await getFilesAuth(acct);
|
|
2221
|
+
const res = await fetch(`${apiBase}/v1/files`, { headers });
|
|
2222
|
+
if (!res.ok) {
|
|
2223
|
+
allLines.push(`[${label}] Error: HTTP ${res.status}`);
|
|
2224
|
+
allLines.push("");
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
const data = await res.json();
|
|
2228
|
+
const files = data.data || [];
|
|
2229
|
+
for (const f of files) fileAccountMap.set(f.id, acct.index);
|
|
2230
|
+
totalFiles += files.length;
|
|
2231
|
+
if (files.length === 0) {
|
|
2232
|
+
allLines.push(`[${label}] No files`);
|
|
2233
|
+
} else {
|
|
2234
|
+
allLines.push(`[${label}] ${files.length} file(s):`);
|
|
2235
|
+
for (const f of files) {
|
|
2236
|
+
const sizeKB = (f.size / 1024).toFixed(1);
|
|
2237
|
+
allLines.push(` ${f.id} ${f.filename} (${sizeKB} KB, ${f.purpose})`);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
allLines.push("");
|
|
2241
|
+
} catch (err) {
|
|
2242
|
+
allLines.push(`[${label}] Error: ${err.message}`);
|
|
2243
|
+
allLines.push("");
|
|
2244
|
+
}
|
|
2095
2245
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2246
|
+
if (totalFiles === 0 && accounts.length > 0) {
|
|
2247
|
+
allLines.push(`Total: No files across ${accounts.length} account(s).`);
|
|
2248
|
+
} else {
|
|
2249
|
+
allLines.push(`Total: ${totalFiles} file(s) across ${accounts.length} account(s).`);
|
|
2100
2250
|
}
|
|
2101
|
-
|
|
2251
|
+
if (accounts.length > 1) {
|
|
2252
|
+
allLines.push("", "Tip: Use --account <email> to target a specific account.");
|
|
2253
|
+
}
|
|
2254
|
+
await sendCommandMessage(input.sessionID, allLines.join("\n"));
|
|
2102
2255
|
return;
|
|
2103
2256
|
}
|
|
2104
2257
|
|
|
2258
|
+
// For all non-list actions, resolve to a single account
|
|
2259
|
+
const resolved = resolveTargetAccount(targetAccountId);
|
|
2260
|
+
if (!resolved) {
|
|
2261
|
+
const errMsg = targetAccountId ? `Account not found: ${targetAccountId}` : "No accounts available.";
|
|
2262
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Files (error)\n\n${errMsg}`);
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
const { account, label } = resolved;
|
|
2266
|
+
const authHeaders = await getFilesAuth(account);
|
|
2267
|
+
|
|
2105
2268
|
// /anthropic files upload <path> — upload a file
|
|
2106
2269
|
if (action === "upload") {
|
|
2107
|
-
const filePath =
|
|
2270
|
+
const filePath = filteredArgs.slice(2).join(" ").trim();
|
|
2108
2271
|
if (!filePath) {
|
|
2109
|
-
await sendCommandMessage(
|
|
2272
|
+
await sendCommandMessage(
|
|
2273
|
+
input.sessionID,
|
|
2274
|
+
"▣ Anthropic Files\n\nUsage: /anthropic files upload <path> [--account <email>]",
|
|
2275
|
+
);
|
|
2110
2276
|
return;
|
|
2111
2277
|
}
|
|
2112
2278
|
const resolvedPath = resolve(filePath);
|
|
@@ -2124,7 +2290,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2124
2290
|
const res = await fetch(`${apiBase}/v1/files`, {
|
|
2125
2291
|
method: "POST",
|
|
2126
2292
|
headers: {
|
|
2127
|
-
authorization:
|
|
2293
|
+
authorization: authHeaders.authorization,
|
|
2128
2294
|
"anthropic-beta": "files-api-2025-04-14",
|
|
2129
2295
|
},
|
|
2130
2296
|
body: form,
|
|
@@ -2133,35 +2299,44 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2133
2299
|
const errBody = await res.text();
|
|
2134
2300
|
await sendCommandMessage(
|
|
2135
2301
|
input.sessionID,
|
|
2136
|
-
`▣ Anthropic Files (error)\n\nUpload failed (HTTP ${res.status}): ${errBody}`,
|
|
2302
|
+
`▣ Anthropic Files (error) [${label}]\n\nUpload failed (HTTP ${res.status}): ${errBody}`,
|
|
2137
2303
|
);
|
|
2138
2304
|
return;
|
|
2139
2305
|
}
|
|
2140
2306
|
const file = await res.json();
|
|
2141
2307
|
const sizeKB = ((file.size || 0) / 1024).toFixed(1);
|
|
2308
|
+
// Cache file_id → account mapping for auto-pinning
|
|
2309
|
+
fileAccountMap.set(file.id, account.index);
|
|
2142
2310
|
await sendCommandMessage(
|
|
2143
2311
|
input.sessionID,
|
|
2144
|
-
`▣ Anthropic Files\n\nUploaded: ${file.id}\n Filename: ${file.filename}\n Size: ${sizeKB} KB`,
|
|
2312
|
+
`▣ Anthropic Files [${label}]\n\nUploaded: ${file.id}\n Filename: ${file.filename}\n Size: ${sizeKB} KB`,
|
|
2145
2313
|
);
|
|
2146
2314
|
return;
|
|
2147
2315
|
}
|
|
2148
2316
|
|
|
2149
2317
|
// /anthropic files get <file_id> — get file metadata
|
|
2150
2318
|
if (action === "get" || action === "info") {
|
|
2151
|
-
const fileId =
|
|
2319
|
+
const fileId = filteredArgs[2]?.trim();
|
|
2152
2320
|
if (!fileId) {
|
|
2153
|
-
await sendCommandMessage(
|
|
2321
|
+
await sendCommandMessage(
|
|
2322
|
+
input.sessionID,
|
|
2323
|
+
"▣ Anthropic Files\n\nUsage: /anthropic files get <file_id> [--account <email>]",
|
|
2324
|
+
);
|
|
2154
2325
|
return;
|
|
2155
2326
|
}
|
|
2156
2327
|
const res = await fetch(`${apiBase}/v1/files/${encodeURIComponent(fileId)}`, { headers: authHeaders });
|
|
2157
2328
|
if (!res.ok) {
|
|
2158
2329
|
const errBody = await res.text();
|
|
2159
|
-
await sendCommandMessage(
|
|
2330
|
+
await sendCommandMessage(
|
|
2331
|
+
input.sessionID,
|
|
2332
|
+
`▣ Anthropic Files (error) [${label}]\n\nHTTP ${res.status}: ${errBody}`,
|
|
2333
|
+
);
|
|
2160
2334
|
return;
|
|
2161
2335
|
}
|
|
2162
2336
|
const file = await res.json();
|
|
2337
|
+
fileAccountMap.set(file.id, account.index);
|
|
2163
2338
|
const lines = [
|
|
2164
|
-
|
|
2339
|
+
`▣ Anthropic Files [${label}]`,
|
|
2165
2340
|
"",
|
|
2166
2341
|
` ID: ${file.id}`,
|
|
2167
2342
|
` Filename: ${file.filename}`,
|
|
@@ -2176,9 +2351,12 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2176
2351
|
|
|
2177
2352
|
// /anthropic files delete <file_id> — delete a file
|
|
2178
2353
|
if (action === "delete" || action === "rm") {
|
|
2179
|
-
const fileId =
|
|
2354
|
+
const fileId = filteredArgs[2]?.trim();
|
|
2180
2355
|
if (!fileId) {
|
|
2181
|
-
await sendCommandMessage(
|
|
2356
|
+
await sendCommandMessage(
|
|
2357
|
+
input.sessionID,
|
|
2358
|
+
"▣ Anthropic Files\n\nUsage: /anthropic files delete <file_id> [--account <email>]",
|
|
2359
|
+
);
|
|
2182
2360
|
return;
|
|
2183
2361
|
}
|
|
2184
2362
|
const res = await fetch(`${apiBase}/v1/files/${encodeURIComponent(fileId)}`, {
|
|
@@ -2187,24 +2365,28 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2187
2365
|
});
|
|
2188
2366
|
if (!res.ok) {
|
|
2189
2367
|
const errBody = await res.text();
|
|
2190
|
-
await sendCommandMessage(
|
|
2368
|
+
await sendCommandMessage(
|
|
2369
|
+
input.sessionID,
|
|
2370
|
+
`▣ Anthropic Files (error) [${label}]\n\nHTTP ${res.status}: ${errBody}`,
|
|
2371
|
+
);
|
|
2191
2372
|
return;
|
|
2192
2373
|
}
|
|
2193
|
-
|
|
2374
|
+
fileAccountMap.delete(fileId);
|
|
2375
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Files [${label}]\n\nDeleted: ${fileId}`);
|
|
2194
2376
|
return;
|
|
2195
2377
|
}
|
|
2196
2378
|
|
|
2197
2379
|
// /anthropic files download <file_id> [output_path] — download file content
|
|
2198
2380
|
if (action === "download" || action === "dl") {
|
|
2199
|
-
const fileId =
|
|
2381
|
+
const fileId = filteredArgs[2]?.trim();
|
|
2200
2382
|
if (!fileId) {
|
|
2201
2383
|
await sendCommandMessage(
|
|
2202
2384
|
input.sessionID,
|
|
2203
|
-
"▣ Anthropic Files\n\nUsage: /anthropic files download <file_id> [output_path]",
|
|
2385
|
+
"▣ Anthropic Files\n\nUsage: /anthropic files download <file_id> [output_path] [--account <email>]",
|
|
2204
2386
|
);
|
|
2205
2387
|
return;
|
|
2206
2388
|
}
|
|
2207
|
-
const outputPath =
|
|
2389
|
+
const outputPath = filteredArgs.slice(3).join(" ").trim();
|
|
2208
2390
|
|
|
2209
2391
|
// Get file metadata first for the filename
|
|
2210
2392
|
const metaRes = await fetch(`${apiBase}/v1/files/${encodeURIComponent(fileId)}`, {
|
|
@@ -2214,7 +2396,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2214
2396
|
const errBody = await metaRes.text();
|
|
2215
2397
|
await sendCommandMessage(
|
|
2216
2398
|
input.sessionID,
|
|
2217
|
-
`▣ Anthropic Files (error)\n\nHTTP ${metaRes.status}: ${errBody}`,
|
|
2399
|
+
`▣ Anthropic Files (error) [${label}]\n\nHTTP ${metaRes.status}: ${errBody}`,
|
|
2218
2400
|
);
|
|
2219
2401
|
return;
|
|
2220
2402
|
}
|
|
@@ -2229,7 +2411,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2229
2411
|
const errBody = await res.text();
|
|
2230
2412
|
await sendCommandMessage(
|
|
2231
2413
|
input.sessionID,
|
|
2232
|
-
`▣ Anthropic Files (error)\n\nDownload failed (HTTP ${res.status}): ${errBody}`,
|
|
2414
|
+
`▣ Anthropic Files (error) [${label}]\n\nDownload failed (HTTP ${res.status}): ${errBody}`,
|
|
2233
2415
|
);
|
|
2234
2416
|
return;
|
|
2235
2417
|
}
|
|
@@ -2238,7 +2420,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2238
2420
|
const sizeKB = (buffer.length / 1024).toFixed(1);
|
|
2239
2421
|
await sendCommandMessage(
|
|
2240
2422
|
input.sessionID,
|
|
2241
|
-
`▣ Anthropic Files\n\nDownloaded: ${meta.filename}\n Saved to: ${savePath}\n Size: ${sizeKB} KB`,
|
|
2423
|
+
`▣ Anthropic Files [${label}]\n\nDownloaded: ${meta.filename}\n Saved to: ${savePath}\n Size: ${sizeKB} KB`,
|
|
2242
2424
|
);
|
|
2243
2425
|
return;
|
|
2244
2426
|
}
|
|
@@ -2247,17 +2429,23 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2247
2429
|
const helpLines = [
|
|
2248
2430
|
"▣ Anthropic Files",
|
|
2249
2431
|
"",
|
|
2250
|
-
"Usage: /anthropic files <action>",
|
|
2432
|
+
"Usage: /anthropic files <action> [--account <email|index>]",
|
|
2251
2433
|
"",
|
|
2252
2434
|
"Actions:",
|
|
2253
|
-
" list List uploaded files",
|
|
2435
|
+
" list List uploaded files (all accounts if no --account)",
|
|
2254
2436
|
" upload <path> Upload a file (max 350MB)",
|
|
2255
2437
|
" get <file_id> Get file metadata",
|
|
2256
2438
|
" delete <file_id> Delete a file",
|
|
2257
2439
|
" download <file_id> [path] Download file content",
|
|
2258
2440
|
"",
|
|
2441
|
+
"Options:",
|
|
2442
|
+
" --account <email|index> Target a specific account (1-based index)",
|
|
2443
|
+
"",
|
|
2259
2444
|
"Supported formats: PDF, DOCX, TXT, CSV, Excel, Markdown, images",
|
|
2260
2445
|
"Files can be referenced by file_id in Messages API requests.",
|
|
2446
|
+
"",
|
|
2447
|
+
"When using round-robin, file_ids are automatically pinned to the",
|
|
2448
|
+
"account that owns them for Messages API requests.",
|
|
2261
2449
|
];
|
|
2262
2450
|
await sendCommandMessage(input.sessionID, helpLines.join("\n"));
|
|
2263
2451
|
return;
|
|
@@ -2584,6 +2772,39 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2584
2772
|
await accountManager.saveToDisk();
|
|
2585
2773
|
}
|
|
2586
2774
|
|
|
2775
|
+
// OPENCODE_ANTHROPIC_INITIAL_ACCOUNT: pin this session to a specific account.
|
|
2776
|
+
// Accepts 1-based index or email. Overrides strategy to sticky and disables
|
|
2777
|
+
// syncActiveIndexFromDisk so other sessions can't override this one.
|
|
2778
|
+
// Use case: terminal 1 with INITIAL_ACCOUNT=1, terminal 2 with =2.
|
|
2779
|
+
const initialAccountEnv = process.env.OPENCODE_ANTHROPIC_INITIAL_ACCOUNT?.trim();
|
|
2780
|
+
if (initialAccountEnv && accountManager.getAccountCount() > 1) {
|
|
2781
|
+
const accounts = accountManager.getEnabledAccounts();
|
|
2782
|
+
let target = null;
|
|
2783
|
+
|
|
2784
|
+
// Try as 1-based index
|
|
2785
|
+
const asIndex = parseInt(initialAccountEnv, 10);
|
|
2786
|
+
if (!isNaN(asIndex) && asIndex >= 1 && asIndex <= accounts.length) {
|
|
2787
|
+
target = accounts[asIndex - 1];
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// Try as email
|
|
2791
|
+
if (!target) {
|
|
2792
|
+
target = accounts.find((a) => a.email && a.email.toLowerCase() === initialAccountEnv.toLowerCase());
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
if (target && accountManager.forceCurrentIndex(target.index)) {
|
|
2796
|
+
config.account_selection_strategy = "sticky";
|
|
2797
|
+
initialAccountPinned = true;
|
|
2798
|
+
debugLog("OPENCODE_ANTHROPIC_INITIAL_ACCOUNT: pinned to account", {
|
|
2799
|
+
index: target.index + 1,
|
|
2800
|
+
email: target.email,
|
|
2801
|
+
strategy: "sticky (overridden)",
|
|
2802
|
+
});
|
|
2803
|
+
} else {
|
|
2804
|
+
debugLog("OPENCODE_ANTHROPIC_INITIAL_ACCOUNT: could not resolve account", initialAccountEnv);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2587
2808
|
return {
|
|
2588
2809
|
apiKey: "",
|
|
2589
2810
|
/**
|
|
@@ -2612,7 +2833,9 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2612
2833
|
const transientRefreshSkips = new Set();
|
|
2613
2834
|
|
|
2614
2835
|
// Sync with CLI changes at request start.
|
|
2615
|
-
|
|
2836
|
+
// Skip when OPENCODE_ANTHROPIC_INITIAL_ACCOUNT pinned this session —
|
|
2837
|
+
// other sessions' CLI changes must not override the pinned account.
|
|
2838
|
+
if (accountManager && !initialAccountPinned) {
|
|
2616
2839
|
await accountManager.syncActiveIndexFromDisk();
|
|
2617
2840
|
}
|
|
2618
2841
|
|
|
@@ -2620,9 +2843,42 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2620
2843
|
// switch to the next account. If it's service-wide, return immediately.
|
|
2621
2844
|
const maxAttempts = accountManager.getTotalAccountCount();
|
|
2622
2845
|
|
|
2846
|
+
// File-ID account pinning: if the request body references file_ids
|
|
2847
|
+
// that we've mapped to a specific account (via /anthropic files),
|
|
2848
|
+
// pin the first attempt to that account so files are accessible.
|
|
2849
|
+
// Without this, round-robin could route to an account that doesn't
|
|
2850
|
+
// have the referenced files, causing file_not_found errors.
|
|
2851
|
+
let pinnedAccount = null;
|
|
2852
|
+
if (typeof requestInit.body === "string" && fileAccountMap.size > 0) {
|
|
2853
|
+
try {
|
|
2854
|
+
const bodyObj = JSON.parse(requestInit.body);
|
|
2855
|
+
const fileIds = extractFileIds(bodyObj);
|
|
2856
|
+
for (const fid of fileIds) {
|
|
2857
|
+
const pinnedIndex = fileAccountMap.get(fid);
|
|
2858
|
+
if (pinnedIndex !== undefined) {
|
|
2859
|
+
const candidates = accountManager.getEnabledAccounts();
|
|
2860
|
+
pinnedAccount = candidates.find((a) => a.index === pinnedIndex) ?? null;
|
|
2861
|
+
if (pinnedAccount) {
|
|
2862
|
+
debugLog("file-id pinning: routing to account", {
|
|
2863
|
+
fileId: fid,
|
|
2864
|
+
accountIndex: pinnedIndex,
|
|
2865
|
+
email: pinnedAccount.email,
|
|
2866
|
+
});
|
|
2867
|
+
break;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
} catch {
|
|
2872
|
+
// Non-JSON body or parse error — skip pinning
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2623
2876
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2624
|
-
// Select account
|
|
2625
|
-
const account =
|
|
2877
|
+
// Select account — use pinned account on first attempt if available
|
|
2878
|
+
const account =
|
|
2879
|
+
attempt === 0 && pinnedAccount && !transientRefreshSkips.has(pinnedAccount.index)
|
|
2880
|
+
? pinnedAccount
|
|
2881
|
+
: accountManager.getCurrentAccount(transientRefreshSkips);
|
|
2626
2882
|
|
|
2627
2883
|
// Toast account usage on first use and whenever the account changes
|
|
2628
2884
|
if (showUsageToast && account && accountManager) {
|
|
@@ -2756,6 +3012,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2756
3012
|
enabled: signatureEmulationEnabled,
|
|
2757
3013
|
claudeCliVersion,
|
|
2758
3014
|
customBetas: config.custom_betas,
|
|
3015
|
+
strategy: config.account_selection_strategy,
|
|
2759
3016
|
});
|
|
2760
3017
|
// Execute the request
|
|
2761
3018
|
let response;
|
package/lib/accounts.mjs
CHANGED
|
@@ -193,6 +193,20 @@ export class AccountManager {
|
|
|
193
193
|
return this.#currentIndex;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Force the active account to a specific index.
|
|
198
|
+
* Used by OPENCODE_ANTHROPIC_INITIAL_ACCOUNT to pin a session to one account.
|
|
199
|
+
* @param {number} index
|
|
200
|
+
* @returns {boolean} Whether the index was valid and set
|
|
201
|
+
*/
|
|
202
|
+
forceCurrentIndex(index) {
|
|
203
|
+
const account = this.#accounts[index];
|
|
204
|
+
if (!account || !account.enabled) return false;
|
|
205
|
+
this.#currentIndex = index;
|
|
206
|
+
this.#cursor = index;
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
196
210
|
/**
|
|
197
211
|
* Get enabled account references for internal plugin operations.
|
|
198
212
|
* Returned objects are mutable managed accounts.
|
package/lib/config.mjs
CHANGED
|
@@ -61,7 +61,7 @@ import { homedir } from "node:os";
|
|
|
61
61
|
|
|
62
62
|
/** @type {AnthropicAuthConfig} */
|
|
63
63
|
export const DEFAULT_CONFIG = {
|
|
64
|
-
account_selection_strategy: "
|
|
64
|
+
account_selection_strategy: "sticky",
|
|
65
65
|
failure_ttl_seconds: 3600,
|
|
66
66
|
debug: false,
|
|
67
67
|
signature_emulation: {
|
package/lib/config.test.mjs
CHANGED
|
@@ -10,7 +10,7 @@ vi.mock("node:fs", () => ({
|
|
|
10
10
|
|
|
11
11
|
describe("DEFAULT_CONFIG", () => {
|
|
12
12
|
it("has expected default strategy", () => {
|
|
13
|
-
expect(DEFAULT_CONFIG.account_selection_strategy).toBe("
|
|
13
|
+
expect(DEFAULT_CONFIG.account_selection_strategy).toBe("sticky");
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
it("has expected health score defaults", () => {
|
|
@@ -114,7 +114,7 @@ describe("loadConfig", () => {
|
|
|
114
114
|
existsSync.mockReturnValue(true);
|
|
115
115
|
readFileSync.mockReturnValue(JSON.stringify({ account_selection_strategy: "invalid" }));
|
|
116
116
|
const config = loadConfig();
|
|
117
|
-
expect(config.account_selection_strategy).toBe("
|
|
117
|
+
expect(config.account_selection_strategy).toBe("sticky");
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
it("accepts boolean debug", () => {
|
|
@@ -199,7 +199,7 @@ describe("loadConfig", () => {
|
|
|
199
199
|
existsSync.mockReturnValue(false);
|
|
200
200
|
process.env.OPENCODE_ANTHROPIC_STRATEGY = "invalid";
|
|
201
201
|
const config = loadConfig();
|
|
202
|
-
expect(config.account_selection_strategy).toBe("
|
|
202
|
+
expect(config.account_selection_strategy).toBe("sticky");
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
it("enables debug from OPENCODE_ANTHROPIC_DEBUG=1", () => {
|