opencode-anthropic-fix 0.0.27 → 0.0.29
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 +186 -11
- package/lib/accounts.mjs +12 -0
- package/lib/oauth.mjs +1 -1
- package/lib/storage.mjs +2 -2
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -1314,6 +1314,10 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
1314
1314
|
// Quiet mode suppresses non-error toasts
|
|
1315
1315
|
if (config.toasts.quiet && variant !== "error") return;
|
|
1316
1316
|
|
|
1317
|
+
// Normalize variant to values OpenCode TUI supports (success, error, info).
|
|
1318
|
+
// "warning" is not a supported variant and causes silent failures.
|
|
1319
|
+
const normalizedVariant = variant === "warning" ? "info" : variant;
|
|
1320
|
+
|
|
1317
1321
|
// Debounce configured toast categories to reduce chatter.
|
|
1318
1322
|
if (variant !== "error" && options.debounceKey) {
|
|
1319
1323
|
const minGapMs = Math.max(0, config.toasts.debounce_seconds) * 1000;
|
|
@@ -1335,7 +1339,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
1335
1339
|
}
|
|
1336
1340
|
|
|
1337
1341
|
try {
|
|
1338
|
-
await client.tui?.showToast({ body: { message, variant } });
|
|
1342
|
+
await client.tui?.showToast({ body: { message, variant: normalizedVariant } });
|
|
1339
1343
|
} catch {
|
|
1340
1344
|
// TUI may not be available
|
|
1341
1345
|
}
|
|
@@ -1801,6 +1805,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
1801
1805
|
|
|
1802
1806
|
let serviceWideRetryCount = 0; // Track 529/503 retries (max 2 per RE doc §5.5)
|
|
1803
1807
|
let shouldRetryCount = 0; // Track x-should-retry forced retries (cap at 3)
|
|
1808
|
+
let consecutive529Count = 0;
|
|
1804
1809
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1805
1810
|
// Select account — use pinned account on first attempt if available
|
|
1806
1811
|
const account =
|
|
@@ -1960,7 +1965,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
1960
1965
|
_reqHasFileRefs,
|
|
1961
1966
|
);
|
|
1962
1967
|
|
|
1963
|
-
|
|
1968
|
+
let body = transformRequestBody(
|
|
1964
1969
|
requestInit.body,
|
|
1965
1970
|
{
|
|
1966
1971
|
enabled: getSignatureEmulationEnabled(),
|
|
@@ -1969,6 +1974,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
1969
1974
|
provider: _reqProvider,
|
|
1970
1975
|
cachePolicy: config.cache_policy || { ttl: "1h", ttl_supported: true },
|
|
1971
1976
|
fastMode: config.fast_mode || false,
|
|
1977
|
+
strategy: getEffectiveStrategy(),
|
|
1972
1978
|
},
|
|
1973
1979
|
{
|
|
1974
1980
|
persistentUserId: signatureUserId,
|
|
@@ -2076,6 +2082,47 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2076
2082
|
{ debounceKey: "quota-warn" },
|
|
2077
2083
|
).catch(() => {});
|
|
2078
2084
|
}
|
|
2085
|
+
|
|
2086
|
+
// Predictive rate limit avoidance: switch account BEFORE hitting 429
|
|
2087
|
+
// Parse reset timestamps to compute time-weighted risk
|
|
2088
|
+
if (maxUtilization > 0.6 && accountManager.getAccountCount() > 1) {
|
|
2089
|
+
let highestRisk = 0;
|
|
2090
|
+
for (const subtype of RATE_LIMIT_SUBTYPES) {
|
|
2091
|
+
const utilizationStr = response.headers.get(`anthropic-ratelimit-unified-${subtype}-utilization`);
|
|
2092
|
+
const resetAtStr = response.headers.get(`anthropic-ratelimit-unified-${subtype}-reset`);
|
|
2093
|
+
if (!utilizationStr || !resetAtStr) continue;
|
|
2094
|
+
|
|
2095
|
+
const utilization = parseFloat(utilizationStr);
|
|
2096
|
+
const resetAt = new Date(resetAtStr).getTime();
|
|
2097
|
+
if (isNaN(utilization) || isNaN(resetAt)) continue;
|
|
2098
|
+
|
|
2099
|
+
// Estimate window duration from reset time
|
|
2100
|
+
// 5h window = 18000s, 7d window = 604800s
|
|
2101
|
+
const timeUntilReset = Math.max(0, resetAt - Date.now());
|
|
2102
|
+
// Risk formula: how fast we're burning through the quota
|
|
2103
|
+
// Higher utilization + less time remaining = higher risk
|
|
2104
|
+
const timeRemainingFraction = Math.max(0.01, timeUntilReset / (5 * 3600 * 1000)); // assume 5h window
|
|
2105
|
+
const risk = utilization / timeRemainingFraction;
|
|
2106
|
+
if (risk > highestRisk) highestRisk = risk;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// Preemptive switch threshold
|
|
2110
|
+
if (highestRisk > 0.85 && accountManager.getAccountCount() > 1) {
|
|
2111
|
+
const currentName = account.email || `Account ${account.index + 1}`;
|
|
2112
|
+
const nextAccount = accountManager.peekNextAccount?.();
|
|
2113
|
+
const nextName = nextAccount?.email || "next account";
|
|
2114
|
+
accountManager.markRateLimited(account, "RATE_LIMIT_EXCEEDED", null);
|
|
2115
|
+
toast(
|
|
2116
|
+
`Predictive switch: ${currentName} at high burn rate, switching to ${nextName}`,
|
|
2117
|
+
"warning",
|
|
2118
|
+
{ debounceKey: "predictive-switch" },
|
|
2119
|
+
).catch(() => {});
|
|
2120
|
+
debugLog("predictive rate limit switch", {
|
|
2121
|
+
accountIndex: account.index,
|
|
2122
|
+
risk: highestRisk.toFixed(2),
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2079
2126
|
}
|
|
2080
2127
|
|
|
2081
2128
|
// On error, check if it's account-specific or service-wide
|
|
@@ -2087,6 +2134,46 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2087
2134
|
// Ignore read errors in debug logging path.
|
|
2088
2135
|
}
|
|
2089
2136
|
|
|
2137
|
+
// Reactive compaction: on "prompt too long" error, trim oldest messages and retry once
|
|
2138
|
+
if (
|
|
2139
|
+
response.status === 400 &&
|
|
2140
|
+
errorBody &&
|
|
2141
|
+
(errorBody.includes("prompt is too long") || errorBody.includes("prompt_too_long")) &&
|
|
2142
|
+
!requestInit._reactiveCompactAttempted
|
|
2143
|
+
) {
|
|
2144
|
+
debugLog("prompt too long — attempting reactive message trimming");
|
|
2145
|
+
try {
|
|
2146
|
+
const parsedBody = JSON.parse(body);
|
|
2147
|
+
if (Array.isArray(parsedBody.messages) && parsedBody.messages.length > 4) {
|
|
2148
|
+
// Keep first 2 messages (initial context) and last 2 messages (recent work)
|
|
2149
|
+
const trimmed = [
|
|
2150
|
+
...parsedBody.messages.slice(0, 2),
|
|
2151
|
+
{
|
|
2152
|
+
role: "user",
|
|
2153
|
+
content: [
|
|
2154
|
+
{
|
|
2155
|
+
type: "text",
|
|
2156
|
+
text: "[Earlier conversation was trimmed due to context limits. Continue from the most recent context.]",
|
|
2157
|
+
},
|
|
2158
|
+
],
|
|
2159
|
+
},
|
|
2160
|
+
...parsedBody.messages.slice(-2),
|
|
2161
|
+
];
|
|
2162
|
+
parsedBody.messages = trimmed;
|
|
2163
|
+
requestInit.body = JSON.stringify(parsedBody);
|
|
2164
|
+
requestInit._reactiveCompactAttempted = true;
|
|
2165
|
+
// Retry with trimmed messages (decrement attempt to not consume account slot)
|
|
2166
|
+
attempt--;
|
|
2167
|
+
toast("Context trimmed — retrying with shortened history", "warning", {
|
|
2168
|
+
debounceKey: "compact-retry",
|
|
2169
|
+
}).catch(() => {});
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
} catch {
|
|
2173
|
+
// If body parse fails, fall through to normal error handling
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2090
2177
|
// Auto-disable extended cache TTL if the API rejects it with a 400
|
|
2091
2178
|
if (
|
|
2092
2179
|
response.status === 400 &&
|
|
@@ -2150,6 +2237,15 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2150
2237
|
// Strategy adaptation: record account-specific throttling signal
|
|
2151
2238
|
recordRateLimitForStrategy();
|
|
2152
2239
|
|
|
2240
|
+
// Graceful degradation: disable fast mode on rate limits
|
|
2241
|
+
if (config.fast_mode && (response.status === 429 || response.status === 529)) {
|
|
2242
|
+
config.fast_mode = false;
|
|
2243
|
+
toast("Fast mode disabled due to rate limiting", "warning", {
|
|
2244
|
+
debounceKey: "fast-mode-off",
|
|
2245
|
+
}).catch(() => {});
|
|
2246
|
+
debugLog("auto-disabled fast mode after rate limit");
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2153
2249
|
const accountName = account.email || `Account ${account.index + 1}`;
|
|
2154
2250
|
const lowerBody = String(errorBody || "").toLowerCase();
|
|
2155
2251
|
const switchMsg =
|
|
@@ -2170,6 +2266,43 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2170
2266
|
// per RE doc §5.5 (Stainless SDK retries 500+ codes up to 2 times)
|
|
2171
2267
|
if ((response.status === 529 || response.status === 503) && serviceWideRetryCount < 2) {
|
|
2172
2268
|
serviceWideRetryCount++;
|
|
2269
|
+
|
|
2270
|
+
// Track consecutive 529s for model fallback
|
|
2271
|
+
if (response.status === 529) {
|
|
2272
|
+
consecutive529Count++;
|
|
2273
|
+
if (consecutive529Count >= 3 && body) {
|
|
2274
|
+
try {
|
|
2275
|
+
const parsedForFallback = JSON.parse(body);
|
|
2276
|
+
const currentModel = parsedForFallback.model || "";
|
|
2277
|
+
let fallbackModel = null;
|
|
2278
|
+
if (/opus-4-6|opus-4/i.test(currentModel))
|
|
2279
|
+
fallbackModel = currentModel.replace(/opus/i, "sonnet");
|
|
2280
|
+
else if (/sonnet-4-6|sonnet-4/i.test(currentModel))
|
|
2281
|
+
fallbackModel = currentModel.replace(/sonnet/i, "haiku");
|
|
2282
|
+
|
|
2283
|
+
if (fallbackModel) {
|
|
2284
|
+
parsedForFallback.model = fallbackModel;
|
|
2285
|
+
requestInit.body = JSON.stringify(parsedForFallback);
|
|
2286
|
+
body = requestInit.body;
|
|
2287
|
+
toast(
|
|
2288
|
+
`Model fallback: ${currentModel} → ${fallbackModel} after ${consecutive529Count} overloads`,
|
|
2289
|
+
"warning",
|
|
2290
|
+
{ debounceKey: "model-fallback" },
|
|
2291
|
+
).catch(() => {});
|
|
2292
|
+
debugLog("model fallback on consecutive 529", {
|
|
2293
|
+
from: currentModel,
|
|
2294
|
+
to: fallbackModel,
|
|
2295
|
+
count: consecutive529Count,
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
} catch {
|
|
2299
|
+
/* ignore parse errors */
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
} else {
|
|
2303
|
+
consecutive529Count = 0;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2173
2306
|
const baseDelay = Math.min(0.5 * Math.pow(2, serviceWideRetryCount), 8);
|
|
2174
2307
|
const jitter = 1 - Math.random() * 0.25;
|
|
2175
2308
|
const sleepMs = Math.round(baseDelay * jitter * 1000);
|
|
@@ -2419,6 +2552,28 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2419
2552
|
},
|
|
2420
2553
|
],
|
|
2421
2554
|
},
|
|
2555
|
+
"experimental.session.compacting": async (input, output) => {
|
|
2556
|
+
// Inject Anthropic-specific context into compaction
|
|
2557
|
+
if (!accountManager) return;
|
|
2558
|
+
const account = accountManager.getCurrentAccount();
|
|
2559
|
+
const name = account?.email || "unknown";
|
|
2560
|
+
const q = sessionMetrics.lastQuota;
|
|
2561
|
+
const contextParts = [];
|
|
2562
|
+
|
|
2563
|
+
contextParts.push(`## Anthropic Account State
|
|
2564
|
+
- Active account: ${name}
|
|
2565
|
+
- Session cost: $${sessionMetrics.sessionCostUsd.toFixed(4)}
|
|
2566
|
+
- Turns: ${sessionMetrics.turns}
|
|
2567
|
+
- Cache hit rate: ${(getAverageCacheHitRate() * 100).toFixed(0)}%`);
|
|
2568
|
+
|
|
2569
|
+
if (q.updatedAt > 0) {
|
|
2570
|
+
contextParts.push(
|
|
2571
|
+
`- Rate limit utilization: tokens=${(q.tokens * 100).toFixed(0)}%, requests=${(q.requests * 100).toFixed(0)}%`,
|
|
2572
|
+
);
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
output.context.push(contextParts.join("\n"));
|
|
2576
|
+
},
|
|
2422
2577
|
};
|
|
2423
2578
|
}
|
|
2424
2579
|
|
|
@@ -2720,18 +2875,19 @@ process.once("beforeExit", () => {
|
|
|
2720
2875
|
// Request building helpers (extracted from original fetch interceptor)
|
|
2721
2876
|
// ---------------------------------------------------------------------------
|
|
2722
2877
|
|
|
2723
|
-
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.
|
|
2878
|
+
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.83";
|
|
2724
2879
|
const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
2725
2880
|
|
|
2726
|
-
// The @anthropic-ai/sdk version bundled with Claude Code v2.1.
|
|
2881
|
+
// The @anthropic-ai/sdk version bundled with Claude Code v2.1.83.
|
|
2727
2882
|
// This is distinct from the CLI version and goes in X-Stainless-Package-Version.
|
|
2728
|
-
|
|
2883
|
+
// Verified by extracting VERSION="0.208.0" from the bundled cli.js of v2.1.80/2.1.81/2.1.83.
|
|
2884
|
+
const ANTHROPIC_SDK_VERSION = "0.208.0";
|
|
2729
2885
|
|
|
2730
2886
|
// Map of CLI version → bundled SDK version (update when CLI version changes)
|
|
2731
2887
|
const CLI_TO_SDK_VERSION = new Map([
|
|
2732
|
-
["2.1.
|
|
2733
|
-
["2.1.
|
|
2734
|
-
["2.1.
|
|
2888
|
+
["2.1.83", "0.208.0"],
|
|
2889
|
+
["2.1.81", "0.208.0"],
|
|
2890
|
+
["2.1.80", "0.208.0"],
|
|
2735
2891
|
]);
|
|
2736
2892
|
|
|
2737
2893
|
/**
|
|
@@ -3179,9 +3335,9 @@ function buildRequestMetadata(input) {
|
|
|
3179
3335
|
|
|
3180
3336
|
/**
|
|
3181
3337
|
* Build the billing header block for Claude Code system prompt injection.
|
|
3182
|
-
* Claude Code v2.1.81
|
|
3338
|
+
* Claude Code v2.1.81+: cch computed via NP1() from first user message, cc_version includes model ID.
|
|
3183
3339
|
*
|
|
3184
|
-
* @param {string} version - CLI version (e.g., "2.1.
|
|
3340
|
+
* @param {string} version - CLI version (e.g., "2.1.83")
|
|
3185
3341
|
* @param {string} [modelId] - Model ID to append (e.g., "claude-opus-4-6")
|
|
3186
3342
|
* @param {string} [firstUserMessage] - First user message text for cch hash computation
|
|
3187
3343
|
* @returns {string}
|
|
@@ -3504,7 +3660,7 @@ function buildAnthropicBetaHeader(
|
|
|
3504
3660
|
const haiku = isHaikuModel(model);
|
|
3505
3661
|
const isRoundRobin = strategy === "round-robin";
|
|
3506
3662
|
|
|
3507
|
-
// === ALWAYS-ON BETAS (Claude Code v2.1.
|
|
3663
|
+
// === ALWAYS-ON BETAS (Claude Code v2.1.83 base set) ===
|
|
3508
3664
|
// These are ALWAYS included regardless of env vars or feature flags.
|
|
3509
3665
|
if (!haiku) {
|
|
3510
3666
|
betas.push(CLAUDE_CODE_BETA_FLAG); // "claude-code-20250219"
|
|
@@ -3887,6 +4043,25 @@ function transformRequestBody(body, signature, runtime, betaHeader) {
|
|
|
3887
4043
|
};
|
|
3888
4044
|
}
|
|
3889
4045
|
|
|
4046
|
+
// Cache breakpoint optimization: add cache_control to the last content block
|
|
4047
|
+
// of each user/assistant message for maximum prefix caching.
|
|
4048
|
+
// Skip for round-robin strategy (cache defeated by account rotation).
|
|
4049
|
+
if (signature.enabled && signature.cachePolicy?.ttl !== "off" && signature.cachePolicy?.ttl_supported !== false) {
|
|
4050
|
+
const strategy = signature.strategy || "sticky";
|
|
4051
|
+
if (strategy !== "round-robin" && Array.isArray(parsed.messages)) {
|
|
4052
|
+
const cacheControl = { type: "ephemeral", ttl: signature.cachePolicy?.ttl || "1h" };
|
|
4053
|
+
for (const msg of parsed.messages) {
|
|
4054
|
+
if (!msg.content || !Array.isArray(msg.content) || msg.content.length === 0) continue;
|
|
4055
|
+
// Only cache user and assistant messages (not tool results which change frequently)
|
|
4056
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
4057
|
+
const lastBlock = msg.content[msg.content.length - 1];
|
|
4058
|
+
if (lastBlock && typeof lastBlock === "object") {
|
|
4059
|
+
lastBlock.cache_control = cacheControl;
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
|
|
3890
4065
|
// Add prefix to tools definitions
|
|
3891
4066
|
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
3892
4067
|
parsed.tools = parsed.tools.map((tool) => ({
|
package/lib/accounts.mjs
CHANGED
|
@@ -222,6 +222,18 @@ export class AccountManager {
|
|
|
222
222
|
return true;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Peek at the next account that would be selected without actually switching.
|
|
227
|
+
* Used for predictive rate limit avoidance messaging.
|
|
228
|
+
* @returns {ManagedAccount | null}
|
|
229
|
+
*/
|
|
230
|
+
peekNextAccount() {
|
|
231
|
+
const enabled = this.#accounts.filter((acc) => acc.enabled && acc.index !== this.#currentIndex);
|
|
232
|
+
if (enabled.length === 0) return null;
|
|
233
|
+
// Return the first enabled account that isn't the current one
|
|
234
|
+
return enabled[0] ?? null;
|
|
235
|
+
}
|
|
236
|
+
|
|
225
237
|
/**
|
|
226
238
|
* Get enabled account references for internal plugin operations.
|
|
227
239
|
* Returned objects are mutable managed accounts.
|
package/lib/oauth.mjs
CHANGED
|
@@ -13,7 +13,7 @@ const OAUTH_TOKEN_URL = `https://${OAUTH_CONSOLE_HOST}/v1/oauth/token`;
|
|
|
13
13
|
const OAUTH_REVOKE_URL = `https://${OAUTH_CONSOLE_HOST}/v1/oauth/revoke`;
|
|
14
14
|
// Real Claude Code uses axios 1.13.6 for OAuth calls — axios sets User-Agent
|
|
15
15
|
// automatically and adds Accept header. We must match this fingerprint exactly.
|
|
16
|
-
// CLI version profile: 2.1.
|
|
16
|
+
// CLI version profile: 2.1.83 (for reference; not sent on OAuth calls)
|
|
17
17
|
const OAUTH_AXIOS_VERSION = "1.13.6";
|
|
18
18
|
const OAUTH_USER_AGENT = `axios/${OAUTH_AXIOS_VERSION}`;
|
|
19
19
|
const OAUTH_ACCEPT = "application/json, text/plain, */*";
|
package/lib/storage.mjs
CHANGED
|
@@ -206,13 +206,13 @@ export async function loadAccounts() {
|
|
|
206
206
|
// rather than silently discarding all accounts
|
|
207
207
|
if (typeof data.version === "number" && data.version > CURRENT_VERSION) {
|
|
208
208
|
// Forward-compat: newer versions likely only add fields
|
|
209
|
-
|
|
209
|
+
|
|
210
210
|
console.warn(
|
|
211
211
|
`[anthropic-auth] accounts file version ${data.version} is newer than expected ${CURRENT_VERSION}; loading with best effort`,
|
|
212
212
|
);
|
|
213
213
|
} else {
|
|
214
214
|
// Unknown/older version — still try to load
|
|
215
|
-
|
|
215
|
+
|
|
216
216
|
console.warn(
|
|
217
217
|
`[anthropic-auth] accounts file version mismatch (${data.version} vs ${CURRENT_VERSION}); attempting load`,
|
|
218
218
|
);
|