@victor-software-house/pi-multicodex 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +31 -20
- package/abort-utils.ts +24 -0
- package/account-manager.ts +262 -0
- package/browser.ts +34 -0
- package/commands.ts +138 -0
- package/extension.ts +41 -0
- package/hooks.ts +22 -0
- package/index.ts +23 -985
- package/package.json +16 -3
- package/provider.ts +75 -0
- package/quota.ts +5 -0
- package/selection.ts +69 -0
- package/storage.ts +49 -0
- package/stream-wrapper.ts +191 -0
- package/usage-client.ts +50 -0
- package/usage.ts +86 -0
package/index.ts
CHANGED
|
@@ -1,985 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
loginOpenAICodex,
|
|
25
|
-
type OAuthCredentials,
|
|
26
|
-
refreshOpenAICodexToken,
|
|
27
|
-
} from "@mariozechner/pi-ai/oauth";
|
|
28
|
-
import type {
|
|
29
|
-
ExtensionAPI,
|
|
30
|
-
ExtensionCommandContext,
|
|
31
|
-
ExtensionContext,
|
|
32
|
-
} from "@mariozechner/pi-coding-agent";
|
|
33
|
-
|
|
34
|
-
// =============================================================================
|
|
35
|
-
// Helpers
|
|
36
|
-
// =============================================================================
|
|
37
|
-
|
|
38
|
-
const USAGE_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
39
|
-
const USAGE_REQUEST_TIMEOUT_MS = 10 * 1000;
|
|
40
|
-
|
|
41
|
-
export function isQuotaErrorMessage(message: string): boolean {
|
|
42
|
-
return /\b429\b|quota|usage limit|rate.?limit|too many requests|limit reached/i.test(
|
|
43
|
-
message,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getErrorMessage(err: unknown): string {
|
|
48
|
-
if (err instanceof Error) return err.message;
|
|
49
|
-
return typeof err === "string" ? err : JSON.stringify(err);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function createErrorAssistantMessage(
|
|
53
|
-
model: Model<Api>,
|
|
54
|
-
message: string,
|
|
55
|
-
): AssistantMessage {
|
|
56
|
-
return {
|
|
57
|
-
role: "assistant",
|
|
58
|
-
content: [],
|
|
59
|
-
api: model.api,
|
|
60
|
-
provider: model.provider,
|
|
61
|
-
model: model.id,
|
|
62
|
-
usage: {
|
|
63
|
-
input: 0,
|
|
64
|
-
output: 0,
|
|
65
|
-
cacheRead: 0,
|
|
66
|
-
cacheWrite: 0,
|
|
67
|
-
totalTokens: 0,
|
|
68
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
69
|
-
},
|
|
70
|
-
stopReason: "error",
|
|
71
|
-
errorMessage: message,
|
|
72
|
-
timestamp: Date.now(),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
interface CodexUsageWindow {
|
|
77
|
-
usedPercent?: number;
|
|
78
|
-
resetAt?: number;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface CodexUsageSnapshot {
|
|
82
|
-
primary?: CodexUsageWindow;
|
|
83
|
-
secondary?: CodexUsageWindow;
|
|
84
|
-
fetchedAt: number;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface WhamUsageResponse {
|
|
88
|
-
rate_limit?: {
|
|
89
|
-
primary_window?: WhamUsageWindow;
|
|
90
|
-
secondary_window?: WhamUsageWindow;
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
type WhamUsageWindow = {
|
|
95
|
-
reset_at?: number;
|
|
96
|
-
used_percent?: number;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export interface ProviderModelDef {
|
|
100
|
-
id: string;
|
|
101
|
-
name: string;
|
|
102
|
-
reasoning: boolean;
|
|
103
|
-
input: ("text" | "image")[];
|
|
104
|
-
cost: {
|
|
105
|
-
input: number;
|
|
106
|
-
output: number;
|
|
107
|
-
cacheRead: number;
|
|
108
|
-
cacheWrite: number;
|
|
109
|
-
};
|
|
110
|
-
contextWindow: number;
|
|
111
|
-
maxTokens: number;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function getOpenAICodexMirror(): {
|
|
115
|
-
baseUrl: string;
|
|
116
|
-
models: ProviderModelDef[];
|
|
117
|
-
} {
|
|
118
|
-
const sourceModels = getModels("openai-codex");
|
|
119
|
-
return {
|
|
120
|
-
baseUrl: sourceModels[0]?.baseUrl || "https://chatgpt.com/backend-api",
|
|
121
|
-
models: sourceModels.map((m) => ({
|
|
122
|
-
id: m.id,
|
|
123
|
-
name: m.name,
|
|
124
|
-
reasoning: m.reasoning,
|
|
125
|
-
input: m.input,
|
|
126
|
-
cost: m.cost,
|
|
127
|
-
contextWindow: m.contextWindow,
|
|
128
|
-
maxTokens: m.maxTokens,
|
|
129
|
-
})),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function normalizeUsedPercent(value?: number): number | undefined {
|
|
134
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
135
|
-
return Math.min(100, Math.max(0, value));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function normalizeResetAt(value?: number): number | undefined {
|
|
139
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
140
|
-
return value * 1000;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function parseUsageWindow(
|
|
144
|
-
window?: WhamUsageWindow,
|
|
145
|
-
): CodexUsageWindow | undefined {
|
|
146
|
-
if (!window) return undefined;
|
|
147
|
-
const usedPercent = normalizeUsedPercent(window.used_percent);
|
|
148
|
-
const resetAt = normalizeResetAt(window.reset_at);
|
|
149
|
-
if (usedPercent === undefined && resetAt === undefined) return undefined;
|
|
150
|
-
return { usedPercent, resetAt };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function parseCodexUsageResponse(
|
|
154
|
-
data: WhamUsageResponse,
|
|
155
|
-
): Omit<CodexUsageSnapshot, "fetchedAt"> {
|
|
156
|
-
return {
|
|
157
|
-
primary: parseUsageWindow(data.rate_limit?.primary_window),
|
|
158
|
-
secondary: parseUsageWindow(data.rate_limit?.secondary_window),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function isUsageUntouched(usage?: CodexUsageSnapshot): boolean {
|
|
163
|
-
const primary = usage?.primary?.usedPercent;
|
|
164
|
-
const secondary = usage?.secondary?.usedPercent;
|
|
165
|
-
if (primary === undefined || secondary === undefined) return false;
|
|
166
|
-
return primary === 0 && secondary === 0;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function getNextResetAt(usage?: CodexUsageSnapshot): number | undefined {
|
|
170
|
-
const candidates = [
|
|
171
|
-
usage?.primary?.resetAt,
|
|
172
|
-
usage?.secondary?.resetAt,
|
|
173
|
-
].filter((value): value is number => typeof value === "number");
|
|
174
|
-
if (candidates.length === 0) return undefined;
|
|
175
|
-
return Math.min(...candidates);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Weekly reset only (secondary window)
|
|
179
|
-
export function getWeeklyResetAt(
|
|
180
|
-
usage?: CodexUsageSnapshot,
|
|
181
|
-
): number | undefined {
|
|
182
|
-
const resetAt = usage?.secondary?.resetAt;
|
|
183
|
-
return typeof resetAt === "number" ? resetAt : undefined;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function formatResetAt(resetAt?: number): string {
|
|
187
|
-
if (!resetAt) return "unknown";
|
|
188
|
-
const diffMs = resetAt - Date.now();
|
|
189
|
-
if (diffMs <= 0) return "now";
|
|
190
|
-
const diffMinutes = Math.max(1, Math.round(diffMs / 60000));
|
|
191
|
-
if (diffMinutes < 60) return `in ${diffMinutes}m`;
|
|
192
|
-
const diffHours = Math.round(diffMinutes / 60);
|
|
193
|
-
if (diffHours < 48) return `in ${diffHours}h`;
|
|
194
|
-
const diffDays = Math.round(diffHours / 24);
|
|
195
|
-
return `in ${diffDays}d`;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async function fetchCodexUsage(
|
|
199
|
-
accessToken: string,
|
|
200
|
-
accountId: string | undefined,
|
|
201
|
-
options?: { signal?: AbortSignal },
|
|
202
|
-
): Promise<CodexUsageSnapshot> {
|
|
203
|
-
const { controller, clear } = createTimeoutController(
|
|
204
|
-
options?.signal,
|
|
205
|
-
USAGE_REQUEST_TIMEOUT_MS,
|
|
206
|
-
);
|
|
207
|
-
try {
|
|
208
|
-
const headers: Record<string, string> = {
|
|
209
|
-
Authorization: `Bearer ${accessToken}`,
|
|
210
|
-
Accept: "application/json",
|
|
211
|
-
};
|
|
212
|
-
if (accountId) {
|
|
213
|
-
headers["ChatGPT-Account-Id"] = accountId;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const response = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
217
|
-
headers,
|
|
218
|
-
signal: controller.signal,
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
if (!response.ok) {
|
|
222
|
-
throw new Error(`Usage request failed: ${response.status}`);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const data = (await response.json()) as WhamUsageResponse;
|
|
226
|
-
return { ...parseCodexUsageResponse(data), fetchedAt: Date.now() };
|
|
227
|
-
} finally {
|
|
228
|
-
clear();
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function createLinkedAbortController(signal?: AbortSignal): AbortController {
|
|
233
|
-
const controller = new AbortController();
|
|
234
|
-
if (signal?.aborted) {
|
|
235
|
-
controller.abort();
|
|
236
|
-
return controller;
|
|
237
|
-
}
|
|
238
|
-
signal?.addEventListener("abort", () => controller.abort(), { once: true });
|
|
239
|
-
return controller;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function createTimeoutController(
|
|
243
|
-
signal: AbortSignal | undefined,
|
|
244
|
-
timeoutMs: number,
|
|
245
|
-
): { controller: AbortController; clear: () => void } {
|
|
246
|
-
const controller = createLinkedAbortController(signal);
|
|
247
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
248
|
-
return {
|
|
249
|
-
controller,
|
|
250
|
-
clear: () => clearTimeout(timeout),
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function withProvider(
|
|
255
|
-
event: AssistantMessageEvent,
|
|
256
|
-
provider: string,
|
|
257
|
-
): AssistantMessageEvent {
|
|
258
|
-
if ("partial" in event) {
|
|
259
|
-
return { ...event, partial: { ...event.partial, provider } };
|
|
260
|
-
}
|
|
261
|
-
if (event.type === "done") {
|
|
262
|
-
return { ...event, message: { ...event.message, provider } };
|
|
263
|
-
}
|
|
264
|
-
if (event.type === "error") {
|
|
265
|
-
return { ...event, error: { ...event.error, provider } };
|
|
266
|
-
}
|
|
267
|
-
return event;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async function openLoginInBrowser(
|
|
271
|
-
pi: ExtensionAPI,
|
|
272
|
-
ctx: ExtensionCommandContext,
|
|
273
|
-
url: string,
|
|
274
|
-
): Promise<void> {
|
|
275
|
-
let command: string;
|
|
276
|
-
let args: string[];
|
|
277
|
-
|
|
278
|
-
if (process.platform === "darwin") {
|
|
279
|
-
command = "open";
|
|
280
|
-
args = [url];
|
|
281
|
-
} else if (process.platform === "win32") {
|
|
282
|
-
command = "cmd";
|
|
283
|
-
args = ["/c", "start", "", url];
|
|
284
|
-
} else {
|
|
285
|
-
command = "xdg-open";
|
|
286
|
-
args = [url];
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
await pi.exec(command, args);
|
|
291
|
-
} catch (error) {
|
|
292
|
-
ctx.ui.notify(
|
|
293
|
-
"Could not open a browser automatically. Please open the login URL manually.",
|
|
294
|
-
"warning",
|
|
295
|
-
);
|
|
296
|
-
console.warn("[multicodex] Failed to open browser:", error);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// =============================================================================
|
|
301
|
-
// Storage
|
|
302
|
-
// =============================================================================
|
|
303
|
-
|
|
304
|
-
export interface Account {
|
|
305
|
-
email: string;
|
|
306
|
-
accessToken: string;
|
|
307
|
-
refreshToken: string;
|
|
308
|
-
expiresAt: number;
|
|
309
|
-
accountId?: string;
|
|
310
|
-
lastUsed?: number;
|
|
311
|
-
quotaExhaustedUntil?: number;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
interface StorageData {
|
|
315
|
-
accounts: Account[];
|
|
316
|
-
activeEmail?: string;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const STORAGE_FILE = path.join(os.homedir(), ".pi", "agent", "multicodex.json");
|
|
320
|
-
const PROVIDER_ID = "multicodex";
|
|
321
|
-
const QUOTA_COOLDOWN_MS = 60 * 60 * 1000; // 1 hour
|
|
322
|
-
|
|
323
|
-
type WarningHandler = (message: string) => void;
|
|
324
|
-
|
|
325
|
-
function isAccountAvailable(account: Account, now: number): boolean {
|
|
326
|
-
return !account.quotaExhaustedUntil || account.quotaExhaustedUntil <= now;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function pickRandomAccount(accounts: Account[]): Account | undefined {
|
|
330
|
-
if (accounts.length === 0) return undefined;
|
|
331
|
-
return accounts[Math.floor(Math.random() * accounts.length)];
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function pickEarliestWeeklyResetAccount(
|
|
335
|
-
accounts: Account[],
|
|
336
|
-
usageByEmail: Map<string, CodexUsageSnapshot>,
|
|
337
|
-
): Account | undefined {
|
|
338
|
-
const candidates = accounts
|
|
339
|
-
.map((account) => ({
|
|
340
|
-
account,
|
|
341
|
-
resetAt: getWeeklyResetAt(usageByEmail.get(account.email)),
|
|
342
|
-
}))
|
|
343
|
-
.filter(
|
|
344
|
-
(entry): entry is { account: Account; resetAt: number } =>
|
|
345
|
-
typeof entry.resetAt === "number",
|
|
346
|
-
)
|
|
347
|
-
.sort((a, b) => a.resetAt - b.resetAt);
|
|
348
|
-
|
|
349
|
-
return candidates[0]?.account;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export function pickBestAccount(
|
|
353
|
-
accounts: Account[],
|
|
354
|
-
usageByEmail: Map<string, CodexUsageSnapshot>,
|
|
355
|
-
options?: { excludeEmails?: Set<string>; now?: number },
|
|
356
|
-
): Account | undefined {
|
|
357
|
-
const now = options?.now ?? Date.now();
|
|
358
|
-
const available = accounts.filter(
|
|
359
|
-
(account) =>
|
|
360
|
-
isAccountAvailable(account, now) &&
|
|
361
|
-
!options?.excludeEmails?.has(account.email),
|
|
362
|
-
);
|
|
363
|
-
if (available.length === 0) return undefined;
|
|
364
|
-
|
|
365
|
-
const withUsage = available.filter((account) =>
|
|
366
|
-
usageByEmail.has(account.email),
|
|
367
|
-
);
|
|
368
|
-
const untouched = withUsage.filter((account) =>
|
|
369
|
-
isUsageUntouched(usageByEmail.get(account.email)),
|
|
370
|
-
);
|
|
371
|
-
|
|
372
|
-
if (untouched.length > 0) {
|
|
373
|
-
return (
|
|
374
|
-
pickEarliestWeeklyResetAccount(untouched, usageByEmail) ??
|
|
375
|
-
pickRandomAccount(untouched)
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const earliestWeeklyReset = pickEarliestWeeklyResetAccount(
|
|
380
|
-
withUsage,
|
|
381
|
-
usageByEmail,
|
|
382
|
-
);
|
|
383
|
-
if (earliestWeeklyReset) return earliestWeeklyReset;
|
|
384
|
-
|
|
385
|
-
return pickRandomAccount(available);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// =============================================================================
|
|
389
|
-
// Account Manager
|
|
390
|
-
// =============================================================================
|
|
391
|
-
|
|
392
|
-
export class AccountManager {
|
|
393
|
-
private data: StorageData;
|
|
394
|
-
private usageCache = new Map<string, CodexUsageSnapshot>();
|
|
395
|
-
private warningHandler?: WarningHandler;
|
|
396
|
-
private manualEmail?: string;
|
|
397
|
-
|
|
398
|
-
constructor() {
|
|
399
|
-
this.data = this.load();
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
private load(): StorageData {
|
|
403
|
-
try {
|
|
404
|
-
if (fs.existsSync(STORAGE_FILE)) {
|
|
405
|
-
return JSON.parse(
|
|
406
|
-
fs.readFileSync(STORAGE_FILE, "utf-8"),
|
|
407
|
-
) as StorageData;
|
|
408
|
-
}
|
|
409
|
-
} catch (e) {
|
|
410
|
-
console.error("Failed to load multicodex accounts:", e);
|
|
411
|
-
}
|
|
412
|
-
return { accounts: [] };
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
private save(): void {
|
|
416
|
-
try {
|
|
417
|
-
const dir = path.dirname(STORAGE_FILE);
|
|
418
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
419
|
-
fs.writeFileSync(STORAGE_FILE, JSON.stringify(this.data, null, 2));
|
|
420
|
-
} catch (e) {
|
|
421
|
-
console.error("Failed to save multicodex accounts:", e);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
getAccounts(): Account[] {
|
|
426
|
-
return this.data.accounts;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
getAccount(email: string): Account | undefined {
|
|
430
|
-
return this.data.accounts.find((a) => a.email === email);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
setWarningHandler(handler?: WarningHandler): void {
|
|
434
|
-
this.warningHandler = handler;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
addOrUpdateAccount(email: string, creds: OAuthCredentials): void {
|
|
438
|
-
const existing = this.getAccount(email);
|
|
439
|
-
const accountId =
|
|
440
|
-
typeof creds.accountId === "string" ? creds.accountId : undefined;
|
|
441
|
-
if (existing) {
|
|
442
|
-
existing.accessToken = creds.access;
|
|
443
|
-
existing.refreshToken = creds.refresh;
|
|
444
|
-
existing.expiresAt = creds.expires;
|
|
445
|
-
if (accountId) {
|
|
446
|
-
existing.accountId = accountId;
|
|
447
|
-
}
|
|
448
|
-
} else {
|
|
449
|
-
this.data.accounts.push({
|
|
450
|
-
email,
|
|
451
|
-
accessToken: creds.access,
|
|
452
|
-
refreshToken: creds.refresh,
|
|
453
|
-
expiresAt: creds.expires,
|
|
454
|
-
accountId,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
this.setActiveAccount(email);
|
|
458
|
-
this.save();
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
getActiveAccount(): Account | undefined {
|
|
462
|
-
const manual = this.getManualAccount();
|
|
463
|
-
if (manual) return manual;
|
|
464
|
-
if (this.data.activeEmail) {
|
|
465
|
-
return this.getAccount(this.data.activeEmail);
|
|
466
|
-
}
|
|
467
|
-
return this.data.accounts[0];
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
getManualAccount(): Account | undefined {
|
|
471
|
-
if (!this.manualEmail) return undefined;
|
|
472
|
-
const account = this.getAccount(this.manualEmail);
|
|
473
|
-
if (!account) {
|
|
474
|
-
this.manualEmail = undefined;
|
|
475
|
-
return undefined;
|
|
476
|
-
}
|
|
477
|
-
return account;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
hasManualAccount(): boolean {
|
|
481
|
-
return Boolean(this.getManualAccount());
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
getAvailableManualAccount(options?: {
|
|
485
|
-
now?: number;
|
|
486
|
-
excludeEmails?: Set<string>;
|
|
487
|
-
}): Account | undefined {
|
|
488
|
-
const now = options?.now ?? Date.now();
|
|
489
|
-
this.clearExpiredExhaustion(now);
|
|
490
|
-
const manual = this.getManualAccount();
|
|
491
|
-
if (!manual) return undefined;
|
|
492
|
-
if (options?.excludeEmails?.has(manual.email)) return undefined;
|
|
493
|
-
if (!isAccountAvailable(manual, now)) return undefined;
|
|
494
|
-
return manual;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
setActiveAccount(email: string): void {
|
|
498
|
-
const account = this.getAccount(email);
|
|
499
|
-
if (!account) return;
|
|
500
|
-
this.data.activeEmail = email;
|
|
501
|
-
account.lastUsed = Date.now();
|
|
502
|
-
this.save();
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
setManualAccount(email: string): void {
|
|
506
|
-
const account = this.getAccount(email);
|
|
507
|
-
if (!account) return;
|
|
508
|
-
this.manualEmail = email;
|
|
509
|
-
account.lastUsed = Date.now();
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
clearManualAccount(): void {
|
|
513
|
-
this.manualEmail = undefined;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
markExhausted(email: string, until: number): void {
|
|
517
|
-
const account = this.getAccount(email);
|
|
518
|
-
if (account) {
|
|
519
|
-
account.quotaExhaustedUntil = until;
|
|
520
|
-
this.save();
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
getCachedUsage(email: string): CodexUsageSnapshot | undefined {
|
|
525
|
-
return this.usageCache.get(email);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
async refreshUsageForAccount(
|
|
529
|
-
account: Account,
|
|
530
|
-
options?: { force?: boolean; signal?: AbortSignal },
|
|
531
|
-
): Promise<CodexUsageSnapshot | undefined> {
|
|
532
|
-
const cached = this.usageCache.get(account.email);
|
|
533
|
-
const now = Date.now();
|
|
534
|
-
if (
|
|
535
|
-
cached &&
|
|
536
|
-
!options?.force &&
|
|
537
|
-
now - cached.fetchedAt < USAGE_CACHE_TTL_MS
|
|
538
|
-
) {
|
|
539
|
-
return cached;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
try {
|
|
543
|
-
const token = await this.ensureValidToken(account);
|
|
544
|
-
const usage = await fetchCodexUsage(token, account.accountId, {
|
|
545
|
-
signal: options?.signal,
|
|
546
|
-
});
|
|
547
|
-
this.usageCache.set(account.email, usage);
|
|
548
|
-
return usage;
|
|
549
|
-
} catch (error) {
|
|
550
|
-
this.warningHandler?.(
|
|
551
|
-
`Multicodex: failed to fetch usage for ${account.email}: ${getErrorMessage(
|
|
552
|
-
error,
|
|
553
|
-
)}`,
|
|
554
|
-
);
|
|
555
|
-
return undefined;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
async refreshUsageForAllAccounts(options?: {
|
|
560
|
-
force?: boolean;
|
|
561
|
-
signal?: AbortSignal;
|
|
562
|
-
}): Promise<void> {
|
|
563
|
-
const accounts = this.getAccounts();
|
|
564
|
-
await Promise.all(
|
|
565
|
-
accounts.map((account) => this.refreshUsageForAccount(account, options)),
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
async refreshUsageIfStale(
|
|
570
|
-
accounts: Account[],
|
|
571
|
-
options?: { signal?: AbortSignal },
|
|
572
|
-
): Promise<void> {
|
|
573
|
-
const now = Date.now();
|
|
574
|
-
const stale = accounts.filter((account) => {
|
|
575
|
-
const cached = this.usageCache.get(account.email);
|
|
576
|
-
return !cached || now - cached.fetchedAt >= USAGE_CACHE_TTL_MS;
|
|
577
|
-
});
|
|
578
|
-
if (stale.length === 0) return;
|
|
579
|
-
await Promise.all(
|
|
580
|
-
stale.map((account) =>
|
|
581
|
-
this.refreshUsageForAccount(account, { force: true, ...options }),
|
|
582
|
-
),
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
async activateBestAccount(options?: {
|
|
587
|
-
excludeEmails?: Set<string>;
|
|
588
|
-
signal?: AbortSignal;
|
|
589
|
-
}): Promise<Account | undefined> {
|
|
590
|
-
const now = Date.now();
|
|
591
|
-
this.clearExpiredExhaustion(now);
|
|
592
|
-
const accounts = this.data.accounts;
|
|
593
|
-
await this.refreshUsageIfStale(accounts, options);
|
|
594
|
-
|
|
595
|
-
const selected = pickBestAccount(accounts, this.usageCache, {
|
|
596
|
-
excludeEmails: options?.excludeEmails,
|
|
597
|
-
now,
|
|
598
|
-
});
|
|
599
|
-
if (selected) {
|
|
600
|
-
this.setActiveAccount(selected.email);
|
|
601
|
-
}
|
|
602
|
-
return selected;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
async handleQuotaExceeded(
|
|
606
|
-
account: Account,
|
|
607
|
-
options?: { signal?: AbortSignal },
|
|
608
|
-
): Promise<void> {
|
|
609
|
-
const usage = await this.refreshUsageForAccount(account, {
|
|
610
|
-
force: true,
|
|
611
|
-
signal: options?.signal,
|
|
612
|
-
});
|
|
613
|
-
const now = Date.now();
|
|
614
|
-
const resetAt = getNextResetAt(usage);
|
|
615
|
-
const fallback = now + QUOTA_COOLDOWN_MS;
|
|
616
|
-
const until = resetAt && resetAt > now ? resetAt : fallback;
|
|
617
|
-
this.markExhausted(account.email, until);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
private clearExpiredExhaustion(now: number): void {
|
|
621
|
-
let changed = false;
|
|
622
|
-
for (const account of this.data.accounts) {
|
|
623
|
-
if (account.quotaExhaustedUntil && account.quotaExhaustedUntil <= now) {
|
|
624
|
-
account.quotaExhaustedUntil = undefined;
|
|
625
|
-
changed = true;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
if (changed) {
|
|
629
|
-
this.save();
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
async ensureValidToken(account: Account): Promise<string> {
|
|
634
|
-
// Valid for at least 5 more mins
|
|
635
|
-
if (Date.now() < account.expiresAt - 5 * 60 * 1000) {
|
|
636
|
-
return account.accessToken;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const result = await refreshOpenAICodexToken(account.refreshToken);
|
|
640
|
-
account.accessToken = result.access;
|
|
641
|
-
account.refreshToken = result.refresh;
|
|
642
|
-
account.expiresAt = result.expires;
|
|
643
|
-
const accountId =
|
|
644
|
-
typeof result.accountId === "string" ? result.accountId : undefined;
|
|
645
|
-
if (accountId) {
|
|
646
|
-
account.accountId = accountId;
|
|
647
|
-
}
|
|
648
|
-
this.save();
|
|
649
|
-
return account.accessToken;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// =============================================================================
|
|
654
|
-
// Extension Entry Point
|
|
655
|
-
// =============================================================================
|
|
656
|
-
|
|
657
|
-
type ApiProviderRef = NonNullable<ReturnType<typeof getApiProvider>>;
|
|
658
|
-
|
|
659
|
-
export function buildMulticodexProviderConfig(accountManager: AccountManager): {
|
|
660
|
-
baseUrl: string;
|
|
661
|
-
apiKey: string;
|
|
662
|
-
api: "openai-codex-responses";
|
|
663
|
-
streamSimple: (
|
|
664
|
-
model: Model<Api>,
|
|
665
|
-
context: Context,
|
|
666
|
-
options?: SimpleStreamOptions,
|
|
667
|
-
) => AssistantMessageEventStream;
|
|
668
|
-
models: ProviderModelDef[];
|
|
669
|
-
} {
|
|
670
|
-
const mirror = getOpenAICodexMirror();
|
|
671
|
-
const baseProvider = getApiProvider("openai-codex-responses");
|
|
672
|
-
if (!baseProvider) {
|
|
673
|
-
throw new Error(
|
|
674
|
-
"OpenAI Codex provider not available. Please update pi to include openai-codex support.",
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
return {
|
|
678
|
-
baseUrl: mirror.baseUrl,
|
|
679
|
-
apiKey: "managed-by-extension",
|
|
680
|
-
api: "openai-codex-responses",
|
|
681
|
-
streamSimple: createStreamWrapper(accountManager, baseProvider),
|
|
682
|
-
models: mirror.models,
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
export default function multicodexExtension(pi: ExtensionAPI) {
|
|
687
|
-
const accountManager = new AccountManager();
|
|
688
|
-
let lastContext: ExtensionContext | undefined;
|
|
689
|
-
|
|
690
|
-
accountManager.setWarningHandler((message) => {
|
|
691
|
-
if (lastContext) {
|
|
692
|
-
lastContext.ui.notify(message, "warning");
|
|
693
|
-
}
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
pi.registerProvider(
|
|
697
|
-
PROVIDER_ID,
|
|
698
|
-
buildMulticodexProviderConfig(accountManager),
|
|
699
|
-
);
|
|
700
|
-
|
|
701
|
-
// Login command
|
|
702
|
-
pi.registerCommand("multicodex-login", {
|
|
703
|
-
description: "Login to an OpenAI Codex account for the rotation pool",
|
|
704
|
-
handler: async (
|
|
705
|
-
args: string,
|
|
706
|
-
ctx: ExtensionCommandContext,
|
|
707
|
-
): Promise<void> => {
|
|
708
|
-
const email = args.trim();
|
|
709
|
-
if (!email) {
|
|
710
|
-
ctx.ui.notify(
|
|
711
|
-
"Please provide an email/identifier: /multicodex-login my@email.com",
|
|
712
|
-
"error",
|
|
713
|
-
);
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
try {
|
|
718
|
-
ctx.ui.notify(
|
|
719
|
-
`Starting login for ${email}... Check your browser.`,
|
|
720
|
-
"info",
|
|
721
|
-
);
|
|
722
|
-
|
|
723
|
-
const creds = await loginOpenAICodex({
|
|
724
|
-
onAuth: ({ url }) => {
|
|
725
|
-
void openLoginInBrowser(pi, ctx, url);
|
|
726
|
-
ctx.ui.notify(`Please open this URL to login: ${url}`, "info");
|
|
727
|
-
console.log(`[multicodex] Login URL: ${url}`);
|
|
728
|
-
},
|
|
729
|
-
onPrompt: async ({ message }) => (await ctx.ui.input(message)) || "",
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
accountManager.addOrUpdateAccount(email, creds);
|
|
733
|
-
ctx.ui.notify(`Successfully logged in as ${email}`, "info");
|
|
734
|
-
} catch (e) {
|
|
735
|
-
ctx.ui.notify(`Login failed: ${getErrorMessage(e)}`, "error");
|
|
736
|
-
}
|
|
737
|
-
},
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
// Switch active account
|
|
741
|
-
pi.registerCommand("multicodex-use", {
|
|
742
|
-
description: "Switch active Codex account for this session",
|
|
743
|
-
handler: async (
|
|
744
|
-
_args: string,
|
|
745
|
-
ctx: ExtensionCommandContext,
|
|
746
|
-
): Promise<void> => {
|
|
747
|
-
const accounts = accountManager.getAccounts();
|
|
748
|
-
if (accounts.length === 0) {
|
|
749
|
-
ctx.ui.notify(
|
|
750
|
-
"No accounts logged in. Use /multicodex-login first.",
|
|
751
|
-
"warning",
|
|
752
|
-
);
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const options = accounts.map(
|
|
757
|
-
(a) =>
|
|
758
|
-
a.email +
|
|
759
|
-
(a.quotaExhaustedUntil && a.quotaExhaustedUntil > Date.now()
|
|
760
|
-
? " (Quota)"
|
|
761
|
-
: ""),
|
|
762
|
-
);
|
|
763
|
-
const selected = await ctx.ui.select("Select Account", options);
|
|
764
|
-
if (!selected) return;
|
|
765
|
-
|
|
766
|
-
const email = selected.split(" ")[0];
|
|
767
|
-
accountManager.setManualAccount(email);
|
|
768
|
-
ctx.ui.notify(`Switched to ${email}`, "info");
|
|
769
|
-
},
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
pi.registerCommand("multicodex-status", {
|
|
773
|
-
description: "Show all Codex accounts and active status",
|
|
774
|
-
handler: async (
|
|
775
|
-
_args: string,
|
|
776
|
-
ctx: ExtensionCommandContext,
|
|
777
|
-
): Promise<void> => {
|
|
778
|
-
await accountManager.refreshUsageForAllAccounts();
|
|
779
|
-
const accounts = accountManager.getAccounts();
|
|
780
|
-
if (accounts.length === 0) {
|
|
781
|
-
ctx.ui.notify(
|
|
782
|
-
"No accounts logged in. Use /multicodex-login first.",
|
|
783
|
-
"warning",
|
|
784
|
-
);
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
const active = accountManager.getActiveAccount();
|
|
789
|
-
const options = accounts.map((account) => {
|
|
790
|
-
const usage = accountManager.getCachedUsage(account.email);
|
|
791
|
-
const isActive = active?.email === account.email;
|
|
792
|
-
const quotaHit =
|
|
793
|
-
account.quotaExhaustedUntil &&
|
|
794
|
-
account.quotaExhaustedUntil > Date.now();
|
|
795
|
-
const untouched = isUsageUntouched(usage) ? "untouched" : null;
|
|
796
|
-
const tags = [
|
|
797
|
-
isActive ? "active" : null,
|
|
798
|
-
quotaHit ? "quota" : null,
|
|
799
|
-
untouched,
|
|
800
|
-
]
|
|
801
|
-
.filter(Boolean)
|
|
802
|
-
.join(", ");
|
|
803
|
-
const suffix = tags ? ` (${tags})` : "";
|
|
804
|
-
const primaryUsed = usage?.primary?.usedPercent;
|
|
805
|
-
const secondaryUsed = usage?.secondary?.usedPercent;
|
|
806
|
-
const primaryReset = usage?.primary?.resetAt;
|
|
807
|
-
const secondaryReset = usage?.secondary?.resetAt;
|
|
808
|
-
const primaryLabel =
|
|
809
|
-
primaryUsed === undefined ? "unknown" : `${Math.round(primaryUsed)}%`;
|
|
810
|
-
const secondaryLabel =
|
|
811
|
-
secondaryUsed === undefined
|
|
812
|
-
? "unknown"
|
|
813
|
-
: `${Math.round(secondaryUsed)}%`;
|
|
814
|
-
const usageSummary = `5h ${primaryLabel} reset:${formatResetAt(primaryReset)} | weekly ${secondaryLabel} reset:${formatResetAt(secondaryReset)}`;
|
|
815
|
-
return `${isActive ? "•" : " "} ${account.email}${suffix} - ${usageSummary}`;
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
await ctx.ui.select("MultiCodex Accounts", options);
|
|
819
|
-
},
|
|
820
|
-
});
|
|
821
|
-
|
|
822
|
-
// Hooks
|
|
823
|
-
pi.on("session_start", (_event: unknown, ctx: ExtensionContext) => {
|
|
824
|
-
lastContext = ctx;
|
|
825
|
-
if (accountManager.getAccounts().length === 0) return;
|
|
826
|
-
void (async () => {
|
|
827
|
-
await accountManager.refreshUsageForAllAccounts({ force: true });
|
|
828
|
-
const manual = accountManager.getAvailableManualAccount();
|
|
829
|
-
if (manual) return;
|
|
830
|
-
if (accountManager.hasManualAccount()) {
|
|
831
|
-
accountManager.clearManualAccount();
|
|
832
|
-
}
|
|
833
|
-
await accountManager.activateBestAccount();
|
|
834
|
-
})();
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
pi.on(
|
|
838
|
-
"session_switch",
|
|
839
|
-
(event: { reason?: string }, ctx: ExtensionContext) => {
|
|
840
|
-
lastContext = ctx;
|
|
841
|
-
if (event.reason === "new") {
|
|
842
|
-
void (async () => {
|
|
843
|
-
await accountManager.refreshUsageForAllAccounts({ force: true });
|
|
844
|
-
const manual = accountManager.getAvailableManualAccount();
|
|
845
|
-
if (manual) return;
|
|
846
|
-
if (accountManager.hasManualAccount()) {
|
|
847
|
-
accountManager.clearManualAccount();
|
|
848
|
-
}
|
|
849
|
-
await accountManager.activateBestAccount();
|
|
850
|
-
})();
|
|
851
|
-
}
|
|
852
|
-
},
|
|
853
|
-
);
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// =============================================================================
|
|
857
|
-
// Stream Wrapper
|
|
858
|
-
// =============================================================================
|
|
859
|
-
|
|
860
|
-
const MAX_ROTATION_RETRIES = 5;
|
|
861
|
-
|
|
862
|
-
export function createStreamWrapper(
|
|
863
|
-
accountManager: AccountManager,
|
|
864
|
-
baseProvider: ApiProviderRef,
|
|
865
|
-
) {
|
|
866
|
-
return (
|
|
867
|
-
model: Model<Api>,
|
|
868
|
-
context: Context,
|
|
869
|
-
options?: SimpleStreamOptions,
|
|
870
|
-
): AssistantMessageEventStream => {
|
|
871
|
-
const stream = createAssistantMessageEventStream();
|
|
872
|
-
|
|
873
|
-
(async () => {
|
|
874
|
-
try {
|
|
875
|
-
const excludedEmails = new Set<string>();
|
|
876
|
-
for (let attempt = 0; attempt <= MAX_ROTATION_RETRIES; attempt++) {
|
|
877
|
-
const now = Date.now();
|
|
878
|
-
const manual = accountManager.getAvailableManualAccount({
|
|
879
|
-
excludeEmails: excludedEmails,
|
|
880
|
-
now,
|
|
881
|
-
});
|
|
882
|
-
const usingManual = Boolean(manual);
|
|
883
|
-
let account = manual;
|
|
884
|
-
if (!account) {
|
|
885
|
-
if (accountManager.hasManualAccount()) {
|
|
886
|
-
accountManager.clearManualAccount();
|
|
887
|
-
}
|
|
888
|
-
account = await accountManager.activateBestAccount({
|
|
889
|
-
excludeEmails: excludedEmails,
|
|
890
|
-
signal: options?.signal,
|
|
891
|
-
});
|
|
892
|
-
}
|
|
893
|
-
if (!account) {
|
|
894
|
-
throw new Error(
|
|
895
|
-
"No available Multicodex accounts. Please use /multicodex-login.",
|
|
896
|
-
);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
const token = await accountManager.ensureValidToken(account);
|
|
900
|
-
|
|
901
|
-
const abortController = createLinkedAbortController(options?.signal);
|
|
902
|
-
|
|
903
|
-
const internalModel: Model<"openai-codex-responses"> = {
|
|
904
|
-
...(model as Model<"openai-codex-responses">),
|
|
905
|
-
provider: "openai-codex",
|
|
906
|
-
api: "openai-codex-responses",
|
|
907
|
-
};
|
|
908
|
-
|
|
909
|
-
const inner = baseProvider.streamSimple(
|
|
910
|
-
{
|
|
911
|
-
...internalModel,
|
|
912
|
-
headers: {
|
|
913
|
-
...(internalModel.headers || {}),
|
|
914
|
-
"X-Multicodex-Account": account.email,
|
|
915
|
-
},
|
|
916
|
-
},
|
|
917
|
-
context,
|
|
918
|
-
{
|
|
919
|
-
...options,
|
|
920
|
-
apiKey: token,
|
|
921
|
-
signal: abortController.signal,
|
|
922
|
-
},
|
|
923
|
-
);
|
|
924
|
-
|
|
925
|
-
let forwardedAny = false;
|
|
926
|
-
let retry = false;
|
|
927
|
-
|
|
928
|
-
for await (const event of inner) {
|
|
929
|
-
if (event.type === "error") {
|
|
930
|
-
const msg = event.error.errorMessage || "";
|
|
931
|
-
const isQuota = isQuotaErrorMessage(msg);
|
|
932
|
-
|
|
933
|
-
if (isQuota && !forwardedAny && attempt < MAX_ROTATION_RETRIES) {
|
|
934
|
-
await accountManager.handleQuotaExceeded(account, {
|
|
935
|
-
signal: options?.signal,
|
|
936
|
-
});
|
|
937
|
-
if (usingManual) {
|
|
938
|
-
accountManager.clearManualAccount();
|
|
939
|
-
}
|
|
940
|
-
excludedEmails.add(account.email);
|
|
941
|
-
abortController.abort();
|
|
942
|
-
retry = true;
|
|
943
|
-
break;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
stream.push(withProvider(event, model.provider));
|
|
947
|
-
stream.end();
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
forwardedAny = true;
|
|
952
|
-
stream.push(withProvider(event, model.provider));
|
|
953
|
-
|
|
954
|
-
if (event.type === "done") {
|
|
955
|
-
stream.end();
|
|
956
|
-
return;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
if (retry) {
|
|
961
|
-
continue;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// If inner finished without done/error, stop to avoid hanging.
|
|
965
|
-
stream.end();
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
} catch (e) {
|
|
969
|
-
const message = getErrorMessage(e);
|
|
970
|
-
const errorEvent: AssistantMessageEvent = {
|
|
971
|
-
type: "error",
|
|
972
|
-
reason: "error",
|
|
973
|
-
error: createErrorAssistantMessage(
|
|
974
|
-
model,
|
|
975
|
-
`Multicodex failed: ${message}`,
|
|
976
|
-
),
|
|
977
|
-
};
|
|
978
|
-
stream.push(withProvider(errorEvent, model.provider));
|
|
979
|
-
stream.end();
|
|
980
|
-
}
|
|
981
|
-
})();
|
|
982
|
-
|
|
983
|
-
return stream;
|
|
984
|
-
};
|
|
985
|
-
}
|
|
1
|
+
export { AccountManager } from "./account-manager";
|
|
2
|
+
export { default } from "./extension";
|
|
3
|
+
export {
|
|
4
|
+
buildMulticodexProviderConfig,
|
|
5
|
+
getOpenAICodexMirror,
|
|
6
|
+
PROVIDER_ID,
|
|
7
|
+
type ProviderModelDef,
|
|
8
|
+
} from "./provider";
|
|
9
|
+
export { isQuotaErrorMessage } from "./quota";
|
|
10
|
+
export {
|
|
11
|
+
isAccountAvailable,
|
|
12
|
+
pickBestAccount,
|
|
13
|
+
} from "./selection";
|
|
14
|
+
export type { Account } from "./storage";
|
|
15
|
+
export { createStreamWrapper } from "./stream-wrapper";
|
|
16
|
+
export type { CodexUsageSnapshot } from "./usage";
|
|
17
|
+
export {
|
|
18
|
+
formatResetAt,
|
|
19
|
+
getNextResetAt,
|
|
20
|
+
getWeeklyResetAt,
|
|
21
|
+
isUsageUntouched,
|
|
22
|
+
parseCodexUsageResponse,
|
|
23
|
+
} from "./usage";
|