opencode-anthropic-fix 0.0.25 → 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/LICENSE +648 -21
- package/README.md +12 -4
- package/index.mjs +924 -124
- package/lib/accounts.mjs +30 -0
- package/lib/backoff.mjs +25 -0
- package/lib/config.mjs +51 -0
- package/lib/oauth.mjs +30 -6
- package/lib/oauth.test.mjs +3 -3
- package/lib/rotation.mjs +19 -0
- package/package.json +2 -1
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.
|
|
14
|
-
const OAUTH_USER_AGENT = `claude-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|
package/lib/oauth.test.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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
|