opencode-anthropic-fix 0.1.27 → 0.1.29

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 CHANGED
@@ -4,7 +4,7 @@ import { randomBytes, randomUUID, createHash as createHashCrypto } from "node:cr
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { join, resolve, basename } from "node:path";
6
6
  import xxhashInit from "xxhash-wasm";
7
- import { AccountManager } from "./lib/accounts.mjs";
7
+ import { AccountManager, RATE_LIMIT_KEY_FAST } from "./lib/accounts.mjs";
8
8
  import {
9
9
  authorize as oauthAuthorize,
10
10
  exchange as oauthExchange,
@@ -37,6 +37,11 @@ import { callHaiku } from "./lib/haiku-call.mjs";
37
37
  import { summarize as rollingSummarize } from "./lib/rolling-summarizer.mjs";
38
38
  import { staleReadEviction, perToolClassPrune } from "./lib/message-transform.mjs";
39
39
 
40
+ // Max times a single logical request may fall back from fast->standard speed on
41
+ // the same account before giving up the fast attempt entirely. 1 is enough: one
42
+ // fast 429 => one standard retry on the same account.
43
+ const MAX_FAST_FALLBACKS = 1;
44
+
40
45
  // ---------------------------------------------------------------------------
41
46
  // Account management CLI prompts
42
47
  // ---------------------------------------------------------------------------
@@ -2461,7 +2466,10 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
2461
2466
  if (
2462
2467
  config.override_model_limits.enabled &&
2463
2468
  !isTruthyEnv(process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT) &&
2464
- (hasOneMillionContext(model.id) || isOpus46Model(model.id) || isOpus47Model(model.id))
2469
+ (hasOneMillionContext(model.id) ||
2470
+ isOpus46Model(model.id) ||
2471
+ isOpus47Model(model.id) ||
2472
+ isOpus48Model(model.id))
2465
2473
  ) {
2466
2474
  model.limit = {
2467
2475
  ...(model.limit ?? {}),
@@ -2642,6 +2650,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
2642
2650
  let serviceWideRetryCount = 0; // Track 529/503 retries (max 2 per RE doc §5.5)
2643
2651
  let shouldRetryCount = 0; // Track x-should-retry forced retries (cap at 3)
2644
2652
  let consecutive529Count = 0;
2653
+ let fastFallbackCount = 0; // Track fast->standard same-account fallbacks (cap below)
2645
2654
  // Classify request for retry budget (A8)
2646
2655
  const requestClass =
2647
2656
  config.request_classification?.enabled !== false ? classifyApiRequest(requestInit.body) : "foreground";
@@ -2682,7 +2691,17 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
2682
2691
  "No enabled Anthropic accounts available. Enable one with 'opencode-anthropic-auth enable <N>'.",
2683
2692
  );
2684
2693
  }
2685
- // All accounts excluded (transient refresh failures) — give up
2694
+ // All accounts excluded (transient refresh failures) — give up.
2695
+ // Diagnostic: surface WHICH accounts were skipped and why, so the
2696
+ // "No available account" symptom is debuggable (e.g. it can be a
2697
+ // downstream effect of an OAuth refresh failure, not a true
2698
+ // exhaustion). transientRefreshSkips holds account indices that
2699
+ // failed/were-rate-limited during refresh this request.
2700
+ debugLog("No available account — all candidates transiently skipped", {
2701
+ enabledCount,
2702
+ transientRefreshSkips: Array.from(transientRefreshSkips),
2703
+ attempt,
2704
+ });
2686
2705
  throw new Error("No available Anthropic account for request.");
2687
2706
  }
2688
2707
 
@@ -2847,6 +2866,12 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
2847
2866
  // suppress context-hint for non-main-thread requests so we don't
2848
2867
  // opt subagent/title/tool one-shots into a server retry loop.
2849
2868
  const _requestRole = classifyRequestRole(_parsedBodyOnce);
2869
+ // opencode tags subagent requests with the x-parent-session-id header
2870
+ // (set in its request builder only when a parent session exists).
2871
+ // This is a reliable subagent signal; body-shape classification alone
2872
+ // sees subagents as "main" (real messages + large max_tokens). Used
2873
+ // only to pick the cheaper 5m cache tier for one-shot subagents.
2874
+ const _isSubagent = getIncomingHeader(input, requestInit, "x-parent-session-id") != null;
2850
2875
  const _baseTE = config.token_economy || {};
2851
2876
  const _disableCtxHint = contextHintState.disabled || _requestRole !== "main";
2852
2877
  const _tokenEconomy = _disableCtxHint
@@ -2979,6 +3004,10 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
2979
3004
  provider: _reqProvider,
2980
3005
  cachePolicy: effectiveCachePolicy,
2981
3006
  fastMode: config.fast_mode || false,
3007
+ // When the account's fast pool is cooling down, suppress
3008
+ // speed:"fast" so this turn runs at standard speed on the same
3009
+ // account instead of re-hitting the exhausted fast pool.
3010
+ fastRateLimited: accountManager.isFastRateLimited(account),
2982
3011
  strategy: getEffectiveStrategy(),
2983
3012
  toolDeferral: config.token_economy_strategies?.tool_deferral,
2984
3013
  toolDescriptionCompaction: config.token_economy_strategies?.tool_description_compaction,
@@ -2995,6 +3024,15 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
2995
3024
  usedTools: sessionMetrics.usedTools,
2996
3025
  tokenEconomySession,
2997
3026
  requestRole: _requestRole,
3027
+ isSubagent: _isSubagent,
3028
+ // Adaptive cache-breakpoint placement: pass the per-boundary
3029
+ // stability snapshot so transformRequestBody can anchor the
3030
+ // cache_control marker on the most-stable boundary instead of
3031
+ // the (possibly thrashing) last tool. Only populated once the
3032
+ // detector is enabled and has a baseline.
3033
+ cacheBoundaryStability: config.cache_break_detection?.adaptive_breakpoint
3034
+ ? cacheBreakState.boundaryStability
3035
+ : null,
2998
3036
  },
2999
3037
  computedBetaHeader,
3000
3038
  config,
@@ -3018,6 +3056,16 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
3018
3056
  if (config.cache_break_detection?.enabled && typeof body === "string") {
3019
3057
  const currentHashes = extractCacheSourceHashes(body);
3020
3058
  if (currentHashes.size > 0) {
3059
+ // Update per-boundary structural stability BEFORE staging the
3060
+ // new hashes, diffing against the prior turn's baseline. This
3061
+ // feeds the adaptive breakpoint placer on the NEXT turn.
3062
+ if (cacheBreakState.sourceHashes.size > 0) {
3063
+ updateBoundaryStability(
3064
+ currentHashes,
3065
+ cacheBreakState.sourceHashes,
3066
+ cacheBreakState.boundaryStability,
3067
+ );
3068
+ }
3021
3069
  cacheBreakState._pendingHashes = currentHashes;
3022
3070
  }
3023
3071
  }
@@ -3665,8 +3713,14 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
3665
3713
  // Check x-should-retry header first — server override
3666
3714
  const shouldRetry = parseShouldRetryHeader(response);
3667
3715
  if (shouldRetry === false) {
3668
- // Server says DO NOT retry — return error directly
3669
- debugLog("x-should-retry: false — not retrying", { status: response.status });
3716
+ // Server says DO NOT retry — return error directly.
3717
+ // Include a snippet of the error body so 4xx causes (e.g. the
3718
+ // thinking-block "cannot be modified" 400, or an unsupported
3719
+ // manual-thinking 400 on adaptive models) are visible in debug.
3720
+ debugLog("x-should-retry: false — not retrying", {
3721
+ status: response.status,
3722
+ errorBody: typeof errorBody === "string" ? errorBody.slice(0, 600) : errorBody,
3723
+ });
3670
3724
  return transformResponse(response);
3671
3725
  }
3672
3726
 
@@ -3713,6 +3767,44 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
3713
3767
  continue;
3714
3768
  }
3715
3769
 
3770
+ // Fast-mode pool isolation: if THIS request used speed:"fast"
3771
+ // and the limit is a plain rate limit, the FAST pool was
3772
+ // exhausted — not the account's standard quota. Per Anthropic,
3773
+ // fast has a separate pool and should fall back to standard
3774
+ // speed, NOT block the account. So: cool down only the fast
3775
+ // bucket, then retry on the SAME account at standard speed
3776
+ // (the next transformRequestBody omits speed:"fast" because
3777
+ // isFastRateLimited(account) is now true). We do NOT flip the
3778
+ // global config.fast_mode flag — other accounts/turns can still
3779
+ // use fast mode.
3780
+ const requestWasFast = typeof body === "string" && body.includes('"speed":"fast"');
3781
+ if (
3782
+ requestWasFast &&
3783
+ response.status === 429 &&
3784
+ reason === "RATE_LIMIT_EXCEEDED" &&
3785
+ fastFallbackCount < MAX_FAST_FALLBACKS
3786
+ ) {
3787
+ fastFallbackCount++;
3788
+ const fastBackoff = accountManager.markRateLimited(
3789
+ account,
3790
+ reason,
3791
+ retryAfterMs,
3792
+ RATE_LIMIT_KEY_FAST,
3793
+ );
3794
+ _fastModeAppliedToast = false;
3795
+ toast("⚡ Fast pool limited — falling back to standard speed", "info", {
3796
+ debounceKey: "fast-fallback",
3797
+ }).catch(() => {});
3798
+ debugLog("fast pool rate-limited; falling back to standard on same account", {
3799
+ account: account.email || `Account ${account.index + 1}`,
3800
+ fastBackoffMs: fastBackoff,
3801
+ fastFallbackCount,
3802
+ });
3803
+ // Retry same account at standard speed without consuming a slot.
3804
+ attempt--;
3805
+ continue;
3806
+ }
3807
+
3716
3808
  accountManager.markRateLimited(account, reason, retryAfterMs);
