opencode-anthropic-fix 0.0.22 → 0.0.23

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 CHANGED
@@ -43,11 +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
+ - **Effort-based thinking for Opus 4.6** — maps `budgetTokens` to effort levels (`low`/`medium`/`high`) and includes `effort-2025-11-24`
47
47
  - **1M context limit override** — patches `model.limit.context` so OpenCode compacts at the right threshold while `models.dev` catches up
48
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)
49
+ - **Files API integration** — upload, list, download, and manage files via `/anthropic files` with endpoint/content-scoped `files-api-2025-04-14` beta injection
50
+ - **Code execution support** — available via explicit custom beta opt-in (`code-execution-2025-08-25`), not auto-enabled
51
51
 
52
52
  ## Installation
53
53
 
@@ -271,6 +271,8 @@ Add or remove beta flags that get included in every `anthropic-beta` header. Per
271
271
  /anthropic betas # show active betas (auto + custom) with presets
272
272
  /anthropic betas add <beta-name> # add a custom beta
273
273
  /anthropic betas remove <beta-name> # remove a custom beta
274
+ /anthropic betas add 1m # shortcut => context-1m-2025-08-07
275
+ /anthropic betas add fast # shortcut => fast-mode-2026-02-01
274
276
  ```
275
277
 
276
278
  **Available preset betas** (shown by `/anthropic betas`):
@@ -284,6 +286,8 @@ Add or remove beta flags that get included in every `anthropic-beta` header. Per
284
286
  | `compact-2026-01-12` | Conversation compaction endpoint |
285
287
  | `mcp-servers-2025-12-04` | MCP servers in API request payload |
286
288
  | `web-search-2025-03-05` | Web search (Vertex/Foundry only) |
289
+ | `context-1m-2025-08-07` | 1M context beta (provider/model dependent) |
290
+ | `fast-mode-2026-02-01` | Faster Opus mode (experimental) |
287
291
 
288
292
  Example workflow:
289
293
 
@@ -295,7 +299,7 @@ Example workflow:
295
299
 
296
300
  ### Files API management
297
301
 
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.
302
+ Upload, list, download, and delete files via the Anthropic Files API. The `files-api-2025-04-14` beta is injected only when needed (`/v1/files` requests or Messages payloads with `file_id`).
299
303
 
300
304
  ```text
301
305
  /anthropic files # list files across ALL accounts
@@ -370,7 +374,7 @@ Some Anthropic API features maintain server-side per-account state that breaks w
370
374
  | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
371
375
  | **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
376
  | **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 |
377
+ | **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 | **Manual opt-in only**: add the beta explicitly, and prefer sticky/pinned sessions for multi-step flows |
374
378
  | **Message Batches** (`message-batches-2024-09-24`) | `batch_id` is per-account; polling from wrong account = 404 | No automatic mitigation (not auto-included) |
375
379
 
376
380
  **Recommendation for round-robin users:** If you need prompt caching or code execution, pin each OpenCode session to a single account:
@@ -383,7 +387,7 @@ OPENCODE_ANTHROPIC_INITIAL_ACCOUNT=1 opencode
383
387
  OPENCODE_ANTHROPIC_INITIAL_ACCOUNT=2 opencode
384
388
  ```
385
389
 
386
- This automatically overrides the strategy to `sticky` for that session, re-enabling all betas. Other sessions are unaffected.
390
+ This automatically overrides the strategy to `sticky` for that session, re-enabling strategy-sensitive auto betas (for example `prompt-caching-scope-2026-01-05`). Other sessions are unaffected.
387
391
 
388
392
  ## Configuration
389
393
 
@@ -471,6 +475,7 @@ Configuration is stored at `~/.config/opencode/anthropic-auth.json`. All setting
471
475
  | `OPENCODE_ANTHROPIC_DEBUG_SYSTEM_PROMPT` | Set to `1` to log the final transformed `system` prompt to stderr (title-generator requests are skipped). |
472
476
  | `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
