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.
Files changed (3) hide show
  1. package/index.mjs +669 -9
  2. package/lib/config.mjs +119 -0
  3. 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
- const _tokenEconomy = config.token_economy || {};
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.107";
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-14T03:13:25Z";
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 proxy capture).
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 = { ...signature, modelId, firstUserMessage, antiVerbosity: config?.anti_verbosity };
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 1h to the last user message
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
- const ccTtl = signature.cachePolicy?.ttl || "1h";
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
- if (fastModeEnabled && parsed.model && isOpus46Model(parsed.model)) {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "main": "./index.mjs",
6
6
  "files": [