opencode-anthropic-fix 0.1.4 → 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 +114 -2
  2. package/lib/config.mjs +32 -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;
@@ -3864,6 +3866,9 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
3864
3866
  // ---------------------------------------------------------------------------
3865
3867
 
3866
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
+
3867
3872
  const sessionMetrics = {
3868
3873
  turns: 0,
3869
3874
  totalInput: 0,
@@ -4787,6 +4792,9 @@ function updateSessionMetrics(usage, model) {
4787
4792
  sessionMetrics.lastModelId = model;
4788
4793
  }
4789
4794
 
4795
+ // Write cache transparency stats to disk for TUI consumption.
4796
+ writeCacheStatsFile(usage, model, hitRate);
4797
+
4790
4798
  // Token budget tracking (A9)
4791
4799
  if (sessionMetrics.tokenBudget.limit > 0) {
4792
4800
  sessionMetrics.tokenBudget.used += usage.outputTokens;
@@ -4808,6 +4816,63 @@ function getAverageCacheHitRate() {
4808
4816
  return rates.reduce((a, b) => a + b, 0) / rates.length;
4809
4817
  }
4810
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
+
4811
4876
  // --- Phase 5: Auto-strategy adaptation ---
4812
4877
  // strategyState is created per-plugin instance inside AnthropicAuthPlugin() to avoid
4813
4878
  // cross-instance pollution (critical for test isolation and multi-instance scenarios).
@@ -5320,6 +5385,33 @@ const COMPACT_TITLE_GENERATOR_SYSTEM_PROMPT = [
5320
5385
  "- Keep important technical terms, numbers, and filenames when present.",
5321
5386
  ].join("\n");
5322
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
+
5323
5415
  /**
5324
5416
  * Returns the persistent device ID (64-char hex string).
5325
5417
  * Migrates legacy UUID-format values to the new 64-hex format automatically.
@@ -6103,6 +6195,18 @@ function buildSystemPromptBlocks(system, signature) {
6103
6195
  sanitized = dedupeSystemBlocks(sanitized);
6104
6196
  }
6105
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
+
6106
6210
  if (!signature.enabled) {
6107
6211
  return sanitized;
6108
6212
  }
@@ -6677,7 +6781,7 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
6677
6781
  const modelId = parsed.model || "";
6678
6782
  // Extract first user message text for billing hash computation (cch)
6679
6783
  const firstUserMessage = extractFirstUserMessageText(parsed.messages);
6680
- const signatureWithModel = { ...signature, modelId, firstUserMessage };
6784
+ const signatureWithModel = { ...signature, modelId, firstUserMessage, antiVerbosity: config?.anti_verbosity };
6681
6785
  // Sanitize system prompt and optionally inject Claude Code identity/billing blocks.
6682
6786
  parsed.system = buildSystemPromptBlocks(normalizeSystemTextBlocks(parsed.system), signatureWithModel);
6683
6787
 
@@ -7185,10 +7289,18 @@ function transformResponse(response, onUsage, onAccountError) {
7185
7289
  },
7186
7290
  });
7187
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
+
7188
7300
  return new Response(stream, {
7189
7301
  status: response.status,
7190
7302
  statusText: response.statusText,
7191
- headers: response.headers,
7303
+ headers: responseHeaders,
7192
7304
  });
7193
7305
  }
7194
7306
 
package/lib/config.mjs CHANGED
@@ -103,6 +103,7 @@ import { randomBytes } from "node:crypto";
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
105
  * @property {{ proactive_disabled: boolean }} account_management
106
+ * @property {{ enabled: boolean, length_anchors: boolean }} anti_verbosity
106
107
  */
107
108
 
108
109
  /** @type {AnthropicAuthConfig} */
@@ -252,6 +253,16 @@ export const DEFAULT_CONFIG = {
252
253
  account_management: {
253
254
  proactive_disabled: true,
254
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
+ },
255
266
  };
256
267
 
257
268
  export const VALID_STRATEGIES = ["sticky", "round-robin", "hybrid"];
@@ -698,6 +709,16 @@ function validateConfig(raw) {
698
709
  };
699
710
  }
700
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
+
701
722
  return config;
702
723
  }
703
724
 
@@ -794,6 +815,17 @@ function applyEnvOverrides(config) {
794
815
  config.account_management.proactive_disabled = false;
795
816
  }
796
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
+
797
829
  return config;
798
830
  }
799
831
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "main": "./index.mjs",
6
6
  "files": [