opencode-anthropic-fix 0.1.3 → 0.1.5

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 +244 -9
  2. package/lib/config.mjs +65 -0
  3. package/package.json +1 -1
package/index.mjs CHANGED
@@ -111,6 +111,7 @@ async function promptManageAccounts(accountManager) {
111
111
 
112
112
  export async function AnthropicAuthPlugin({ client, project, directory, worktree, serverUrl, $ }) {
113
113
  const config = loadConfig();
114
+ _pluginConfig = config; // expose to module-level functions (cache stats, response headers)
114
115
  // QA fix H6: read emulation settings live from config instead of stale const capture
115
116
  // so that runtime toggles via `/anthropic set emulation` take effect immediately
116
117
  const getSignatureEmulationEnabled = () => config.signature_emulation.enabled;
@@ -653,6 +654,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
653
654
  `telemetry-emulation: ${fresh.telemetry?.emulate_minimal ? "on (silent observer)" : "off"}`,
654
655
  `usage-toast: ${fresh.usage_toast ? "on" : "off"}`,
655
656
  `adaptive-context: ${fresh.adaptive_context?.enabled ? `on (↑${Math.round((fresh.adaptive_context.escalation_threshold || 150000) / 1000)}K ↓${Math.round((fresh.adaptive_context.deescalation_threshold || 100000) / 1000)}K)${adaptiveContextState.active ? " [ACTIVE]" : ""}` : "off"}`,
657
+ `anti-verbosity: ${fresh.anti_verbosity?.enabled !== false ? "on" : "off"} (length-anchors: ${fresh.anti_verbosity?.length_anchors !== false ? "on" : "off"})`,
656
658
  ];
657
659
  await sendCommandMessage(input.sessionID, lines.join("\n"));
658
660
  return;
@@ -3041,7 +3043,15 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
3041
3043
  sessionMetrics.lastQuota.inputTokens = maxUtilization;
3042
3044
  }
3043
3045
 