477
  | `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). |
478
+ | `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` | Set to `1` to suppress experimental auto-betas (mirrors Claude Code gateway safety switch). |
474
479
 
475
480
  ### OAuth-only behavior
476
481
 
@@ -496,7 +501,7 @@ The plugin also:
496
501
  - Emulates Claude-style request headers and beta flags by default
497
502
  - Sanitizes "OpenCode" references to "Claude Code" in system prompts (required by Anthropic's API)
498
503
  - In `prompt_compaction="minimal"`, deduplicates repeated/contained system blocks and uses a compact dedicated prompt for internal title-generation requests
499
- - Adds `?beta=true` to `/v1/messages` requests
504
+ - Adds `?beta=true` to `/v1/messages` and `/v1/messages/count_tokens` requests
500
505
 
501
506
  When signature emulation is disabled (`signature_emulation.enabled=false`), the plugin falls back to legacy behavior including the Claude Code system prompt prefix.
502
507
 
package/index.mjs CHANGED
@@ -106,6 +106,8 @@ async function promptManageAccounts(accountManager) {
106
106
  const FALLBACK_CLAUDE_CLI_VERSION = "2.1.76";
107
107
  const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
108
108
  const CLAUDE_CODE_BETA_FLAG = "claude-code-20250219";
109
+ const EFFORT_BETA_FLAG = "effort-2025-11-24";
110
+ const TOKEN_COUNTING_BETA_FLAG = "token-counting-2024-11-01";
109
111
  const BILLING_HASH_SALT = "59cf53e54c78";
110
112
  const BILLING_HASH_POSITIONS = [4, 7, 20];
111
113
  const CLAUDE_CODE_IDENTITY_STRING = "You are Claude Code, Anthropic's official CLI for Claude.";
@@ -122,6 +124,32 @@ const BEDROCK_UNSUPPORTED_BETAS = new Set([
122
124
  "code-execution-2025-08-25",
123
125
  "files-api-2025-04-14",
124
126
  ]);
127
+ const EXPERIMENTAL_BETA_FLAGS = new Set([
128
+ "adaptive-thinking-2026-01-28",
129
+ "advanced-tool-use-2025-11-20",
130
+ "afk-mode-2026-01-31",
131
+ "code-execution-2025-08-25",
132
+ "context-1m-2025-08-07",
133
+ "context-management-2025-06-27",
134
+ "fast-mode-2026-02-01",
135
+ "files-api-2025-04-14",
136
+ "fine-grained-tool-streaming-2025-05-14",
137
+ "interleaved-thinking-2025-05-14",
138
+ "prompt-caching-scope-2026-01-05",
139
+ "redact-thinking-2026-02-12",
140
+ "structured-outputs-2025-12-15",
141
+ "tool-examples-2025-10-29",
142
+ "tool-search-tool-2025-10-19",
143
+ "web-search-2025-03-05",
144
+ ]);
145
+ const BETA_SHORTCUTS = new Map([
146
+ ["1m", "context-1m-2025-08-07"],
147
+ ["1m-context", "context-1m-2025-08-07"],
148
+ ["context-1m", "context-1m-2025-08-07"],
149
+ ["fast", "fast-mode-2026-02-01"],
150
+ ["fast-mode", "fast-mode-2026-02-01"],
151
+ ["opus-fast", "fast-mode-2026-02-01"],
152
+ ]);
125
153
  const STAINLESS_HELPER_KEYS = [
126
154
  "x_stainless_helper",
127
155
  "x-stainless-helper",
@@ -162,6 +190,17 @@ function isFalsyEnv(value) {
162
190
  return normalized === "0" || normalized === "false" || normalized === "no";
163
191
  }
164
192
 
193
+ /**
194
+ * @param {string | undefined} value
195
+ * @returns {string}
196
+ */
197
+ function resolveBetaShortcut(value) {
198
+ if (!value) return "";
199
+ const trimmed = value.trim();
200
+ const mapped = BETA_SHORTCUTS.get(trimmed.toLowerCase());
201
+ return mapped || trimmed;
202
+ }
203
+
165
204
  /**
166
205
  * @returns {boolean}
167
206
  */
@@ -341,11 +380,11 @@ function detectProvider(requestUrl) {
341
380
 
342
381
  /**
343
382
  * @param {string | undefined} body
344
- * @returns {{model: string, tools: any[], messages: any[]}}
383
+ * @returns {{model: string, tools: any[], messages: any[], hasFileReferences: boolean}}
345
384
  */
346
385
  function parseRequestBodyMetadata(body) {
347
386
  if (!body || typeof body !== "string") {
348
- return { model: "", tools: [], messages: [] };
387
+ return { model: "", tools: [], messages: [], hasFileReferences: false };
349
388
  }
350
389
 
351
390
  try {
@@ -353,9 +392,10 @@ function parseRequestBodyMetadata(body) {
353
392
  const model = typeof parsed?.model === "string" ? parsed.model : "";
354
393
  const tools = Array.isArray(parsed?.tools) ? parsed.tools : [];
355
394
  const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
356
- return { model, tools, messages };
395
+ const hasFileReferences = extractFileIds(parsed).length > 0;
396
+ return { model, tools, messages, hasFileReferences };
357
397
  } catch {
358
- return { model: "", tools: [], messages: [] };
398
+ return { model: "", tools: [], messages: [], hasFileReferences: false };
359
399
  }
360
400
  }
361
401
 
@@ -663,19 +703,40 @@ function buildSystemPromptBlocks(system, signature, messages) {
663
703
  * @param {"anthropic" | "bedrock" | "vertex" | "foundry"} provider
664
704
  * @param {string[]} [customBetas]
665
705
  * @param {import('./lib/config.mjs').AccountSelectionStrategy} [strategy]
706
+ * @param {string} [requestPath]
707
+ * @param {boolean} [hasFileReferences]
666
708
  * @returns {string}
667
709
  */
668
- function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provider, customBetas, strategy) {
710
+ function buildAnthropicBetaHeader(
711
+ incomingBeta,
712
+ signatureEnabled,
713
+ model,
714
+ provider,
715
+ customBetas,
716
+ strategy,
717
+ requestPath,
718
+ hasFileReferences,
719
+ ) {
669
720
  const incomingBetasList = incomingBeta
670
721
  .split(",")
671
722
  .map((b) => b.trim())
672
723
  .filter(Boolean);
673
724
 
674
725
  const betas = ["oauth-2025-04-20"];
726
+ const disableExperimentalBetas = isTruthyEnv(process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS);
727
+ const isMessagesCountTokensPath = requestPath === "/v1/messages/count_tokens";
728
+ const isFilesEndpoint = requestPath?.startsWith("/v1/files") ?? false;
675
729
 
676
730
  if (!signatureEnabled) {
677
731
  betas.push("interleaved-thinking-2025-05-14");
678
- return [...new Set([...betas, ...incomingBetasList])].join(",");
732
+ if (isMessagesCountTokensPath) {
733
+ betas.push(TOKEN_COUNTING_BETA_FLAG);
734
+ }
735
+ let mergedBetas = [...new Set([...betas, ...incomingBetasList])];
736
+ if (disableExperimentalBetas) {
737
+ mergedBetas = mergedBetas.filter((beta) => !EXPERIMENTAL_BETA_FLAGS.has(beta));
738
+ }
739
+ return mergedBetas.join(",");
679
740
  }
680
741
 
681
742
  const nonInteractive = isNonInteractiveMode();
@@ -684,72 +745,75 @@ function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provide
684
745
 
685
746
  if (!haiku) {
686
747
  betas.push(CLAUDE_CODE_BETA_FLAG);
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
- }
692
748
  }
693
749
 
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.
696
- betas.push("files-api-2025-04-14");
750
+ // Files API beta is endpoint/content-scoped instead of globally applied.
751
+ if ((isFilesEndpoint || hasFileReferences) && !disableExperimentalBetas) {
752
+ betas.push("files-api-2025-04-14");
753
+ }
697
754
 
698
755
  if (isOpus46Model(model)) {
699
- // Opus 4.6 uses adaptive thinking — model decides how much to think based on task complexity.
700
- // The effort parameter guides it; budgetTokens/interleaved-thinking are for older models.
701
- if (!isTruthyEnv(process.env.DISABLE_INTERLEAVED_THINKING)) {
702
- betas.push("adaptive-thinking-2026-01-28");
703
- }
704
- } else if (!isTruthyEnv(process.env.DISABLE_INTERLEAVED_THINKING) && supportsThinking(model)) {
756
+ // Opus 4.6 uses effort-based thinking controls.
757
+ betas.push(EFFORT_BETA_FLAG);
758
+ } else if (
759
+ !disableExperimentalBetas &&
760
+ !isTruthyEnv(process.env.DISABLE_INTERLEAVED_THINKING) &&
761
+ supportsThinking(model)
762
+ ) {
705
763
  betas.push("interleaved-thinking-2025-05-14");
706
764
  }
707
765
 
708
766
  // context-1m-2025-08-07 is only supported for API key users; OAuth provider does not support it.
709
767
  // For OAuth (this plugin's only auth mode), compaction is gated by model.limit.input instead.
710
- if (hasOneMillionContext(model) && provider !== "anthropic") {
768
+ if (!disableExperimentalBetas && hasOneMillionContext(model) && provider !== "anthropic") {
711
769
  betas.push("context-1m-2025-08-07");
712
770
  }
713
771
 
714
772
  if (
773
+ !disableExperimentalBetas &&
715
774
  nonInteractive &&
716
775
  (isTruthyEnv(process.env.USE_API_CONTEXT_MANAGEMENT) || isTruthyEnv(process.env.TENGU_MARBLE_ANVIL))
717
776
  ) {
718
777
  betas.push("context-management-2025-06-27");
719
778
  }
720
779
 
721
- if (supportsStructuredOutputs(model) && isTruthyEnv(process.env.TENGU_TOOL_PEAR)) {
780
+ if (!disableExperimentalBetas && supportsStructuredOutputs(model) && isTruthyEnv(process.env.TENGU_TOOL_PEAR)) {
722
781
  betas.push("structured-outputs-2025-12-15");
723
782
  }
724
783
 
725
- if (nonInteractive && isTruthyEnv(process.env.TENGU_SCARF_COFFEE)) {
784
+ if (!disableExperimentalBetas && nonInteractive && isTruthyEnv(process.env.TENGU_SCARF_COFFEE)) {
726
785
  betas.push("tool-examples-2025-10-29");
727
786
  }
728
787
 
729
- if ((provider === "vertex" || provider === "foundry") && supportsWebSearch(model)) {
788
+ if (!disableExperimentalBetas && (provider === "vertex" || provider === "foundry") && supportsWebSearch(model)) {
730
789
  betas.push("web-search-2025-03-05");
731
790
  }
732
791
 
733
792
  // Prompt caching is per-workspace (since Feb 2026); round-robin across accounts
734
793
  // means zero cache hits and doubled token costs. Skip in round-robin.
735
- if (nonInteractive && !isRoundRobin) {
794
+ if (!disableExperimentalBetas && nonInteractive && !isRoundRobin) {
736
795
  betas.push("prompt-caching-scope-2026-01-05");
737
796
  }
738
797
 
739
- if (process.env.ANTHROPIC_BETAS && !haiku) {
798
+ if (isMessagesCountTokensPath) {
799
+ betas.push(TOKEN_COUNTING_BETA_FLAG);
800
+ }
801
+
802
+ if (process.env.ANTHROPIC_BETAS) {
740
803
  const envBetas = process.env.ANTHROPIC_BETAS.split(",")
741
804
  .map((b) => b.trim())
742
805
  .filter(Boolean);
743
806
  betas.push(...envBetas);
744
807
  }
745
808
 
746
- betas.push("fine-grained-tool-streaming-2025-05-14");
747
-
748
809
  if (Array.isArray(customBetas)) {
749
810
  betas.push(...customBetas.filter(Boolean));
750
811
  }
751
812
 
752
- const mergedBetas = [...new Set([...betas, ...incomingBetasList])];
813
+ let mergedBetas = [...new Set([...betas, ...incomingBetasList])];
814
+ if (disableExperimentalBetas) {
815
+ mergedBetas = mergedBetas.filter((beta) => !EXPERIMENTAL_BETA_FLAGS.has(beta));
816
+ }
753
817
  if (provider === "bedrock") {
754
818
  return mergedBetas.filter((beta) => !BEDROCK_UNSUPPORTED_BETAS.has(beta)).join(",");
755
819
  }
@@ -759,7 +823,7 @@ function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provide
759
823
  /** @typedef {'low' | 'medium' | 'high'} ThinkingEffort */
760
824
 
761
825
  /**
762
- * Map budgetTokens to an adaptive thinking effort level.
826
+ * Map budgetTokens to an effort level.
763
827
  * Used when an Opus 4.6 request arrives with the legacy budgetTokens shape.
764
828
  * @param {number} budgetTokens
765
829
  * @returns {ThinkingEffort}
@@ -781,11 +845,11 @@ function isValidEffort(value) {
781
845
 
782
846
  /**
783
847
  * Normalise the `thinking` block in the request body for the target model:
784
- * - Opus 4.6 (adaptive thinking): produces `{ type: "enabled", effort: <effort> }`
848
+ * - Opus 4.6 (effort-based thinking): produces `{ type: "enabled", effort: <effort> }`
785
849
  * - Older models: passes the existing thinking block through unchanged.
786
850
  *
787
851
  * Handles three incoming shapes:
788
- * 1. Already adaptive: `{ type: "enabled", effort: "..." }` → kept as-is for Opus 4.6
852
+ * 1. Already effort-based: `{ type: "enabled", effort: "..." }` → kept as-is for Opus 4.6
789
853
  * 2. Legacy manual: `{ type: "enabled", budget_tokens: N }` → mapped to effort for Opus 4.6
790
854
  * 3. Absent / disabled: no transform
791
855
  *
@@ -873,7 +937,7 @@ async function fetchLatestClaudeCodeVersion(timeoutMs = 1200) {
873
937
  * @param {string} accessToken
874
938
  * @param {string | undefined} requestBody
875
939
  * @param {URL | null} requestUrl
876
- * @param {{enabled: boolean, claudeCliVersion: string, strategy?: import('./lib/config.mjs').AccountSelectionStrategy}} signature
940
+ * @param {{enabled: boolean, claudeCliVersion: string, strategy?: import('./lib/config.mjs').AccountSelectionStrategy, customBetas?: string[]}} signature
877
941
  * @returns {Headers}
878
942
  */
879
943
  function buildRequestHeaders(input, requestInit, accessToken, requestBody, requestUrl, signature) {
@@ -905,7 +969,7 @@ function buildRequestHeaders(input, requestInit, accessToken, requestBody, reque
905
969
 
906
970
  // Preserve all incoming beta headers while ensuring OAuth requirements
907
971
  const incomingBeta = requestHeaders.get("anthropic-beta") || "";
908
- const { model, tools, messages } = parseRequestBodyMetadata(requestBody);
972
+ const { model, tools, messages, hasFileReferences } = parseRequestBodyMetadata(requestBody);
909
973
  const provider = detectProvider(requestUrl);
910
974
  const mergedBetas = buildAnthropicBetaHeader(
911
975
  incomingBeta,
@@ -914,6 +978,8 @@ function buildRequestHeaders(input, requestInit, accessToken, requestBody, reque
914
978
  provider,
915
979
  signature.customBetas,
916
980
  signature.strategy,
981
+ requestUrl?.pathname,
982
+ hasFileReferences,
917
983
  );
918
984
 
919
985
  const authTokenOverride = process.env.ANTHROPIC_AUTH_TOKEN?.trim();
@@ -1040,7 +1106,7 @@ function transformRequestBody(body, signature, runtime) {
1040
1106
  }
1041
1107
 
1042
1108
  /**
1043
- * Transform the request URL: add ?beta=true to /v1/messages.
1109
+ * Transform the request URL: add ?beta=true to /v1/messages and /v1/messages/count_tokens.
1044
1110
  * Preserves behaviors F1-F3.
1045
1111
  *
1046
1112
  * @param {any} input
@@ -1059,7 +1125,11 @@ function transformRequestUrl(input) {
1059
1125
  requestUrl = null;
1060
1126
  }
1061
1127
 
1062
- if (requestUrl && requestUrl.pathname === "/v1/messages" && !requestUrl.searchParams.has("beta")) {
1128
+ if (
1129
+ requestUrl &&
1130
+ (requestUrl.pathname === "/v1/messages" || requestUrl.pathname === "/v1/messages/count_tokens") &&
1131
+ !requestUrl.searchParams.has("beta")
1132
+ ) {
1063
1133
  requestUrl.searchParams.set("beta", "true");
1064
1134
  requestInput = input instanceof Request ? new Request(requestUrl.toString(), input) : requestUrl;
1065
1135
  }
@@ -2039,12 +2109,12 @@ export async function AnthropicAuthPlugin({ client }) {
2039
2109
  "",
2040
2110
  "Preset betas (auto-computed per model/provider):",
2041
2111
  " oauth-2025-04-20, claude-code-20250219,",
2042
- " interleaved-thinking-2025-05-14 OR adaptive-thinking-2026-01-28,",
2043
- " fine-grained-tool-streaming-2025-05-14,",
2044
- ` code-execution-2025-08-25 (non-Haiku${strategy === "round-robin" ? ", skipped in round-robin" : ""}),`,
2045
- " files-api-2025-04-14,",
2112
+ " interleaved-thinking-2025-05-14 (non-Opus 4.6) OR effort-2025-11-24 (Opus 4.6),",
2113
+ " files-api-2025-04-14 (only /v1/files and requests with file_id),",
2114
+ " token-counting-2024-11-01 (only /v1/messages/count_tokens),",
2046
2115
  ` prompt-caching-scope-2026-01-05 (non-interactive${strategy === "round-robin" ? ", skipped in round-robin" : ""})`,
2047
2116
  "",
2117
+ `Experimental betas: ${isTruthyEnv(process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS) ? "disabled (CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1)" : "enabled"}`,
2048
2118
  `Strategy: ${strategy}${initialAccountPinned ? " (pinned via OPENCODE_ANTHROPIC_INITIAL_ACCOUNT)" : ""}`,
2049
2119
  `Custom betas: ${fresh.custom_betas.length ? fresh.custom_betas.join(", ") : "(none)"}`,
2050
2120
  "",
@@ -2055,6 +2125,8 @@ export async function AnthropicAuthPlugin({ client }) {
2055
2125
  " /anthropic betas add web-search-2025-03-05",
2056
2126
  " /anthropic betas add compact-2026-01-12",
2057
2127
  " /anthropic betas add mcp-servers-2025-12-04",
2128
+ " /anthropic betas add 1m (shortcut for context-1m-2025-08-07)",
2129
+ " /anthropic betas add fast (shortcut for fast-mode-2026-02-01)",
2058
2130
  "",
2059
2131
  "Remove: /anthropic betas remove <beta>",
2060
2132
  ];
@@ -2063,11 +2135,12 @@ export async function AnthropicAuthPlugin({ client }) {
2063
2135
  }
2064
2136
 
2065
2137
  if (action === "add") {
2066
- const beta = args[2]?.trim();
2067
- if (!beta) {
2138
+ const betaInput = args[2]?.trim();
2139
+ if (!betaInput) {
2068
2140
  await sendCommandMessage(input.sessionID, "▣ Anthropic Betas\n\nUsage: /anthropic betas add <beta-name>");
2069
2141
  return;
2070
2142
  }
2143
+ const beta = resolveBetaShortcut(betaInput);
2071
2144
  const fresh = loadConfigFresh();
2072
2145
  const current = fresh.custom_betas || [];
2073
2146
  if (current.includes(beta)) {
@@ -2076,16 +2149,21 @@ export async function AnthropicAuthPlugin({ client }) {
2076
2149
  }
2077
2150
  saveConfig({ custom_betas: [...current, beta] });
2078
2151
  Object.assign(config, loadConfigFresh());
2079
- await sendCommandMessage(input.sessionID, `▣ Anthropic Betas\n\nAdded: ${beta}`);
2152
+ const fromShortcut = beta !== betaInput;
2153
+ await sendCommandMessage(
2154
+ input.sessionID,
2155
+ `▣ Anthropic Betas\n\nAdded: ${beta}${fromShortcut ? ` (from shortcut: ${betaInput})` : ""}`,
2156
+ );
2080
2157
  return;
2081
2158
  }
2082
2159
 
2083
2160
  if (action === "remove" || action === "rm") {
2084
- const beta = args[2]?.trim();
2085
- if (!beta) {
2161
+ const betaInput = args[2]?.trim();
2162
+ if (!betaInput) {
2086
2163
  await sendCommandMessage(input.sessionID, "▣ Anthropic Betas\n\nUsage: /anthropic betas remove <beta-name>");
2087
2164
  return;
2088
2165
  }
2166
+ const beta = resolveBetaShortcut(betaInput);
2089
2167
  const fresh = loadConfigFresh();
2090
2168
  const current = fresh.custom_betas || [];
2091
2169
  if (!current.includes(beta)) {
@@ -2164,7 +2242,7 @@ export async function AnthropicAuthPlugin({ client }) {
2164
2242
  }
2165
2243
  return {
2166
2244
  authorization: `Bearer ${tok}`,
2167
- "anthropic-beta": "files-api-2025-04-14",
2245
+ "anthropic-beta": "oauth-2025-04-20,files-api-2025-04-14",
2168
2246
  };
2169
2247
  }
2170
2248
 
@@ -2291,7 +2369,7 @@ export async function AnthropicAuthPlugin({ client }) {
2291
2369
  method: "POST",
2292
2370
  headers: {
2293
2371
  authorization: authHeaders.authorization,
2294
- "anthropic-beta": "files-api-2025-04-14",
2372
+ "anthropic-beta": "oauth-2025-04-20,files-api-2025-04-14",
2295
2373
  },
2296
2374
  body: form,
2297
2375
  });
package/lib/oauth.mjs CHANGED
@@ -5,6 +5,12 @@ import { CLIENT_ID } from "./config.mjs";
5
5
  // OAuth helpers — shared between plugin (index.mjs) and CLI (cli.mjs)
6
6
  // ---------------------------------------------------------------------------
7
7
 
8
+ const OAUTH_CONSOLE_HOST = "platform.claude.com";
9
+ const OAUTH_MAX_HOST = "claude.ai";
10
+ const OAUTH_REDIRECT_URI = `https://${OAUTH_CONSOLE_HOST}/oauth/code/callback`;
11
+ const OAUTH_TOKEN_URL = `https://${OAUTH_CONSOLE_HOST}/v1/oauth/token`;
12
+ const OAUTH_REVOKE_URL = `https://${OAUTH_CONSOLE_HOST}/v1/oauth/revoke`;
13
+
8
14
  function base64url(input) {
9
15
  return Buffer.from(input).toString("base64url");
10
16
  }
