opencode-anthropic-fix 0.0.18 → 0.0.19

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.
Files changed (2) hide show
  1. package/index.mjs +48 -14
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -103,7 +103,7 @@ async function promptManageAccounts(accountManager) {
103
103
  // Request building helpers (extracted from original fetch interceptor)
104
104
  // ---------------------------------------------------------------------------
105
105
 
106
- const FALLBACK_CLAUDE_CLI_VERSION = "2.1.68";
106
+ const FALLBACK_CLAUDE_CLI_VERSION = "2.1.76";
107
107
  const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
108
108
  const CLAUDE_CODE_BETA_FLAG = "claude-code-20250219";
109
109
  const BILLING_HASH_SALT = "59cf53e54c78";
@@ -292,7 +292,10 @@ function supportsThinking(model) {
292
292
  */
293
293
  function isOpus46Model(model) {
294
294
  if (!model) return false;
295
- return /claude-opus-4[._-]6/i.test(model);
295
+ // Match standard IDs (claude-opus-4-6, claude-opus-4.6) and Bedrock ARNs
296
+ // (arn:aws:bedrock:...anthropic.claude-opus-4-6-...).
297
+ // Also match bare "opus-4-6" / "opus-4.6" fragments for non-standard strings.
298
+ return /claude-opus-4[._-]6|opus[._-]4[._-]6/i.test(model);
296
299
  }
297
300
 
298
301
  /**
@@ -300,7 +303,8 @@ function isOpus46Model(model) {
300
303
  * @returns {boolean}
301
304
  */
302
305
  function hasOneMillionContext(model) {
303
- return /(^|[-_ ])1m($|[-_ ])|context[-_]?1m/i.test(model);
306
+ // Models with explicit 1m suffix, or Opus 4.6 (1M by default since v2.1.75).
307
+ return /(^|[-_ ])1m($|[-_ ])|context[-_]?1m/i.test(model) || isOpus46Model(model);
304
308
  }
305
309
 
306
310
  /**
@@ -732,7 +736,7 @@ function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provide
732
736
  return mergedBetas.join(",");
733
737
  }
734
738
 
735
- /** @typedef {'low' | 'medium' | 'high' | 'max'} ThinkingEffort */
739
+ /** @typedef {'low' | 'medium' | 'high'} ThinkingEffort */
736
740
 
737
741
  /**
738
742
  * Map budgetTokens to an adaptive thinking effort level.
@@ -743,8 +747,7 @@ function buildAnthropicBetaHeader(incomingBeta, signatureEnabled, model, provide
743
747
  function budgetTokensToEffort(budgetTokens) {
744
748
  if (budgetTokens <= 1024) return "low";
745
749
  if (budgetTokens <= 8000) return "medium";
746
- if (budgetTokens <= 16000) return "high";
747
- return "max";
750
+ return "high";
748
751
  }
749
752
 
750
753
  /**
@@ -753,7 +756,7 @@ function budgetTokensToEffort(budgetTokens) {
753
756
  * @returns {value is ThinkingEffort}
754
757
  */
755
758
  function isValidEffort(value) {
756
- return value === "low" || value === "medium" || value === "high" || value === "max";
759
+ return value === "low" || value === "medium" || value === "high";
757
760
  }
758
761
 
759
762
  /**
@@ -1427,10 +1430,14 @@ function applyDiskAuthIfFresher(account, diskAuth, options = {}) {
1427
1430
  * @param {import('./lib/accounts.mjs').ManagedAccount} account
1428
1431
  * @param {ReturnType<typeof import('@opencode-ai/sdk').createOpencodeClient>} client
1429
1432
  * @param {"foreground" | "idle"} [source]
1433
+ * @param {{ onTokensUpdated?: () => Promise<void> }} [options] - If provided,
1434
+ * called under the cross-process lock after token update to persist rotated
1435
+ * tokens before the lock is released. Omitting means tokens won't be saved
1436
+ * to disk until the caller arranges it (risking the rotation race).
1430
1437
  * @returns {Promise<string>} The new access token
1431
1438
  * @throws {Error} If refresh fails
1432
1439
  */
1433
- async function refreshAccountToken(account, client, source = "foreground") {
1440
+ async function refreshAccountToken(account, client, source = "foreground", { onTokensUpdated } = {}) {
1434
1441
  const lockResult = await acquireRefreshLock(account.id, {
1435
1442
  timeoutMs: 2_000,
1436
1443
  backoffMs: 60,
@@ -1471,6 +1478,22 @@ async function refreshAccountToken(account, client, source = "foreground") {
1471
1478
  }
1472
1479
  markTokenStateUpdated(account);
1473
1480
 
1481
+ // Persist new tokens to disk BEFORE releasing the cross-process lock.
1482
+ // This is critical: if we release the lock first, another process can
1483
+ // acquire it and read the old (now-rotated) refresh token from disk,
1484
+ // leading to an invalid_grant failure. The debounced requestSaveToDisk()
1485
+ // that callers used previously left a ~1 s window where this race could
1486
+ // (and did) happen.
1487
+ if (onTokensUpdated) {
1488
+ try {
1489
+ await onTokensUpdated();
1490
+ } catch {
1491
+ // Best-effort: in-memory tokens remain valid for this process.
1492
+ // The callback is responsible for scheduling its own fallback
1493
+ // (e.g. a debounced retry) if the synchronous save fails.
1494
+ }
1495
+ }
1496
+
1474
1497
  // Also persist to OpenCode's auth.json for compatibility.
1475
1498
  // This should be best-effort: a persistence hiccup should not invalidate an
1476
1499
  // otherwise successful refresh token exchange.
@@ -2011,7 +2034,21 @@ export async function AnthropicAuthPlugin({ client }) {
2011
2034
  const entry = { source, promise: Promise.resolve("") };
2012
2035
  const p = (async () => {
2013
2036
  try {
2014
- return await refreshAccountToken(account, client, source);
2037
+ return await refreshAccountToken(account, client, source, {
2038
+ onTokensUpdated: async () => {
2039
+ try {
2040
+ await accountManager.saveToDisk();
2041
+ } catch {
2042
+ // Synchronous save failed (disk full, permissions, etc.).
2043
+ // Schedule a debounced retry so the rotated token eventually
2044
+ // reaches disk. Another process may hit invalid_grant in the
2045
+ // interim, but its retry-from-disk logic can recover once this
2046
+ // save lands.
2047
+ accountManager.requestSaveToDisk();
2048
+ throw new Error("save failed, debounced retry scheduled");
2049
+ }
2050
+ },
2051
+ });
2015
2052
  } finally {
2016
2053
  if (refreshInFlight.get(key) === entry) {
2017
2054
  refreshInFlight.delete(key);
@@ -2040,7 +2077,6 @@ export async function AnthropicAuthPlugin({ client }) {
2040
2077
  try {
2041
2078
  try {
2042
2079
  await refreshAccountTokenSingleFlight(account, "idle");
2043
- accountManager.requestSaveToDisk();
2044
2080
  return;
2045
2081
  } catch (err) {
2046
2082
  let details = parseRefreshFailure(err);
@@ -2068,7 +2104,6 @@ export async function AnthropicAuthPlugin({ client }) {
2068
2104
 
2069
2105
  try {
2070
2106
  await refreshAccountTokenSingleFlight(account, "idle");
2071
- accountManager.requestSaveToDisk();
2072
2107
  return;
2073
2108
  } catch (retryErr) {
2074
2109
  details = parseRefreshFailure(retryErr);
@@ -2252,8 +2287,8 @@ export async function AnthropicAuthPlugin({ client }) {
2252
2287
  const attemptedRefreshToken = account.refreshToken;
2253
2288
  try {
2254
2289
  accessToken = await refreshAccountTokenSingleFlight(account);
2255
- // Persist updated tokens (especially if refresh token rotated)
2256
- accountManager.requestSaveToDisk();
2290
+ // Tokens are now saved under the refresh lock (inside
2291
+ // refreshAccountToken) so no debounced save needed here.
2257
2292
  } catch (err) {
2258
2293
  // Token refresh failed — check if another instance rotated the
2259
2294
  // refresh token and persisted it between attempts.
@@ -2287,7 +2322,6 @@ export async function AnthropicAuthPlugin({ client }) {
2287
2322
 
2288
2323
  try {
2289
2324
  accessToken = await refreshAccountTokenSingleFlight(account);
2290
- accountManager.requestSaveToDisk();
2291
2325
  } catch (retryErr) {
2292
2326
  finalError = retryErr;
2293
2327
  details = parseRefreshFailure(retryErr);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "main": "./index.mjs",
5
5
  "files": [
6
6
  "index.mjs",