opencode-anthropic-fix 0.1.9 → 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 +669 -9
- package/lib/config.mjs +119 -0
- 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;
|
|
@@ -2712,8 +2741,19 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2712
2741
|
|
|
2713
2742
|
const _adaptiveOverride = _adaptiveOverrideForRequest;
|
|
2714
2743
|
|
|
2715
|
-
// Token economy config (resolved once, passed to beta builder)
|
|
2716
|
-
|
|
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 };
|
|
2717
2757
|
|
|
2718
2758
|
// Microcompact: inject clear betas at high context utilization
|
|
2719
2759
|
let _microcompactBetas = null;
|
|
@@ -2769,6 +2809,12 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2769
2809
|
// Merge latched betas that aren't in the current set
|
|
2770
2810
|
const merged = new Set(currentBetas);
|
|
2771
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
|
+
}
|
|
2772
2818
|
computedBetaHeader = [...merged].join(",");
|
|
2773
2819
|
betaLatchState.lastHeader = computedBetaHeader;
|
|
2774
2820
|
}
|
|
@@ -2807,6 +2853,8 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2807
2853
|
accountId: getAccountIdentifier(account),
|
|
2808
2854
|
turns: sessionMetrics.turns,
|
|
2809
2855
|
usedTools: sessionMetrics.usedTools,
|
|
2856
|
+
tokenEconomySession,
|
|
2857
|
+
requestRole: _requestRole,
|
|
2810
2858
|
},
|
|
2811
2859
|
computedBetaHeader,
|
|
2812
2860
|
config,
|
|
@@ -3169,6 +3217,63 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
3169
3217
|
// Ignore read errors in debug logging path.
|
|
3170
3218
|
}
|
|
3171
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
|
+
|
|
3172
3277
|
// Reactive compaction: on "prompt too long" error, trim oldest messages and retry once
|
|
3173
3278
|
if (
|
|
3174
3279
|
response.status === 400 &&
|
|
@@ -5077,17 +5182,25 @@ process.once("beforeExit", _beforeExitHandler);
|
|
|
5077
5182
|
// Request building helpers (extracted from original fetch interceptor)
|
|
5078
5183
|
// ---------------------------------------------------------------------------
|
|
5079
5184
|
|
|
5080
|
-
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.
|
|
5185
|
+
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.114";
|
|
5081
5186
|
const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
5082
|
-
const CLAUDE_CODE_BUILD_TIME = "2026-04-
|
|
5187
|
+
const CLAUDE_CODE_BUILD_TIME = "2026-04-17T22:37:24Z";
|
|
5083
5188
|
|
|
5084
5189
|
// The @anthropic-ai/sdk version bundled with Claude Code.
|
|
5085
5190
|
// This is distinct from the CLI version and goes in X-Stainless-Package-Version.
|
|
5086
|
-
// 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).
|
|
5087
5193
|
const ANTHROPIC_SDK_VERSION = "0.81.0";
|
|
5088
5194
|
|
|
5089
5195
|
// Map of CLI version → bundled SDK version (update when CLI version changes)
|
|
5090
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"],
|
|
5091
5204
|
["2.1.107", "0.81.0"],
|
|
5092
5205
|
["2.1.105", "0.81.0"],
|
|
5093
5206
|
["2.1.97", "0.208.0"],
|
|
@@ -5205,6 +5318,334 @@ function computeBillingCacheHash(firstUserMessage, version) {
|
|
|
5205
5318
|
* @param {Array} messages — The messages array from the parsed request body
|
|
5206
5319
|
* @returns {Array} — Repaired messages array
|
|
5207
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
|
+
|
|
5208
5649
|
function repairOrphanedToolUseBlocks(messages) {
|
|
5209
5650
|
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
5210
5651
|
|
|
@@ -5401,6 +5842,7 @@ const EXPERIMENTAL_BETA_FLAGS = new Set([
|
|
|
5401
5842
|
"code-execution-2025-08-25",
|
|
5402
5843
|
"compact-2026-01-12",
|
|
5403
5844
|
"context-1m-2025-08-07",
|
|
5845
|
+
"context-hint-2026-04-09",
|
|
5404
5846
|
"context-management-2025-06-27",
|
|
5405
5847
|
"fast-mode-2026-02-01",
|
|
5406
5848
|
"files-api-2025-04-14",
|
|
@@ -5416,6 +5858,8 @@ const BETA_SHORTCUTS = new Map([
|
|
|
5416
5858
|
["1m", "context-1m-2025-08-07"],
|
|
5417
5859
|
["1m-context", "context-1m-2025-08-07"],
|
|
5418
5860
|
["context-1m", "context-1m-2025-08-07"],
|
|
5861
|
+
["context-hint", "context-hint-2026-04-09"],
|
|
5862
|
+
["hint", "context-hint-2026-04-09"],
|
|
5419
5863
|
["fast", "fast-mode-2026-02-01"],
|
|
5420
5864
|
["fast-mode", "fast-mode-2026-02-01"],
|
|
5421
5865
|
["opus-fast", "fast-mode-2026-02-01"],
|
|
@@ -5729,6 +6173,46 @@ function detectProvider(requestUrl) {
|
|
|
5729
6173
|
return "anthropic";
|
|
5730
6174
|
}
|
|
5731
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
|
+
|
|
5732
6216
|
/**
|
|
5733
6217
|
* @param {any} body
|
|
5734
6218
|
* @returns {{model: string, tools: any[], messages: any[], hasFileReferences: boolean}}
|
|
@@ -6316,6 +6800,21 @@ function buildSystemPromptBlocks(system, signature) {
|
|
|
6316
6800
|
return sanitized;
|
|
6317
6801
|
}
|
|
6318
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
|
+
|
|
6319
6818
|
// Build attribution header
|
|
6320
6819
|
const billingHeader = buildAnthropicBillingHeader(
|
|
6321
6820
|
signature.claudeCliVersion,
|
|
@@ -6479,6 +6978,23 @@ function buildAnthropicBetaHeader(
|
|
|
6479
6978
|
betas.push("advisor-tool-2026-03-01");
|
|
6480
6979
|
}
|
|
6481
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
|
+
|
|
6482
6998
|
// Files API — scoped to file endpoints/references
|
|
6483
6999
|
if (isFilesEndpoint || hasFileReferences) {
|
|
6484
7000
|
betas.push("files-api-2025-04-14");
|
|
@@ -6900,11 +7416,108 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
|
|
|
6900
7416
|
parsed.messages = stripSlashCommandMessages(parsed.messages);
|
|
6901
7417
|
}
|
|
6902
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
|
+
|
|
6903
7505
|
// QA fix H2: avoid mutating the signature parameter; capture modelId locally
|
|
6904
7506
|
const modelId = parsed.model || "";
|
|
6905
7507
|
// Extract first user message text for billing hash computation (cch)
|
|
6906
7508
|
const firstUserMessage = extractFirstUserMessageText(parsed.messages);
|
|
6907
|
-
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
|
+
};
|
|
6908
7521
|
// Sanitize system prompt and optionally inject Claude Code identity/billing blocks.
|
|
6909
7522
|
parsed.system = buildSystemPromptBlocks(normalizeSystemTextBlocks(parsed.system), signatureWithModel);
|
|
6910
7523
|
|
|
@@ -6972,9 +7585,18 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
|
|
|
6972
7585
|
) {
|
|
6973
7586
|
// Strip ALL incoming cache_control from tools and messages to prevent
|
|
6974
7587
|
// TTL ordering violations (host SDK may set ttl=5m which conflicts with
|
|
6975
|
-
// our system prompt ttl=1h). Then add our own
|
|
7588
|
+
// our system prompt ttl=1h). Then add our own to the last user message
|
|
6976
7589
|
// (matching real CC behavior seen in proxy capture).
|
|
6977
|
-
|
|
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;
|
|
6978
7600
|
if (Array.isArray(parsed.tools) && parsed.tools.length > 0) {
|
|
6979
7601
|
for (const tool of parsed.tools) {
|
|
6980
7602
|
if (tool.cache_control) delete tool.cache_control;
|
|
@@ -7112,10 +7734,48 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
|
|
|
7112
7734
|
}
|
|
7113
7735
|
}
|
|
7114
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
|
+
|
|
7115
7746
|
// Fast mode: inject speed parameter for Opus 4.6 only (v2.1.97 restriction).
|
|
7116
7747
|
// Real CC v2.1.97 xJ() checks: model.includes("opus-4-6") — Sonnet is NOT eligible.
|
|
7117
7748
|
const fastModeEnabled = signature.fastMode && !isFalsyEnv(process.env.OPENCODE_ANTHROPIC_DISABLE_FAST_MODE);
|
|
7118
|
-
|
|
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)) {
|
|
7119
7779
|
parsed.speed = "fast";
|
|
7120
7780
|
}
|
|
7121
7781
|
|
package/lib/config.mjs
CHANGED
|
@@ -193,6 +193,75 @@ export const DEFAULT_CONFIG = {
|
|
|
193
193
|
* server-side for lower bandwidth). Off by default so thinking stays
|
|
194
194
|
* visible. Opt in via `/anthropic set redact-thinking on`. */
|
|
195
195
|
redact_thinking: false,
|
|
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.
|
|
196
265
|
},
|
|
197
266
|
/** Output cap: default max_tokens to save context window. */
|
|
198
267
|
output_cap: {
|
|
@@ -574,6 +643,56 @@ function validateConfig(raw) {
|
|
|
574
643
|
: DEFAULT_CONFIG.token_economy.token_efficient_tools,
|
|
575
644
|
redact_thinking:
|
|
576
645
|
typeof te.redact_thinking === "boolean" ? te.redact_thinking : DEFAULT_CONFIG.token_economy.redact_thinking,
|
|
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,
|
|
577
696
|
};
|
|
578
697
|
}
|
|
579
698
|
|