opencode-anthropic-fix 0.0.24 → 0.0.26

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/lib/accounts.mjs CHANGED
@@ -334,6 +334,36 @@ export class AccountManager {
334
334
  this.#tokenTracker.refund(account.index);
335
335
  }
336
336
 
337
+ /**
338
+ * Apply a utilization-based health penalty (proactive, before actual rate limit).
339
+ * @param {ManagedAccount} account
340
+ * @param {number} penalty - Points to deduct (0-10 typical)
341
+ */
342
+ applyUtilizationPenalty(account, penalty) {
343
+ if (penalty > 0) {
344
+ this.#healthTracker.applyPenalty(account.index, penalty);
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Handle surpassed rate limit threshold — aggressive penalty and soft cooldown.
350
+ * @param {ManagedAccount} account
351
+ * @param {string | null} resetAt - ISO timestamp or null
352
+ */
353
+ applySurpassedThreshold(account, resetAt) {
354
+ // Apply a significant health penalty
355
+ this.#healthTracker.applyPenalty(account.index, 15);
356
+
357
+ // If reset time provided, set a soft cooldown
358
+ if (resetAt) {
359
+ const resetTime = new Date(resetAt).getTime();
360
+ if (!isNaN(resetTime) && resetTime > Date.now()) {
361
+ const SOFT_COOLDOWN_KEY = "soft_cooldown";
362
+ account.rateLimitResetTimes[SOFT_COOLDOWN_KEY] = resetTime;
363
+ }
364
+ }
365
+ }
366
+
337
367
  /**
338
368
  * Add a new account to the pool.
339
369
  * @param {string} refreshToken
package/lib/backoff.mjs CHANGED
@@ -7,6 +7,31 @@ const AUTH_FAILED_BACKOFF = 5_000;
7
7
  const RATE_LIMIT_EXCEEDED_BACKOFF = 30_000;
8
8
  const MIN_BACKOFF_MS = 2_000;
9
9
 
10
+ /**
11
+ * Parse the retry-after-ms header from a response.
12
+ * Returns milliseconds directly (no conversion needed).
13
+ * @param {Response} response
14
+ * @returns {number | null}
15
+ */
16
+ export function parseRetryAfterMsHeader(response) {
17
+ const header = response.headers.get("retry-after-ms");
18
+ if (!header) return null;
19
+ const ms = parseFloat(header);
20
+ return !isNaN(ms) && ms > 0 ? ms : null;
21
+ }
22
+
23
+ /**
24
+ * Parse the x-should-retry header from a response.
25
+ * @param {Response} response
26
+ * @returns {boolean | null} true = force retry, false = force no retry, null = not present
27
+ */
28
+ export function parseShouldRetryHeader(response) {
29
+ const header = response.headers.get("x-should-retry");
30
+ if (header === "true") return true;
31
+ if (header === "false") return false;
32
+ return null;
33
+ }
34
+
10
35
  /**
11
36
  * Parse the Retry-After header from a response.
12
37
  * Supports both seconds (integer) and HTTP-date formats.
package/lib/config.mjs CHANGED
@@ -44,6 +44,14 @@ import { homedir } from "node:os";
44
44
  * @property {number} min_interval_minutes - Minimum minutes between idle refresh attempts per account
45
45
  */
46
46
 
47
+ /**
48
+ * @typedef {object} CachePolicyConfig
49
+ * @property {string} ttl - "1h" (default), "5m", or "off"
50
+ * @property {boolean} ttl_supported - Auto-set to false if API rejects TTL
51
+ * @property {boolean} boundary_marker - Inject static/dynamic boundary marker into system prompt (opt-in)
52
+ * @property {number} hit_rate_warning_threshold - Warn when avg cache hit rate falls below this (0-1). Default: 0.3
53
+ */
54
+
47
55
  /**
48
56
  * @typedef {object} AnthropicAuthConfig
49
57
  * @property {AccountSelectionStrategy} account_selection_strategy
@@ -57,6 +65,9 @@ import { homedir } from "node:os";
57
65
  * @property {ToastConfig} toasts
58
66
  * @property {HeaderConfig} headers
59
67
  * @property {IdleRefreshConfig} idle_refresh
68
+ * @property {CachePolicyConfig} cache_policy
69
+ * @property {boolean} fast_mode
70
+ * @property {{emulate_minimal: boolean}} telemetry
60
71
  */
61
72
 
62
73
  /** @type {AnthropicAuthConfig} */
@@ -98,6 +109,16 @@ export const DEFAULT_CONFIG = {
98
109
  window_minutes: 60,
99
110
  min_interval_minutes: 30,
100
111
  },
112
+ cache_policy: {
113
+ ttl: "1h",
114
+ ttl_supported: true,
115
+ boundary_marker: false,
116
+ hit_rate_warning_threshold: 0.3,
117
+ },
118
+ fast_mode: false,
119
+ telemetry: {
120
+ emulate_minimal: false,
121
+ },
101
122
  };
102
123
 
103
124
  export const VALID_STRATEGIES = ["sticky", "round-robin", "hybrid"];
@@ -119,6 +140,9 @@ function createDefaultConfig() {
119
140
  token_bucket: { ...DEFAULT_CONFIG.token_bucket },
120
141
  toasts: { ...DEFAULT_CONFIG.toasts },
121
142
  idle_refresh: { ...DEFAULT_CONFIG.idle_refresh },
143
+ cache_policy: { ...DEFAULT_CONFIG.cache_policy },
144
+ fast_mode: DEFAULT_CONFIG.fast_mode,
145
+ telemetry: { ...DEFAULT_CONFIG.telemetry },
122
146
  };
123
147
  }
