@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/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="flex h-screen items-center justify-center bg-gray-950">
43
- <div className="flex flex-col items-center gap-4">
44
- <div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-600 border-t-blue-500" />
45
- <p className="text-sm text-gray-500">{t("loading.config")}</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="flex h-screen items-center justify-center bg-gray-950">
55
- <div className="flex flex-col items-center gap-4 max-w-md text-center">
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 = `${saved}%`;
117
+ document.documentElement.style.setProperty("--ui-zoom", String(saved / 100));
117
118
  }
118
119
  }
119
120
 
120
- createRoot(document.getElementById("root")!).render(
121
- <StrictMode>
122
- <Root />
123
- </StrictMode>
124
- );
125
-
126
- // ─── PWA: register service worker (production only — dev must not
127
- // cache HTML pages, or SPA routes like /providers can return stale HTML
128
- // to API fetches and break JSON parsing) ─────────────────────
129
- if ("serviceWorker" in navigator && import.meta.env.PROD) {
130
- window.addEventListener("load", () => {
131
- navigator.serviceWorker.register("/sw.js").catch((err) => {
132
- console.warn("SW registration failed:", err);
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.readCopilotUsage();
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
- const now = new Date();
318
- // Date buckets follow China time (UTC+8) regardless of system timezone
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.readCodexUsage();
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/all-usage-range
484
- if (method === "GET" && pathOnly === "/api/pi/all-usage-range") {
485
- const parsedUrl = new URL(url, "http://localhost");
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 allRecords = pi.filterByProvider(pi.readAllCombinedUsage(), providerId);
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
  }
@@ -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
- }