opencode-anthropic-fix 0.0.31 → 0.0.32

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
@@ -590,6 +590,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
590
590
  `fast-mode: ${fresh.fast_mode ? "on" : "off"}`,
591
591
  `telemetry-emulation: ${fresh.telemetry?.emulate_minimal ? "on (silent observer)" : "off"}`,
592
592
  `usage-toast: ${fresh.usage_toast ? "on" : "off"}`,
593
+ `adaptive-context: ${fresh.adaptive_context?.enabled ? `on (↑${Math.round((fresh.adaptive_context.escalation_threshold || 150000) / 1000)}K ↓${Math.round((fresh.adaptive_context.deescalation_threshold || 100000) / 1000)}K)${adaptiveContextState.active ? " [ACTIVE]" : ""}` : "off"}`,
593
594
  ];
594
595
  await sendCommandMessage(input.sessionID, lines.join("\n"));
595
596
  return;
@@ -818,6 +819,22 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
818
819
  saveConfig({ usage_toast: enabled });
819
820
  config.usage_toast = enabled;
820
821
  },
822
+ "adaptive-context": () => {
823
+ const enabled = value === "on" || value === "true" || value === "1";
824
+ saveConfig({ adaptive_context: { enabled } });
825
+ if (!config.adaptive_context)
826
+ config.adaptive_context = {
827
+ enabled: false,
828
+ escalation_threshold: 150_000,
829
+ deescalation_threshold: 100_000,
830
+ };
831
+ config.adaptive_context.enabled = enabled;
832
+ // Reset state when toggled off
833
+ if (!enabled) {
834
+ adaptiveContextState.active = false;
835
+ adaptiveContextState.escalatedByError = false;
836
+ }
837
+ },
821
838
  };
822
839
 
823
840
  if (!key || !setters[key]) {
@@ -1956,6 +1973,32 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
1956
1973
  requestInit.body,
1957
1974
  );
1958
1975
  const _reqProvider = detectProvider(requestUrl);
1976
+
1977
+ // --- Adaptive 1M context decision ---
1978
+ const _prevAdaptiveState = adaptiveContextState.active;
1979
+ const _use1MContext = resolveAdaptiveContext(
1980
+ requestInit.body,
1981
+ _reqModel,
1982
+ config.adaptive_context || {
1983
+ enabled: false,
1984
+ escalation_threshold: 150_000,
1985
+ deescalation_threshold: 100_000,
1986
+ },
1987
+ );
1988
+ // Emit visual cue on state transitions (only when adaptive mode is on)
1989
+ if (config.adaptive_context?.enabled && _prevAdaptiveState !== adaptiveContextState.active) {
1990
+ const label = adaptiveContextState.active ? "1M context ON" : "1M context OFF";
1991
+ const variant = adaptiveContextState.active ? "info" : "success";
1992
+ const est = estimatePromptTokens(requestInit.body);
1993
+ toast(`⬡ ${label} (est. ${Math.round(est / 1000)}K tokens)`, variant, {
1994
+ debounceKey: "adaptive-ctx",
1995
+ }).catch(() => {});
1996
+ }
1997
+
1998
+ const _adaptiveOverride = config.adaptive_context?.enabled
1999
+ ? { use1MContext: _use1MContext }
2000
+ : undefined;
2001
+
1959
2002
  const computedBetaHeader = buildAnthropicBetaHeader(
1960
2003
  "",
1961
2004
  getSignatureEmulationEnabled(),
@@ -1965,6 +2008,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
1965
2008
  getEffectiveStrategy(),
1966
2009
  requestUrl?.pathname,
1967
2010
  _reqHasFileRefs,
2011
+ _adaptiveOverride,
1968
2012
  );
1969
2013
 
