opencode-anthropic-fix 0.0.27 → 0.0.28
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 +176 -2
- package/lib/accounts.mjs +12 -0
- 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
|
|
|
@@ -3887,6 +4042,25 @@ function transformRequestBody(body, signature, runtime, betaHeader) {
|
|
|
3887
4042
|
};
|
|
3888
4043
|
}
|
|
3889
4044
|
|
|
4045
|
+
// Cache breakpoint optimization: add cache_control to the last content block
|
|
4046
|
+
// of each user/assistant message for maximum prefix caching.
|
|
4047
|
+
// Skip for round-robin strategy (cache defeated by account rotation).
|
|
4048
|
+
if (signature.enabled && signature.cachePolicy?.ttl !== "off" && signature.cachePolicy?.ttl_supported !== false) {
|
|
4049
|
+
const strategy = signature.strategy || "sticky";
|
|
4050
|
+
if (strategy !== "round-robin" && Array.isArray(parsed.messages)) {
|
|
4051
|
+
const cacheControl = { type: "ephemeral", ttl: signature.cachePolicy?.ttl || "1h" };
|
|
4052
|
+
for (const msg of parsed.messages) {
|
|
4053
|
+
if (!msg.content || !Array.isArray(msg.content) || msg.content.length === 0) continue;
|
|
4054
|
+
// Only cache user and assistant messages (not tool results which change frequently)
|
|
4055
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
4056
|
+
const lastBlock = msg.content[msg.content.length - 1];
|
|
4057
|
+
if (lastBlock && typeof lastBlock === "object") {
|
|
4058
|
+
lastBlock.cache_control = cacheControl;
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
|
|
3890
4064
|
// Add prefix to tools definitions
|
|
3891
4065
|
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
3892
4066
|
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/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
|
);
|