124
148
 
@@ -293,6 +317,33 @@ function validateConfig(raw) {
293
317
  };
294
318
  }
295
319
 
320
+ // Cache policy sub-config
321
+ if (raw.cache_policy && typeof raw.cache_policy === "object") {
322
+ const cp = /** @type {Record<string, unknown>} */ (raw.cache_policy);
323
+ const validTtls = ["1h", "5m", "off"];
324
+ config.cache_policy = {
325
+ ttl: typeof cp.ttl === "string" && validTtls.includes(cp.ttl) ? cp.ttl : DEFAULT_CONFIG.cache_policy.ttl,
326
+ ttl_supported:
327
+ typeof cp.ttl_supported === "boolean" ? cp.ttl_supported : DEFAULT_CONFIG.cache_policy.ttl_supported,
328
+ boundary_marker:
329
+ typeof cp.boundary_marker === "boolean" ? cp.boundary_marker : DEFAULT_CONFIG.cache_policy.boundary_marker,
330
+ hit_rate_warning_threshold:
331
+ typeof cp.hit_rate_warning_threshold === "number" && Number.isFinite(cp.hit_rate_warning_threshold)
332
+ ? Math.max(0, Math.min(1, cp.hit_rate_warning_threshold))
333
+ : DEFAULT_CONFIG.cache_policy.hit_rate_warning_threshold,
334
+ };
335
+ }
336
+
337
+ // Fast mode
338
+ config.fast_mode = typeof raw.fast_mode === "boolean" ? raw.fast_mode : false;
339
+
340
+ // Telemetry
341
+ config.telemetry = config.telemetry || {};
342
+ config.telemetry.emulate_minimal =
343
+ typeof raw.telemetry?.emulate_minimal === "boolean"
344
+ ? raw.telemetry.emulate_minimal
345
+ : DEFAULT_CONFIG.telemetry.emulate_minimal;
346
+
296
347
  return config;
297
348
  }
298
349
 
package/lib/oauth.mjs CHANGED
@@ -10,8 +10,17 @@ const OAUTH_MAX_HOST = "claude.ai";
10
10
  const OAUTH_REDIRECT_URI = `https://${OAUTH_CONSOLE_HOST}/oauth/code/callback`;
11
11
  const OAUTH_TOKEN_URL = `https://${OAUTH_CONSOLE_HOST}/v1/oauth/token`;
12
12
  const OAUTH_REVOKE_URL = `https://${OAUTH_CONSOLE_HOST}/v1/oauth/revoke`;
13
- const OAUTH_CLI_VERSION_PROFILE = "2.1.79";
14
- const OAUTH_USER_AGENT = `claude-cli/${OAUTH_CLI_VERSION_PROFILE} (external, cli)`;
13
+ const OAUTH_CLI_VERSION_PROFILE = "2.1.80";
14
+ const OAUTH_USER_AGENT = `claude-code/${OAUTH_CLI_VERSION_PROFILE}`;
15
+
16
+ export const CLAUDE_AI_SCOPES = [
17
+ "user:profile",
18
+ "user:inference",
19
+ "user:sessions:claude_code",
20
+ "user:mcp_servers",
21
+ "user:file_upload",
22
+ ];
23
+ const CONSOLE_SCOPES = ["org:create_api_key", "user:profile"];
15
24
 
