@raingor/pi-web-switch 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.ja.md +137 -0
  2. package/README.md +277 -0
  3. package/README.zh-CN.md +176 -0
  4. package/index.html +13 -0
  5. package/package.json +44 -0
  6. package/pi-package/index.ts +100 -0
  7. package/pi-package/skills/pi-web-switch/SKILL.md +60 -0
  8. package/public/pi.svg +4 -0
  9. package/server/pi-reader.ts +678 -0
  10. package/src/App.tsx +25 -0
  11. package/src/components/dashboard/DashboardPage.tsx +607 -0
  12. package/src/components/layout/AppShell.tsx +18 -0
  13. package/src/components/layout/Sidebar.tsx +116 -0
  14. package/src/components/models/ModelsPage.tsx +570 -0
  15. package/src/components/providers/ProvidersPage.tsx +466 -0
  16. package/src/components/sessions/MemoryPage.tsx +177 -0
  17. package/src/components/sessions/SessionsPage.tsx +347 -0
  18. package/src/components/settings/SettingsPage.tsx +351 -0
  19. package/src/components/ui/Badge.tsx +29 -0
  20. package/src/components/ui/EmptyState.tsx +20 -0
  21. package/src/components/ui/Modal.tsx +41 -0
  22. package/src/components/ui/StatCard.tsx +37 -0
  23. package/src/data/builtin-providers.ts +148 -0
  24. package/src/data/mock-config.ts +261 -0
  25. package/src/data/mock-usage.ts +153 -0
  26. package/src/index.css +217 -0
  27. package/src/lib/config.ts +56 -0
  28. package/src/lib/currency.ts +48 -0
  29. package/src/lib/i18n.tsx +98 -0
  30. package/src/lib/translations/en.ts +168 -0
  31. package/src/lib/translations/index.ts +14 -0
  32. package/src/lib/translations/ja.ts +158 -0
  33. package/src/lib/translations/zh-CN.ts +158 -0
  34. package/src/lib/translations/zh-TW.ts +158 -0
  35. package/src/lib/utils.ts +51 -0
  36. package/src/main.tsx +106 -0
  37. package/src/store/config-store.ts +459 -0
  38. package/src/types/index.ts +187 -0
  39. package/src/vite-env.d.ts +1 -0
  40. package/tsconfig.json +24 -0
  41. package/vite.config.ts +172 -0
