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.
- package/index.mjs +244 -9
- package/lib/config.mjs +65 -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;
|
|
@@ -3041,7 +3043,15 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
3041
3043
|
sessionMetrics.lastQuota.inputTokens = maxUtilization;
|
|
3042
3044
|
}
|
|
3043
3045
|
|
|
3044
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
6154
|
-
|
|
6318
|
+
// effort-2025-11-24 — real 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:
|
|
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
|
|