@oh-my-pi/pi-coding-agent 6.9.69 → 7.0.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/CHANGELOG.md +132 -51
- package/examples/sdk/04-skills.ts +1 -1
- package/package.json +6 -6
- package/src/core/agent-session.ts +112 -4
- package/src/core/auth-storage.ts +524 -202
- package/src/core/bash-executor.ts +1 -1
- package/src/core/model-registry.ts +7 -0
- package/src/core/python-executor.ts +29 -8
- package/src/core/python-gateway-coordinator.ts +55 -1
- package/src/core/python-prelude.py +201 -8
- package/src/core/tools/find.ts +18 -5
- package/src/core/tools/lsp/index.ts +13 -2
- package/src/core/tools/python.ts +1 -0
- package/src/core/tools/read.ts +4 -4
- package/src/modes/interactive/controllers/command-controller.ts +349 -0
- package/src/modes/interactive/controllers/input-controller.ts +55 -7
- package/src/modes/interactive/interactive-mode.ts +6 -1
- package/src/modes/interactive/types.ts +2 -1
- package/src/prompts/system/system-prompt.md +81 -79
- package/src/prompts/tools/python.md +0 -1
package/src/core/auth-storage.ts
CHANGED
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
* Handles loading, saving, and refreshing credentials from agent.db.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
7
8
|
import {
|
|
9
|
+
antigravityUsageProvider,
|
|
10
|
+
claudeUsageProvider,
|
|
8
11
|
getEnvApiKey,
|
|
9
12
|
getOAuthApiKey,
|
|
13
|
+
githubCopilotUsageProvider,
|
|
14
|
+
googleGeminiCliUsageProvider,
|
|
10
15
|
loginAnthropic,
|
|
11
16
|
loginAntigravity,
|
|
12
17
|
loginCursor,
|
|
@@ -16,6 +21,16 @@ import {
|
|
|
16
21
|
type OAuthController,
|
|
17
22
|
type OAuthCredentials,
|
|
18
23
|
type OAuthProvider,
|
|
24
|
+
openaiCodexUsageProvider,
|
|
25
|
+
type Provider,
|
|
26
|
+
type UsageCache,
|
|
27
|
+
type UsageCacheEntry,
|
|
28
|
+
type UsageCredential,
|
|
29
|
+
type UsageLimit,
|
|
30
|
+
type UsageLogger,
|
|
31
|
+
type UsageProvider,
|
|
32
|
+
type UsageReport,
|
|
33
|
+
zaiUsageProvider,
|
|
19
34
|
} from "@oh-my-pi/pi-ai";
|
|
20
35
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
21
36
|
import { getAgentDbPath, getAuthPath } from "../config";
|
|
@@ -61,43 +76,64 @@ export interface SerializedAuthStorage {
|
|
|
61
76
|
*/
|
|
62
77
|
type StoredCredential = { id: number; credential: AuthCredential };
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
export type AuthStorageOptions = {
|
|
80
|
+
usageProviderResolver?: (provider: Provider) => UsageProvider | undefined;
|
|
81
|
+
usageCache?: UsageCache;
|
|
82
|
+
usageFetch?: typeof fetch;
|
|
83
|
+
usageNow?: () => number;
|
|
84
|
+
usageLogger?: UsageLogger;
|
|
69
85
|
};
|
|
70
86
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
const DEFAULT_USAGE_PROVIDERS: UsageProvider[] = [
|
|
88
|
+
openaiCodexUsageProvider,
|
|
89
|
+
antigravityUsageProvider,
|
|
90
|
+
googleGeminiCliUsageProvider,
|
|
91
|
+
claudeUsageProvider,
|
|
92
|
+
zaiUsageProvider,
|
|
93
|
+
githubCopilotUsageProvider,
|
|
94
|
+
];
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
expiresAt: number;
|
|
83
|
-
usage?: CodexUsage;
|
|
84
|
-
};
|
|
96
|
+
const DEFAULT_USAGE_PROVIDER_MAP = new Map<Provider, UsageProvider>(
|
|
97
|
+
DEFAULT_USAGE_PROVIDERS.map((provider) => [provider.id, provider]),
|
|
98
|
+
);
|
|
85
99
|
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
const USAGE_CACHE_PREFIX = "usage_cache:";
|
|
101
|
+
|
|
102
|
+
function resolveDefaultUsageProvider(provider: Provider): UsageProvider | undefined {
|
|
103
|
+
return DEFAULT_USAGE_PROVIDER_MAP.get(provider);
|
|
88
104
|
}
|
|
89
105
|
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
106
|
+
function parseUsageCacheEntry(raw: string): UsageCacheEntry | undefined {
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(raw) as { value?: UsageReport | null; expiresAt?: unknown };
|
|
109
|
+
const expiresAt = typeof parsed.expiresAt === "number" ? parsed.expiresAt : undefined;
|
|
110
|
+
if (!expiresAt || !Number.isFinite(expiresAt)) return undefined;
|
|
111
|
+
return { value: parsed.value ?? null, expiresAt };
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined;
|
|
95
114
|
}
|
|
96
|
-
return undefined;
|
|
97
115
|
}
|
|
98
116
|
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
class AuthStorageUsageCache implements UsageCache {
|
|
118
|
+
constructor(private storage: AgentStorage) {}
|
|
119
|
+
|
|
120
|
+
get(key: string): UsageCacheEntry | undefined {
|
|
121
|
+
const raw = this.storage.getCache(`${USAGE_CACHE_PREFIX}${key}`);
|
|
122
|
+
if (!raw) return undefined;
|
|
123
|
+
const entry = parseUsageCacheEntry(raw);
|
|
124
|
+
if (!entry) return undefined;
|
|
125
|
+
if (entry.expiresAt <= Date.now()) return undefined;
|
|
126
|
+
return entry;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
set(key: string, entry: UsageCacheEntry): void {
|
|
130
|
+
const payload = JSON.stringify({ value: entry.value ?? null, expiresAt: entry.expiresAt });
|
|
131
|
+
this.storage.setCache(`${USAGE_CACHE_PREFIX}${key}`, payload, Math.floor(entry.expiresAt / 1000));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
cleanup(): void {
|
|
135
|
+
this.storage.cleanExpiredCache();
|
|
136
|
+
}
|
|
101
137
|
}
|
|
102
138
|
|
|
103
139
|
/**
|
|
@@ -105,14 +141,11 @@ function toBoolean(value: unknown): boolean | undefined {
|
|
|
105
141
|
* Reads from SQLite and migrates legacy auth.json paths.
|
|
106
142
|
*/
|
|
107
143
|
export class AuthStorage {
|
|
108
|
-
private static readonly codexUsageCacheTtlMs = 60_000; // Cache usage data for 1 minute
|
|
109
144
|
private static readonly defaultBackoffMs = 60_000; // Default backoff when no reset time available
|
|
110
|
-
private static readonly cacheCleanupIntervalMs = 300_000; // Clean expired cache every 5 minutes
|
|
111
145
|
|
|
112
146
|
/** Provider -> credentials cache, populated from agent.db on reload(). */
|
|
113
147
|
private data: Map<string, StoredCredential[]> = new Map();
|
|
114
148
|
private storage: AgentStorage;
|
|
115
|
-
private lastCacheCleanup = 0;
|
|
116
149
|
/** Resolved path to agent.db (derived from authPath or used directly if .db). */
|
|
117
150
|
private dbPath: string;
|
|
118
151
|
private runtimeOverrides: Map<string, string> = new Map();
|
|
@@ -122,8 +155,11 @@ export class AuthStorage {
|
|
|
122
155
|
private sessionLastCredential: Map<string, Map<string, { type: AuthCredential["type"]; index: number }>> = new Map();
|
|
123
156
|
/** Maps provider:type -> credentialIndex -> blockedUntilMs for temporary backoff. */
|
|
124
157
|
private credentialBackoff: Map<string, Map<number, number>> = new Map();
|
|
125
|
-
|
|
126
|
-
private
|
|
158
|
+
private usageProviderResolver?: (provider: Provider) => UsageProvider | undefined;
|
|
159
|
+
private usageCache?: UsageCache;
|
|
160
|
+
private usageFetch: typeof fetch;
|
|
161
|
+
private usageNow: () => number;
|
|
162
|
+
private usageLogger?: UsageLogger;
|
|
127
163
|
private fallbackResolver?: (provider: string) => string | undefined;
|
|
128
164
|
|
|
129
165
|
/**
|
|
@@ -133,16 +169,27 @@ export class AuthStorage {
|
|
|
133
169
|
constructor(
|
|
134
170
|
private authPath: string,
|
|
135
171
|
private fallbackPaths: string[] = [],
|
|
172
|
+
options: AuthStorageOptions = {},
|
|
136
173
|
) {
|
|
137
174
|
this.dbPath = AuthStorage.resolveDbPath(authPath);
|
|
138
175
|
this.storage = AgentStorage.open(this.dbPath);
|
|
176
|
+
this.usageProviderResolver = options.usageProviderResolver ?? resolveDefaultUsageProvider;
|
|
177
|
+
this.usageCache = options.usageCache ?? new AuthStorageUsageCache(this.storage);
|
|
178
|
+
this.usageFetch = options.usageFetch ?? fetch;
|
|
179
|
+
this.usageNow = options.usageNow ?? Date.now;
|
|
180
|
+
this.usageLogger =
|
|
181
|
+
options.usageLogger ??
|
|
182
|
+
({
|
|
183
|
+
debug: (message, meta) => logger.debug(message, meta),
|
|
184
|
+
warn: (message, meta) => logger.warn(message, meta),
|
|
185
|
+
} satisfies UsageLogger);
|
|
139
186
|
}
|
|
140
187
|
|
|
141
188
|
/**
|
|
142
189
|
* Create an in-memory AuthStorage instance from serialized data.
|
|
143
190
|
* Used by subagent workers to bypass discovery and use parent's credentials.
|
|
144
191
|
*/
|
|
145
|
-
static fromSerialized(data: SerializedAuthStorage): AuthStorage {
|
|
192
|
+
static fromSerialized(data: SerializedAuthStorage, options: AuthStorageOptions = {}): AuthStorage {
|
|
146
193
|
const instance = Object.create(AuthStorage.prototype) as AuthStorage;
|
|
147
194
|
const authPath = data.authPath ?? data.dbPath ?? getAuthPath();
|
|
148
195
|
instance.authPath = authPath;
|
|
@@ -154,8 +201,16 @@ export class AuthStorage {
|
|
|
154
201
|
instance.providerRoundRobinIndex = new Map();
|
|
155
202
|
instance.sessionLastCredential = new Map();
|
|
156
203
|
instance.credentialBackoff = new Map();
|
|
157
|
-
instance.
|
|
158
|
-
instance.
|
|
204
|
+
instance.usageProviderResolver = options.usageProviderResolver ?? resolveDefaultUsageProvider;
|
|
205
|
+
instance.usageCache = options.usageCache ?? new AuthStorageUsageCache(instance.storage);
|
|
206
|
+
instance.usageFetch = options.usageFetch ?? fetch;
|
|
207
|
+
instance.usageNow = options.usageNow ?? Date.now;
|
|
208
|
+
instance.usageLogger =
|
|
209
|
+
options.usageLogger ??
|
|
210
|
+
({
|
|
211
|
+
debug: (message, meta) => logger.debug(message, meta),
|
|
212
|
+
warn: (message, meta) => logger.warn(message, meta),
|
|
213
|
+
} satisfies UsageLogger);
|
|
159
214
|
|
|
160
215
|
for (const [provider, creds] of Object.entries(data.credentials)) {
|
|
161
216
|
instance.data.set(
|
|
@@ -257,7 +312,15 @@ export class AuthStorage {
|
|
|
257
312
|
list.push({ id: record.id, credential: record.credential });
|
|
258
313
|
grouped.set(record.provider, list);
|
|
259
314
|
}
|
|
260
|
-
|
|
315
|
+
|
|
316
|
+
const dedupedGrouped = new Map<string, StoredCredential[]>();
|
|
317
|
+
for (const [provider, entries] of grouped.entries()) {
|
|
318
|
+
const deduped = this.pruneDuplicateStoredCredentials(provider, entries);
|
|
319
|
+
if (deduped.length > 0) {
|
|
320
|
+
dedupedGrouped.set(provider, deduped);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
this.data = dedupedGrouped;
|
|
261
324
|
}
|
|
262
325
|
|
|
263
326
|
/**
|
|
@@ -283,6 +346,115 @@ export class AuthStorage {
|
|
|
283
346
|
}
|
|
284
347
|
}
|
|
285
348
|
|
|
349
|
+
private getOAuthIdentifiers(credential: OAuthCredential): string[] {
|
|
350
|
+
const identifiers: string[] = [];
|
|
351
|
+
const accountId = credential.accountId?.trim();
|
|
352
|
+
if (accountId) identifiers.push(`account:${accountId}`);
|
|
353
|
+
const email = credential.email?.trim().toLowerCase();
|
|
354
|
+
if (email) identifiers.push(`email:${email}`);
|
|
355
|
+
if (identifiers.length > 0) return identifiers;
|
|
356
|
+
const tokenIdentifiers = this.getOAuthIdentifiersFromToken(credential.access) ?? [];
|
|
357
|
+
for (const identifier of tokenIdentifiers) {
|
|
358
|
+
identifiers.push(identifier);
|
|
359
|
+
}
|
|
360
|
+
if (identifiers.length > 0) return identifiers;
|
|
361
|
+
const refreshIdentifiers = this.getOAuthIdentifiersFromToken(credential.refresh) ?? [];
|
|
362
|
+
for (const identifier of refreshIdentifiers) {
|
|
363
|
+
identifiers.push(identifier);
|
|
364
|
+
}
|
|
365
|
+
return identifiers;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private getOAuthIdentifiersFromToken(token: string | undefined): string[] | undefined {
|
|
369
|
+
if (!token) return undefined;
|
|
370
|
+
const parts = token.split(".");
|
|
371
|
+
if (parts.length !== 3) return undefined;
|
|
372
|
+
const payloadRaw = parts[1];
|
|
373
|
+
try {
|
|
374
|
+
const payload = JSON.parse(
|
|
375
|
+
Buffer.from(payloadRaw.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"),
|
|
376
|
+
) as Record<string, unknown>;
|
|
377
|
+
if (!payload || typeof payload !== "object") return undefined;
|
|
378
|
+
const identifiers: string[] = [];
|
|
379
|
+
const email = typeof payload.email === "string" ? payload.email.trim().toLowerCase() : undefined;
|
|
380
|
+
if (email) identifiers.push(`email:${email}`);
|
|
381
|
+
const accountId =
|
|
382
|
+
typeof payload.account_id === "string"
|
|
383
|
+
? payload.account_id
|
|
384
|
+
: typeof payload.accountId === "string"
|
|
385
|
+
? payload.accountId
|
|
386
|
+
: typeof payload.user_id === "string"
|
|
387
|
+
? payload.user_id
|
|
388
|
+
: typeof payload.sub === "string"
|
|
389
|
+
? payload.sub
|
|
390
|
+
: undefined;
|
|
391
|
+
const trimmedAccountId = accountId?.trim();
|
|
392
|
+
if (trimmedAccountId) identifiers.push(`account:${trimmedAccountId}`);
|
|
393
|
+
return identifiers.length > 0 ? identifiers : undefined;
|
|
394
|
+
} catch {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private dedupeOAuthCredentials(credentials: AuthCredential[]): AuthCredential[] {
|
|
400
|
+
const seen = new Set<string>();
|
|
401
|
+
const deduped: AuthCredential[] = [];
|
|
402
|
+
for (let index = credentials.length - 1; index >= 0; index -= 1) {
|
|
403
|
+
const credential = credentials[index];
|
|
404
|
+
if (credential.type !== "oauth") {
|
|
405
|
+
deduped.push(credential);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const identifiers = this.getOAuthIdentifiers(credential);
|
|
409
|
+
if (identifiers.length === 0) {
|
|
410
|
+
deduped.push(credential);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (identifiers.some((identifier) => seen.has(identifier))) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
for (const identifier of identifiers) {
|
|
417
|
+
seen.add(identifier);
|
|
418
|
+
}
|
|
419
|
+
deduped.push(credential);
|
|
420
|
+
}
|
|
421
|
+
return deduped.reverse();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private pruneDuplicateStoredCredentials(provider: string, entries: StoredCredential[]): StoredCredential[] {
|
|
425
|
+
const seen = new Set<string>();
|
|
426
|
+
const kept: StoredCredential[] = [];
|
|
427
|
+
const removed: StoredCredential[] = [];
|
|
428
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
429
|
+
const entry = entries[index];
|
|
430
|
+
const credential = entry.credential;
|
|
431
|
+
if (credential.type !== "oauth") {
|
|
432
|
+
kept.push(entry);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const identifiers = this.getOAuthIdentifiers(credential);
|
|
436
|
+
if (identifiers.length === 0) {
|
|
437
|
+
kept.push(entry);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (identifiers.some((identifier) => seen.has(identifier))) {
|
|
441
|
+
removed.push(entry);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
for (const identifier of identifiers) {
|
|
445
|
+
seen.add(identifier);
|
|
446
|
+
}
|
|
447
|
+
kept.push(entry);
|
|
448
|
+
}
|
|
449
|
+
if (removed.length > 0) {
|
|
450
|
+
for (const entry of removed) {
|
|
451
|
+
this.storage.deleteAuthCredential(entry.id);
|
|
452
|
+
}
|
|
453
|
+
this.resetProviderAssignments(provider);
|
|
454
|
+
}
|
|
455
|
+
return kept.reverse();
|
|
456
|
+
}
|
|
457
|
+
|
|
286
458
|
/** Returns all credentials for a provider as an array */
|
|
287
459
|
private getCredentialsForProvider(provider: string): AuthCredential[] {
|
|
288
460
|
return this.getStoredCredentials(provider).map((entry) => entry.credential);
|
|
@@ -469,7 +641,8 @@ export class AuthStorage {
|
|
|
469
641
|
*/
|
|
470
642
|
async set(provider: string, credential: AuthCredentialEntry): Promise<void> {
|
|
471
643
|
const normalized = Array.isArray(credential) ? credential : [credential];
|
|
472
|
-
const
|
|
644
|
+
const deduped = this.dedupeOAuthCredentials(normalized);
|
|
645
|
+
const stored = this.storage.replaceAuthCredentialsForProvider(provider, deduped);
|
|
473
646
|
this.setStoredCredentials(
|
|
474
647
|
provider,
|
|
475
648
|
stored.map((record) => ({ id: record.id, credential: record.credential })),
|
|
@@ -611,202 +784,335 @@ export class AuthStorage {
|
|
|
611
784
|
}
|
|
612
785
|
|
|
613
786
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
614
|
-
//
|
|
615
|
-
// Queries
|
|
787
|
+
// Usage API Integration
|
|
788
|
+
// Queries provider usage endpoints to detect rate limits before they occur.
|
|
616
789
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
617
790
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
return base;
|
|
791
|
+
private buildUsageCredential(credential: OAuthCredential): UsageCredential {
|
|
792
|
+
return {
|
|
793
|
+
type: "oauth",
|
|
794
|
+
accessToken: credential.access,
|
|
795
|
+
refreshToken: credential.refresh,
|
|
796
|
+
expiresAt: credential.expires,
|
|
797
|
+
accountId: credential.accountId,
|
|
798
|
+
projectId: credential.projectId,
|
|
799
|
+
email: credential.email,
|
|
800
|
+
enterpriseUrl: credential.enterpriseUrl,
|
|
801
|
+
};
|
|
631
802
|
}
|
|
632
803
|
|
|
633
|
-
private
|
|
634
|
-
|
|
804
|
+
private getUsageReportMetadataValue(report: UsageReport, key: string): string | undefined {
|
|
805
|
+
const metadata = report.metadata;
|
|
806
|
+
if (!metadata || typeof metadata !== "object") return undefined;
|
|
807
|
+
const value = metadata[key];
|
|
808
|
+
return typeof value === "string" ? value.trim() : undefined;
|
|
635
809
|
}
|
|
636
810
|
|
|
637
|
-
private
|
|
638
|
-
const
|
|
639
|
-
|
|
811
|
+
private getUsageReportScopeAccountId(report: UsageReport): string | undefined {
|
|
812
|
+
const ids = new Set<string>();
|
|
813
|
+
for (const limit of report.limits) {
|
|
814
|
+
const accountId = limit.scope.accountId?.trim();
|
|
815
|
+
if (accountId) ids.add(accountId);
|
|
816
|
+
}
|
|
817
|
+
if (ids.size === 1) return [...ids][0];
|
|
818
|
+
return undefined;
|
|
640
819
|
}
|
|
641
820
|
|
|
642
|
-
private
|
|
643
|
-
|
|
821
|
+
private getUsageReportIdentifiers(report: UsageReport): string[] {
|
|
822
|
+
const identifiers: string[] = [];
|
|
823
|
+
const email = this.getUsageReportMetadataValue(report, "email");
|
|
824
|
+
if (email) identifiers.push(`email:${email.toLowerCase()}`);
|
|
825
|
+
const accountId = this.getUsageReportMetadataValue(report, "accountId");
|
|
826
|
+
if (accountId) identifiers.push(`account:${accountId}`);
|
|
827
|
+
const account = this.getUsageReportMetadataValue(report, "account");
|
|
828
|
+
if (account) identifiers.push(`account:${account}`);
|
|
829
|
+
const user = this.getUsageReportMetadataValue(report, "user");
|
|
830
|
+
if (user) identifiers.push(`account:${user}`);
|
|
831
|
+
const username = this.getUsageReportMetadataValue(report, "username");
|
|
832
|
+
if (username) identifiers.push(`account:${username}`);
|
|
833
|
+
const scopeAccountId = this.getUsageReportScopeAccountId(report);
|
|
834
|
+
if (scopeAccountId) identifiers.push(`account:${scopeAccountId}`);
|
|
835
|
+
return identifiers.map((identifier) => `${report.provider}:${identifier.toLowerCase()}`);
|
|
644
836
|
}
|
|
645
837
|
|
|
646
|
-
private
|
|
647
|
-
if (
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
838
|
+
private mergeUsageReportGroup(reports: UsageReport[]): UsageReport {
|
|
839
|
+
if (reports.length === 1) return reports[0];
|
|
840
|
+
const sorted = [...reports].sort((a, b) => {
|
|
841
|
+
const limitDiff = b.limits.length - a.limits.length;
|
|
842
|
+
if (limitDiff !== 0) return limitDiff;
|
|
843
|
+
return (b.fetchedAt ?? 0) - (a.fetchedAt ?? 0);
|
|
844
|
+
});
|
|
845
|
+
const base = sorted[0];
|
|
846
|
+
const mergedLimits = [...base.limits];
|
|
847
|
+
const limitIds = new Set(mergedLimits.map((limit) => limit.id));
|
|
848
|
+
const mergedMetadata: Record<string, unknown> = { ...(base.metadata ?? {}) };
|
|
849
|
+
let fetchedAt = base.fetchedAt;
|
|
850
|
+
|
|
851
|
+
for (const report of sorted.slice(1)) {
|
|
852
|
+
fetchedAt = Math.max(fetchedAt, report.fetchedAt);
|
|
853
|
+
for (const limit of report.limits) {
|
|
854
|
+
if (!limitIds.has(limit.id)) {
|
|
855
|
+
limitIds.add(limit.id);
|
|
856
|
+
mergedLimits.push(limit);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (report.metadata) {
|
|
860
|
+
for (const [key, value] of Object.entries(report.metadata)) {
|
|
861
|
+
if (mergedMetadata[key] === undefined) {
|
|
862
|
+
mergedMetadata[key] = value;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
654
867
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const secondary = this.extractCodexUsageWindow(rateLimit.secondary_window);
|
|
661
|
-
const usage: CodexUsage = {
|
|
662
|
-
allowed: toBoolean(rateLimit.allowed),
|
|
663
|
-
limitReached: toBoolean(rateLimit.limit_reached),
|
|
664
|
-
primary,
|
|
665
|
-
secondary,
|
|
868
|
+
return {
|
|
869
|
+
...base,
|
|
870
|
+
fetchedAt,
|
|
871
|
+
limits: mergedLimits,
|
|
872
|
+
metadata: Object.keys(mergedMetadata).length > 0 ? mergedMetadata : undefined,
|
|
666
873
|
};
|
|
667
|
-
if (!primary && !secondary && usage.allowed === undefined && usage.limitReached === undefined) return undefined;
|
|
668
|
-
return usage;
|
|
669
874
|
}
|
|
670
875
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
876
|
+
private dedupeUsageReports(reports: UsageReport[]): UsageReport[] {
|
|
877
|
+
const groups: UsageReport[][] = [];
|
|
878
|
+
const idToGroup = new Map<string, number>();
|
|
879
|
+
|
|
880
|
+
for (const report of reports) {
|
|
881
|
+
const identifiers = this.getUsageReportIdentifiers(report);
|
|
882
|
+
let groupIndex: number | undefined;
|
|
883
|
+
for (const identifier of identifiers) {
|
|
884
|
+
const existing = idToGroup.get(identifier);
|
|
885
|
+
if (existing !== undefined) {
|
|
886
|
+
groupIndex = existing;
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (groupIndex === undefined) {
|
|
891
|
+
groupIndex = groups.length;
|
|
892
|
+
groups.push([]);
|
|
893
|
+
}
|
|
894
|
+
groups[groupIndex].push(report);
|
|
895
|
+
for (const identifier of identifiers) {
|
|
896
|
+
idToGroup.set(identifier, groupIndex);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const deduped = groups.map((group) => this.mergeUsageReportGroup(group));
|
|
901
|
+
if (deduped.length !== reports.length) {
|
|
902
|
+
this.usageLogger?.debug("Usage reports deduped", {
|
|
903
|
+
before: reports.length,
|
|
904
|
+
after: deduped.length,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
return deduped;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
private isUsageLimitExhausted(limit: UsageLimit): boolean {
|
|
911
|
+
if (limit.status === "exhausted") return true;
|
|
912
|
+
const amount = limit.amount;
|
|
913
|
+
if (amount.usedFraction !== undefined && amount.usedFraction >= 1) return true;
|
|
914
|
+
if (amount.remainingFraction !== undefined && amount.remainingFraction <= 0) return true;
|
|
915
|
+
if (amount.used !== undefined && amount.limit !== undefined && amount.used >= amount.limit) return true;
|
|
916
|
+
if (amount.remaining !== undefined && amount.remaining <= 0) return true;
|
|
917
|
+
if (amount.unit === "percent" && amount.used !== undefined && amount.used >= 100) return true;
|
|
676
918
|
return false;
|
|
677
919
|
}
|
|
678
920
|
|
|
679
|
-
/**
|
|
680
|
-
private
|
|
681
|
-
|
|
921
|
+
/** Returns true if usage indicates rate limit has been reached. */
|
|
922
|
+
private isUsageLimitReached(report: UsageReport): boolean {
|
|
923
|
+
return report.limits.some((limit) => this.isUsageLimitExhausted(limit));
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** Extracts the earliest reset timestamp from exhausted windows (in ms). */
|
|
927
|
+
private getUsageResetAtMs(report: UsageReport, nowMs: number): number | undefined {
|
|
682
928
|
const candidates: number[] = [];
|
|
683
|
-
|
|
684
|
-
if (!
|
|
685
|
-
const
|
|
686
|
-
if (
|
|
687
|
-
candidates.push(
|
|
688
|
-
}
|
|
689
|
-
};
|
|
690
|
-
const useAll = usage.limitReached === true || usage.allowed === false;
|
|
691
|
-
if (useAll) {
|
|
692
|
-
addCandidate(usage.primary?.resetAt);
|
|
693
|
-
addCandidate(usage.secondary?.resetAt);
|
|
694
|
-
} else {
|
|
695
|
-
if (usage.primary?.usedPercent !== undefined && usage.primary.usedPercent >= 100) {
|
|
696
|
-
addCandidate(usage.primary.resetAt);
|
|
929
|
+
for (const limit of report.limits) {
|
|
930
|
+
if (!this.isUsageLimitExhausted(limit)) continue;
|
|
931
|
+
const window = limit.window;
|
|
932
|
+
if (window?.resetsAt && window.resetsAt > nowMs) {
|
|
933
|
+
candidates.push(window.resetsAt);
|
|
697
934
|
}
|
|
698
|
-
if (
|
|
699
|
-
|
|
935
|
+
if (window?.resetInMs && window.resetInMs > 0) {
|
|
936
|
+
const resetAt = nowMs + window.resetInMs;
|
|
937
|
+
if (resetAt > nowMs) candidates.push(resetAt);
|
|
700
938
|
}
|
|
701
939
|
}
|
|
702
940
|
if (candidates.length === 0) return undefined;
|
|
703
941
|
return Math.min(...candidates);
|
|
704
942
|
}
|
|
705
943
|
|
|
706
|
-
private
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const
|
|
713
|
-
if (!
|
|
714
|
-
return Math.min(defaultExpiry, resetAtMs);
|
|
715
|
-
}
|
|
944
|
+
private async getUsageReport(
|
|
945
|
+
provider: Provider,
|
|
946
|
+
credential: OAuthCredential,
|
|
947
|
+
options?: { baseUrl?: string },
|
|
948
|
+
): Promise<UsageReport | null> {
|
|
949
|
+
const resolver = this.usageProviderResolver;
|
|
950
|
+
const cache = this.usageCache;
|
|
951
|
+
if (!resolver || !cache) return null;
|
|
716
952
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const accountId = credential.accountId;
|
|
720
|
-
if (!accountId) return undefined;
|
|
953
|
+
const providerImpl = resolver(provider);
|
|
954
|
+
if (!providerImpl) return null;
|
|
721
955
|
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
"chatgpt-account-id": accountId,
|
|
727
|
-
"openai-beta": "responses=experimental",
|
|
728
|
-
originator: "codex_cli_rs",
|
|
956
|
+
const params = {
|
|
957
|
+
provider,
|
|
958
|
+
credential: this.buildUsageCredential(credential),
|
|
959
|
+
baseUrl: options?.baseUrl,
|
|
729
960
|
};
|
|
730
961
|
|
|
731
|
-
|
|
732
|
-
const response = await fetch(url, { headers });
|
|
733
|
-
if (!response.ok) {
|
|
734
|
-
logger.debug("AuthStorage codex usage fetch failed", {
|
|
735
|
-
status: response.status,
|
|
736
|
-
statusText: response.statusText,
|
|
737
|
-
});
|
|
738
|
-
return undefined;
|
|
739
|
-
}
|
|
962
|
+
if (providerImpl.supports && !providerImpl.supports(params)) return null;
|
|
740
963
|
|
|
741
|
-
|
|
742
|
-
return
|
|
964
|
+
try {
|
|
965
|
+
return await providerImpl.fetchUsage(params, {
|
|
966
|
+
cache,
|
|
967
|
+
fetch: this.usageFetch,
|
|
968
|
+
now: this.usageNow,
|
|
969
|
+
logger: this.usageLogger,
|
|
970
|
+
});
|
|
743
971
|
} catch (error) {
|
|
744
|
-
logger.debug("AuthStorage
|
|
745
|
-
|
|
972
|
+
logger.debug("AuthStorage usage fetch failed", {
|
|
973
|
+
provider,
|
|
974
|
+
error: String(error),
|
|
975
|
+
});
|
|
976
|
+
return null;
|
|
746
977
|
}
|
|
747
978
|
}
|
|
748
979
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const
|
|
980
|
+
async fetchUsageReports(options?: {
|
|
981
|
+
baseUrlResolver?: (provider: Provider) => string | undefined;
|
|
982
|
+
}): Promise<UsageReport[] | null> {
|
|
983
|
+
const resolver = this.usageProviderResolver;
|
|
984
|
+
const cache = this.usageCache;
|
|
985
|
+
if (!resolver || !cache) return null;
|
|
986
|
+
|
|
987
|
+
const tasks: Array<Promise<UsageReport | null>> = [];
|
|
988
|
+
const providers = new Set<string>([
|
|
989
|
+
...this.data.keys(),
|
|
990
|
+
...DEFAULT_USAGE_PROVIDERS.map((provider) => provider.id),
|
|
991
|
+
]);
|
|
992
|
+
this.usageLogger?.debug("Usage fetch requested", {
|
|
993
|
+
providers: Array.from(providers).sort(),
|
|
994
|
+
});
|
|
995
|
+
for (const provider of providers) {
|
|
996
|
+
const providerImpl = resolver(provider as Provider);
|
|
997
|
+
if (!providerImpl) continue;
|
|
998
|
+
const baseUrl = options?.baseUrlResolver?.(provider as Provider);
|
|
999
|
+
let entries = this.getStoredCredentials(provider);
|
|
1000
|
+
if (entries.length > 0) {
|
|
1001
|
+
const dedupedEntries = this.pruneDuplicateStoredCredentials(provider, entries);
|
|
1002
|
+
if (dedupedEntries.length !== entries.length) {
|
|
1003
|
+
this.setStoredCredentials(provider, dedupedEntries);
|
|
1004
|
+
}
|
|
1005
|
+
entries = dedupedEntries;
|
|
1006
|
+
}
|
|
757
1007
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1008
|
+
if (entries.length === 0) {
|
|
1009
|
+
const runtimeKey = this.runtimeOverrides.get(provider);
|
|
1010
|
+
const envKey = getEnvApiKey(provider);
|
|
1011
|
+
const apiKey = runtimeKey ?? envKey;
|
|
1012
|
+
if (!apiKey) {
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
const params = {
|
|
1016
|
+
provider: provider as Provider,
|
|
1017
|
+
credential: { type: "api_key", apiKey } satisfies UsageCredential,
|
|
1018
|
+
baseUrl,
|
|
1019
|
+
};
|
|
1020
|
+
if (providerImpl.supports && !providerImpl.supports(params)) {
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
this.usageLogger?.debug("Usage fetch queued", {
|
|
1024
|
+
provider,
|
|
1025
|
+
credentialType: "api_key",
|
|
1026
|
+
baseUrl,
|
|
1027
|
+
});
|
|
1028
|
+
tasks.push(
|
|
1029
|
+
providerImpl
|
|
1030
|
+
.fetchUsage(params, {
|
|
1031
|
+
cache,
|
|
1032
|
+
fetch: this.usageFetch,
|
|
1033
|
+
now: this.usageNow,
|
|
1034
|
+
logger: this.usageLogger,
|
|
1035
|
+
})
|
|
1036
|
+
.catch((error) => {
|
|
1037
|
+
logger.debug("AuthStorage usage fetch failed", {
|
|
1038
|
+
provider,
|
|
1039
|
+
error: String(error),
|
|
1040
|
+
});
|
|
1041
|
+
return null;
|
|
1042
|
+
}),
|
|
1043
|
+
);
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
762
1046
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1047
|
+
for (const entry of entries) {
|
|
1048
|
+
const credential = entry.credential;
|
|
1049
|
+
const usageCredential: UsageCredential =
|
|
1050
|
+
credential.type === "api_key"
|
|
1051
|
+
? { type: "api_key", apiKey: credential.key }
|
|
1052
|
+
: this.buildUsageCredential(credential);
|
|
1053
|
+
const params = {
|
|
1054
|
+
provider: provider as Provider,
|
|
1055
|
+
credential: usageCredential,
|
|
1056
|
+
baseUrl,
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
if (providerImpl.supports && !providerImpl.supports(params)) {
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
768
1062
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
this.codexUsageCache.set(cacheKey, {
|
|
776
|
-
fetchedAt: now,
|
|
777
|
-
expiresAt: now + AuthStorage.codexUsageCacheTtlMs,
|
|
778
|
-
usage: parsed,
|
|
1063
|
+
this.usageLogger?.debug("Usage fetch queued", {
|
|
1064
|
+
provider,
|
|
1065
|
+
credentialType: usageCredential.type,
|
|
1066
|
+
baseUrl,
|
|
1067
|
+
accountId: usageCredential.accountId,
|
|
1068
|
+
email: usageCredential.email,
|
|
779
1069
|
});
|
|
780
|
-
return parsed;
|
|
781
|
-
} catch {
|
|
782
|
-
// Invalid cache, continue to fetch
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
1070
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1071
|
+
tasks.push(
|
|
1072
|
+
providerImpl
|
|
1073
|
+
.fetchUsage(params, {
|
|
1074
|
+
cache,
|
|
1075
|
+
fetch: this.usageFetch,
|
|
1076
|
+
now: this.usageNow,
|
|
1077
|
+
logger: this.usageLogger,
|
|
1078
|
+
})
|
|
1079
|
+
.catch((error) => {
|
|
1080
|
+
logger.debug("AuthStorage usage fetch failed", {
|
|
1081
|
+
provider,
|
|
1082
|
+
error: String(error),
|
|
1083
|
+
});
|
|
1084
|
+
return null;
|
|
1085
|
+
}),
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
798
1088
|
}
|
|
799
1089
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1090
|
+
if (tasks.length === 0) return [];
|
|
1091
|
+
const results = await Promise.all(tasks);
|
|
1092
|
+
const reports = results.filter((report): report is UsageReport => report !== null);
|
|
1093
|
+
const deduped = this.dedupeUsageReports(reports);
|
|
1094
|
+
this.usageLogger?.debug("Usage fetch resolved", {
|
|
1095
|
+
reports: deduped.map((report) => {
|
|
1096
|
+
const accountLabel =
|
|
1097
|
+
this.getUsageReportMetadataValue(report, "email") ??
|
|
1098
|
+
this.getUsageReportMetadataValue(report, "accountId") ??
|
|
1099
|
+
this.getUsageReportMetadataValue(report, "account") ??
|
|
1100
|
+
this.getUsageReportMetadataValue(report, "user") ??
|
|
1101
|
+
this.getUsageReportMetadataValue(report, "username") ??
|
|
1102
|
+
this.getUsageReportScopeAccountId(report);
|
|
1103
|
+
return {
|
|
1104
|
+
provider: report.provider,
|
|
1105
|
+
limits: report.limits.length,
|
|
1106
|
+
account: accountLabel,
|
|
1107
|
+
};
|
|
1108
|
+
}),
|
|
803
1109
|
});
|
|
804
|
-
return
|
|
1110
|
+
return deduped;
|
|
805
1111
|
}
|
|
806
1112
|
|
|
807
1113
|
/**
|
|
808
1114
|
* Marks the current session's credential as temporarily blocked due to usage limits.
|
|
809
|
-
*
|
|
1115
|
+
* Uses usage reports to determine accurate reset time when available.
|
|
810
1116
|
* Returns true if a credential was blocked, enabling automatic fallback to the next credential.
|
|
811
1117
|
*/
|
|
812
1118
|
async markUsageLimitReached(
|
|
@@ -818,15 +1124,15 @@ export class AuthStorage {
|
|
|
818
1124
|
if (!sessionCredential) return false;
|
|
819
1125
|
|
|
820
1126
|
const providerKey = this.getProviderTypeKey(provider, sessionCredential.type);
|
|
821
|
-
const now =
|
|
1127
|
+
const now = this.usageNow();
|
|
822
1128
|
let blockedUntil = now + (options?.retryAfterMs ?? AuthStorage.defaultBackoffMs);
|
|
823
1129
|
|
|
824
1130
|
if (provider === "openai-codex" && sessionCredential.type === "oauth") {
|
|
825
1131
|
const credential = this.getCredentialsForProvider(provider)[sessionCredential.index];
|
|
826
1132
|
if (credential?.type === "oauth") {
|
|
827
|
-
const
|
|
828
|
-
if (
|
|
829
|
-
const resetAtMs = this.
|
|
1133
|
+
const report = await this.getUsageReport(provider, credential, options);
|
|
1134
|
+
if (report && this.isUsageLimitReached(report)) {
|
|
1135
|
+
const resetAtMs = this.getUsageResetAtMs(report, this.usageNow());
|
|
830
1136
|
if (resetAtMs && resetAtMs > blockedUntil) {
|
|
831
1137
|
blockedUntil = resetAtMs;
|
|
832
1138
|
}
|
|
@@ -848,7 +1154,7 @@ export class AuthStorage {
|
|
|
848
1154
|
|
|
849
1155
|
/**
|
|
850
1156
|
* Resolves an OAuth API key, trying credentials in priority order.
|
|
851
|
-
* Skips blocked credentials and checks usage limits for
|
|
1157
|
+
* Skips blocked credentials and checks usage limits for providers with usage data.
|
|
852
1158
|
* Falls back to earliest-unblocking credential if all are blocked.
|
|
853
1159
|
*/
|
|
854
1160
|
private async resolveOAuthApiKey(
|
|
@@ -902,14 +1208,18 @@ export class AuthStorage {
|
|
|
902
1208
|
return undefined;
|
|
903
1209
|
}
|
|
904
1210
|
|
|
1211
|
+
let usage: UsageReport | null = null;
|
|
1212
|
+
let usageChecked = false;
|
|
1213
|
+
|
|
905
1214
|
if (checkUsage) {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1215
|
+
usage = await this.getUsageReport(provider, selection.credential, options);
|
|
1216
|
+
usageChecked = true;
|
|
1217
|
+
if (usage && this.isUsageLimitReached(usage)) {
|
|
1218
|
+
const resetAtMs = this.getUsageResetAtMs(usage, this.usageNow());
|
|
909
1219
|
this.markCredentialBlocked(
|
|
910
1220
|
providerKey,
|
|
911
1221
|
selection.index,
|
|
912
|
-
resetAtMs ??
|
|
1222
|
+
resetAtMs ?? this.usageNow() + AuthStorage.defaultBackoffMs,
|
|
913
1223
|
);
|
|
914
1224
|
return undefined;
|
|
915
1225
|
}
|
|
@@ -923,17 +1233,29 @@ export class AuthStorage {
|
|
|
923
1233
|
const result = await getOAuthApiKey(provider as OAuthProvider, oauthCreds);
|
|
924
1234
|
if (!result) return undefined;
|
|
925
1235
|
|
|
926
|
-
const updated: OAuthCredential = {
|
|
1236
|
+
const updated: OAuthCredential = {
|
|
1237
|
+
type: "oauth",
|
|
1238
|
+
access: result.newCredentials.access,
|
|
1239
|
+
refresh: result.newCredentials.refresh,
|
|
1240
|
+
expires: result.newCredentials.expires,
|
|
1241
|
+
accountId: result.newCredentials.accountId ?? selection.credential.accountId,
|
|
1242
|
+
email: result.newCredentials.email ?? selection.credential.email,
|
|
1243
|
+
projectId: result.newCredentials.projectId ?? selection.credential.projectId,
|
|
1244
|
+
enterpriseUrl: result.newCredentials.enterpriseUrl ?? selection.credential.enterpriseUrl,
|
|
1245
|
+
};
|
|
927
1246
|
this.replaceCredentialAt(provider, selection.index, updated);
|
|
928
1247
|
|
|
929
1248
|
if (checkUsage) {
|
|
930
|
-
const
|
|
931
|
-
if (
|
|
932
|
-
|
|
1249
|
+
const sameAccount = selection.credential.accountId === updated.accountId;
|
|
1250
|
+
if (!usageChecked || !sameAccount) {
|
|
1251
|
+
usage = await this.getUsageReport(provider, updated, options);
|
|
1252
|
+
}
|
|
1253
|
+
if (usage && this.isUsageLimitReached(usage)) {
|
|
1254
|
+
const resetAtMs = this.getUsageResetAtMs(usage, this.usageNow());
|
|
933
1255
|
this.markCredentialBlocked(
|
|
934
1256
|
providerKey,
|
|
935
1257
|
selection.index,
|
|
936
|
-
resetAtMs ??
|
|
1258
|
+
resetAtMs ?? this.usageNow() + AuthStorage.defaultBackoffMs,
|
|
937
1259
|
);
|
|
938
1260
|
return undefined;
|
|
939
1261
|
}
|