@cortexkit/anthropic-auth-core 1.10.3 → 1.11.0
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/dist/accounts.d.ts +52 -0
- package/dist/accounts.js +392 -94
- package/dist/auth.d.ts +8 -0
- package/dist/auth.js +8 -1
- package/dist/cachekeep.d.ts +3 -1
- package/dist/cachekeep.js +34 -7
- package/dist/commands/account.d.ts +56 -0
- package/dist/commands/account.js +219 -0
- package/dist/dump.js +3 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/logger.d.ts +25 -1
- package/dist/logger.js +238 -18
- package/dist/logging.d.ts +19 -0
- package/dist/logging.js +59 -0
- package/dist/provider.d.ts +19 -0
- package/dist/provider.js +0 -0
- package/dist/quota-manager.d.ts +1 -0
- package/dist/quota-manager.js +16 -7
- package/dist/routing.js +2 -6
- package/package.json +1 -1
package/dist/logger.js
CHANGED
|
@@ -1,7 +1,205 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
-
const
|
|
4
|
+
const LEVEL_ORDER = {
|
|
5
|
+
error: 0,
|
|
6
|
+
warn: 1,
|
|
7
|
+
info: 2,
|
|
8
|
+
debug: 3,
|
|
9
|
+
trace: 4,
|
|
10
|
+
};
|
|
11
|
+
const VALID_LEVELS = new Set(Object.keys(LEVEL_ORDER));
|
|
12
|
+
function parseLogLevel(value) {
|
|
13
|
+
if (!value)
|
|
14
|
+
return null;
|
|
15
|
+
const normalized = value.trim().toLowerCase();
|
|
16
|
+
return VALID_LEVELS.has(normalized) ? normalized : null;
|
|
17
|
+
}
|
|
18
|
+
const envLevel = parseLogLevel(process.env.OPENCODE_ANTHROPIC_AUTH_LOG_LEVEL);
|
|
19
|
+
let currentLevel = envLevel ?? 'info';
|
|
20
|
+
export function setLogLevel(level) {
|
|
21
|
+
currentLevel = level;
|
|
22
|
+
}
|
|
23
|
+
export function getLogLevel() {
|
|
24
|
+
return currentLevel;
|
|
25
|
+
}
|
|
26
|
+
export { parseLogLevel };
|
|
27
|
+
// -- Emission control ------------------------------------------------------
|
|
28
|
+
export function shouldEmit(level) {
|
|
29
|
+
return LEVEL_ORDER[level] <= LEVEL_ORDER[currentLevel];
|
|
30
|
+
}
|
|
31
|
+
// -- File path -------------------------------------------------------------
|
|
32
|
+
const LOG_FILE = process.env.OPENCODE_ANTHROPIC_AUTH_LOG_FILE ??
|
|
33
|
+
path.join(os.tmpdir(), 'opencode-anthropic-auth.log');
|
|
34
|
+
export function getLogFilePath() {
|
|
35
|
+
return LOG_FILE;
|
|
36
|
+
}
|
|
37
|
+
// -- Rotation --------------------------------------------------------------
|
|
38
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
39
|
+
const MAX_GENERATIONS = 3;
|
|
40
|
+
function rotateLogFile() {
|
|
41
|
+
try {
|
|
42
|
+
if (!fs.existsSync(LOG_FILE))
|
|
43
|
+
return;
|
|
44
|
+
const stat = fs.statSync(LOG_FILE);
|
|
45
|
+
if (stat.size < MAX_FILE_SIZE)
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
for (let i = MAX_GENERATIONS; i >= 1; i--) {
|
|
53
|
+
const oldPath = i === 1 ? LOG_FILE : `${LOG_FILE}.${i - 1}`;
|
|
54
|
+
const newPath = `${LOG_FILE}.${i}`;
|
|
55
|
+
try {
|
|
56
|
+
if (i === MAX_GENERATIONS) {
|
|
57
|
+
try {
|
|
58
|
+
fs.unlinkSync(newPath);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// .3 might not exist
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (fs.existsSync(oldPath)) {
|
|
65
|
+
fs.renameSync(oldPath, newPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Rotation best-effort — never throw
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Rotation must never throw
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// -- Redaction ------------------------------------------------------------
|
|
78
|
+
export const SECRET_KEY_EXACT = /^(authorization|x-api-key|cookie|set-cookie|refresh|access|token)$/i;
|
|
79
|
+
export function isSecretKey(key) {
|
|
80
|
+
if (SECRET_KEY_EXACT.test(key))
|
|
81
|
+
return true;
|
|
82
|
+
const k = key.toLowerCase().replace(/[-_]/g, '');
|
|
83
|
+
if (k.includes('apikey'))
|
|
84
|
+
return true;
|
|
85
|
+
if (k.endsWith('secret') || k.endsWith('password'))
|
|
86
|
+
return true;
|
|
87
|
+
if (k.endsWith('token') && !k.endsWith('tokens'))
|
|
88
|
+
return true;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const REDACT_VALUE_PATTERNS = [
|
|
92
|
+
/Bearer\s+\S+/,
|
|
93
|
+
/sk-[A-Za-z0-9-]+/,
|
|
94
|
+
/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/,
|
|
95
|
+
];
|
|
96
|
+
function isRedactableValue(value) {
|
|
97
|
+
if (typeof value !== 'string')
|
|
98
|
+
return false;
|
|
99
|
+
return REDACT_VALUE_PATTERNS.some((pattern) => pattern.test(value));
|
|
100
|
+
}
|
|
101
|
+
export function redactPayload(payload) {
|
|
102
|
+
if (payload == null || Object.keys(payload).length === 0)
|
|
103
|
+
return undefined;
|
|
104
|
+
const seen = new WeakSet();
|
|
105
|
+
function walk(value) {
|
|
106
|
+
if (value == null || typeof value !== 'object') {
|
|
107
|
+
if (isRedactableValue(value))
|
|
108
|
+
return '***REDACTED***';
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
if (seen.has(value))
|
|
112
|
+
return '[Circular]';
|
|
113
|
+
seen.add(value);
|
|
114
|
+
try {
|
|
115
|
+
if (Array.isArray(value))
|
|
116
|
+
return value.map(walk);
|
|
117
|
+
const obj = value;
|
|
118
|
+
const result = {};
|
|
119
|
+
for (const [key, entry] of Object.entries(obj)) {
|
|
120
|
+
if (isSecretKey(key)) {
|
|
121
|
+
result[key] = '***REDACTED***';
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
result[key] = walk(entry);
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
seen.delete(value);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return walk(payload);
|
|
133
|
+
}
|
|
134
|
+
// -- Line formatting ------------------------------------------------------
|
|
135
|
+
export function formatLogLine(level, channel, message, payload) {
|
|
136
|
+
const now = new Date().toISOString();
|
|
137
|
+
const levelUpper = level.toUpperCase().padEnd(5);
|
|
138
|
+
const channelSegment = channel ? ` [${channel}]` : '';
|
|
139
|
+
const payloadJson = (() => {
|
|
140
|
+
try {
|
|
141
|
+
const redacted = redactPayload(payload);
|
|
142
|
+
const withPid = redacted
|
|
143
|
+
? { ...redacted, pid: process.pid }
|
|
144
|
+
: { pid: process.pid };
|
|
145
|
+
return ` ${JSON.stringify(withPid)}`;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return ` {"pid":${process.pid},"payload":"[unserializable]"}`;
|
|
149
|
+
}
|
|
150
|
+
})();
|
|
151
|
+
return `[${now}] ${levelUpper}${channelSegment} ${message}${payloadJson}`;
|
|
152
|
+
}
|
|
153
|
+
let testSink = null;
|
|
154
|
+
export function __setLogTestSink(sink) {
|
|
155
|
+
testSink = sink;
|
|
156
|
+
}
|
|
157
|
+
// -- Leveled API ----------------------------------------------------------
|
|
158
|
+
function emit(level, channel, message, payload) {
|
|
159
|
+
if (!shouldEmit(level))
|
|
160
|
+
return;
|
|
161
|
+
if (testSink) {
|
|
162
|
+
try {
|
|
163
|
+
testSink({ level, channel, message, payload: redactPayload(payload) });
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Sink must never throw into production.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (isTestEnv)
|
|
170
|
+
return;
|
|
171
|
+
try {
|
|
172
|
+
const line = `${formatLogLine(level, channel, message, payload)}\n`;
|
|
173
|
+
buffer.push(line);
|
|
174
|
+
if (buffer.length >= BUFFER_SIZE_LIMIT) {
|
|
175
|
+
flush();
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
scheduleFlush();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Logging must never throw.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export const logger = {
|
|
186
|
+
error(channel, message, payload) {
|
|
187
|
+
emit('error', channel, message, payload);
|
|
188
|
+
},
|
|
189
|
+
warn(channel, message, payload) {
|
|
190
|
+
emit('warn', channel, message, payload);
|
|
191
|
+
},
|
|
192
|
+
info(channel, message, payload) {
|
|
193
|
+
emit('info', channel, message, payload);
|
|
194
|
+
},
|
|
195
|
+
debug(channel, message, payload) {
|
|
196
|
+
emit('debug', channel, message, payload);
|
|
197
|
+
},
|
|
198
|
+
trace(channel, message, payload) {
|
|
199
|
+
emit('trace', channel, message, payload);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
// -- Buffered writes ------------------------------------------------------
|
|
5
203
|
const isTestEnv = process.env.NODE_ENV === 'test';
|
|
6
204
|
const FLUSH_INTERVAL_MS = 500;
|
|
7
205
|
const BUFFER_SIZE_LIMIT = 50;
|
|
@@ -17,6 +215,7 @@ function flush() {
|
|
|
17
215
|
const data = buffer.join('');
|
|
18
216
|
buffer = [];
|
|
19
217
|
try {
|
|
218
|
+
rotateLogFile();
|
|
20
219
|
fs.appendFileSync(LOG_FILE, data);
|
|
21
220
|
}
|
|
22
221
|
catch {
|
|
@@ -31,36 +230,57 @@ function scheduleFlush() {
|
|
|
31
230
|
flush();
|
|
32
231
|
}, FLUSH_INTERVAL_MS);
|
|
33
232
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return '';
|
|
37
|
-
if (data instanceof Error) {
|
|
38
|
-
return ` ${data.message}${data.stack ? `\n${data.stack}` : ''}`;
|
|
39
|
-
}
|
|
40
|
-
return ` ${JSON.stringify(data)}`;
|
|
41
|
-
}
|
|
233
|
+
// -- Back-compat shims ----------------------------------------------------
|
|
234
|
+
const CHANNEL_REGEX = /^\[([\w-]+)\]\s*/;
|
|
42
235
|
export function log(message, data) {
|
|
236
|
+
if (!shouldEmit('debug'))
|
|
237
|
+
return;
|
|
43
238
|
if (isTestEnv)
|
|
44
239
|
return;
|
|
45
240
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
241
|
+
let channel = '';
|
|
242
|
+
let msg = message;
|
|
243
|
+
const match = message.match(CHANNEL_REGEX);
|
|
244
|
+
if (match) {
|
|
245
|
+
channel = match[1] ?? '';
|
|
246
|
+
msg = message.slice(match[0].length);
|
|
49
247
|
}
|
|
50
|
-
|
|
51
|
-
|
|
248
|
+
let payload;
|
|
249
|
+
if (data != null) {
|
|
250
|
+
if (data instanceof Error) {
|
|
251
|
+
payload = {
|
|
252
|
+
error: `${data.message}${data.stack ? `\n${data.stack}` : ''}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
else if (typeof data === 'object') {
|
|
256
|
+
payload = data;
|
|
257
|
+
}
|
|
52
258
|
}
|
|
259
|
+
emit('debug', channel, msg, payload);
|
|
53
260
|
}
|
|
54
261
|
catch {
|
|
55
262
|
// Logging must never throw.
|
|
56
263
|
}
|
|
57
264
|
}
|
|
58
265
|
export function relayLog(message, data) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
266
|
+
if (!shouldEmit('debug'))
|
|
267
|
+
return;
|
|
268
|
+
if (isTestEnv)
|
|
269
|
+
return;
|
|
270
|
+
try {
|
|
271
|
+
let payload;
|
|
272
|
+
if (data != null) {
|
|
273
|
+
if (typeof data === 'object' && !(data instanceof Error)) {
|
|
274
|
+
payload = data;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
emit('debug', 'relay', message, payload);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// Logging must never throw.
|
|
281
|
+
}
|
|
63
282
|
}
|
|
283
|
+
// -- Exit flush -----------------------------------------------------------
|
|
64
284
|
if (!isTestEnv) {
|
|
65
285
|
process.on('exit', flush);
|
|
66
286
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { LogLevel } from './logger.ts';
|
|
2
|
+
export declare const CLAUDE_LOGGING_COMMAND_NAME = "claude-logging";
|
|
3
|
+
export declare const LOGGING_LEVELS: LogLevel[];
|
|
4
|
+
export type LoggingCommandAction = {
|
|
5
|
+
type: 'status';
|
|
6
|
+
} | {
|
|
7
|
+
type: 'level';
|
|
8
|
+
level: LogLevel;
|
|
9
|
+
} | {
|
|
10
|
+
type: 'usage';
|
|
11
|
+
};
|
|
12
|
+
export declare function parseLoggingCommandAction(argumentsText: string): LoggingCommandAction;
|
|
13
|
+
export declare function buildLoggingStatusSummary(input?: {
|
|
14
|
+
level?: LogLevel;
|
|
15
|
+
}): string;
|
|
16
|
+
export declare function executeLoggingCommand(input: {
|
|
17
|
+
argumentsText: string;
|
|
18
|
+
level?: LogLevel;
|
|
19
|
+
}): string;
|
package/dist/logging.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Known limitation: the TUI bundle is a SEPARATE module instance from the loader,
|
|
2
|
+
// so /claude-logging sets the SERVER-side (loader) logger level and persists it
|
|
3
|
+
// (applied on next boot); the TUI-side logger level is only raised via the
|
|
4
|
+
// OPENCODE_ANTHROPIC_AUTH_LOG_LEVEL env override. The command still correctly
|
|
5
|
+
// persists and applies server-side — just note this asymmetry.
|
|
6
|
+
export const CLAUDE_LOGGING_COMMAND_NAME = 'claude-logging';
|
|
7
|
+
export const LOGGING_LEVELS = [
|
|
8
|
+
'error',
|
|
9
|
+
'warn',
|
|
10
|
+
'info',
|
|
11
|
+
'debug',
|
|
12
|
+
'trace',
|
|
13
|
+
];
|
|
14
|
+
const LOGGING_USAGE = 'Usage: `/claude-logging`, `/claude-logging error|warn|info|debug|trace`.';
|
|
15
|
+
export function parseLoggingCommandAction(argumentsText) {
|
|
16
|
+
const tokens = argumentsText.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
17
|
+
if (tokens.length === 0)
|
|
18
|
+
return { type: 'status' };
|
|
19
|
+
if (tokens.length === 1 && LOGGING_LEVELS.includes(tokens[0])) {
|
|
20
|
+
return { type: 'level', level: tokens[0] };
|
|
21
|
+
}
|
|
22
|
+
return { type: 'usage' };
|
|
23
|
+
}
|
|
24
|
+
export function buildLoggingStatusSummary(input) {
|
|
25
|
+
const level = input?.level ?? 'info';
|
|
26
|
+
return [
|
|
27
|
+
'## Claude Log Level',
|
|
28
|
+
'',
|
|
29
|
+
`Current log level: \`${level}\``,
|
|
30
|
+
'- Persisted: ~/.config/opencode/anthropic-auth.json',
|
|
31
|
+
'- Valid levels: error, warn, info, debug, trace',
|
|
32
|
+
'',
|
|
33
|
+
LOGGING_USAGE,
|
|
34
|
+
].join('\n');
|
|
35
|
+
}
|
|
36
|
+
export function executeLoggingCommand(input) {
|
|
37
|
+
const action = parseLoggingCommandAction(input.argumentsText);
|
|
38
|
+
const level = input.level ?? 'info';
|
|
39
|
+
if (action.type === 'status')
|
|
40
|
+
return buildLoggingStatusSummary({ level });
|
|
41
|
+
if (action.type === 'level') {
|
|
42
|
+
return [
|
|
43
|
+
'## Claude Log Level Updated',
|
|
44
|
+
'',
|
|
45
|
+
`Level set to \`${action.level}\`.`,
|
|
46
|
+
'',
|
|
47
|
+
...buildLoggingStatusSummary({ level: action.level })
|
|
48
|
+
.split('\n')
|
|
49
|
+
.slice(2),
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
return [
|
|
53
|
+
'## Claude Log Level Usage',
|
|
54
|
+
'',
|
|
55
|
+
LOGGING_USAGE,
|
|
56
|
+
'',
|
|
57
|
+
...buildLoggingStatusSummary({ level }).split('\n').slice(2),
|
|
58
|
+
].join('\n');
|
|
59
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duck-typed error contract for provider HTTP errors.
|
|
3
|
+
*
|
|
4
|
+
* Shared-core extraction of anthropic-auth and openai-auth is mechanical
|
|
5
|
+
* when all error classification uses this contract instead of instanceof
|
|
6
|
+
* or message-regex matching on provider-specific error classes.
|
|
7
|
+
*/
|
|
8
|
+
export type ProviderHttpError = {
|
|
9
|
+
/** HTTP status code. */
|
|
10
|
+
status?: number;
|
|
11
|
+
/** Parsed Retry-After header value in seconds, if the server provided one. */
|
|
12
|
+
retryAfter?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Provider-agnostic marker that the error originated from a token-REFRESH
|
|
15
|
+
* step (as opposed to a quota-endpoint or other non-refresh error). Consumers
|
|
16
|
+
* use this flag to arm the refresh backoff without instanceof checks.
|
|
17
|
+
*/
|
|
18
|
+
isRefreshError?: boolean;
|
|
19
|
+
};
|
package/dist/provider.js
ADDED
|
File without changes
|
package/dist/quota-manager.d.ts
CHANGED
package/dist/quota-manager.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Handles deduplication, rate-limiting (429 backoff), and staleness.
|
|
7
7
|
*/
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
|
-
import { acquireRefreshFileLock, buildQuotaOperationError, fetchOAuthQuotaSnapshot, getPersistedMainQuota, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getQuotaRefreshEveryNRequests, quotaBackoffActive, } from "./accounts.js";
|
|
9
|
+
import { acquireRefreshFileLock, buildQuotaOperationError, fetchOAuthQuotaSnapshot, getPersistedMainQuota, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getQuotaRefreshEveryNRequests, isQuotaPolicyAuthError, quotaBackoffActive, } from "./accounts.js";
|
|
10
10
|
// Capture real setTimeout before tests can mock globalThis.setTimeout
|
|
11
11
|
const nativeSetTimeout = globalThis.setTimeout;
|
|
12
12
|
/**
|
|
@@ -31,6 +31,7 @@ export class QuotaManager {
|
|
|
31
31
|
fallbackTokenFps = new Map();
|
|
32
32
|
// --- Inflight deduplication ---
|
|
33
33
|
inflightMain = null;
|
|
34
|
+
inflightMainFp = null;
|
|
34
35
|
inflightFallbacks = new Map();
|
|
35
36
|
// --- Rate-limiting (scoped per route so a fallback 429 never backs off the
|
|
36
37
|
// main account or vice versa) ---
|
|
@@ -127,8 +128,8 @@ export class QuotaManager {
|
|
|
127
128
|
this.main = null;
|
|
128
129
|
this.mainTokenFp = null;
|
|
129
130
|
}
|
|
130
|
-
// Deduplicate — return in-flight promise if
|
|
131
|
-
if (this.inflightMain)
|
|
131
|
+
// Deduplicate — return in-flight promise only if same token fingerprint
|
|
132
|
+
if (this.inflightMain && this.inflightMainFp === fp)
|
|
132
133
|
return this.inflightMain;
|
|
133
134
|
// Rate-limit — if API recently 429'd, return stale or throw
|
|
134
135
|
if (this.isBackedOff()) {
|
|
@@ -136,6 +137,7 @@ export class QuotaManager {
|
|
|
136
137
|
return this.main.quota;
|
|
137
138
|
throw new Error('Quota API rate-limited — try again later');
|
|
138
139
|
}
|
|
140
|
+
this.inflightMainFp = fp;
|
|
139
141
|
this.inflightMain = this._fetchMain(accessToken);
|
|
140
142
|
return this.inflightMain;
|
|
141
143
|
}
|
|
@@ -342,6 +344,7 @@ export class QuotaManager {
|
|
|
342
344
|
return queued;
|
|
343
345
|
}
|
|
344
346
|
async _fetchMain(accessToken) {
|
|
347
|
+
const thisFetchFp = tokenFingerprint(accessToken);
|
|
345
348
|
return this._enqueueApiFetch(async () => {
|
|
346
349
|
try {
|
|
347
350
|
// Re-check backoff inside gate — may have been set by
|
|
@@ -388,7 +391,10 @@ export class QuotaManager {
|
|
|
388
391
|
}
|
|
389
392
|
}
|
|
390
393
|
finally {
|
|
391
|
-
this.
|
|
394
|
+
if (this.inflightMainFp === thisFetchFp) {
|
|
395
|
+
this.inflightMain = null;
|
|
396
|
+
this.inflightMainFp = null;
|
|
397
|
+
}
|
|
392
398
|
}
|
|
393
399
|
});
|
|
394
400
|
}
|
|
@@ -441,10 +447,13 @@ export class QuotaManager {
|
|
|
441
447
|
}
|
|
442
448
|
});
|
|
443
449
|
}
|
|
444
|
-
// A 401 is an auth/token problem
|
|
445
|
-
//
|
|
446
|
-
// so
|
|
450
|
+
// A 401 is an auth/token problem and a 403 is an account/org policy problem,
|
|
451
|
+
// not quota endpoint saturation. Surface both without recording quota backoff
|
|
452
|
+
// so callers can refresh, re-auth, or try another account immediately.
|
|
447
453
|
static isAuthError(error) {
|
|
454
|
+
const status = error.status;
|
|
455
|
+
if (status === 401 || isQuotaPolicyAuthError(error))
|
|
456
|
+
return true;
|
|
448
457
|
const message = error instanceof Error ? error.message : String(error);
|
|
449
458
|
return /quota check failed: 401\b/.test(message);
|
|
450
459
|
}
|
package/dist/routing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAccountStoragePath, loadAccounts, saveAccounts, } from "./accounts.js";
|
|
1
|
+
import { createEmptyStorage, getAccountStoragePath, loadAccounts, saveAccounts, } from "./accounts.js";
|
|
2
2
|
export const CLAUDE_ROUTING_COMMAND_NAME = 'claude-routing';
|
|
3
3
|
export const ROUTING_MODES = ['main-first', 'fallback-first'];
|
|
4
4
|
export const DEFAULT_ROUTING_MODE = 'main-first';
|
|
@@ -13,11 +13,7 @@ export function getRoutingMode(storage) {
|
|
|
13
13
|
return normalizeRoutingMode(storage?.routing?.mode);
|
|
14
14
|
}
|
|
15
15
|
export async function setRoutingMode(mode, path = getAccountStoragePath()) {
|
|
16
|
-
const storage = (await loadAccounts(path)) ??
|
|
17
|
-
version: 1,
|
|
18
|
-
main: { type: 'opencode', provider: 'anthropic' },
|
|
19
|
-
accounts: [],
|
|
20
|
-
};
|
|
16
|
+
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
21
17
|
storage.routing = {
|
|
22
18
|
...(storage.routing ?? {}),
|
|
23
19
|
mode,
|