@@ -0,0 +1,351 @@
1
+ import { useState, useRef } from "react";
2
+ import { useConfigStore } from "@/store/config-store";
3
+ import { useTranslation } from "@/lib/i18n";
4
+ import { Badge } from "@/components/ui/Badge";
5
+ import { Modal } from "@/components/ui/Modal";
6
+ import { exportConfig, parseImportFile, saveLocalBackup } from "@/lib/config";
7
+ import type { PiConfig } from "@/types";
8
+ import { cn } from "@/lib/utils";
9
+ import {
10
+ Settings,
11
+ Download,
12
+ Upload,
13
+ RotateCcw,
14
+ Sun,
15
+ Moon,
16
+ Monitor,
17
+ Package,
18
+ Plus,
19
+ Trash2,
20
+ CheckCircle2,
21
+ } from "lucide-react";
22
+
23
+ export function SettingsPage() {
24
+ const { t } = useTranslation();
25
+ const { settings, auth, modelsJson, allProviders, allModels, updateSettings, setTheme, addPackage, removePackage, importConfig: importConfigAction, resetToDefaults } = useConfigStore();
26
+ const fileInputRef = useRef<HTMLInputElement>(null);
27
+ const [newPackage, setNewPackage] = useState("");
28
+ const [importError, setImportError] = useState("");
29
+ const [showResetConfirm, setShowResetConfirm] = useState(false);
30
+
31
+ const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
32
+ const file = e.target.files?.[0];
33
+ if (!file) return;
34
+ const reader = new FileReader();
35
+ reader.onload = async (ev) => {
36
+ const result = parseImportFile(ev.target?.result as string);
37
+ if (result) {
38
+ await importConfigAction(result);
39
+ setImportError("");
40
+ } else {
41
+ setImportError("Invalid config file format");
42
+ }
43
+ };
44
+ reader.readAsText(file);
45
+ };
46
+
47
+ const buildConfigForExport = (): PiConfig => ({
48
+ settings: settings ?? { theme: "dark", packages: [], enabledModels: [] },
49
+ auth: auth ?? {},
50
+ modelsJson: modelsJson ?? { providers: {} },
51
+ });
52
+
53
+ const handleExport = () => {
54
+ const cfg = buildConfigForExport();
55
+ saveLocalBackup(cfg);
56
+ exportConfig(cfg);
57
+ };
58
+
59
+ const providerOptions = allProviders.map((p) => ({
60
+ value: p.id,
61
+ label: p.name,
62
+ }));
63
+
64
+ const modelOptions = allModels.map((m) => ({
65
+ value: `${m.providerId}/${m.id}`,
66
+ label: `${m.providerName} / ${m.name || m.id}`,
67
+ }));
68
+
69
+ return (
70
+ <div className="space-y-6">
71
+ <div>
72
+ <h1 className="text-2xl font-bold text-white">Settings</h1>
73
+ <p className="mt-1 text-sm text-gray-400">Configure pi agent preferences</p>
74
+ </div>
75
+
76
+ {/* Default Provider/Model */}
77
+ <section className="rounded-xl border border-gray-800 bg-gray-900/50">
78
+ <div className="border-b border-gray-800 px-6 py-4">
79
+ <h2 className="text-sm font-semibold text-gray-300">Defaults</h2>
80
+ </div>
81
+ <div className="space-y-4 p-6">
82
+ <div className="grid gap-4 sm:grid-cols-2">
83
+ <div>
84
+ <label className="block text-xs font-medium text-gray-400">Default Provider</label>
85
+ <select
86
+ value={settings?.defaultProvider ?? ""}
87
+ onChange={(e) => updateSettings({ defaultProvider: e.target.value })}
88
+ className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-white"
89
+ >
90
+ <option value="">— None —</option>
91
+ {providerOptions.map((o) => (
92
+ <option key={o.value} value={o.value}>{o.label}</option>
93
+ ))}
94
+ </select>
95
+ </div>
96
+ <div>
97
+ <label className="block text-xs font-medium text-gray-400">Default Model</label>
98
+ <select
99
+ value={settings?.defaultModel ?? ""}
100
+ onChange={(e) => updateSettings({ defaultModel: e.target.value })}
101
+ className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-white"
102
+ >
103
+ <option value="">— None —</option>
104
+ {modelOptions.map((o) => (
105
+ <option key={o.value} value={o.value}>{o.label}</option>
106
+ ))}
107
+ </select>
108
+ </div>
109
+ </div>
110
+ <div className="grid gap-4 sm:grid-cols-2">
111
+ <div>
112
+ <label className="block text-xs font-medium text-gray-400">Default Thinking Level</label>
113
+ <select
114
+ value={settings?.defaultThinkingLevel ?? "medium"}
115
+ onChange={(e) => updateSettings({ defaultThinkingLevel: e.target.value })}
116
+ className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-white"
117
+ >
118
+ {["off", "minimal", "low", "medium", "high", "xhigh"].map((l) => (
119
+ <option key={l} value={l}>{l.charAt(0).toUpperCase() + l.slice(1)}</option>
120
+ ))}
121
+ </select>
122
+ </div>
123
+ <div>
124
+ <label className="block text-xs font-medium text-gray-400">Project Trust</label>
125
+ <select
126
+ value={settings?.defaultProjectTrust ?? "prompt"}
127
+ onChange={(e) => updateSettings({ defaultProjectTrust: e.target.value })}
128
+ className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-white"
129
+ >
130
+ {["prompt", "always", "never"].map((t) => (
131
+ <option key={t} value={t}>{t.charAt(0).toUpperCase() + t.slice(1)}</option>
132
+ ))}
133
+ </select>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </section>
138
+
139
+ {/* Theme */}
140
+ <section className="rounded-xl border border-gray-800 bg-gray-900/50">
141
+ <div className="border-b border-gray-800 px-6 py-4">
142
+ <h2 className="text-sm font-semibold text-gray-300">Appearance</h2>
143
+ </div>
144
+ <div className="p-6">
145
+ <div className="flex gap-3">
146
+ {[
147
+ { value: "light" as const, icon: Sun, label: "Light" },
148
+ { value: "dark" as const, icon: Moon, label: "Dark" },
149
+ { value: "light/dark" as const, icon: Monitor, label: "System" },
150
+ ].map(({ value, icon: Icon, label }) => {
151
+ const active = (settings?.theme ?? "light/dark") === value;
152
+ return (
153
+ <button
154
+ key={value}
155
+ onClick={() => setTheme(value)}
156
+ className={cn(
157
+ "flex items-center gap-2 rounded-lg border px-4 py-3 text-sm transition-all",
158
+ active
159
+ ? "border-blue-500 bg-blue-500/10 text-blue-400"
160
+ : "border-gray-700 bg-gray-800 text-gray-400 hover:border-gray-600"
161
+ )}
162
+ >
163
+ <Icon className="h-4 w-4" />
164
+ {label}
165
+ {active && <CheckCircle2 className="h-3.5 w-3.5 ml-1" />}
166
+ </button>
167
+ );
168
+ })}
169
+ </div>
170
+ <div className="mt-4 flex items-center gap-3">
171
+ <label className="flex items-center gap-2 text-sm text-gray-400">
172
+ <input
173
+ type="checkbox"
174
+ checked={settings?.hideThinkingBlock ?? false}
175
+ onChange={(e) => updateSettings({ hideThinkingBlock: e.target.checked })}
176
+ className="rounded border-gray-600 bg-gray-800 text-blue-500"
177
+ />
178
+ Hide thinking blocks
179
+ </label>
180
+ </div>
181
+ </div>
182
+ </section>
183
+
184
+ {/* Enabled Models */}
185
+ <section className="rounded-xl border border-gray-800 bg-gray-900/50">
186
+ <div className="border-b border-gray-800 px-6 py-4">
187
+ <h2 className="text-sm font-semibold text-gray-300">Enabled Models</h2>
188
+ </div>
189
+ <div className="p-6">
190
+ <div className="space-y-2">
191
+ {(settings?.enabledModels ?? []).length === 0 ? (
192
+ <p className="text-sm text-gray-500">No models enabled. Enable models from the Models page.</p>
193
+ ) : (
194
+ (settings?.enabledModels ?? []).map((ref) => (
195
+ <div key={ref} className="flex items-center justify-between rounded-lg border border-gray-800 bg-gray-800/30 px-4 py-2.5">
196
+ <div className="flex items-center gap-2">
197
+ <CheckCircle2 className="h-4 w-4 text-emerald-400" />
198
+ <span className="text-sm text-gray-200">{ref}</span>
199
+ </div>
200
+ <button
201
+ onClick={() => useConfigStore.getState().removeEnabledModel(ref)}
202
+ className="rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-red-400"
203
+ >
204
+ <Trash2 className="h-3.5 w-3.5" />
205
+ </button>
206
+ </div>
207
+ ))
208
+ )}
209
+ </div>
210
+ </div>
211
+ </section>
212
+
213
+ {/* Packages */}
214
+ <section className="rounded-xl border border-gray-800 bg-gray-900/50">
215
+ <div className="border-b border-gray-800 px-6 py-4">
216
+ <h2 className="text-sm font-semibold text-gray-300">Extensions & Packages</h2>
217
+ </div>
218
+ <div className="p-6 space-y-4">
219
+ <div className="flex gap-2">
220
+ <input
221
+ type="text"
222
+ value={newPackage}
223
+ onChange={(e) => setNewPackage(e.target.value)}
224
+ placeholder="npm:package-name"
225
+ className="flex-1 rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-white placeholder-gray-500"
226
+ onKeyDown={(e) => {
227
+ if (e.key === "Enter" && newPackage) {
228
+ addPackage(newPackage);
229
+ setNewPackage("");
230
+ }
231
+ }}
232
+ />
233
+ <button
234
+ onClick={() => {
235
+ if (newPackage) {
236
+ addPackage(newPackage);
237
+ setNewPackage("");
238
+ }
239
+ }}
240
+ className="flex items-center gap-1 rounded-lg bg-blue-600 px-3 py-2 text-sm text-white hover:bg-blue-500"
241
+ >
242
+ <Plus className="h-4 w-4" />
243
+ Add
244
+ </button>
245
+ </div>
246
+ <div className="space-y-2">
247
+ {(settings?.packages ?? []).length === 0 ? (
248
+ <p className="text-sm text-gray-500">No packages installed</p>
249
+ ) : (
250
+ (settings?.packages ?? []).map((pkg) => (
251
+ <div key={pkg} className="flex items-center justify-between rounded-lg border border-gray-800 bg-gray-800/30 px-4 py-2.5">
252
+ <div className="flex items-center gap-2">
253
+ <Package className="h-4 w-4 text-blue-400" />
254
+ <span className="text-sm text-gray-200">{pkg}</span>
255
+ </div>
256
+ <button
257
+ onClick={() => removePackage(pkg)}
258
+ className="rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-red-400"
259
+ >
260
+ <Trash2 className="h-3.5 w-3.5" />
261
+ </button>
262
+ </div>
263
+ ))
264
+ )}
265
+ </div>
266
+ </div>
267
+ </section>
268
+
269
+ {/* Import/Export */}
270
+ <section className="rounded-xl border border-gray-800 bg-gray-900/50">
271
+ <div className="border-b border-gray-800 px-6 py-4">
272
+ <h2 className="text-sm font-semibold text-gray-300">Import & Export</h2>
273
+ </div>
274
+ <div className="p-6 space-y-4">
275
+ <div className="flex flex-wrap gap-3">
276
+ <button
277
+ onClick={handleExport}
278
+ className="flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-800 px-4 py-2 text-sm text-gray-300 hover:bg-gray-700"
279
+ >
280
+ <Download className="h-4 w-4" />
281
+ Export Config
282
+ </button>
283
+ <button
284
+ onClick={() => fileInputRef.current?.click()}
285
+ className="flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-800 px-4 py-2 text-sm text-gray-300 hover:bg-gray-700"
286
+ >
287
+ <Upload className="h-4 w-4" />
288
+ Import Config
289
+ </button>
290
+ <input
291
+ ref={fileInputRef}
292
+ type="file"
293
+ accept=".json"
294
+ className="hidden"
295
+ onChange={handleImport}
296
+ />
297
+ </div>
298
+ {importError && <p className="text-sm text-red-400">{importError}</p>}
299
+ </div>
300
+ </section>
301
+
302
+ {/* Danger Zone */}
303
+ <section className="rounded-xl border border-red-900/30 bg-red-950/20">
304
+ <div className="border-b border-red-900/30 px-6 py-4">
305
+ <h2 className="text-sm font-semibold text-red-400">Danger Zone</h2>
306
+ </div>
307
+ <div className="p-6">
308
+ <button
309
+ onClick={() => setShowResetConfirm(true)}
310
+ className="flex items-center gap-2 rounded-lg border border-red-700 bg-red-900/30 px-4 py-2 text-sm text-red-400 hover:bg-red-900/50"
311
+ >
312
+ <RotateCcw className="h-4 w-4" />
313
+ Reset to Defaults
314
+ </button>
315
+ </div>
316
+ </section>
317
+
318
+ {/* Reset Confirm */}
319
+ <Modal
320
+ open={showResetConfirm}
321
+ onClose={() => setShowResetConfirm(false)}
322
+ title="Reset Configuration?"
323
+ >
324
+ <div className="space-y-4">
325
+ <p className="text-sm text-gray-400">
326
+ This will reset all settings, auth keys, and custom providers to defaults.
327
+ Changes are written directly to your pi config files (~/.pi/agent/).
328
+ This action cannot be undone.
329
+ </p>
330
+ <div className="flex justify-end gap-3">
331
+ <button
332
+ onClick={() => setShowResetConfirm(false)}
333
+ className="rounded-lg px-4 py-2 text-sm text-gray-400 hover:bg-gray-800"
334
+ >
335
+ Cancel
336
+ </button>
337
+ <button
338
+ onClick={() => {
339
+ resetToDefaults();
340
+ setShowResetConfirm(false);
341
+ }}
342
+ className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-500"
343
+ >
344
+ Reset
345
+ </button>
346
+ </div>
347
+ </div>
348
+ </Modal>
349
+ </div>
350
+ );
351
+ }
@@ -0,0 +1,29 @@
1
+ import { cn } from "@/lib/utils";
2
+
3
+ interface BadgeProps {
4
+ children: string;
5
+ variant?: "default" | "success" | "warning" | "error" | "info";
6
+ className?: string;
7
+ }
8
+
9
+ const variants = {
10
+ default: "bg-gray-800 text-gray-300",
11
+ success: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20",
12
+ warning: "bg-amber-500/10 text-amber-400 border-amber-500/20",
13
+ error: "bg-red-500/10 text-red-400 border-red-500/20",
14
+ info: "bg-blue-500/10 text-blue-400 border-blue-500/20",
15
+ };
16
+
17
+ export function Badge({ children, variant = "default", className }: BadgeProps) {
18
+ return (
19
+ <span
20
+ className={cn(
21
+ "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium border",
22
+ variants[variant],
23
+ className
24
+ )}
25
+ >
26
+ {children}
27
+ </span>
28
+ );
29
+ }
@@ -0,0 +1,20 @@
1
+ import { cn } from "@/lib/utils";
2
+
3
+ interface EmptyStateProps {
4
+ icon: React.ReactNode;
5
+ title: string;
6
+ description?: string;
7
+ action?: React.ReactNode;
8
+ className?: string;
9
+ }
10
+
11
+ export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
12
+ return (
13
+ <div className={cn("flex flex-col items-center justify-center rounded-xl border border-dashed border-gray-700 p-12 text-center", className)}>
14
+ <div className="mb-4 text-gray-500">{icon}</div>
15
+ <h3 className="text-lg font-medium text-gray-300">{title}</h3>
16
+ {description && <p className="mt-1 text-sm text-gray-500">{description}</p>}
17
+ {action && <div className="mt-4">{action}</div>}
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,41 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ interface ModalProps {
4
+ open: boolean;
5
+ onClose: () => void;
6
+ title: string;
7
+ children: ReactNode;
8
+ size?: "sm" | "md" | "lg";
9
+ }
10
+
11
+ export function Modal({ open, onClose, title, children, size = "md" }: ModalProps) {
12
+ if (!open) return null;
13
+
14
+ const sizeClasses = {
15
+ sm: "max-w-md",
16
+ md: "max-w-lg",
17
+ lg: "max-w-2xl",
18
+ };
19
+
20
+ return (
21
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
22
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
23
+ <div
24
+ className={`relative w-full ${sizeClasses[size]} mx-4 rounded-xl border border-gray-700 bg-gray-900 shadow-2xl`}
25
+ >
26
+ <div className="flex items-center justify-between border-b border-gray-800 px-6 py-4">
27
+ <h2 className="text-lg font-semibold text-white">{title}</h2>
28
+ <button
29
+ onClick={onClose}
30
+ className="rounded-lg p-1 text-gray-400 hover:bg-gray-800 hover:text-white"
31
+ >
32
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
33
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
34
+ </svg>
35
+ </button>
36
+ </div>
37
+ <div className="px-6 py-4">{children}</div>
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,37 @@
1
+ import { cn } from "@/lib/utils";
2
+ import type { ReactNode } from "react";
3
+
4
+ interface StatCardProps {
5
+ title: string;
6
+ value: string;
7
+ subtitle?: string;
8
+ icon: ReactNode;
9
+ trend?: { value: string; positive: boolean };
10
+ className?: string;
11
+ }
12
+
13
+ export function StatCard({ title, value, subtitle, icon, trend, className }: StatCardProps) {
14
+ return (
15
+ <div className={cn("rounded-xl border border-gray-800 bg-gray-900/50 p-5 backdrop-blur-sm", className)}>
16
+ <div className="flex items-start justify-between">
17
+ <div className="space-y-1">
18
+ <p className="text-sm font-medium text-gray-400">{title}</p>
19
+ <p className="text-2xl font-bold tracking-tight text-white">{value}</p>
20
+ {subtitle && <p className="text-xs text-gray-500">{subtitle}</p>}
21
+ {trend && (
22
+ <span
23
+ className={cn(
24
+ "inline-flex items-center gap-1 text-xs font-medium",
25
+ trend.positive ? "text-emerald-400" : "text-red-400"
26
+ )}
27
+ >
28
+ <span>{trend.positive ? "↑" : "↓"}</span>
29
+ {trend.value}
30
+ </span>
31
+ )}
32
+ </div>
33
+ <div className="rounded-lg bg-blue-500/10 p-2.5 text-blue-400">{icon}</div>
34
+ </div>
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,148 @@
1
+ // ─── Built-in Providers ───────────────────────────────────
2
+ // These are hardcoded in pi's Rust source and not exposed as config files.
3
+ // We maintain this list to display known providers in the UI.
4
+
5
+ import type { Provider } from "@/types";
6
+
7
+ export const BUILTIN_PROVIDERS: Provider[] = [
8
+ {
9
+ id: "anthropic",
10
+ name: "Anthropic",
11
+ type: "builtin",
12
+ api: "anthropic-messages",
13
+ hasAuth: true,
14
+ authMethod: "env",
15
+ models: [
16
+ { 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 },
17
+ { 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 },
18
+ { 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 },
19
+ { 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 },
20
+ ],
21
+ },
22
+ {
23
+ id: "openai",
24
+ name: "OpenAI",
25
+ type: "builtin",
26
+ api: "openai-completions",
27
+ hasAuth: true,
28
+ authMethod: "env",
29
+ models: [
30
+ { 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 },
31
+ { 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 },
32
+ { 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 },
33
+ { 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 },
34
+ ],
35
+ },
36
+ {
37
+ id: "deepseek",
38
+ name: "DeepSeek",
39
+ type: "builtin",
40
+ api: "openai-completions",
41
+ hasAuth: true,
42
+ authMethod: "env",
43
+ models: [
44
+ { 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 },
45
+ { 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 },
46
+ ],
47
+ },
48
+ {
49
+ id: "opencode",
50
+ name: "OpenCode",
51
+ type: "builtin",
52
+ api: "openai-completions",
53
+ hasAuth: true,
54
+ authMethod: "file",
55
+ models: [
56
+ { 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 },
57
+ { 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 },
58
+ ],
59
+ },
60
+ {
61
+ id: "opencode-go",
62
+ name: "OpenCode Go",
63
+ type: "builtin",
64
+ api: "openai-completions",
65
+ hasAuth: true,
66
+ authMethod: "file",
67
+ models: [
68
+ { 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 },
69
+ { 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 },
70
+ { 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 },
71
+ { 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 },
72
+ { 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 },
73
+ ],
74
+ },
75
+ {
76
+ id: "sensenova",
77
+ name: "SenseNova",
78
+ type: "builtin",
79
+ api: "openai-completions",
80
+ hasAuth: true,
81
+ authMethod: "env",
82
+ models: [
83
+ { id: "glm-5.2", name: "GLM 5.2", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.8, output: 2.4, cacheRead: 0.4, cacheWrite: 0.8 }, enabled: true },
84
+ { 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 },
85
+ ],
86
+ },
87
+ {
88
+ id: "google",
89
+ name: "Google Gemini",
90
+ type: "builtin",
91
+ api: "google-generative-ai",
92
+ hasAuth: true,
93
+ authMethod: "env",
94
+ models: [
95
+ { 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 },
96
+ { 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 },
97
+ ],
98
+ },
99
+ {
100
+ id: "openrouter",
101
+ name: "OpenRouter",
102
+ type: "builtin",
103
+ api: "openai-completions",
104
+ hasAuth: false,
105
+ authMethod: "none",
106
+ models: [
107
+ { 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 },
108
+ { 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 },
109
+ ],
110
+ },
111
+ {
112
+ id: "mistral",
113
+ name: "Mistral",
114
+ type: "builtin",
115
+ api: "mistral-conversations",
116
+ hasAuth: false,
117
+ authMethod: "none",
118
+ models: [
119
+ { 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 },
120
+ ],
121
+ },
122
+ {
123
+ id: "github-copilot",
124
+ name: "GitHub Copilot",
125
+ type: "builtin",
126
+ api: "openai-completions",
127
+ hasAuth: false,
128
+ authMethod: "none",
129
+ models: [
130
+ { 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 },
131
+ ],
132
+ },
133
+ {
134
+ id: "groq",
135
+ name: "Groq",
136
+ type: "builtin",
137
+ api: "openai-completions",
138
+ hasAuth: false,
139
+ authMethod: "none",
140
+ models: [
141
+ { 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 },
142
+ ],
143
+ },
144
+ ];
145
+
146
+ export function getBuiltinProviders(): Provider[] {
147
+ return BUILTIN_PROVIDERS;
148
+ }