@cortexkit/opencode-anthropic-auth 1.1.0 → 1.1.2
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/README.md +25 -3
- package/dist/index.js +289 -32
- package/dist/prompt-context.js +4 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ This repo is a Bun workspace monorepo with two user-facing integrations and one
|
|
|
19
19
|
| Primary Claude Pro/Max OAuth | OpenCode `/connect anthropic` | Pi `/login anthropic` |
|
|
20
20
|
| Provider integration point | OpenCode plugin fetch/request transform | Pi `registerProvider("anthropic")` provider override |
|
|
21
21
|
| Sidecar config | `~/.config/opencode/anthropic-auth.json` | `~/.pi/agent/anthropic-auth.json` |
|
|
22
|
-
| Commands | `/claude-cache`, `/claude-fast`, `/claude-quota`, `/claude-dump` | `/claude-cache`, `/claude-fast`, `/claude-quota`, `/claude-dump` |
|
|
22
|
+
| Commands | `/claude-cache`, `/claude-cachekeep`, `/claude-fast`, `/claude-quota`, `/claude-dump` | `/claude-cache`, `/claude-cachekeep`, `/claude-fast`, `/claude-quota`, `/claude-dump` |
|
|
23
23
|
| Fallback accounts, quota routing, relay, dumps, fast mode | Supported | Supported through the same shared core and Pi sidecar |
|
|
24
24
|
|
|
25
25
|
## What CortexKit adds over the original plugin
|
|
@@ -27,6 +27,7 @@ This repo is a Bun workspace monorepo with two user-facing integrations and one
|
|
|
27
27
|
- **Fallback Claude accounts**: keep each agent's normal Anthropic login as the primary account, then route to ordered fallback OAuth accounts on auth/quota/rate-limit failures.
|
|
28
28
|
- **Quota-aware routing**: skip main or fallback accounts when their 5-hour or 7-day Claude quota falls below your configured minimum.
|
|
29
29
|
- **Persistent Claude cache controls**: manage Anthropic 1-hour prompt caching from `/claude-cache` with explicit, automatic, or hybrid modes.
|
|
30
|
+
- **Cache keepalive**: use `/claude-cachekeep HH-HH` to pre-warm hybrid cache anchors for active sessions before the 1-hour TTL expires.
|
|
30
31
|
- **Fast mode toggle**: use `/claude-fast on|off` to request Anthropic fast mode for supported Opus models.
|
|
31
32
|
- **Live quota visibility**: use `/claude-quota` to see main and fallback quota state, reset times, and refresh errors.
|
|
32
33
|
- **User-owned Cloudflare relay**: optionally provision your own Worker relay to reduce repeated client upload bytes for large OpenCode or Pi requests.
|
|
@@ -41,7 +42,7 @@ This repo is a Bun workspace monorepo with two user-facing integrations and one
|
|
|
41
42
|
- Support fallback Claude accounts stored in a local per-agent sidecar file.
|
|
42
43
|
- Keep fallback OAuth tokens fresh in the background.
|
|
43
44
|
- Apply quota thresholds before routing to main or fallback accounts.
|
|
44
|
-
- Add `/claude-cache`, `/claude-fast`, `/claude-quota`, and `/claude-dump` commands.
|
|
45
|
+
- Add `/claude-cache`, `/claude-cachekeep`, `/claude-fast`, `/claude-quota`, and `/claude-dump` commands.
|
|
45
46
|
- Optionally relay large requests through a Cloudflare Worker owned by the user.
|
|
46
47
|
|
|
47
48
|
## Install
|
|
@@ -172,6 +173,11 @@ Example:
|
|
|
172
173
|
"enabled": false,
|
|
173
174
|
"mode": "explicit"
|
|
174
175
|
},
|
|
176
|
+
"cacheKeep": {
|
|
177
|
+
"enabled": false,
|
|
178
|
+
"startHour": 9,
|
|
179
|
+
"endHour": 23
|
|
180
|
+
},
|
|
175
181
|
"dump": {
|
|
176
182
|
"enabled": false
|
|
177
183
|
},
|
|
@@ -189,7 +195,7 @@ Example:
|
|
|
189
195
|
}
|
|
190
196
|
```
|
|
191
197
|
|
|
192
|
-
The `claudeCache` block controls the `/claude-cache` command's persisted mode, and `claudeFast` controls `/claude-fast`. The `main` field identifies OpenCode's primary auth entry; Pi keeps primary OAuth credentials in Pi's own credential store, but uses the same sidecar shape for CortexKit settings and fallback accounts.
|
|
198
|
+
The `claudeCache` block controls the `/claude-cache` command's persisted mode, `cacheKeep` controls `/claude-cachekeep`, and `claudeFast` controls `/claude-fast`. The `main` field identifies OpenCode's primary auth entry; Pi keeps primary OAuth credentials in Pi's own credential store, but uses the same sidecar shape for CortexKit settings and fallback accounts.
|
|
193
199
|
|
|
194
200
|
## Fallback accounts
|
|
195
201
|
|
|
@@ -275,6 +281,22 @@ Modes:
|
|
|
275
281
|
|
|
276
282
|
In OpenCode, subagent requests do not receive 1-hour TTL caching. The plugin detects child sessions through OpenCode's `x-parent-session-id` header, strips that internal header before forwarding to Anthropic, and leaves default ephemeral caching in place for those requests.
|
|
277
283
|
|
|
284
|
+
### Cache keepalive
|
|
285
|
+
|
|
286
|
+
`/claude-cachekeep` keeps recently used hybrid-mode session caches warm while the agent process is running:
|
|
287
|
+
|
|
288
|
+
```text
|
|
289
|
+
/claude-cachekeep
|
|
290
|
+
/claude-cachekeep 09-23
|
|
291
|
+
/claude-cachekeep off
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The hour range uses local 24-hour time and is start-inclusive/end-exclusive. `09-23` means cache keepalive may run from 09:00 until 22:59. Overnight windows such as `23-09` are accepted.
|
|
295
|
+
|
|
296
|
+
Cache keepalive only tracks requests when `/claude-cache` is enabled in `hybrid` mode. For each active session seen that day, the package keeps an in-memory clone of the latest rewritten Anthropic request and sends a non-streaming `max_tokens: 0` pre-warm request about five minutes before the 1-hour cache entry would expire. Nothing is written to disk except the schedule configuration.
|
|
297
|
+
|
|
298
|
+
Pre-warm requests preserve explicit cache anchors but remove response-only fields that Anthropic rejects with `max_tokens: 0`, such as streaming, enabled thinking, structured output format, and forced/any tool choice. The feature works only while OpenCode or Pi is running and the machine is awake, and cache writes are still billed when the cache entry is no longer warm.
|
|
299
|
+
|
|
278
300
|
## Claude fast mode
|
|
279
301
|
|
|
280
302
|
Both OpenCode and Pi packages can persistently request Anthropic fast mode for supported Opus models:
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { authorize, buildClaudeQuotaSummary, buildFallbackQuotaSummaries, buildRefreshOperationError, CACHE_1H_COMMAND_NAME, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CLAUDE_CACHE_KEEP_COMMAND_NAME, CLAUDE_DUMP_COMMAND_NAME, CLAUDE_FAST_COMMAND_NAME, CLAUDE_QUOTAS_COMMAND_NAME, ClaudeOAuthRefreshError, exchange, executeCache1hCommand, executeCacheKeepCommand, executeDumpCommand, executeFastModeCommand, FallbackAccountManager, fetchOAuthQuotaSnapshot, formatRefreshBackoffMessage, getCache1hMode, getCache1hPersistentMode, getCacheKeepWindow, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getRelayConfig, hashRefreshToken, isCache1hEnabled, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isCacheKeepPersistentlyEnabled, isDumpPersistentlyEnabled, isFastModeEnabled, isFastModePersistentlyEnabled, isFastModeSupportedModel, loadAccounts, log, mergeAnthropicBetas, parseCache1hCommandAction, parseCacheKeepCommandAction, parseDumpCommandAction, parseFastModeCommandAction, quotaSnapshotPassesPolicy, refreshBackoffActive, refreshClaudeOAuthToken, resolveClaudeCodeIdentity, saveAccounts, sendViaRelay, setCache1hPersistentEnabled, setCache1hPersistentMode, setCache1hState, setCacheKeepPersistentEnabled, setCacheKeepPersistentWindow, setDumpEnabled, setDumpPersistentEnabled, setFastModeEnabled, setFastModePersistentEnabled, shouldFallbackStatus, } from '@cortexkit/anthropic-auth-core';
|
|
2
3
|
import { resolvePromptContext } from "./prompt-context.js";
|
|
3
4
|
import { addFastModeBetaHeader, createStrippedStream, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js";
|
|
4
5
|
const HANDLED_SENTINEL = '__OPENCODE_ANTHROPIC_AUTH_COMMAND_HANDLED__';
|
|
@@ -6,6 +7,9 @@ const MAIN_AUTH_REFRESH_TICK_MS = 60_000;
|
|
|
6
7
|
const DEFAULT_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES = 30;
|
|
7
8
|
let nextPerfRequestId = 1;
|
|
8
9
|
let eventLoopLagMonitorStarted = false;
|
|
10
|
+
function perfLoggingEnabled() {
|
|
11
|
+
return process.env.OPENCODE_ANTHROPIC_AUTH_PERF === '1';
|
|
12
|
+
}
|
|
9
13
|
function nowMs() {
|
|
10
14
|
return performance.now();
|
|
11
15
|
}
|
|
@@ -13,8 +17,11 @@ function roundMs(value) {
|
|
|
13
17
|
return Math.round(value * 10) / 10;
|
|
14
18
|
}
|
|
15
19
|
function startEventLoopLagMonitor() {
|
|
16
|
-
if (eventLoopLagMonitorStarted ||
|
|
20
|
+
if (eventLoopLagMonitorStarted ||
|
|
21
|
+
process.env.NODE_ENV === 'test' ||
|
|
22
|
+
!perfLoggingEnabled()) {
|
|
17
23
|
return;
|
|
24
|
+
}
|
|
18
25
|
eventLoopLagMonitorStarted = true;
|
|
19
26
|
const intervalMs = 100;
|
|
20
27
|
const thresholdMs = 250;
|
|
@@ -39,28 +46,37 @@ function createPerfTrace(data) {
|
|
|
39
46
|
last: start,
|
|
40
47
|
mark(stage, stageData) {
|
|
41
48
|
const current = nowMs();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
if (perfLoggingEnabled()) {
|
|
50
|
+
log('[perf] opencode request stage', {
|
|
51
|
+
requestId: trace.requestId,
|
|
52
|
+
stage,
|
|
53
|
+
deltaMs: roundMs(current - trace.last),
|
|
54
|
+
totalMs: roundMs(current - trace.start),
|
|
55
|
+
...stageData,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
49
58
|
trace.last = current;
|
|
50
59
|
},
|
|
51
60
|
done(stage, stageData) {
|
|
52
61
|
const current = nowMs();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
if (perfLoggingEnabled()) {
|
|
63
|
+
log('[perf] opencode request done', {
|
|
64
|
+
requestId: trace.requestId,
|
|
65
|
+
stage,
|
|
66
|
+
deltaMs: roundMs(current - trace.last),
|
|
67
|
+
totalMs: roundMs(current - trace.start),
|
|
68
|
+
...stageData,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
60
71
|
trace.last = current;
|
|
61
72
|
},
|
|
62
73
|
};
|
|
63
|
-
|
|
74
|
+
if (perfLoggingEnabled()) {
|
|
75
|
+
log('[perf] opencode request start', {
|
|
76
|
+
requestId: trace.requestId,
|
|
77
|
+
...data,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
64
80
|
return trace;
|
|
65
81
|
}
|
|
66
82
|
async function sendIgnoredMessage(ctx, sessionId, text) {
|
|
@@ -97,6 +113,43 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
97
113
|
const { client } = ctx;
|
|
98
114
|
const fallbackManager = new FallbackAccountManager();
|
|
99
115
|
fallbackManager.startBackgroundRefresh();
|
|
116
|
+
let latestRefreshMainAccessToken = null;
|
|
117
|
+
const cacheKeepManager = new CacheKeepManager({
|
|
118
|
+
loadStorage: () => loadAccounts(),
|
|
119
|
+
prepareHeaders: async (headers, target) => {
|
|
120
|
+
if (!latestGetAuth)
|
|
121
|
+
return headers;
|
|
122
|
+
const auth = await latestGetAuth();
|
|
123
|
+
if (auth.type !== 'oauth')
|
|
124
|
+
return headers;
|
|
125
|
+
if (!auth.access || (auth.expires && auth.expires < Date.now())) {
|
|
126
|
+
if (!latestRefreshMainAccessToken)
|
|
127
|
+
return headers;
|
|
128
|
+
auth.access = await latestRefreshMainAccessToken();
|
|
129
|
+
}
|
|
130
|
+
if (!auth.access)
|
|
131
|
+
return headers;
|
|
132
|
+
try {
|
|
133
|
+
const parsedBody = JSON.parse(target.bodyText);
|
|
134
|
+
const identity = await resolveClaudeCodeIdentity(auth.access, typeof parsedBody.model === 'string' ? parsedBody.model : undefined);
|
|
135
|
+
headers.delete('anthropic-beta');
|
|
136
|
+
setOAuthHeaders(headers, auth.access, {
|
|
137
|
+
body: parsedBody,
|
|
138
|
+
identity,
|
|
139
|
+
});
|
|
140
|
+
headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [
|
|
141
|
+
CACHE_KEEP_EXTENDED_TTL_BETA,
|
|
142
|
+
]));
|
|
143
|
+
if (parsedBody.speed === 'fast')
|
|
144
|
+
addFastModeBetaHeader(headers);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
setOAuthHeaders(headers, auth.access);
|
|
148
|
+
}
|
|
149
|
+
return headers;
|
|
150
|
+
},
|
|
151
|
+
log,
|
|
152
|
+
});
|
|
100
153
|
const initialCache1hStorage = await loadAccounts();
|
|
101
154
|
const relayConfig = getRelayConfig(initialCache1hStorage);
|
|
102
155
|
setCache1hState({
|
|
@@ -115,6 +168,24 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
115
168
|
function mainRefreshEnabled(storage) {
|
|
116
169
|
return storage?.refresh?.enabled !== false;
|
|
117
170
|
}
|
|
171
|
+
async function clearStaleMainRefreshError(refreshToken) {
|
|
172
|
+
if (!refreshToken)
|
|
173
|
+
return;
|
|
174
|
+
const storage = await loadAccounts();
|
|
175
|
+
const error = storage?.refresh?.mainLastRefreshError;
|
|
176
|
+
if (!storage?.refresh || !error?.tokenHash)
|
|
177
|
+
return;
|
|
178
|
+
const tokenHash = hashRefreshToken(refreshToken);
|
|
179
|
+
if (error.tokenHash === tokenHash)
|
|
180
|
+
return;
|
|
181
|
+
storage.refresh.mainLastRefreshError = undefined;
|
|
182
|
+
await saveAccounts(storage);
|
|
183
|
+
log('[refresh] opencode main oauth cleared stale backoff after token rotation', {
|
|
184
|
+
previousCheckedAt: error.checkedAt,
|
|
185
|
+
previousNextRetryAt: error.nextRetryAt,
|
|
186
|
+
previousRetryCount: error.retryCount,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
118
189
|
async function buildQuotaCommandSummary() {
|
|
119
190
|
const accounts = [];
|
|
120
191
|
if (latestGetAuth) {
|
|
@@ -179,6 +250,26 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
179
250
|
setCache1hState({ enabled, mode });
|
|
180
251
|
return executeCache1hCommand({ argumentsText, enabled, mode });
|
|
181
252
|
}
|
|
253
|
+
async function executePersistentCacheKeepCommand(argumentsText) {
|
|
254
|
+
const action = parseCacheKeepCommandAction(argumentsText);
|
|
255
|
+
let storage = await loadAccounts();
|
|
256
|
+
if (action.type === 'window') {
|
|
257
|
+
storage = await setCacheKeepPersistentWindow(action.startHour, action.endHour);
|
|
258
|
+
}
|
|
259
|
+
else if (action.type === 'disable') {
|
|
260
|
+
storage = await setCacheKeepPersistentEnabled(false);
|
|
261
|
+
}
|
|
262
|
+
const window = getCacheKeepWindow(storage);
|
|
263
|
+
const stats = cacheKeepManager.stats(window);
|
|
264
|
+
return executeCacheKeepCommand({
|
|
265
|
+
argumentsText,
|
|
266
|
+
enabled: isCacheKeepPersistentlyEnabled(storage),
|
|
267
|
+
window,
|
|
268
|
+
hybridActive: isCacheKeepHybridActive(storage),
|
|
269
|
+
trackedSessions: stats.trackedSessions,
|
|
270
|
+
nextPrewarmAt: stats.nextPrewarmAt,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
182
273
|
async function executePersistentDumpCommand(argumentsText) {
|
|
183
274
|
const action = parseDumpCommandAction(argumentsText);
|
|
184
275
|
if (action.type === 'enable' || action.type === 'disable') {
|
|
@@ -213,6 +304,10 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
213
304
|
template: CACHE_1H_COMMAND_NAME,
|
|
214
305
|
description: 'Show or toggle 1-hour Anthropic ephemeral prompt cache TTL.',
|
|
215
306
|
},
|
|
307
|
+
[CLAUDE_CACHE_KEEP_COMMAND_NAME]: {
|
|
308
|
+
template: CLAUDE_CACHE_KEEP_COMMAND_NAME,
|
|
309
|
+
description: 'Keep hybrid Claude cache warm for recently used sessions during a local time window.',
|
|
310
|
+
},
|
|
216
311
|
[CLAUDE_QUOTAS_COMMAND_NAME]: {
|
|
217
312
|
template: CLAUDE_QUOTAS_COMMAND_NAME,
|
|
218
313
|
description: 'Show current Claude OAuth quota usage for all accounts.',
|
|
@@ -232,6 +327,10 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
232
327
|
await sendIgnoredMessage(ctx, input.sessionID, await executePersistentCache1hCommand(input.arguments));
|
|
233
328
|
throwHandledSentinel();
|
|
234
329
|
}
|
|
330
|
+
if (input.command === CLAUDE_CACHE_KEEP_COMMAND_NAME) {
|
|
331
|
+
await sendIgnoredMessage(ctx, input.sessionID, await executePersistentCacheKeepCommand(input.arguments));
|
|
332
|
+
throwHandledSentinel();
|
|
333
|
+
}
|
|
235
334
|
if (input.command === CLAUDE_QUOTAS_COMMAND_NAME) {
|
|
236
335
|
await sendIgnoredMessage(ctx, input.sessionID, await buildQuotaCommandSummary());
|
|
237
336
|
throwHandledSentinel();
|
|
@@ -273,7 +372,20 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
273
372
|
refreshPromise = (async () => {
|
|
274
373
|
const maxRetries = 2;
|
|
275
374
|
const baseDelayMs = 500;
|
|
375
|
+
let leaseId = null;
|
|
376
|
+
let leaseTokenHash = null;
|
|
377
|
+
async function updateMainRefreshState(update) {
|
|
378
|
+
const storage = (await loadAccounts()) ?? {
|
|
379
|
+
version: 1,
|
|
380
|
+
main: { type: 'opencode', provider: 'anthropic' },
|
|
381
|
+
accounts: [],
|
|
382
|
+
};
|
|
383
|
+
storage.refresh = storage.refresh ?? {};
|
|
384
|
+
update(storage);
|
|
385
|
+
await saveAccounts(storage);
|
|
386
|
+
}
|
|
276
387
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
388
|
+
let freshAuth = null;
|
|
277
389
|
try {
|
|
278
390
|
if (attempt > 0) {
|
|
279
391
|
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
@@ -282,24 +394,69 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
282
394
|
// Re-read auth to get the latest refresh token.
|
|
283
395
|
// The outer `auth` snapshot may be stale if tokens
|
|
284
396
|
// were rotated since the fetch() call was made.
|
|
285
|
-
|
|
397
|
+
freshAuth = await getAuth();
|
|
286
398
|
if (!freshAuth.refresh) {
|
|
287
399
|
throw new Error('Token refresh failed: missing refresh token');
|
|
288
400
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
401
|
+
const storage = await loadAccounts();
|
|
402
|
+
const refreshTokenHash = hashRefreshToken(freshAuth.refresh);
|
|
403
|
+
const mainError = storage?.refresh?.mainLastRefreshError;
|
|
404
|
+
log('[refresh] opencode main oauth refresh check', {
|
|
405
|
+
attempt,
|
|
406
|
+
expiresInMs: freshAuth.expires
|
|
407
|
+
? freshAuth.expires - Date.now()
|
|
408
|
+
: undefined,
|
|
409
|
+
hasBackoff: Boolean(mainError),
|
|
410
|
+
backoffActive: mainError
|
|
411
|
+
? refreshBackoffActive(mainError, freshAuth.refresh, Date.now())
|
|
412
|
+
: false,
|
|
413
|
+
retryCount: mainError?.retryCount,
|
|
414
|
+
nextRetryAt: mainError?.nextRetryAt,
|
|
415
|
+
});
|
|
416
|
+
if (mainError &&
|
|
417
|
+
refreshBackoffActive(mainError, freshAuth.refresh, Date.now())) {
|
|
418
|
+
log('[refresh] opencode main oauth refresh skipped backoff', {
|
|
419
|
+
nextRetryAt: mainError.nextRetryAt,
|
|
420
|
+
retryCount: mainError.retryCount,
|
|
293
421
|
});
|
|
422
|
+
throw new Error(formatRefreshBackoffMessage(mainError, Date.now()));
|
|
294
423
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
424
|
+
if (storage?.refresh?.mainRefreshLeaseUntil &&
|
|
425
|
+
storage.refresh.mainRefreshLeaseUntil > Date.now() &&
|
|
426
|
+
storage.refresh.mainRefreshLeaseTokenHash ===
|
|
427
|
+
refreshTokenHash) {
|
|
428
|
+
log('[refresh] opencode main oauth refresh skipped lease', {
|
|
429
|
+
leaseUntil: storage.refresh.mainRefreshLeaseUntil,
|
|
430
|
+
});
|
|
431
|
+
throw new Error('Claude OAuth refresh is already in progress');
|
|
432
|
+
}
|
|
433
|
+
leaseId = randomUUID();
|
|
434
|
+
leaseTokenHash = refreshTokenHash;
|
|
435
|
+
await updateMainRefreshState((nextStorage) => {
|
|
436
|
+
nextStorage.refresh = nextStorage.refresh ?? {};
|
|
437
|
+
nextStorage.refresh.mainRefreshLeaseId =
|
|
438
|
+
leaseId ?? undefined;
|
|
439
|
+
nextStorage.refresh.mainRefreshLeaseUntil =
|
|
440
|
+
Date.now() + 2 * 60_000;
|
|
441
|
+
nextStorage.refresh.mainRefreshLeaseTokenHash =
|
|
442
|
+
refreshTokenHash;
|
|
443
|
+
});
|
|
444
|
+
const latestLease = await loadAccounts();
|
|
445
|
+
log('[refresh] opencode main oauth refresh lease acquired', {
|
|
446
|
+
attempt,
|
|
447
|
+
leaseUntil: Date.now() + 2 * 60_000,
|
|
448
|
+
});
|
|
449
|
+
if (latestLease?.refresh?.mainRefreshLeaseId !== leaseId ||
|
|
450
|
+
latestLease.refresh.mainRefreshLeaseTokenHash !==
|
|
451
|
+
refreshTokenHash) {
|
|
452
|
+
throw new Error('Claude OAuth refresh is already in progress');
|
|
302
453
|
}
|
|
454
|
+
log('[refresh] opencode main oauth refresh request start', {
|
|
455
|
+
attempt,
|
|
456
|
+
});
|
|
457
|
+
const refreshed = await refreshClaudeOAuthToken({
|
|
458
|
+
refreshToken: freshAuth.refresh,
|
|
459
|
+
});
|
|
303
460
|
// biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set
|
|
304
461
|
await client.auth.set({
|
|
305
462
|
path: {
|
|
@@ -312,6 +469,20 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
312
469
|
expires: refreshed.expires,
|
|
313
470
|
},
|
|
314
471
|
});
|
|
472
|
+
await updateMainRefreshState((storage) => {
|
|
473
|
+
if (!storage?.refresh)
|
|
474
|
+
return;
|
|
475
|
+
storage.refresh.mainLastRefreshError = undefined;
|
|
476
|
+
if (storage.refresh.mainRefreshLeaseId === leaseId) {
|
|
477
|
+
storage.refresh.mainRefreshLeaseId = undefined;
|
|
478
|
+
storage.refresh.mainRefreshLeaseUntil = undefined;
|
|
479
|
+
storage.refresh.mainRefreshLeaseTokenHash = undefined;
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
log('[refresh] opencode main oauth refresh succeeded', {
|
|
483
|
+
attempt,
|
|
484
|
+
expiresInMs: refreshed.expires - Date.now(),
|
|
485
|
+
});
|
|
315
486
|
return refreshed.access;
|
|
316
487
|
}
|
|
317
488
|
catch (error) {
|
|
@@ -322,11 +493,50 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
322
493
|
error.code === 'ECONNREFUSED' ||
|
|
323
494
|
error.code === 'ETIMEDOUT' ||
|
|
324
495
|
error.code === 'UND_ERR_CONNECT_TIMEOUT')));
|
|
325
|
-
if (attempt < maxRetries &&
|
|
496
|
+
if (attempt < maxRetries &&
|
|
497
|
+
(isNetworkError ||
|
|
498
|
+
(error instanceof ClaudeOAuthRefreshError &&
|
|
499
|
+
error.status >= 500))) {
|
|
326
500
|
continue;
|
|
327
501
|
}
|
|
502
|
+
log('[refresh] opencode main oauth refresh attempt failed', {
|
|
503
|
+
attempt,
|
|
504
|
+
error: error instanceof Error
|
|
505
|
+
? error.message
|
|
506
|
+
: String(error),
|
|
507
|
+
transient: isNetworkError,
|
|
508
|
+
});
|
|
509
|
+
const failedRefreshToken = freshAuth?.refresh;
|
|
510
|
+
if (failedRefreshToken &&
|
|
511
|
+
error instanceof ClaudeOAuthRefreshError) {
|
|
512
|
+
await updateMainRefreshState((storage) => {
|
|
513
|
+
storage.refresh = storage.refresh ?? {};
|
|
514
|
+
storage.refresh.mainLastRefreshError =
|
|
515
|
+
buildRefreshOperationError({
|
|
516
|
+
error,
|
|
517
|
+
now: Date.now(),
|
|
518
|
+
refreshToken: failedRefreshToken,
|
|
519
|
+
previous: storage.refresh.mainLastRefreshError,
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
}
|
|
328
523
|
throw error;
|
|
329
524
|
}
|
|
525
|
+
finally {
|
|
526
|
+
if (leaseId) {
|
|
527
|
+
await updateMainRefreshState((storage) => {
|
|
528
|
+
if (!storage?.refresh)
|
|
529
|
+
return;
|
|
530
|
+
if (storage.refresh.mainRefreshLeaseId === leaseId &&
|
|
531
|
+
storage.refresh.mainRefreshLeaseTokenHash ===
|
|
532
|
+
leaseTokenHash) {
|
|
533
|
+
storage.refresh.mainRefreshLeaseId = undefined;
|
|
534
|
+
storage.refresh.mainRefreshLeaseUntil = undefined;
|
|
535
|
+
storage.refresh.mainRefreshLeaseTokenHash = undefined;
|
|
536
|
+
}
|
|
537
|
+
}).catch(() => { });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
330
540
|
}
|
|
331
541
|
// Unreachable — each iteration either returns or throws.
|
|
332
542
|
// Kept as a TypeScript exhaustiveness guard.
|
|
@@ -337,6 +547,7 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
337
547
|
}
|
|
338
548
|
return refreshPromise;
|
|
339
549
|
}
|
|
550
|
+
latestRefreshMainAccessToken = refreshMainAccessToken;
|
|
340
551
|
function startMainBackgroundRefresh() {
|
|
341
552
|
if (mainBackgroundRefreshTimer) {
|
|
342
553
|
clearInterval(mainBackgroundRefreshTimer);
|
|
@@ -350,10 +561,31 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
350
561
|
const latestAuth = await getAuth();
|
|
351
562
|
if (latestAuth.type !== 'oauth')
|
|
352
563
|
return;
|
|
564
|
+
await clearStaleMainRefreshError(latestAuth.refresh);
|
|
353
565
|
if (!latestAuth.expires)
|
|
354
566
|
return;
|
|
355
|
-
|
|
356
|
-
|
|
567
|
+
const expiresInMs = latestAuth.expires - Date.now();
|
|
568
|
+
const refreshBeforeMs = mainRefreshBeforeExpiryMs(storage);
|
|
569
|
+
if (expiresInMs > refreshBeforeMs) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
log('[refresh] opencode main oauth background due', {
|
|
573
|
+
expiresInMs,
|
|
574
|
+
refreshBeforeMs,
|
|
575
|
+
});
|
|
576
|
+
if (latestAuth.refresh &&
|
|
577
|
+
refreshBackoffActive(storage?.refresh?.mainLastRefreshError, latestAuth.refresh, Date.now())) {
|
|
578
|
+
log('[refresh] opencode main oauth background skipped backoff', {
|
|
579
|
+
nextRetryAt: storage?.refresh?.mainLastRefreshError?.nextRetryAt,
|
|
580
|
+
retryCount: storage?.refresh?.mainLastRefreshError?.retryCount,
|
|
581
|
+
});
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (latestAuth.refresh &&
|
|
585
|
+
storage?.refresh?.mainRefreshLeaseUntil &&
|
|
586
|
+
storage.refresh.mainRefreshLeaseUntil > Date.now() &&
|
|
587
|
+
storage.refresh.mainRefreshLeaseTokenHash ===
|
|
588
|
+
hashRefreshToken(latestAuth.refresh)) {
|
|
357
589
|
return;
|
|
358
590
|
}
|
|
359
591
|
await refreshMainAccessToken();
|
|
@@ -521,6 +753,24 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
521
753
|
});
|
|
522
754
|
}
|
|
523
755
|
const rewritten = rewriteUrl(input);
|
|
756
|
+
if (route === 'main' &&
|
|
757
|
+
typeof body === 'string' &&
|
|
758
|
+
!subagentRequest &&
|
|
759
|
+
isCache1hEnabled() &&
|
|
760
|
+
getCache1hMode() === 'hybrid') {
|
|
761
|
+
const storage = await loadAccounts();
|
|
762
|
+
const tracked = await cacheKeepManager.track({
|
|
763
|
+
sessionId: relayAffinity,
|
|
764
|
+
url: rewritten.url?.toString() ?? rewritten.input.toString(),
|
|
765
|
+
headers: requestHeaders,
|
|
766
|
+
bodyText: body,
|
|
767
|
+
storage,
|
|
768
|
+
cacheMode: 'hybrid',
|
|
769
|
+
});
|
|
770
|
+
if (tracked.tracked) {
|
|
771
|
+
trace?.mark('cachekeep_track', { session: relayAffinity });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
524
774
|
const directFetch = () => fetch(rewritten.input, {
|
|
525
775
|
...init,
|
|
526
776
|
body,
|
|
@@ -660,7 +910,14 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
660
910
|
trace.done('non_oauth_passthrough', { status: response.status });
|
|
661
911
|
return response;
|
|
662
912
|
}
|
|
913
|
+
await clearStaleMainRefreshError(auth.refresh);
|
|
663
914
|
if (!auth.access || !auth.expires || auth.expires < Date.now()) {
|
|
915
|
+
log('[refresh] opencode main oauth refresh required for request', {
|
|
916
|
+
hasAccess: Boolean(auth.access),
|
|
917
|
+
expiresInMs: auth.expires
|
|
918
|
+
? auth.expires - Date.now()
|
|
919
|
+
: undefined,
|
|
920
|
+
});
|
|
664
921
|
const refreshStart = nowMs();
|
|
665
922
|
auth.access = await refreshMainAccessToken();
|
|
666
923
|
trace.mark('refresh_main_access', {
|
package/dist/prompt-context.js
CHANGED
|
@@ -63,7 +63,10 @@ export async function resolvePromptContext(client, sessionId) {
|
|
|
63
63
|
return null;
|
|
64
64
|
let messages = [];
|
|
65
65
|
try {
|
|
66
|
-
messages = extractMessages(await Promise.resolve(typedClient.session.messages({
|
|
66
|
+
messages = extractMessages(await Promise.resolve(typedClient.session.messages({
|
|
67
|
+
path: { id: sessionId },
|
|
68
|
+
query: { limit: 100 },
|
|
69
|
+
})));
|
|
67
70
|
}
|
|
68
71
|
catch {
|
|
69
72
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/opencode-anthropic-auth",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,6 +40,6 @@
|
|
|
40
40
|
"@opencode-ai/plugin": "*"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@cortexkit/anthropic-auth-core": "1.1.
|
|
43
|
+
"@cortexkit/anthropic-auth-core": "1.1.2"
|
|
44
44
|
}
|
|
45
45
|
}
|