3044
- if (maxUtilization > 0.8) {
3046
+ // Proactive account management is gated on config. When
3047
+ // account_management.proactive_disabled is true (default),
3048
+ // we never apply penalties on a 200 OK response — those
3049
+ // penalties were locking out single-account users whose
3050
+ // server-side quota was still in `allowed_warning` state.
3051
+ // The reactive 429 path below is unaffected.
3052
+ const proactiveDisabled = config.account_management?.proactive_disabled !== false;
3053
+
3054
+ if (!proactiveDisabled && maxUtilization > 0.8) {
3045
3055
  const penalty = Math.round((maxUtilization - 0.8) * 50); // 0-10 points
3046
3056
  accountManager.applyUtilizationPenalty(account, penalty);
3047
3057
  debugLog("high rate limit utilization", {
@@ -3052,7 +3062,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
3052
3062
  });
3053
3063
  }
3054
3064
 
3055
- if (anySurpassed) {
3065
+ if (!proactiveDisabled && anySurpassed) {
3056
3066
  accountManager.applySurpassedThreshold(account, surpassedResetAt);
3057
3067
  debugLog("rate limit threshold surpassed", {
3058
3068
  accountIndex: account.index,
@@ -3070,8 +3080,10 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
3070
3080
  }
3071
3081
 
3072
3082
  // Predictive rate limit avoidance: switch account BEFORE hitting 429
3073
- // Parse reset timestamps to compute time-weighted risk
3074
- if (maxUtilization > 0.6 && accountManager.getAccountCount() > 1) {
3083
+ // Parse reset timestamps to compute time-weighted risk.
3084
+ // Gated on proactive_disabled when true (default), no automatic
3085
+ // switches happen on 200 OK responses (fully manual rotation).
3086
+ if (!proactiveDisabled && maxUtilization > 0.6 && accountManager.getAccountCount() > 1) {
3075
3087
  let highestRisk = 0;
3076
3088
  for (const win of RATE_LIMIT_WINDOWS) {
3077
3089
  const utilizationStr = response.headers.get(`anthropic-ratelimit-unified-${win.key}-utilization`);
@@ -3854,6 +3866,9 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
3854
3866
  // ---------------------------------------------------------------------------
3855
3867
 
3856
3868
  /** @type {{turns: number, totalInput: number, totalOutput: number, totalCacheRead: number, totalCacheWrite: number, totalWebSearchRequests: number, recentCacheRates: number[], sessionCostUsd: number, costBreakdown: {input: number, output: number, cacheRead: number, cacheWrite: number}, sessionStartTime: number, lastQuota: {tokens: number, requests: number, inputTokens: number, updatedAt: number, fiveHour: {utilization: number, resets_at: string|null, status: string|null, surpassedThreshold: number|null}, sevenDay: {utilization: number, resets_at: string|null, status: string|null, surpassedThreshold: number|null}, overallStatus: string|null, representativeClaim: string|null, fallback: string|null, fallbackPercentage: number|null, overageStatus: string|null, overageReason: string|null, lastPollAt: number}, lastStopReason: string | null, perModel: Record<string, {input: number, output: number, cacheRead: number, cacheWrite: number, costUsd: number, turns: number}>, lastModelId: string | null, lastRequestBody: string | null, tokenBudget: {limit: number, used: number, continuations: number, outputHistory: number[]}}} */
3869
+ /** Module-level config ref for functions outside AnthropicAuthPlugin closure. */
3870
+ let _pluginConfig = null;
3871
+
3857
3872
  const sessionMetrics = {
3858
3873
  turns: 0,
3859
3874
  totalInput: 0,
@@ -4777,6 +4792,9 @@ function updateSessionMetrics(usage, model) {
4777
4792
  sessionMetrics.lastModelId = model;
4778
4793
  }
4779
4794
 
4795
+ // Write cache transparency stats to disk for TUI consumption.
4796
+ writeCacheStatsFile(usage, model, hitRate);
4797
+
4780
4798
  // Token budget tracking (A9)
4781
4799
  if (sessionMetrics.tokenBudget.limit > 0) {
4782
4800
  sessionMetrics.tokenBudget.used += usage.outputTokens;
@@ -4798,6 +4816,63 @@ function getAverageCacheHitRate() {
4798
4816
  return rates.reduce((a, b) => a + b, 0) / rates.length;
4799
4817
  }
4800
4818
 
4819
+ /**
4820
+ * Write cache transparency stats to a well-known JSON file for TUI consumption.
4821
+ * The OpenCode TUI watches this file to display cache metrics in the status bar.
4822
+ * @param {UsageStats} usage - Current turn usage
4823
+ * @param {string} model - Model used
4824
+ * @param {number} hitRate - Cache hit rate for this turn (0-1)
4825
+ */
4826
+ function writeCacheStatsFile(usage, model, hitRate) {
4827
+ try {
4828
+ const statsPath = join(getConfigDir(), "cache-stats.json");
4829
+ const avgHitRate = getAverageCacheHitRate();
4830
+ const totalPrompt = sessionMetrics.totalInput + sessionMetrics.totalCacheRead + sessionMetrics.totalCacheWrite;
4831
+ const sessionHitRate = totalPrompt > 0 ? sessionMetrics.totalCacheRead / totalPrompt : 0;
4832
+
4833
+ // Calculate cache savings in USD
4834
+ const pricing = MODEL_PRICING[model] || MODEL_PRICING["claude-opus-4-6"] || { input: 15, cacheRead: 1.5 };
4835
+ const savedPerMToken = pricing.input - (pricing.cacheRead || pricing.input * 0.1);
4836
+ const sessionSavingsUsd = (sessionMetrics.totalCacheRead / 1_000_000) * savedPerMToken;
4837
+
4838
+ const stats = {
4839
+ // Per-turn stats (latest request)
4840
+ turn: {
4841
+ input_tokens: usage.inputTokens,
4842
+ output_tokens: usage.outputTokens,
4843
+ cache_read_tokens: usage.cacheReadTokens,
4844
+ cache_write_tokens: usage.cacheWriteTokens,
4845
+ cache_hit_rate: Math.round(hitRate * 1000) / 1000,
4846
+ model,
4847
+ },
4848
+ // Session-level stats
4849
+ session: {
4850
+ turns: sessionMetrics.turns,
4851
+ total_input: sessionMetrics.totalInput,
4852
+ total_output: sessionMetrics.totalOutput,
4853
+ total_cache_read: sessionMetrics.totalCacheRead,
4854
+ total_cache_write: sessionMetrics.totalCacheWrite,
4855
+ session_hit_rate: Math.round(sessionHitRate * 1000) / 1000,
4856
+ avg_recent_hit_rate: Math.round(avgHitRate * 1000) / 1000,
4857
+ cost_usd: Math.round(sessionMetrics.sessionCostUsd * 10000) / 10000,
4858
+ cache_savings_usd: Math.round(sessionSavingsUsd * 10000) / 10000,
4859
+ },
4860
+ // Config state
4861
+ config: {
4862
+ cache_ttl: _pluginConfig?.cache_policy?.ttl ?? "1h",
4863
+ boundary_marker: _pluginConfig?.cache_policy?.boundary_marker ?? false,
4864
+ anti_verbosity: _pluginConfig?.anti_verbosity?.enabled !== false,
4865
+ length_anchors: _pluginConfig?.anti_verbosity?.length_anchors !== false,
4866
+ },
4867
+ timestamp: new Date().toISOString(),
4868
+ };
4869
+
4870
+ writeFileSync(statsPath, JSON.stringify(stats, null, 2));
4871
+ } catch {
4872
+ // Non-critical — silently ignore write failures
4873
+ }
4874
+ }
4875
+
4801
4876
  // --- Phase 5: Auto-strategy adaptation ---
4802
4877
  // strategyState is created per-plugin instance inside AnthropicAuthPlugin() to avoid
4803
4878
  // cross-instance pollution (critical for test isolation and multi-instance scenarios).
@@ -5310,6 +5385,33 @@ const COMPACT_TITLE_GENERATOR_SYSTEM_PROMPT = [
5310
5385
  "- Keep important technical terms, numbers, and filenames when present.",
5311
5386
  ].join("\n");
5312
5387
 
5388
+ /**
5389
+ * Anti-verbosity system prompt text.
5390
+ * Extracted from CC v2.1.100 (gated on quiet_salted_ember A/B test for Opus 4.6).
5391
+ * Significantly reduces output token count by instructing the model to be concise.
5392
+ */
5393
+ const ANTI_VERBOSITY_SYSTEM_PROMPT = [
5394
+ "# Communication style",
5395
+ "Assume users can't see most tool calls or thinking — only your text output. Before your first tool call, state in one sentence what you're about to do. While working, give short updates at key moments: when you find something, when you change direction, or when you hit a blocker. Brief is good — silent is not. One sentence per update is almost always enough.",
5396
+ "",
5397
+ "Don't narrate your internal deliberation. User-facing text should be relevant communication to the user, not a running commentary on your thought process. State results and decisions directly, and focus user-facing text on relevant updates for the user.",
5398
+ "",
5399
+ "When you do write updates, write so the reader can pick up cold: complete sentences, no unexplained jargon or shorthand from earlier in the session. But keep it tight — a clear sentence is better than a clear paragraph.",
5400
+ "",
5401
+ "End-of-turn summary: one or two sentences. What changed and what's next. Nothing else.",
5402
+ "",
5403
+ "Match responses to the task: a simple question gets a direct answer, not headers and sections.",
5404
+ "",
5405
+ "In code: default to writing no comments. Never write multi-paragraph docstrings or multi-line comment blocks — one short line max. Don't create planning, decision, or analysis documents unless the user asks for them — work from conversation context, not intermediate files.",
5406
+ ].join("\n");
5407
+
5408
+ /**
5409
+ * Numeric length anchors text.
5410
+ * Extracted from CC v2.1.100. Hard word-count limits for output.
5411
+ */
5412
+ const NUMERIC_LENGTH_ANCHORS_PROMPT =
5413
+ "Length limits: keep text between tool calls to ≤25 words. Keep final responses to ≤100 words unless the task requires more detail.";
5414
+
5313
5415
  /**
5314
5416
  * Returns the persistent device ID (64-char hex string).
5315
5417
  * Migrates legacy UUID-format values to the new 64-hex format automatically.
@@ -5722,6 +5824,24 @@ function buildAnthropicBillingHeader(version, firstUserMessage, provider) {
5722
5824
  // Opencode's customizations after ~5800 chars diverge and trigger extra usage billing.
5723
5825
  const MAX_SAFE_SYSTEM_TEXT_LENGTH = 5000;
5724
5826
 
5827
+ // A5: Subagent CC-prefix cache.
5828
+ //
5829
+ // Context: opencode/packages/opencode/src/session/llm.ts:110 uses
5830
+ // `input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(model)`
5831
+ // so any agent with a custom prompt (explore, fast, title, summary, etc.)
5832
+ // fires WITHOUT the base CC prompt — the server-side fingerprint match fails
5833
+ // and the request is billed as pay-as-you-go credits instead of Max-plan usage.
5834
+ //
5835
+ // Fix: on the first main-agent call (where the anchor is present), cache the
5836
+ // sanitized CC prefix. On subsequent subagent calls (anchor missing), prepend
5837
+ // the cached prefix to the sanitized blocks so the fingerprint matches again.
5838
+ //
5839
+ // The cache lives at module scope because buildSystemPromptBlocks is re-entered
5840
+ // per request. It gets populated exactly once per process on the first main call.
5841
+ const MAX_SUBAGENT_CC_PREFIX = MAX_SAFE_SYSTEM_TEXT_LENGTH;
5842
+ const SUBAGENT_CC_ANCHOR = "You are an interactive";
5843
+ let cachedCCPrompt = null;
5844
+
5725
5845
  function sanitizeSystemText(text) {
5726
5846
  // QA fix M4: use word boundaries to avoid mangling URLs and code identifiers
5727
5847
  let sanitized = text.replace(/\bOpenCode\b/g, "Claude Code").replace(/\bopencode\b/gi, "Claude");
@@ -6035,12 +6155,58 @@ function buildSystemPromptBlocks(system, signature) {
6035
6155
  text: compactSystemText(sanitizeSystemText(item.text), signature.promptCompactionMode),
6036
6156
  }));
6037
6157
 
6158
+ // A5: Subagent CC-prefix cache/inject (see constant declaration above for context).
6159
+ //
6160
+ // After sanitize, main-agent blocks start with "You are an interactive..." because
6161
+ // sanitizeSystemText() strips everything before that anchor. Subagent blocks
6162
+ // (custom prompts from input.agent.prompt) do NOT start with the anchor —
6163
+ // they start with whatever the agent template says (e.g., "You are a file search
6164
+ // specialist.").
6165
+ //
6166
+ // This logic runs ONLY for Anthropic requests with signature enabled (signature.enabled
6167
+ // is false for non-Anthropic providers), and skips the title-generator fast path
6168
+ // because that one is replaced wholesale with COMPACT_TITLE_GENERATOR_SYSTEM_PROMPT below.
6169
+ if (signature.enabled && !titleGeneratorRequest && sanitized.length > 0) {
6170
+ const firstText = typeof sanitized[0]?.text === "string" ? sanitized[0].text : "";
6171
+ const hasCcAnchor = firstText.startsWith(SUBAGENT_CC_ANCHOR);
6172
+
6173
+ if (hasCcAnchor) {
6174
+ // Main-agent path: cache the prefix on the first hit so subagents can reuse it.
6175
+ // We slice to MAX_SUBAGENT_CC_PREFIX to avoid unbounded growth if the upstream
6176
+ // sanitize limit is ever raised.
6177
+ if (!cachedCCPrompt) {
6178
+ cachedCCPrompt = firstText.slice(0, MAX_SUBAGENT_CC_PREFIX);
6179
+ }
6180
+ } else if (cachedCCPrompt) {
6181
+ // Subagent path: prepend the cached CC prefix so the fingerprint matches.
6182
+ // We prepend, not concatenate, so the original subagent prompt stays as a
6183
+ // separate block — dedupeSystemBlocks and splitSysPromptPrefix handle the
6184
+ // join on their own downstream.
6185
+ sanitized = [{ type: "text", text: cachedCCPrompt }, ...sanitized];
6186
+ }
6187
+ // If !hasCcAnchor && !cachedCCPrompt: no-op. The cache primes on the very
6188
+ // first main call in a process. In practice opencode always fires a main
6189
+ // call before any subagent, so this branch is only hit in synthetic tests.
6190
+ }
6191
+
6038
6192
  if (titleGeneratorRequest) {
6039
6193
  sanitized = [{ type: "text", text: COMPACT_TITLE_GENERATOR_SYSTEM_PROMPT }];
6040
6194
  } else if (signature.promptCompactionMode !== "off") {
6041
6195
  sanitized = dedupeSystemBlocks(sanitized);
6042
6196
  }
6043
6197
 
6198
+ // Anti-verbosity injection (CC v2.1.100 quiet_salted_ember equivalent).
6199
+ // Only for Opus 4.6 and non-title-generator requests.
6200
+ if (!titleGeneratorRequest && signature.modelId && isOpus46Model(signature.modelId)) {
6201
+ const avConfig = signature.antiVerbosity;
6202
+ if (avConfig?.enabled !== false) {
6203
+ sanitized.push({ type: "text", text: ANTI_VERBOSITY_SYSTEM_PROMPT });
6204
+ }
6205
+ if (avConfig?.length_anchors !== false) {
6206
+ sanitized.push({ type: "text", text: NUMERIC_LENGTH_ANCHORS_PROMPT });
6207
+ }
6208
+ }
6209
+
6044
6210
  if (!signature.enabled) {
6045
6211
  return sanitized;
6046
6212
  }
@@ -6148,10 +6314,20 @@ function buildAnthropicBetaHeader(
6148
6314
  }
6149
6315
 
6150
6316
  betas.push(FAST_MODE_BETA_FLAG); // "fast-mode-2026-02-01"
6151
- betas.push(EFFORT_BETA_FLAG); // "effort-2025-11-24"
6152
6317
 
6153
- // Interleaved thinking always-on unless explicitly disabled
6154
- if (!isTruthyEnv(process.env.DISABLE_INTERLEAVED_THINKING)) {
6318
+ // effort-2025-11-24real CC's Lyz() only pushes this flag when rE(model)
6319
+ // is true (Opus 4.6 / Sonnet 4.6). Pushing it for non-adaptive models like
6320
+ // Haiku is a fingerprint mismatch vs real CC and can contaminate billing
6321
+ // attribution even when the request body has no effort field.
6322
+ if (isAdaptiveThinkingModel(model)) {
6323
+ betas.push(EFFORT_BETA_FLAG); // "effort-2025-11-24"
6324
+ }
6325
+
6326
+ // Interleaved thinking — real CC's i01 pushes via hv4(model), which is
6327
+ // (firstParty && non-Claude-3). Claude 3.x models don't support interleaved
6328
+ // thinking and real CC never sends this flag for them, so emitting it
6329
+ // diverges the fingerprint for legacy Haiku/Sonnet 3.x requests.
6330
+ if (!isTruthyEnv(process.env.DISABLE_INTERLEAVED_THINKING) && !/claude-3-/i.test(model)) {
6155
6331
  betas.push("interleaved-thinking-2025-05-14");
6156
6332
  }
6157
6333
 
@@ -6549,6 +6725,27 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
6549
6725
  parsed.thinking = normalizeThinkingBlock(parsed.thinking, parsed.model || "");
6550
6726
  }
6551
6727
 
6728
+ // Fingerprint fix: real Claude Code v2.1.87+ nests the effort control inside
6729
+ // `output_config.effort` (via Lyz() in cli.js). opencode's provider transform
6730
+ // for variant=max on Opus 4.6 / Sonnet 4.6 sets `effort` at the top level,
6731
+ // which causes Anthropic's server to fingerprint the body as non-CC and bill
6732
+ // it as pay-as-you-go — surfacing as "You're out of extra usage" even on a
6733
+ // valid Max plan. Move it into output_config when we're talking to an
6734
+ // adaptive-thinking model so the wire shape matches real CC.
6735
+ if (typeof parsed.effort === "string" && parsed.model && isAdaptiveThinkingModel(parsed.model)) {
6736
+ if (!parsed.output_config || typeof parsed.output_config !== "object") {
6737
+ parsed.output_config = {};
6738
+ }
6739
+ if (!("effort" in parsed.output_config)) {
6740
+ parsed.output_config.effort = parsed.effort;
6741
+ }
6742
+ delete parsed.effort;
6743
+ } else if (Object.prototype.hasOwnProperty.call(parsed, "effort")) {
6744
+ // Non-adaptive models never carry a top-level effort in real CC — strip it
6745
+ // to avoid polluting the fingerprint for models like Haiku.
6746
+ delete parsed.effort;
6747
+ }
6748
+
6552
6749
  // Claude Code temperature rule: when extended thinking is active (any type),
6553
6750
  // temperature must be omitted (undefined). Otherwise default to 1.
6554
6751
  const thinkingActive =
@@ -6584,7 +6781,7 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
6584
6781
  const modelId = parsed.model || "";
6585
6782
  // Extract first user message text for billing hash computation (cch)
6586
6783
  const firstUserMessage = extractFirstUserMessageText(parsed.messages);
6587
- const signatureWithModel = { ...signature, modelId, firstUserMessage };
6784
+ const signatureWithModel = { ...signature, modelId, firstUserMessage, antiVerbosity: config?.anti_verbosity };
6588
6785
  // Sanitize system prompt and optionally inject Claude Code identity/billing blocks.
6589
6786
  parsed.system = buildSystemPromptBlocks(normalizeSystemTextBlocks(parsed.system), signatureWithModel);
6590
6787
 
@@ -7092,10 +7289,18 @@ function transformResponse(response, onUsage, onAccountError) {
7092
7289
  },
7093
7290
  });
7094
7291
 
7292
+ // Inject cache transparency headers (session-level, available before stream completes).
7293
+ const responseHeaders = new Headers(response.headers);
7294
+ responseHeaders.set("x-opencode-cache-hit-rate", String(Math.round(getAverageCacheHitRate() * 1000) / 1000));
7295
+ responseHeaders.set("x-opencode-cache-read-total", String(sessionMetrics.totalCacheRead));
7296
+ responseHeaders.set("x-opencode-session-cost", String(Math.round(sessionMetrics.sessionCostUsd * 10000) / 10000));
7297
+ responseHeaders.set("x-opencode-turns", String(sessionMetrics.turns));
7298
+ responseHeaders.set("x-opencode-anti-verbosity", _pluginConfig?.anti_verbosity?.enabled !== false ? "on" : "off");
7299
+
7095
7300
  return new Response(stream, {
7096
7301
  status: response.status,
7097
7302
  statusText: response.statusText,
7098
- headers: response.headers,
7303
+ headers: responseHeaders,
7099
7304
  });
7100
7305
  }
7101
7306
 
@@ -7373,4 +7578,34 @@ function extractFileIds(body) {
7373
7578
  return ids;
7374
7579
  }
7375
7580
 
7581
+ // Internals exposed for tests only. Do not consume from production code paths.
7582
+ //
7583
+ // IMPORTANT: do NOT add a new `export` declaration here. Opencode's plugin
7584
+ // loader (opencode/packages/opencode/src/plugin/index.ts:74-79) iterates
7585
+ // `Object.values(mod)` of the loaded module and throws "Plugin export is not
7586
+ // a function" if ANY export is not a plugin function. A named `export const
7587
+ // __testing__ = {...}` object would break plugin loading entirely.
7588
+ //
7589
+ // Instead, attach the test hooks as a PROPERTY of the exported function.
7590
+ // Functions are objects in JS, so this is valid. The module surface still
7591
+ // has only one exported value (the AnthropicAuthPlugin function), which is
7592
+ // what the loader expects. Tests reach internals via
7593
+ // `import { AnthropicAuthPlugin } from "./index.mjs"` then
7594
+ // `AnthropicAuthPlugin.__testing__`.
7595
+ AnthropicAuthPlugin.__testing__ = {
7596
+ sanitizeSystemText,
7597
+ compactSystemText,
7598
+ dedupeSystemBlocks,
7599
+ normalizeSystemTextBlocks,
7600
+ buildSystemPromptBlocks,
7601
+ get cachedCCPrompt() {
7602
+ return cachedCCPrompt;
7603
+ },
7604
+ resetCachedCCPrompt() {
7605
+ cachedCCPrompt = null;
7606
+ },
7607
+ SUBAGENT_CC_ANCHOR,
7608
+ CLAUDE_CODE_IDENTITY_STRING,
7609
+ };
7610
+
7376
7611
  export default AnthropicAuthPlugin;
package/lib/config.mjs CHANGED
@@ -102,6 +102,8 @@ import { randomBytes } from "node:crypto";
102
102
  * @property {{ enabled: boolean, default: number, completion_threshold: number }} token_budget
103
103
  * @property {{ enabled: boolean, threshold_percent: number }} microcompact
104
104
  * @property {{ enabled: boolean, default_cooldown_ms: number, poll_quota_on_overload: boolean }} overload_recovery
105
+ * @property {{ proactive_disabled: boolean }} account_management
106
+ * @property {{ enabled: boolean, length_anchors: boolean }} anti_verbosity
105
107
  */
106
108
 
107
109
  /** @type {AnthropicAuthConfig} */
@@ -241,6 +243,26 @@ export const DEFAULT_CONFIG = {
241
243
  /** Whether to poll /api/oauth/usage on 529 exhaustion for smarter cooldowns. */
242
244
  poll_quota_on_overload: true,
243
245
  },
246
+ /** Account management: control automatic account penalties and switching.
247
+ * When proactive_disabled is true (default), the plugin will NOT apply
248
+ * utilization penalties, surpassed-threshold penalties, or predictive
249
+ * switches based on response headers (200 OK responses). Reactive 429
250
+ * handling still works. This makes account switching fully manual and
251
+ * prevents single-account users from being locally locked out by warning
252
+ * thresholds the server still allows. */
253
+ account_management: {
254
+ proactive_disabled: true,
255
+ },
256
+ /** Anti-verbosity: inject conciseness instructions into system prompt for Opus 4.6.
257
+ * Mirrors CC v2.1.100 anti_verbosity + numeric_length_anchors sections (gated on
258
+ * quiet_salted_ember A/B test in CC; unconditional here since we always want savings).
259
+ * Only activates when model is opus-4-6. */
260
+ anti_verbosity: {
261
+ /** Master switch: inject anti-verbosity communication style instructions. */
262
+ enabled: true,
263
+ /** Also inject numeric length anchors (≤25 words between tool calls, ≤100 words final). */
264
+ length_anchors: true,
265
+ },
244
266
  };
245
267
 
246
268
  export const VALID_STRATEGIES = ["sticky", "round-robin", "hybrid"];
@@ -278,6 +300,7 @@ function createDefaultConfig() {
278
300
  token_budget: { ...DEFAULT_CONFIG.token_budget },
279
301
  microcompact: { ...DEFAULT_CONFIG.microcompact },
280
302
  overload_recovery: { ...DEFAULT_CONFIG.overload_recovery },
303
+ account_management: { ...DEFAULT_CONFIG.account_management },
281
304
  };
282
305
  }
283
306
 
@@ -675,6 +698,27 @@ function validateConfig(raw) {
675
698
  };
676
699
  }
677
700
 
701
+ // Account management sub-config
702
+ if (raw.account_management && typeof raw.account_management === "object") {
703
+ const am = /** @type {Record<string, unknown>} */ (raw.account_management);
704
+ config.account_management = {
705
+ proactive_disabled:
706
+ typeof am.proactive_disabled === "boolean"
707
+ ? am.proactive_disabled
708
+ : DEFAULT_CONFIG.account_management.proactive_disabled,
709
+ };
710
+ }
711
+
712
+ // Anti-verbosity sub-config
713
+ if (raw.anti_verbosity && typeof raw.anti_verbosity === "object") {
714
+ const av = /** @type {Record<string, unknown>} */ (raw.anti_verbosity);
715
+ config.anti_verbosity = {
716
+ enabled: typeof av.enabled === "boolean" ? av.enabled : DEFAULT_CONFIG.anti_verbosity.enabled,
717
+ length_anchors:
718
+ typeof av.length_anchors === "boolean" ? av.length_anchors : DEFAULT_CONFIG.anti_verbosity.length_anchors,
719
+ };
720
+ }
721
+
678
722
  return config;
679
723
  }
680
724
 
@@ -761,6 +805,27 @@ function applyEnvOverrides(config) {
761
805
  config.adaptive_context.enabled = false;
762
806
  }
763
807
 
808
+ // Account management: env override for proactive penalties / predictive switch.
809
+ // Set to 1/true to disable all proactive account management (matches default).
810
+ // Set to 0/false to re-enable the legacy proactive behavior.
811
+ if (env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED === "1" || env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED === "true") {
812
+ config.account_management.proactive_disabled = true;
813
+ }
814
+ if (env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED === "0" || env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED === "false") {
815
+ config.account_management.proactive_disabled = false;
816
+ }
817
+
818
+ // Anti-verbosity: env override for conciseness injection.
819
+ if (env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY === "1" || env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY === "true") {
820
+ config.anti_verbosity.enabled = true;
821
+ }
822
+ if (env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY === "0" || env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY === "false") {
823
+ config.anti_verbosity.enabled = false;
824
+ }
825
+ if (env.OPENCODE_ANTHROPIC_LENGTH_ANCHORS === "0" || env.OPENCODE_ANTHROPIC_LENGTH_ANCHORS === "false") {
826
+ config.anti_verbosity.length_anchors = false;
827
+ }
828
+
764
829
  return config;
765
830
  }
766
831
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "main": "./index.mjs",
6
6
  "files": [