opencode-anthropic-fix 0.1.2 → 0.1.4
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 +134 -10
- package/lib/config.mjs +33 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -3041,7 +3041,15 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
3041
3041
|
sessionMetrics.lastQuota.inputTokens = maxUtilization;
|
|
3042
3042
|
}
|
|
3043
3043
|
|
|
3044
|
-
|
|
3044
|
+
// Proactive account management is gated on config. When
|
|
3045
|
+
// account_management.proactive_disabled is true (default),
|
|
3046
|
+
// we never apply penalties on a 200 OK response — those
|
|
3047
|
+
// penalties were locking out single-account users whose
|
|
3048
|
+
// server-side quota was still in `allowed_warning` state.
|
|
3049
|
+
// The reactive 429 path below is unaffected.
|
|
3050
|
+
const proactiveDisabled = config.account_management?.proactive_disabled !== false;
|
|
3051
|
+
|
|
3052
|
+
if (!proactiveDisabled && maxUtilization > 0.8) {
|
|
3045
3053
|
const penalty = Math.round((maxUtilization - 0.8) * 50); // 0-10 points
|
|
3046
3054
|
accountManager.applyUtilizationPenalty(account, penalty);
|
|
3047
3055
|
debugLog("high rate limit utilization", {
|
|
@@ -3052,7 +3060,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
3052
3060
|
});
|
|
3053
3061
|
}
|
|
3054
3062
|
|
|
3055
|
-
if (anySurpassed) {
|
|
3063
|
+
if (!proactiveDisabled && anySurpassed) {
|
|
3056
3064
|
accountManager.applySurpassedThreshold(account, surpassedResetAt);
|
|
3057
3065
|
debugLog("rate limit threshold surpassed", {
|
|
3058
3066
|
accountIndex: account.index,
|
|
@@ -3070,8 +3078,10 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
3070
3078
|
}
|
|
3071
3079
|
|
|
3072
3080
|
// Predictive rate limit avoidance: switch account BEFORE hitting 429
|
|
3073
|
-
// Parse reset timestamps to compute time-weighted risk
|
|
3074
|
-
|
|
3081
|
+
// Parse reset timestamps to compute time-weighted risk.
|
|
3082
|
+
// Gated on proactive_disabled — when true (default), no automatic
|
|
3083
|
+
// switches happen on 200 OK responses (fully manual rotation).
|
|
3084
|
+
if (!proactiveDisabled && maxUtilization > 0.6 && accountManager.getAccountCount() > 1) {
|
|
3075
3085
|
let highestRisk = 0;
|
|
3076
3086
|
for (const win of RATE_LIMIT_WINDOWS) {
|
|
3077
3087
|
const utilizationStr = response.headers.get(`anthropic-ratelimit-unified-${win.key}-utilization`);
|
|
@@ -5722,6 +5732,24 @@ function buildAnthropicBillingHeader(version, firstUserMessage, provider) {
|
|
|
5722
5732
|
// Opencode's customizations after ~5800 chars diverge and trigger extra usage billing.
|
|
5723
5733
|
const MAX_SAFE_SYSTEM_TEXT_LENGTH = 5000;
|
|
5724
5734
|
|
|
5735
|
+
// A5: Subagent CC-prefix cache.
|
|
5736
|
+
//
|
|
5737
|
+
// Context: opencode/packages/opencode/src/session/llm.ts:110 uses
|
|
5738
|
+
// `input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(model)`
|
|
5739
|
+
// so any agent with a custom prompt (explore, fast, title, summary, etc.)
|
|
5740
|
+
// fires WITHOUT the base CC prompt — the server-side fingerprint match fails
|
|
5741
|
+
// and the request is billed as pay-as-you-go credits instead of Max-plan usage.
|
|
5742
|
+
//
|
|
5743
|
+
// Fix: on the first main-agent call (where the anchor is present), cache the
|
|
5744
|
+
// sanitized CC prefix. On subsequent subagent calls (anchor missing), prepend
|
|
5745
|
+
// the cached prefix to the sanitized blocks so the fingerprint matches again.
|
|
5746
|
+
//
|
|
5747
|
+
// The cache lives at module scope because buildSystemPromptBlocks is re-entered
|
|
5748
|
+
// per request. It gets populated exactly once per process on the first main call.
|
|
5749
|
+
const MAX_SUBAGENT_CC_PREFIX = MAX_SAFE_SYSTEM_TEXT_LENGTH;
|
|
5750
|
+
const SUBAGENT_CC_ANCHOR = "You are an interactive";
|
|
5751
|
+
let cachedCCPrompt = null;
|
|
5752
|
+
|
|
5725
5753
|
function sanitizeSystemText(text) {
|
|
5726
5754
|
// QA fix M4: use word boundaries to avoid mangling URLs and code identifiers
|
|
5727
5755
|
let sanitized = text.replace(/\bOpenCode\b/g, "Claude Code").replace(/\bopencode\b/gi, "Claude");
|
|
@@ -6035,6 +6063,40 @@ function buildSystemPromptBlocks(system, signature) {
|
|
|
6035
6063
|
text: compactSystemText(sanitizeSystemText(item.text), signature.promptCompactionMode),
|
|
6036
6064
|
}));
|
|
6037
6065
|
|
|
6066
|
+
// A5: Subagent CC-prefix cache/inject (see constant declaration above for context).
|
|
6067
|
+
//
|
|
6068
|
+
// After sanitize, main-agent blocks start with "You are an interactive..." because
|
|
6069
|
+
// sanitizeSystemText() strips everything before that anchor. Subagent blocks
|
|
6070
|
+
// (custom prompts from input.agent.prompt) do NOT start with the anchor —
|
|
6071
|
+
// they start with whatever the agent template says (e.g., "You are a file search
|
|
6072
|
+
// specialist.").
|
|
6073
|
+
//
|
|
6074
|
+
// This logic runs ONLY for Anthropic requests with signature enabled (signature.enabled
|
|
6075
|
+
// is false for non-Anthropic providers), and skips the title-generator fast path
|
|
6076
|
+
// because that one is replaced wholesale with COMPACT_TITLE_GENERATOR_SYSTEM_PROMPT below.
|
|
6077
|
+
if (signature.enabled && !titleGeneratorRequest && sanitized.length > 0) {
|
|
6078
|
+
const firstText = typeof sanitized[0]?.text === "string" ? sanitized[0].text : "";
|
|
6079
|
+
const hasCcAnchor = firstText.startsWith(SUBAGENT_CC_ANCHOR);
|
|
6080
|
+
|
|
6081
|
+
if (hasCcAnchor) {
|
|
6082
|
+
// Main-agent path: cache the prefix on the first hit so subagents can reuse it.
|
|
6083
|
+
// We slice to MAX_SUBAGENT_CC_PREFIX to avoid unbounded growth if the upstream
|
|
6084
|
+
// sanitize limit is ever raised.
|
|
6085
|
+
if (!cachedCCPrompt) {
|
|
6086
|
+
cachedCCPrompt = firstText.slice(0, MAX_SUBAGENT_CC_PREFIX);
|
|
6087
|
+
}
|
|
6088
|
+
} else if (cachedCCPrompt) {
|
|
6089
|
+
// Subagent path: prepend the cached CC prefix so the fingerprint matches.
|
|
6090
|
+
// We prepend, not concatenate, so the original subagent prompt stays as a
|
|
6091
|
+
// separate block — dedupeSystemBlocks and splitSysPromptPrefix handle the
|
|
6092
|
+
// join on their own downstream.
|
|
6093
|
+
sanitized = [{ type: "text", text: cachedCCPrompt }, ...sanitized];
|
|
6094
|
+
}
|
|
6095
|
+
// If !hasCcAnchor && !cachedCCPrompt: no-op. The cache primes on the very
|
|
6096
|
+
// first main call in a process. In practice opencode always fires a main
|
|
6097
|
+
// call before any subagent, so this branch is only hit in synthetic tests.
|
|
6098
|
+
}
|
|
6099
|
+
|
|
6038
6100
|
if (titleGeneratorRequest) {
|
|
6039
6101
|
sanitized = [{ type: "text", text: COMPACT_TITLE_GENERATOR_SYSTEM_PROMPT }];
|
|
6040
6102
|
} else if (signature.promptCompactionMode !== "off") {
|
|
@@ -6133,9 +6195,10 @@ function buildAnthropicBetaHeader(
|
|
|
6133
6195
|
|
|
6134
6196
|
// === ALWAYS-ON BETAS (Claude Code v2.1.90 base set) ===
|
|
6135
6197
|
// These are ALWAYS included regardless of env vars or feature flags.
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6198
|
+
// NOTE: Real Claude Code skips this beta for Haiku, but we include it
|
|
6199
|
+
// so that Haiku subagents (via model-router delegation) get full mimic
|
|
6200
|
+
// behavior from the Anthropic API.
|
|
6201
|
+
betas.push(CLAUDE_CODE_BETA_FLAG); // "claude-code-20250219"
|
|
6139
6202
|
|
|
6140
6203
|
// Tool search: use provider-aware header.
|
|
6141
6204
|
// 1P/Foundry u2192 advanced-tool-use-2025-11-20 (enables broader tool capabilities)
|
|
@@ -6147,10 +6210,20 @@ function buildAnthropicBetaHeader(
|
|
|
6147
6210
|
}
|
|
6148
6211
|
|
|
6149
6212
|
betas.push(FAST_MODE_BETA_FLAG); // "fast-mode-2026-02-01"
|
|
6150
|
-
betas.push(EFFORT_BETA_FLAG); // "effort-2025-11-24"
|
|
6151
6213
|
|
|
6152
|
-
//
|
|
6153
|
-
|
|
6214
|
+
// effort-2025-11-24 — real CC's Lyz() only pushes this flag when rE(model)
|
|
6215
|
+
// is true (Opus 4.6 / Sonnet 4.6). Pushing it for non-adaptive models like
|
|
6216
|
+
// Haiku is a fingerprint mismatch vs real CC and can contaminate billing
|
|
6217
|
+
// attribution even when the request body has no effort field.
|
|
6218
|
+
if (isAdaptiveThinkingModel(model)) {
|
|
6219
|
+
betas.push(EFFORT_BETA_FLAG); // "effort-2025-11-24"
|
|
6220
|
+
}
|
|
6221
|
+
|
|
6222
|
+
// Interleaved thinking — real CC's i01 pushes via hv4(model), which is
|
|
6223
|
+
// (firstParty && non-Claude-3). Claude 3.x models don't support interleaved
|
|
6224
|
+
// thinking and real CC never sends this flag for them, so emitting it
|
|
6225
|
+
// diverges the fingerprint for legacy Haiku/Sonnet 3.x requests.
|
|
6226
|
+
if (!isTruthyEnv(process.env.DISABLE_INTERLEAVED_THINKING) && !/claude-3-/i.test(model)) {
|
|
6154
6227
|
betas.push("interleaved-thinking-2025-05-14");
|
|
6155
6228
|
}
|
|
6156
6229
|
|
|
@@ -6548,6 +6621,27 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
|
|
|
6548
6621
|
parsed.thinking = normalizeThinkingBlock(parsed.thinking, parsed.model || "");
|
|
6549
6622
|
}
|
|
6550
6623
|
|
|
6624
|
+
// Fingerprint fix: real Claude Code v2.1.87+ nests the effort control inside
|
|
6625
|
+
// `output_config.effort` (via Lyz() in cli.js). opencode's provider transform
|
|
6626
|
+
// for variant=max on Opus 4.6 / Sonnet 4.6 sets `effort` at the top level,
|
|
6627
|
+
// which causes Anthropic's server to fingerprint the body as non-CC and bill
|
|
6628
|
+
// it as pay-as-you-go — surfacing as "You're out of extra usage" even on a
|
|
6629
|
+
// valid Max plan. Move it into output_config when we're talking to an
|
|
6630
|
+
// adaptive-thinking model so the wire shape matches real CC.
|
|
6631
|
+
if (typeof parsed.effort === "string" && parsed.model && isAdaptiveThinkingModel(parsed.model)) {
|
|
6632
|
+
if (!parsed.output_config || typeof parsed.output_config !== "object") {
|
|
6633
|
+
parsed.output_config = {};
|
|
6634
|
+
}
|
|
6635
|
+
if (!("effort" in parsed.output_config)) {
|
|
6636
|
+
parsed.output_config.effort = parsed.effort;
|
|
6637
|
+
}
|
|
6638
|
+
delete parsed.effort;
|
|
6639
|
+
} else if (Object.prototype.hasOwnProperty.call(parsed, "effort")) {
|
|
6640
|
+
// Non-adaptive models never carry a top-level effort in real CC — strip it
|
|
6641
|
+
// to avoid polluting the fingerprint for models like Haiku.
|
|
6642
|
+
delete parsed.effort;
|
|
6643
|
+
}
|
|
6644
|
+
|
|
6551
6645
|
// Claude Code temperature rule: when extended thinking is active (any type),
|
|
6552
6646
|
// temperature must be omitted (undefined). Otherwise default to 1.
|
|
6553
6647
|
const thinkingActive =
|
|
@@ -7372,4 +7466,34 @@ function extractFileIds(body) {
|
|
|
7372
7466
|
return ids;
|
|
7373
7467
|
}
|
|
7374
7468
|
|
|
7469
|
+
// Internals exposed for tests only. Do not consume from production code paths.
|
|
7470
|
+
//
|
|
7471
|
+
// IMPORTANT: do NOT add a new `export` declaration here. Opencode's plugin
|
|
7472
|
+
// loader (opencode/packages/opencode/src/plugin/index.ts:74-79) iterates
|
|
7473
|
+
// `Object.values(mod)` of the loaded module and throws "Plugin export is not
|
|
7474
|
+
// a function" if ANY export is not a plugin function. A named `export const
|
|
7475
|
+
// __testing__ = {...}` object would break plugin loading entirely.
|
|
7476
|
+
//
|
|
7477
|
+
// Instead, attach the test hooks as a PROPERTY of the exported function.
|
|
7478
|
+
// Functions are objects in JS, so this is valid. The module surface still
|
|
7479
|
+
// has only one exported value (the AnthropicAuthPlugin function), which is
|
|
7480
|
+
// what the loader expects. Tests reach internals via
|
|
7481
|
+
// `import { AnthropicAuthPlugin } from "./index.mjs"` then
|
|
7482
|
+
// `AnthropicAuthPlugin.__testing__`.
|
|
7483
|
+
AnthropicAuthPlugin.__testing__ = {
|
|
7484
|
+
sanitizeSystemText,
|
|
7485
|
+
compactSystemText,
|
|
7486
|
+
dedupeSystemBlocks,
|
|
7487
|
+
normalizeSystemTextBlocks,
|
|
7488
|
+
buildSystemPromptBlocks,
|
|
7489
|
+
get cachedCCPrompt() {
|
|
7490
|
+
return cachedCCPrompt;
|
|
7491
|
+
},
|
|
7492
|
+
resetCachedCCPrompt() {
|
|
7493
|
+
cachedCCPrompt = null;
|
|
7494
|
+
},
|
|
7495
|
+
SUBAGENT_CC_ANCHOR,
|
|
7496
|
+
CLAUDE_CODE_IDENTITY_STRING,
|
|
7497
|
+
};
|
|
7498
|
+
|
|
7375
7499
|
export default AnthropicAuthPlugin;
|
package/lib/config.mjs
CHANGED
|
@@ -102,6 +102,7 @@ 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
|
|
105
106
|
*/
|
|
106
107
|
|
|
107
108
|
/** @type {AnthropicAuthConfig} */
|
|
@@ -241,6 +242,16 @@ export const DEFAULT_CONFIG = {
|
|
|
241
242
|
/** Whether to poll /api/oauth/usage on 529 exhaustion for smarter cooldowns. */
|
|
242
243
|
poll_quota_on_overload: true,
|
|
243
244
|
},
|
|
245
|
+
/** Account management: control automatic account penalties and switching.
|
|
246
|
+
* When proactive_disabled is true (default), the plugin will NOT apply
|
|
247
|
+
* utilization penalties, surpassed-threshold penalties, or predictive
|
|
248
|
+
* switches based on response headers (200 OK responses). Reactive 429
|
|
249
|
+
* handling still works. This makes account switching fully manual and
|
|
250
|
+
* prevents single-account users from being locally locked out by warning
|
|
251
|
+
* thresholds the server still allows. */
|
|
252
|
+
account_management: {
|
|
253
|
+
proactive_disabled: true,
|
|
254
|
+
},
|
|
244
255
|
};
|
|
245
256
|
|
|
246
257
|
export const VALID_STRATEGIES = ["sticky", "round-robin", "hybrid"];
|
|
@@ -278,6 +289,7 @@ function createDefaultConfig() {
|
|
|
278
289
|
token_budget: { ...DEFAULT_CONFIG.token_budget },
|
|
279
290
|
microcompact: { ...DEFAULT_CONFIG.microcompact },
|
|
280
291
|
overload_recovery: { ...DEFAULT_CONFIG.overload_recovery },
|
|
292
|
+
account_management: { ...DEFAULT_CONFIG.account_management },
|
|
281
293
|
};
|
|
282
294
|
}
|
|
283
295
|
|
|
@@ -675,6 +687,17 @@ function validateConfig(raw) {
|
|
|
675
687
|
};
|
|
676
688
|
}
|
|
677
689
|
|
|
690
|
+
// Account management sub-config
|
|
691
|
+
if (raw.account_management && typeof raw.account_management === "object") {
|
|
692
|
+
const am = /** @type {Record<string, unknown>} */ (raw.account_management);
|
|
693
|
+
config.account_management = {
|
|
694
|
+
proactive_disabled:
|
|
695
|
+
typeof am.proactive_disabled === "boolean"
|
|
696
|
+
? am.proactive_disabled
|
|
697
|
+
: DEFAULT_CONFIG.account_management.proactive_disabled,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
678
701
|
return config;
|
|
679
702
|
}
|
|
680
703
|
|
|
@@ -761,6 +784,16 @@ function applyEnvOverrides(config) {
|
|
|
761
784
|
config.adaptive_context.enabled = false;
|
|
762
785
|
}
|
|
763
786
|
|
|
787
|
+
// Account management: env override for proactive penalties / predictive switch.
|
|
788
|
+
// Set to 1/true to disable all proactive account management (matches default).
|
|
789
|
+
// Set to 0/false to re-enable the legacy proactive behavior.
|
|
790
|
+
if (env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED === "1" || env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED === "true") {
|
|
791
|
+
config.account_management.proactive_disabled = true;
|
|
792
|
+
}
|
|
793
|
+
if (env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED === "0" || env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED === "false") {
|
|
794
|
+
config.account_management.proactive_disabled = false;
|
|
795
|
+
}
|
|
796
|
+
|
|
764
797
|
return config;
|
|
765
798
|
}
|
|
766
799
|
|