opencode-anthropic-fix 0.0.37 → 0.0.39
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/index.mjs +302 -25
- package/lib/config.mjs +14 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -158,6 +158,24 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
158
158
|
let willowLastRequestTime = Date.now();
|
|
159
159
|
let willowLastSuggestionTime = 0;
|
|
160
160
|
|
|
161
|
+
// Beta header latching: once a beta is sent in a session, it stays on for
|
|
162
|
+
// the rest of the session to prevent cache key churn (~50-70K tokens per flip).
|
|
163
|
+
// Cleared on /clear or /compact if needed.
|
|
164
|
+
const betaLatchState = {
|
|
165
|
+
/** @type {Set<string>} betas that have been sent at least once this session */
|
|
166
|
+
sent: new Set(),
|
|
167
|
+
/** When true, a config change invalidated the latch and next request rebuilds. */
|
|
168
|
+
dirty: false,
|
|
169
|
+
/** @type {string | null} The last computed beta header string (for latching). */
|
|
170
|
+
lastHeader: null,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Cache TTL session latching: latch the cache policy at session start
|
|
174
|
+
// so mid-session toggles don't bust the server-side prompt cache.
|
|
175
|
+
let sessionCachePolicyLatched = false;
|
|
176
|
+
/** @type {{ttl: string, ttl_supported: boolean, boundary_marker?: boolean} | null} */
|
|
177
|
+
let latchedCachePolicy = null;
|
|
178
|
+
|
|
161
179
|
/**
|
|
162
180
|
* Whether OPENCODE_ANTHROPIC_INITIAL_ACCOUNT env var pinned this session to a
|
|
163
181
|
* specific account. When true, syncActiveIndexFromDisk is skipped and strategy
|
|
@@ -846,6 +864,43 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
846
864
|
adaptiveContextState.lastTransitionTurn = sessionMetrics.turns;
|
|
847
865
|
}
|
|
848
866
|
},
|
|
867
|
+
"token-efficient-tools": () => {
|
|
868
|
+
const enabled = value === "on" || value === "true" || value === "1";
|
|
869
|
+
const te = config.token_economy || {
|
|
870
|
+
token_efficient_tools: true,
|
|
871
|
+
redact_thinking: false,
|
|
872
|
+
connector_text_summarization: true,
|
|
873
|
+
};
|
|
874
|
+
te.token_efficient_tools = enabled;
|
|
875
|
+
saveConfig({ token_economy: te });
|
|
876
|
+
config.token_economy = te;
|
|
877
|
+
// Invalidate latched betas so the change takes effect next request
|
|
878
|
+
betaLatchState.dirty = true;
|
|
879
|
+
},
|
|
880
|
+
"redact-thinking": () => {
|
|
881
|
+
const enabled = value === "on" || value === "true" || value === "1";
|
|
882
|
+
const te = config.token_economy || {
|
|
883
|
+
token_efficient_tools: true,
|
|
884
|
+
redact_thinking: false,
|
|
885
|
+
connector_text_summarization: true,
|
|
886
|
+
};
|
|
887
|
+
te.redact_thinking = enabled;
|
|
888
|
+
saveConfig({ token_economy: te });
|
|
889
|
+
config.token_economy = te;
|
|
890
|
+
betaLatchState.dirty = true;
|
|
891
|
+
},
|
|
892
|
+
"connector-text": () => {
|
|
893
|
+
const enabled = value === "on" || value === "true" || value === "1";
|
|
894
|
+
const te = config.token_economy || {
|
|
895
|
+
token_efficient_tools: true,
|
|
896
|
+
redact_thinking: false,
|
|
897
|
+
connector_text_summarization: true,
|
|
898
|
+
};
|
|
899
|
+
te.connector_text_summarization = enabled;
|
|
900
|
+
saveConfig({ token_economy: te });
|
|
901
|
+
config.token_economy = te;
|
|
902
|
+
betaLatchState.dirty = true;
|
|
903
|
+
},
|
|
849
904
|
};
|
|
850
905
|
|
|
851
906
|
if (!key || !setters[key]) {
|
|
@@ -2397,7 +2452,10 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2397
2452
|
|
|
2398
2453
|
const _adaptiveOverride = _adaptiveOverrideForRequest;
|
|
2399
2454
|
|
|
2400
|
-
|
|
2455
|
+
// Token economy config (resolved once, passed to beta builder)
|
|
2456
|
+
const _tokenEconomy = config.token_economy || {};
|
|
2457
|
+
|
|
2458
|
+
let computedBetaHeader = buildAnthropicBetaHeader(
|
|
2401
2459
|
"",
|
|
2402
2460
|
getSignatureEmulationEnabled(),
|
|
2403
2461
|
_reqModel,
|
|
@@ -2407,8 +2465,43 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2407
2465
|
requestUrl?.pathname,
|
|
2408
2466
|
_reqHasFileRefs,
|
|
2409
2467
|
_adaptiveOverride,
|
|
2468
|
+
_tokenEconomy,
|
|
2410
2469
|
);
|
|
2411
2470
|
|
|
2471
|
+
// Beta header latching: once a beta has been sent in this session,
|
|
2472
|
+
// keep sending it to avoid server-side cache key churn (~50-70K tokens
|
|
2473
|
+
// per flip). The latch is sticky-on: new betas can be added but never
|
|
2474
|
+
// removed mid-session unless explicitly invalidated by config change.
|
|
2475
|
+
{
|
|
2476
|
+
const currentBetas = computedBetaHeader
|
|
2477
|
+
.split(",")
|
|
2478
|
+
.map((b) => b.trim())
|
|
2479
|
+
.filter(Boolean);
|
|
2480
|
+
// Add all current betas to the latch set
|
|
2481
|
+
for (const b of currentBetas) betaLatchState.sent.add(b);
|
|
2482
|
+
// If a config change dirtied the latch, rebuild from current (allow removal)
|
|
2483
|
+
if (betaLatchState.dirty) {
|
|
2484
|
+
betaLatchState.dirty = false;
|
|
2485
|
+
betaLatchState.sent = new Set(currentBetas);
|
|
2486
|
+
}
|
|
2487
|
+
// Merge latched betas that aren't in the current set
|
|
2488
|
+
const merged = new Set(currentBetas);
|
|
2489
|
+
for (const b of betaLatchState.sent) merged.add(b);
|
|
2490
|
+
computedBetaHeader = [...merged].join(",");
|
|
2491
|
+
betaLatchState.lastHeader = computedBetaHeader;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// Cache TTL session latching: latch the cache policy at session start
|
|
2495
|
+
// so mid-session toggles don't bust the server-side prompt cache.
|
|
2496
|
+
if (!sessionCachePolicyLatched) {
|
|
2497
|
+
sessionCachePolicyLatched = true;
|
|
2498
|
+
latchedCachePolicy = config.cache_policy
|
|
2499
|
+
? { ...config.cache_policy }
|
|
2500
|
+
: { ttl: "1h", ttl_supported: true };
|
|
2501
|
+
}
|
|
2502
|
+
const effectiveCachePolicy = latchedCachePolicy ||
|
|
2503
|
+
config.cache_policy || { ttl: "1h", ttl_supported: true };
|
|
2504
|
+
|
|
2412
2505
|
let body = transformRequestBody(
|
|
2413
2506
|
requestInit.body,
|
|
2414
2507
|
{
|
|
@@ -2416,7 +2509,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2416
2509
|
claudeCliVersion,
|
|
2417
2510
|
promptCompactionMode: getPromptCompactionMode(),
|
|
2418
2511
|
provider: _reqProvider,
|
|
2419
|
-
cachePolicy:
|
|
2512
|
+
cachePolicy: effectiveCachePolicy,
|
|
2420
2513
|
fastMode: config.fast_mode || false,
|
|
2421
2514
|
strategy: getEffectiveStrategy(),
|
|
2422
2515
|
},
|
|
@@ -2443,6 +2536,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2443
2536
|
strategy: getEffectiveStrategy(),
|
|
2444
2537
|
},
|
|
2445
2538
|
_adaptiveOverride,
|
|
2539
|
+
_tokenEconomy,
|
|
2446
2540
|
);
|
|
2447
2541
|
// Execute the request
|
|
2448
2542
|
let response;
|
|
@@ -2629,10 +2723,15 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2629
2723
|
// Auto-escalate adaptive context on prompt_too_long so the retry
|
|
2630
2724
|
// includes the 1M beta header (if model supports it).
|
|
2631
2725
|
if (config.adaptive_context?.enabled) {
|
|
2632
|
-
forceEscalateAdaptiveContext();
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2726
|
+
const stateChanged = forceEscalateAdaptiveContext();
|
|
2727
|
+
if (stateChanged) {
|
|
2728
|
+
// Invalidate cached adaptive decision so the retry loop
|
|
2729
|
+
// re-evaluates with the new active=true state.
|
|
2730
|
+
_adaptiveDecisionMade = false;
|
|
2731
|
+
toast("⬡ 1M context force-activated (prompt too long)", "warning", {
|
|
2732
|
+
debounceKey: "adaptive-ctx",
|
|
2733
|
+
}).catch(() => {});
|
|
2734
|
+
}
|
|
2636
2735
|
}
|
|
2637
2736
|
try {
|
|
2638
2737
|
const parsedBody = JSON.parse(body);
|
|
@@ -3215,7 +3314,7 @@ function estimatePromptTokens(bodyString) {
|
|
|
3215
3314
|
* @returns {boolean}
|
|
3216
3315
|
*/
|
|
3217
3316
|
function resolveAdaptiveContext(bodyString, model, adaptiveConfig) {
|
|
3218
|
-
// Non-adaptive: use static check
|
|
3317
|
+
// Non-adaptive: use static check (only explicit "1m" models)
|
|
3219
3318
|
if (!adaptiveConfig.enabled) {
|
|
3220
3319
|
return hasOneMillionContext(model);
|
|
3221
3320
|
}
|
|
@@ -3225,8 +3324,8 @@ function resolveAdaptiveContext(bodyString, model, adaptiveConfig) {
|
|
|
3225
3324
|
return false;
|
|
3226
3325
|
}
|
|
3227
3326
|
|
|
3228
|
-
// Model must be eligible for 1M context at all
|
|
3229
|
-
if (!
|
|
3327
|
+
// Model must be eligible for 1M context at all (includes Opus 4.6)
|
|
3328
|
+
if (!isEligibleFor1MContext(model)) {
|
|
3230
3329
|
return false;
|
|
3231
3330
|
}
|
|
3232
3331
|
|
|
@@ -3235,10 +3334,25 @@ function resolveAdaptiveContext(bodyString, model, adaptiveConfig) {
|
|
|
3235
3334
|
|
|
3236
3335
|
if (adaptiveContextState.active) {
|
|
3237
3336
|
// Currently active — consider de-escalation
|
|
3238
|
-
|
|
3337
|
+
|
|
3338
|
+
// Error-escalated: sticky for ERROR_STICKY_TURNS turns, then allow de-escalation
|
|
3339
|
+
// if prompt has dropped well below threshold (prevents permanent 1M lock-in).
|
|
3340
|
+
const ERROR_STICKY_TURNS = 5;
|
|
3239
3341
|
if (adaptiveContextState.escalatedByError) {
|
|
3240
|
-
|
|
3342
|
+
if (turnsSinceTransition < ERROR_STICKY_TURNS) {
|
|
3343
|
+
return true; // Still within sticky window
|
|
3344
|
+
}
|
|
3345
|
+
// Past sticky window: allow de-escalation if tokens dropped significantly
|
|
3346
|
+
// (below 75% of deescalation threshold to avoid flapping)
|
|
3347
|
+
if (estimatedTokens < adaptiveConfig.deescalation_threshold * 0.75) {
|
|
3348
|
+
adaptiveContextState.active = false;
|
|
3349
|
+
adaptiveContextState.escalatedByError = false;
|
|
3350
|
+
adaptiveContextState.lastTransitionTurn = sessionMetrics.turns;
|
|
3351
|
+
return false;
|
|
3352
|
+
}
|
|
3353
|
+
return true; // Still high enough to keep 1M
|
|
3241
3354
|
}
|
|
3355
|
+
|
|
3242
3356
|
// Hysteresis: require at least 2 turns before considering de-escalation
|
|
3243
3357
|
if (turnsSinceTransition < 2) {
|
|
3244
3358
|
return true;
|
|
@@ -3268,13 +3382,17 @@ function resolveAdaptiveContext(bodyString, model, adaptiveConfig) {
|
|
|
3268
3382
|
|
|
3269
3383
|
/**
|
|
3270
3384
|
* Force-escalate adaptive context (e.g. after prompt_too_long error).
|
|
3385
|
+
* Returns true so callers can invalidate cached decisions.
|
|
3386
|
+
* @returns {boolean}
|
|
3271
3387
|
*/
|
|
3272
3388
|
function forceEscalateAdaptiveContext() {
|
|
3389
|
+
const wasActive = adaptiveContextState.active;
|
|
3273
3390
|
if (!adaptiveContextState.active) {
|
|
3274
3391
|
adaptiveContextState.active = true;
|
|
3275
3392
|
adaptiveContextState.lastTransitionTurn = sessionMetrics.turns;
|
|
3276
3393
|
}
|
|
3277
3394
|
adaptiveContextState.escalatedByError = true;
|
|
3395
|
+
return !wasActive; // true if state actually changed
|
|
3278
3396
|
}
|
|
3279
3397
|
|
|
3280
3398
|
const MODEL_PRICING = {
|
|
@@ -3607,6 +3725,96 @@ function computeBillingCacheHash(firstUserMessage, version) {
|
|
|
3607
3725
|
* @param {any[] | undefined} messages
|
|
3608
3726
|
* @returns {string}
|
|
3609
3727
|
*/
|
|
3728
|
+
/**
|
|
3729
|
+
* Strip leaked /anthropic slash command messages from conversation history.
|
|
3730
|
+
*
|
|
3731
|
+
* When a user runs `/anthropic <subcommand>`, OpenCode may still include the
|
|
3732
|
+
* command text as a user message and the sendCommandMessage output as an
|
|
3733
|
+
* assistant message in the API request. This function removes those messages
|
|
3734
|
+
* so the model never sees internal plugin commands in its context.
|
|
3735
|
+
*
|
|
3736
|
+
* Detection heuristics:
|
|
3737
|
+
* - User messages that start with `/anthropic` (with optional leading whitespace)
|
|
3738
|
+
* - User messages where the ONLY text content is a `/anthropic` command
|
|
3739
|
+
* - Assistant messages that start with the `▣ Anthropic` prefix used by sendCommandMessage
|
|
3740
|
+
*
|
|
3741
|
+
* After filtering, if the last remaining message is an assistant message, drop it
|
|
3742
|
+
* to maintain the user→assistant alternation required by the API.
|
|
3743
|
+
*
|
|
3744
|
+
* @param {Array} messages — The messages array from the parsed request body
|
|
3745
|
+
* @returns {Array} — Filtered messages array
|
|
3746
|
+
*/
|
|
3747
|
+
function stripSlashCommandMessages(messages) {
|
|
3748
|
+
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
3749
|
+
|
|
3750
|
+
// Pattern: /anthropic followed by optional subcommand
|
|
3751
|
+
const CMD_RE = /^\s*\/anthropic\b/i;
|
|
3752
|
+
// Pattern: ▣ Anthropic — prefix used by all sendCommandMessage outputs
|
|
3753
|
+
const RESP_RE = /^▣\s*Anthropic/;
|
|
3754
|
+
|
|
3755
|
+
/**
|
|
3756
|
+
* Extract the first text content from a message's content field.
|
|
3757
|
+
* Handles both string content and array-of-blocks content.
|
|
3758
|
+
*/
|
|
3759
|
+
function getFirstText(msg) {
|
|
3760
|
+
if (typeof msg.content === "string") return msg.content;
|
|
3761
|
+
if (Array.isArray(msg.content)) {
|
|
3762
|
+
for (const block of msg.content) {
|
|
3763
|
+
if (block.type === "text" && typeof block.text === "string") return block.text;
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
return "";
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
/**
|
|
3770
|
+
* Check if a user message is a /anthropic command.
|
|
3771
|
+
* A message is a command if its text content starts with /anthropic.
|
|
3772
|
+
*/
|
|
3773
|
+
function isCommandMessage(msg) {
|
|
3774
|
+
if (msg.role !== "user") return false;
|
|
3775
|
+
const text = getFirstText(msg);
|
|
3776
|
+
return CMD_RE.test(text);
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
/**
|
|
3780
|
+
* Check if an assistant message is a sendCommandMessage response.
|
|
3781
|
+
* These always start with ▣ Anthropic.
|
|
3782
|
+
*/
|
|
3783
|
+
function isCommandResponse(msg) {
|
|
3784
|
+
if (msg.role !== "assistant") return false;
|
|
3785
|
+
const text = getFirstText(msg);
|
|
3786
|
+
return RESP_RE.test(text);
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
const filtered = [];
|
|
3790
|
+
for (let i = 0; i < messages.length; i++) {
|
|
3791
|
+
const msg = messages[i];
|
|
3792
|
+
|
|
3793
|
+
// Drop /anthropic command messages
|
|
3794
|
+
if (isCommandMessage(msg)) {
|
|
3795
|
+
// Also drop the immediately following assistant response if it's a command response
|
|
3796
|
+
if (i + 1 < messages.length && isCommandResponse(messages[i + 1])) {
|
|
3797
|
+
i++; // Skip the response too
|
|
3798
|
+
}
|
|
3799
|
+
continue;
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
// Drop orphaned command responses (in case the command message was already removed
|
|
3803
|
+
// or the ordering is different)
|
|
3804
|
+
if (isCommandResponse(msg)) {
|
|
3805
|
+
continue;
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
filtered.push(msg);
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
// Safety: if filtering removed ALL messages, return the original to avoid sending
|
|
3812
|
+
// an empty messages array to the API.
|
|
3813
|
+
if (filtered.length === 0) return messages;
|
|
3814
|
+
|
|
3815
|
+
return filtered;
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3610
3818
|
function extractFirstUserMessageText(messages) {
|
|
3611
3819
|
if (!Array.isArray(messages)) return "";
|
|
3612
3820
|
for (const msg of messages) {
|
|
@@ -3656,6 +3864,8 @@ const EXPERIMENTAL_BETA_FLAGS = new Set([
|
|
|
3656
3864
|
"prompt-caching-scope-2026-01-05",
|
|
3657
3865
|
"redact-thinking-2026-02-12",
|
|
3658
3866
|
"structured-outputs-2025-12-15",
|
|
3867
|
+
"summarize-connector-text-2026-03-13",
|
|
3868
|
+
"token-efficient-tools-2026-03-28",
|
|
3659
3869
|
"tool-examples-2025-10-29",
|
|
3660
3870
|
"tool-search-tool-2025-10-19",
|
|
3661
3871
|
"web-search-2025-03-05",
|
|
@@ -3669,6 +3879,11 @@ const BETA_SHORTCUTS = new Map([
|
|
|
3669
3879
|
["opus-fast", "fast-mode-2026-02-01"],
|
|
3670
3880
|
["task-budgets", "task-budgets-2026-03-13"],
|
|
3671
3881
|
["budgets", "task-budgets-2026-03-13"],
|
|
3882
|
+
["efficient-tools", "token-efficient-tools-2026-03-28"],
|
|
3883
|
+
["token-efficient", "token-efficient-tools-2026-03-28"],
|
|
3884
|
+
["redact-thinking", "redact-thinking-2026-02-12"],
|
|
3885
|
+
["connector-text", "summarize-connector-text-2026-03-13"],
|
|
3886
|
+
["anti-distill", "summarize-connector-text-2026-03-13"],
|
|
3672
3887
|
]);
|
|
3673
3888
|
const STAINLESS_HELPER_KEYS = [
|
|
3674
3889
|
"x_stainless_helper",
|
|
@@ -3884,14 +4099,26 @@ function isAdaptiveThinkingModel(model) {
|
|
|
3884
4099
|
}
|
|
3885
4100
|
|
|
3886
4101
|
/**
|
|
3887
|
-
*
|
|
4102
|
+
* Check if a model is eligible for 1M context (can receive context-1m beta).
|
|
4103
|
+
* This includes models with explicit "1m" in the name AND Opus 4.6.
|
|
4104
|
+
* @param {string} model
|
|
3888
4105
|
* @returns {boolean}
|
|
3889
4106
|
*/
|
|
3890
|
-
function
|
|
3891
|
-
// Models with explicit 1m suffix, or Opus 4.6 (1M by default since v2.1.75).
|
|
4107
|
+
function isEligibleFor1MContext(model) {
|
|
3892
4108
|
return /(^|[-_ ])1m($|[-_ ])|context[-_]?1m/i.test(model) || isOpus46Model(model);
|
|
3893
4109
|
}
|
|
3894
4110
|
|
|
4111
|
+
/**
|
|
4112
|
+
* Check if a model should ALWAYS use 1M context (static mode, no adaptive gating).
|
|
4113
|
+
* Only models with explicit "1m" in the name — NOT bare Opus 4.6.
|
|
4114
|
+
* When adaptive_context is enabled, Opus 4.6 uses the adaptive decision instead.
|
|
4115
|
+
* @param {string} model
|
|
4116
|
+
* @returns {boolean}
|
|
4117
|
+
*/
|
|
4118
|
+
function hasOneMillionContext(model) {
|
|
4119
|
+
return /(^|[-_ ])1m($|[-_ ])|context[-_]?1m/i.test(model);
|
|
4120
|
+
}
|
|
4121
|
+
|
|
3895
4122
|
/**
|
|
3896
4123
|
* @param {string | undefined} body
|
|
3897
4124
|
* @returns {boolean}
|
|
@@ -4324,6 +4551,7 @@ function buildAnthropicBetaHeader(
|
|
|
4324
4551
|
requestPath,
|
|
4325
4552
|
hasFileReferences,
|
|
4326
4553
|
adaptiveOverride,
|
|
4554
|
+
tokenEconomy,
|
|
4327
4555
|
) {
|
|
4328
4556
|
const incomingBetasList = incomingBeta
|
|
4329
4557
|
.split(",")
|
|
@@ -4350,13 +4578,23 @@ function buildAnthropicBetaHeader(
|
|
|
4350
4578
|
const nonInteractive = isNonInteractiveMode();
|
|
4351
4579
|
const haiku = isHaikuModel(model);
|
|
4352
4580
|
const isRoundRobin = strategy === "round-robin";
|
|
4581
|
+
const te = tokenEconomy || {};
|
|
4353
4582
|
|
|
4354
4583
|
// === ALWAYS-ON BETAS (Claude Code v2.1.84 base set) ===
|
|
4355
4584
|
// These are ALWAYS included regardless of env vars or feature flags.
|
|
4356
4585
|
if (!haiku) {
|
|
4357
4586
|
betas.push(CLAUDE_CODE_BETA_FLAG); // "claude-code-20250219"
|
|
4358
4587
|
}
|
|
4359
|
-
|
|
4588
|
+
|
|
4589
|
+
// Tool search: use provider-aware header.
|
|
4590
|
+
// 1P/Foundry → advanced-tool-use-2025-11-20 (enables broader tool capabilities)
|
|
4591
|
+
// Vertex/Bedrock → tool-search-tool-2025-10-19 (3P-compatible subset)
|
|
4592
|
+
if (provider === "vertex" || provider === "bedrock") {
|
|
4593
|
+
betas.push("tool-search-tool-2025-10-19");
|
|
4594
|
+
} else {
|
|
4595
|
+
betas.push(ADVANCED_TOOL_USE_BETA_FLAG); // "advanced-tool-use-2025-11-20"
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4360
4598
|
betas.push(FAST_MODE_BETA_FLAG); // "fast-mode-2026-02-01"
|
|
4361
4599
|
betas.push(EFFORT_BETA_FLAG); // "effort-2025-11-24"
|
|
4362
4600
|
|
|
@@ -4386,8 +4624,14 @@ function buildAnthropicBetaHeader(
|
|
|
4386
4624
|
// Context management — always-on in Claude Code (not env-gated)
|
|
4387
4625
|
betas.push("context-management-2025-06-27");
|
|
4388
4626
|
|
|
4389
|
-
// Structured outputs
|
|
4390
|
-
|
|
4627
|
+
// Structured outputs vs token-efficient-tools: mutually exclusive.
|
|
4628
|
+
// token-efficient-tools gives ~4.5% output token reduction (JSON FC v3).
|
|
4629
|
+
// When both are configured, token-efficient-tools wins (per CC source:
|
|
4630
|
+
// "strict wins" internally, but we prefer token savings for proxy users).
|
|
4631
|
+
if (te.token_efficient_tools && !disableExperimentalBetas) {
|
|
4632
|
+
betas.push("token-efficient-tools-2026-03-28");
|
|
4633
|
+
// Skip structured-outputs — API rejects both together
|
|
4634
|
+
} else if (supportsStructuredOutputs(model)) {
|
|
4391
4635
|
betas.push("structured-outputs-2025-12-15");
|
|
4392
4636
|
}
|
|
4393
4637
|
|
|
@@ -4396,9 +4640,6 @@ function buildAnthropicBetaHeader(
|
|
|
4396
4640
|
betas.push("web-search-2025-03-05");
|
|
4397
4641
|
}
|
|
4398
4642
|
|
|
4399
|
-
// Tool search tool
|
|
4400
|
-
betas.push("tool-search-tool-2025-10-19");
|
|
4401
|
-
|
|
4402
4643
|
// Files API — scoped to file endpoints/references
|
|
4403
4644
|
if (isFilesEndpoint || hasFileReferences) {
|
|
4404
4645
|
betas.push("files-api-2025-04-14");
|
|
@@ -4409,10 +4650,20 @@ function buildAnthropicBetaHeader(
|
|
|
4409
4650
|
betas.push(TOKEN_COUNTING_BETA_FLAG);
|
|
4410
4651
|
}
|
|
4411
4652
|
|
|
4412
|
-
// ===
|
|
4653
|
+
// === TOKEN ECONOMY BETAS (config-controlled) ===
|
|
4654
|
+
|
|
4655
|
+
// redact-thinking: suppresses thinking summaries server-side.
|
|
4656
|
+
// When enabled, API returns redacted_thinking blocks instead of summaries.
|
|
4657
|
+
// Saves bandwidth and tokens for users who don't inspect thinking.
|
|
4658
|
+
if (te.redact_thinking && !disableExperimentalBetas) {
|
|
4659
|
+
betas.push("redact-thinking-2026-02-12");
|
|
4660
|
+
}
|
|
4413
4661
|
|
|
4414
|
-
//
|
|
4415
|
-
//
|
|
4662
|
+
// summarize-connector-text: API summarizes assistant text between tool calls.
|
|
4663
|
+
// Anti-distillation measure — same mechanism as thinking blocks (summary + signature).
|
|
4664
|
+
if (te.connector_text_summarization && !disableExperimentalBetas) {
|
|
4665
|
+
betas.push("summarize-connector-text-2026-03-13");
|
|
4666
|
+
}
|
|
4416
4667
|
|
|
4417
4668
|
// afk-mode — NOT auto-included (requires user opt-in)
|
|
4418
4669
|
// Available via: /anthropic betas add afk-mode-2026-01-31
|
|
@@ -4559,7 +4810,16 @@ async function fetchLatestClaudeCodeVersion(timeoutMs = 1200) {
|
|
|
4559
4810
|
* @param {{enabled: boolean, claudeCliVersion: string, strategy?: import('./lib/config.mjs').AccountSelectionStrategy, customBetas?: string[]}} signature
|
|
4560
4811
|
* @returns {Headers}
|
|
4561
4812
|
*/
|
|
4562
|
-
function buildRequestHeaders(
|
|
4813
|
+
function buildRequestHeaders(
|
|
4814
|
+
input,
|
|
4815
|
+
requestInit,
|
|
4816
|
+
accessToken,
|
|
4817
|
+
requestBody,
|
|
4818
|
+
requestUrl,
|
|
4819
|
+
signature,
|
|
4820
|
+
adaptiveOverride,
|
|
4821
|
+
tokenEconomy,
|
|
4822
|
+
) {
|
|
4563
4823
|
const requestHeaders = new Headers();
|
|
4564
4824
|
if (input instanceof Request) {
|
|
4565
4825
|
input.headers.forEach((value, key) => {
|
|
@@ -4600,6 +4860,7 @@ function buildRequestHeaders(input, requestInit, accessToken, requestBody, reque
|
|
|
4600
4860
|
requestUrl?.pathname,
|
|
4601
4861
|
hasFileReferences,
|
|
4602
4862
|
adaptiveOverride,
|
|
4863
|
+
tokenEconomy,
|
|
4603
4864
|
);
|
|
4604
4865
|
|
|
4605
4866
|
const authTokenOverride = process.env.ANTHROPIC_AUTH_TOKEN?.trim();
|
|
@@ -4727,6 +4988,14 @@ function transformRequestBody(body, signature, runtime, betaHeader) {
|
|
|
4727
4988
|
parsed.temperature = 1;
|
|
4728
4989
|
}
|
|
4729
4990
|
|
|
4991
|
+
// Strip leaked /anthropic slash command messages from conversation history.
|
|
4992
|
+
// OpenCode may include command text and sendCommandMessage output as regular
|
|
4993
|
+
// user messages even when output.noReply = true was set. Filter them out
|
|
4994
|
+
// so the agent never sees /anthropic commands in its context.
|
|
4995
|
+
if (Array.isArray(parsed.messages)) {
|
|
4996
|
+
parsed.messages = stripSlashCommandMessages(parsed.messages);
|
|
4997
|
+
}
|
|
4998
|
+
|
|
4730
4999
|
// QA fix H2: avoid mutating the signature parameter; capture modelId locally
|
|
4731
5000
|
const modelId = parsed.model || "";
|
|
4732
5001
|
// Extract first user message text for billing hash computation (cch)
|
|
@@ -4753,7 +5022,15 @@ function transformRequestBody(body, signature, runtime, betaHeader) {
|
|
|
4753
5022
|
// Cache breakpoint optimization: add cache_control to the last content block
|
|
4754
5023
|
// of each user/assistant message for maximum prefix caching.
|
|
4755
5024
|
// Skip for round-robin strategy (cache defeated by account rotation).
|
|
4756
|
-
|
|
5025
|
+
// Skip for title generators / fire-and-forget queries: these are one-shot
|
|
5026
|
+
// requests that don't benefit from caching and would pollute the cache pool.
|
|
5027
|
+
const isTitleGen = isTitleGeneratorSystemBlocks(parsed.system || []);
|
|
5028
|
+
if (
|
|
5029
|
+
signature.enabled &&
|
|
5030
|
+
signature.cachePolicy?.ttl !== "off" &&
|
|
5031
|
+
signature.cachePolicy?.ttl_supported !== false &&
|
|
5032
|
+
!isTitleGen
|
|
5033
|
+
) {
|
|
4757
5034
|
const strategy = signature.strategy || "sticky";
|
|
4758
5035
|
if (strategy !== "round-robin" && Array.isArray(parsed.messages)) {
|
|
4759
5036
|
const cacheControl = { type: "ephemeral", ttl: signature.cachePolicy?.ttl || "1h" };
|
package/lib/config.mjs
CHANGED
|
@@ -166,6 +166,19 @@ export const DEFAULT_CONFIG = {
|
|
|
166
166
|
* (no point suggesting reset at the start of a session). */
|
|
167
167
|
min_turns_before_suggest: 3,
|
|
168
168
|
},
|
|
169
|
+
/** Token economy optimizations. */
|
|
170
|
+
token_economy: {
|
|
171
|
+
/** Enable token-efficient-tools-2026-03-28 beta (~4.5% output token reduction).
|
|
172
|
+
* Mutually exclusive with structured-outputs — this flag takes precedence. */
|
|
173
|
+
token_efficient_tools: true,
|
|
174
|
+
/** Enable redact-thinking-2026-02-12 beta (suppresses thinking summaries
|
|
175
|
+
* server-side for lower bandwidth). Off by default — set to true if you
|
|
176
|
+
* don't need to inspect thinking blocks. */
|
|
177
|
+
redact_thinking: false,
|
|
178
|
+
/** Enable summarize-connector-text-2026-03-13 beta (API summarizes assistant
|
|
179
|
+
* text between tool calls — anti-distillation measure). */
|
|
180
|
+
connector_text_summarization: true,
|
|
181
|
+
},
|
|
169
182
|
};
|
|
170
183
|
|
|
171
184
|
export const VALID_STRATEGIES = ["sticky", "round-robin", "hybrid"];
|
|
@@ -194,6 +207,7 @@ function createDefaultConfig() {
|
|
|
194
207
|
cc_credential_reuse: { ...DEFAULT_CONFIG.cc_credential_reuse },
|
|
195
208
|
adaptive_context: { ...DEFAULT_CONFIG.adaptive_context },
|
|
196
209
|
willow_mode: { ...DEFAULT_CONFIG.willow_mode },
|
|
210
|
+
token_economy: { ...DEFAULT_CONFIG.token_economy },
|
|
197
211
|
};
|
|
198
212
|
}
|
|
199
213
|
|