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 +12 -7
- package/index.mjs +125 -47
- package/lib/oauth.mjs +12 -6
- package/package.json +1 -1
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
|
-
- **
|
|
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
|
|
50
|
-
- **Code execution support** —
|
|
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
|
|
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 | **
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
695
|
-
|
|
696
|
-
|
|
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
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
|
2043
|
-
"
|
|
2044
|
-
|
|
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
|
|
2067
|
-
if (!
|
|
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
|
-
|
|
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
|
|
2085
|
-
if (!
|
|
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" ?
|
|
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",
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
119
|
+
const resp = await fetch(OAUTH_TOKEN_URL, {
|
|
114
120
|
method: "POST",
|
|
115
121
|
headers: { "Content-Type": "application/json" },
|
|
116
122
|
body: JSON.stringify({
|