1970
2014
  let body = transformRequestBody(
@@ -1988,12 +2032,20 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
1988
2032
  logTransformedSystemPrompt(body);
1989
2033
 
1990
2034
  // Build headers with the selected account's token
1991
- const requestHeaders = buildRequestHeaders(input, requestInit, accessToken, body, requestUrl, {
1992
- enabled: getSignatureEmulationEnabled(),
1993
- claudeCliVersion,
1994
- customBetas: config.custom_betas,
1995
- strategy: getEffectiveStrategy(),
1996
- });
2035
+ const requestHeaders = buildRequestHeaders(
2036
+ input,
2037
+ requestInit,
2038
+ accessToken,
2039
+ body,
2040
+ requestUrl,
2041
+ {
2042
+ enabled: getSignatureEmulationEnabled(),
2043
+ claudeCliVersion,
2044
+ customBetas: config.custom_betas,
2045
+ strategy: getEffectiveStrategy(),
2046
+ },
2047
+ _adaptiveOverride,
2048
+ );
1997
2049
  // Execute the request
1998
2050
  let response;
1999
2051
  try {
@@ -2144,6 +2196,14 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
2144
2196
  !requestInit._reactiveCompactAttempted
2145
2197
  ) {
2146
2198
  debugLog("prompt too long — attempting reactive message trimming");
2199
+ // Auto-escalate adaptive context on prompt_too_long so the retry
2200
+ // includes the 1M beta header (if model supports it).
2201
+ if (config.adaptive_context?.enabled) {
2202
+ forceEscalateAdaptiveContext();
2203
+ toast("⬡ 1M context force-activated (prompt too long)", "warning", {
2204
+ debounceKey: "adaptive-ctx",
2205
+ }).catch(() => {});
2206
+ }
2147
2207
  try {
2148
2208
  const parsedBody = JSON.parse(body);
2149
2209
  if (Array.isArray(parsedBody.messages) && parsedBody.messages.length > 4) {
@@ -2618,6 +2678,111 @@ const sessionMetrics = {
2618
2678
  lastQuota: { tokens: 0, requests: 0, inputTokens: 0, updatedAt: 0 },
2619
2679
  };
2620
2680
 
2681
+ // ---------------------------------------------------------------------------
2682
+ // Adaptive 1M context state
2683
+ // ---------------------------------------------------------------------------
2684
+
2685
+ /**
2686
+ * Tracks whether the 1M context beta is currently active for adaptive mode.
2687
+ * When adaptive_context.enabled is true, the context-1m-2025-08-07 beta is
2688
+ * toggled per-request based on estimated prompt size.
2689
+ *
2690
+ * @type {{ active: boolean, lastTransitionTurn: number, escalatedByError: boolean }}
2691
+ */
2692
+ const adaptiveContextState = {
2693
+ /** Whether 1M context beta is currently being sent. */
2694
+ active: false,
2695
+ /** Turn number of the last transition (to avoid flapping). */
2696
+ lastTransitionTurn: 0,
2697
+ /** Set when escalation was triggered by a prompt_too_long error. */
2698
+ escalatedByError: false,
2699
+ };
2700
+
2701
+ /**
2702
+ * Estimate prompt token count from the raw request body string.
2703
+ * Uses a 4-character-per-token heuristic (conservative for English + code).
2704
+ * @param {string} bodyString - JSON string of the request body
2705
+ * @returns {number} Estimated token count
2706
+ */
2707
+ function estimatePromptTokens(bodyString) {
2708
+ if (!bodyString || typeof bodyString !== "string") return 0;
2709
+ // The body includes tools, system prompt, and messages.
2710
+ // 4 chars/token is a reasonable heuristic for JSON-encoded conversation.
2711
+ return Math.ceil(bodyString.length / 4);
2712
+ }
2713
+
2714
+ /**
2715
+ * Decide whether to include the context-1m beta for this request.
2716
+ * Returns true if 1M context should be activated.
2717
+ *
2718
+ * Decision logic:
2719
+ * - If adaptive_context is disabled, defer to hasOneMillionContext(model) as before.
2720
+ * - If model does not support 1M context, always false.
2721
+ * - Escalate when estimated prompt tokens exceed escalation_threshold.
2722
+ * - De-escalate when estimated prompt tokens drop below deescalation_threshold.
2723
+ * - Never de-escalate if escalation was triggered by a prompt_too_long error
2724
+ * (sticky until session compacts or drops far below threshold).
2725
+ * - Hysteresis: require at least 2 turns between transitions to avoid flapping.
2726
+ *
2727
+ * @param {string} bodyString - JSON request body
2728
+ * @param {string} model - Model ID
2729
+ * @param {import('./lib/config.mjs').AdaptiveContextConfig} adaptiveConfig
2730
+ * @returns {boolean}
2731
+ */
2732
+ function resolveAdaptiveContext(bodyString, model, adaptiveConfig) {
2733
+ // Non-adaptive: use static check
2734
+ if (!adaptiveConfig.enabled) {
2735
+ return hasOneMillionContext(model);
2736
+ }
2737
+
2738
+ // Model must be eligible for 1M context at all
2739
+ if (!hasOneMillionContext(model)) {
2740
+ return false;
2741
+ }
2742
+
2743
+ const estimatedTokens = estimatePromptTokens(bodyString);
2744
+ const turnsSinceTransition = sessionMetrics.turns - adaptiveContextState.lastTransitionTurn;
2745
+
2746
+ if (adaptiveContextState.active) {
2747
+ // Currently active — consider de-escalation
2748
+ // Don't de-escalate if error-escalated (sticky)
2749
+ if (adaptiveContextState.escalatedByError) {
2750
+ return true;
2751
+ }
2752
+ // Hysteresis: require at least 2 turns before considering de-escalation
2753
+ if (turnsSinceTransition < 2) {
2754
+ return true;
2755
+ }
2756
+ if (estimatedTokens < adaptiveConfig.deescalation_threshold) {
2757
+ // De-escalate
2758
+ adaptiveContextState.active = false;
2759
+ adaptiveContextState.lastTransitionTurn = sessionMetrics.turns;
2760
+ return false;
2761
+ }
2762
+ return true;
2763
+ } else {
2764
+ // Currently inactive — consider escalation
2765
+ if (estimatedTokens > adaptiveConfig.escalation_threshold) {
2766
+ // Escalate
2767
+ adaptiveContextState.active = true;
2768
+ adaptiveContextState.lastTransitionTurn = sessionMetrics.turns;
2769
+ return true;
2770
+ }
2771
+ return false;
2772
+ }
2773
+ }
2774
+
2775
+ /**
2776
+ * Force-escalate adaptive context (e.g. after prompt_too_long error).
2777
+ */
2778
+ function forceEscalateAdaptiveContext() {
2779
+ if (!adaptiveContextState.active) {
2780
+ adaptiveContextState.active = true;
2781
+ adaptiveContextState.lastTransitionTurn = sessionMetrics.turns;
2782
+ }
2783
+ adaptiveContextState.escalatedByError = true;
2784
+ }
2785
+
2621
2786
  const MODEL_PRICING = {
2622
2787
  "claude-opus-4-6": { input: 15.0, output: 75.0, cacheRead: 1.5, cacheWrite: 18.75 },
2623
2788
  "claude-sonnet-4-6": { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },
@@ -3644,6 +3809,7 @@ function buildSystemPromptBlocks(system, signature) {
3644
3809
  * @param {import('./lib/config.mjs').AccountSelectionStrategy} [strategy]
3645
3810
  * @param {string} [requestPath]
3646
3811
  * @param {boolean} [hasFileReferences]
3812
+ * @param {{ use1MContext?: boolean }} [adaptiveOverride] - When set, overrides the static hasOneMillionContext() check.
3647
3813
  * @returns {string}
3648
3814
  */
3649
3815
  function buildAnthropicBetaHeader(
@@ -3655,6 +3821,7 @@ function buildAnthropicBetaHeader(
3655
3821
  strategy,
3656
3822
  requestPath,
3657
3823
  hasFileReferences,
3824
+ adaptiveOverride,
3658
3825
  ) {
3659
3826
  const incomingBetasList = incomingBeta
3660
3827
  .split(",")
@@ -3696,9 +3863,15 @@ function buildAnthropicBetaHeader(
3696
3863
  betas.push("interleaved-thinking-2025-05-14");
3697
3864
  }
3698
3865
 
3699
- // Context 1M — always-on for eligible models (Claude Code includes this for OAuth too)
3700
- if (hasOneMillionContext(model)) {
3701
- betas.push("context-1m-2025-08-07");
3866
+ // Context 1M — when adaptive override is provided, use it; otherwise fall back to static check.
3867
+ {
3868
+ const use1M =
3869
+ adaptiveOverride && typeof adaptiveOverride.use1MContext === "boolean"
3870
+ ? adaptiveOverride.use1MContext
3871
+ : hasOneMillionContext(model);
3872
+ if (use1M) {
3873
+ betas.push("context-1m-2025-08-07");
3874
+ }
3702
3875
  }
3703
3876
 
3704
3877
  // Prompt caching scope — always-on EXCEPT in round-robin (per-workspace state conflicts)
@@ -3887,7 +4060,7 @@ async function fetchLatestClaudeCodeVersion(timeoutMs = 1200) {
3887
4060
  * @param {{enabled: boolean, claudeCliVersion: string, strategy?: import('./lib/config.mjs').AccountSelectionStrategy, customBetas?: string[]}} signature
3888
4061
  * @returns {Headers}
3889
4062
  */
3890
- function buildRequestHeaders(input, requestInit, accessToken, requestBody, requestUrl, signature) {
4063
+ function buildRequestHeaders(input, requestInit, accessToken, requestBody, requestUrl, signature, adaptiveOverride) {
3891
4064
  const requestHeaders = new Headers();
3892
4065
  if (input instanceof Request) {
3893
4066
  input.headers.forEach((value, key) => {
@@ -3927,6 +4100,7 @@ function buildRequestHeaders(input, requestInit, accessToken, requestBody, reque
3927
4100
  signature.strategy,
3928
4101
  requestUrl?.pathname,
3929
4102
  hasFileReferences,
4103
+ adaptiveOverride,
3930
4104
  );
3931
4105
 
3932
4106
  const authTokenOverride = process.env.ANTHROPIC_AUTH_TOKEN?.trim();
package/lib/config.mjs CHANGED
@@ -53,6 +53,13 @@ import { randomBytes } from "node:crypto";
53
53
  * @property {number} hit_rate_warning_threshold - Warn when avg cache hit rate falls below this (0-1). Default: 0.3
54
54
  */
55
55
 
56
+ /**
57
+ * @typedef {object} AdaptiveContextConfig
58
+ * @property {boolean} enabled - When true, context-1m beta is toggled per-request based on prompt size. Default: false.
59
+ * @property {number} escalation_threshold - Token count (input+cache) above which 1M context is activated. Default: 150_000.
60
+ * @property {number} deescalation_threshold - Token count below which 1M context is deactivated. Default: 100_000.
61
+ */
62
+
56
63
  /**
57
64
  * @typedef {object} CCCredentialReuseConfig
58
65
  * @property {boolean} enabled - Master switch for CC credential reuse. Default: false.
@@ -78,6 +85,7 @@ import { randomBytes } from "node:crypto";
78
85
  * @property {{emulate_minimal: boolean}} telemetry
79
86
  * @property {boolean} usage_toast
80
87
  * @property {CCCredentialReuseConfig} cc_credential_reuse
88
+ * @property {AdaptiveContextConfig} adaptive_context
81
89
  */
82
90
 
83
91
  /** @type {AnthropicAuthConfig} */
@@ -141,6 +149,11 @@ export const DEFAULT_CONFIG = {
141
149
  auto_detect: true,
142
150
  prefer_over_oauth: false,
143
151
  },
152
+ adaptive_context: {
153
+ enabled: false,
154
+ escalation_threshold: 150_000,
155
+ deescalation_threshold: 100_000,
156
+ },
144
157
  };
145
158
 
146
159
  export const VALID_STRATEGIES = ["sticky", "round-robin", "hybrid"];
@@ -167,6 +180,7 @@ function createDefaultConfig() {
167
180
  telemetry: { ...DEFAULT_CONFIG.telemetry },
168
181
  headers: { ...DEFAULT_CONFIG.headers, overrides: {}, disable: [] },
169
182
  cc_credential_reuse: { ...DEFAULT_CONFIG.cc_credential_reuse },
183
+ adaptive_context: { ...DEFAULT_CONFIG.adaptive_context },
170
184
  };
171
185
  }
172
186
 
@@ -385,6 +399,26 @@ function validateConfig(raw) {
385
399
  };
386
400
  }
387
401
 
402
+ // Adaptive context sub-config
403
+ if (raw.adaptive_context && typeof raw.adaptive_context === "object") {
404
+ const ac = /** @type {Record<string, unknown>} */ (raw.adaptive_context);
405
+ config.adaptive_context = {
406
+ enabled: typeof ac.enabled === "boolean" ? ac.enabled : DEFAULT_CONFIG.adaptive_context.enabled,
407
+ escalation_threshold: clampNumber(
408
+ ac.escalation_threshold,
409
+ 50_000,
410
+ 500_000,
411
+ DEFAULT_CONFIG.adaptive_context.escalation_threshold,
412
+ ),
413
+ deescalation_threshold: clampNumber(
414
+ ac.deescalation_threshold,
415
+ 20_000,
416
+ 400_000,
417
+ DEFAULT_CONFIG.adaptive_context.deescalation_threshold,
418
+ ),
419
+ };
420
+ }
421
+
388
422
  return config;
389
423
  }
390
424
 
@@ -464,6 +498,13 @@ function applyEnvOverrides(config) {
464
498
  config.cc_credential_reuse.enabled = false;
465
499
  }
466
500
 
501
+ if (env.OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT === "1" || env.OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT === "true") {
502
+ config.adaptive_context.enabled = true;
503
+ }
504
+ if (env.OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT === "0" || env.OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT === "false") {
505
+ config.adaptive_context.enabled = false;
506
+ }
507
+
467
508
  return config;
468
509
  }
469
510
 
@@ -70,6 +70,7 @@ describe("loadConfig", () => {
70
70
  delete process.env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE;
71
71
  delete process.env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION;
72
72
  delete process.env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION;
73
+ delete process.env.OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT;
73
74
  });
74
75
 
75
76
  afterEach(() => {
@@ -304,4 +305,77 @@ describe("loadConfig", () => {
304
305
  const config = loadConfig();
305
306
  expect(config.toasts.quiet).toBe(false);
306
307
  });
308
+
309
+ // --- adaptive_context config tests ---
310
+
311
+ it("has adaptive_context defaults (disabled)", () => {
312
+ existsSync.mockReturnValue(false);
313
+ const config = loadConfig();
314
+ expect(config.adaptive_context).toEqual({
315
+ enabled: false,
316
+ escalation_threshold: 150_000,
317
+ deescalation_threshold: 100_000,
318
+ });
319
+ });
320
+
321
+ it("merges adaptive_context from config file", () => {
322
+ existsSync.mockReturnValue(true);
323
+ readFileSync.mockReturnValue(
324
+ JSON.stringify({ adaptive_context: { enabled: true, escalation_threshold: 200_000 } }),
325
+ );
326
+ const config = loadConfig();
327
+ expect(config.adaptive_context.enabled).toBe(true);
328
+ expect(config.adaptive_context.escalation_threshold).toBe(200_000);
329
+ expect(config.adaptive_context.deescalation_threshold).toBe(100_000); // default preserved
330
+ });
331
+
332
+ it("clamps adaptive_context thresholds to valid ranges", () => {
333
+ existsSync.mockReturnValue(true);
334
+ readFileSync.mockReturnValue(
335
+ JSON.stringify({
336
+ adaptive_context: {
337
+ enabled: true,
338
+ escalation_threshold: 999_999, // above 500K max → clamped
339
+ deescalation_threshold: 1, // below 20K min → clamped
340
+ },
341
+ }),
342
+ );
343
+ const config = loadConfig();
344
+ expect(config.adaptive_context.escalation_threshold).toBe(500_000);
345
+ expect(config.adaptive_context.deescalation_threshold).toBe(20_000);
346
+ });
347
+
348
+ it("ignores non-boolean adaptive_context.enabled", () => {
349
+ existsSync.mockReturnValue(true);
350
+ readFileSync.mockReturnValue(JSON.stringify({ adaptive_context: { enabled: "yes" } }));
351
+ const config = loadConfig();
352
+ expect(config.adaptive_context.enabled).toBe(false);
353
+ });
354
+
355
+ it("ignores non-number adaptive_context thresholds", () => {
356
+ existsSync.mockReturnValue(true);
357
+ readFileSync.mockReturnValue(
358
+ JSON.stringify({
359
+ adaptive_context: { escalation_threshold: "high", deescalation_threshold: null },
360
+ }),
361
+ );
362
+ const config = loadConfig();
363
+ expect(config.adaptive_context.escalation_threshold).toBe(150_000);
364
+ expect(config.adaptive_context.deescalation_threshold).toBe(100_000);
365
+ });
366
+
367
+ it("enables adaptive context from OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT=1", () => {
368
+ existsSync.mockReturnValue(false);
369
+ process.env.OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT = "1";
370
+ const config = loadConfig();
371
+ expect(config.adaptive_context.enabled).toBe(true);
372
+ });
373
+
374
+ it("disables adaptive context from OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT=0", () => {
375
+ existsSync.mockReturnValue(true);
376
+ readFileSync.mockReturnValue(JSON.stringify({ adaptive_context: { enabled: true } }));
377
+ process.env.OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT = "0";
378
+ const config = loadConfig();
379
+ expect(config.adaptive_context.enabled).toBe(false);
380
+ });
307
381
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "0.0.31",
3
+ "version": "0.0.32",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "main": "./index.mjs",
6
6
  "files": [