@cortexkit/opencode-anthropic-auth 1.1.1 → 1.1.3
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 +26 -4
- package/dist/cli.js +1 -1
- package/dist/index.js +204 -21
- 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
|
|
@@ -157,7 +158,7 @@ Example:
|
|
|
157
158
|
"refresh": {
|
|
158
159
|
"enabled": true,
|
|
159
160
|
"intervalMinutes": 10,
|
|
160
|
-
"refreshBeforeExpiryMinutes":
|
|
161
|
+
"refreshBeforeExpiryMinutes": 240
|
|
161
162
|
},
|
|
162
163
|
"quota": {
|
|
163
164
|
"enabled": true,
|
|
@@ -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/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { authorize, buildClaudeQuotaSummary, buildFallbackQuotaSummaries, buildRefreshOperationError, CACHE_1H_COMMAND_NAME, CLAUDE_DUMP_COMMAND_NAME, CLAUDE_FAST_COMMAND_NAME, CLAUDE_QUOTAS_COMMAND_NAME, ClaudeOAuthRefreshError, exchange, executeCache1hCommand, executeDumpCommand, executeFastModeCommand, FallbackAccountManager, fetchOAuthQuotaSnapshot, formatRefreshBackoffMessage, getCache1hMode, getCache1hPersistentMode, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getRelayConfig, hashRefreshToken, isCache1hEnabled, isCache1hPersistentlyEnabled, isDumpPersistentlyEnabled, isFastModeEnabled, isFastModePersistentlyEnabled, isFastModeSupportedModel, loadAccounts, log, parseCache1hCommandAction, parseDumpCommandAction, parseFastModeCommandAction, quotaSnapshotPassesPolicy, refreshBackoffActive, refreshClaudeOAuthToken, resolveClaudeCodeIdentity, saveAccounts, sendViaRelay, setCache1hPersistentEnabled, setCache1hPersistentMode, setCache1hState, setDumpEnabled, setDumpPersistentEnabled, setFastModeEnabled, setFastModePersistentEnabled, shouldFallbackStatus, } from '@cortexkit/anthropic-auth-core';
|
|
2
|
+
import { acquireRefreshFileLock, 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';
|
|
3
3
|
import { resolvePromptContext } from "./prompt-context.js";
|
|
4
4
|
import { addFastModeBetaHeader, createStrippedStream, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js";
|
|
5
5
|
const HANDLED_SENTINEL = '__OPENCODE_ANTHROPIC_AUTH_COMMAND_HANDLED__';
|
|
6
6
|
const MAIN_AUTH_REFRESH_TICK_MS = 60_000;
|
|
7
|
-
const
|
|
7
|
+
const MIN_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES = 240;
|
|
8
|
+
const DEFAULT_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES = MIN_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES;
|
|
8
9
|
let nextPerfRequestId = 1;
|
|
9
10
|
let eventLoopLagMonitorStarted = false;
|
|
11
|
+
function perfLoggingEnabled() {
|
|
12
|
+
return process.env.OPENCODE_ANTHROPIC_AUTH_PERF === '1';
|
|
13
|
+
}
|
|
10
14
|
function nowMs() {
|
|
11
15
|
return performance.now();
|
|
12
16
|
}
|
|
@@ -14,8 +18,11 @@ function roundMs(value) {
|
|
|
14
18
|
return Math.round(value * 10) / 10;
|
|
15
19
|
}
|
|
16
20
|
function startEventLoopLagMonitor() {
|
|
17
|
-
if (eventLoopLagMonitorStarted ||
|
|
21
|
+
if (eventLoopLagMonitorStarted ||
|
|
22
|
+
process.env.NODE_ENV === 'test' ||
|
|
23
|
+
!perfLoggingEnabled()) {
|
|
18
24
|
return;
|
|
25
|
+
}
|
|
19
26
|
eventLoopLagMonitorStarted = true;
|
|
20
27
|
const intervalMs = 100;
|
|
21
28
|
const thresholdMs = 250;
|
|
@@ -40,28 +47,37 @@ function createPerfTrace(data) {
|
|
|
40
47
|
last: start,
|
|
41
48
|
mark(stage, stageData) {
|
|
42
49
|
const current = nowMs();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
if (perfLoggingEnabled()) {
|
|
51
|
+
log('[perf] opencode request stage', {
|
|
52
|
+
requestId: trace.requestId,
|
|
53
|
+
stage,
|
|
54
|
+
deltaMs: roundMs(current - trace.last),
|
|
55
|
+
totalMs: roundMs(current - trace.start),
|
|
56
|
+
...stageData,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
50
59
|
trace.last = current;
|
|
51
60
|
},
|
|
52
61
|
done(stage, stageData) {
|
|
53
62
|
const current = nowMs();
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
if (perfLoggingEnabled()) {
|
|
64
|
+
log('[perf] opencode request done', {
|
|
65
|
+
requestId: trace.requestId,
|
|
66
|
+
stage,
|
|
67
|
+
deltaMs: roundMs(current - trace.last),
|
|
68
|
+
totalMs: roundMs(current - trace.start),
|
|
69
|
+
...stageData,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
61
72
|
trace.last = current;
|
|
62
73
|
},
|
|
63
74
|
};
|
|
64
|
-
|
|
75
|
+
if (perfLoggingEnabled()) {
|
|
76
|
+
log('[perf] opencode request start', {
|
|
77
|
+
requestId: trace.requestId,
|
|
78
|
+
...data,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
65
81
|
return trace;
|
|
66
82
|
}
|
|
67
83
|
async function sendIgnoredMessage(ctx, sessionId, text) {
|
|
@@ -98,6 +114,43 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
98
114
|
const { client } = ctx;
|
|
99
115
|
const fallbackManager = new FallbackAccountManager();
|
|
100
116
|
fallbackManager.startBackgroundRefresh();
|
|
117
|
+
let latestRefreshMainAccessToken = null;
|
|
118
|
+
const cacheKeepManager = new CacheKeepManager({
|
|
119
|
+
loadStorage: () => loadAccounts(),
|
|
120
|
+
prepareHeaders: async (headers, target) => {
|
|
121
|
+
if (!latestGetAuth)
|
|
122
|
+
return headers;
|
|
123
|
+
const auth = await latestGetAuth();
|
|
124
|
+
if (auth.type !== 'oauth')
|
|
125
|
+
return headers;
|
|
126
|
+
if (!auth.access || (auth.expires && auth.expires < Date.now())) {
|
|
127
|
+
if (!latestRefreshMainAccessToken)
|
|
128
|
+
return headers;
|
|
129
|
+
auth.access = await latestRefreshMainAccessToken();
|
|
130
|
+
}
|
|
131
|
+
if (!auth.access)
|
|
132
|
+
return headers;
|
|
133
|
+
try {
|
|
134
|
+
const parsedBody = JSON.parse(target.bodyText);
|
|
135
|
+
const identity = await resolveClaudeCodeIdentity(auth.access, typeof parsedBody.model === 'string' ? parsedBody.model : undefined);
|
|
136
|
+
headers.delete('anthropic-beta');
|
|
137
|
+
setOAuthHeaders(headers, auth.access, {
|
|
138
|
+
body: parsedBody,
|
|
139
|
+
identity,
|
|
140
|
+
});
|
|
141
|
+
headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [
|
|
142
|
+
CACHE_KEEP_EXTENDED_TTL_BETA,
|
|
143
|
+
]));
|
|
144
|
+
if (parsedBody.speed === 'fast')
|
|
145
|
+
addFastModeBetaHeader(headers);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
setOAuthHeaders(headers, auth.access);
|
|
149
|
+
}
|
|
150
|
+
return headers;
|
|
151
|
+
},
|
|
152
|
+
log,
|
|
153
|
+
});
|
|
101
154
|
const initialCache1hStorage = await loadAccounts();
|
|
102
155
|
const relayConfig = getRelayConfig(initialCache1hStorage);
|
|
103
156
|
setCache1hState({
|
|
@@ -111,11 +164,29 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
111
164
|
function mainRefreshBeforeExpiryMs(storage) {
|
|
112
165
|
const minutes = storage?.refresh?.refreshBeforeExpiryMinutes ??
|
|
113
166
|
DEFAULT_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES;
|
|
114
|
-
return Math.max(
|
|
167
|
+
return Math.max(MIN_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES, minutes) * 60_000;
|
|
115
168
|
}
|
|
116
169
|
function mainRefreshEnabled(storage) {
|
|
117
170
|
return storage?.refresh?.enabled !== false;
|
|
118
171
|
}
|
|
172
|
+
async function clearStaleMainRefreshError(refreshToken) {
|
|
173
|
+
if (!refreshToken)
|
|
174
|
+
return;
|
|
175
|
+
const storage = await loadAccounts();
|
|
176
|
+
const error = storage?.refresh?.mainLastRefreshError;
|
|
177
|
+
if (!storage?.refresh || !error?.tokenHash)
|
|
178
|
+
return;
|
|
179
|
+
const tokenHash = hashRefreshToken(refreshToken);
|
|
180
|
+
if (error.tokenHash === tokenHash)
|
|
181
|
+
return;
|
|
182
|
+
storage.refresh.mainLastRefreshError = undefined;
|
|
183
|
+
await saveAccounts(storage);
|
|
184
|
+
log('[refresh] opencode main oauth cleared stale backoff after token rotation', {
|
|
185
|
+
previousCheckedAt: error.checkedAt,
|
|
186
|
+
previousNextRetryAt: error.nextRetryAt,
|
|
187
|
+
previousRetryCount: error.retryCount,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
119
190
|
async function buildQuotaCommandSummary() {
|
|
120
191
|
const accounts = [];
|
|
121
192
|
if (latestGetAuth) {
|
|
@@ -180,6 +251,26 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
180
251
|
setCache1hState({ enabled, mode });
|
|
181
252
|
return executeCache1hCommand({ argumentsText, enabled, mode });
|
|
182
253
|
}
|
|
254
|
+
async function executePersistentCacheKeepCommand(argumentsText) {
|
|
255
|
+
const action = parseCacheKeepCommandAction(argumentsText);
|
|
256
|
+
let storage = await loadAccounts();
|
|
257
|
+
if (action.type === 'window') {
|
|
258
|
+
storage = await setCacheKeepPersistentWindow(action.startHour, action.endHour);
|
|
259
|
+
}
|
|
260
|
+
else if (action.type === 'disable') {
|
|
261
|
+
storage = await setCacheKeepPersistentEnabled(false);
|
|
262
|
+
}
|
|
263
|
+
const window = getCacheKeepWindow(storage);
|
|
264
|
+
const stats = cacheKeepManager.stats(window);
|
|
265
|
+
return executeCacheKeepCommand({
|
|
266
|
+
argumentsText,
|
|
267
|
+
enabled: isCacheKeepPersistentlyEnabled(storage),
|
|
268
|
+
window,
|
|
269
|
+
hybridActive: isCacheKeepHybridActive(storage),
|
|
270
|
+
trackedSessions: stats.trackedSessions,
|
|
271
|
+
nextPrewarmAt: stats.nextPrewarmAt,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
183
274
|
async function executePersistentDumpCommand(argumentsText) {
|
|
184
275
|
const action = parseDumpCommandAction(argumentsText);
|
|
185
276
|
if (action.type === 'enable' || action.type === 'disable') {
|
|
@@ -214,6 +305,10 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
214
305
|
template: CACHE_1H_COMMAND_NAME,
|
|
215
306
|
description: 'Show or toggle 1-hour Anthropic ephemeral prompt cache TTL.',
|
|
216
307
|
},
|
|
308
|
+
[CLAUDE_CACHE_KEEP_COMMAND_NAME]: {
|
|
309
|
+
template: CLAUDE_CACHE_KEEP_COMMAND_NAME,
|
|
310
|
+
description: 'Keep hybrid Claude cache warm for recently used sessions during a local time window.',
|
|
311
|
+
},
|
|
217
312
|
[CLAUDE_QUOTAS_COMMAND_NAME]: {
|
|
218
313
|
template: CLAUDE_QUOTAS_COMMAND_NAME,
|
|
219
314
|
description: 'Show current Claude OAuth quota usage for all accounts.',
|
|
@@ -233,6 +328,10 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
233
328
|
await sendIgnoredMessage(ctx, input.sessionID, await executePersistentCache1hCommand(input.arguments));
|
|
234
329
|
throwHandledSentinel();
|
|
235
330
|
}
|
|
331
|
+
if (input.command === CLAUDE_CACHE_KEEP_COMMAND_NAME) {
|
|
332
|
+
await sendIgnoredMessage(ctx, input.sessionID, await executePersistentCacheKeepCommand(input.arguments));
|
|
333
|
+
throwHandledSentinel();
|
|
334
|
+
}
|
|
236
335
|
if (input.command === CLAUDE_QUOTAS_COMMAND_NAME) {
|
|
237
336
|
await sendIgnoredMessage(ctx, input.sessionID, await buildQuotaCommandSummary());
|
|
238
337
|
throwHandledSentinel();
|
|
@@ -276,6 +375,7 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
276
375
|
const baseDelayMs = 500;
|
|
277
376
|
let leaseId = null;
|
|
278
377
|
let leaseTokenHash = null;
|
|
378
|
+
let releaseFileLock = null;
|
|
279
379
|
async function updateMainRefreshState(update) {
|
|
280
380
|
const storage = (await loadAccounts()) ?? {
|
|
281
381
|
version: 1,
|
|
@@ -303,16 +403,44 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
303
403
|
const storage = await loadAccounts();
|
|
304
404
|
const refreshTokenHash = hashRefreshToken(freshAuth.refresh);
|
|
305
405
|
const mainError = storage?.refresh?.mainLastRefreshError;
|
|
406
|
+
log('[refresh] opencode main oauth refresh check', {
|
|
407
|
+
attempt,
|
|
408
|
+
expiresInMs: freshAuth.expires
|
|
409
|
+
? freshAuth.expires - Date.now()
|
|
410
|
+
: undefined,
|
|
411
|
+
hasBackoff: Boolean(mainError),
|
|
412
|
+
backoffActive: mainError
|
|
413
|
+
? refreshBackoffActive(mainError, freshAuth.refresh, Date.now())
|
|
414
|
+
: false,
|
|
415
|
+
retryCount: mainError?.retryCount,
|
|
416
|
+
nextRetryAt: mainError?.nextRetryAt,
|
|
417
|
+
});
|
|
306
418
|
if (mainError &&
|
|
307
419
|
refreshBackoffActive(mainError, freshAuth.refresh, Date.now())) {
|
|
420
|
+
log('[refresh] opencode main oauth refresh skipped backoff', {
|
|
421
|
+
nextRetryAt: mainError.nextRetryAt,
|
|
422
|
+
retryCount: mainError.retryCount,
|
|
423
|
+
});
|
|
308
424
|
throw new Error(formatRefreshBackoffMessage(mainError, Date.now()));
|
|
309
425
|
}
|
|
310
426
|
if (storage?.refresh?.mainRefreshLeaseUntil &&
|
|
311
427
|
storage.refresh.mainRefreshLeaseUntil > Date.now() &&
|
|
312
428
|
storage.refresh.mainRefreshLeaseTokenHash ===
|
|
313
429
|
refreshTokenHash) {
|
|
430
|
+
log('[refresh] opencode main oauth refresh skipped lease', {
|
|
431
|
+
leaseUntil: storage.refresh.mainRefreshLeaseUntil,
|
|
432
|
+
});
|
|
433
|
+
throw new Error('Claude OAuth refresh is already in progress');
|
|
434
|
+
}
|
|
435
|
+
const fileLock = await acquireRefreshFileLock({
|
|
436
|
+
name: 'opencode-main-oauth-refresh',
|
|
437
|
+
ttlMs: 2 * 60_000,
|
|
438
|
+
});
|
|
439
|
+
if (!fileLock) {
|
|
440
|
+
log('[refresh] opencode main oauth refresh skipped file lock');
|
|
314
441
|
throw new Error('Claude OAuth refresh is already in progress');
|
|
315
442
|
}
|
|
443
|
+
releaseFileLock = fileLock.release;
|
|
316
444
|
leaseId = randomUUID();
|
|
317
445
|
leaseTokenHash = refreshTokenHash;
|
|
318
446
|
await updateMainRefreshState((nextStorage) => {
|
|
@@ -325,11 +453,18 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
325
453
|
refreshTokenHash;
|
|
326
454
|
});
|
|
327
455
|
const latestLease = await loadAccounts();
|
|
456
|
+
log('[refresh] opencode main oauth refresh lease acquired', {
|
|
457
|
+
attempt,
|
|
458
|
+
leaseUntil: Date.now() + 2 * 60_000,
|
|
459
|
+
});
|
|
328
460
|
if (latestLease?.refresh?.mainRefreshLeaseId !== leaseId ||
|
|
329
461
|
latestLease.refresh.mainRefreshLeaseTokenHash !==
|
|
330
462
|
refreshTokenHash) {
|
|
331
463
|
throw new Error('Claude OAuth refresh is already in progress');
|
|
332
464
|
}
|
|
465
|
+
log('[refresh] opencode main oauth refresh request start', {
|
|
466
|
+
attempt,
|
|
467
|
+
});
|
|
333
468
|
const refreshed = await refreshClaudeOAuthToken({
|
|
334
469
|
refreshToken: freshAuth.refresh,
|
|
335
470
|
});
|
|
@@ -355,6 +490,10 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
355
490
|
storage.refresh.mainRefreshLeaseTokenHash = undefined;
|
|
356
491
|
}
|
|
357
492
|
});
|
|
493
|
+
log('[refresh] opencode main oauth refresh succeeded', {
|
|
494
|
+
attempt,
|
|
495
|
+
expiresInMs: refreshed.expires - Date.now(),
|
|
496
|
+
});
|
|
358
497
|
return refreshed.access;
|
|
359
498
|
}
|
|
360
499
|
catch (error) {
|
|
@@ -371,6 +510,13 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
371
510
|
error.status >= 500))) {
|
|
372
511
|
continue;
|
|
373
512
|
}
|
|
513
|
+
log('[refresh] opencode main oauth refresh attempt failed', {
|
|
514
|
+
attempt,
|
|
515
|
+
error: error instanceof Error
|
|
516
|
+
? error.message
|
|
517
|
+
: String(error),
|
|
518
|
+
transient: isNetworkError,
|
|
519
|
+
});
|
|
374
520
|
const failedRefreshToken = freshAuth?.refresh;
|
|
375
521
|
if (failedRefreshToken &&
|
|
376
522
|
error instanceof ClaudeOAuthRefreshError) {
|
|
@@ -401,6 +547,7 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
401
547
|
}
|
|
402
548
|
}).catch(() => { });
|
|
403
549
|
}
|
|
550
|
+
await releaseFileLock?.().catch(() => { });
|
|
404
551
|
}
|
|
405
552
|
}
|
|
406
553
|
// Unreachable — each iteration either returns or throws.
|
|
@@ -412,6 +559,7 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
412
559
|
}
|
|
413
560
|
return refreshPromise;
|
|
414
561
|
}
|
|
562
|
+
latestRefreshMainAccessToken = refreshMainAccessToken;
|
|
415
563
|
function startMainBackgroundRefresh() {
|
|
416
564
|
if (mainBackgroundRefreshTimer) {
|
|
417
565
|
clearInterval(mainBackgroundRefreshTimer);
|
|
@@ -425,14 +573,24 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
425
573
|
const latestAuth = await getAuth();
|
|
426
574
|
if (latestAuth.type !== 'oauth')
|
|
427
575
|
return;
|
|
576
|
+
await clearStaleMainRefreshError(latestAuth.refresh);
|
|
428
577
|
if (!latestAuth.expires)
|
|
429
578
|
return;
|
|
430
|
-
|
|
431
|
-
|
|
579
|
+
const expiresInMs = latestAuth.expires - Date.now();
|
|
580
|
+
const refreshBeforeMs = mainRefreshBeforeExpiryMs(storage);
|
|
581
|
+
if (expiresInMs > refreshBeforeMs) {
|
|
432
582
|
return;
|
|
433
583
|
}
|
|
584
|
+
log('[refresh] opencode main oauth background due', {
|
|
585
|
+
expiresInMs,
|
|
586
|
+
refreshBeforeMs,
|
|
587
|
+
});
|
|
434
588
|
if (latestAuth.refresh &&
|
|
435
589
|
refreshBackoffActive(storage?.refresh?.mainLastRefreshError, latestAuth.refresh, Date.now())) {
|
|
590
|
+
log('[refresh] opencode main oauth background skipped backoff', {
|
|
591
|
+
nextRetryAt: storage?.refresh?.mainLastRefreshError?.nextRetryAt,
|
|
592
|
+
retryCount: storage?.refresh?.mainLastRefreshError?.retryCount,
|
|
593
|
+
});
|
|
436
594
|
return;
|
|
437
595
|
}
|
|
438
596
|
if (latestAuth.refresh &&
|
|
@@ -607,6 +765,24 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
607
765
|
});
|
|
608
766
|
}
|
|
609
767
|
const rewritten = rewriteUrl(input);
|
|
768
|
+
if (route === 'main' &&
|
|
769
|
+
typeof body === 'string' &&
|
|
770
|
+
!subagentRequest &&
|
|
771
|
+
isCache1hEnabled() &&
|
|
772
|
+
getCache1hMode() === 'hybrid') {
|
|
773
|
+
const storage = await loadAccounts();
|
|
774
|
+
const tracked = await cacheKeepManager.track({
|
|
775
|
+
sessionId: relayAffinity,
|
|
776
|
+
url: rewritten.url?.toString() ?? rewritten.input.toString(),
|
|
777
|
+
headers: requestHeaders,
|
|
778
|
+
bodyText: body,
|
|
779
|
+
storage,
|
|
780
|
+
cacheMode: 'hybrid',
|
|
781
|
+
});
|
|
782
|
+
if (tracked.tracked) {
|
|
783
|
+
trace?.mark('cachekeep_track', { session: relayAffinity });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
610
786
|
const directFetch = () => fetch(rewritten.input, {
|
|
611
787
|
...init,
|
|
612
788
|
body,
|
|
@@ -746,7 +922,14 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
746
922
|
trace.done('non_oauth_passthrough', { status: response.status });
|
|
747
923
|
return response;
|
|
748
924
|
}
|
|
925
|
+
await clearStaleMainRefreshError(auth.refresh);
|
|
749
926
|
if (!auth.access || !auth.expires || auth.expires < Date.now()) {
|
|
927
|
+
log('[refresh] opencode main oauth refresh required for request', {
|
|
928
|
+
hasAccess: Boolean(auth.access),
|
|
929
|
+
expiresInMs: auth.expires
|
|
930
|
+
? auth.expires - Date.now()
|
|
931
|
+
: undefined,
|
|
932
|
+
});
|
|
750
933
|
const refreshStart = nowMs();
|
|
751
934
|
auth.access = await refreshMainAccessToken();
|
|
752
935
|
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.3",
|
|
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.3"
|
|
44
44
|
}
|
|
45
45
|
}
|