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.
Files changed (3) hide show
  1. package/index.mjs +302 -25
  2. package/lib/config.mjs +14 -0
  3. 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
- const computedBetaHeader = buildAnthropicBetaHeader(
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: config.cache_policy || { ttl: "1h", ttl_supported: true },
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
- toast("⬡ 1M context force-activated (prompt too long)", "warning", {
2634
- debounceKey: "adaptive-ctx",
2635
- }).catch(() => {});
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 (!hasOneMillionContext(model)) {
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
- // Don't de-escalate if error-escalated (sticky)
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
- return true;
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
- * @param {string | undefined} body
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 hasOneMillionContext(model) {
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
- betas.push(ADVANCED_TOOL_USE_BETA_FLAG); // "advanced-tool-use-2025-11-20"
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 — always-on for capable models
4390
- if (supportsStructuredOutputs(model)) {
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
- // === OPTIONAL BETAS (env-gated, intentionally NOT always-on) ===
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
- // redact-thinking — NOT auto-included (users benefit from seeing thinking blocks)
4415
- // Available via: /anthropic betas add redact-thinking-2026-02-12
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(input, requestInit, accessToken, requestBody, requestUrl, signature, adaptiveOverride) {
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
- if (signature.enabled && signature.cachePolicy?.ttl !== "off" && signature.cachePolicy?.ttl_supported !== false) {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "main": "./index.mjs",
6
6
  "files": [