3717
3809
 
3718
3810
  // On auth failures, clear token so next selection forces refresh
@@ -3724,7 +3816,10 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
3724
3816
  // Strategy adaptation: record account-specific throttling signal
3725
3817
  recordRateLimitForStrategy();
3726
3818
 
3727
- // Graceful degradation: disable fast mode on rate limits
3819
+ // Graceful degradation: disable fast mode on rate limits.
3820
+ // Note: a fast-pool-only 429 is handled by the fast-fallback
3821
+ // branch above and never reaches here, so this global disable
3822
+ // only fires for standard-pool limits or 529 overloads.
3728
3823
  if (config.fast_mode && (response.status === 429 || response.status === 529)) {
3729
3824
  config.fast_mode = false;
3730
3825
  _fastModeAppliedToast = false;
@@ -4372,8 +4467,40 @@ const cacheBreakState = {
4372
4467
  prevCacheRead: 0,
4373
4468
  sourceHashes: new Map(),
4374
4469
  lastAlertTurn: 0,
4470
+ /**
4471
+ * Per-boundary structural stability: how many consecutive turns each cache
4472
+ * source ("system_prompt", "tools", "messages_prefix") has been unchanged.
4473
+ * Used by the adaptive breakpoint placer to anchor the cache_control marker
4474
+ * on the most-stable boundary instead of blindly on the last tool (which
4475
+ * thrashes when the host reorders/adds tools between turns).
4476
+ * @type {Map<string, number>}
4477
+ */
4478
+ boundaryStability: new Map(),
4375
4479
  };
4376
4480
 
4481
+ /**
4482
+ * Update per-boundary stability counters by diffing the current request's cache
4483
+ * source hashes against the previous turn's. A boundary that is unchanged gets
4484
+ * its counter incremented; a changed (or newly-appeared) boundary resets to 0.
4485
+ *
4486
+ * @param {Map<string,string>} current - hashes for this turn (source -> hash)
4487
+ * @param {Map<string,string>} previous - hashes from the prior turn
4488
+ * @param {Map<string,number>} stability - mutated in place
4489
+ */
4490
+ function updateBoundaryStability(current, previous, stability) {
4491
+ for (const [source, hash] of current) {
4492
+ if (previous.get(source) === hash) {
4493
+ stability.set(source, (stability.get(source) || 0) + 1);
4494
+ } else {
4495
+ stability.set(source, 0);
4496
+ }
4497
+ }
4498
+ // Drop boundaries that no longer appear so stale entries don't linger.
4499
+ for (const source of [...stability.keys()]) {
4500
+ if (!current.has(source)) stability.delete(source);
4501
+ }
4502
+ }
4503
+
4377
4504
  // ---------------------------------------------------------------------------
4378
4505
  // Microcompact state (Phase 3, Task 3.4)
4379
4506
  // ---------------------------------------------------------------------------
@@ -4603,6 +4730,116 @@ function tryQuotaAwareAccountSwitch(account, accountManager, config) {
4603
4730
  * @param {string} bodyStr - JSON request body
4604
4731
  * @returns {Map<string, string>} source_id → hash
4605
4732
  */
4733
+ /**
4734
+ * Resolve the prompt-cache TTL for tool/message breakpoints, mirroring the
4735
+ * decompiled Claude Code 2.1.154 `REH(querySource)` precedence.
4736
+ *
4737
+ * Precedence (highest first):
4738
+ * 1. FORCE_PROMPT_CACHING_5M truthy -> "5m"
4739
+ * 2. ENABLE_PROMPT_CACHING_1H truthy -> "1h"
4740
+ * 3. role-scoping on AND not the main role -> "5m" (side-queries are cheap)
4741
+ * 4. otherwise -> configuredTtl (main interactive thread, default "1h")
4742
+ *
4743
+ * @param {object} args
4744
+ * @param {string} args.configuredTtl - Default TTL when 1h is eligible (usually "1h").
4745
+ * @param {boolean} args.roleScopedTtl - Whether role-scoped 5m downgrade is enabled.
4746
+ * @param {boolean} args.isMainForCache - True when the request is the main/interactive role.
4747
+ * @param {boolean} [args.isSubagent] - True when the request is an opencode subagent
4748
+ * (detected via the `x-parent-session-id` header). One-shot subagents prefer 5m.
4749
+ * @param {Record<string, string | undefined>} [args.env] - Environment (defaults to process.env).
4750
+ * @returns {string} The TTL string ("5m" or "1h"/configuredTtl).
4751
+ */
4752
+ function resolveCacheTtl({ configuredTtl, roleScopedTtl, isMainForCache, isSubagent = false, env = process.env }) {
4753
+ // Env overrides (exact CC REH() behavior). isTruthyEnv mirrors CC's uH().
4754
+ if (isTruthyEnv(env.FORCE_PROMPT_CACHING_5M)) return "5m";
4755
+ if (isTruthyEnv(env.ENABLE_PROMPT_CACHING_1H)) return "1h";
4756
+ // Subagent requests (opencode marks them with the x-parent-session-id header)
4757
+ // are usually one-shot, and opencode runs them sequentially with a DIFFERENT
4758
+ // system prompt than the main thread, so they rarely re-hit a 1h cache. Prefer
4759
+ // the cheap 5m write tier (~1.25x base) over the 1h tier (~2x base) to avoid
4760
+ // paying the long-cache write premium on a prefix that won't be reused. Gated
4761
+ // on the same role-scoped master switch as the side-query downgrade below.
4762
+ if (roleScopedTtl && isSubagent) return "5m";
4763
+ // Role-scoped downgrade: non-main (side-query) requests use the cheap 5m tier.
4764
+ if (roleScopedTtl && !isMainForCache) return "5m";
4765
+ return configuredTtl;
4766
+ }
4767
+
4768
+ /**
4769
+ * Read a single incoming request header by name (case-insensitive) from either a
4770
+ * `Request` input or a fetch `init.headers` (Headers | array | plain object).
4771
+ * Returns the trimmed value or null. Used to detect opencode's subagent marker
4772
+ * `x-parent-session-id` without consuming/normalizing the full header set.
4773
+ * @param {any} input
4774
+ * @param {any} requestInit
4775
+ * @param {string} name
4776
+ * @returns {string | null}
4777
+ */
4778
+ function getIncomingHeader(input, requestInit, name) {
4779
+ const lower = name.toLowerCase();
4780
+ const pick = (v) => (v != null && String(v).trim() !== "" ? String(v).trim() : null);
4781
+ try {
4782
+ if (input && typeof input === "object" && input.headers && typeof input.headers.get === "function") {
4783
+ const v = pick(input.headers.get(name));
4784
+ if (v) return v;
4785
+ }
4786
+ } catch {
4787
+ // malformed Request — fall through to init.headers
4788
+ }
4789
+ const h = requestInit?.headers;
4790
+ if (!h) return null;
4791
+ try {
4792
+ if (typeof h.get === "function") return pick(h.get(name));
4793
+ if (Array.isArray(h)) {
4794
+ for (const pair of h) {
4795
+ if (Array.isArray(pair) && String(pair[0]).toLowerCase() === lower) return pick(pair[1]);
4796
+ }
4797
+ return null;
4798
+ }
4799
+ for (const [k, v] of Object.entries(h)) {
4800
+ if (k.toLowerCase() === lower) return pick(v);
4801
+ }
4802
+ } catch {
4803
+ // unexpected header shape — treat as absent
4804
+ }
4805
+ return null;
4806
+ }
4807
+
4808
+ /**
4809
+ * Decide whether to place the prompt-cache breakpoint on the last tool.
4810
+ *
4811
+ * Default (no stability data, or detector disabled): TRUE — exact Claude Code
4812
+ * behavior (breakpoint on the last tool).
4813
+ *
4814
+ * Adaptive override: returns FALSE only when there is concrete evidence that the
4815
+ * tool boundary is thrashing (a `tool:*` source changed within the last turn,
4816
+ * i.e. stability 0) WHILE the system-prompt boundary is stable (unchanged for
4817
+ * >= STABLE_TURNS turns). In that case the stable system-prompt breakpoint is a
4818
+ * better cache anchor than a tool array that moves every turn.
4819
+ *
4820
+ * @param {Map<string, number> | null | undefined} stability - source -> consecutive-unchanged turns
4821
+ * @returns {boolean} whether to stamp cache_control on the last tool
4822
+ */
4823
+ function shouldPlaceToolBreakpoint(stability) {
4824
+ if (!stability || stability.size === 0) return true; // no data -> CC behavior
4825
+ const STABLE_TURNS = 2;
4826
+ const systemStability = stability.get("system_prompt");
4827
+ const systemIsStable = typeof systemStability === "number" && systemStability >= STABLE_TURNS;
4828
+ if (!systemIsStable) return true; // system not proven stable -> keep CC behavior
4829
+
4830
+ // Is any tool boundary thrashing right now (changed within the last turn)?
4831
+ let toolThrashing = false;
4832
+ for (const [source, turns] of stability) {
4833
+ if (source.startsWith("tool:") && turns === 0) {
4834
+ toolThrashing = true;
4835
+ break;
4836
+ }
4837
+ }
4838
+ // Skip the last-tool breakpoint only when tools thrash AND system is the
4839
+ // stable anchor. Otherwise fall back to CC behavior.
4840
+ return !toolThrashing;
4841
+ }
4842
+
4606
4843
  function extractCacheSourceHashes(bodyStr, parsedBody = undefined) {
4607
4844
  const hashes = new Map();
4608
4845
  try {
@@ -5141,6 +5378,11 @@ function forceEscalateAdaptiveContext() {
5141
5378
  }
5142
5379
 
5143
5380
  const MODEL_PRICING = {
5381
+ // Opus 4.8 (launched 2026-05-28): $5/$25 per 1M (input/output), notably
5382
+ // cheaper than 4.6/4.7. cacheRead = 0.1x input, cacheWrite = 1.25x input
5383
+ // (Anthropic's standard 5m-write ratio).
5384
+ "claude-opus-4-8": { input: 5.0, output: 25.0, cacheRead: 0.5, cacheWrite: 6.25 },
5385
+ "claude-opus-4-7": { input: 15.0, output: 75.0, cacheRead: 1.5, cacheWrite: 18.75 },
5144
5386
  "claude-opus-4-6": { input: 15.0, output: 75.0, cacheRead: 1.5, cacheWrite: 18.75 },
5145
5387
  "claude-sonnet-4-6": { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },
5146
5388
  "claude-haiku-4-5": { input: 0.8, output: 4.0, cacheRead: 0.08, cacheWrite: 1.0 },
@@ -6245,8 +6487,8 @@ function extractFirstUserMessageText(messages) {
6245
6487
  }
6246
6488
 
6247
6489
  const CLAUDE_CODE_BETA_FLAG = "claude-code-20250219";
6248
- const EFFORT_BETA_FLAG = "effort-2025-11-24";
6249
- const ADVANCED_TOOL_USE_BETA_FLAG = "advanced-tool-use-2025-11-20";
6490
+ const _EFFORT_BETA_FLAG = "effort-2025-11-24";
6491
+ const _ADVANCED_TOOL_USE_BETA_FLAG = "advanced-tool-use-2025-11-20";
6250
6492
  const FAST_MODE_BETA_FLAG = "fast-mode-2026-02-01";
6251
6493
  const TOKEN_COUNTING_BETA_FLAG = "token-counting-2024-11-01";
6252
6494
  const CLAUDE_CODE_IDENTITY_STRING = "You are Claude Code, Anthropic's official CLI for Claude.";
@@ -6481,6 +6723,20 @@ function isOpus47Model(model) {
6481
6723
  return /claude-opus-4[._-]7|opus[._-]4[._-]7/i.test(model);
6482
6724
  }
6483
6725
 
6726
+ /**
6727
+ * Detects claude-opus-4.8 / claude-opus-4-8 model IDs.
6728
+ * Opus 4.8 (launched 2026-05-28) is an adaptive-thinking model: manual
6729
+ * `thinking: {type: "enabled", budget_tokens}` returns a 400 — it MUST use
6730
+ * `thinking: {type: "adaptive"}` + the effort parameter. It also supports
6731
+ * `speed: "fast"` (fast-mode research preview) and 1M context by default.
6732
+ * @param {string | undefined} model
6733
+ * @returns {boolean}
6734
+ */
6735
+ function isOpus48Model(model) {
6736
+ if (!model) return false;
6737
+ return /claude-opus-4[._-]8|opus[._-]4[._-]8/i.test(model);
6738
+ }
6739
+
6484
6740
  /**
6485
6741
  * Models eligible for the "simple system prompt" mode that real CC ships in
6486
6742
  * v2.1.133+ under GrowthBook flag `tengu_vellum_lantern` (or forced via
@@ -6495,7 +6751,7 @@ function isOpus47Model(model) {
6495
6751
  */
6496
6752
  function isSimpleSystemPromptEligible(model) {
6497
6753
  if (!model) return false;
6498
- if (isOpus47Model(model)) return true;
6754
+ if (isOpus47Model(model) || isOpus48Model(model)) return true;
6499
6755
  // -eap suffix variant, e.g. "claude-opus-4-7-eap" or "claude-opus-4-7-eap[1m]"
6500
6756
  if (/-eap(?:\[|$|-)/i.test(model)) return true;
6501
6757
  return false;
@@ -6513,12 +6769,12 @@ function isSonnet46Model(model) {
6513
6769
 
6514
6770
  /**
6515
6771
  * Detects models that support adaptive thinking ({type: "adaptive"}).
6516
- * Currently: Opus 4.6, Opus 4.7, and Sonnet 4.6.
6772
+ * Currently: Opus 4.6, Opus 4.7, Opus 4.8, and Sonnet 4.6.
6517
6773
  * @param {string | undefined} body
6518
6774
  * @returns {boolean}
6519
6775
  */
6520
6776
  function isAdaptiveThinkingModel(model) {
6521
- return isOpus46Model(model) || isOpus47Model(model) || isSonnet46Model(model);
6777
+ return isOpus46Model(model) || isOpus47Model(model) || isOpus48Model(model) || isSonnet46Model(model);
6522
6778
  }
6523
6779
 
6524
6780
  /**
@@ -6533,8 +6789,11 @@ function isEligibleFor1MContext(model) {
6533
6789
  // Explicit 1m suffix/tag in model name
6534
6790
  if (/(^|[-_ ])1m($|[-_ ])|context[-_]?1m|\[1m\]/i.test(model)) return true;
6535
6791
  // CC v2.1.97 U01: claude-sonnet-4* (any Sonnet 4.x) or opus-4-6.
6536
- // Opus 4.7 (successor to 4.6) is also 1M-context eligible.
6537
- return /claude-sonnet-4|sonnet[._-]4/i.test(model) || isOpus46Model(model) || isOpus47Model(model);
6792
+ // Opus 4.7 / 4.8 (successors to 4.6) are also 1M-context eligible.
6793
+ // Opus 4.8 ships with 1M context by default on the Claude API.
6794
+ return (
6795
+ /claude-sonnet-4|sonnet[._-]4/i.test(model) || isOpus46Model(model) || isOpus47Model(model) || isOpus48Model(model)
6796
+ );
6538
6797
  }
6539
6798
 
6540
6799
  /**
@@ -7309,6 +7568,11 @@ function buildSystemPromptBlocks(system, signature) {
7309
7568
  * @param {string} [requestPath]
7310
7569
  * @param {boolean} [hasFileReferences]
7311
7570
  * @param {{ use1MContext?: boolean }} [adaptiveOverride] - When set, overrides the static hasOneMillionContext() check.
7571
+ * @param {boolean} [fastModeActive] - When true, emits FAST_MODE_BETA_FLAG (fast-mode-2026-02-01).
7572
+ * Must be derived structurally from the already-transformed outgoing body (body.includes('"speed":"fast"')).
7573
+ * Only passed from the buildRequestHeaders call site (after body transform). The pre-transform
7574
+ * computedBetaHeader call site (index.mjs:2905) leaves this undefined so the latch never
7575
+ * accumulates fast-mode independently of actual body content.
7312
7576
  * @returns {string}
7313
7577
  */
7314
7578
  function buildAnthropicBetaHeader(
@@ -7323,6 +7587,7 @@ function buildAnthropicBetaHeader(
7323
7587
  adaptiveOverride,
7324
7588
  tokenEconomy,
7325
7589
  microcompactBetas, // NEW 11th param
7590
+ fastModeActive, // NEW 12th param — see @param above
7326
7591
  ) {
7327
7592
  const incomingBetasList = incomingBeta
7328
7593
  .split(",")
@@ -7358,24 +7623,12 @@ function buildAnthropicBetaHeader(
7358
7623
  // behavior from the Anthropic API.
7359
7624
  betas.push(CLAUDE_CODE_BETA_FLAG); // "claude-code-20250219"
7360
7625
 
7361
- // Tool search: use provider-aware header.
7362
- // 1P/Foundry u2192 advanced-tool-use-2025-11-20 (enables broader tool capabilities)
7363
- // Vertex/Bedrock u2192 tool-search-tool-2025-10-19 (3P-compatible subset)
7364
- if (provider === "vertex" || provider === "bedrock" || provider === "mantle") {
7365
- betas.push("tool-search-tool-2025-10-19");
7366
- } else {
7367
- betas.push(ADVANCED_TOOL_USE_BETA_FLAG); // "advanced-tool-use-2025-11-20"
7368
- }
7626
+ // v2.1.150: advanced-tool-use / tool-search-tool removed from always-on.
7627
+ // CC 2.1.150 D5q does not push these per-request. Available via custom betas.
7369
7628
 
7370
- betas.push(FAST_MODE_BETA_FLAG); // "fast-mode-2026-02-01"
7629
+ // v2.1.150: fast-mode-2026-02-01 removed from always-on (CC sends only when speed feature active).
7371
7630
 
7372
- // effort-2025-11-24 — real CC's Lyz() only pushes this flag when rE(model)
7373
- // is true (Opus 4.6 / Sonnet 4.6). Pushing it for non-adaptive models like
7374
- // Haiku is a fingerprint mismatch vs real CC and can contaminate billing
7375
- // attribution even when the request body has no effort field.
7376
- if (isAdaptiveThinkingModel(model)) {
7377
- betas.push(EFFORT_BETA_FLAG); // "effort-2025-11-24"
7378
- }
7631
+ // v2.1.150: effort-2025-11-24 removed from always-on (CC sends only when effort is explicitly requested).
7379
7632
 
7380
7633
  // Interleaved thinking — real CC's i01 pushes via hv4(model), which is
7381
7634
  // (firstParty && non-Claude-3). Claude 3.x models don't support interleaved
@@ -7400,18 +7653,20 @@ function buildAnthropicBetaHeader(
7400
7653
  if (!isRoundRobin) {
7401
7654
  betas.push("prompt-caching-scope-2026-01-05");
7402
7655
  }
7656
+ // v2.1.150: extended-cache-ttl for better cache hit rates (opt-out via token_economy.extended_cache_ttl = false).
7657
+ if (te.extended_cache_ttl !== false && !isRoundRobin) {
7658
+ betas.push("extended-cache-ttl-2025-04-11");
7659
+ }
7403
7660
 
7404
7661
  // === CONDITIONAL BETAS (model/context-dependent) ===
7405
7662
 
7406
- // Context management — gated to Claude 4+ models in CC v2.1.90.
7407
- // Excluded for Claude 3.x (not supported). Always-on for Claude 4+ on 1P/Foundry.
7408
- if (!/claude-3-/i.test(model)) {
7663
+ // v2.1.150: context-management gated behind opt-in (CC has hardcoded && false).
7664
+ if (te.context_management && !/claude-3-/i.test(model)) {
7409
7665
  betas.push("context-management-2025-06-27");
7410
7666
  }
7411
7667
 
7412
- // Structured outputs: only -2025-12-15 is active in v2.1.90 runtime.
7413
- // token-efficient-tools-2026-03-28 was fully removed from v90 bundle.
7414
- if (supportsStructuredOutputs(model)) {
7668
+ // v2.1.150: structured-outputs gated behind opt-in (CC has behind tengu_tool_pear flag).
7669
+ if (te.structured_outputs && supportsStructuredOutputs(model)) {
7415
7670
  betas.push("structured-outputs-2025-12-15");
7416
7671
  }
7417
7672
 
@@ -7445,12 +7700,6 @@ function buildAnthropicBetaHeader(
7445
7700
  betas.push("context-hint-2026-04-09");
7446
7701
  }
7447
7702
 
7448
- // extended-cache-ttl-2025-04-11 — found in CC v2.1.143. Extends server-side
7449
- // prompt cache TTL. Auto-on for 1P + Claude 4+ to improve cache hit rates.
7450
- if (isFirstPartyProvider && !/claude-3-/i.test(model)) {
7451
- betas.push("extended-cache-ttl-2025-04-11");
7452
- }
7453
-
7454
7703
  // Files API — scoped to file endpoints/references
7455
7704
  if (isFilesEndpoint || hasFileReferences) {
7456
7705
  betas.push("files-api-2025-04-14");
@@ -7463,13 +7712,18 @@ function buildAnthropicBetaHeader(
7463
7712
 
7464
7713
  // === TOKEN ECONOMY BETAS (on by default for token savings) ===
7465
7714
 
7466
- // redact-thinking: suppresses thinking summaries server-side.
7467
- // CC v108 enables this by default but we keep it off so thinking is visible.
7468
- // Users can opt in via `/anthropic set redact-thinking on`.
7469
- if (te.redact_thinking && !disableExperimentalBetas) {
7715
+ // v2.1.150: redact-thinking is default-ON in CC (first-party, non-SDK, thinking models).
7716
+ // Opt out via `/anthropic set redact-thinking off` to see thinking content.
7717
+ if (te.redact_thinking !== false && !disableExperimentalBetas && !/claude-3-/i.test(model)) {
7470
7718
  betas.push("redact-thinking-2026-02-12");
7471
7719
  }
7472
7720
 
7721
+ // v2.1.150: thinking-token-count for token budget tracking (behind tengu_chert_bezel in CC).
7722
+ // Default ON for token economy visibility. Opt out via token_economy.thinking_token_count = false.
7723
+ if (te.thinking_token_count !== false && !disableExperimentalBetas && !/claude-3-/i.test(model)) {
7724
+ betas.push("thinking-token-count-2026-05-13");
7725
+ }
7726
+
7473
7727
  // compact-2026-01-12 and mcp-client-2025-11-20 exist only in docs, not runtime.
7474
7728
 
7475
7729
  // afk-mode — NOT auto-included (requires user opt-in)
@@ -7482,6 +7736,16 @@ function buildAnthropicBetaHeader(
7482
7736
  }
7483
7737
  }
7484
7738
 
7739
+ // Fast-mode beta — the PLUGIN injects speed:"fast" into the body, not the host.
7740
+ // Because the host never sends speed:"fast", it also never sends the matching beta.
7741
+ // We must add it here, derived structurally from the already-transformed body so
7742
+ // it is always in lockstep with the speed field. Added BEFORE the dedupe merge so
7743
+ // any duplicate (e.g. via incoming passthrough) is collapsed without duplication.
7744
+ // Unlike effort-2025-11-24 / advanced-tool-use / tool-search-tool (which arrive via
7745
+ // incoming-header passthrough because the HOST sets those body features), fast-mode
7746
+ // is plugin-only and requires this explicit push.
7747
+ if (fastModeActive) betas.push(FAST_MODE_BETA_FLAG);
7748
+
7485
7749
  // Merge incoming betas from the original request, filtering out host-injected
7486
7750
  // betas (e.g. fine-grained-tool-streaming-2025-05-14, structured-outputs-2025-11-13)
7487
7751
  // that OpenCode's Anthropic SDK adds but real Claude Code never sends.
@@ -7534,7 +7798,9 @@ function budgetTokensToEffort(budgetTokens) {
7534
7798
 
7535
7799
  /**
7536
7800
  * Normalise the `thinking` block in the request body for the target model:
7537
- * - Opus 4.6 / Sonnet 4.6 (adaptive thinking): produces `{ type: "adaptive" }`
7801
+ * - Opus 4.6 / 4.7 / 4.8 / Sonnet 4.6 (adaptive thinking): produces
7802
+ * `{ type: "adaptive" }`. This is REQUIRED for Opus 4.7/4.8 — manual
7803
+ * `{ type: "enabled", budget_tokens }` returns a 400 on those models.
7538
7804
  * - Older models: passes the existing thinking block through unchanged.
7539
7805
  *
7540
7806
  * @param {any} thinking
@@ -7667,6 +7933,14 @@ function buildRequestHeaders(
7667
7933
  const incomingBeta = requestHeaders.get("anthropic-beta") || "";
7668
7934
  const { model, tools, messages, hasFileReferences } = parseRequestBodyMetadata(requestBody);
7669
7935
  const provider = detectProvider(requestUrl);
7936
+ // Detect fast mode structurally from the already-transformed body string.
7937
+ // transformRequestBody runs before buildRequestHeaders (see index.mjs:2986 vs 3039),
7938
+ // so speed:"fast" is already present when fast mode fired. Same detection idiom as the
7939
+ // toast check (~line 3018). This is the correct call site for fast-mode: the
7940
+ // computedBetaHeader call at index.mjs:2905 runs BEFORE body transform, so fastModeActive
7941
+ // is always false/absent there, keeping the latch free of fast-mode. The
7942
+ // requestHeaders.set("anthropic-beta", mergedBetas) call below is what reaches the wire.
7943
+ const fastModeActive = typeof requestBody === "string" && requestBody.includes('"speed":"fast"');
7670
7944
  const mergedBetas = buildAnthropicBetaHeader(
7671
7945
  incomingBeta,
7672
7946
  signature.enabled,
@@ -7678,6 +7952,8 @@ function buildRequestHeaders(
7678
7952
  hasFileReferences,
7679
7953
  adaptiveOverride,
7680
7954
  tokenEconomy,
7955
+ undefined, // microcompactBetas: not available at this call site (handled via computedBetaHeader path at 2905)
7956
+ fastModeActive, // NEW 12th param: emit fast-mode-2026-02-01 beta when body contains speed:"fast"
7681
7957
  );
7682
7958
 
7683
7959
  const authTokenOverride = process.env.ANTHROPIC_AUTH_TOKEN?.trim();
@@ -7704,7 +7980,7 @@ function buildRequestHeaders(
7704
7980
  // the minifier identifier renamed, d66 → g86). Re-verify on every upstream bump:
7705
7981
  // rg -n '"0\.\d+\.\d+"' _tmp_claude_pkg/<version>/package/cli.js | rg -C2 stainless
7706
7982
  // See docs/future-improvements.md §7 and claude-code-reverse-engineering.md §16.
7707
- requestHeaders.set("x-stainless-package-version", "0.81.0");
7983
+ requestHeaders.set("x-stainless-package-version", "0.94.0");
7708
7984
  // Real CC on Windows/Node reports "node" — confirmed via proxy capture.
7709
7985
  requestHeaders.set("x-stainless-runtime", "node");
7710
7986
  requestHeaders.set("x-stainless-runtime-version", process.version);
@@ -7792,6 +8068,21 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
7792
8068
 
7793
8069
  try {
7794
8070
  const parsed = JSON.parse(body);
8071
+ // Diagnostic: log the incoming model + thinking/speed shape so model-routing
8072
+ // issues (e.g. an unrecognized fast variant, or "No available account"
8073
+ // symptoms tied to a specific model) are observable with /anthropic set debug on.
8074
+ // NOTE: transformRequestBody is a top-level function outside the plugin
8075
+ // closure where debugLog() lives, so we mirror debugLog's behaviour directly
8076
+ // (console.error + same prefix, gated on config.debug).
8077
+ if (config?.debug) {
8078
+ console.error("[opencode-anthropic-auth]", "transformRequestBody: incoming model", {
8079
+ model: parsed.model,
8080
+ adaptive: isAdaptiveThinkingModel(parsed.model || ""),
8081
+ thinkingType: parsed.thinking?.type,
8082
+ hasEffort: parsed.effort !== undefined || parsed.output_config?.effort !== undefined,
8083
+ speed: parsed.speed,
8084
+ });
8085
+ }
7795
8086
  // Output cap: resolve max_tokens before any other body transforms
7796
8087
  if (config?.output_cap?.enabled) {
7797
8088
  parsed.max_tokens = resolveMaxTokens(parsed, config);
@@ -7863,10 +8154,19 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
7863
8154
  // Anthropic API rejects temperature when thinking is enabled
7864
8155
  delete parsed.temperature;
7865
8156
 
7866
- // Claude Code v2.1.84: inject context_management body field when thinking
7867
- // is active and context-management beta is in use. This tells the API how
7868
- // to handle thinking blocks during context management operations.
7869
- if (!parsed.context_management) {
8157
+ // Claude Code v2.1.84: inject the context_management body field ONLY when the
8158
+ // context-management beta is actually enabled (token_economy.context_management,
8159
+ // the SAME flag that gates the beta in buildAnthropicBetaHeader). A top-level
8160
+ // context_management field WITHOUT the beta is rejected by the API with
8161
+ // "context_management: Extra inputs are not permitted". Keeping the field and
8162
+ // the anthropic-beta header in lockstep prevents that 400 — previously the
8163
+ // field was injected on every thinking turn regardless of the beta, which only
8164
+ // worked if the beta happened to be forced on via custom_betas/ANTHROPIC_BETAS.
8165
+ if (
8166
+ config?.token_economy?.context_management &&
8167
+ !/claude-3-/i.test(parsed.model || "") &&
8168
+ !parsed.context_management
8169
+ ) {
7870
8170
  parsed.context_management = {
7871
8171
  edits: [{ type: "clear_thinking_20251015", keep: "all" }],
7872
8172
  };
@@ -8084,31 +8384,68 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
8084
8384
  // our system prompt ttl=1h). Then add our own to the last user message
8085
8385
  // (matching real CC behavior seen in proxy capture).
8086
8386
  //
8087
- // Role-scoped TTL: real CC's MoY(querySource) gates the `ttl:"1h"` field
8088
- // on an allowlist of query sources (`repl_main_thread*`, `sdk`,
8089
- // `auto_mode`). Non-matching requests fall back to the default 5m tier
8090
- // — which is cheaper to write. We match this via classifyRequestRole
8091
- // (main → configured TTL, usually 1h; everything else → 5m).
8387
+ // Role-scoped TTL: real CC's REH(querySource) decides the `ttl:"1h"` field.
8388
+ // Decompiled from CC 2.1.154 (function REH, offset 225174828), precedence:
8389
+ // 1. FORCE_PROMPT_CACHING_5M => 5m (highest priority)
8390
+ // 2. ENABLE_PROMPT_CACHING_1H => 1h
8391
+ // 3. no tengu support / overage => 5m
8392
+ // 4. querySource ∈ allowlist [repl_main_thread*, sdk, auto_mode, ...] => 1h, else 5m
8393
+ // We map step 4 via classifyRequestRole: main (interactive/auto_mode thread)
8394
+ // → 1h; side-queries (title-gen, etc.) → 5m. The env vars are honored
8395
+ // exactly as CC does, giving the user a manual override + mimicry fidelity.
8092
8396
  const configuredTtl = signature.cachePolicy?.ttl || "1h";
8093
8397
  const roleScopedTtl = config?.token_economy?.role_scoped_cache_ttl !== false;
8094
8398
  const isMainForCache = runtime?.requestRole === "main" || runtime?.requestRole == null;
8095
- const ccTtl = roleScopedTtl && !isMainForCache ? "5m" : configuredTtl;
8399
+ const isSubagentForCache = runtime?.isSubagent === true;
8400
+ const ccTtl = resolveCacheTtl({
8401
+ configuredTtl,
8402
+ roleScopedTtl,
8403
+ isMainForCache,
8404
+ isSubagent: isSubagentForCache,
8405
+ env: process.env,
8406
+ });
8096
8407
  if (Array.isArray(parsed.tools) && parsed.tools.length > 0) {
8097
8408
  for (const tool of parsed.tools) {
8098
8409
  if (tool.cache_control) delete tool.cache_control;
8099
8410
  }
8100
- // Add cache_control to last tool as prompt-cache breakpoint (CC does this)
8101
- parsed.tools[parsed.tools.length - 1].cache_control = { type: "ephemeral", ttl: ccTtl };
8411
+ // Adaptive breakpoint placement (opt-in, runtime-driven). Real CC always
8412
+ // pins the breakpoint on the last tool because CC controls a STABLE tool
8413
+ // array. opencode may reorder/add/remove tools between turns, which moves
8414
+ // the "last tool" and invalidates the cached prefix after it. When the
8415
+ // detector reports the tool boundary is thrashing while the system prompt
8416
+ // is stable, we SKIP the volatile last-tool breakpoint and let the stable
8417
+ // system-prompt breakpoint (added by buildSystemPromptBlocks) anchor the
8418
+ // cache. Falls back to exact CC behavior when no stability data exists.
8419
+ const placeToolBreakpoint = shouldPlaceToolBreakpoint(runtime?.cacheBoundaryStability);
8420
+ if (placeToolBreakpoint) {
8421
+ parsed.tools[parsed.tools.length - 1].cache_control = { type: "ephemeral", ttl: ccTtl };
8422
+ }
8102
8423
  }
8103
8424
  if (Array.isArray(parsed.messages)) {
8104
8425
  for (const msg of parsed.messages) {
8105
8426
  if (Array.isArray(msg.content)) {
8106
8427
  for (const block of msg.content) {
8428
+ // CONTRACT GUARD: `thinking` / `redacted_thinking` blocks MUST be
8429
+ // round-tripped byte-identical to the model's original response
8430
+ // (Anthropic extended-thinking rules). ANY mutation — including
8431
+ // `delete block.cache_control` — triggers the 400 error
8432
+ // "thinking or redacted_thinking blocks in the latest assistant
8433
+ // message cannot be modified". `cache_control` is not even a valid
8434
+ // field on a thinking block, so we never strip nor add it here;
8435
+ // we leave the block exactly as received. This is required for
8436
+ // adaptive-thinking models (Opus 4.6/4.7/4.8, Sonnet 4.6) on
8437
+ // tool-continuation turns. See docs/mimese-http-header-system-prompt.md.
8438
+ if (!block || typeof block !== "object") continue;
8439
+ if (block.type === "thinking" || block.type === "redacted_thinking") {
8440
+ continue;
8441
+ }
8107
8442
  if (block.cache_control) delete block.cache_control;
8108
8443
  }
8109
8444
  }
8110
8445
  }
8111
- // Add cache_control to last user message (real CC does this)
8446
+ // Add cache_control to last user message (real CC does this).
8447
+ // (User messages never contain thinking blocks, so the guard above
8448
+ // does not affect this breakpoint.)
8112
8449
  for (let i = parsed.messages.length - 1; i >= 0; i--) {
8113
8450
  const msg = parsed.messages[i];
8114
8451
  if (msg.role !== "user" || !Array.isArray(msg.content) || msg.content.length === 0) continue;
@@ -8239,12 +8576,18 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
8239
8576
  parsed.context_hint = { enabled: true };
8240
8577
  }
8241
8578
 
8242
- // Fast mode: inject speed parameter for Opus 4.6 only (v2.1.97 restriction).
8243
- // Real CC v2.1.97 xJ() checks: model.includes("opus-4-6") — Sonnet is NOT eligible.
8244
- // NOTE: Deliberately NOT extended to Opus 4.7. xJ() is a fingerprint-sensitive
8245
- // check tied to the exact real-CC version we mirror; blindly enabling
8246
- // speed:"fast" for 4.7 could diverge from real CC's behavior for that model.
8247
- // Revisit once a real-CC dump confirms xJ() matches opus-4-7.
8579
+ // Fast mode: inject speed parameter for fast-mode-eligible models.
8580
+ // Per Anthropic fast-mode docs, `speed: "fast"` (beta fast-mode-2026-02-01) is
8581
+ // supported on Opus 4.6, Opus 4.7, and Opus 4.8 (research preview). Opus 4.7
8582
+ // is the /fast default in real Claude Code v2.1.142+. Sonnet is NOT eligible.
8583
+ // NOTE: switching speed invalidates system + message prompt caches (per
8584
+ // Anthropic fast-mode docs), so only flip it deliberately.
8585
+ // When the selected account's fast pool is cooling down (a prior fast 429),
8586
+ // suppress speed:"fast" entirely so this turn runs at standard speed on the
8587
+ // same account rather than re-hitting the exhausted fast pool.
8588
+ const fastPoolAvailable = !signature.fastRateLimited;
8589
+ const isFastModeEligibleModel = (m) =>
8590
+ fastPoolAvailable && (isOpus46Model(m) || isOpus47Model(m) || isOpus48Model(m));
8248
8591
  const fastModeEnabled = signature.fastMode && !isFalsyEnv(process.env.OPENCODE_ANTHROPIC_DISABLE_FAST_MODE);
8249
8592
  let fastModeAutoApplied = false;
8250
8593
  if (
@@ -8252,7 +8595,7 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
8252
8595
  te.fast_mode_auto === true &&
8253
8596
  isMainRole &&
8254
8597
  parsed.model &&
8255
- isOpus46Model(parsed.model) &&
8598
+ isFastModeEligibleModel(parsed.model) &&
8256
8599
  Array.isArray(parsed.messages) &&
8257
8600
  parsed.messages.length >= 2
8258
8601
  ) {
@@ -8275,7 +8618,7 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
8275
8618
  }
8276
8619
  }
8277
8620
  }
8278
- if ((fastModeEnabled || fastModeAutoApplied) && parsed.model && isOpus46Model(parsed.model)) {
8621
+ if ((fastModeEnabled || fastModeAutoApplied) && parsed.model && isFastModeEligibleModel(parsed.model)) {
8279
8622
  parsed.speed = "fast";
8280
8623
  }
8281
8624
 
@@ -8583,6 +8926,32 @@ function stripMcpPrefixFromParsedEvent(parsed) {
8583
8926
  return modified;
8584
8927
  }
8585
8928
 
8929
+ /**
8930
+ * Resolve the byte-stream idle timeout in milliseconds.
8931
+ *
8932
+ * Parity with Claude Code's `tengu_byte_stream_idle_timeout_ms`: a watchdog that
8933
+ * fires when the upstream SSE stream produces no bytes for N ms, so a stalled /
8934
+ * half-dead connection surfaces as a fast error instead of an indefinite hang.
8935
+ *
8936
+ * Resolution order: env `OPENCODE_ANTHROPIC_STREAM_IDLE_TIMEOUT_MS` →
8937
+ * `config.streaming.idle_timeout_ms` → 0 (disabled). Disabled by default because
8938
+ * a too-aggressive value could abort a legitimate long generation; Anthropic SSE
8939
+ * emits periodic `ping` events, so a multi-minute silence is the safe signal.
8940
+ *
8941
+ * @param {any} cfg
8942
+ * @returns {number} timeout in ms, or 0 to disable
8943
+ */
8944
+ function resolveStreamIdleTimeoutMs(cfg) {
8945
+ const envRaw = process.env.OPENCODE_ANTHROPIC_STREAM_IDLE_TIMEOUT_MS;
8946
+ if (envRaw != null && envRaw !== "") {
8947
+ const n = Number(envRaw);
8948
+ if (Number.isFinite(n) && n >= 0) return Math.floor(n);
8949
+ }
8950
+ const c = cfg?.streaming?.idle_timeout_ms;
8951
+ if (typeof c === "number" && Number.isFinite(c) && c >= 0) return Math.floor(c);
8952
+ return 0;
8953
+ }
8954
+
8586
8955
  /**
8587
8956
  * Wrap a response body stream to strip mcp_ prefix from tool names,
8588
8957
  * extract token usage stats from SSE events, and detect mid-stream
@@ -8603,6 +8972,10 @@ function transformResponse(response, onUsage, onAccountError) {
8603
8972
  const encoder = new TextEncoder();
8604
8973
  const EMPTY_CHUNK = new Uint8Array();
8605
8974
 
8975
+ // Byte-stream idle-timeout watchdog (parity with CC tengu_byte_stream_idle_timeout_ms).
8976
+ // 0 = disabled (default). See resolveStreamIdleTimeoutMs.
8977
+ const idleTimeoutMs = resolveStreamIdleTimeoutMs(_pluginConfig);
8978
+
8606
8979
  /** @type {UsageStats} */
8607
8980
  const stats = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
8608
8981
  let sseBuffer = "";
@@ -8682,7 +9055,38 @@ function transformResponse(response, onUsage, onAccountError) {
8682
9055
 
8683
9056
  const stream = new ReadableStream({
8684
9057
  async pull(controller) {
8685
- const { done, value } = await reader.read();
9058
+ let readResult;
9059
+ if (idleTimeoutMs > 0) {
9060
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
9061
+ let idleTimer;
9062
+ try {
9063
+ readResult = await Promise.race([
9064
+ reader.read(),
9065
+ new Promise((_resolve, reject) => {
9066
+ idleTimer = setTimeout(
9067
+ () => reject(new Error(`stream idle timeout: no bytes for ${idleTimeoutMs}ms`)),
9068
+ idleTimeoutMs,
9069
+ );
9070
+ }),
9071
+ ]);
9072
+ } catch (err) {
9073
+ // Idle timeout (or read error): cancel the upstream reader and surface a
9074
+ // clear error so the consumer can retry instead of waiting on a dead
9075
+ // connection. Mirrors CC's stream-idle watchdog (no silent hang).
9076
+ try {
9077
+ await reader.cancel();
9078
+ } catch {
9079
+ // Reader may already be released; nothing to do.
9080
+ }
9081
+ controller.error(err instanceof Error ? err : new Error(String(err)));
9082
+ return;
9083
+ } finally {
9084
+ clearTimeout(idleTimer);
9085
+ }
9086
+ } else {
9087
+ readResult = await reader.read();
9088
+ }
9089
+ const { done, value } = readResult;
8686
9090
  if (done) {
8687
9091
  processSSEBuffer(true);
8688
9092
 
@@ -9034,6 +9438,8 @@ AnthropicAuthPlugin.__testing__ = {
9034
9438
  buildSystemPromptBlocks,
9035
9439
  stripMcpPrefixFromParsedEvent,
9036
9440
  CORE_TOOL_NAMES,
9441
+ // exposed for subagent-detection tests (x-parent-session-id header extraction)
9442
+ getIncomingHeader,
9037
9443
  // exposed for determinism regression tests (phase C1)
9038
9444
  applyContextHintCompaction,
9039
9445
  // exposed for session-dedupe regression tests (phase C3)
@@ -9069,4 +9475,19 @@ AnthropicAuthPlugin.__testing__ = {
9069
9475
  },
9070
9476
  };
9071
9477
 
9478
+ /**
9479
+ * Internal cache helpers exposed for unit testing only. Not part of the public
9480
+ * plugin API. Attached as a PROPERTY of the function for the SAME reason as
9481
+ * `__testing__` above: a bare `export const __cacheInternals = {...}` object
9482
+ * export breaks Opencode's plugin loader, which iterates `Object.values(mod)`
9483
+ * and throws "Plugin export is not a function" on ANY non-function export —
9484
+ * silently disabling the whole plugin (no slash command, no OAuth). Tests reach
9485
+ * these via `AnthropicAuthPlugin.__cacheInternals`.
9486
+ */
9487
+ AnthropicAuthPlugin.__cacheInternals = {
9488
+ resolveCacheTtl,
9489
+ shouldPlaceToolBreakpoint,
9490
+ updateBoundaryStability,
9491
+ };
9492
+
9072
9493
  export default AnthropicAuthPlugin;
package/lib/accounts.mjs CHANGED
@@ -47,6 +47,16 @@ function hashTokenFragment(token) {
47
47
 
48
48
  const MAX_ACCOUNTS = 10;
49
49
  const RATE_LIMIT_KEY = "anthropic";
50
+ // Fast mode (speed:"fast") has a SEPARATE rate-limit pool from standard Opus
51
+ // per Anthropic docs. We track it under a distinct bucket so that exhausting the
52
+ // fast pool does NOT block standard-speed requests on the same account (it should
53
+ // only trigger a fallback to standard speed). The key lives in the same
54
+ // rateLimitResetTimes record, so no schema migration is needed — existing
55
+ // accounts simply won't have this key until they hit a fast-mode rate limit, and
56
+ // #clearExpiredRateLimits already iterates all keys generically.
57
+ const RATE_LIMIT_KEY_FAST = "anthropic:fast";
58
+
59
+ export { RATE_LIMIT_KEY, RATE_LIMIT_KEY_FAST };
50
60
 
51
61
  /**
52
62
  * @typedef {object} StatsDelta
@@ -351,16 +361,29 @@ export class AccountManager {
351
361
  }
352
362
 
353
363
  /**
354
- * Check if an account is currently rate-limited.
364
+ * Check if an account is currently rate-limited for the given bucket.
365
+ * Defaults to the STANDARD bucket so that a fast-only rate limit never makes
366
+ * an account look unavailable for standard requests.
355
367
  * @param {ManagedAccount} account
368
+ * @param {string} [bucket] - Rate-limit bucket key (default: standard).
356
369
  * @returns {boolean}
357
370
  */
358
- #isRateLimited(account) {
371
+ #isRateLimited(account, bucket = RATE_LIMIT_KEY) {
359
372
  this.#clearExpiredRateLimits(account);
360
- const resetTime = account.rateLimitResetTimes[RATE_LIMIT_KEY];
373
+ const resetTime = account.rateLimitResetTimes[bucket];
361
374
  return resetTime !== undefined && Date.now() < resetTime;
362
375
  }
363
376
 
377
+ /**
378
+ * Public check: is this account rate-limited for the fast-mode pool?
379
+ * Used to decide whether to inject speed:"fast" on a request.
380
+ * @param {ManagedAccount} account
381
+ * @returns {boolean}
382
+ */
383
+ isFastRateLimited(account) {
384
+ return this.#isRateLimited(account, RATE_LIMIT_KEY_FAST);
385
+ }
386
+
364
387
  /**
365
388
  * Select the best account for the current request.
366
389
  * @param {Set<number>} [excludedIndices] - Temporary per-request exclusions
@@ -408,27 +431,43 @@ export class AccountManager {
408
431
 
409
432
  /**
410
433
  * Mark an account as rate-limited.
434
+ *
435
+ * When `bucket` is the FAST pool (RATE_LIMIT_KEY_FAST), this is a soft marker:
436
+ * it only cools down the fast pool and does NOT increment consecutiveFailures
437
+ * or apply a health penalty, because the standard pool is still fully usable
438
+ * (the request can simply fall back to standard speed). Marking the whole
439
+ * account unhealthy for a fast-only limit would defeat Anthropic's separate
440
+ * fast pool and waste standard quota.
411
441
  * @param {ManagedAccount} account
412
442
  * @param {RateLimitReason} reason
413
443
  * @param {number | null} [retryAfterMs]
444
+ * @param {string} [bucket] - Rate-limit bucket key (default: standard).
414
445
  * @returns {number} The backoff duration in ms
415
446
  */
416
- markRateLimited(account, reason, retryAfterMs) {
447
+ markRateLimited(account, reason, retryAfterMs, bucket = RATE_LIMIT_KEY) {
417
448
  const now = Date.now();
449
+ const isFastBucket = bucket === RATE_LIMIT_KEY_FAST;
418
450
 
419
- // Reset consecutive failures if TTL has expired
420
- if (account.lastFailureTime !== null && now - account.lastFailureTime > this.#config.failure_ttl_seconds * 1000) {
421
- account.consecutiveFailures = 0;
422
- }
451
+ if (!isFastBucket) {
452
+ // Reset consecutive failures if TTL has expired
453
+ if (account.lastFailureTime !== null && now - account.lastFailureTime > this.#config.failure_ttl_seconds * 1000) {
454
+ account.consecutiveFailures = 0;
455
+ }
423
456
 
424
- account.consecutiveFailures += 1;
425
- account.lastFailureTime = now;
457
+ account.consecutiveFailures += 1;
458
+ account.lastFailureTime = now;
459
+ }
426
460
 
427
- const backoffMs = calculateBackoffMs(reason, account.consecutiveFailures - 1, retryAfterMs);
461
+ // Fast bucket does not track consecutiveFailures, so use index 0 to avoid a
462
+ // negative array index in calculateBackoffMs (QUOTA_EXHAUSTED path).
463
+ const failureIndex = isFastBucket ? 0 : account.consecutiveFailures - 1;
464
+ const backoffMs = calculateBackoffMs(reason, failureIndex, retryAfterMs);
428
465
 
429
- account.rateLimitResetTimes[RATE_LIMIT_KEY] = now + backoffMs;
466
+ account.rateLimitResetTimes[bucket] = now + backoffMs;
430
467
 
431
- this.#healthTracker.recordRateLimit(account.index);
468
+ if (!isFastBucket) {
469
+ this.#healthTracker.recordRateLimit(account.index);
470
+ }
432
471
 
433
472
  this.requestSaveToDisk();
434
473
 
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { AccountManager } from "./accounts.mjs";
2
+ import { AccountManager, RATE_LIMIT_KEY_FAST } from "./accounts.mjs";
3
3
  import { DEFAULT_CONFIG } from "./config.mjs";
4
4
 
5
5
  // Mock storage module — pass through pure helpers, mock I/O
@@ -331,6 +331,41 @@ describe("AccountManager rate limiting", () => {
331
331
  manager.markRateLimited(account, "RATE_LIMIT_EXCEEDED", null);
332
332
  expect(account.consecutiveFailures).toBe(1); // Reset to 0, then +1
333
333
  });
334
+
335
+ // ── Fast-mode pool isolation (Anthropic: fast has a SEPARATE rate-limit pool) ──
336
+ it("fast-bucket rate limit does NOT block standard requests on the same account", () => {
337
+ const account = manager.getCurrentAccount();
338
+ manager.markRateLimited(account, "RATE_LIMIT_EXCEEDED", null, RATE_LIMIT_KEY_FAST);
339
+ // Fast pool is limited...
340
+ expect(manager.isFastRateLimited(account)).toBe(true);
341
+ // ...but the account is still selectable for standard requests (standard
342
+ // bucket untouched, so getCurrentAccount still returns it).
343
+ expect(manager.getCurrentAccount()).not.toBeNull();
344
+ });
345
+
346
+ it("fast-bucket rate limit does NOT increment consecutiveFailures or set lastFailureTime", () => {
347
+ const account = manager.getCurrentAccount();
348
+ manager.markRateLimited(account, "RATE_LIMIT_EXCEEDED", null, RATE_LIMIT_KEY_FAST);
349
+ expect(account.consecutiveFailures).toBe(0);
350
+ expect(account.lastFailureTime).toBeNull();
351
+ expect(manager.isFastRateLimited(account)).toBe(true);
352
+ });
353
+
354
+ it("standard rate limit does NOT mark the fast bucket as limited", () => {
355
+ const account = manager.getCurrentAccount();
356
+ manager.markRateLimited(account, "RATE_LIMIT_EXCEEDED", null);
357
+ expect(manager.isFastRateLimited(account)).toBe(false);
358
+ });
359
+
360
+ it("fast-bucket backoff returns a positive duration and expires independently", () => {
361
+ const account = manager.getCurrentAccount();
362
+ const backoffMs = manager.markRateLimited(account, "RATE_LIMIT_EXCEEDED", null, RATE_LIMIT_KEY_FAST);
363
+ expect(backoffMs).toBeGreaterThan(0);
364
+ expect(manager.isFastRateLimited(account)).toBe(true);
365
+ // After the backoff expires, the fast pool is available again.
366
+ vi.advanceTimersByTime(backoffMs + 1000);
367
+ expect(manager.isFastRateLimited(account)).toBe(false);
368
+ });
334
369
  });
335
370
 
336
371
  // ---------------------------------------------------------------------------
package/lib/config.mjs CHANGED
@@ -97,7 +97,7 @@ import { randomBytes } from "node:crypto";
97
97
  * @property {{ enabled: boolean, default_max_tokens: number, escalated_max_tokens: number }} output_cap
98
98
  * @property {{ enabled: boolean, timeout_ms: number }} preconnect
99
99
  * @property {{ enabled: boolean, safety_margin: number }} overflow_recovery
100
- * @property {{ enabled: boolean, alert_threshold: number }} cache_break_detection
100
+ * @property {{ enabled: boolean, alert_threshold: number, adaptive_breakpoint: boolean }} cache_break_detection
101
101
  * @property {{ enabled: boolean, background_max_service_retries: number, background_max_should_retries: number }} request_classification
102
102
  * @property {{ enabled: boolean, default: number, completion_threshold: number }} token_budget
103
103
  * @property {{ enabled: boolean, threshold_percent: number }} microcompact
@@ -191,15 +191,25 @@ export const DEFAULT_CONFIG = {
191
191
  * (no point suggesting reset at the start of a session). */
192
192
  min_turns_before_suggest: 3,
193
193
  },
194
+ /** SSE response streaming behavior. */
195
+ streaming: {
196
+ /** Byte-stream idle-timeout watchdog (ms). Parity with Claude Code's
197
+ * `tengu_byte_stream_idle_timeout_ms`: if the upstream produces no bytes for
198
+ * this long, the reader is cancelled and the stream errors instead of hanging.
199
+ * 0 = disabled (default — Anthropic SSE sends periodic pings, so only set a
200
+ * multi-minute value to catch genuinely dead connections). Env override:
201
+ * `OPENCODE_ANTHROPIC_STREAM_IDLE_TIMEOUT_MS`. */
202
+ idle_timeout_ms: 0,
203
+ },
194
204
  /** Token economy optimizations. */
195
205
  token_economy: {
196
206
  /** [DEPRECATED] token-efficient-tools-2026-03-28 was removed in CC v2.1.90.
197
207
  * This flag is retained for config backward compatibility but has no effect. */
198
208
  token_efficient_tools: false,
199
209
  /** Enable redact-thinking-2026-02-12 beta (suppresses thinking summaries
200
- * server-side for lower bandwidth). Off by default so thinking stays
201
- * visible. Opt in via `/anthropic set redact-thinking on`. */
202
- redact_thinking: false,
210
+ * server-side for lower bandwidth). On by default for non-Claude-3 models
211
+ * (CC v2.1.150+). Opt out via `/anthropic set redact-thinking off`. */
212
+ redact_thinking: true,
203
213
  /** Enable context-hint-2026-04-09 beta (CC v2.1.110+). When on, the server
204
214
  * MAY reject 422/424 and trigger client-side compaction retries.
205
215
  * Default ON (Phase C2). Server-side gating + contextHintState.disabled
@@ -276,15 +286,18 @@ export const DEFAULT_CONFIG = {
276
286
  * `ttl:"1h"` on an allowlist of query sources (`repl_main_thread*`, `sdk`,
277
287
  * `auto_mode`). Everything else falls back to the 5m tier, which is
278
288
  * cheaper to write. Matches by classifying request role: main → 1h (or
279
- * whatever signature.cachePolicy.ttl is configured as), else → 5m. */
280
- role_scoped_cache_ttl: false,
289
+ * whatever signature.cachePolicy.ttl is configured as), else → 5m.
290
+ * Default ON: this MATCHES real CC behaviour (main thread 1h, side-queries
291
+ * 5m) and only makes non-main cache writes cheaper — no fingerprint risk. */
292
+ role_scoped_cache_ttl: true,
281
293
 
282
294
  /** Lean system prompt for non-main requests. For title-gen / small /
283
295
  * subagent-shaped requests, strip billing identity + CC identity
284
- * injection. Saves ~1-2kB per request. Opt-in: changes the system-prompt
285
- * shape that downstream observers (billing/telemetry) may rely on, so
286
- * enable deliberately rather than by default. */
287
- lean_system_non_main: false,
296
+ * injection. Saves ~1-2kB per request. Default ON: only affects non-main
297
+ * (title-gen / subagent) requests where the CC identity/billing block is
298
+ * not load-bearing for the main-thread fingerprint; the main interactive
299
+ * thread is untouched. */
300
+ lean_system_non_main: true,
288
301
 
289
302
  /** Simple system prompt mode (real CC v2.1.133+ `tengu_vellum_lantern`
290
303
  * equivalent). When enabled AND the request model is eligible
@@ -294,8 +307,11 @@ export const DEFAULT_CONFIG = {
294
307
  * Conservative implementation: gates ONLY anti-verbosity. Identity
295
308
  * preamble, billing block, user instructions, and `<system-reminder>`
296
309
  * blocks are all preserved so the CC fingerprint match still holds.
297
- * Saves ~600-1500 tokens per request on eligible models. Opt-in. */
298
- simple_system_prompt: false,
310
+ * Saves ~600-1500 tokens per request on eligible models. Default ON:
311
+ * gates ONLY the anti-verbosity boilerplate (which real CC v2.1.133+ also
312
+ * drops for eligible Opus 4.x via tengu_vellum_lantern); identity, billing
313
+ * and system-reminders are preserved, so the fingerprint match holds. */
314
+ simple_system_prompt: true,
299
315
 
300
316
  // NOTE: identical-tool-call short-circuit was considered but requires SSE
301
317
  // response-stream rewriting that breaks normal tool-execution semantics.
@@ -361,6 +377,17 @@ export const DEFAULT_CONFIG = {
361
377
  cache_break_detection: {
362
378
  enabled: true,
363
379
  alert_threshold: 2_000,
380
+ /**
381
+ * Adaptive breakpoint placement (opt-in). When true, the prompt-cache
382
+ * breakpoint is anchored on the most-stable boundary (e.g. the system
383
+ * prompt) instead of always on the last tool. This is a DELIBERATE
384
+ * divergence from Claude Code, useful for hosts (like opencode) whose tool
385
+ * array order/count is less stable than CC's. Default true (opencode's tool
386
+ * presentation is less stable than CC's, so adaptive anchoring improves cache
387
+ * hit rate). Set false to force exact CC behavior. Safe either way: with no
388
+ * stability baseline (first turns) it falls back to CC's last-tool breakpoint.
389
+ */
390
+ adaptive_breakpoint: true,
364
391
  },
365
392
  /** Request classification: reduced retry budget for background requests. */
366
393
  request_classification: {
@@ -885,6 +912,10 @@ function validateConfig(raw) {
885
912
  1_000_000,
886
913
  DEFAULT_CONFIG.cache_break_detection.alert_threshold,
887
914
  ),
915
+ adaptive_breakpoint:
916
+ typeof cbd.adaptive_breakpoint === "boolean"
917
+ ? cbd.adaptive_breakpoint
918
+ : DEFAULT_CONFIG.cache_break_detection.adaptive_breakpoint,
888
919
  };
889
920
  }
890
921
 
@@ -16,9 +16,15 @@
16
16
  // Version / build / SDK constants
17
17
  // ---------------------------------------------------------------------------
18
18
 
19
- export const FALLBACK_CLAUDE_CLI_VERSION = "2.1.143";
19
+ export const FALLBACK_CLAUDE_CLI_VERSION = "2.1.159";
20
20
  export const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
21
- export const CLAUDE_CODE_BUILD_TIME = "2026-05-15T17:39:39Z";
21
+ // Real build markers extracted from the 2.1.159 linux-x64 native binary
22
+ // (Bun-embedded JS): `BUILD_TIME:"2026-05-31T16:22:50Z"`,
23
+ // `GIT_SHA:"dd8c11fc8d05cea0b2b9fc8f5a99a5c5c5dffc9b"`.
24
+ // (Prior 2.1.154 win32-x64: BUILD_TIME 2026-05-28T12:27:24Z,
25
+ // GIT_SHA b84d2da9ada13121515426fc644786a303e9ac53.)
26
+ export const CLAUDE_CODE_BUILD_TIME = "2026-05-31T16:22:50Z";
27
+ export const CLAUDE_CODE_GIT_SHA = "dd8c11fc8d05cea0b2b9fc8f5a99a5c5c5dffc9b";
22
28
 
23
29
  // The @anthropic-ai/sdk version bundled with Claude Code.
24
30
  // This is distinct from the CLI version and goes in X-Stainless-Package-Version.
@@ -28,10 +34,27 @@ export const CLAUDE_CODE_BUILD_TIME = "2026-05-15T17:39:39Z";
28
34
  // no SDK version bump; mid-conversation-system-2026-04-07 registered in 2.1.143 but not
29
35
  // auto-emitted; two new conditional x- headers x-claude-code-agent-id /
30
36
  // x-claude-code-parent-agent-id are only emitted in subagent dispatch contexts).
31
- export const ANTHROPIC_SDK_VERSION = "0.81.0";
37
+ export const ANTHROPIC_SDK_VERSION = "0.94.0";
32
38
 
33
39
  // Map of CLI version to bundled SDK version (update when CLI version changes)
34
40
  export const CLI_TO_SDK_VERSION = new Map([
41
+ // 2.1.155-2.1.159: only wire change is the revival of
42
+ // summarize-connector-text-2026-03-13 (label narration_summaries), gated by
43
+ // GrowthBook pewter_owl_header (default-off) — not always-on. SDK still 0.94.0
44
+ // (binary-verified: x-stainless-package-version NQ="0.94.0" in 2.1.159).
45
+ ["2.1.159", "0.94.0"],
46
+ ["2.1.158", "0.94.0"],
47
+ ["2.1.157", "0.94.0"],
48
+ ["2.1.156", "0.94.0"],
49
+ ["2.1.155", "0.94.0"],
50
+ // 2.1.151-2.1.154: Opus 4.8 launch (2026-05-28). npm now ships a native
51
+ // binary (no JS bundle), so the bundled SDK version can't be string-extracted;
52
+ // no SDK bump was observed across 2.1.150->2.1.154, so we carry 0.94.0 forward.
53
+ ["2.1.154", "0.94.0"],
54
+ ["2.1.153", "0.94.0"],
55
+ ["2.1.152", "0.94.0"],
56
+ ["2.1.151", "0.94.0"],
57
+ ["2.1.150", "0.94.0"],
35
58
  ["2.1.143", "0.81.0"],
36
59
  ["2.1.142", "0.81.0"],
37
60
  ["2.1.141", "0.81.0"],
@@ -113,6 +136,9 @@ export const EXPERIMENTAL_BETA_FLAGS = new Set([
113
136
  "advisor-tool-2026-03-01",
114
137
  "afk-mode-2026-01-31",
115
138
  "cache-diagnosis-2026-04-07",
139
+ // CCR bring-your-own-cloud beta. Registered in the 2.1.154 binary's _W() table
140
+ // but not emitted on /v1/messages; kept here for the disable-experimental guard.
141
+ "ccr-byoc-2025-07-29",
116
142
  "code-execution-2025-08-25",
117
143
  "compact-2026-01-12",
118
144
  "context-1m-2025-08-07",
@@ -123,6 +149,9 @@ export const EXPERIMENTAL_BETA_FLAGS = new Set([
123
149
  "fast-mode-2026-02-01",
124
150
  "files-api-2025-04-14",
125
151
  "interleaved-thinking-2025-05-14",
152
+ // Registered in the 2.1.154 binary's _W() table (mcp_servers label). Plugin
153
+ // proxies MCP tool calls inline and does not emit this on chat completions.
154
+ "mcp-servers-2025-12-04",
126
155
  // Registered in CC v2.1.143 but NOT in real CC's always-on emission set
127
156
  // (master registry only, not in the always-on subset). Likely GrowthBook-gated
128
157
  // pending server rollout. Paired telemetry: tengu_mid_conv_system_fallback_retry.
@@ -130,6 +159,11 @@ export const EXPERIMENTAL_BETA_FLAGS = new Set([
130
159
  "prompt-caching-scope-2026-01-05",
131
160
  "redact-thinking-2026-02-12",
132
161
  "structured-outputs-2025-12-15",
162
+ // Revived in CC 2.1.159 as registry label `narration_summaries` (it was a dead
163
+ // slot from v2.1.90-2.1.154). Real CC emits it only when GrowthBook flag
164
+ // `pewter_owl_header` is on (default-off), first-party, and NOT in fast-mode.
165
+ // Listed here for the disable-guard + manual opt-in ONLY; never emitted always-on.
166
+ "summarize-connector-text-2026-03-13",
133
167
  "task-budgets-2026-03-13",
134
168
  "tool-search-tool-2025-10-19",
135
169
  // SDK admin route beta (used for /v1/user_profiles* endpoints). Registered for
@@ -159,6 +193,21 @@ export const BETA_SHORTCUTS = new Map([
159
193
  ["redact-thinking", "redact-thinking-2026-02-12"],
160
194
  ["mid-conv-system", "mid-conversation-system-2026-04-07"],
161
195
  ["mid-system", "mid-conversation-system-2026-04-07"],
196
+ ["context-management", "context-management-2025-06-27"],
197
+ ["structured-outputs", "structured-outputs-2025-12-15"],
198
+ ["web-search", "web-search-2025-03-05"],
199
+ ["advanced-tool-use", "advanced-tool-use-2025-11-20"],
200
+ ["tool-search-tool", "tool-search-tool-2025-10-19"],
201
+ ["effort", "effort-2025-11-24"],
202
+ ["prompt-caching-scope", "prompt-caching-scope-2026-01-05"],
203
+ ["thinking-token-count", "thinking-token-count-2026-05-13"],
204
+ ["afk-mode", "afk-mode-2026-01-31"],
205
+ ["advisor-tool", "advisor-tool-2026-03-01"],
206
+ ["mcp-servers", "mcp-servers-2025-12-04"],
207
+ ["ccr-byoc", "ccr-byoc-2025-07-29"],
208
+ ["connector-text", "summarize-connector-text-2026-03-13"],
209
+ ["narration-summaries", "summarize-connector-text-2026-03-13"],
210
+ ["summarize-connector-text", "summarize-connector-text-2026-03-13"],
162
211
  ]);
163
212
 
164
213
  // ---------------------------------------------------------------------------
@@ -13,8 +13,8 @@ import {
13
13
  } from "./request-headers.mjs";
14
14
 
15
15
  describe("request-headers constants", () => {
16
- it("FALLBACK_CLAUDE_CLI_VERSION is 2.1.143", () => {
17
- expect(FALLBACK_CLAUDE_CLI_VERSION).toBe("2.1.143");
16
+ it("FALLBACK_CLAUDE_CLI_VERSION is 2.1.159", () => {
17
+ expect(FALLBACK_CLAUDE_CLI_VERSION).toBe("2.1.159");
18
18
  });
19
19
 
20
20
  it("ANTHROPIC_SDK_VERSION matches expected value", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "repository": {
6
6
  "type": "git",