@@ -23,11 +29,11 @@ function generatePKCE() {
23
29
  export async function authorize(mode) {
24
30
  const pkce = generatePKCE();
25
31
 
26
- const url = new URL(`https://${mode === "console" ? "console.anthropic.com" : "claude.ai"}/oauth/authorize`);
32
+ const url = new URL(`https://${mode === "console" ? OAUTH_CONSOLE_HOST : OAUTH_MAX_HOST}/oauth/authorize`);
27
33
  url.searchParams.set("code", "true");
28
34
  url.searchParams.set("client_id", CLIENT_ID);
29
35
  url.searchParams.set("response_type", "code");
30
- url.searchParams.set("redirect_uri", "https://console.anthropic.com/oauth/code/callback");
36
+ url.searchParams.set("redirect_uri", OAUTH_REDIRECT_URI);
31
37
  url.searchParams.set("scope", "org:create_api_key user:profile user:inference");
32
38
  url.searchParams.set("code_challenge", pkce.challenge);
33
39
  url.searchParams.set("code_challenge_method", "S256");
@@ -46,7 +52,7 @@ export async function authorize(mode) {
46
52
  */
47
53
  export async function exchange(code, verifier) {
48
54
  const splits = code.split("#");
49
- const result = await fetch("https://console.anthropic.com/v1/oauth/token", {
55
+ const result = await fetch(OAUTH_TOKEN_URL, {
50
56
  method: "POST",
51
57
  headers: {
52
58
  "Content-Type": "application/json",
@@ -56,7 +62,7 @@ export async function exchange(code, verifier) {
56
62
  state: splits[1],
57
63
  grant_type: "authorization_code",
58
64
  client_id: CLIENT_ID,
59
- redirect_uri: "https://console.anthropic.com/oauth/code/callback",
65
+ redirect_uri: OAUTH_REDIRECT_URI,
60
66
  code_verifier: verifier,
61
67
  }),
62
68
  });
@@ -86,7 +92,7 @@ export async function exchange(code, verifier) {
86
92
  */
87
93
  export async function revoke(refreshToken) {
88
94
  try {
89
- const resp = await fetch("https://console.anthropic.com/v1/oauth/revoke", {
95
+ const resp = await fetch(OAUTH_REVOKE_URL, {
90
96
  method: "POST",
91
97
  headers: { "Content-Type": "application/json" },
92
98
  body: JSON.stringify({
@@ -110,7 +116,7 @@ export async function revoke(refreshToken) {
110
116
  * @throws {Error} On HTTP errors or network failures
111
117
  */
112
118
  export async function refreshToken(refreshTokenValue, options = {}) {
113
- const resp = await fetch("https://console.anthropic.com/v1/oauth/token", {
119
+ const resp = await fetch(OAUTH_TOKEN_URL, {
114
120
  method: "POST",
115
121
  headers: { "Content-Type": "application/json" },
116
122
  body: JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "main": "./index.mjs",
5
5
  "files": [
6
6
  "index.mjs",