@raingor/pi-web-switch 0.4.3 → 0.4.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/dist/index.html +4 -4
- package/index.html +2 -2
- package/package.json +5 -3
- package/public/manifest.webmanifest +2 -2
- package/public/sw.js +51 -51
- package/server/pi-reader.ts +326 -19
- package/src/App.tsx +2 -0
- package/src/components/dashboard/DashboardPage.tsx +341 -27
- package/src/components/layout/AppShell.tsx +45 -13
- package/src/components/layout/Sidebar.tsx +78 -74
- package/src/components/providers/ProvidersModelsPage.tsx +125 -131
- package/src/components/sessions/MemoryPage.tsx +32 -12
- package/src/components/sessions/SessionsPage.tsx +18 -4
- package/src/components/settings/SettingsPage.tsx +6 -1
- package/src/components/speedtest/ModelSpeedTestPage.tsx +429 -0
- package/src/components/ui/EmptyState.tsx +7 -6
- package/src/components/ui/Modal.tsx +33 -25
- package/src/components/ui/StatCard.tsx +10 -13
- package/src/data/builtin-providers.test.ts +109 -0
- package/src/data/builtin-providers.ts +67 -44
- package/src/data/model-catalog.test.ts +122 -0
- package/src/data/model-catalog.ts +697 -478
- package/src/index.css +624 -210
- package/src/lib/translations/en.ts +88 -9
- package/src/lib/translations/ja.ts +88 -9
- package/src/lib/translations/zh-CN.ts +88 -9
- package/src/lib/translations/zh-TW.ts +87 -9
- package/src/main.tsx +99 -22
- package/vite.config.ts +62 -137
- package/src/data/mock-config.ts +0 -247
- package/src/data/mock-usage.ts +0 -151
package/src/main.tsx
CHANGED
|
@@ -39,10 +39,10 @@ function useThemeSync(initialized: boolean) {
|
|
|
39
39
|
function LoadingScreen() {
|
|
40
40
|
const { t } = useTranslation();
|
|
41
41
|
return (
|
|
42
|
-
<div className="
|
|
43
|
-
<div className="
|
|
44
|
-
<div className="
|
|
45
|
-
<p
|
|
42
|
+
<div className="loading-console">
|
|
43
|
+
<div className="loading-core">
|
|
44
|
+
<div className="loading-ring" />
|
|
45
|
+
<p>{t("loading.config")}</p>
|
|
46
46
|
</div>
|
|
47
47
|
</div>
|
|
48
48
|
);
|
|
@@ -51,8 +51,8 @@ function LoadingScreen() {
|
|
|
51
51
|
function ErrorScreen({ error, onRetry }: { error: string; onRetry: () => void }) {
|
|
52
52
|
const { t } = useTranslation();
|
|
53
53
|
return (
|
|
54
|
-
<div className="
|
|
55
|
-
<div className="flex flex-col items-center gap-4
|
|
54
|
+
<div className="loading-console">
|
|
55
|
+
<div className="tech-panel relative z-10 flex max-w-md flex-col items-center gap-4 rounded-xl border p-8 text-center">
|
|
56
56
|
<p className="text-lg font-semibold text-red-400">{t("loading.error_title")}</p>
|
|
57
57
|
<p className="text-sm text-gray-400">{error}</p>
|
|
58
58
|
<button
|
|
@@ -112,24 +112,101 @@ function applySavedFontSize() {
|
|
|
112
112
|
applySavedZoom();
|
|
113
113
|
function applySavedZoom() {
|
|
114
114
|
const saved = Number(localStorage.getItem("pi-ui-zoom"));
|
|
115
|
+
document.documentElement.style.zoom = "";
|
|
115
116
|
if (saved >= 50 && saved <= 200) {
|
|
116
|
-
document.documentElement.style.zoom
|
|
117
|
+
document.documentElement.style.setProperty("--ui-zoom", String(saved / 100));
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
121
|
+
// ─── Service worker lifecycle ───────────────────────────
|
|
122
|
+
// A production SW can remain registered when the same localhost origin is
|
|
123
|
+
// later opened with Vite dev. Actively remove it in development; otherwise a
|
|
124
|
+
// normal Cmd+R may be served an old HTML shell while Cmd+Shift+R bypasses it.
|
|
125
|
+
const APP_CACHE_PREFIX = "pi-web-switch-";
|
|
126
|
+
const DEV_SW_CLEANUP_KEY = "pi-web-switch-dev-sw-cleaned";
|
|
127
|
+
|
|
128
|
+
async function clearAppCaches() {
|
|
129
|
+
if (!("caches" in window)) return;
|
|
130
|
+
const keys = await caches.keys();
|
|
131
|
+
await Promise.all(
|
|
132
|
+
keys.filter((key) => key.startsWith(APP_CACHE_PREFIX)).map((key) => caches.delete(key))
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getAppBaseUrl() {
|
|
137
|
+
const moduleScript = Array.from(document.scripts).find(
|
|
138
|
+
(script) => script.type === "module" && script.src
|
|
139
|
+
);
|
|
140
|
+
return new URL("../", moduleScript?.src ?? document.baseURI);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function configureServiceWorker() {
|
|
144
|
+
if (!("serviceWorker" in navigator)) return;
|
|
145
|
+
|
|
146
|
+
const appBaseUrl = getAppBaseUrl();
|
|
147
|
+
|
|
148
|
+
if (import.meta.env.DEV) {
|
|
149
|
+
try {
|
|
150
|
+
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
151
|
+
const appRegistrations = registrations.filter(
|
|
152
|
+
(registration) => registration.scope === appBaseUrl.href
|
|
153
|
+
);
|
|
154
|
+
const hadController = Boolean(navigator.serviceWorker.controller);
|
|
155
|
+
const removed = await Promise.all(
|
|
156
|
+
appRegistrations.map((registration) => registration.unregister())
|
|
157
|
+
);
|
|
158
|
+
await clearAppCaches();
|
|
159
|
+
|
|
160
|
+
// If this document was already controlled by the old production worker,
|
|
161
|
+
// reload once after unregistering it so the first API/static requests are
|
|
162
|
+
// also guaranteed to bypass the old worker. sessionStorage prevents a
|
|
163
|
+
// reload loop if the browser reports the controller for one extra tick.
|
|
164
|
+
if (
|
|
165
|
+
hadController &&
|
|
166
|
+
removed.some(Boolean) &&
|
|
167
|
+
sessionStorage.getItem(DEV_SW_CLEANUP_KEY) !== "1"
|
|
168
|
+
) {
|
|
169
|
+
sessionStorage.setItem(DEV_SW_CLEANUP_KEY, "1");
|
|
170
|
+
window.location.reload();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
sessionStorage.removeItem(DEV_SW_CLEANUP_KEY);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
// Cache cleanup must never prevent React from mounting.
|
|
176
|
+
console.warn("SW cleanup failed:", err);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
// Resolve from the built JS asset directory so this also works when the
|
|
183
|
+
// app is hosted under a subdirectory instead of the domain root.
|
|
184
|
+
const swUrl = new URL("sw.js", appBaseUrl);
|
|
185
|
+
const registration = await navigator.serviceWorker.register(swUrl, {
|
|
186
|
+
updateViaCache: "none",
|
|
133
187
|
});
|
|
134
|
-
|
|
135
|
-
|
|
188
|
+
// Check on every application start instead of waiting for the browser's
|
|
189
|
+
// periodic service-worker update window.
|
|
190
|
+
await registration.update();
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.warn("SW registration failed:", err);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function bootstrap() {
|
|
197
|
+
if (import.meta.env.DEV) {
|
|
198
|
+
await configureServiceWorker();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
createRoot(document.getElementById("root")!).render(
|
|
202
|
+
<StrictMode>
|
|
203
|
+
<Root />
|
|
204
|
+
</StrictMode>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (import.meta.env.PROD) {
|
|
208
|
+
void configureServiceWorker();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
void bootstrap();
|
package/vite.config.ts
CHANGED
|
@@ -23,7 +23,7 @@ function piApiPlugin(): Plugin {
|
|
|
23
23
|
/* ignore warm-up failure */
|
|
24
24
|
}
|
|
25
25
|
try {
|
|
26
|
-
pi.
|
|
26
|
+
pi.readChatgptUsage();
|
|
27
27
|
} catch {
|
|
28
28
|
/* ignore warm-up failure */
|
|
29
29
|
}
|
|
@@ -49,6 +49,48 @@ function piApiPlugin(): Plugin {
|
|
|
49
49
|
res.setHeader("Content-Type", "application/json");
|
|
50
50
|
res.end(JSON.stringify(data ?? {}));
|
|
51
51
|
},
|
|
52
|
+
"GET /api/pi/official-usage-config"(_, res) {
|
|
53
|
+
const config = pi.readOfficialUsageConfig();
|
|
54
|
+
res.setHeader("Content-Type", "application/json");
|
|
55
|
+
res.end(JSON.stringify({
|
|
56
|
+
endpoint: config.endpoint,
|
|
57
|
+
authMode: config.authMode,
|
|
58
|
+
keyCount: config.apiKeys.length,
|
|
59
|
+
maskedKeys: config.apiKeys.map((key: string) => key.length > 8 ? `${key.slice(0, 4)}••••${key.slice(-4)}` : "••••••••"),
|
|
60
|
+
}));
|
|
61
|
+
},
|
|
62
|
+
"POST /api/pi/official-usage-refresh"(_, res) {
|
|
63
|
+
pi.queryOfficialUsage(pi.readOfficialUsageConfig()).then((usage) => {
|
|
64
|
+
res.setHeader("Content-Type", "application/json");
|
|
65
|
+
res.end(JSON.stringify({ success: true, usage }));
|
|
66
|
+
}).catch((error) => {
|
|
67
|
+
res.statusCode = 400;
|
|
68
|
+
res.setHeader("Content-Type", "application/json");
|
|
69
|
+
res.end(JSON.stringify({ success: false, error: error instanceof Error ? error.message : "Official usage query failed" }));
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
"POST /api/pi/official-usage-query"(req, res) {
|
|
73
|
+
let body = "";
|
|
74
|
+
req.on("data", (chunk: string) => (body += chunk));
|
|
75
|
+
req.on("end", async () => {
|
|
76
|
+
try {
|
|
77
|
+
const input = JSON.parse(body);
|
|
78
|
+
const config = {
|
|
79
|
+
endpoint: typeof input.endpoint === "string" ? input.endpoint : "",
|
|
80
|
+
apiKeys: Array.isArray(input.apiKeys) ? input.apiKeys : [],
|
|
81
|
+
authMode: input.authMode,
|
|
82
|
+
};
|
|
83
|
+
const usage = await pi.queryOfficialUsage(config);
|
|
84
|
+
const saved = pi.writeOfficialUsageConfig(config);
|
|
85
|
+
res.setHeader("Content-Type", "application/json");
|
|
86
|
+
res.end(JSON.stringify({ success: saved, usage, error: saved ? undefined : "Failed to save configuration" }));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
res.statusCode = 400;
|
|
89
|
+
res.setHeader("Content-Type", "application/json");
|
|
90
|
+
res.end(JSON.stringify({ success: false, error: error instanceof Error ? error.message : "Official usage query failed" }));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
},
|
|
52
94
|
"POST /api/pi/auth"(req, res) {
|
|
53
95
|
let body = "";
|
|
54
96
|
req.on("data", (chunk: string) => (body += chunk));
|
|
@@ -290,6 +332,14 @@ function piApiPlugin(): Plugin {
|
|
|
290
332
|
// Strip query string
|
|
291
333
|
const pathOnly = url.split("?")[0];
|
|
292
334
|
|
|
335
|
+
// Automatically archive sessions that have been inactive for more
|
|
336
|
+
// than two weeks. This is recoverable through the existing trash tab.
|
|
337
|
+
if (method === "POST" && pathOnly === "/api/pi/sessions/auto-trash") {
|
|
338
|
+
const result = pi.autoTrashStaleSessions(14);
|
|
339
|
+
res.setHeader("Content-Type", "application/json");
|
|
340
|
+
return res.end(JSON.stringify(result));
|
|
341
|
+
}
|
|
342
|
+
|
|
293
343
|
// Handle DELETE /api/pi/session?path=... (move to trash) and /api/pi/trash?path=... (permanent)
|
|
294
344
|
if (method === "DELETE" && (pathOnly === "/api/pi/session" || pathOnly === "/api/pi/trash")) {
|
|
295
345
|
const parsedUrl = new URL(url, "http://localhost");
|
|
@@ -313,121 +363,11 @@ function piApiPlugin(): Plugin {
|
|
|
313
363
|
const range = parsedUrl.searchParams.get("range") || "today";
|
|
314
364
|
const fromParam = parsedUrl.searchParams.get("from") || "";
|
|
315
365
|
const toParam = parsedUrl.searchParams.get("to") || "";
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const localDateStr = (dt: Date) =>
|
|
320
|
-
new Intl.DateTimeFormat("en-CA", {
|
|
321
|
-
timeZone: "Asia/Shanghai",
|
|
322
|
-
year: "numeric",
|
|
323
|
-
month: "2-digit",
|
|
324
|
-
day: "2-digit",
|
|
325
|
-
}).format(dt);
|
|
326
|
-
let fromDate: string;
|
|
327
|
-
let toDate = localDateStr(now);
|
|
328
|
-
|
|
329
|
-
if (range === "today") {
|
|
330
|
-
fromDate = toDate;
|
|
331
|
-
} else if (range === "7d") {
|
|
332
|
-
const d = new Date(now); d.setDate(d.getDate() - 6); fromDate = localDateStr(d);
|
|
333
|
-
} else if (range === "30d") {
|
|
334
|
-
const d = new Date(now); d.setDate(d.getDate() - 29); fromDate = localDateStr(d);
|
|
335
|
-
} else if (range === "custom" && fromParam) {
|
|
336
|
-
fromDate = fromParam;
|
|
337
|
-
if (toParam) toDate = toParam;
|
|
338
|
-
} else {
|
|
339
|
-
fromDate = toDate;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const allRecords = pi.readAllUsage();
|
|
343
|
-
const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
|
|
344
|
-
res.setHeader("Content-Type", "application/json");
|
|
345
|
-
return res.end(JSON.stringify(usage));
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Handle GET /api/pi/cindy-usage-range?range=today|7d|30d|custom&from=...&to=...
|
|
349
|
-
if (method === "GET" && pathOnly === "/api/pi/cindy-usage-range") {
|
|
350
|
-
const parsedUrl = new URL(url, "http://localhost");
|
|
351
|
-
const range = parsedUrl.searchParams.get("range") || "today";
|
|
352
|
-
const fromParam = parsedUrl.searchParams.get("from") || "";
|
|
353
|
-
const toParam = parsedUrl.searchParams.get("to") || "";
|
|
354
|
-
|
|
355
|
-
const now = new Date();
|
|
356
|
-
// Date buckets follow China time (UTC+8) regardless of system timezone
|
|
357
|
-
const localDateStr = (dt: Date) =>
|
|
358
|
-
new Intl.DateTimeFormat("en-CA", {
|
|
359
|
-
timeZone: "Asia/Shanghai",
|
|
360
|
-
year: "numeric",
|
|
361
|
-
month: "2-digit",
|
|
362
|
-
day: "2-digit",
|
|
363
|
-
}).format(dt);
|
|
364
|
-
let fromDate: string;
|
|
365
|
-
let toDate = localDateStr(now);
|
|
366
|
-
|
|
367
|
-
if (range === "today") {
|
|
368
|
-
fromDate = toDate;
|
|
369
|
-
} else if (range === "7d") {
|
|
370
|
-
const d = new Date(now); d.setDate(d.getDate() - 6); fromDate = localDateStr(d);
|
|
371
|
-
} else if (range === "30d") {
|
|
372
|
-
const d = new Date(now); d.setDate(d.getDate() - 29); fromDate = localDateStr(d);
|
|
373
|
-
} else if (range === "custom" && fromParam) {
|
|
374
|
-
fromDate = fromParam;
|
|
375
|
-
if (toParam) toDate = toParam;
|
|
376
|
-
} else {
|
|
377
|
-
fromDate = toDate;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const allRecords = pi.readCindyUsage();
|
|
381
|
-
const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
|
|
382
|
-
res.setHeader("Content-Type", "application/json");
|
|
383
|
-
return res.end(JSON.stringify(usage));
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Handle GET /api/pi/claude-usage-range?range=today|7d|30d|custom&from=...&to=...
|
|
387
|
-
if (method === "GET" && pathOnly === "/api/pi/claude-usage-range") {
|
|
388
|
-
const parsedUrl = new URL(url, "http://localhost");
|
|
389
|
-
const range = parsedUrl.searchParams.get("range") || "today";
|
|
390
|
-
const fromParam = parsedUrl.searchParams.get("from") || "";
|
|
391
|
-
const toParam = parsedUrl.searchParams.get("to") || "";
|
|
392
|
-
|
|
393
|
-
const now = new Date();
|
|
394
|
-
// Date buckets follow China time (UTC+8) regardless of system timezone
|
|
395
|
-
const localDateStr = (dt: Date) =>
|
|
396
|
-
new Intl.DateTimeFormat("en-CA", {
|
|
397
|
-
timeZone: "Asia/Shanghai",
|
|
398
|
-
year: "numeric",
|
|
399
|
-
month: "2-digit",
|
|
400
|
-
day: "2-digit",
|
|
401
|
-
}).format(dt);
|
|
402
|
-
let fromDate: string;
|
|
403
|
-
let toDate = localDateStr(now);
|
|
404
|
-
|
|
405
|
-
if (range === "today") {
|
|
406
|
-
fromDate = toDate;
|
|
407
|
-
} else if (range === "7d") {
|
|
408
|
-
const d = new Date(now); d.setDate(d.getDate() - 6); fromDate = localDateStr(d);
|
|
409
|
-
} else if (range === "30d") {
|
|
410
|
-
const d = new Date(now); d.setDate(d.getDate() - 29); fromDate = localDateStr(d);
|
|
411
|
-
} else if (range === "custom" && fromParam) {
|
|
412
|
-
fromDate = fromParam;
|
|
413
|
-
if (toParam) toDate = toParam;
|
|
414
|
-
} else {
|
|
415
|
-
fromDate = toDate;
|
|
366
|
+
if (parsedUrl.searchParams.get("refresh") === "1") {
|
|
367
|
+
pi.clearUsageCache();
|
|
368
|
+
pi.clearChatgptUsageCache();
|
|
416
369
|
}
|
|
417
370
|
|
|
418
|
-
const allRecords = pi.readClaudeUsage();
|
|
419
|
-
const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
|
|
420
|
-
res.setHeader("Content-Type", "application/json");
|
|
421
|
-
return res.end(JSON.stringify(usage));
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Handle GET /api/pi/codex-usage-range?range=today|7d|30d|custom&from=...&to=...
|
|
425
|
-
if (method === "GET" && pathOnly === "/api/pi/codex-usage-range") {
|
|
426
|
-
const parsedUrl = new URL(url, "http://localhost");
|
|
427
|
-
const range = parsedUrl.searchParams.get("range") || "today";
|
|
428
|
-
const fromParam = parsedUrl.searchParams.get("from") || "";
|
|
429
|
-
const toParam = parsedUrl.searchParams.get("to") || "";
|
|
430
|
-
|
|
431
371
|
const now = new Date();
|
|
432
372
|
// Date buckets follow China time (UTC+8) regardless of system timezone
|
|
433
373
|
const localDateStr = (dt: Date) =>
|
|
@@ -453,7 +393,7 @@ function piApiPlugin(): Plugin {
|
|
|
453
393
|
fromDate = toDate;
|
|
454
394
|
}
|
|
455
395
|
|
|
456
|
-
const allRecords = pi.
|
|
396
|
+
const allRecords = pi.readAllUsage();
|
|
457
397
|
const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
|
|
458
398
|
res.setHeader("Content-Type", "application/json");
|
|
459
399
|
return res.end(JSON.stringify(usage));
|
|
@@ -480,33 +420,18 @@ function piApiPlugin(): Plugin {
|
|
|
480
420
|
return { fromDate, toDate };
|
|
481
421
|
};
|
|
482
422
|
|
|
483
|
-
// Handle GET /api/pi/
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const range = parsedUrl.searchParams.get("range") || "today";
|
|
487
|
-
const fromParam = parsedUrl.searchParams.get("from") || "";
|
|
488
|
-
const toParam = parsedUrl.searchParams.get("to") || "";
|
|
489
|
-
const { fromDate, toDate } = resolveDateRange(range, fromParam, toParam);
|
|
490
|
-
const allRecords = pi.readAllCombinedUsage();
|
|
491
|
-
const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
|
|
492
|
-
res.setHeader("Content-Type", "application/json");
|
|
493
|
-
return res.end(JSON.stringify(usage));
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Handle provider-filtered endpoints: /api/pi/{provider}-usage-range
|
|
497
|
-
// Copilot is read from the local ~/.copilot/session-store.db (no
|
|
498
|
-
// GitHub REST API, no PAT), so it shares the same sync pipeline as
|
|
499
|
-
// the other local sources.
|
|
500
|
-
const providerMatch = pathOnly.match(/^\/api\/pi\/(atomcode|copilot|opencode|gemini|grok)-usage-range$/);
|
|
501
|
-
if (method === "GET" && providerMatch) {
|
|
502
|
-
const providerId = providerMatch[1]!;
|
|
423
|
+
// Handle GET /api/pi/chatgpt-usage-range using local Codex Desktop
|
|
424
|
+
// rollout JSONL files under ~/.codex/sessions and archived_sessions.
|
|
425
|
+
if (method === "GET" && pathOnly === "/api/pi/chatgpt-usage-range") {
|
|
503
426
|
const parsedUrl = new URL(url, "http://localhost");
|
|
504
427
|
const range = parsedUrl.searchParams.get("range") || "today";
|
|
505
428
|
const fromParam = parsedUrl.searchParams.get("from") || "";
|
|
506
429
|
const toParam = parsedUrl.searchParams.get("to") || "";
|
|
430
|
+
if (parsedUrl.searchParams.get("refresh") === "1") {
|
|
431
|
+
pi.clearChatgptUsageCache();
|
|
432
|
+
}
|
|
507
433
|
const { fromDate, toDate } = resolveDateRange(range, fromParam, toParam);
|
|
508
|
-
const
|
|
509
|
-
const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
|
|
434
|
+
const usage = pi.getUsageByRange(pi.readChatgptUsage(), fromDate, toDate);
|
|
510
435
|
res.setHeader("Content-Type", "application/json");
|
|
511
436
|
return res.end(JSON.stringify(usage));
|
|
512
437
|
}
|
package/src/data/mock-config.ts
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import type { PiConfig, Provider, Model, PiSettings, PiAuth, PiModelsJson } from "@/types";
|
|
2
|
-
|
|
3
|
-
// ─── Built-in Providers ───────────────────────────────────
|
|
4
|
-
|
|
5
|
-
const BUILTIN_PROVIDERS: Provider[] = [
|
|
6
|
-
{
|
|
7
|
-
id: "anthropic",
|
|
8
|
-
name: "Anthropic",
|
|
9
|
-
type: "builtin",
|
|
10
|
-
api: "anthropic-messages",
|
|
11
|
-
hasAuth: true,
|
|
12
|
-
authMethod: "env",
|
|
13
|
-
models: [
|
|
14
|
-
{ id: "claude-sonnet-4", name: "Claude 4 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: true },
|
|
15
|
-
{ id: "claude-sonnet-4-5", name: "Claude 4.5 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: true },
|
|
16
|
-
{ id: "claude-opus-4", name: "Claude 4 Opus", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 }, enabled: false },
|
|
17
|
-
{ id: "claude-haiku-3-5", name: "Claude 3.5 Haiku", reasoning: false, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 }, enabled: true },
|
|
18
|
-
],
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
id: "openai",
|
|
22
|
-
name: "OpenAI",
|
|
23
|
-
type: "builtin",
|
|
24
|
-
api: "openai-completions",
|
|
25
|
-
hasAuth: true,
|
|
26
|
-
authMethod: "env",
|
|
27
|
-
models: [
|
|
28
|
-
{ id: "gpt-4o", name: "GPT-4o", reasoning: false, input: ["text", "image"], contextWindow: 128000, maxTokens: 16384, cost: { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 3.75 }, enabled: true },
|
|
29
|
-
{ id: "gpt-4o-mini", name: "GPT-4o Mini", reasoning: false, input: ["text", "image"], contextWindow: 128000, maxTokens: 16384, cost: { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0.225 }, enabled: true },
|
|
30
|
-
{ id: "gpt-5.1", name: "GPT-5.1", reasoning: true, input: ["text", "image"], contextWindow: 256000, maxTokens: 65536, cost: { input: 10, output: 40, cacheRead: 5, cacheWrite: 10 }, enabled: false },
|
|
31
|
-
{ id: "o3-mini", name: "o3-mini", reasoning: true, input: ["text"], contextWindow: 200000, maxTokens: 100000, cost: { input: 1.1, output: 4.4, cacheRead: 0.55, cacheWrite: 1.65 }, enabled: false },
|
|
32
|
-
],
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
id: "deepseek",
|
|
36
|
-
name: "DeepSeek",
|
|
37
|
-
type: "builtin",
|
|
38
|
-
api: "openai-completions",
|
|
39
|
-
hasAuth: true,
|
|
40
|
-
authMethod: "env",
|
|
41
|
-
models: [
|
|
42
|
-
{ id: "deepseek-chat", name: "DeepSeek V3", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.27, output: 1.1, cacheRead: 0.07, cacheWrite: 0.27 }, enabled: true },
|
|
43
|
-
{ id: "deepseek-reasoner", name: "DeepSeek R1", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.55, output: 2.19, cacheRead: 0.14, cacheWrite: 0.55 }, enabled: true },
|
|
44
|
-
],
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
id: "opencode",
|
|
48
|
-
name: "OpenCode",
|
|
49
|
-
type: "builtin",
|
|
50
|
-
api: "openai-completions",
|
|
51
|
-
hasAuth: true,
|
|
52
|
-
authMethod: "file",
|
|
53
|
-
models: [
|
|
54
|
-
{ id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash (Free)", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, enabled: true },
|
|
55
|
-
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.3, output: 0.6, cacheRead: 0.15, cacheWrite: 0.3 }, enabled: true },
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
id: "opencode-go",
|
|
60
|
-
name: "OpenCode Go",
|
|
61
|
-
type: "builtin",
|
|
62
|
-
api: "openai-completions",
|
|
63
|
-
hasAuth: true,
|
|
64
|
-
authMethod: "file",
|
|
65
|
-
models: [
|
|
66
|
-
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.3, output: 0.6, cacheRead: 0.15, cacheWrite: 0.3 }, enabled: true },
|
|
67
|
-
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 2, output: 8, cacheRead: 1, cacheWrite: 2 }, enabled: true },
|
|
68
|
-
{ id: "glm-5.1", name: "GLM 5.1", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.5, output: 2, cacheRead: 0.25, cacheWrite: 0.5 }, enabled: true },
|
|
69
|
-
{ id: "qwen3.7-max", name: "Qwen 3.7 Max", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 1.5, output: 6, cacheRead: 0.75, cacheWrite: 1.5 }, enabled: true },
|
|
70
|
-
{ id: "mimo-v2.5", name: "MiMo V2.5", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 1.2, output: 4.8, cacheRead: 0.6, cacheWrite: 1.2 }, enabled: true },
|
|
71
|
-
],
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
id: "google",
|
|
75
|
-
name: "Google Gemini",
|
|
76
|
-
type: "builtin",
|
|
77
|
-
api: "google-generative-ai",
|
|
78
|
-
hasAuth: true,
|
|
79
|
-
authMethod: "env",
|
|
80
|
-
models: [
|
|
81
|
-
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", reasoning: false, input: ["text", "image"], contextWindow: 1048576, maxTokens: 8192, cost: { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0.15 }, enabled: true },
|
|
82
|
-
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 8192, cost: { input: 1.25, output: 10, cacheRead: 0.625, cacheWrite: 1.25 }, enabled: false },
|
|
83
|
-
],
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
id: "openrouter",
|
|
87
|
-
name: "OpenRouter",
|
|
88
|
-
type: "builtin",
|
|
89
|
-
api: "openai-completions",
|
|
90
|
-
hasAuth: false,
|
|
91
|
-
authMethod: "none",
|
|
92
|
-
models: [
|
|
93
|
-
{ id: "openrouter/anthropic/claude-sonnet-4", name: "Claude 4 Sonnet (OpenRouter)", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: false },
|
|
94
|
-
{ id: "openrouter/deepseek/deepseek-r1", name: "DeepSeek R1 (OpenRouter)", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.55, output: 2.19, cacheRead: 0.14, cacheWrite: 0.55 }, enabled: false },
|
|
95
|
-
],
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
id: "mistral",
|
|
99
|
-
name: "Mistral",
|
|
100
|
-
type: "builtin",
|
|
101
|
-
api: "mistral-conversations",
|
|
102
|
-
hasAuth: false,
|
|
103
|
-
authMethod: "none",
|
|
104
|
-
models: [
|
|
105
|
-
{ id: "mistral-large", name: "Mistral Large", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 2, output: 6, cacheRead: 1, cacheWrite: 2 }, enabled: false },
|
|
106
|
-
],
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
id: "github-copilot",
|
|
110
|
-
name: "GitHub Copilot",
|
|
111
|
-
type: "builtin",
|
|
112
|
-
api: "openai-completions",
|
|
113
|
-
hasAuth: false,
|
|
114
|
-
authMethod: "none",
|
|
115
|
-
models: [
|
|
116
|
-
{ id: "copilot-gpt-4o", name: "Copilot GPT-4o", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 4096, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, enabled: false },
|
|
117
|
-
],
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
id: "groq",
|
|
121
|
-
name: "Groq",
|
|
122
|
-
type: "builtin",
|
|
123
|
-
api: "openai-completions",
|
|
124
|
-
hasAuth: false,
|
|
125
|
-
authMethod: "none",
|
|
126
|
-
models: [
|
|
127
|
-
{ id: "llama-3.3-70b", name: "Llama 3.3 70B", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.59, output: 0.79, cacheRead: 0, cacheWrite: 0 }, enabled: false },
|
|
128
|
-
],
|
|
129
|
-
},
|
|
130
|
-
];
|
|
131
|
-
|
|
132
|
-
// ─── Default Settings ─────────────────────────────────────
|
|
133
|
-
|
|
134
|
-
export const DEFAULT_SETTINGS: PiSettings = {
|
|
135
|
-
lastChangelogVersion: "0.80.3",
|
|
136
|
-
defaultProvider: "opencode-go",
|
|
137
|
-
defaultModel: "deepseek-v4-flash",
|
|
138
|
-
defaultThinkingLevel: "high",
|
|
139
|
-
defaultProjectTrust: "always",
|
|
140
|
-
theme: "light/dark",
|
|
141
|
-
hideThinkingBlock: true,
|
|
142
|
-
retry: { enabled: true },
|
|
143
|
-
packages: [
|
|
144
|
-
"npm:pi-hermes-memory",
|
|
145
|
-
"npm:@pi-unipi/notify",
|
|
146
|
-
"npm:context-mode",
|
|
147
|
-
"npm:pi-subagents",
|
|
148
|
-
"npm:pi-web-access",
|
|
149
|
-
"npm:pi-rtk-optimizer",
|
|
150
|
-
"npm:pi-puppeteer",
|
|
151
|
-
"npm:pi-intercom",
|
|
152
|
-
"npm:pi-prompt-template-model",
|
|
153
|
-
],
|
|
154
|
-
terminal: { showTerminalProgress: true },
|
|
155
|
-
warnings: { anthropicExtraUsage: true },
|
|
156
|
-
treeFilterMode: "default",
|
|
157
|
-
doubleEscapeAction: "tree",
|
|
158
|
-
enabledModels: [
|
|
159
|
-
"opencode-go/deepseek-v4-flash",
|
|
160
|
-
"opencode-go/deepseek-v4-pro",
|
|
161
|
-
"opencode/deepseek-v4-flash-free",
|
|
162
|
-
"opencode-go/glm-5.1",
|
|
163
|
-
"opencode-go/qwen3.7-max",
|
|
164
|
-
"opencode-go/mimo-v2.5",
|
|
165
|
-
],
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
// ─── Default Auth ─────────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
export const DEFAULT_AUTH: PiAuth = {
|
|
171
|
-
opencode: {
|
|
172
|
-
type: "api_key",
|
|
173
|
-
key: "sk-hqW3wtminBBkiTbjR57V15MClbOoZutz1StHXtv64HU0tiICeRDiA3DfC9vb0NGW",
|
|
174
|
-
},
|
|
175
|
-
"opencode-go": {
|
|
176
|
-
type: "api_key",
|
|
177
|
-
key: "sk-hqW3wtminBBkiTbjR57V15MClbOoZutz1StHXtv64HU0tiICeRDiA3DfC9vb0NGW",
|
|
178
|
-
},
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
// ─── Default models.json (empty — no custom providers yet) ─
|
|
182
|
-
|
|
183
|
-
export const DEFAULT_MODELS_JSON: PiModelsJson = {
|
|
184
|
-
providers: {},
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
// ─── Full Config ──────────────────────────────────────────
|
|
188
|
-
|
|
189
|
-
export const DEFAULT_PI_CONFIG: PiConfig = {
|
|
190
|
-
settings: DEFAULT_SETTINGS,
|
|
191
|
-
auth: DEFAULT_AUTH,
|
|
192
|
-
modelsJson: DEFAULT_MODELS_JSON,
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
// ─── Helper: get builtin providers ────────────────────────
|
|
196
|
-
|
|
197
|
-
export function getBuiltinProviders(): Provider[] {
|
|
198
|
-
return BUILTIN_PROVIDERS;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ─── Helper: merge custom providers from models.json ──────
|
|
202
|
-
|
|
203
|
-
export function getCustomProviders(modelsJson: PiModelsJson | null): Provider[] {
|
|
204
|
-
if (!modelsJson) return [];
|
|
205
|
-
return Object.entries(modelsJson.providers).map(([id, cfg]) => ({
|
|
206
|
-
id,
|
|
207
|
-
name: id.charAt(0).toUpperCase() + id.slice(1),
|
|
208
|
-
type: "custom" as const,
|
|
209
|
-
baseUrl: cfg.baseUrl,
|
|
210
|
-
api: cfg.api,
|
|
211
|
-
apiKey: cfg.apiKey,
|
|
212
|
-
authHeader: cfg.authHeader,
|
|
213
|
-
headers: cfg.headers,
|
|
214
|
-
compat: cfg.compat,
|
|
215
|
-
hasAuth: !!cfg.apiKey,
|
|
216
|
-
authMethod: (cfg.apiKey ? "file" : "none") as "file" | "none",
|
|
217
|
-
models: (cfg.models ?? []).map((m) => ({ ...m, enabled: true })),
|
|
218
|
-
}));
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ─── Helper: all providers merged ─────────────────────────
|
|
222
|
-
|
|
223
|
-
export function getAllProviders(config: PiConfig): Provider[] {
|
|
224
|
-
const builtins = getBuiltinProviders().map((p) => {
|
|
225
|
-
// Apply auth from settings
|
|
226
|
-
const authEntry = config.auth[p.id];
|
|
227
|
-
return {
|
|
228
|
-
...p,
|
|
229
|
-
hasAuth: !!authEntry || p.hasAuth,
|
|
230
|
-
authMethod: authEntry ? "file" : p.authMethod,
|
|
231
|
-
};
|
|
232
|
-
});
|
|
233
|
-
const customs = getCustomProviders(config.modelsJson);
|
|
234
|
-
return [...builtins, ...customs];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ─── Helper: all models flat ──────────────────────────────
|
|
238
|
-
|
|
239
|
-
export function getAllModels(config: PiConfig): (Model & { providerId: string; providerName: string })[] {
|
|
240
|
-
return getAllProviders(config).flatMap((p) =>
|
|
241
|
-
p.models.map((m) => ({
|
|
242
|
-
...m,
|
|
243
|
-
providerId: p.id,
|
|
244
|
-
providerName: p.name,
|
|
245
|
-
}))
|
|
246
|
-
);
|
|
247
|
-
}
|