16
25
  function base64url(input) {
17
26
  return Buffer.from(input).toString("base64url");
@@ -20,13 +29,14 @@ function base64url(input) {
20
29
  function generatePKCE() {
21
30
  const verifier = base64url(randomBytes(32));
22
31
  const challenge = base64url(createHash("sha256").update(verifier).digest());
23
- return { verifier, challenge };
32
+ const state = base64url(randomBytes(32));
33
+ return { verifier, challenge, state };
24
34
  }
25
35
 
26
36
  /**
27
37
  * Build an OAuth authorization URL with PKCE challenge.
28
38
  * @param {"max" | "console"} mode
29
- * @returns {Promise<{url: string, verifier: string}>}
39
+ * @returns {Promise<{url: string, verifier: string, state: string}>}
30
40
  */
31
41
  export async function authorize(mode) {
32
42
  const pkce = generatePKCE();
@@ -36,13 +46,15 @@ export async function authorize(mode) {
36
46
  url.searchParams.set("client_id", CLIENT_ID);
37
47
  url.searchParams.set("response_type", "code");
38
48
  url.searchParams.set("redirect_uri", OAUTH_REDIRECT_URI);
39
- url.searchParams.set("scope", "org:create_api_key user:profile user:inference");
49
+ const scopes = mode === "console" ? CONSOLE_SCOPES : CLAUDE_AI_SCOPES;
50
+ url.searchParams.set("scope", scopes.join(" "));
40
51
  url.searchParams.set("code_challenge", pkce.challenge);
41
52
  url.searchParams.set("code_challenge_method", "S256");
42
- url.searchParams.set("state", pkce.verifier);
53
+ url.searchParams.set("state", pkce.state);
43
54
  return {
44
55
  url: url.toString(),
45
56
  verifier: pkce.verifier,
57
+ state: pkce.state,
46
58
  };
47
59
  }
48
60
 
@@ -182,6 +194,7 @@ export async function refreshToken(refreshTokenValue, options = {}) {
182
194
  grant_type: "refresh_token",
183
195
  client_id: CLIENT_ID,
184
196
  refresh_token: refreshTokenValue,
197
+ scope: CLAUDE_AI_SCOPES.join(" "),
185
198
  }),
186
199
  ...(options.signal ? { signal: options.signal } : {}),
187
200
  });
@@ -201,3 +214,14 @@ export async function refreshToken(refreshTokenValue, options = {}) {
201
214
 
202
215
  return resp.json();
203
216
  }
217
+
218
+ /**
219
+ * Check if an account has all required Claude.ai scopes.
220
+ * @param {string[] | undefined} scopes
221
+ * @returns {boolean}
222
+ */
223
+ export function hasRequiredScopes(scopes) {
224
+ if (!Array.isArray(scopes) || scopes.length === 0) return false;
225
+ const stored = new Set(scopes);
226
+ return CLAUDE_AI_SCOPES.every((s) => stored.has(s));
227
+ }
@@ -23,7 +23,7 @@ describe("oauth headers", () => {
23
23
  await exchange("code#state", "verifier");
24
24
 
25
25
  const [, init] = mockFetch.mock.calls[0];
26
- expect(init.headers["User-Agent"]).toBe("claude-cli/2.1.79 (external, cli)");
26
+ expect(init.headers["User-Agent"]).toBe("claude-code/2.1.80");
27
27
  });
28
28
 
29
29
  it("sends Claude Code user-agent on token refresh", async () => {
@@ -35,7 +35,7 @@ describe("oauth headers", () => {
35
35
  await refreshToken("refresh-token");
36
36
 
37
37
  const [, init] = mockFetch.mock.calls[0];
38
- expect(init.headers["User-Agent"]).toBe("claude-cli/2.1.79 (external, cli)");
38
+ expect(init.headers["User-Agent"]).toBe("claude-code/2.1.80");
39
39
  });
40
40
 
41
41
  it("sends Claude Code user-agent on token revoke", async () => {
@@ -44,6 +44,6 @@ describe("oauth headers", () => {
44
44
  await revoke("refresh-token");
45
45
 
46
46
  const [, init] = mockFetch.mock.calls[0];
47
- expect(init.headers["User-Agent"]).toBe("claude-cli/2.1.79 (external, cli)");
47
+ expect(init.headers["User-Agent"]).toBe("claude-code/2.1.80");
48
48
  });
49
49
  });
package/lib/rotation.mjs CHANGED
@@ -72,6 +72,25 @@ export class HealthScoreTracker {
72
72
  });
73
73
  }
74
74
 
75
+ /**
76
+ * Apply a direct penalty to an account's health score.
77
+ * @param {number} accountIndex
78
+ * @param {number} penalty - Points to deduct
79
+ */
80
+ applyPenalty(accountIndex, penalty) {
81
+ const state = this.#scores.get(accountIndex);
82
+ if (!state) {
83
+ this.#scores.set(accountIndex, {
84
+ score: Math.max(0, this.#config.initial - penalty),
85
+ lastUpdated: Date.now(),
86
+ consecutiveFailures: 0,
87
+ });
88
+ } else {
89
+ state.score = Math.max(0, this.getScore(accountIndex) - penalty);
90
+ state.lastUpdated = Date.now();
91
+ }
92
+ }
93
+
75
94
  /**
76
95
  * Record a general failure.
77
96
  * @param {number} accountIndex
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
+ "license": "GPL-3.0-or-later",
4
5
  "main": "./index.mjs",
5
6
  "files": [
6
7
  "index.mjs",