opencode-anthropic-fix 0.0.19 → 0.0.20
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 +64 -15
- package/index.mjs +144 -4
- 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,9 @@ 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
|
|
48
49
|
|
|
49
50
|
## Installation
|
|
50
51
|
|
|
@@ -231,20 +232,65 @@ Most commands have short aliases: `ln`, `lo`, `ra`, `rf`, `ls`, `st`, `sw`, `en`
|
|
|
231
232
|
|
|
232
233
|
## Slash Commands in OpenCode
|
|
233
234
|
|
|
234
|
-
The plugin
|
|
235
|
+
The plugin registers a built-in `/anthropic` slash command for account management, feature toggles, and custom beta headers — all without leaving OpenCode.
|
|
235
236
|
|
|
236
|
-
###
|
|
237
|
+
### Account management
|
|
237
238
|
|
|
238
239
|
```text
|
|
239
|
-
/anthropic # list (default)
|
|
240
|
+
/anthropic # list accounts (default)
|
|
240
241
|
/anthropic usage # full account list + quota windows
|
|
241
242
|
/anthropic switch 2
|
|
242
243
|
/anthropic refresh 1
|
|
243
244
|
/anthropic logout 2 # revoke tokens and remove account 2
|
|
244
245
|
/anthropic logout --all # revoke all tokens and clear all accounts
|
|
245
|
-
/anthropic strategy hybrid
|
|
246
246
|
/anthropic stats
|
|
247
|
-
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Runtime configuration
|
|
250
|
+
|
|
251
|
+
Toggle features live without restarting OpenCode. Changes are persisted to `anthropic-auth.json`.
|
|
252
|
+
|
|
253
|
+
```text
|
|
254
|
+
/anthropic config # show all current settings
|
|
255
|
+
/anthropic set emulation on # enable/disable Claude signature emulation
|
|
256
|
+
/anthropic set compaction minimal # set prompt compaction mode (minimal/off)
|
|
257
|
+
/anthropic set 1m-context on # enable/disable 1M context limit override
|
|
258
|
+
/anthropic set idle-refresh on # enable/disable idle account refresh
|
|
259
|
+
/anthropic set strategy round-robin # change account selection strategy
|
|
260
|
+
/anthropic set debug on # enable/disable debug logging
|
|
261
|
+
/anthropic set quiet on # suppress non-error toasts
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Custom beta headers
|
|
265
|
+
|
|
266
|
+
Add or remove beta flags that get included in every `anthropic-beta` header. Persisted across sessions.
|
|
267
|
+
|
|
268
|
+
```text
|
|
269
|
+
/anthropic betas # show active betas (auto + custom) with presets
|
|
270
|
+
/anthropic betas add <beta-name> # add a custom beta
|
|
271
|
+
/anthropic betas remove <beta-name> # remove a custom beta
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Available preset betas** (shown by `/anthropic betas`):
|
|
275
|
+
|
|
276
|
+
| Beta | Description |
|
|
277
|
+
| --------------------------------- | ------------------------------------------------- |
|
|
278
|
+
| `prompt-caching-scope-2026-01-05` | Cache control with `scope: "org"` — free perf win |
|
|
279
|
+
| `context-management-2025-06-27` | Server-side auto-summarization when context fills |
|
|
280
|
+
| `structured-outputs-2025-12-15` | Strict JSON schema output enforcement |
|
|
281
|
+
| `tool-examples-2025-10-29` | Input/output examples in tool definitions |
|
|
282
|
+
| `code-execution-2025-08-25` | Python sandbox execution by the model |
|
|
283
|
+
| `files-api-2025-04-14` | Upload once, reuse file ID across requests |
|
|
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
|
|
248
294
|
```
|
|
249
295
|
|
|
250
296
|
### OAuth flows from slash command
|
|
@@ -272,11 +318,11 @@ Pending slash OAuth flows expire after 10 minutes. If completion fails with an e
|
|
|
272
318
|
|
|
273
319
|
Control how the plugin picks which account to use for each request.
|
|
274
320
|
|
|
275
|
-
| Strategy
|
|
276
|
-
|
|
|
277
|
-
| **`
|
|
278
|
-
| **`
|
|
279
|
-
| **`hybrid`**
|
|
321
|
+
| Strategy | Behavior | Best For |
|
|
322
|
+
| --------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------- |
|
|
323
|
+
| **`round-robin`** (default) | Rotate through accounts on every request | Spreading load evenly across accounts |
|
|
324
|
+
| **`sticky`** | Stay on one account until it fails or is rate-limited | Single account, or when you want predictable behavior |
|
|
325
|
+
| **`hybrid`** | Score-based selection with stickiness bias. Considers health, token budget, and freshness | Multiple accounts with varying rate limits |
|
|
280
326
|
|
|
281
327
|
### Change Strategy
|
|
282
328
|
|
|
@@ -297,8 +343,8 @@ Configuration is stored at `~/.config/opencode/anthropic-auth.json`. All setting
|
|
|
297
343
|
|
|
298
344
|
```jsonc
|
|
299
345
|
{
|
|
300
|
-
// Account selection strategy: "
|
|
301
|
-
"account_selection_strategy": "
|
|
346
|
+
// Account selection strategy: "round-robin" | "sticky" | "hybrid"
|
|
347
|
+
"account_selection_strategy": "round-robin",
|
|
302
348
|
|
|
303
349
|
// Seconds before consecutive failure count resets (60-7200)
|
|
304
350
|
"failure_ttl_seconds": 3600,
|
|
@@ -325,8 +371,8 @@ Configuration is stored at `~/.config/opencode/anthropic-auth.json`. All setting
|
|
|
325
371
|
// Only applied for OAuth (Max Plan) sessions — API key users use the
|
|
326
372
|
// context-1m-2025-08-07 beta header instead.
|
|
327
373
|
"override_model_limits": {
|
|
328
|
-
// Enable/disable the override
|
|
329
|
-
"enabled":
|
|
374
|
+
// Enable/disable the override (default: off — enable if you need 1M context)
|
|
375
|
+
"enabled": false,
|
|
330
376
|
// Context window to inject (tokens). Default: 1_000_000.
|
|
331
377
|
"context": 1000000,
|
|
332
378
|
// Max output tokens to inject. 0 = leave the model's default unchanged.
|
|
@@ -351,6 +397,9 @@ Configuration is stored at `~/.config/opencode/anthropic-auth.json`. All setting
|
|
|
351
397
|
"initial_tokens": 50,
|
|
352
398
|
},
|
|
353
399
|
|
|
400
|
+
// Custom beta headers (added to every request via /anthropic betas add)
|
|
401
|
+
"custom_betas": [],
|
|
402
|
+
|
|
354
403
|
// Toast notification settings
|
|
355
404
|
"toasts": {
|
|
356
405
|
// Suppress non-error toasts (account status, switching)
|
package/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
5
5
|
import { join } 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";
|
|
@@ -659,9 +659,10 @@ function buildSystemPromptBlocks(system, signature, messages) {
|
|
|
659
659
|
* @param {boolean} signatureEnabled
|
|
660
660
|
* @param {string} model
|
|
661
661
|
* @param {"anthropic" | "bedrock" | "vertex" | "foundry"} provider
|
|
662
|
+
* @param {string[]} [customBetas]
|
|
662
663
|
* @returns {string}
|
|
663
664
|
*/
|
|
664
|
-
function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provider) {
|
|
665
|
+
function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provider, customBetas) {
|
|
665
666
|
const incomingBetasList = incomingBeta
|
|
666
667
|
.split(",")
|
|
667
668
|
.map((b) => b.trim())
|
|
@@ -729,6 +730,10 @@ function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provide
|
|
|
729
730
|
|
|
730
731
|
betas.push("fine-grained-tool-streaming-2025-05-14");
|
|
731
732
|
|
|
733
|
+
if (Array.isArray(customBetas)) {
|
|
734
|
+
betas.push(...customBetas.filter(Boolean));
|
|
735
|
+
}
|
|
736
|
+
|
|
732
737
|
const mergedBetas = [...new Set([...betas, ...incomingBetasList])];
|
|
733
738
|
if (provider === "bedrock") {
|
|
734
739
|
return mergedBetas.filter((beta) => !BEDROCK_UNSUPPORTED_BETAS.has(beta)).join(",");
|
|
@@ -887,7 +892,7 @@ function buildRequestHeaders(input, requestInit, accessToken, requestBody, reque
|
|
|
887
892
|
const incomingBeta = requestHeaders.get("anthropic-beta") || "";
|
|
888
893
|
const { model, tools, messages } = parseRequestBodyMetadata(requestBody);
|
|
889
894
|
const provider = detectProvider(requestUrl);
|
|
890
|
-
const mergedBetas = buildAnthropicBetaHeader(incomingBeta, signature.enabled, model, provider);
|
|
895
|
+
const mergedBetas = buildAnthropicBetaHeader(incomingBeta, signature.enabled, model, provider, signature.customBetas);
|
|
891
896
|
|
|
892
897
|
const authTokenOverride = process.env.ANTHROPIC_AUTH_TOKEN?.trim();
|
|
893
898
|
const bearerToken = authTokenOverride || accessToken;
|
|
@@ -1900,6 +1905,140 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
1900
1905
|
return;
|
|
1901
1906
|
}
|
|
1902
1907
|
|
|
1908
|
+
// /anthropic config — show effective config
|
|
1909
|
+
if (primary === "config") {
|
|
1910
|
+
const fresh = loadConfigFresh();
|
|
1911
|
+
const lines = [
|
|
1912
|
+
"▣ Anthropic Config",
|
|
1913
|
+
"",
|
|
1914
|
+
`strategy: ${fresh.account_selection_strategy}`,
|
|
1915
|
+
`emulation: ${fresh.signature_emulation.enabled ? "on" : "off"}`,
|
|
1916
|
+
`compaction: ${fresh.signature_emulation.prompt_compaction}`,
|
|
1917
|
+
`1m-context: ${fresh.override_model_limits.enabled ? "on" : "off"}`,
|
|
1918
|
+
`idle-refresh: ${fresh.idle_refresh.enabled ? "on" : "off"}`,
|
|
1919
|
+
`debug: ${fresh.debug ? "on" : "off"}`,
|
|
1920
|
+
`quiet: ${fresh.toasts.quiet ? "on" : "off"}`,
|
|
1921
|
+
`custom_betas: ${fresh.custom_betas.length ? fresh.custom_betas.join(", ") : "(none)"}`,
|
|
1922
|
+
];
|
|
1923
|
+
await sendCommandMessage(input.sessionID, lines.join("\n"));
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// /anthropic set <key> <value> — toggle features at runtime
|
|
1928
|
+
if (primary === "set") {
|
|
1929
|
+
const key = (args[1] || "").toLowerCase();
|
|
1930
|
+
const value = (args[2] || "").toLowerCase();
|
|
1931
|
+
/** @type {Record<string, () => void>} */
|
|
1932
|
+
const setters = {
|
|
1933
|
+
emulation: () =>
|
|
1934
|
+
saveConfig({ signature_emulation: { enabled: value === "on" || value === "1" || value === "true" } }),
|
|
1935
|
+
compaction: () =>
|
|
1936
|
+
saveConfig({ signature_emulation: { prompt_compaction: value === "off" ? "off" : "minimal" } }),
|
|
1937
|
+
"1m-context": () =>
|
|
1938
|
+
saveConfig({ override_model_limits: { enabled: value === "on" || value === "1" || value === "true" } }),
|
|
1939
|
+
"idle-refresh": () =>
|
|
1940
|
+
saveConfig({ idle_refresh: { enabled: value === "on" || value === "1" || value === "true" } }),
|
|
1941
|
+
debug: () => saveConfig({ debug: value === "on" || value === "1" || value === "true" }),
|
|
1942
|
+
quiet: () => saveConfig({ toasts: { quiet: value === "on" || value === "1" || value === "true" } }),
|
|
1943
|
+
strategy: () => {
|
|
1944
|
+
const valid = ["sticky", "round-robin", "hybrid"];
|
|
1945
|
+
if (valid.includes(value)) saveConfig({ account_selection_strategy: value });
|
|
1946
|
+
else throw new Error(`Invalid strategy. Valid: ${valid.join(", ")}`);
|
|
1947
|
+
},
|
|
1948
|
+
};
|
|
1949
|
+
|
|
1950
|
+
if (!key || !setters[key]) {
|
|
1951
|
+
const keys = Object.keys(setters).join(", ");
|
|
1952
|
+
await sendCommandMessage(
|
|
1953
|
+
input.sessionID,
|
|
1954
|
+
`▣ Anthropic Set\n\nUsage: /anthropic set <key> <value>\nKeys: ${keys}\nValues: on/off (or specific values for strategy/compaction)`,
|
|
1955
|
+
);
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
if (!value) {
|
|
1959
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Set\n\nMissing value for "${key}".`);
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
setters[key]();
|
|
1963
|
+
// Reload config into runtime
|
|
1964
|
+
Object.assign(config, loadConfigFresh());
|
|
1965
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Set\n\n${key} = ${value}`);
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// /anthropic betas [add|remove <beta>] — show/manage custom betas
|
|
1970
|
+
if (primary === "betas") {
|
|
1971
|
+
const action = (args[1] || "").toLowerCase();
|
|
1972
|
+
|
|
1973
|
+
if (!action || action === "list") {
|
|
1974
|
+
const fresh = loadConfigFresh();
|
|
1975
|
+
const lines = [
|
|
1976
|
+
"▣ Anthropic Betas",
|
|
1977
|
+
"",
|
|
1978
|
+
"Preset betas (auto-computed per model/provider):",
|
|
1979
|
+
" oauth-2025-04-20, claude-code-20250219,",
|
|
1980
|
+
" interleaved-thinking-2025-05-14 OR adaptive-thinking-2026-01-28,",
|
|
1981
|
+
" fine-grained-tool-streaming-2025-05-14",
|
|
1982
|
+
"",
|
|
1983
|
+
`Custom betas: ${fresh.custom_betas.length ? fresh.custom_betas.join(", ") : "(none)"}`,
|
|
1984
|
+
"",
|
|
1985
|
+
"Toggleable presets:",
|
|
1986
|
+
" /anthropic betas add structured-outputs-2025-12-15",
|
|
1987
|
+
" /anthropic betas add context-management-2025-06-27",
|
|
1988
|
+
" /anthropic betas add prompt-caching-scope-2026-01-05",
|
|
1989
|
+
" /anthropic betas add tool-examples-2025-10-29",
|
|
1990
|
+
" /anthropic betas add web-search-2025-03-05",
|
|
1991
|
+
" /anthropic betas add code-execution-2025-08-25",
|
|
1992
|
+
" /anthropic betas add compact-2026-01-12",
|
|
1993
|
+
" /anthropic betas add mcp-servers-2025-12-04",
|
|
1994
|
+
" /anthropic betas add files-api-2025-04-14",
|
|
1995
|
+
"",
|
|
1996
|
+
"Remove: /anthropic betas remove <beta>",
|
|
1997
|
+
];
|
|
1998
|
+
await sendCommandMessage(input.sessionID, lines.join("\n"));
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
if (action === "add") {
|
|
2003
|
+
const beta = args[2]?.trim();
|
|
2004
|
+
if (!beta) {
|
|
2005
|
+
await sendCommandMessage(input.sessionID, "▣ Anthropic Betas\n\nUsage: /anthropic betas add <beta-name>");
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
const fresh = loadConfigFresh();
|
|
2009
|
+
const current = fresh.custom_betas || [];
|
|
2010
|
+
if (current.includes(beta)) {
|
|
2011
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Betas\n\n"${beta}" already added.`);
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
saveConfig({ custom_betas: [...current, beta] });
|
|
2015
|
+
Object.assign(config, loadConfigFresh());
|
|
2016
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Betas\n\nAdded: ${beta}`);
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
if (action === "remove" || action === "rm") {
|
|
2021
|
+
const beta = args[2]?.trim();
|
|
2022
|
+
if (!beta) {
|
|
2023
|
+
await sendCommandMessage(input.sessionID, "▣ Anthropic Betas\n\nUsage: /anthropic betas remove <beta-name>");
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
const fresh = loadConfigFresh();
|
|
2027
|
+
const current = fresh.custom_betas || [];
|
|
2028
|
+
if (!current.includes(beta)) {
|
|
2029
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Betas\n\n"${beta}" not in custom betas.`);
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
saveConfig({ custom_betas: current.filter((b) => b !== beta) });
|
|
2033
|
+
Object.assign(config, loadConfigFresh());
|
|
2034
|
+
await sendCommandMessage(input.sessionID, `▣ Anthropic Betas\n\nRemoved: ${beta}`);
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
await sendCommandMessage(input.sessionID, "▣ Anthropic Betas\n\nUsage: /anthropic betas [add|remove <beta>]");
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
1903
2042
|
// Interactive CLI command is not compatible with slash flow.
|
|
1904
2043
|
if (primary === "manage" || primary === "mg") {
|
|
1905
2044
|
await sendCommandMessage(
|
|
@@ -2160,7 +2299,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2160
2299
|
input.command ??= {};
|
|
2161
2300
|
input.command["anthropic"] = {
|
|
2162
2301
|
template: "/anthropic",
|
|
2163
|
-
description: "Manage Anthropic
|
|
2302
|
+
description: "Manage Anthropic auth, config, and betas (usage, login, config, set, betas, switch)",
|
|
2164
2303
|
};
|
|
2165
2304
|
},
|
|
2166
2305
|
"command.execute.before": async (input) => {
|
|
@@ -2388,6 +2527,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2388
2527
|
const requestHeaders = buildRequestHeaders(input, requestInit, accessToken, body, requestUrl, {
|
|
2389
2528
|
enabled: signatureEmulationEnabled,
|
|
2390
2529
|
claudeCliVersion,
|
|
2530
|
+
customBetas: config.custom_betas,
|
|
2391
2531
|
});
|
|
2392
2532
|
// Execute the request
|
|
2393
2533
|
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", () => {
|