opencode-anthropic-fix 0.1.8 → 0.1.11
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 +891 -36
- package/lib/config.mjs +121 -9
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -184,6 +184,35 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
184
184
|
lastHeader: null,
|
|
185
185
|
};
|
|
186
186
|
|
|
187
|
+
// Context-hint controller (CC v2.1.110+). Mirrors real CC's `createContextHintController`:
|
|
188
|
+
// sticky on across requests until the server responds with a specific error family.
|
|
189
|
+
// - 422/424 → apply hint compaction (clear thinking + microcompact) and retry
|
|
190
|
+
// - 400 "Unexpected value" + "anthropic-beta" → disable for session (beta unsupported)
|
|
191
|
+
// - 409 → disable for session (conflict)
|
|
192
|
+
// - 529 / overloaded → disable for session (temporary overload)
|
|
193
|
+
// When disabled, we strip context-hint from betas + body on subsequent requests so
|
|
194
|
+
// we don't keep triggering the same rejection and churning the cache.
|
|
195
|
+
const contextHintState = {
|
|
196
|
+
/** Permanently disabled for this session after a server rejection. */
|
|
197
|
+
disabled: false,
|
|
198
|
+
/** Number of 422/424 compactions applied this session (for telemetry). */
|
|
199
|
+
compactionsApplied: 0,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Token economy — session state for layered compaction strategies.
|
|
203
|
+
const tokenEconomySession = {
|
|
204
|
+
/** When thinking was last stripped (TTL-based strategy). 0 = never. */
|
|
205
|
+
lastThinkingStripMs: 0,
|
|
206
|
+
/** When proactive microcompact was last run (threshold-based). 0 = never. */
|
|
207
|
+
lastMicrocompactMs: 0,
|
|
208
|
+
/** Running count of tool_results client-compacted this session. */
|
|
209
|
+
toolResultsCompacted: 0,
|
|
210
|
+
/** Running count of thinking blocks stripped this session. */
|
|
211
|
+
thinkingStripped: 0,
|
|
212
|
+
/** Map of content-hash → first-seen tool_use_id for cross-turn dedupe. */
|
|
213
|
+
seenContentHashes: new Map(),
|
|
214
|
+
};
|
|
215
|
+
|
|
187
216
|
// Cache TTL session latching: latch the cache policy at session start
|
|
188
217
|
// so mid-session toggles don't bust the server-side prompt cache.
|
|
189
218
|
let sessionCachePolicyLatched = false;
|
|
@@ -666,6 +695,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
666
695
|
const secondary = (args[1] || "").toLowerCase();
|
|
667
696
|
if (secondary === "reset") {
|
|
668
697
|
sessionMetrics.turns = 0;
|
|
698
|
+
sessionMetrics.usedTools.clear();
|
|
669
699
|
sessionMetrics.totalInput = 0;
|
|
670
700
|
sessionMetrics.totalOutput = 0;
|
|
671
701
|
sessionMetrics.totalCacheRead = 0;
|
|
@@ -1081,8 +1111,6 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
1081
1111
|
const enabled = value === "on" || value === "true" || value === "1";
|
|
1082
1112
|
const te = config.token_economy || {
|
|
1083
1113
|
token_efficient_tools: true,
|
|
1084
|
-
redact_thinking: false,
|
|
1085
|
-
connector_text_summarization: true,
|
|
1086
1114
|
};
|
|
1087
1115
|
te.token_efficient_tools = enabled;
|
|
1088
1116
|
saveConfig({ token_economy: te });
|
|
@@ -1094,25 +1122,35 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
1094
1122
|
const enabled = value === "on" || value === "true" || value === "1";
|
|
1095
1123
|
const te = config.token_economy || {
|
|
1096
1124
|
token_efficient_tools: true,
|
|
1097
|
-
redact_thinking: false,
|
|
1098
|
-
connector_text_summarization: true,
|
|
1099
1125
|
};
|
|
1100
1126
|
te.redact_thinking = enabled;
|
|
1101
1127
|
saveConfig({ token_economy: te });
|
|
1102
1128
|
config.token_economy = te;
|
|
1103
1129
|
betaLatchState.dirty = true;
|
|
1104
1130
|
},
|
|
1105
|
-
"
|
|
1131
|
+
"tool-deferral": () => {
|
|
1106
1132
|
const enabled = value === "on" || value === "true" || value === "1";
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
saveConfig({
|
|
1114
|
-
config.
|
|
1115
|
-
|
|
1133
|
+
saveConfig({ token_economy_strategies: { tool_deferral: enabled } });
|
|
1134
|
+
if (!config.token_economy_strategies) config.token_economy_strategies = {};
|
|
1135
|
+
config.token_economy_strategies.tool_deferral = enabled;
|
|
1136
|
+
},
|
|
1137
|
+
"tool-compaction": () => {
|
|
1138
|
+
const enabled = value === "on" || value === "true" || value === "1";
|
|
1139
|
+
saveConfig({ token_economy_strategies: { tool_description_compaction: enabled } });
|
|
1140
|
+
if (!config.token_economy_strategies) config.token_economy_strategies = {};
|
|
1141
|
+
config.token_economy_strategies.tool_description_compaction = enabled;
|
|
1142
|
+
},
|
|
1143
|
+
"adaptive-tools": () => {
|
|
1144
|
+
const enabled = value === "on" || value === "true" || value === "1";
|
|
1145
|
+
saveConfig({ token_economy_strategies: { adaptive_tool_set: enabled } });
|
|
1146
|
+
if (!config.token_economy_strategies) config.token_economy_strategies = {};
|
|
1147
|
+
config.token_economy_strategies.adaptive_tool_set = enabled;
|
|
1148
|
+
},
|
|
1149
|
+
"prompt-tailing": () => {
|
|
1150
|
+
const enabled = value === "on" || value === "true" || value === "1";
|
|
1151
|
+
saveConfig({ token_economy_strategies: { system_prompt_tailing: enabled } });
|
|
1152
|
+
if (!config.token_economy_strategies) config.token_economy_strategies = {};
|
|
1153
|
+
config.token_economy_strategies.system_prompt_tailing = enabled;
|
|
1116
1154
|
},
|
|
1117
1155
|
};
|
|
1118
1156
|
|
|
@@ -2703,8 +2741,19 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2703
2741
|
|
|
2704
2742
|
const _adaptiveOverride = _adaptiveOverrideForRequest;
|
|
2705
2743
|
|
|
2706
|
-
// Token economy config (resolved once, passed to beta builder)
|
|
2707
|
-
|
|
2744
|
+
// Token economy config (resolved once, passed to beta builder).
|
|
2745
|
+
// If the server has rejected context-hint this session, reflect it here
|
|
2746
|
+
// so buildAnthropicBetaHeader (called again inside buildRequestHeaders)
|
|
2747
|
+
// drops the beta on subsequent requests.
|
|
2748
|
+
// Also classify the request role (CC's querySource analog) and
|
|
2749
|
+
// suppress context-hint for non-main-thread requests so we don't
|
|
2750
|
+
// opt subagent/title/tool one-shots into a server retry loop.
|
|
2751
|
+
const _requestRole = classifyRequestRole(_parsedBodyOnce);
|
|
2752
|
+
const _baseTE = config.token_economy || {};
|
|
2753
|
+
const _disableCtxHint = contextHintState.disabled || _requestRole !== "main";
|
|
2754
|
+
const _tokenEconomy = _disableCtxHint
|
|
2755
|
+
? { ..._baseTE, context_hint: false, __requestRole: _requestRole }
|
|
2756
|
+
: { ..._baseTE, __requestRole: _requestRole };
|
|
2708
2757
|
|
|
2709
2758
|
// Microcompact: inject clear betas at high context utilization
|
|
2710
2759
|
let _microcompactBetas = null;
|
|
@@ -2760,6 +2809,12 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2760
2809
|
// Merge latched betas that aren't in the current set
|
|
2761
2810
|
const merged = new Set(currentBetas);
|
|
2762
2811
|
for (const b of betaLatchState.sent) merged.add(b);
|
|
2812
|
+
// Context-hint kill switch: once server rejected it this session,
|
|
2813
|
+
// stop sending the beta (body field is gated on header, so it drops too).
|
|
2814
|
+
if (contextHintState.disabled) {
|
|
2815
|
+
merged.delete("context-hint-2026-04-09");
|
|
2816
|
+
betaLatchState.sent.delete("context-hint-2026-04-09");
|
|
2817
|
+
}
|
|
2763
2818
|
computedBetaHeader = [...merged].join(",");
|
|
2764
2819
|
betaLatchState.lastHeader = computedBetaHeader;
|
|
2765
2820
|
}
|
|
@@ -2785,11 +2840,21 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2785
2840
|
cachePolicy: effectiveCachePolicy,
|
|
2786
2841
|
fastMode: config.fast_mode || false,
|
|
2787
2842
|
strategy: getEffectiveStrategy(),
|
|
2843
|
+
toolDeferral: config.token_economy_strategies?.tool_deferral,
|
|
2844
|
+
toolDescriptionCompaction: config.token_economy_strategies?.tool_description_compaction,
|
|
2845
|
+
adaptiveToolSet: config.token_economy_strategies?.adaptive_tool_set,
|
|
2846
|
+
systemPromptTailing: config.token_economy_strategies?.system_prompt_tailing,
|
|
2847
|
+
systemPromptTailTurns: config.token_economy_strategies?.system_prompt_tail_turns,
|
|
2848
|
+
systemPromptTailMaxChars: config.token_economy_strategies?.system_prompt_tail_max_chars,
|
|
2788
2849
|
},
|
|
2789
2850
|
{
|
|
2790
2851
|
persistentUserId: signatureUserId,
|
|
2791
2852
|
sessionId: signatureSessionId,
|
|
2792
2853
|
accountId: getAccountIdentifier(account),
|
|
2854
|
+
turns: sessionMetrics.turns,
|
|
2855
|
+
usedTools: sessionMetrics.usedTools,
|
|
2856
|
+
tokenEconomySession,
|
|
2857
|
+
requestRole: _requestRole,
|
|
2793
2858
|
},
|
|
2794
2859
|
computedBetaHeader,
|
|
2795
2860
|
config,
|
|
@@ -3152,6 +3217,63 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
3152
3217
|
// Ignore read errors in debug logging path.
|
|
3153
3218
|
}
|
|
3154
3219
|
|
|
3220
|
+
// Context-hint protocol (CC v2.1.110+): detect server rejections and
|
|
3221
|
+
// apply the same disable/compact semantics as real Claude Code.
|
|
3222
|
+
// - 400 w/ "Unexpected value" + "anthropic-beta" → beta unsupported, disable
|
|
3223
|
+
// - 409 / 529 / overloaded → temporary, disable
|
|
3224
|
+
// - 422 / 424 → compact messages (strip
|
|
3225
|
+
// thinking blocks + old tool_result content) and retry ONCE.
|
|
3226
|
+
// Disable is permanent for the session; the beta latch strips it from future
|
|
3227
|
+
// requests so we don't keep triggering the same rejection.
|
|
3228
|
+
if (!contextHintState.disabled) {
|
|
3229
|
+
if (
|
|
3230
|
+
response.status === 400 &&
|
|
3231
|
+
errorBody &&
|
|
3232
|
+
errorBody.includes("Unexpected value") &&
|
|
3233
|
+
errorBody.includes("anthropic-beta") &&
|
|
3234
|
+
errorBody.includes("context-hint")
|
|
3235
|
+
) {
|
|
3236
|
+
contextHintState.disabled = true;
|
|
3237
|
+
betaLatchState.dirty = true;
|
|
3238
|
+
debugLog("context-hint: beta rejected by server (400), disabling for session");
|
|
3239
|
+
} else if (response.status === 409) {
|
|
3240
|
+
contextHintState.disabled = true;
|
|
3241
|
+
betaLatchState.dirty = true;
|
|
3242
|
+
debugLog("context-hint: 409 conflict, disabling for session");
|
|
3243
|
+
} else if (response.status === 529 && errorBody && errorBody.includes("context_hint")) {
|
|
3244
|
+
contextHintState.disabled = true;
|
|
3245
|
+
betaLatchState.dirty = true;
|
|
3246
|
+
debugLog("context-hint: 529 overloaded referencing hint, disabling for session");
|
|
3247
|
+
} else if (
|
|
3248
|
+
(response.status === 422 || response.status === 424) &&
|
|
3249
|
+
!requestInit._contextHintCompactAttempted
|
|
3250
|
+
) {
|
|
3251
|
+
try {
|
|
3252
|
+
const hintBody = JSON.parse(requestInit.body);
|
|
3253
|
+
if (Array.isArray(hintBody.messages)) {
|
|
3254
|
+
const compacted = applyContextHintCompaction(hintBody.messages);
|
|
3255
|
+
if (compacted.changed) {
|
|
3256
|
+
hintBody.messages = compacted.messages;
|
|
3257
|
+
requestInit.body = JSON.stringify(hintBody);
|
|
3258
|
+
_parsedBodyOnce = null;
|
|
3259
|
+
requestInit._contextHintCompactAttempted = true;
|
|
3260
|
+
contextHintState.compactionsApplied += 1;
|
|
3261
|
+
attempt--;
|
|
3262
|
+
toast(
|
|
3263
|
+
`⚙ Context hint compaction (${response.status}) — cleared ${compacted.stats.thinkingCleared} thinking / ${compacted.stats.toolResultsCleared} tool results`,
|
|
3264
|
+
"info",
|
|
3265
|
+
{ debounceKey: "context-hint-compact" },
|
|
3266
|
+
).catch(() => {});
|
|
3267
|
+
debugLog("context-hint: applied compaction on status", response.status, compacted.stats);
|
|
3268
|
+
continue;
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
} catch {
|
|
3272
|
+
// fall through to normal error handling
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3155
3277
|
// Reactive compaction: on "prompt too long" error, trim oldest messages and retry once
|
|
3156
3278
|
if (
|
|
3157
3279
|
response.status === 400 &&
|
|
@@ -3908,6 +4030,8 @@ const sessionMetrics = {
|
|
|
3908
4030
|
continuations: 0,
|
|
3909
4031
|
outputHistory: [], // last 5 output token deltas
|
|
3910
4032
|
},
|
|
4033
|
+
/** Tools used in this session (populated from assistant tool_use blocks in messages) */
|
|
4034
|
+
usedTools: new Set(),
|
|
3911
4035
|
};
|
|
3912
4036
|
|
|
3913
4037
|
// ---------------------------------------------------------------------------
|
|
@@ -5058,17 +5182,25 @@ process.once("beforeExit", _beforeExitHandler);
|
|
|
5058
5182
|
// Request building helpers (extracted from original fetch interceptor)
|
|
5059
5183
|
// ---------------------------------------------------------------------------
|
|
5060
5184
|
|
|
5061
|
-
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.
|
|
5185
|
+
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.114";
|
|
5062
5186
|
const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
5063
|
-
const CLAUDE_CODE_BUILD_TIME = "2026-04-
|
|
5187
|
+
const CLAUDE_CODE_BUILD_TIME = "2026-04-17T22:37:24Z";
|
|
5064
5188
|
|
|
5065
5189
|
// The @anthropic-ai/sdk version bundled with Claude Code.
|
|
5066
5190
|
// This is distinct from the CLI version and goes in X-Stainless-Package-Version.
|
|
5067
|
-
// v2.1.107 switched from @anthropic-ai/sdk v0.208.0 to v0.81.0 (confirmed via
|
|
5191
|
+
// v2.1.107 switched from @anthropic-ai/sdk v0.208.0 to v0.81.0 (confirmed via bundle var x$H="0.81.0").
|
|
5192
|
+
// Still 0.81.0 in v2.1.114 (verified via strings extraction from native Bun binary).
|
|
5068
5193
|
const ANTHROPIC_SDK_VERSION = "0.81.0";
|
|
5069
5194
|
|
|
5070
5195
|
// Map of CLI version → bundled SDK version (update when CLI version changes)
|
|
5071
5196
|
const CLI_TO_SDK_VERSION = new Map([
|
|
5197
|
+
["2.1.114", "0.81.0"],
|
|
5198
|
+
["2.1.113", "0.81.0"],
|
|
5199
|
+
["2.1.112", "0.81.0"],
|
|
5200
|
+
["2.1.111", "0.81.0"],
|
|
5201
|
+
["2.1.110", "0.81.0"],
|
|
5202
|
+
["2.1.109", "0.81.0"],
|
|
5203
|
+
["2.1.108", "0.81.0"],
|
|
5072
5204
|
["2.1.107", "0.81.0"],
|
|
5073
5205
|
["2.1.105", "0.81.0"],
|
|
5074
5206
|
["2.1.97", "0.208.0"],
|
|
@@ -5186,6 +5318,334 @@ function computeBillingCacheHash(firstUserMessage, version) {
|
|
|
5186
5318
|
* @param {Array} messages — The messages array from the parsed request body
|
|
5187
5319
|
* @returns {Array} — Repaired messages array
|
|
5188
5320
|
*/
|
|
5321
|
+
/**
|
|
5322
|
+
* Apply context-hint compaction to a message array. Mirrors real CC's
|
|
5323
|
+
* `applyHintEdits` (d85) + `qD4` microcompact: clears thinking/redacted_thinking
|
|
5324
|
+
* blocks from assistant messages and replaces old tool_result content with a
|
|
5325
|
+
* placeholder, keeping the last few tool results intact. Used on 422/424
|
|
5326
|
+
* responses before retrying.
|
|
5327
|
+
*
|
|
5328
|
+
* @param {Array} messages — Parsed messages array
|
|
5329
|
+
* @param {object} [opts]
|
|
5330
|
+
* @param {number} [opts.keepRecentToolResults=8] — How many most-recent tool_result blocks to preserve verbatim
|
|
5331
|
+
* @param {string} [opts.clearedPlaceholder] — Replacement content for older tool_result blocks
|
|
5332
|
+
* @returns {{ messages: Array, changed: boolean, stats: { thinkingCleared: number, toolResultsCleared: number } }}
|
|
5333
|
+
*/
|
|
5334
|
+
function applyContextHintCompaction(messages, opts = {}) {
|
|
5335
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
5336
|
+
return { messages, changed: false, stats: { thinkingCleared: 0, toolResultsCleared: 0 } };
|
|
5337
|
+
}
|
|
5338
|
+
const keepRecent = opts.keepRecentToolResults ?? 8;
|
|
5339
|
+
const placeholder = opts.clearedPlaceholder ?? "[Old tool result content cleared]";
|
|
5340
|
+
|
|
5341
|
+
// First pass: count tool_result blocks so we know which are "old" vs "recent".
|
|
5342
|
+
const toolResultRefs = [];
|
|
5343
|
+
for (let i = 0; i < messages.length; i++) {
|
|
5344
|
+
const msg = messages[i];
|
|
5345
|
+
if (msg.role !== "user" || !Array.isArray(msg.content)) continue;
|
|
5346
|
+
for (let j = 0; j < msg.content.length; j++) {
|
|
5347
|
+
if (msg.content[j]?.type === "tool_result") {
|
|
5348
|
+
toolResultRefs.push({ msgIdx: i, blockIdx: j });
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5351
|
+
}
|
|
5352
|
+
const oldCutoff = Math.max(0, toolResultRefs.length - keepRecent);
|
|
5353
|
+
const oldSet = new Set(toolResultRefs.slice(0, oldCutoff).map((r) => `${r.msgIdx}:${r.blockIdx}`));
|
|
5354
|
+
|
|
5355
|
+
let thinkingCleared = 0;
|
|
5356
|
+
let toolResultsCleared = 0;
|
|
5357
|
+
const out = messages.map((msg, i) => {
|
|
5358
|
+
if (!Array.isArray(msg.content)) return msg;
|
|
5359
|
+
if (msg.role === "assistant") {
|
|
5360
|
+
const newContent = msg.content.filter((block) => {
|
|
5361
|
+
if (block?.type === "thinking" || block?.type === "redacted_thinking") {
|
|
5362
|
+
thinkingCleared += 1;
|
|
5363
|
+
return false;
|
|
5364
|
+
}
|
|
5365
|
+
return true;
|
|
5366
|
+
});
|
|
5367
|
+
if (newContent.length !== msg.content.length) {
|
|
5368
|
+
return { ...msg, content: newContent };
|
|
5369
|
+
}
|
|
5370
|
+
return msg;
|
|
5371
|
+
}
|
|
5372
|
+
if (msg.role === "user") {
|
|
5373
|
+
let mutated = false;
|
|
5374
|
+
const newContent = msg.content.map((block, j) => {
|
|
5375
|
+
if (block?.type !== "tool_result") return block;
|
|
5376
|
+
const key = `${i}:${j}`;
|
|
5377
|
+
if (!oldSet.has(key)) return block;
|
|
5378
|
+
toolResultsCleared += 1;
|
|
5379
|
+
mutated = true;
|
|
5380
|
+
// Replace content with placeholder, preserve tool_use_id
|
|
5381
|
+
return {
|
|
5382
|
+
...block,
|
|
5383
|
+
content: placeholder,
|
|
5384
|
+
};
|
|
5385
|
+
});
|
|
5386
|
+
return mutated ? { ...msg, content: newContent } : msg;
|
|
5387
|
+
}
|
|
5388
|
+
return msg;
|
|
5389
|
+
});
|
|
5390
|
+
|
|
5391
|
+
return {
|
|
5392
|
+
messages: out,
|
|
5393
|
+
changed: thinkingCleared > 0 || toolResultsCleared > 0,
|
|
5394
|
+
stats: { thinkingCleared, toolResultsCleared },
|
|
5395
|
+
};
|
|
5396
|
+
}
|
|
5397
|
+
|
|
5398
|
+
/**
|
|
5399
|
+
* TTL-based thinking strip. When the time since the last strip exceeds the
|
|
5400
|
+
* cache TTL (roughly the point at which the prompt prefix cache would expire),
|
|
5401
|
+
* remove all `thinking` / `redacted_thinking` blocks from prior assistant
|
|
5402
|
+
* messages. Mirrors CC's `logThinkingClearLatched("ttl", ...)`.
|
|
5403
|
+
*
|
|
5404
|
+
* We keep the MOST RECENT assistant's thinking intact — chain-of-thought
|
|
5405
|
+
* continuity for the current turn matters; older ones don't.
|
|
5406
|
+
*
|
|
5407
|
+
* @param {Array} messages
|
|
5408
|
+
* @param {{ lastClearMs: number, ttlMs: number, now?: number }} ctx
|
|
5409
|
+
* @returns {{ messages: Array, changed: boolean, cleared: number, ranStripAt: number }}
|
|
5410
|
+
*/
|
|
5411
|
+
function applyTtlThinkingStrip(messages, ctx) {
|
|
5412
|
+
const now = ctx.now ?? Date.now();
|
|
5413
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
5414
|
+
return { messages, changed: false, cleared: 0, ranStripAt: ctx.lastClearMs };
|
|
5415
|
+
}
|
|
5416
|
+
if (ctx.lastClearMs > 0 && now - ctx.lastClearMs < ctx.ttlMs) {
|
|
5417
|
+
return { messages, changed: false, cleared: 0, ranStripAt: ctx.lastClearMs };
|
|
5418
|
+
}
|
|
5419
|
+
|
|
5420
|
+
// Find the last assistant message index — preserve its thinking.
|
|
5421
|
+
let lastAsstIdx = -1;
|
|
5422
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
5423
|
+
if (messages[i]?.role === "assistant") {
|
|
5424
|
+
lastAsstIdx = i;
|
|
5425
|
+
break;
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
|
|
5429
|
+
let cleared = 0;
|
|
5430
|
+
const out = messages.map((msg, i) => {
|
|
5431
|
+
if (msg.role !== "assistant" || i === lastAsstIdx || !Array.isArray(msg.content)) {
|
|
5432
|
+
return msg;
|
|
5433
|
+
}
|
|
5434
|
+
const newContent = msg.content.filter((b) => {
|
|
5435
|
+
if (b?.type === "thinking" || b?.type === "redacted_thinking") {
|
|
5436
|
+
cleared += 1;
|
|
5437
|
+
return false;
|
|
5438
|
+
}
|
|
5439
|
+
return true;
|
|
5440
|
+
});
|
|
5441
|
+
return newContent.length !== msg.content.length ? { ...msg, content: newContent } : msg;
|
|
5442
|
+
});
|
|
5443
|
+
|
|
5444
|
+
return { messages: out, changed: cleared > 0, cleared, ranStripAt: cleared > 0 ? now : ctx.lastClearMs };
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5447
|
+
/**
|
|
5448
|
+
* Proactive microcompact — client-side, runs BEFORE the request goes out.
|
|
5449
|
+
* At or above `percent` of the model's context window, replace old
|
|
5450
|
+
* tool_result.content with a placeholder (keeping last `keepRecent` verbatim).
|
|
5451
|
+
*
|
|
5452
|
+
* Returns the new messages array + change stats.
|
|
5453
|
+
*
|
|
5454
|
+
* @param {Array} messages
|
|
5455
|
+
* @param {{ estimatedTokens: number, contextWindow: number, percent: number, keepRecent: number }} ctx
|
|
5456
|
+
* @returns {{ messages: Array, changed: boolean, cleared: number, triggered: boolean }}
|
|
5457
|
+
*/
|
|
5458
|
+
function applyProactiveMicrocompact(messages, ctx) {
|
|
5459
|
+
const threshold = ctx.contextWindow * (ctx.percent / 100);
|
|
5460
|
+
if (ctx.estimatedTokens < threshold) {
|
|
5461
|
+
return { messages, changed: false, cleared: 0, triggered: false };
|
|
5462
|
+
}
|
|
5463
|
+
const result = applyContextHintCompaction(messages, { keepRecentToolResults: ctx.keepRecent });
|
|
5464
|
+
return {
|
|
5465
|
+
messages: result.messages,
|
|
5466
|
+
changed: result.changed,
|
|
5467
|
+
cleared: result.stats.toolResultsCleared,
|
|
5468
|
+
triggered: true,
|
|
5469
|
+
};
|
|
5470
|
+
}
|
|
5471
|
+
|
|
5472
|
+
/**
|
|
5473
|
+
* Stable tool ordering — sort tools by name so the system-prompt prefix stays
|
|
5474
|
+
* cache-stable across turns. Safe: tool semantics are name-based, not index-based.
|
|
5475
|
+
*
|
|
5476
|
+
* @param {any[]} tools
|
|
5477
|
+
* @returns {any[]}
|
|
5478
|
+
*/
|
|
5479
|
+
function applyStableToolOrdering(tools) {
|
|
5480
|
+
if (!Array.isArray(tools) || tools.length < 2) return tools;
|
|
5481
|
+
// Preserve a pinned "first" for tools whose position is load-bearing (none today).
|
|
5482
|
+
return [...tools].sort((a, b) => {
|
|
5483
|
+
const an = typeof a?.name === "string" ? a.name : "";
|
|
5484
|
+
const bn = typeof b?.name === "string" ? b.name : "";
|
|
5485
|
+
return an < bn ? -1 : an > bn ? 1 : 0;
|
|
5486
|
+
});
|
|
5487
|
+
}
|
|
5488
|
+
|
|
5489
|
+
/**
|
|
5490
|
+
* Tool schema deferral — replace the `input_schema` of deferred tools with a
|
|
5491
|
+
* minimal placeholder until the tool has been invoked in this session.
|
|
5492
|
+
*
|
|
5493
|
+
* @param {any[]} tools
|
|
5494
|
+
* @param {{ deferred: Set<string>, invoked: Set<string> }} ctx
|
|
5495
|
+
* @returns {{ tools: any[], deferredCount: number }}
|
|
5496
|
+
*/
|
|
5497
|
+
function applyToolSchemaDeferral(tools, ctx) {
|
|
5498
|
+
if (!Array.isArray(tools) || ctx.deferred.size === 0) {
|
|
5499
|
+
return { tools, deferredCount: 0 };
|
|
5500
|
+
}
|
|
5501
|
+
let deferredCount = 0;
|
|
5502
|
+
const out = tools.map((t) => {
|
|
5503
|
+
const name = typeof t?.name === "string" ? t.name : "";
|
|
5504
|
+
if (!ctx.deferred.has(name) || ctx.invoked.has(name)) return t;
|
|
5505
|
+
deferredCount += 1;
|
|
5506
|
+
// Minimal schema — `type:object` with no properties is accepted by the API.
|
|
5507
|
+
return {
|
|
5508
|
+
...t,
|
|
5509
|
+
input_schema: { type: "object", properties: {}, additionalProperties: true },
|
|
5510
|
+
};
|
|
5511
|
+
});
|
|
5512
|
+
return { tools: out, deferredCount };
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5515
|
+
/**
|
|
5516
|
+
* Adaptive thinking — zero the thinking budget for trivially simple follow-ups.
|
|
5517
|
+
* "Simple" heuristic: most recent user message is short (<200 chars), no file
|
|
5518
|
+
* references, and the conversation is past turn 1 (so we have context).
|
|
5519
|
+
*
|
|
5520
|
+
* @param {any} parsed Parsed request body (mutated in place if simple)
|
|
5521
|
+
* @returns {{ applied: boolean, previousBudget: number | null }}
|
|
5522
|
+
*/
|
|
5523
|
+
function applyAdaptiveThinkingZero(parsed) {
|
|
5524
|
+
if (!parsed || !parsed.thinking || parsed.thinking.type !== "enabled") {
|
|
5525
|
+
return { applied: false, previousBudget: null };
|
|
5526
|
+
}
|
|
5527
|
+
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
5528
|
+
if (messages.length < 2) return { applied: false, previousBudget: null };
|
|
5529
|
+
const last = messages[messages.length - 1];
|
|
5530
|
+
if (!last || last.role !== "user") return { applied: false, previousBudget: null };
|
|
5531
|
+
|
|
5532
|
+
let userText = "";
|
|
5533
|
+
if (typeof last.content === "string") userText = last.content;
|
|
5534
|
+
else if (Array.isArray(last.content)) {
|
|
5535
|
+
for (const b of last.content) {
|
|
5536
|
+
if (b?.type === "text" && typeof b.text === "string") userText += b.text;
|
|
5537
|
+
if (b?.type === "tool_result") return { applied: false, previousBudget: null };
|
|
5538
|
+
}
|
|
5539
|
+
}
|
|
5540
|
+
if (userText.length > 200) return { applied: false, previousBudget: null };
|
|
5541
|
+
if (/\b(analyze|refactor|design|review|audit|plan)\b/i.test(userText)) {
|
|
5542
|
+
return { applied: false, previousBudget: null };
|
|
5543
|
+
}
|
|
5544
|
+
|
|
5545
|
+
const previousBudget = typeof parsed.thinking.budget_tokens === "number" ? parsed.thinking.budget_tokens : null;
|
|
5546
|
+
// "Zero" means remove thinking entirely — API disallows budget_tokens:0.
|
|
5547
|
+
delete parsed.thinking;
|
|
5548
|
+
if (typeof parsed.temperature !== "number") parsed.temperature = 1;
|
|
5549
|
+
return { applied: true, previousBudget };
|
|
5550
|
+
}
|
|
5551
|
+
|
|
5552
|
+
/**
|
|
5553
|
+
* Cross-turn tool_result dedupe — when the same (tool name, input) pair
|
|
5554
|
+
* appeared earlier in the conversation, replace the later result content
|
|
5555
|
+
* with a pointer string. Safe-set only: Read, Grep, Glob, LS.
|
|
5556
|
+
*
|
|
5557
|
+
* @param {Array} messages
|
|
5558
|
+
* @param {{ seen: Map<string, string>, safeTools: Set<string> }} ctx
|
|
5559
|
+
* @returns {{ messages: Array, changed: boolean, deduped: number }}
|
|
5560
|
+
*/
|
|
5561
|
+
function applyToolResultDedupe(messages, ctx) {
|
|
5562
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
5563
|
+
return { messages, changed: false, deduped: 0 };
|
|
5564
|
+
}
|
|
5565
|
+
// First pass: build map of tool_use_id → { name, inputHash } from assistant messages.
|
|
5566
|
+
const idToKey = new Map();
|
|
5567
|
+
for (const msg of messages) {
|
|
5568
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
5569
|
+
for (const b of msg.content) {
|
|
5570
|
+
if (b?.type !== "tool_use") continue;
|
|
5571
|
+
if (!ctx.safeTools.has(b.name)) continue;
|
|
5572
|
+
const inputStr = JSON.stringify(b.input ?? {});
|
|
5573
|
+
idToKey.set(b.id, `${b.name}::${inputStr}`);
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
|
|
5577
|
+
let deduped = 0;
|
|
5578
|
+
const out = messages.map((msg) => {
|
|
5579
|
+
if (msg.role !== "user" || !Array.isArray(msg.content)) return msg;
|
|
5580
|
+
let mutated = false;
|
|
5581
|
+
const newContent = msg.content.map((b) => {
|
|
5582
|
+
if (b?.type !== "tool_result") return b;
|
|
5583
|
+
const key = idToKey.get(b.tool_use_id);
|
|
5584
|
+
if (!key) return b;
|
|
5585
|
+
const firstSeen = ctx.seen.get(key);
|
|
5586
|
+
if (firstSeen && firstSeen !== b.tool_use_id) {
|
|
5587
|
+
deduped += 1;
|
|
5588
|
+
mutated = true;
|
|
5589
|
+
return { ...b, content: `[Identical to tool_use_id=${firstSeen}]` };
|
|
5590
|
+
}
|
|
5591
|
+
if (!firstSeen) ctx.seen.set(key, b.tool_use_id);
|
|
5592
|
+
return b;
|
|
5593
|
+
});
|
|
5594
|
+
return mutated ? { ...msg, content: newContent } : msg;
|
|
5595
|
+
});
|
|
5596
|
+
|
|
5597
|
+
return { messages: out, changed: deduped > 0, deduped };
|
|
5598
|
+
}
|
|
5599
|
+
|
|
5600
|
+
/**
|
|
5601
|
+
* Trailing-summary trimmer — strip the final text block of past assistant
|
|
5602
|
+
* messages if it looks like a summary (ends with "...done" / "summary" /
|
|
5603
|
+
* numbered list / "I've X'd Y"). Only applies to messages past the last one.
|
|
5604
|
+
*
|
|
5605
|
+
* @param {Array} messages
|
|
5606
|
+
* @returns {{ messages: Array, changed: boolean, trimmed: number }}
|
|
5607
|
+
*/
|
|
5608
|
+
function applyTrailingSummaryTrim(messages) {
|
|
5609
|
+
if (!Array.isArray(messages) || messages.length < 2) {
|
|
5610
|
+
return { messages, changed: false, trimmed: 0 };
|
|
5611
|
+
}
|
|
5612
|
+
const SUMMARY_PATTERNS = [
|
|
5613
|
+
/\b(summary|summar(y|ised|ized)):/i,
|
|
5614
|
+
/\bto summari[sz]e\b/i,
|
|
5615
|
+
/^\s*in (summary|short|brief)/im,
|
|
5616
|
+
/\bi['']ve (done|completed|implemented|added|updated|fixed) /i,
|
|
5617
|
+
/\bthat's it\b/i,
|
|
5618
|
+
];
|
|
5619
|
+
|
|
5620
|
+
let lastAsstIdx = -1;
|
|
5621
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
5622
|
+
if (messages[i]?.role === "assistant") {
|
|
5623
|
+
lastAsstIdx = i;
|
|
5624
|
+
break;
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
|
|
5628
|
+
let trimmed = 0;
|
|
5629
|
+
const out = messages.map((msg, i) => {
|
|
5630
|
+
if (msg.role !== "assistant" || i === lastAsstIdx) return msg;
|
|
5631
|
+
if (!Array.isArray(msg.content)) return msg;
|
|
5632
|
+
|
|
5633
|
+
const last = msg.content[msg.content.length - 1];
|
|
5634
|
+
if (!last || last.type !== "text" || typeof last.text !== "string") return msg;
|
|
5635
|
+
const text = last.text;
|
|
5636
|
+
if (text.length < 80) return msg;
|
|
5637
|
+
const isSummary = SUMMARY_PATTERNS.some((p) => p.test(text));
|
|
5638
|
+
if (!isSummary) return msg;
|
|
5639
|
+
|
|
5640
|
+
trimmed += 1;
|
|
5641
|
+
const newContent = msg.content.slice(0, -1);
|
|
5642
|
+
if (newContent.length === 0) return msg; // keep at least one block
|
|
5643
|
+
return { ...msg, content: newContent };
|
|
5644
|
+
});
|
|
5645
|
+
|
|
5646
|
+
return { messages: out, changed: trimmed > 0, trimmed };
|
|
5647
|
+
}
|
|
5648
|
+
|
|
5189
5649
|
function repairOrphanedToolUseBlocks(messages) {
|
|
5190
5650
|
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
5191
5651
|
|
|
@@ -5357,6 +5817,23 @@ const BEDROCK_UNSUPPORTED_BETAS = new Set([
|
|
|
5357
5817
|
"context-1m-2025-08-07",
|
|
5358
5818
|
"tool-search-tool-2025-10-19",
|
|
5359
5819
|
]);
|
|
5820
|
+
// OpenCode SDK betas that leak through the host's Anthropic SDK but are NOT
|
|
5821
|
+
// part of CC's beta vocabulary. Filtered out when signature emulation is on.
|
|
5822
|
+
// Core tool names (CC PascalCase) that are always eager-loaded.
|
|
5823
|
+
const CORE_TOOL_NAMES = new Set([
|
|
5824
|
+
"Bash",
|
|
5825
|
+
"Read",
|
|
5826
|
+
"Glob",
|
|
5827
|
+
"Grep",
|
|
5828
|
+
"Edit",
|
|
5829
|
+
"Write",
|
|
5830
|
+
"WebFetch",
|
|
5831
|
+
"TodoWrite",
|
|
5832
|
+
"Skill",
|
|
5833
|
+
"Task",
|
|
5834
|
+
"Compress",
|
|
5835
|
+
]);
|
|
5836
|
+
const HOST_SDK_BETAS_BLOCKLIST = new Set(["fine-grained-tool-streaming-2025-05-14", "structured-outputs-2025-11-13"]);
|
|
5360
5837
|
const EXPERIMENTAL_BETA_FLAGS = new Set([
|
|
5361
5838
|
"adaptive-thinking-2026-01-28",
|
|
5362
5839
|
"advanced-tool-use-2025-11-20",
|
|
@@ -5365,6 +5842,7 @@ const EXPERIMENTAL_BETA_FLAGS = new Set([
|
|
|
5365
5842
|
"code-execution-2025-08-25",
|
|
5366
5843
|
"compact-2026-01-12",
|
|
5367
5844
|
"context-1m-2025-08-07",
|
|
5845
|
+
"context-hint-2026-04-09",
|
|
5368
5846
|
"context-management-2025-06-27",
|
|
5369
5847
|
"fast-mode-2026-02-01",
|
|
5370
5848
|
"files-api-2025-04-14",
|
|
@@ -5380,6 +5858,8 @@ const BETA_SHORTCUTS = new Map([
|
|
|
5380
5858
|
["1m", "context-1m-2025-08-07"],
|
|
5381
5859
|
["1m-context", "context-1m-2025-08-07"],
|
|
5382
5860
|
["context-1m", "context-1m-2025-08-07"],
|
|
5861
|
+
["context-hint", "context-hint-2026-04-09"],
|
|
5862
|
+
["hint", "context-hint-2026-04-09"],
|
|
5383
5863
|
["fast", "fast-mode-2026-02-01"],
|
|
5384
5864
|
["fast-mode", "fast-mode-2026-02-01"],
|
|
5385
5865
|
["opus-fast", "fast-mode-2026-02-01"],
|
|
@@ -5693,6 +6173,46 @@ function detectProvider(requestUrl) {
|
|
|
5693
6173
|
return "anthropic";
|
|
5694
6174
|
}
|
|
5695
6175
|
|
|
6176
|
+
/**
|
|
6177
|
+
* Classify a request by inferred role, mirroring CC's `querySource` gate.
|
|
6178
|
+
* CC gates features like context-hint on `querySource.startsWith("repl_main_thread")`.
|
|
6179
|
+
* We don't have that string on the wire, so we infer from body shape.
|
|
6180
|
+
*
|
|
6181
|
+
* Returns one of:
|
|
6182
|
+
* - "main" → interactive main thread (long system, normal max_tokens, messages present)
|
|
6183
|
+
* - "title" → title / name generation (tiny max_tokens, 1 message)
|
|
6184
|
+
* - "small" → short background query (small max_tokens but not title)
|
|
6185
|
+
* - "empty" → pre-warm / no messages
|
|
6186
|
+
* - "unknown" → treat as main for safety
|
|
6187
|
+
*
|
|
6188
|
+
* @param {any} parsed Parsed request body
|
|
6189
|
+
* @returns {"main"|"title"|"small"|"empty"|"unknown"}
|
|
6190
|
+
*/
|
|
6191
|
+
function classifyRequestRole(parsed) {
|
|
6192
|
+
if (!parsed || typeof parsed !== "object") return "unknown";
|
|
6193
|
+
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
6194
|
+
const maxTokens = typeof parsed.max_tokens === "number" ? parsed.max_tokens : null;
|
|
6195
|
+
|
|
6196
|
+
if (messages.length === 0) return "empty";
|
|
6197
|
+
if (maxTokens != null) {
|
|
6198
|
+
if (maxTokens <= 256 && messages.length <= 2) return "title";
|
|
6199
|
+
if (maxTokens <= 1024) return "small";
|
|
6200
|
+
}
|
|
6201
|
+
// System prompt length heuristic
|
|
6202
|
+
let sysLen = 0;
|
|
6203
|
+
if (typeof parsed.system === "string") {
|
|
6204
|
+
sysLen = parsed.system.length;
|
|
6205
|
+
} else if (Array.isArray(parsed.system)) {
|
|
6206
|
+
for (const s of parsed.system) {
|
|
6207
|
+
if (s && typeof s.text === "string") sysLen += s.text.length;
|
|
6208
|
+
}
|
|
6209
|
+
}
|
|
6210
|
+
if (sysLen < 200 && messages.length <= 2 && (maxTokens == null || maxTokens <= 2048)) {
|
|
6211
|
+
return "small";
|
|
6212
|
+
}
|
|
6213
|
+
return "main";
|
|
6214
|
+
}
|
|
6215
|
+
|
|
5696
6216
|
/**
|
|
5697
6217
|
* @param {any} body
|
|
5698
6218
|
* @returns {{model: string, tools: any[], messages: any[], hasFileReferences: boolean}}
|
|
@@ -5883,6 +6403,55 @@ function sanitizeSystemText(text) {
|
|
|
5883
6403
|
* @param {'minimal' | 'off'} mode
|
|
5884
6404
|
* @returns {string}
|
|
5885
6405
|
*/
|
|
6406
|
+
function tailSystemBlock(text, maxChars, turnThreshold) {
|
|
6407
|
+
const lines = text.split("\n");
|
|
6408
|
+
const kept = [];
|
|
6409
|
+
let charCount = 0;
|
|
6410
|
+
const importantRe = /\b(MUST|NEVER|CRITICAL|IMPORTANT|REQUIRED|DO NOT|ALWAYS|FORBIDDEN)\b/i;
|
|
6411
|
+
const headerRe = /^#{1,4}\s/;
|
|
6412
|
+
const listItemRe = /^\s*[-*]\s/;
|
|
6413
|
+
// Always keep the first paragraph (identity/role definition)
|
|
6414
|
+
let firstParaEnd = 0;
|
|
6415
|
+
for (let j = 0; j < lines.length; j++) {
|
|
6416
|
+
if (lines[j].trim() === "" && j > 0) {
|
|
6417
|
+
firstParaEnd = j;
|
|
6418
|
+
break;
|
|
6419
|
+
}
|
|
6420
|
+
}
|
|
6421
|
+
if (firstParaEnd === 0) firstParaEnd = Math.min(5, lines.length);
|
|
6422
|
+
for (let j = 0; j <= firstParaEnd; j++) {
|
|
6423
|
+
kept.push(lines[j]);
|
|
6424
|
+
charCount += (lines[j]?.length || 0) + 1;
|
|
6425
|
+
}
|
|
6426
|
+
// Scan remaining lines: keep headers, important constraints, short list items
|
|
6427
|
+
for (let j = firstParaEnd + 1; j < lines.length; j++) {
|
|
6428
|
+
const line = lines[j];
|
|
6429
|
+
const isHeader = headerRe.test(line);
|
|
6430
|
+
const isImportant = importantRe.test(line);
|
|
6431
|
+
const isShortListItem = listItemRe.test(line) && line.length < 120;
|
|
6432
|
+
if (isHeader || isImportant || isShortListItem) {
|
|
6433
|
+
if (charCount + line.length + 1 > maxChars) break;
|
|
6434
|
+
kept.push(line);
|
|
6435
|
+
charCount += line.length + 1;
|
|
6436
|
+
}
|
|
6437
|
+
}
|
|
6438
|
+
kept.push("", "[Verbose instructions trimmed after turn " + turnThreshold + ". Key constraints preserved above.]");
|
|
6439
|
+
return kept.join("\n");
|
|
6440
|
+
}
|
|
6441
|
+
|
|
6442
|
+
function compactToolDescription(text) {
|
|
6443
|
+
return text
|
|
6444
|
+
.replace(/<example[\s\S]*?<\/example>/gi, "")
|
|
6445
|
+
.replace(/\|[\s|:-]+\|/g, "")
|
|
6446
|
+
.replace(/^\|.*\|$/gm, "")
|
|
6447
|
+
.replace(/^(?:\s*[-*]\s+.{200,})$/gm, (m) => m.slice(0, 200) + "...")
|
|
6448
|
+
.replace(/^(#{1,3}\s+)/gm, "")
|
|
6449
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
6450
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
6451
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
6452
|
+
.trim();
|
|
6453
|
+
}
|
|
6454
|
+
|
|
5886
6455
|
function compactSystemText(text, mode) {
|
|
5887
6456
|
const withoutDuplicateIdentityPrefix = text.startsWith(`${CLAUDE_CODE_IDENTITY_STRING}\n`)
|
|
5888
6457
|
? text.slice(CLAUDE_CODE_IDENTITY_STRING.length).trimStart()
|
|
@@ -6231,6 +6800,21 @@ function buildSystemPromptBlocks(system, signature) {
|
|
|
6231
6800
|
return sanitized;
|
|
6232
6801
|
}
|
|
6233
6802
|
|
|
6803
|
+
// Lean system prompt for non-main requests (title-gen is already handled
|
|
6804
|
+
// above via the COMPACT_TITLE_GENERATOR_SYSTEM_PROMPT swap). For "title"
|
|
6805
|
+
// and "small" request roles — one-off queries that don't belong to the main
|
|
6806
|
+
// REPL thread — we skip billing identity + CC identity injection. This
|
|
6807
|
+
// matches the spirit of real CC's querySource gates: identity context is
|
|
6808
|
+
// for interactive main-thread conversations, not fire-and-forget calls.
|
|
6809
|
+
// Opt-in (default off) because it changes the system-prompt shape.
|
|
6810
|
+
const leanNonMain =
|
|
6811
|
+
signature.leanNonMain === true &&
|
|
6812
|
+
(signature.requestRole === "title" || signature.requestRole === "small") &&
|
|
6813
|
+
!titleGeneratorRequest;
|
|
6814
|
+
if (leanNonMain) {
|
|
6815
|
+
return sanitized;
|
|
6816
|
+
}
|
|
6817
|
+
|
|
6234
6818
|
// Build attribution header
|
|
6235
6819
|
const billingHeader = buildAnthropicBillingHeader(
|
|
6236
6820
|
signature.claudeCliVersion,
|
|
@@ -6386,6 +6970,31 @@ function buildAnthropicBetaHeader(
|
|
|
6386
6970
|
betas.push("web-search-2025-03-05");
|
|
6387
6971
|
}
|
|
6388
6972
|
|
|
6973
|
+
// Advisor tool — in CC this is gated by server-side feature flag
|
|
6974
|
+
// (tengu_sage_compass2) and firstParty+isLoggedIn. Since we can't check
|
|
6975
|
+
// CC's feature flags, include it unconditionally for Claude 4+ models.
|
|
6976
|
+
// CC v108 sends it in MITM captures for Max/Pro users.
|
|
6977
|
+
if (!/claude-3-/i.test(model)) {
|
|
6978
|
+
betas.push("advisor-tool-2026-03-01");
|
|
6979
|
+
}
|
|
6980
|
+
|
|
6981
|
+
// context-hint-2026-04-09 — introduced in CC v2.1.110. Paired with the body
|
|
6982
|
+
// field `context_hint: { enabled: true }` (injected in transformRequestBody).
|
|
6983
|
+
// Real CC gates it on includeFirstPartyBetas + querySource startsWith
|
|
6984
|
+
// "repl_main_thread" (i.e. interactive sessions on 1P Anthropic only).
|
|
6985
|
+
// Server responds with hints suggesting which messages to forget for
|
|
6986
|
+
// token-efficient context compaction. Sticky: disabled permanently on
|
|
6987
|
+
// 400/409/529 errors referencing the hint. Users can opt out via
|
|
6988
|
+
// token_economy.context_hint = false.
|
|
6989
|
+
const isFirstPartyProvider = provider !== "vertex" && provider !== "bedrock" && provider !== "mantle";
|
|
6990
|
+
// Mimicry note: real CC gates context-hint on querySource.startsWith("repl_main_thread").
|
|
6991
|
+
// We infer main-thread from body shape via classifyRequestRole and pass it via
|
|
6992
|
+
// tokenEconomy.__requestRole. Treat absent marker as "main" for backward compat.
|
|
6993
|
+
const _isMainThread = te.__requestRole == null || te.__requestRole === "main";
|
|
6994
|
+
if (isFirstPartyProvider && !/claude-3-/i.test(model) && te.context_hint !== false && _isMainThread) {
|
|
6995
|
+
betas.push("context-hint-2026-04-09");
|
|
6996
|
+
}
|
|
6997
|
+
|
|
6389
6998
|
// Files API — scoped to file endpoints/references
|
|
6390
6999
|
if (isFilesEndpoint || hasFileReferences) {
|
|
6391
7000
|
betas.push("files-api-2025-04-14");
|
|
@@ -6396,16 +7005,15 @@ function buildAnthropicBetaHeader(
|
|
|
6396
7005
|
betas.push(TOKEN_COUNTING_BETA_FLAG);
|
|
6397
7006
|
}
|
|
6398
7007
|
|
|
6399
|
-
// === TOKEN ECONOMY BETAS (
|
|
7008
|
+
// === TOKEN ECONOMY BETAS (on by default for token savings) ===
|
|
6400
7009
|
|
|
6401
7010
|
// redact-thinking: suppresses thinking summaries server-side.
|
|
6402
|
-
//
|
|
6403
|
-
//
|
|
7011
|
+
// CC v108 enables this by default but we keep it off so thinking is visible.
|
|
7012
|
+
// Users can opt in via `/anthropic set redact-thinking on`.
|
|
6404
7013
|
if (te.redact_thinking && !disableExperimentalBetas) {
|
|
6405
7014
|
betas.push("redact-thinking-2026-02-12");
|
|
6406
7015
|
}
|
|
6407
7016
|
|
|
6408
|
-
// summarize-connector-text-2026-03-13 was removed in v2.1.90 (dead slot njq="").
|
|
6409
7017
|
// compact-2026-01-12 and mcp-client-2025-11-20 exist only in docs, not runtime.
|
|
6410
7018
|
|
|
6411
7019
|
// afk-mode — NOT auto-included (requires user opt-in)
|
|
@@ -6418,8 +7026,11 @@ function buildAnthropicBetaHeader(
|
|
|
6418
7026
|
}
|
|
6419
7027
|
}
|
|
6420
7028
|
|
|
6421
|
-
// Merge incoming betas from the original request
|
|
6422
|
-
|
|
7029
|
+
// Merge incoming betas from the original request, filtering out host-injected
|
|
7030
|
+
// betas (e.g. fine-grained-tool-streaming-2025-05-14, structured-outputs-2025-11-13)
|
|
7031
|
+
// that OpenCode's Anthropic SDK adds but real Claude Code never sends.
|
|
7032
|
+
const filteredIncoming = incomingBetasList.filter((b) => !HOST_SDK_BETAS_BLOCKLIST.has(b));
|
|
7033
|
+
let mergedBetas = [...new Set([...betas, ...filteredIncoming])];
|
|
6423
7034
|
|
|
6424
7035
|
// Add custom betas from config
|
|
6425
7036
|
if (customBetas?.length) {
|
|
@@ -6713,7 +7324,7 @@ function resolveMaxTokens(body, config) {
|
|
|
6713
7324
|
*
|
|
6714
7325
|
* @param {string | undefined} body
|
|
6715
7326
|
* @param {{enabled: boolean, claudeCliVersion: string, promptCompactionMode: 'minimal' | 'off', provider?: string}} signature
|
|
6716
|
-
* @param {{persistentUserId: string, sessionId: string, accountId: string}} runtime
|
|
7327
|
+
* @param {{persistentUserId: string, sessionId: string, accountId: string, turns?: number, usedTools?: Set<string>}} runtime
|
|
6717
7328
|
* @param {string} [betaHeader] - Pre-computed anthropic-beta header value to inject into the body.
|
|
6718
7329
|
* @param {import('./lib/config.mjs').AnthropicAuthConfig} [config] - Plugin configuration for output cap
|
|
6719
7330
|
* @returns {string | undefined}
|
|
@@ -6805,14 +7416,127 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
|
|
|
6805
7416
|
parsed.messages = stripSlashCommandMessages(parsed.messages);
|
|
6806
7417
|
}
|
|
6807
7418
|
|
|
7419
|
+
// === Token economy: layered message/history compaction ===
|
|
7420
|
+
// Only applies to main-thread requests (subagents/title-gen stay untouched).
|
|
7421
|
+
// Strategies stack in order: TTL thinking strip → proactive microcompact →
|
|
7422
|
+
// trailing-summary trim → tool_result dedupe. Each is independently gated.
|
|
7423
|
+
const te = config?.token_economy || {};
|
|
7424
|
+
const tes = runtime?.tokenEconomySession;
|
|
7425
|
+
const isMainRole = runtime?.requestRole === "main" || runtime?.requestRole == null;
|
|
7426
|
+
|
|
7427
|
+
if (isMainRole && Array.isArray(parsed.messages) && tes) {
|
|
7428
|
+
// (1) TTL-based thinking strip
|
|
7429
|
+
if (te.ttl_thinking_strip !== false) {
|
|
7430
|
+
const ttlMs = signature?.cachePolicy?.ttl === "5m" ? 5 * 60_000 : 60 * 60_000;
|
|
7431
|
+
const res = applyTtlThinkingStrip(parsed.messages, {
|
|
7432
|
+
lastClearMs: tes.lastThinkingStripMs,
|
|
7433
|
+
ttlMs,
|
|
7434
|
+
});
|
|
7435
|
+
if (res.changed) {
|
|
7436
|
+
parsed.messages = res.messages;
|
|
7437
|
+
tes.lastThinkingStripMs = res.ranStripAt;
|
|
7438
|
+
tes.thinkingStripped += res.cleared;
|
|
7439
|
+
}
|
|
7440
|
+
}
|
|
7441
|
+
|
|
7442
|
+
// (2) Proactive microcompact (client-side, pre-422)
|
|
7443
|
+
if (te.proactive_microcompact !== false) {
|
|
7444
|
+
const estimated = estimatePromptTokensFromParsed(parsed);
|
|
7445
|
+
const cw = 200_000; // conservative — 1M models still benefit
|
|
7446
|
+
const res = applyProactiveMicrocompact(parsed.messages, {
|
|
7447
|
+
estimatedTokens: estimated,
|
|
7448
|
+
contextWindow: cw,
|
|
7449
|
+
percent: te.microcompact_percent ?? 70,
|
|
7450
|
+
keepRecent: te.microcompact_keep_recent ?? 8,
|
|
7451
|
+
});
|
|
7452
|
+
if (res.changed) {
|
|
7453
|
+
parsed.messages = res.messages;
|
|
7454
|
+
tes.lastMicrocompactMs = Date.now();
|
|
7455
|
+
tes.toolResultsCompacted += res.cleared;
|
|
7456
|
+
}
|
|
7457
|
+
}
|
|
7458
|
+
|
|
7459
|
+
// (3) Trailing-summary trim (opt-in)
|
|
7460
|
+
if (te.trailing_summary_trim === true) {
|
|
7461
|
+
const res = applyTrailingSummaryTrim(parsed.messages);
|
|
7462
|
+
if (res.changed) parsed.messages = res.messages;
|
|
7463
|
+
}
|
|
7464
|
+
|
|
7465
|
+
// (4) Cross-turn tool_result dedupe (opt-in)
|
|
7466
|
+
if (te.tool_result_dedupe === true) {
|
|
7467
|
+
const SAFE_READ_TOOLS = new Set(["Read", "Grep", "Glob", "LS", "BashOutput"]);
|
|
7468
|
+
const res = applyToolResultDedupe(parsed.messages, {
|
|
7469
|
+
seen: tes.seenContentHashes,
|
|
7470
|
+
safeTools: SAFE_READ_TOOLS,
|
|
7471
|
+
});
|
|
7472
|
+
if (res.changed) parsed.messages = res.messages;
|
|
7473
|
+
}
|
|
7474
|
+
}
|
|
7475
|
+
|
|
7476
|
+
// === Token economy: tool-array transforms (stable ordering, deferral) ===
|
|
7477
|
+
if (isMainRole && Array.isArray(parsed.tools)) {
|
|
7478
|
+
if (te.stable_tool_ordering !== false) {
|
|
7479
|
+
parsed.tools = applyStableToolOrdering(parsed.tools);
|
|
7480
|
+
}
|
|
7481
|
+
if (Array.isArray(te.deferred_tool_names) && te.deferred_tool_names.length > 0) {
|
|
7482
|
+
// "Invoked" means any assistant message in the convo has used the tool.
|
|
7483
|
+
const invoked = new Set();
|
|
7484
|
+
if (Array.isArray(parsed.messages)) {
|
|
7485
|
+
for (const m of parsed.messages) {
|
|
7486
|
+
if (m.role !== "assistant" || !Array.isArray(m.content)) continue;
|
|
7487
|
+
for (const b of m.content) {
|
|
7488
|
+
if (b?.type === "tool_use" && typeof b.name === "string") invoked.add(b.name);
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
}
|
|
7492
|
+
const res = applyToolSchemaDeferral(parsed.tools, {
|
|
7493
|
+
deferred: new Set(te.deferred_tool_names),
|
|
7494
|
+
invoked,
|
|
7495
|
+
});
|
|
7496
|
+
parsed.tools = res.tools;
|
|
7497
|
+
}
|
|
7498
|
+
}
|
|
7499
|
+
|
|
7500
|
+
// === Token economy: adaptive thinking zero-out for trivial follow-ups ===
|
|
7501
|
+
if (isMainRole && te.adaptive_thinking_zero_simple !== false) {
|
|
7502
|
+
applyAdaptiveThinkingZero(parsed);
|
|
7503
|
+
}
|
|
7504
|
+
|
|
6808
7505
|
// QA fix H2: avoid mutating the signature parameter; capture modelId locally
|
|
6809
7506
|
const modelId = parsed.model || "";
|
|
6810
7507
|
// Extract first user message text for billing hash computation (cch)
|
|
6811
7508
|
const firstUserMessage = extractFirstUserMessageText(parsed.messages);
|
|
6812
|
-
const signatureWithModel = {
|
|
7509
|
+
const signatureWithModel = {
|
|
7510
|
+
...signature,
|
|
7511
|
+
modelId,
|
|
7512
|
+
firstUserMessage,
|
|
7513
|
+
antiVerbosity: config?.anti_verbosity,
|
|
7514
|
+
// Role-aware system-prompt leaning: for non-main-thread requests (title,
|
|
7515
|
+
// small, empty shapes) strip billing identity + CC identity injection.
|
|
7516
|
+
// Title-gen path is handled separately by isTitleGeneratorSystemBlocks().
|
|
7517
|
+
// Default off — opt-in via `token_economy.lean_system_non_main: true`.
|
|
7518
|
+
requestRole: runtime?.requestRole,
|
|
7519
|
+
leanNonMain: config?.token_economy?.lean_system_non_main === true,
|
|
7520
|
+
};
|
|
6813
7521
|
// Sanitize system prompt and optionally inject Claude Code identity/billing blocks.
|
|
6814
7522
|
parsed.system = buildSystemPromptBlocks(normalizeSystemTextBlocks(parsed.system), signatureWithModel);
|
|
6815
7523
|
|
|
7524
|
+
// Strategy 5 — System prompt tailing: after N turns, trim large system blocks
|
|
7525
|
+
// to essential sections only. The model has internalized verbose instructions
|
|
7526
|
+
// (shell strategy, package manager tables, delegation protocols) by this point.
|
|
7527
|
+
// Preserves: first paragraph (identity/role), lines containing MUST/NEVER/CRITICAL/
|
|
7528
|
+
// IMPORTANT, section headers, and short blocks. Drops verbose body paragraphs.
|
|
7529
|
+
const tailThreshold = signature.systemPromptTailTurns ?? 6;
|
|
7530
|
+
if (signature.systemPromptTailing !== false && runtime.turns >= tailThreshold && Array.isArray(parsed.system)) {
|
|
7531
|
+
const maxChars = signature.systemPromptTailMaxChars ?? 2000;
|
|
7532
|
+
for (let i = 0; i < parsed.system.length; i++) {
|
|
7533
|
+
const block = parsed.system[i];
|
|
7534
|
+
if (block.type === "text" && block.text && block.text.length > maxChars * 2) {
|
|
7535
|
+
block.text = tailSystemBlock(block.text, maxChars, tailThreshold);
|
|
7536
|
+
}
|
|
7537
|
+
}
|
|
7538
|
+
}
|
|
7539
|
+
|
|
6816
7540
|
// Token budget (A9): parse NL budget from last user message, inject status block
|
|
6817
7541
|
if (config?.token_budget?.enabled && Array.isArray(parsed.messages)) {
|
|
6818
7542
|
const budgetExpr = parseNaturalLanguageBudget(parsed.messages);
|
|
@@ -6861,13 +7585,24 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
|
|
|
6861
7585
|
) {
|
|
6862
7586
|
// Strip ALL incoming cache_control from tools and messages to prevent
|
|
6863
7587
|
// TTL ordering violations (host SDK may set ttl=5m which conflicts with
|
|
6864
|
-
// our system prompt ttl=1h). Then add our own
|
|
7588
|
+
// our system prompt ttl=1h). Then add our own to the last user message
|
|
6865
7589
|
// (matching real CC behavior seen in proxy capture).
|
|
6866
|
-
|
|
6867
|
-
|
|
7590
|
+
//
|
|
7591
|
+
// Role-scoped TTL: real CC's MoY(querySource) gates the `ttl:"1h"` field
|
|
7592
|
+
// on an allowlist of query sources (`repl_main_thread*`, `sdk`,
|
|
7593
|
+
// `auto_mode`). Non-matching requests fall back to the default 5m tier
|
|
7594
|
+
// — which is cheaper to write. We match this via classifyRequestRole
|
|
7595
|
+
// (main → configured TTL, usually 1h; everything else → 5m).
|
|
7596
|
+
const configuredTtl = signature.cachePolicy?.ttl || "1h";
|
|
7597
|
+
const roleScopedTtl = config?.token_economy?.role_scoped_cache_ttl !== false;
|
|
7598
|
+
const isMainForCache = runtime?.requestRole === "main" || runtime?.requestRole == null;
|
|
7599
|
+
const ccTtl = roleScopedTtl && !isMainForCache ? "5m" : configuredTtl;
|
|
7600
|
+
if (Array.isArray(parsed.tools) && parsed.tools.length > 0) {
|
|
6868
7601
|
for (const tool of parsed.tools) {
|
|
6869
7602
|
if (tool.cache_control) delete tool.cache_control;
|
|
6870
7603
|
}
|
|
7604
|
+
// Add cache_control to last tool as prompt-cache breakpoint (CC does this)
|
|
7605
|
+
parsed.tools[parsed.tools.length - 1].cache_control = { type: "ephemeral", ttl: ccTtl };
|
|
6871
7606
|
}
|
|
6872
7607
|
if (Array.isArray(parsed.messages)) {
|
|
6873
7608
|
for (const msg of parsed.messages) {
|
|
@@ -6925,6 +7660,69 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
|
|
|
6925
7660
|
}
|
|
6926
7661
|
}
|
|
6927
7662
|
}
|
|
7663
|
+
// Track which tools the model has used (from assistant tool_use blocks).
|
|
7664
|
+
// Names are already CC PascalCase after renaming above.
|
|
7665
|
+
if (Array.isArray(parsed.messages)) {
|
|
7666
|
+
for (const msg of parsed.messages) {
|
|
7667
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
7668
|
+
for (const block of msg.content) {
|
|
7669
|
+
if (block.type === "tool_use" && block.name) {
|
|
7670
|
+
runtime.usedTools.add(block.name);
|
|
7671
|
+
}
|
|
7672
|
+
}
|
|
7673
|
+
}
|
|
7674
|
+
}
|
|
7675
|
+
}
|
|
7676
|
+
|
|
7677
|
+
// Strategy 4 — Adaptive tool set: after turn 3, defer non-core tools that
|
|
7678
|
+
// the model hasn't used yet. This saves schema bytes on turns where tools
|
|
7679
|
+
// are unlikely to be needed. Core tools stay eager. Resets on /clear.
|
|
7680
|
+
if (
|
|
7681
|
+
Array.isArray(parsed.tools) &&
|
|
7682
|
+
signature.adaptiveToolSet !== false &&
|
|
7683
|
+
runtime.turns >= 3 &&
|
|
7684
|
+
parsed.model &&
|
|
7685
|
+
!/claude-3-|haiku/i.test(parsed.model)
|
|
7686
|
+
) {
|
|
7687
|
+
const used = runtime.usedTools;
|
|
7688
|
+
for (const tool of parsed.tools) {
|
|
7689
|
+
if (tool.name && !used.has(tool.name) && !CORE_TOOL_NAMES.has(tool.name)) {
|
|
7690
|
+
tool.defer_loading = true;
|
|
7691
|
+
}
|
|
7692
|
+
}
|
|
7693
|
+
}
|
|
7694
|
+
|
|
7695
|
+
// Tool description compaction: apply the same compaction logic used for
|
|
7696
|
+
// system prompts (strip examples, collapse whitespace, dedup lines) to tool
|
|
7697
|
+
// descriptions. The top 4 tools (Bash 10.6K, TodoWrite 9.7K, Task 5.5K,
|
|
7698
|
+
// Compress 4.5K) account for 30KB. Compaction typically saves 30-50%.
|
|
7699
|
+
if (Array.isArray(parsed.tools) && signature.toolDescriptionCompaction !== false) {
|
|
7700
|
+
for (const tool of parsed.tools) {
|
|
7701
|
+
if (tool.description && tool.description.length > 500) {
|
|
7702
|
+
tool.description = compactToolDescription(tool.description);
|
|
7703
|
+
}
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
|
|
7707
|
+
// MCP tool deferral: mark non-core tools with defer_loading: true.
|
|
7708
|
+
// CC defers all MCP tools by default — the API omits their full schemas from token
|
|
7709
|
+
// counting, only sending the tool name. When the model needs a deferred tool, it uses
|
|
7710
|
+
// tool_reference to load the schema on demand. This saves ~20KB per turn.
|
|
7711
|
+
// Core tools (OC_TO_CC_TOOL_NAMES) are always eager-loaded.
|
|
7712
|
+
if (
|
|
7713
|
+
Array.isArray(parsed.tools) &&
|
|
7714
|
+
signature.toolDeferral !== false &&
|
|
7715
|
+
parsed.model &&
|
|
7716
|
+
!/claude-3-|haiku/i.test(parsed.model)
|
|
7717
|
+
) {
|
|
7718
|
+
const coreToolNames = new Set(Object.values(OC_TO_CC_TOOL_NAMES));
|
|
7719
|
+
for (const tool of parsed.tools) {
|
|
7720
|
+
if (tool.name && !coreToolNames.has(tool.name)) {
|
|
7721
|
+
tool.defer_loading = true;
|
|
7722
|
+
}
|
|
7723
|
+
}
|
|
7724
|
+
}
|
|
7725
|
+
|
|
6928
7726
|
// Task budgets: when the task-budgets beta is active, preserve or inject output_config.
|
|
6929
7727
|
// The beta unlocks output_config.max_output_tokens for per-task budget control.
|
|
6930
7728
|
// Model-router compatibility: the beta header + output_config body are forwarded as-is.
|
|
@@ -6936,10 +7734,48 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
|
|
|
6936
7734
|
}
|
|
6937
7735
|
}
|
|
6938
7736
|
|
|
7737
|
+
// Context hint (CC v2.1.110+): pair the context-hint-2026-04-09 beta with
|
|
7738
|
+
// the body field context_hint:{enabled:true}. Extracted from CC bundle
|
|
7739
|
+
// buildRequestParams(): `{betaHeader, body:{context_hint:{enabled:!0}}}`.
|
|
7740
|
+
// When active, the server responds with optional hints suggesting which
|
|
7741
|
+
// messages to drop for token-efficient context compaction.
|
|
7742
|
+
if (betaHeader && betaHeader.includes("context-hint-2026-04-09") && !parsed.context_hint) {
|
|
7743
|
+
parsed.context_hint = { enabled: true };
|
|
7744
|
+
}
|
|
7745
|
+
|
|
6939
7746
|
// Fast mode: inject speed parameter for Opus 4.6 only (v2.1.97 restriction).
|
|
6940
7747
|
// Real CC v2.1.97 xJ() checks: model.includes("opus-4-6") — Sonnet is NOT eligible.
|
|
6941
7748
|
const fastModeEnabled = signature.fastMode && !isFalsyEnv(process.env.OPENCODE_ANTHROPIC_DISABLE_FAST_MODE);
|
|
6942
|
-
|
|
7749
|
+
let fastModeAutoApplied = false;
|
|
7750
|
+
if (
|
|
7751
|
+
!fastModeEnabled &&
|
|
7752
|
+
te.fast_mode_auto === true &&
|
|
7753
|
+
isMainRole &&
|
|
7754
|
+
parsed.model &&
|
|
7755
|
+
isOpus46Model(parsed.model) &&
|
|
7756
|
+
Array.isArray(parsed.messages) &&
|
|
7757
|
+
parsed.messages.length >= 2
|
|
7758
|
+
) {
|
|
7759
|
+
// Simple exchange heuristic: last user message is short, no tool_result,
|
|
7760
|
+
// no file references. Suggests a follow-up question that doesn't need
|
|
7761
|
+
// deep reasoning.
|
|
7762
|
+
const last = parsed.messages[parsed.messages.length - 1];
|
|
7763
|
+
if (last && last.role === "user") {
|
|
7764
|
+
let txt = "";
|
|
7765
|
+
let hasToolResult = false;
|
|
7766
|
+
if (typeof last.content === "string") txt = last.content;
|
|
7767
|
+
else if (Array.isArray(last.content)) {
|
|
7768
|
+
for (const b of last.content) {
|
|
7769
|
+
if (b?.type === "tool_result") hasToolResult = true;
|
|
7770
|
+
if (b?.type === "text" && typeof b.text === "string") txt += b.text;
|
|
7771
|
+
}
|
|
7772
|
+
}
|
|
7773
|
+
if (!hasToolResult && txt.length < 240 && !/\bfile:|\.md\b|\.mjs\b|\.ts\b/i.test(txt)) {
|
|
7774
|
+
fastModeAutoApplied = true;
|
|
7775
|
+
}
|
|
7776
|
+
}
|
|
7777
|
+
}
|
|
7778
|
+
if ((fastModeEnabled || fastModeAutoApplied) && parsed.model && isOpus46Model(parsed.model)) {
|
|
6943
7779
|
parsed.speed = "fast";
|
|
6944
7780
|
}
|
|
6945
7781
|
|
|
@@ -7019,6 +7855,22 @@ function transformRequestUrl(input) {
|
|
|
7019
7855
|
}
|
|
7020
7856
|
}
|
|
7021
7857
|
|
|
7858
|
+
// MITM proxy redirect: rewrite host/port/protocol when OPENCODE_MITM_BASE_URL is set.
|
|
7859
|
+
// This allows capturing the exact over-the-wire request for conformance testing.
|
|
7860
|
+
// Example: OPENCODE_MITM_BASE_URL=http://localhost:9999
|
|
7861
|
+
const mitmBase = process.env.OPENCODE_MITM_BASE_URL;
|
|
7862
|
+
if (mitmBase && requestUrl) {
|
|
7863
|
+
try {
|
|
7864
|
+
const mitmUrl = new URL(mitmBase);
|
|
7865
|
+
requestUrl.protocol = mitmUrl.protocol;
|
|
7866
|
+
requestUrl.hostname = mitmUrl.hostname;
|
|
7867
|
+
requestUrl.port = mitmUrl.port;
|
|
7868
|
+
requestInput = input instanceof Request ? new Request(requestUrl.toString(), input) : requestUrl;
|
|
7869
|
+
} catch {
|
|
7870
|
+
// Invalid MITM URL — ignore silently
|
|
7871
|
+
}
|
|
7872
|
+
}
|
|
7873
|
+
|
|
7022
7874
|
return { requestInput, requestUrl };
|
|
7023
7875
|
}
|
|
7024
7876
|
|
|
@@ -7189,10 +8041,10 @@ function stripMcpPrefixFromParsedEvent(parsed) {
|
|
|
7189
8041
|
|
|
7190
8042
|
let modified = false;
|
|
7191
8043
|
|
|
7192
|
-
// content_block_start: { content_block: { type: "tool_use", name: "..." } }
|
|
8044
|
+
// content_block_start: { content_block: { type: "tool_use"|"tool_reference", name: "..." } }
|
|
7193
8045
|
if (
|
|
7194
8046
|
parsed.content_block &&
|
|
7195
|
-
parsed.content_block.type === "tool_use" &&
|
|
8047
|
+
(parsed.content_block.type === "tool_use" || parsed.content_block.type === "tool_reference") &&
|
|
7196
8048
|
typeof parsed.content_block.name === "string"
|
|
7197
8049
|
) {
|
|
7198
8050
|
const mapped = reverseMapToolName(parsed.content_block.name);
|
|
@@ -7202,10 +8054,10 @@ function stripMcpPrefixFromParsedEvent(parsed) {
|
|
|
7202
8054
|
}
|
|
7203
8055
|
}
|
|
7204
8056
|
|
|
7205
|
-
// message_start: { message: { content: [{ type: "tool_use", name: "..." }] } }
|
|
8057
|
+
// message_start: { message: { content: [{ type: "tool_use"|"tool_reference", name: "..." }] } }
|
|
7206
8058
|
if (parsed.message && Array.isArray(parsed.message.content)) {
|
|
7207
8059
|
for (const block of parsed.message.content) {
|
|
7208
|
-
if (block.type === "tool_use" && typeof block.name === "string") {
|
|
8060
|
+
if ((block.type === "tool_use" || block.type === "tool_reference") && typeof block.name === "string") {
|
|
7209
8061
|
const mapped = reverseMapToolName(block.name);
|
|
7210
8062
|
if (mapped !== block.name) {
|
|
7211
8063
|
block.name = mapped;
|
|
@@ -7218,7 +8070,7 @@ function stripMcpPrefixFromParsedEvent(parsed) {
|
|
|
7218
8070
|
// Top-level content array (non-streaming responses forwarded through SSE)
|
|
7219
8071
|
if (Array.isArray(parsed.content)) {
|
|
7220
8072
|
for (const block of parsed.content) {
|
|
7221
|
-
if (block.type === "tool_use" && typeof block.name === "string") {
|
|
8073
|
+
if ((block.type === "tool_use" || block.type === "tool_reference") && typeof block.name === "string") {
|
|
7222
8074
|
const mapped = reverseMapToolName(block.name);
|
|
7223
8075
|
if (mapped !== block.name) {
|
|
7224
8076
|
block.name = mapped;
|
|
@@ -7674,9 +8526,12 @@ function extractFileIds(body) {
|
|
|
7674
8526
|
AnthropicAuthPlugin.__testing__ = {
|
|
7675
8527
|
sanitizeSystemText,
|
|
7676
8528
|
compactSystemText,
|
|
8529
|
+
compactToolDescription,
|
|
7677
8530
|
dedupeSystemBlocks,
|
|
7678
8531
|
normalizeSystemTextBlocks,
|
|
7679
8532
|
buildSystemPromptBlocks,
|
|
8533
|
+
stripMcpPrefixFromParsedEvent,
|
|
8534
|
+
CORE_TOOL_NAMES,
|
|
7680
8535
|
get cachedCCPrompt() {
|
|
7681
8536
|
return cachedCCPrompt;
|
|
7682
8537
|
},
|
package/lib/config.mjs
CHANGED
|
@@ -190,12 +190,78 @@ export const DEFAULT_CONFIG = {
|
|
|
190
190
|
* This flag is retained for config backward compatibility but has no effect. */
|
|
191
191
|
token_efficient_tools: false,
|
|
192
192
|
/** Enable redact-thinking-2026-02-12 beta (suppresses thinking summaries
|
|
193
|
-
* server-side for lower bandwidth). Off by default
|
|
194
|
-
*
|
|
193
|
+
* server-side for lower bandwidth). Off by default so thinking stays
|
|
194
|
+
* visible. Opt in via `/anthropic set redact-thinking on`. */
|
|
195
195
|
redact_thinking: false,
|
|
196
|
-
/**
|
|
197
|
-
*
|
|
198
|
-
|
|
196
|
+
/** Enable context-hint-2026-04-09 beta (CC v2.1.110+). When on, the server
|
|
197
|
+
* MAY reject 422/424 and trigger client-side compaction retries.
|
|
198
|
+
* Server-side rollout is partial — many accounts 400-reject the beta.
|
|
199
|
+
* Off by default; the plugin still mimics CC's error-handling semantics
|
|
200
|
+
* when a user opts in. Even when on, the beta is only sent for requests
|
|
201
|
+
* classified as "main-thread" (see classifyRequestRole). */
|
|
202
|
+
context_hint: false,
|
|
203
|
+
|
|
204
|
+
/** TTL-based thinking strip (CC parity). When the time since the last
|
|
205
|
+
* thinking strip exceeds the cache TTL, drop all `thinking` /
|
|
206
|
+
* `redacted_thinking` blocks from prior assistant messages. Mirrors CC's
|
|
207
|
+
* `logThinkingClearLatched("ttl", ...)` behavior. Zero quality impact
|
|
208
|
+
* because stale thinking blocks don't influence the next response. */
|
|
209
|
+
ttl_thinking_strip: true,
|
|
210
|
+
|
|
211
|
+
/** Proactive client-side microcompact. At or above `microcompact_percent`
|
|
212
|
+
* of the model context window, replace old tool_result content with a
|
|
213
|
+
* placeholder (keeping the last `microcompact_keep_recent` verbatim).
|
|
214
|
+
* Runs BEFORE the request hits the server — preempts 422s. */
|
|
215
|
+
proactive_microcompact: true,
|
|
216
|
+
microcompact_percent: 70,
|
|
217
|
+
microcompact_keep_recent: 8,
|
|
218
|
+
|
|
219
|
+
/** Stable tool ordering — sort tools deterministically across turns so
|
|
220
|
+
* the system-prompt prefix stays cache-stable. Safe and cheap. */
|
|
221
|
+
stable_tool_ordering: true,
|
|
222
|
+
|
|
223
|
+
/** Tool schema deferral — ship minimal (name+description only) schemas
|
|
224
|
+
* for tools below the defer threshold until they're actually invoked in
|
|
225
|
+
* the session. Accepts a list of tool names to defer. Empty = feature off. */
|
|
226
|
+
deferred_tool_names: [],
|
|
227
|
+
|
|
228
|
+
/** Adaptive thinking — zero the thinking budget for simple follow-up
|
|
229
|
+
* user turns (short user message, no file refs, no tool_use in prior
|
|
230
|
+
* assistant turn). Reduces thinking tokens on trivial exchanges. */
|
|
231
|
+
adaptive_thinking_zero_simple: true,
|
|
232
|
+
|
|
233
|
+
/** Cross-turn tool_result dedupe — when the same (tool_use_name + input)
|
|
234
|
+
* appears twice, replace the later result content with a pointer to the
|
|
235
|
+
* earlier tool_use_id. Opt-in; can surprise models that expected a fresh
|
|
236
|
+
* read. Applies only to read-only tools in the safe set. */
|
|
237
|
+
tool_result_dedupe: false,
|
|
238
|
+
|
|
239
|
+
/** Fast-mode auto-opt-in — detect simple exchanges (short user msg, no
|
|
240
|
+
* tools likely needed) and emit speed:fast for that turn. Opt-in. */
|
|
241
|
+
fast_mode_auto: false,
|
|
242
|
+
|
|
243
|
+
/** Trailing-summary trimmer — detect trailing summary paragraphs in past
|
|
244
|
+
* assistant text blocks and strip them from the outgoing messages array.
|
|
245
|
+
* Opt-in; can hide context the model relied on. */
|
|
246
|
+
trailing_summary_trim: false,
|
|
247
|
+
|
|
248
|
+
/** Role-scoped cache TTL (CC parity). Real CC's MoY(querySource) gates
|
|
249
|
+
* `ttl:"1h"` on an allowlist of query sources (`repl_main_thread*`, `sdk`,
|
|
250
|
+
* `auto_mode`). Everything else falls back to the 5m tier, which is
|
|
251
|
+
* cheaper to write. Matches by classifying request role: main → 1h (or
|
|
252
|
+
* whatever signature.cachePolicy.ttl is configured as), else → 5m. */
|
|
253
|
+
role_scoped_cache_ttl: true,
|
|
254
|
+
|
|
255
|
+
/** Lean system prompt for non-main requests. For title-gen / small /
|
|
256
|
+
* subagent-shaped requests, strip billing identity + CC identity
|
|
257
|
+
* injection. Saves ~1-2kB per request. Opt-in: changes the system-prompt
|
|
258
|
+
* shape that downstream observers (billing/telemetry) may rely on, so
|
|
259
|
+
* enable deliberately rather than by default. */
|
|
260
|
+
lean_system_non_main: false,
|
|
261
|
+
|
|
262
|
+
// NOTE: identical-tool-call short-circuit was considered but requires SSE
|
|
263
|
+
// response-stream rewriting that breaks normal tool-execution semantics.
|
|
264
|
+
// Tracked as future work — not exposed as a config flag here.
|
|
199
265
|
},
|
|
200
266
|
/** Output cap: default max_tokens to save context window. */
|
|
201
267
|
output_cap: {
|
|
@@ -577,10 +643,56 @@ function validateConfig(raw) {
|
|
|
577
643
|
: DEFAULT_CONFIG.token_economy.token_efficient_tools,
|
|
578
644
|
redact_thinking:
|
|
579
645
|
typeof te.redact_thinking === "boolean" ? te.redact_thinking : DEFAULT_CONFIG.token_economy.redact_thinking,
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
646
|
+
context_hint: typeof te.context_hint === "boolean" ? te.context_hint : DEFAULT_CONFIG.token_economy.context_hint,
|
|
647
|
+
ttl_thinking_strip:
|
|
648
|
+
typeof te.ttl_thinking_strip === "boolean"
|
|
649
|
+
? te.ttl_thinking_strip
|
|
650
|
+
: DEFAULT_CONFIG.token_economy.ttl_thinking_strip,
|
|
651
|
+
proactive_microcompact:
|
|
652
|
+
typeof te.proactive_microcompact === "boolean"
|
|
653
|
+
? te.proactive_microcompact
|
|
654
|
+
: DEFAULT_CONFIG.token_economy.proactive_microcompact,
|
|
655
|
+
microcompact_percent: clampNumber(
|
|
656
|
+
te.microcompact_percent,
|
|
657
|
+
30,
|
|
658
|
+
95,
|
|
659
|
+
DEFAULT_CONFIG.token_economy.microcompact_percent,
|
|
660
|
+
),
|
|
661
|
+
microcompact_keep_recent: clampNumber(
|
|
662
|
+
te.microcompact_keep_recent,
|
|
663
|
+
1,
|
|
664
|
+
64,
|
|
665
|
+
DEFAULT_CONFIG.token_economy.microcompact_keep_recent,
|
|
666
|
+
),
|
|
667
|
+
stable_tool_ordering:
|
|
668
|
+
typeof te.stable_tool_ordering === "boolean"
|
|
669
|
+
? te.stable_tool_ordering
|
|
670
|
+
: DEFAULT_CONFIG.token_economy.stable_tool_ordering,
|
|
671
|
+
deferred_tool_names: Array.isArray(te.deferred_tool_names)
|
|
672
|
+
? te.deferred_tool_names.filter((n) => typeof n === "string")
|
|
673
|
+
: DEFAULT_CONFIG.token_economy.deferred_tool_names,
|
|
674
|
+
adaptive_thinking_zero_simple:
|
|
675
|
+
typeof te.adaptive_thinking_zero_simple === "boolean"
|
|
676
|
+
? te.adaptive_thinking_zero_simple
|
|
677
|
+
: DEFAULT_CONFIG.token_economy.adaptive_thinking_zero_simple,
|
|
678
|
+
tool_result_dedupe:
|
|
679
|
+
typeof te.tool_result_dedupe === "boolean"
|
|
680
|
+
? te.tool_result_dedupe
|
|
681
|
+
: DEFAULT_CONFIG.token_economy.tool_result_dedupe,
|
|
682
|
+
fast_mode_auto:
|
|
683
|
+
typeof te.fast_mode_auto === "boolean" ? te.fast_mode_auto : DEFAULT_CONFIG.token_economy.fast_mode_auto,
|
|
684
|
+
trailing_summary_trim:
|
|
685
|
+
typeof te.trailing_summary_trim === "boolean"
|
|
686
|
+
? te.trailing_summary_trim
|
|
687
|
+
: DEFAULT_CONFIG.token_economy.trailing_summary_trim,
|
|
688
|
+
role_scoped_cache_ttl:
|
|
689
|
+
typeof te.role_scoped_cache_ttl === "boolean"
|
|
690
|
+
? te.role_scoped_cache_ttl
|
|
691
|
+
: DEFAULT_CONFIG.token_economy.role_scoped_cache_ttl,
|
|
692
|
+
lean_system_non_main:
|
|
693
|
+
typeof te.lean_system_non_main === "boolean"
|
|
694
|
+
? te.lean_system_non_main
|
|
695
|
+
: DEFAULT_CONFIG.token_economy.lean_system_non_main,
|
|
584
696
|
};
|
|
585
697
|
}
|
|
586
698
|
|