opencode-anthropic-fix 0.0.19 → 0.0.21
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 +79 -15
- package/index.mjs +373 -5
- package/lib/config.mjs +50 -5
- package/lib/config.test.mjs +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,8 +43,11 @@ The [original plugin](https://github.com/anomalyco/opencode-anthropic-auth) prov
|
|
|
43
43
|
- **Standalone CLI** — manage accounts without opening OpenCode
|
|
44
44
|
- **Configurable strategies** — sticky, round-robin, or hybrid account selection
|
|
45
45
|
- **Claude Code signature emulation** — full HTTP header, system prompt, beta flag, and metadata mimicry derived from Claude Code's open source code
|
|
46
|
-
- **Adaptive thinking for Opus 4.6** — automatically uses `adaptive-thinking-2026-01-28` and maps `budgetTokens` to effort levels (`low`/`medium`/`high
|
|
46
|
+
- **Adaptive thinking for Opus 4.6** — automatically uses `adaptive-thinking-2026-01-28` and maps `budgetTokens` to effort levels (`low`/`medium`/`high`)
|
|
47
47
|
- **1M context limit override** — patches `model.limit.context` so OpenCode compacts at the right threshold while `models.dev` catches up
|
|
48
|
+
- **Runtime config + custom betas** — `/anthropic set`, `/anthropic config`, and `/anthropic betas` slash commands for live feature toggling without restarting OpenCode
|
|
49
|
+
- **Files API integration** — upload, list, download, and manage files via `/anthropic files` with auto-included `files-api-2025-04-14` beta
|
|
50
|
+
- **Code execution support** — auto-includes `code-execution-2025-08-25` beta for sandbox code execution (non-Haiku models)
|
|
48
51
|
|
|
49
52
|
## Installation
|
|
50
53
|
|
|
@@ -231,22 +234,80 @@ Most commands have short aliases: `ln`, `lo`, `ra`, `rf`, `ls`, `st`, `sw`, `en`
|
|
|
231
234
|
|
|
232
235
|
## Slash Commands in OpenCode
|
|
233
236
|
|
|
234
|
-
The plugin
|
|
237
|
+
The plugin registers a built-in `/anthropic` slash command for account management, feature toggles, and custom beta headers — all without leaving OpenCode.
|
|
235
238
|
|
|
236
|
-
###
|
|
239
|
+
### Account management
|
|
237
240
|
|
|
238
241
|
```text
|
|
239
|
-
/anthropic # list (default)
|
|
242
|
+
/anthropic # list accounts (default)
|
|
240
243
|
/anthropic usage # full account list + quota windows
|
|
241
244
|
/anthropic switch 2
|
|
242
245
|
/anthropic refresh 1
|
|
243
246
|
/anthropic logout 2 # revoke tokens and remove account 2
|
|
244
247
|
/anthropic logout --all # revoke all tokens and clear all accounts
|
|
245
|
-
/anthropic strategy hybrid
|
|
246
248
|
/anthropic stats
|
|
247
|
-
/anthropic config
|
|
248
249
|
```
|
|
249
250
|
|
|
251
|
+
### Runtime configuration
|
|
252
|
+
|
|
253
|
+
Toggle features live without restarting OpenCode. Changes are persisted to `anthropic-auth.json`.
|
|
254
|
+
|
|
255
|
+
```text
|
|
256
|
+
/anthropic config # show all current settings
|
|
257
|
+
/anthropic set emulation on # enable/disable Claude signature emulation
|
|
258
|
+
/anthropic set compaction minimal # set prompt compaction mode (minimal/off)
|
|
259
|
+
/anthropic set 1m-context on # enable/disable 1M context limit override
|
|
260
|
+
/anthropic set idle-refresh on # enable/disable idle account refresh
|
|
261
|
+
/anthropic set strategy round-robin # change account selection strategy
|
|
262
|
+
/anthropic set debug on # enable/disable debug logging
|
|
263
|
+
/anthropic set quiet on # suppress non-error toasts
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Custom beta headers
|
|
267
|
+
|
|
268
|
+
Add or remove beta flags that get included in every `anthropic-beta` header. Persisted across sessions.
|
|
269
|
+
|
|
270
|
+
```text
|
|
271
|
+
/anthropic betas # show active betas (auto + custom) with presets
|
|
272
|
+
/anthropic betas add <beta-name> # add a custom beta
|
|
273
|
+
/anthropic betas remove <beta-name> # remove a custom beta
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Available preset betas** (shown by `/anthropic betas`):
|
|
277
|
+
|
|
278
|
+
| Beta | Description |
|
|
279
|
+
| --------------------------------- | ------------------------------------------------- |
|
|
280
|
+
| `prompt-caching-scope-2026-01-05` | Cache control with `scope: "org"` — free perf win |
|
|
281
|
+
| `context-management-2025-06-27` | Server-side auto-summarization when context fills |
|
|
282
|
+
| `structured-outputs-2025-12-15` | Strict JSON schema output enforcement |
|
|
283
|
+
| `tool-examples-2025-10-29` | Input/output examples in tool definitions |
|
|
284
|
+
| `compact-2026-01-12` | Conversation compaction endpoint |
|
|
285
|
+
| `mcp-servers-2025-12-04` | MCP servers in API request payload |
|
|
286
|
+
| `web-search-2025-03-05` | Web search (Vertex/Foundry only) |
|
|
287
|
+
|
|
288
|
+
Example workflow:
|
|
289
|
+
|
|
290
|
+
```text
|
|
291
|
+
/anthropic betas add prompt-caching-scope-2026-01-05
|
|
292
|
+
/anthropic betas add context-management-2025-06-27
|
|
293
|
+
/anthropic betas # verify
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Files API management
|
|
297
|
+
|
|
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
|
+
|
|
300
|
+
```text
|
|
301
|
+
/anthropic files # list uploaded files
|
|
302
|
+
/anthropic files upload ./report.pdf # upload a file
|
|
303
|
+
/anthropic files get file_abc123 # get file metadata
|
|
304
|
+
/anthropic files download file_abc123 # download to current directory
|
|
305
|
+
/anthropic files download file_abc123 ./out.pdf # download to specific path
|
|
306
|
+
/anthropic files delete file_abc123 # delete a file
|
|
307
|
+
```
|
|
308
|
+
|
|
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
|
+
|
|
250
311
|
### OAuth flows from slash command
|
|
251
312
|
|
|
252
313
|
Login and reauth are two-step flows in slash mode:
|
|
@@ -272,11 +333,11 @@ Pending slash OAuth flows expire after 10 minutes. If completion fails with an e
|
|
|
272
333
|
|
|
273
334
|
Control how the plugin picks which account to use for each request.
|
|
274
335
|
|
|
275
|
-
| Strategy
|
|
276
|
-
|
|
|
277
|
-
| **`
|
|
278
|
-
| **`
|
|
279
|
-
| **`hybrid`**
|
|
336
|
+
| Strategy | Behavior | Best For |
|
|
337
|
+
| --------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------- |
|
|
338
|
+
| **`round-robin`** (default) | Rotate through accounts on every request | Spreading load evenly across accounts |
|
|
339
|
+
| **`sticky`** | Stay on one account until it fails or is rate-limited | Single account, or when you want predictable behavior |
|
|
340
|
+
| **`hybrid`** | Score-based selection with stickiness bias. Considers health, token budget, and freshness | Multiple accounts with varying rate limits |
|
|
280
341
|
|
|
281
342
|
### Change Strategy
|
|
282
343
|
|
|
@@ -297,8 +358,8 @@ Configuration is stored at `~/.config/opencode/anthropic-auth.json`. All setting
|
|
|
297
358
|
|
|
298
359
|
```jsonc
|
|
299
360
|
{
|
|
300
|
-
// Account selection strategy: "
|
|
301
|
-
"account_selection_strategy": "
|
|
361
|
+
// Account selection strategy: "round-robin" | "sticky" | "hybrid"
|
|
362
|
+
"account_selection_strategy": "round-robin",
|
|
302
363
|
|
|
303
364
|
// Seconds before consecutive failure count resets (60-7200)
|
|
304
365
|
"failure_ttl_seconds": 3600,
|
|
@@ -325,8 +386,8 @@ Configuration is stored at `~/.config/opencode/anthropic-auth.json`. All setting
|
|
|
325
386
|
// Only applied for OAuth (Max Plan) sessions — API key users use the
|
|
326
387
|
// context-1m-2025-08-07 beta header instead.
|
|
327
388
|
"override_model_limits": {
|
|
328
|
-
// Enable/disable the override
|
|
329
|
-
"enabled":
|
|
389
|
+
// Enable/disable the override (default: off — enable if you need 1M context)
|
|
390
|
+
"enabled": false,
|
|
330
391
|
// Context window to inject (tokens). Default: 1_000_000.
|
|
331
392
|
"context": 1000000,
|
|
332
393
|
// Max output tokens to inject. 0 = leave the model's default unchanged.
|
|
@@ -351,6 +412,9 @@ Configuration is stored at `~/.config/opencode/anthropic-auth.json`. All setting
|
|
|
351
412
|
"initial_tokens": 50,
|
|
352
413
|
},
|
|
353
414
|
|
|
415
|
+
// Custom beta headers (added to every request via /anthropic betas add)
|
|
416
|
+
"custom_betas": [],
|
|
417
|
+
|
|
354
418
|
// Toast notification settings
|
|
355
419
|
"toasts": {
|
|
356
420
|
// Suppress non-error toasts (account status, switching)
|
package/index.mjs
CHANGED
|
@@ -2,10 +2,10 @@ import { createInterface } from "node:readline/promises";
|
|
|
2
2
|
import { stdin, stdout } from "node:process";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { join } from "node:path";
|
|
5
|
+
import { join, resolve, basename } from "node:path";
|
|
6
6
|
import { AccountManager } from "./lib/accounts.mjs";
|
|
7
7
|
import { authorize, exchange, refreshToken } from "./lib/oauth.mjs";
|
|
8
|
-
import { loadConfig, CLIENT_ID, getConfigDir } from "./lib/config.mjs";
|
|
8
|
+
import { loadConfig, loadConfigFresh, saveConfig, CLIENT_ID, getConfigDir } from "./lib/config.mjs";
|
|
9
9
|
import { loadAccounts, saveAccounts, clearAccounts, createDefaultStats } from "./lib/storage.mjs";
|
|
10
10
|
import { applyOAuthCredentials, resetAccountTracking } from "./lib/account-state.mjs";
|
|
11
11
|
import { acquireRefreshLock, releaseRefreshLock } from "./lib/refresh-lock.mjs";
|
|
@@ -119,6 +119,8 @@ const BEDROCK_UNSUPPORTED_BETAS = new Set([
|
|
|
119
119
|
"context-1m-2025-08-07",
|
|
120
120
|
"tool-search-tool-2025-10-19",
|
|
121
121
|
"tool-examples-2025-10-29",
|
|
122
|
+
"code-execution-2025-08-25",
|
|
123
|
+
"files-api-2025-04-14",
|
|
122
124
|
]);
|
|
123
125
|
const STAINLESS_HELPER_KEYS = [
|
|
124
126
|
"x_stainless_helper",
|
|
@@ -659,9 +661,10 @@ function buildSystemPromptBlocks(system, signature, messages) {
|
|
|
659
661
|
* @param {boolean} signatureEnabled
|
|
660
662
|
* @param {string} model
|
|
661
663
|
* @param {"anthropic" | "bedrock" | "vertex" | "foundry"} provider
|
|
664
|
+
* @param {string[]} [customBetas]
|
|
662
665
|
* @returns {string}
|
|
663
666
|
*/
|
|
664
|
-
function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provider) {
|
|
667
|
+
function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provider, customBetas) {
|
|
665
668
|
const incomingBetasList = incomingBeta
|
|
666
669
|
.split(",")
|
|
667
670
|
.map((b) => b.trim())
|
|
@@ -679,8 +682,12 @@ function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provide
|
|
|
679
682
|
|
|
680
683
|
if (!haiku) {
|
|
681
684
|
betas.push(CLAUDE_CODE_BETA_FLAG);
|
|
685
|
+
betas.push("code-execution-2025-08-25");
|
|
682
686
|
}
|
|
683
687
|
|
|
688
|
+
// Files API is model-independent; enables /v1/files endpoint and file_id references.
|
|
689
|
+
betas.push("files-api-2025-04-14");
|
|
690
|
+
|
|
684
691
|
if (isOpus46Model(model)) {
|
|
685
692
|
// Opus 4.6 uses adaptive thinking — model decides how much to think based on task complexity.
|
|
686
693
|
// The effort parameter guides it; budgetTokens/interleaved-thinking are for older models.
|
|
@@ -729,6 +736,10 @@ function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provide
|
|
|
729
736
|
|
|
730
737
|
betas.push("fine-grained-tool-streaming-2025-05-14");
|
|
731
738
|
|
|
739
|
+
if (Array.isArray(customBetas)) {
|
|
740
|
+
betas.push(...customBetas.filter(Boolean));
|
|
741
|
+
}
|
|
742
|
+
|
|
732
743
|
const mergedBetas = [...new Set([...betas, ...incomingBetasList])];
|
|
733
744
|
if (provider === "bedrock") {
|
|
734
745
|
return mergedBetas.filter((beta) => !BEDROCK_UNSUPPORTED_BETAS.has(beta)).join(",");
|
|
@@ -887,7 +898,7 @@ function buildRequestHeaders(input, requestInit, accessToken, requestBody, reque
|
|
|
887
898
|
const incomingBeta = requestHeaders.get("anthropic-beta") || "";
|
|
888
899
|
const { model, tools, messages } = parseRequestBodyMetadata(requestBody);
|
|
889
900
|
const provider = detectProvider(requestUrl);
|
|
890
|
-
const mergedBetas = buildAnthropicBetaHeader(incomingBeta, signature.enabled, model, provider);
|
|
901
|
+
const mergedBetas = buildAnthropicBetaHeader(incomingBeta, signature.enabled, model, provider, signature.customBetas);
|
|
891
902
|
|
|
892
903
|
const authTokenOverride = process.env.ANTHROPIC_AUTH_TOKEN?.trim();
|
|
893
904
|
const bearerToken = authTokenOverride || accessToken;
|
|
@@ -1900,6 +1911,362 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
1900
1911
|
return;
|
|
1901
1912
|
}
|
|
1902
1913
|
|
|
1914
|
+
// /anthropic config — show effective config
|
|
1915
|
+
if (primary === "config") {
|
|
1916
|
+
const fresh = loadConfigFresh();
|
|
1917
|
+
const lines = [
|
|
1918
|
+
"▣ Anthropic Config",
|
|
1919
|
+
"",
|
|
1920
|
+
`strategy: ${fresh.account_selection_strategy}`,
|
|
1921
|
+
`emulation: ${fresh.signature_emulation.enabled ? "on" : "off"}`,
|
|
1922
|
+
`compaction: ${fresh.signature_emulation.prompt_compaction}`,
|
|
1923
|
+
`1m-context: ${fresh.override_model_limits.enabled ? "on" : "off"}`,
|
|
1924
|
+
`idle-refresh: ${fresh.idle_refresh.enabled ? "on" : "off"}`,
|
|
1925
|
+
`debug: ${fresh.debug ? "on" : "off"}`,
|
|
1926
|
+
`quiet: ${fresh.toasts.quiet ? "on" : "off"}`,
|
|
1927
|
+
`custom_betas: ${fresh.custom_betas.length ? fresh.custom_betas.join(", ") : "(none)"}`,
|
|
1928
|
+
];
|
|
1929
|
+
await sendCommandMessage(input.sessionID, lines.join("\n"));
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// /anthropic set <key> <value> — toggle features at runtime
|
|
1934
|
+
if (primary === "set") {
|
|
1935
|
+
const key = (args[1] || "").toLowerCase();
|
|
1936
|
+
const value = (args[2] || "").toLowerCase();
|
|
1937
|
+
/** @type {Record<string, () => void>} */
|
|
1938
|
+
const setters = {
|
|
1939
|
+
emulation: () =>
|
|
1940
|
+
saveConfig({ signature_emulation: { enabled: value === "on" || value === "1" || value === "true" } }),
|
|
1941
|
+
compaction: () =>
|
|
1942
|
+
saveConfig({ signature_emulation: { prompt_compaction: value === "off" ? "off" : "minimal" } }),
|
|
1943
|
+
"1m-context": () =>
|
|
1944
|
+
saveConfig({ override_model_limits: { enabled: value === "on" || value === "1" || value === "true" } }),
|
|
1945
|
+
"idle-refresh": () =>
|
|
1946
|
+
saveConfig({ idle_refresh: { enabled: value === "on" || value === "1" || value === "true" } }),
|
|
1947
|
+
debug: () => saveConfig({ debug: value === "on" || value === "1" || value === "true" }),
|
|
1948
|
+
quiet: () => saveConfig({ toasts: { quiet: value === "on" || value === "1" || value === "true" } }),
|
|
1949
|
+
strategy: () => {
|
|
1950
|
+
const valid = ["sticky", "round-robin", "hybrid"];
|
|
1951
|
+
if (valid.includes(value)) saveConfig({ account_selection_strategy: value });
|
|
1952
|
+
else throw new Error(`Invalid strategy. Valid: ${valid.join(", ")}`);
|
|
1953
|
+
},
|
|
1954
|
+
};
|
|
1955
|
+
|
|
1956
|
+
if (!key || !setters[key]) {
|
|
1957
|
+
const keys = Object.keys(setters).join(", ");
|
|
1958
|
+
await sendCommandMessage(
|
|
1959
|
+
input.sessionID,
|
|
1960
|
+
`▣ Anthropic Set\n\nUsage: /anthropic set <key> <value>\nKeys: ${keys}\nValues: on/off (or specific values for strategy/compaction)`,
|
|
1961
|
+
);
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
if (!value) {
|
|
1965
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Set\n\nMissing value for "${key}".`);
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
setters[key]();
|
|
1969
|
+
// Reload config into runtime
|
|
1970
|
+
Object.assign(config, loadConfigFresh());
|
|
1971
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Set\n\n${key} = ${value}`);
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// /anthropic betas [add|remove <beta>] — show/manage custom betas
|
|
1976
|
+
if (primary === "betas") {
|
|
1977
|
+
const action = (args[1] || "").toLowerCase();
|
|
1978
|
+
|
|
1979
|
+
if (!action || action === "list") {
|
|
1980
|
+
const fresh = loadConfigFresh();
|
|
1981
|
+
const lines = [
|
|
1982
|
+
"▣ Anthropic Betas",
|
|
1983
|
+
"",
|
|
1984
|
+
"Preset betas (auto-computed per model/provider):",
|
|
1985
|
+
" oauth-2025-04-20, claude-code-20250219,",
|
|
1986
|
+
" interleaved-thinking-2025-05-14 OR adaptive-thinking-2026-01-28,",
|
|
1987
|
+
" fine-grained-tool-streaming-2025-05-14,",
|
|
1988
|
+
" code-execution-2025-08-25 (non-Haiku),",
|
|
1989
|
+
" files-api-2025-04-14",
|
|
1990
|
+
"",
|
|
1991
|
+
`Custom betas: ${fresh.custom_betas.length ? fresh.custom_betas.join(", ") : "(none)"}`,
|
|
1992
|
+
"",
|
|
1993
|
+
"Toggleable presets:",
|
|
1994
|
+
" /anthropic betas add structured-outputs-2025-12-15",
|
|
1995
|
+
" /anthropic betas add context-management-2025-06-27",
|
|
1996
|
+
" /anthropic betas add prompt-caching-scope-2026-01-05",
|
|
1997
|
+
" /anthropic betas add tool-examples-2025-10-29",
|
|
1998
|
+
" /anthropic betas add web-search-2025-03-05",
|
|
1999
|
+
" /anthropic betas add compact-2026-01-12",
|
|
2000
|
+
" /anthropic betas add mcp-servers-2025-12-04",
|
|
2001
|
+
"",
|
|
2002
|
+
"Remove: /anthropic betas remove <beta>",
|
|
2003
|
+
];
|
|
2004
|
+
await sendCommandMessage(input.sessionID, lines.join("\n"));
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
if (action === "add") {
|
|
2009
|
+
const beta = args[2]?.trim();
|
|
2010
|
+
if (!beta) {
|
|
2011
|
+
await sendCommandMessage(input.sessionID, "▣ Anthropic Betas\n\nUsage: /anthropic betas add <beta-name>");
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
const fresh = loadConfigFresh();
|
|
2015
|
+
const current = fresh.custom_betas || [];
|
|
2016
|
+
if (current.includes(beta)) {
|
|
2017
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Betas\n\n"${beta}" already added.`);
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
saveConfig({ custom_betas: [...current, beta] });
|
|
2021
|
+
Object.assign(config, loadConfigFresh());
|
|
2022
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Betas\n\nAdded: ${beta}`);
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
if (action === "remove" || action === "rm") {
|
|
2027
|
+
const beta = args[2]?.trim();
|
|
2028
|
+
if (!beta) {
|
|
2029
|
+
await sendCommandMessage(input.sessionID, "▣ Anthropic Betas\n\nUsage: /anthropic betas remove <beta-name>");
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
const fresh = loadConfigFresh();
|
|
2033
|
+
const current = fresh.custom_betas || [];
|
|
2034
|
+
if (!current.includes(beta)) {
|
|
2035
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Betas\n\n"${beta}" not in custom betas.`);
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
saveConfig({ custom_betas: current.filter((b) => b !== beta) });
|
|
2039
|
+
Object.assign(config, loadConfigFresh());
|
|
2040
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Betas\n\nRemoved: ${beta}`);
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
await sendCommandMessage(input.sessionID, "▣ Anthropic Betas\n\nUsage: /anthropic betas [add|remove <beta>]");
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// /anthropic files [list|upload|get|delete|download] — Files API management
|
|
2049
|
+
if (primary === "files") {
|
|
2050
|
+
const action = (args[1] || "").toLowerCase();
|
|
2051
|
+
|
|
2052
|
+
// Get current account and ensure valid token
|
|
2053
|
+
const account = accountManager?.getCurrentAccount();
|
|
2054
|
+
if (!account) {
|
|
2055
|
+
await sendCommandMessage(
|
|
2056
|
+
input.sessionID,
|
|
2057
|
+
"▣ Anthropic Files (error)\n\nNo accounts configured. Use /anthropic login first.",
|
|
2058
|
+
);
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
let token = account.access;
|
|
2063
|
+
if (!token || !account.expires || account.expires < Date.now()) {
|
|
2064
|
+
try {
|
|
2065
|
+
token = await refreshAccountTokenSingleFlight(account);
|
|
2066
|
+
} catch (err) {
|
|
2067
|
+
await sendCommandMessage(
|
|
2068
|
+
input.sessionID,
|
|
2069
|
+
`▣ Anthropic Files (error)\n\nToken refresh failed: ${err.message}`,
|
|
2070
|
+
);
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
const apiBase = "https://api.anthropic.com";
|
|
2076
|
+
const authHeaders = {
|
|
2077
|
+
authorization: `Bearer ${token}`,
|
|
2078
|
+
"anthropic-beta": "files-api-2025-04-14",
|
|
2079
|
+
};
|
|
2080
|
+
|
|
2081
|
+
try {
|
|
2082
|
+
// /anthropic files list — list all uploaded files
|
|
2083
|
+
if (!action || action === "list") {
|
|
2084
|
+
const res = await fetch(`${apiBase}/v1/files`, { headers: authHeaders });
|
|
2085
|
+
if (!res.ok) {
|
|
2086
|
+
const errBody = await res.text();
|
|
2087
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Files (error)\n\nHTTP ${res.status}: ${errBody}`);
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
const data = await res.json();
|
|
2091
|
+
const files = data.data || [];
|
|
2092
|
+
if (files.length === 0) {
|
|
2093
|
+
await sendCommandMessage(input.sessionID, "▣ Anthropic Files\n\nNo files uploaded.");
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
const lines = ["▣ Anthropic Files", "", `${files.length} file(s):`, ""];
|
|
2097
|
+
for (const f of files) {
|
|
2098
|
+
const sizeKB = (f.size / 1024).toFixed(1);
|
|
2099
|
+
lines.push(` ${f.id} ${f.filename} (${sizeKB} KB, ${f.purpose})`);
|
|
2100
|
+
}
|
|
2101
|
+
await sendCommandMessage(input.sessionID, lines.join("\n"));
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// /anthropic files upload <path> — upload a file
|
|
2106
|
+
if (action === "upload") {
|
|
2107
|
+
const filePath = args.slice(2).join(" ").trim();
|
|
2108
|
+
if (!filePath) {
|
|
2109
|
+
await sendCommandMessage(input.sessionID, "▣ Anthropic Files\n\nUsage: /anthropic files upload <path>");
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
const resolvedPath = resolve(filePath);
|
|
2113
|
+
if (!existsSync(resolvedPath)) {
|
|
2114
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Files (error)\n\nFile not found: ${resolvedPath}`);
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
const content = readFileSync(resolvedPath);
|
|
2118
|
+
const filename = basename(resolvedPath);
|
|
2119
|
+
const blob = new Blob([content]);
|
|
2120
|
+
const form = new FormData();
|
|
2121
|
+
form.append("file", blob, filename);
|
|
2122
|
+
form.append("purpose", "assistants");
|
|
2123
|
+
|
|
2124
|
+
const res = await fetch(`${apiBase}/v1/files`, {
|
|
2125
|
+
method: "POST",
|
|
2126
|
+
headers: {
|
|
2127
|
+
authorization: `Bearer ${token}`,
|
|
2128
|
+
"anthropic-beta": "files-api-2025-04-14",
|
|
2129
|
+
},
|
|
2130
|
+
body: form,
|
|
2131
|
+
});
|
|
2132
|
+
if (!res.ok) {
|
|
2133
|
+
const errBody = await res.text();
|
|
2134
|
+
await sendCommandMessage(
|
|
2135
|
+
input.sessionID,
|
|
2136
|
+
`▣ Anthropic Files (error)\n\nUpload failed (HTTP ${res.status}): ${errBody}`,
|
|
2137
|
+
);
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
const file = await res.json();
|
|
2141
|
+
const sizeKB = ((file.size || 0) / 1024).toFixed(1);
|
|
2142
|
+
await sendCommandMessage(
|
|
2143
|
+
input.sessionID,
|
|
2144
|
+
`▣ Anthropic Files\n\nUploaded: ${file.id}\n Filename: ${file.filename}\n Size: ${sizeKB} KB`,
|
|
2145
|
+
);
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
// /anthropic files get <file_id> — get file metadata
|
|
2150
|
+
if (action === "get" || action === "info") {
|
|
2151
|
+
const fileId = args[2]?.trim();
|
|
2152
|
+
if (!fileId) {
|
|
2153
|
+
await sendCommandMessage(input.sessionID, "▣ Anthropic Files\n\nUsage: /anthropic files get <file_id>");
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
const res = await fetch(`${apiBase}/v1/files/${encodeURIComponent(fileId)}`, { headers: authHeaders });
|
|
2157
|
+
if (!res.ok) {
|
|
2158
|
+
const errBody = await res.text();
|
|
2159
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Files (error)\n\nHTTP ${res.status}: ${errBody}`);
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
const file = await res.json();
|
|
2163
|
+
const lines = [
|
|
2164
|
+
"▣ Anthropic Files",
|
|
2165
|
+
"",
|
|
2166
|
+
` ID: ${file.id}`,
|
|
2167
|
+
` Filename: ${file.filename}`,
|
|
2168
|
+
` Purpose: ${file.purpose}`,
|
|
2169
|
+
` Size: ${((file.size || 0) / 1024).toFixed(1)} KB`,
|
|
2170
|
+
` Type: ${file.mime_type || "unknown"}`,
|
|
2171
|
+
` Created: ${file.created_at || "unknown"}`,
|
|
2172
|
+
];
|
|
2173
|
+
await sendCommandMessage(input.sessionID, lines.join("\n"));
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// /anthropic files delete <file_id> — delete a file
|
|
2178
|
+
if (action === "delete" || action === "rm") {
|
|
2179
|
+
const fileId = args[2]?.trim();
|
|
2180
|
+
if (!fileId) {
|
|
2181
|
+
await sendCommandMessage(input.sessionID, "▣ Anthropic Files\n\nUsage: /anthropic files delete <file_id>");
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
const res = await fetch(`${apiBase}/v1/files/${encodeURIComponent(fileId)}`, {
|
|
2185
|
+
method: "DELETE",
|
|
2186
|
+
headers: authHeaders,
|
|
2187
|
+
});
|
|
2188
|
+
if (!res.ok) {
|
|
2189
|
+
const errBody = await res.text();
|
|
2190
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Files (error)\n\nHTTP ${res.status}: ${errBody}`);
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Files\n\nDeleted: ${fileId}`);
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
// /anthropic files download <file_id> [output_path] — download file content
|
|
2198
|
+
if (action === "download" || action === "dl") {
|
|
2199
|
+
const fileId = args[2]?.trim();
|
|
2200
|
+
if (!fileId) {
|
|
2201
|
+
await sendCommandMessage(
|
|
2202
|
+
input.sessionID,
|
|
2203
|
+
"▣ Anthropic Files\n\nUsage: /anthropic files download <file_id> [output_path]",
|
|
2204
|
+
);
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
const outputPath = args.slice(3).join(" ").trim();
|
|
2208
|
+
|
|
2209
|
+
// Get file metadata first for the filename
|
|
2210
|
+
const metaRes = await fetch(`${apiBase}/v1/files/${encodeURIComponent(fileId)}`, {
|
|
2211
|
+
headers: authHeaders,
|
|
2212
|
+
});
|
|
2213
|
+
if (!metaRes.ok) {
|
|
2214
|
+
const errBody = await metaRes.text();
|
|
2215
|
+
await sendCommandMessage(
|
|
2216
|
+
input.sessionID,
|
|
2217
|
+
`▣ Anthropic Files (error)\n\nHTTP ${metaRes.status}: ${errBody}`,
|
|
2218
|
+
);
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
const meta = await metaRes.json();
|
|
2222
|
+
const savePath = outputPath ? resolve(outputPath) : resolve(meta.filename);
|
|
2223
|
+
|
|
2224
|
+
// Download file content
|
|
2225
|
+
const res = await fetch(`${apiBase}/v1/files/${encodeURIComponent(fileId)}/content`, {
|
|
2226
|
+
headers: authHeaders,
|
|
2227
|
+
});
|
|
2228
|
+
if (!res.ok) {
|
|
2229
|
+
const errBody = await res.text();
|
|
2230
|
+
await sendCommandMessage(
|
|
2231
|
+
input.sessionID,
|
|
2232
|
+
`▣ Anthropic Files (error)\n\nDownload failed (HTTP ${res.status}): ${errBody}`,
|
|
2233
|
+
);
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
2237
|
+
writeFileSync(savePath, buffer);
|
|
2238
|
+
const sizeKB = (buffer.length / 1024).toFixed(1);
|
|
2239
|
+
await sendCommandMessage(
|
|
2240
|
+
input.sessionID,
|
|
2241
|
+
`▣ Anthropic Files\n\nDownloaded: ${meta.filename}\n Saved to: ${savePath}\n Size: ${sizeKB} KB`,
|
|
2242
|
+
);
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// Unknown action — show help
|
|
2247
|
+
const helpLines = [
|
|
2248
|
+
"▣ Anthropic Files",
|
|
2249
|
+
"",
|
|
2250
|
+
"Usage: /anthropic files <action>",
|
|
2251
|
+
"",
|
|
2252
|
+
"Actions:",
|
|
2253
|
+
" list List uploaded files",
|
|
2254
|
+
" upload <path> Upload a file (max 350MB)",
|
|
2255
|
+
" get <file_id> Get file metadata",
|
|
2256
|
+
" delete <file_id> Delete a file",
|
|
2257
|
+
" download <file_id> [path] Download file content",
|
|
2258
|
+
"",
|
|
2259
|
+
"Supported formats: PDF, DOCX, TXT, CSV, Excel, Markdown, images",
|
|
2260
|
+
"Files can be referenced by file_id in Messages API requests.",
|
|
2261
|
+
];
|
|
2262
|
+
await sendCommandMessage(input.sessionID, helpLines.join("\n"));
|
|
2263
|
+
return;
|
|
2264
|
+
} catch (err) {
|
|
2265
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Files (error)\n\n${err.message}`);
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
1903
2270
|
// Interactive CLI command is not compatible with slash flow.
|
|
1904
2271
|
if (primary === "manage" || primary === "mg") {
|
|
1905
2272
|
await sendCommandMessage(
|
|
@@ -2160,7 +2527,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2160
2527
|
input.command ??= {};
|
|
2161
2528
|
input.command["anthropic"] = {
|
|
2162
2529
|
template: "/anthropic",
|
|
2163
|
-
description: "Manage Anthropic
|
|
2530
|
+
description: "Manage Anthropic auth, config, and betas (usage, login, config, set, betas, switch)",
|
|
2164
2531
|
};
|
|
2165
2532
|
},
|
|
2166
2533
|
"command.execute.before": async (input) => {
|
|
@@ -2388,6 +2755,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2388
2755
|
const requestHeaders = buildRequestHeaders(input, requestInit, accessToken, body, requestUrl, {
|
|
2389
2756
|
enabled: signatureEmulationEnabled,
|
|
2390
2757
|
claudeCliVersion,
|
|
2758
|
+
customBetas: config.custom_betas,
|
|
2391
2759
|
});
|
|
2392
2760
|
// Execute the request
|
|
2393
2761
|
let response;
|
package/lib/config.mjs
CHANGED
|
@@ -51,6 +51,7 @@ import { homedir } from "node:os";
|
|
|
51
51
|
* @property {boolean} debug
|
|
52
52
|
* @property {{ enabled: boolean, fetch_claude_code_version_on_startup: boolean, prompt_compaction: 'minimal' | 'off' }} signature_emulation
|
|
53
53
|
* @property {OverrideModelLimitsConfig} override_model_limits
|
|
54
|
+
* @property {string[]} custom_betas
|
|
54
55
|
* @property {HealthScoreConfig} health_score
|
|
55
56
|
* @property {TokenBucketConfig} token_bucket
|
|
56
57
|
* @property {ToastConfig} toasts
|
|
@@ -60,7 +61,7 @@ import { homedir } from "node:os";
|
|
|
60
61
|
|
|
61
62
|
/** @type {AnthropicAuthConfig} */
|
|
62
63
|
export const DEFAULT_CONFIG = {
|
|
63
|
-
account_selection_strategy: "
|
|
64
|
+
account_selection_strategy: "round-robin",
|
|
64
65
|
failure_ttl_seconds: 3600,
|
|
65
66
|
debug: false,
|
|
66
67
|
signature_emulation: {
|
|
@@ -69,10 +70,11 @@ export const DEFAULT_CONFIG = {
|
|
|
69
70
|
prompt_compaction: "minimal",
|
|
70
71
|
},
|
|
71
72
|
override_model_limits: {
|
|
72
|
-
enabled:
|
|
73
|
+
enabled: false,
|
|
73
74
|
context: 1_000_000,
|
|
74
75
|
output: 0,
|
|
75
76
|
},
|
|
77
|
+
custom_betas: [],
|
|
76
78
|
health_score: {
|
|
77
79
|
initial: 70,
|
|
78
80
|
success_reward: 1,
|
|
@@ -112,6 +114,7 @@ function createDefaultConfig() {
|
|
|
112
114
|
...DEFAULT_CONFIG,
|
|
113
115
|
signature_emulation: { ...DEFAULT_CONFIG.signature_emulation },
|
|
114
116
|
override_model_limits: { ...DEFAULT_CONFIG.override_model_limits },
|
|
117
|
+
custom_betas: [...DEFAULT_CONFIG.custom_betas],
|
|
115
118
|
health_score: { ...DEFAULT_CONFIG.health_score },
|
|
116
119
|
token_bucket: { ...DEFAULT_CONFIG.token_bucket },
|
|
117
120
|
toasts: { ...DEFAULT_CONFIG.toasts },
|
|
@@ -197,6 +200,11 @@ function validateConfig(raw) {
|
|
|
197
200
|
};
|
|
198
201
|
}
|
|
199
202
|
|
|
203
|
+
// Custom betas
|
|
204
|
+
if (Array.isArray(raw.custom_betas)) {
|
|
205
|
+
config.custom_betas = raw.custom_betas.filter((b) => typeof b === "string" && b.trim()).map((b) => b.trim());
|
|
206
|
+
}
|
|
207
|
+
|
|
200
208
|
// Health score sub-config
|
|
201
209
|
if (raw.health_score && typeof raw.health_score === "object") {
|
|
202
210
|
const hs = /** @type {Record<string, unknown>} */ (raw.health_score);
|
|
@@ -408,19 +416,56 @@ export function loadRawConfig() {
|
|
|
408
416
|
* Uses atomic write (temp + rename) for safety.
|
|
409
417
|
* @param {Record<string, unknown>} updates - Keys to merge into the config file
|
|
410
418
|
*/
|
|
419
|
+
/**
|
|
420
|
+
* Deep merge source into target (one level deep for objects, replace for arrays/primitives).
|
|
421
|
+
* @param {Record<string, unknown>} target
|
|
422
|
+
* @param {Record<string, unknown>} source
|
|
423
|
+
* @returns {Record<string, unknown>}
|
|
424
|
+
*/
|
|
425
|
+
function deepMergeConfig(target, source) {
|
|
426
|
+
const result = { ...target };
|
|
427
|
+
for (const [key, value] of Object.entries(source)) {
|
|
428
|
+
if (
|
|
429
|
+
value &&
|
|
430
|
+
typeof value === "object" &&
|
|
431
|
+
!Array.isArray(value) &&
|
|
432
|
+
typeof result[key] === "object" &&
|
|
433
|
+
result[key] &&
|
|
434
|
+
!Array.isArray(result[key])
|
|
435
|
+
) {
|
|
436
|
+
result[key] = {
|
|
437
|
+
.../** @type {Record<string, unknown>} */ (result[key]),
|
|
438
|
+
.../** @type {Record<string, unknown>} */ (value),
|
|
439
|
+
};
|
|
440
|
+
} else {
|
|
441
|
+
result[key] = value;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
|
|
411
447
|
export function saveConfig(updates) {
|
|
412
448
|
const configPath = getConfigPath();
|
|
413
449
|
const dir = dirname(configPath);
|
|
414
450
|
mkdirSync(dir, { recursive: true });
|
|
415
451
|
|
|
416
|
-
//
|
|
452
|
+
// Re-read from disk to avoid clobbering concurrent writes
|
|
417
453
|
const existing = loadRawConfig();
|
|
418
454
|
|
|
419
|
-
//
|
|
420
|
-
const merged =
|
|
455
|
+
// Deep merge (one level) so sub-config objects are merged, not replaced
|
|
456
|
+
const merged = deepMergeConfig(existing, updates);
|
|
421
457
|
|
|
422
458
|
// Atomic write: temp file + rename
|
|
423
459
|
const tmpPath = configPath + `.tmp.${process.pid}`;
|
|
424
460
|
writeFileSync(tmpPath, JSON.stringify(merged, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
425
461
|
renameSync(tmpPath, configPath);
|
|
426
462
|
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Load config fresh from disk (bypassing any startup cache).
|
|
466
|
+
* Useful for slash commands that need current on-disk state.
|
|
467
|
+
* @returns {AnthropicAuthConfig}
|
|
468
|
+
*/
|
|
469
|
+
export function loadConfigFresh() {
|
|
470
|
+
return loadConfig();
|
|
471
|
+
}
|
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("round-robin");
|
|
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("round-robin");
|
|
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("round-robin");
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
it("enables debug from OPENCODE_ANTHROPIC_DEBUG=1", () => {
|