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 +184 -10
- package/lib/config.mjs +41 -0
- package/lib/config.test.mjs +74 -0
- package/package.json +1 -1
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(
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
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 —
|
|
3700
|
-
|
|
3701
|
-
|
|
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
|
|
package/lib/config.test.mjs
CHANGED
|
@@ -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
|
});
|