@raingor/pi-web-switch 0.3.2 → 0.4.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.
@@ -0,0 +1,502 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useTranslation } from "@/lib/i18n";
3
+ import { Badge } from "@/components/ui/Badge";
4
+ import { EmptyState } from "@/components/ui/EmptyState";
5
+ import { formatTokens } from "@/lib/utils";
6
+ import type { AgentDef, ChainDef, ChainStep, RunRecord, SubagentsData } from "@/types";
7
+ import {
8
+ Brain,
9
+ GitBranch,
10
+ History,
11
+ Box,
12
+ Users,
13
+ FileCode,
14
+ CheckCircle2,
15
+ XCircle,
16
+ Loader2,
17
+ Search,
18
+ ExternalLink,
19
+ } from "lucide-react";
20
+
21
+ const API_BASE = "/api/pi";
22
+
23
+ type Tab = "agents" | "chains" | "history";
24
+
25
+ export function SubagentsPage() {
26
+ const { t } = useTranslation();
27
+ const [data, setData] = useState<SubagentsData | null>(null);
28
+ const [loading, setLoading] = useState(true);
29
+ const [error, setError] = useState<string | null>(null);
30
+ const [tab, setTab] = useState<Tab>("agents");
31
+ const [search, setSearch] = useState("");
32
+
33
+ const loadData = async () => {
34
+ setLoading(true);
35
+ setError(null);
36
+ try {
37
+ const res = await fetch(`${API_BASE}/subagents`);
38
+ if (!res.ok) throw new Error(`API /subagents: ${res.status}`);
39
+ const json = await res.json();
40
+ setData(json);
41
+ } catch (e: any) {
42
+ setError(e.message || "Failed to load subagents data");
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ };
47
+
48
+ useEffect(() => {
49
+ loadData();
50
+ }, []);
51
+
52
+ const q = search.trim().toLowerCase();
53
+
54
+ const filteredAgents = data?.agents.filter(
55
+ (a) => !q || a.name.toLowerCase().includes(q) || a.description.toLowerCase().includes(q)
56
+ );
57
+
58
+ const filteredChains = data?.chains.filter(
59
+ (c) => !q || c.name.toLowerCase().includes(q) || c.description.toLowerCase().includes(q)
60
+ );
61
+
62
+ const filteredHistory = data?.runHistory.filter(
63
+ (r) => !q || r.agent.toLowerCase().includes(q) || r.status.toLowerCase().includes(q)
64
+ );
65
+
66
+ if (loading && !data) {
67
+ return (
68
+ <div className="flex h-60 items-center justify-center">
69
+ <Loader2 className="h-6 w-6 animate-spin text-gray-500" />
70
+ </div>
71
+ );
72
+ }
73
+
74
+ if (error && !data) {
75
+ return (
76
+ <div className="flex h-60 flex-col items-center justify-center gap-3">
77
+ <p className="text-sm text-red-400">{error}</p>
78
+ <button
79
+ onClick={loadData}
80
+ className="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-500"
81
+ >
82
+ {t("loading.retry")}
83
+ </button>
84
+ </div>
85
+ );
86
+ }
87
+
88
+ return (
89
+ <div className="space-y-6">
90
+ {/* Header */}
91
+ <div className="flex items-center justify-between">
92
+ <div>
93
+ <h1 className="text-2xl font-bold" style={{ color: "var(--page-text)" }}>
94
+ {t("nav.subagents")}
95
+ </h1>
96
+ <p className="mt-1 text-sm" style={{ color: "var(--muted-text)" }}>
97
+ {data && t("subagents.summary", String(data.agents.length), String(data.chains.length))}
98
+ </p>
99
+ </div>
100
+ <button
101
+ onClick={loadData}
102
+ className="flex items-center gap-2 rounded-lg border border-gray-700 px-3 py-2 text-sm text-gray-300 hover:bg-gray-800"
103
+ >
104
+ <Loader2 className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
105
+ {t("dashboard.refresh_now")}
106
+ </button>
107
+ </div>
108
+
109
+ {/* Tabs */}
110
+ <div className="flex gap-1 rounded-lg bg-gray-900 p-1">
111
+ {(["agents", "chains", "history"] as Tab[]).map((tKey) => (
112
+ <button
113
+ key={tKey}
114
+ onClick={() => setTab(tKey)}
115
+ className={`flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-colors ${
116
+ tab === tKey
117
+ ? "bg-blue-600/10 text-blue-400"
118
+ : "text-gray-400 hover:text-gray-200"
119
+ }`}
120
+ >
121
+ {tKey === "agents" && <Brain className="h-4 w-4" />}
122
+ {tKey === "chains" && <GitBranch className="h-4 w-4" />}
123
+ {tKey === "history" && <History className="h-4 w-4" />}
124
+ {t(`subagents.tab_${tKey}`)}
125
+ {data && tKey === "agents" && (
126
+ <span className={`rounded-full px-2 py-0.5 text-xs ${tab === "agents" ? "bg-blue-600/20 text-blue-300" : "bg-blue-600/10 text-blue-400"}`}>{data.agents.length}</span>
127
+ )}
128
+ {data && tKey === "chains" && (
129
+ <span className={`rounded-full px-2 py-0.5 text-xs ${tab === "chains" ? "bg-blue-600/20 text-blue-300" : "bg-blue-600/10 text-blue-400"}`}>{data.chains.length}</span>
130
+ )}
131
+ {data && tKey === "history" && (
132
+ <span className={`rounded-full px-2 py-0.5 text-xs ${tab === "history" ? "bg-blue-600/20 text-blue-300" : "bg-blue-600/10 text-blue-400"}`}>{data.runHistory.length}</span>
133
+ )}
134
+ </button>
135
+ ))}
136
+ {data && (data.agents.length > 5 || data.chains.length > 5) && (
137
+ <div className="relative ml-auto">
138
+ <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-500" />
139
+ <input
140
+ type="text"
141
+ value={search}
142
+ onChange={(e) => setSearch(e.target.value)}
143
+ placeholder={t("models.search_placeholder")}
144
+ className="w-48 rounded-lg border border-gray-700 bg-gray-800 py-2 pl-9 pr-3 text-sm text-white"
145
+ />
146
+ </div>
147
+ )}
148
+ </div>
149
+
150
+ {/* Tab Content */}
151
+ <div>
152
+ {tab === "agents" && (
153
+ <AgentList
154
+ agents={filteredAgents ?? []}
155
+ onRefresh={loadData}
156
+ searchActive={!!q}
157
+ />
158
+ )}
159
+ {tab === "chains" && (
160
+ <ChainList
161
+ chains={filteredChains ?? []}
162
+ searchActive={!!q}
163
+ />
164
+ )}
165
+ {tab === "history" && (
166
+ <RunHistoryList
167
+ records={filteredHistory ?? []}
168
+ searchActive={!!q}
169
+ />
170
+ )}
171
+ </div>
172
+ </div>
173
+ );
174
+ }
175
+
176
+ // ─── Agent List ────────────────────────────────────────────
177
+
178
+ function AgentList({
179
+ agents,
180
+ onRefresh,
181
+ searchActive,
182
+ }: {
183
+ agents: AgentDef[];
184
+ onRefresh: () => void;
185
+ searchActive: boolean;
186
+ }) {
187
+ const { t } = useTranslation();
188
+ const [selected, setSelected] = useState<AgentDef | null>(null);
189
+
190
+ if (agents.length === 0) {
191
+ return (
192
+ <EmptyState
193
+ icon={<Brain className="h-8 w-8" />}
194
+ title={t("subagents.no_agents")}
195
+ description={t("subagents.no_agents_desc")}
196
+ />
197
+ );
198
+ }
199
+
200
+ return (
201
+ <div className="flex overflow-hidden rounded-xl border border-gray-800 bg-gray-900/50">
202
+ {/* Left: Agent cards */}
203
+ <div className="w-72 shrink-0 border-r border-gray-800 p-3 space-y-2 overflow-y-auto max-h-[70vh]">
204
+ {agents.map((agent) => (
205
+ <button
206
+ key={agent.fileName}
207
+ onClick={() => setSelected(agent)}
208
+ className={`w-full rounded-lg border px-3 py-3 text-left transition-colors ${
209
+ selected?.fileName === agent.fileName
210
+ ? "border-blue-500/30 bg-gray-800 text-white"
211
+ : "border-gray-700 text-gray-300 hover:bg-gray-800 hover:text-white"
212
+ }`}
213
+ >
214
+ <div className="flex items-center gap-2">
215
+ <Brain className="h-4 w-4 shrink-0 text-blue-400" />
216
+ <span className="truncate text-sm font-medium">{agent.name}</span>
217
+ <Badge variant={agent.package === "custom" ? "default" : "info"}>
218
+ {agent.package}
219
+ </Badge>
220
+ </div>
221
+ <p className="mt-1 line-clamp-2 text-xs text-gray-500">{agent.description}</p>
222
+ </button>
223
+ ))}
224
+ </div>
225
+
226
+ {/* Right: Agent detail */}
227
+ <div className="min-w-0 flex-1 p-6">
228
+ {selected ? (
229
+ <AgentDetail agent={selected} />
230
+ ) : (
231
+ <div className="flex h-40 items-center justify-center text-sm text-gray-500">
232
+ {t("providers_models.select_hint")}
233
+ </div>
234
+ )}
235
+ </div>
236
+ </div>
237
+ );
238
+ }
239
+
240
+ function AgentDetail({ agent }: { agent: AgentDef }) {
241
+ const { t } = useTranslation();
242
+ return (
243
+ <div className="space-y-4">
244
+ <div className="flex items-center gap-3">
245
+ <h2 className="text-lg font-semibold text-white">{agent.name}</h2>
246
+ <Badge variant={agent.package === "custom" ? "default" : "info"}>
247
+ {agent.package}
248
+ </Badge>
249
+ </div>
250
+
251
+ <p className="text-sm text-gray-400">{agent.description}</p>
252
+
253
+ <div className="grid grid-cols-2 gap-4 rounded-lg border border-gray-800 bg-gray-900/70 p-4">
254
+ {agent.model && (
255
+ <div>
256
+ <span className="text-xs text-gray-500">{t("subagents.model")}</span>
257
+ <p className="mt-0.5 text-sm text-gray-200 font-mono">{agent.model}</p>
258
+ </div>
259
+ )}
260
+ {agent.thinking && (
261
+ <div>
262
+ <span className="text-xs text-gray-500">{t("subagents.thinking")}</span>
263
+ <p className="mt-0.5 text-sm text-gray-200">{agent.thinking}</p>
264
+ </div>
265
+ )}
266
+ {agent.tools && (
267
+ <div className="col-span-2">
268
+ <span className="text-xs text-gray-500">{t("subagents.tools")}</span>
269
+ <div className="mt-1 flex flex-wrap gap-1.5">
270
+ {agent.tools.map((tool) => (
271
+ <span
272
+ key={tool}
273
+ className="rounded-md border border-gray-700 bg-gray-800 px-2 py-0.5 font-mono text-xs text-gray-300"
274
+ >
275
+ {tool}
276
+ </span>
277
+ ))}
278
+ </div>
279
+ </div>
280
+ )}
281
+ <div>
282
+ <span className="text-xs text-gray-500">{t("subagents.system_prompt_mode")}</span>
283
+ <p className="mt-0.5 text-sm text-gray-200">{agent.systemPromptMode || "replace"}</p>
284
+ </div>
285
+ <div>
286
+ <span className="text-xs text-gray-500">{t("subagents.input")}</span>
287
+ <p className="mt-0.5 text-sm text-gray-200">{(agent.input ?? ["text"]).join(", ")}</p>
288
+ </div>
289
+ </div>
290
+
291
+ {/* Body (system prompt preview) */}
292
+ <div>
293
+ <span className="text-xs text-gray-500">{t("subagents.system_prompt")}</span>
294
+ <pre className="mt-1.5 max-h-48 overflow-y-auto rounded-lg border border-gray-800 bg-gray-950 p-3 text-xs text-gray-400 whitespace-pre-wrap font-mono">
295
+ {agent.body || t("subagents.empty_prompt")}
296
+ </pre>
297
+ </div>
298
+ </div>
299
+ );
300
+ }
301
+
302
+ // ─── Chain List ────────────────────────────────────────────
303
+
304
+ function ChainList({
305
+ chains,
306
+ searchActive,
307
+ }: {
308
+ chains: ChainDef[];
309
+ searchActive: boolean;
310
+ }) {
311
+ const { t } = useTranslation();
312
+ const [selected, setSelected] = useState<ChainDef | null>(null);
313
+
314
+ if (chains.length === 0) {
315
+ return (
316
+ <EmptyState
317
+ icon={<GitBranch className="h-8 w-8" />}
318
+ title={t("subagents.no_chains")}
319
+ description={t("subagents.no_chains_desc")}
320
+ />
321
+ );
322
+ }
323
+
324
+ return (
325
+ <div className="flex overflow-hidden rounded-xl border border-gray-800 bg-gray-900/50">
326
+ <div className="w-72 shrink-0 border-r border-gray-800 p-3 space-y-2 overflow-y-auto max-h-[70vh]">
327
+ {chains.map((chain) => (
328
+ <button
329
+ key={chain.fileName}
330
+ onClick={() => setSelected(chain)}
331
+ className={`w-full rounded-lg border px-3 py-3 text-left transition-colors ${
332
+ selected?.fileName === chain.fileName
333
+ ? "border-blue-500/30 bg-gray-800 text-white"
334
+ : "border-gray-700 text-gray-300 hover:bg-gray-800 hover:text-white"
335
+ }`}
336
+ >
337
+ <div className="flex items-center gap-2">
338
+ <GitBranch className="h-4 w-4 shrink-0 text-emerald-400" />
339
+ <span className="truncate text-sm font-medium">{chain.name}</span>
340
+ </div>
341
+ <p className="mt-1 line-clamp-2 text-xs text-gray-500">{chain.description}</p>
342
+ <p className="mt-1 text-xs text-gray-600">
343
+ {chain.steps.length} {t("subagents.steps_count").toLowerCase()}
344
+ </p>
345
+ </button>
346
+ ))}
347
+ </div>
348
+
349
+ <div className="min-w-0 flex-1 p-6">
350
+ {selected ? (
351
+ <ChainDetail chain={selected} />
352
+ ) : (
353
+ <div className="flex h-40 items-center justify-center text-sm text-gray-500">
354
+ {t("providers_models.select_hint")}
355
+ </div>
356
+ )}
357
+ </div>
358
+ </div>
359
+ );
360
+ }
361
+
362
+ function StepIcon({ agent }: { agent: string }) {
363
+ const isParallel = agent.includes("|");
364
+ if (isParallel) return <Users className="h-4 w-4 text-purple-400" />;
365
+ return <Box className="h-4 w-4 text-blue-400" />;
366
+ }
367
+
368
+ function ChainDetail({ chain }: { chain: ChainDef }) {
369
+ const { t } = useTranslation();
370
+ return (
371
+ <div className="space-y-4">
372
+ <h2 className="text-lg font-semibold text-white">{chain.name}</h2>
373
+ <p className="text-sm text-gray-400">{chain.description}</p>
374
+
375
+ <div className="space-y-3">
376
+ <span className="text-xs font-medium text-gray-500">{t("subagents.pipeline")}</span>
377
+ <div className="relative">
378
+ {/* Vertical line connector */}
379
+ <div className="absolute left-4 top-2 bottom-2 w-0.5 bg-gray-700" />
380
+
381
+ {chain.steps.map((step, i) => (
382
+ <div key={i} className="relative flex items-start gap-4 pb-4 last:pb-0">
383
+ <div className="z-10 flex h-8 w-8 items-center justify-center rounded-full border border-gray-600 bg-gray-800">
384
+ <StepIcon agent={step.agent} />
385
+ </div>
386
+ <div className="min-w-0 flex-1 pt-1">
387
+ <p className="text-sm text-gray-200">
388
+ {step.agent.split("|").map((a, j) => (
389
+ <span key={j}>
390
+ {j > 0 && <span className="text-gray-500 mx-1">|</span>}
391
+ <code className="text-blue-300">{a.trim()}</code>
392
+ </span>
393
+ ))}
394
+ </p>
395
+ <div className="mt-1 flex flex-wrap gap-2 text-xs text-gray-500">
396
+ {step.phase && <span>{t("subagents.phase")}: {step.phase}</span>}
397
+ {step.label && <span>{t("subagents.label")}: {step.label}</span>}
398
+ {step.output && <span>{t("subagents.output")}: {step.output}</span>}
399
+ </div>
400
+ </div>
401
+ <span className="shrink-0 text-xs text-gray-600">#{i + 1}</span>
402
+ </div>
403
+ ))}
404
+ </div>
405
+ </div>
406
+ </div>
407
+ );
408
+ }
409
+
410
+ // ─── Run History ───────────────────────────────────────────
411
+
412
+ function RunHistoryList({
413
+ records,
414
+ searchActive,
415
+ }: {
416
+ records: RunRecord[];
417
+ searchActive: boolean;
418
+ }) {
419
+ const { t } = useTranslation();
420
+
421
+ if (records.length === 0) {
422
+ return (
423
+ <EmptyState
424
+ icon={<History className="h-8 w-8" />}
425
+ title={t("subagents.no_history")}
426
+ description={t("subagents.no_history_desc")}
427
+ />
428
+ );
429
+ }
430
+
431
+ return (
432
+ <div className="overflow-hidden rounded-xl border border-gray-800">
433
+ <table className="w-full text-sm">
434
+ <thead>
435
+ <tr className="border-b border-gray-800 bg-gray-900/70">
436
+ <th className="px-4 py-3 text-left text-xs font-medium text-gray-500">{t("subagents.agent")}</th>
437
+ <th className="px-4 py-3 text-left text-xs font-medium text-gray-500">{t("subagents.time")}</th>
438
+ <th className="px-4 py-3 text-left text-xs font-medium text-gray-500">{t("subagents.status")}</th>
439
+ <th className="px-4 py-3 text-right text-xs font-medium text-gray-500">{t("subagents.duration")}</th>
440
+ </tr>
441
+ </thead>
442
+ <tbody className="divide-y divide-gray-800">
443
+ {records.map((r, i) => (
444
+ <tr key={`${r.taskHash}-${i}`} className="hover:bg-gray-800/40">
445
+ <td className="px-4 py-3">
446
+ <code className="text-sm text-blue-300">{r.agent}</code>
447
+ </td>
448
+ <td className="px-4 py-3 text-gray-400">
449
+ {formatTimestamp(r.ts)}
450
+ </td>
451
+ <td className="px-4 py-3">
452
+ {r.status === "ok" ? (
453
+ <span className="flex items-center gap-1 text-emerald-400">
454
+ <CheckCircle2 className="h-3.5 w-3.5" />
455
+ {t("subagents.status_ok")}
456
+ </span>
457
+ ) : (
458
+ <span className="flex items-center gap-1 text-red-400">
459
+ <XCircle className="h-3.5 w-3.5" />
460
+ {t("subagents.status_error")}
461
+ {r.exit != null && <span className="text-xs text-gray-500">(exit {r.exit})</span>}
462
+ </span>
463
+ )}
464
+ </td>
465
+ <td className="px-4 py-3 text-right text-gray-400">
466
+ {r.duration != null ? formatDuration(r.duration) : "—"}
467
+ </td>
468
+ </tr>
469
+ ))}
470
+ </tbody>
471
+ </table>
472
+ {records.length >= 100 && (
473
+ <p className="border-t border-gray-800 px-4 py-2 text-xs text-gray-500">
474
+ {t("subagents.showing_recent")}
475
+ </p>
476
+ )}
477
+ </div>
478
+ );
479
+ }
480
+
481
+ // ─── Helpers ───────────────────────────────────────────────
482
+
483
+ function formatTimestamp(ts: number): string {
484
+ const d = new Date(ts * 1000);
485
+ const now = new Date();
486
+ const diffMs = now.getTime() - d.getTime();
487
+ const diffMin = Math.floor(diffMs / 60000);
488
+ if (diffMin < 1) return "just now";
489
+ if (diffMin < 60) return `${diffMin}m ago`;
490
+ const diffHr = Math.floor(diffMin / 60);
491
+ if (diffHr < 24) return `${diffHr}h ago`;
492
+ return d.toLocaleDateString();
493
+ }
494
+
495
+ function formatDuration(ms: number): string {
496
+ if (ms < 1000) return `${ms}ms`;
497
+ const sec = Math.floor(ms / 1000);
498
+ if (sec < 60) return `${sec}s`;
499
+ const min = Math.floor(sec / 60);
500
+ const rem = sec % 60;
501
+ return `${min}m ${rem}s`;
502
+ }
@@ -10,12 +10,13 @@ export const BUILTIN_PROVIDERS: Provider[] = [
10
10
  name: "Anthropic",
11
11
  type: "builtin",
12
12
  api: "anthropic-messages",
13
+ baseUrl: "https://api.anthropic.com/v1",
13
14
  hasAuth: true,
14
15
  authMethod: "env",
15
16
  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 },
17
+ { id: "claude-sonnet-4", name: "Claude 4 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 16384, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: true },
18
+ { id: "claude-sonnet-4-5", name: "Claude 4.5 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 16384, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: false },
19
+ { id: "claude-opus-4", name: "Claude 4 Opus", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 32000, cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 }, enabled: false },
19
20
  { 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
  ],
21
22
  },
@@ -24,6 +25,7 @@ export const BUILTIN_PROVIDERS: Provider[] = [
24
25
  name: "OpenAI",
25
26
  type: "builtin",
26
27
  api: "openai-completions",
28
+ baseUrl: "https://api.openai.com/v1",
27
29
  hasAuth: true,
28
30
  authMethod: "env",
29
31
  models: [
@@ -38,6 +40,7 @@ export const BUILTIN_PROVIDERS: Provider[] = [
38
40
  name: "DeepSeek",
39
41
  type: "builtin",
40
42
  api: "openai-completions",
43
+ baseUrl: "https://api.deepseek.com/v1",
41
44
  hasAuth: true,
42
45
  authMethod: "env",
43
46
  models: [
@@ -77,11 +80,12 @@ export const BUILTIN_PROVIDERS: Provider[] = [
77
80
  name: "Google Gemini",
78
81
  type: "builtin",
79
82
  api: "google-generative-ai",
83
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta",
80
84
  hasAuth: true,
81
85
  authMethod: "env",
82
86
  models: [
83
- { 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 },
84
- { 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 },
87
+ { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", reasoning: false, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0.15 }, enabled: true },
88
+ { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 1.25, output: 10, cacheRead: 0.625, cacheWrite: 1.25 }, enabled: false },
85
89
  ],
86
90
  },
87
91
  {
@@ -89,10 +93,11 @@ export const BUILTIN_PROVIDERS: Provider[] = [
89
93
  name: "OpenRouter",
90
94
  type: "builtin",
91
95
  api: "openai-completions",
96
+ baseUrl: "https://openrouter.ai/api/v1",
92
97
  hasAuth: false,
93
98
  authMethod: "none",
94
99
  models: [
95
- { 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 },
100
+ { id: "openrouter/anthropic/claude-sonnet-4", name: "Claude 4 Sonnet (OpenRouter)", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 16384, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: false },
96
101
  { 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 },
97
102
  ],
98
103
  },
@@ -101,6 +106,7 @@ export const BUILTIN_PROVIDERS: Provider[] = [
101
106
  name: "Mistral",
102
107
  type: "builtin",
103
108
  api: "mistral-conversations",
109
+ baseUrl: "https://api.mistral.ai/v1",
104
110
  hasAuth: false,
105
111
  authMethod: "none",
106
112
  models: [
@@ -112,6 +118,7 @@ export const BUILTIN_PROVIDERS: Provider[] = [
112
118
  name: "GitHub Copilot",
113
119
  type: "builtin",
114
120
  api: "openai-completions",
121
+ baseUrl: "https://api.githubcopilot.com",
115
122
  hasAuth: false,
116
123
  authMethod: "none",
117
124
  models: [
@@ -123,10 +130,11 @@ export const BUILTIN_PROVIDERS: Provider[] = [
123
130
  name: "Groq",
124
131
  type: "builtin",
125
132
  api: "openai-completions",
133
+ baseUrl: "https://api.groq.com/openai/v1",
126
134
  hasAuth: false,
127
135
  authMethod: "none",
128
136
  models: [
129
- { 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 },
137
+ { id: "llama-3.3-70b", name: "Llama 3.3 70B", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 4096, cost: { input: 0.59, output: 0.79, cacheRead: 0, cacheWrite: 0 }, enabled: false },
130
138
  ],
131
139
  },
132
140
  ];