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.
- package/index.mjs +114 -2
- package/lib/config.mjs +32 -0
- 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:
|
|
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
|
|