@raingor/pi-web-switch 0.4.2 → 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.
Files changed (37) hide show
  1. package/dist/index.html +18 -0
  2. package/index.html +2 -2
  3. package/package.json +7 -38
  4. package/public/apple-touch-icon.png +0 -0
  5. package/public/icon-192.png +0 -0
  6. package/public/icon-512.png +0 -0
  7. package/public/manifest.webmanifest +2 -2
  8. package/public/pi.svg +41 -6
  9. package/public/sw.js +51 -51
  10. package/server/pi-reader.ts +353 -237
  11. package/src/App.tsx +2 -0
  12. package/src/components/dashboard/DashboardPage.tsx +341 -56
  13. package/src/components/layout/AppShell.tsx +45 -13
  14. package/src/components/layout/Sidebar.tsx +78 -85
  15. package/src/components/providers/ProvidersModelsPage.tsx +125 -131
  16. package/src/components/sessions/MemoryPage.tsx +34 -13
  17. package/src/components/sessions/SessionsPage.tsx +20 -40
  18. package/src/components/settings/SettingsPage.tsx +6 -1
  19. package/src/components/speedtest/ModelSpeedTestPage.tsx +429 -0
  20. package/src/components/ui/EmptyState.tsx +7 -6
  21. package/src/components/ui/Modal.tsx +33 -25
  22. package/src/components/ui/StatCard.tsx +10 -13
  23. package/src/data/builtin-providers.test.ts +109 -0
  24. package/src/data/builtin-providers.ts +67 -44
  25. package/src/data/model-catalog.test.ts +122 -0
  26. package/src/data/model-catalog.ts +697 -478
  27. package/src/index.css +624 -210
  28. package/src/lib/translations/en.ts +88 -14
  29. package/src/lib/translations/ja.ts +88 -14
  30. package/src/lib/translations/zh-CN.ts +88 -14
  31. package/src/lib/translations/zh-TW.ts +87 -14
  32. package/src/main.tsx +97 -44
  33. package/src/types/index.ts +0 -1
  34. package/vite.config.ts +65 -164
  35. package/dist-electron/main/main.cjs +0 -2453
  36. package/src/data/mock-config.ts +0 -247
  37. package/src/data/mock-usage.ts +0 -151
@@ -12,26 +12,23 @@ interface StatCardProps {
12
12
 
13
13
  export function StatCard({ title, value, subtitle, icon, trend, className }: StatCardProps) {
14
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">
15
+ <div className={cn("tech-panel stat-instrument", className)}>
16
+ <span className="panel-corner panel-corner-tl" />
17
+ <span className="panel-corner panel-corner-br" />
18
+ <div className="flex items-start justify-between gap-4">
17
19
  <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>}
20
+ <p className="instrument-label">{title}</p>
21
+ <p className="instrument-value">{value}</p>
22
+ {subtitle && <p className="instrument-meta">{subtitle}</p>}
21
23
  {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
- >
24
+ <span className={cn("instrument-trend", trend.positive ? "is-positive" : "is-negative")}>
28
25
  <span>{trend.positive ? "↑" : "↓"}</span>
29
26
  {trend.value}
30
27
  </span>
31
28
  )}
32
29
  </div>
33
- <div className="rounded-lg bg-blue-500/10 p-2.5 text-blue-400">{icon}</div>
30
+ <div className="instrument-icon">{icon}</div>
34
31
  </div>
35
32
  </div>
36
33
  );
37
- }
34
+ }
@@ -0,0 +1,109 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { BUILTIN_PROVIDERS } from "./builtin-providers";
5
+
6
+ // This file is only the fallback for /api/pi/builtin-providers, which normally
7
+ // serves pi's real catalog from @earendil-works/pi-ai. Drift between the two is
8
+ // invisible at runtime (the fallback only kicks in when pi is missing), so these
9
+ // tests pin the fallback to the catalog shipped with the pinned dependency.
10
+
11
+ type CatalogModel = {
12
+ id: string;
13
+ name?: string;
14
+ reasoning?: boolean;
15
+ input?: string[];
16
+ contextWindow?: number;
17
+ maxTokens?: number;
18
+ cost?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
19
+ };
20
+
21
+ function catalogDir(): string | null {
22
+ let dir = dirname(new URL(import.meta.url).pathname);
23
+ for (let i = 0; i < 6; i++) {
24
+ const candidate = join(dir, "node_modules", "@earendil-works", "pi-ai", "dist", "providers");
25
+ if (existsSync(join(candidate, "data"))) return candidate;
26
+ dir = dirname(dir);
27
+ }
28
+ return null;
29
+ }
30
+
31
+ const dir = catalogDir();
32
+
33
+ /** Flatten one provider's data file into an id -> model map, as pi-reader does. */
34
+ function readProvider(providerId: string): Record<string, CatalogModel> | null {
35
+ if (!dir) return null;
36
+ const file = join(dir, "data", `${providerId}.json`);
37
+ if (!existsSync(file)) return null;
38
+ const data = JSON.parse(readFileSync(file, "utf-8")) as Record<string, Record<string, CatalogModel>>;
39
+ const out: Record<string, CatalogModel> = {};
40
+ for (const api of Object.keys(data)) {
41
+ for (const model of Object.values(data[api] ?? {})) {
42
+ if (model?.id) out[model.id] = model;
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+
48
+ describe.skipIf(!dir)("builtin providers match the pi-ai catalog", () => {
49
+ it("every provider id exists in the catalog", () => {
50
+ const available = new Set(
51
+ readdirSync(join(dir!, "data"))
52
+ .filter((f) => f.endsWith(".json") && !f.startsWith("."))
53
+ .map((f) => f.replace(/\.json$/, ""))
54
+ );
55
+ const unknown = BUILTIN_PROVIDERS.map((p) => p.id).filter((id) => !available.has(id));
56
+ expect(unknown).toEqual([]);
57
+ });
58
+
59
+ it.each(BUILTIN_PROVIDERS.map((p) => [p.id, p] as const))(
60
+ "%s models match the catalog",
61
+ (providerId, provider) => {
62
+ const catalog = readProvider(providerId);
63
+ expect(catalog, `${providerId}.json missing`).toBeTruthy();
64
+
65
+ for (const model of provider.models) {
66
+ const ref = catalog![model.id] as CatalogModel | undefined;
67
+ expect(ref, `${providerId}/${model.id} is not in the catalog`).toBeDefined();
68
+ if (!ref) continue;
69
+ expect(model.contextWindow, `${providerId}/${model.id} contextWindow`).toBe(ref.contextWindow);
70
+ expect(model.maxTokens, `${providerId}/${model.id} maxTokens`).toBe(ref.maxTokens);
71
+ expect(model.reasoning, `${providerId}/${model.id} reasoning`).toBe(!!ref.reasoning);
72
+ expect(model.input, `${providerId}/${model.id} input`).toEqual(ref.input ?? ["text"]);
73
+ // Entry pricing tier only — cost.tiers surcharges have no Model field.
74
+ expect(model.cost?.input, `${providerId}/${model.id} cost.input`).toBe(ref.cost?.input ?? 0);
75
+ expect(model.cost?.output, `${providerId}/${model.id} cost.output`).toBe(ref.cost?.output ?? 0);
76
+ expect(model.cost?.cacheRead, `${providerId}/${model.id} cost.cacheRead`).toBe(ref.cost?.cacheRead ?? 0);
77
+ expect(model.cost?.cacheWrite, `${providerId}/${model.id} cost.cacheWrite`).toBe(ref.cost?.cacheWrite ?? 0);
78
+ }
79
+ }
80
+ );
81
+ });
82
+
83
+ describe("builtin providers shape", () => {
84
+ it("provider ids are unique", () => {
85
+ const ids = BUILTIN_PROVIDERS.map((p) => p.id);
86
+ expect(new Set(ids).size).toBe(ids.length);
87
+ });
88
+
89
+ it("model ids are unique within each provider", () => {
90
+ for (const provider of BUILTIN_PROVIDERS) {
91
+ const ids = provider.models.map((m) => m.id);
92
+ expect(new Set(ids).size, provider.id).toBe(ids.length);
93
+ }
94
+ });
95
+
96
+ it("maxTokens never exceeds contextWindow", () => {
97
+ for (const provider of BUILTIN_PROVIDERS) {
98
+ for (const model of provider.models) {
99
+ expect(model.maxTokens!, `${provider.id}/${model.id}`).toBeLessThanOrEqual(model.contextWindow!);
100
+ }
101
+ }
102
+ });
103
+
104
+ it("hasAuth agrees with authMethod", () => {
105
+ for (const provider of BUILTIN_PROVIDERS) {
106
+ expect(provider.hasAuth, provider.id).toBe(provider.authMethod !== "none");
107
+ }
108
+ });
109
+ });
@@ -1,6 +1,13 @@
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.
1
+ // ─── Built-in Providers (static fallback) ─────────────────
2
+ // pi's real builtin catalog lives in @earendil-works/pi-ai as
3
+ // dist/providers/data/*.json and is served at /api/pi/builtin-providers via
4
+ // readBuiltinCatalog(). This list is only the fallback used when pi cannot be
5
+ // located on the machine, so it must stay consistent with that catalog.
6
+ //
7
+ // Generated from the pi-ai catalog shipped with this repo's pinned
8
+ // @earendil-works/pi-ai. Costs are USD per 1M tokens; only the entry pricing
9
+ // tier is kept (pi-ai's `cost.tiers` long-context surcharges are dropped
10
+ // because Model has no field for them).
4
11
 
5
12
  import type { Provider } from "@/types";
6
13
 
@@ -10,29 +17,31 @@ export const BUILTIN_PROVIDERS: Provider[] = [
10
17
  name: "Anthropic",
11
18
  type: "builtin",
12
19
  api: "anthropic-messages",
13
- baseUrl: "https://api.anthropic.com/v1",
20
+ baseUrl: "https://api.anthropic.com",
14
21
  hasAuth: true,
15
22
  authMethod: "env",
16
23
  models: [
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 },
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 },
24
+ { id: "claude-opus-5", name: "Claude Opus 5", reasoning: true, input: ["text", "image"], contextWindow: 1000000, maxTokens: 128000, cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, enabled: false },
25
+ { id: "claude-sonnet-5", name: "Claude Sonnet 5", reasoning: true, input: ["text", "image"], contextWindow: 1000000, maxTokens: 128000, cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 }, enabled: true },
26
+ { id: "claude-opus-4-5", name: "Claude Opus 4.5 (latest)", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 64000, cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, enabled: true },
27
+ { id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5 (latest)", reasoning: true, input: ["text", "image"], contextWindow: 1000000, maxTokens: 64000, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: true },
28
+ { id: "claude-haiku-4-5", name: "Claude Haiku 4.5 (latest)", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 64000, cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 }, enabled: true },
21
29
  ],
22
30
  },
23
31
  {
24
32
  id: "openai",
25
33
  name: "OpenAI",
26
34
  type: "builtin",
27
- api: "openai-completions",
35
+ api: "openai-responses",
28
36
  baseUrl: "https://api.openai.com/v1",
29
37
  hasAuth: true,
30
38
  authMethod: "env",
31
39
  models: [
32
- { 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 },
33
- { 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 },
34
- { 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 },
35
- { 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 },
40
+ { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", reasoning: true, input: ["text", "image"], contextWindow: 272000, maxTokens: 128000, cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }, enabled: true },
41
+ { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", reasoning: true, input: ["text", "image"], contextWindow: 272000, maxTokens: 128000, cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 3.125 }, enabled: true },
42
+ { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", reasoning: true, input: ["text", "image"], contextWindow: 272000, maxTokens: 128000, cost: { input: 1, output: 6, cacheRead: 0.1, cacheWrite: 1.25 }, enabled: false },
43
+ { id: "gpt-5.1", name: "GPT-5.1", reasoning: true, input: ["text", "image"], contextWindow: 400000, maxTokens: 128000, cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 }, enabled: true },
44
+ { 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: 0 }, enabled: false },
36
45
  ],
37
46
  },
38
47
  {
@@ -40,52 +49,60 @@ export const BUILTIN_PROVIDERS: Provider[] = [
40
49
  name: "DeepSeek",
41
50
  type: "builtin",
42
51
  api: "openai-completions",
43
- baseUrl: "https://api.deepseek.com/v1",
52
+ baseUrl: "https://api.deepseek.com",
44
53
  hasAuth: true,
45
54
  authMethod: "env",
46
55
  models: [
47
- { 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 },
48
- { 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 },
56
+ { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: true, input: ["text"], contextWindow: 1000000, maxTokens: 384000, cost: { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 }, enabled: true },
57
+ { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true, input: ["text"], contextWindow: 1000000, maxTokens: 384000, cost: { input: 0.435, output: 0.87, cacheRead: 0.003625, cacheWrite: 0 }, enabled: true },
49
58
  ],
50
59
  },
51
60
  {
52
- id: "opencode",
53
- name: "OpenCode",
61
+ id: "google",
62
+ name: "Google",
54
63
  type: "builtin",
55
- api: "openai-completions",
64
+ api: "google-generative-ai",
65
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta",
56
66
  hasAuth: true,
57
- authMethod: "file",
67
+ authMethod: "env",
58
68
  models: [
59
- { 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 },
60
- { 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: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 }, enabled: false },
70
+ { id: "gemini-3.5-flash", name: "Gemini 3.5 Flash", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 }, enabled: true },
71
+ { 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.125, cacheWrite: 0 }, enabled: false },
72
+ { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0 }, enabled: true },
61
73
  ],
62
74
  },
63
75
  {
64
- id: "opencode-go",
65
- name: "OpenCode Go",
76
+ id: "opencode",
77
+ name: "OpenCode Zen",
66
78
  type: "builtin",
67
79
  api: "openai-completions",
80
+ baseUrl: "https://opencode.ai/zen/v1",
68
81
  hasAuth: true,
69
82
  authMethod: "file",
70
83
  models: [
71
- { 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 },
72
- { 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 },
73
- { 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 },
74
- { 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 },
75
- { 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 },
84
+ { id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash Free", reasoning: true, input: ["text"], contextWindow: 200000, maxTokens: 128000, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, enabled: true },
85
+ { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: true, input: ["text"], contextWindow: 1000000, maxTokens: 384000, cost: { input: 0.14, output: 0.28, cacheRead: 0.028, cacheWrite: 0 }, enabled: true },
86
+ { id: "claude-sonnet-5", name: "Claude Sonnet 5", reasoning: true, input: ["text", "image"], contextWindow: 1000000, maxTokens: 128000, cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 }, enabled: true },
87
+ { id: "gpt-5", name: "GPT-5", reasoning: true, input: ["text", "image"], contextWindow: 400000, maxTokens: 128000, cost: { input: 1.07, output: 8.5, cacheRead: 0.107, cacheWrite: 0 }, enabled: false },
88
+ { id: "glm-5.2", name: "GLM-5.2", reasoning: true, input: ["text"], contextWindow: 1000000, maxTokens: 131072, cost: { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 }, enabled: true },
76
89
  ],
77
90
  },
78
91
  {
79
- id: "google",
80
- name: "Google Gemini",
92
+ id: "opencode-go",
93
+ name: "OpenCode Zen Go",
81
94
  type: "builtin",
82
- api: "google-generative-ai",
83
- baseUrl: "https://generativelanguage.googleapis.com/v1beta",
95
+ api: "openai-completions",
96
+ baseUrl: "https://opencode.ai/zen/go/v1",
84
97
  hasAuth: true,
85
- authMethod: "env",
98
+ authMethod: "file",
86
99
  models: [
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 },
100
+ { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: true, input: ["text"], contextWindow: 1000000, maxTokens: 384000, cost: { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 }, enabled: true },
101
+ { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true, input: ["text"], contextWindow: 1000000, maxTokens: 384000, cost: { input: 0.435, output: 0.87, cacheRead: 0.003625, cacheWrite: 0 }, enabled: true },
102
+ { id: "glm-5.2", name: "GLM-5.2", reasoning: true, input: ["text"], contextWindow: 1000000, maxTokens: 131072, cost: { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 }, enabled: true },
103
+ { id: "qwen3.7-max", name: "Qwen3.7 Max", reasoning: true, input: ["text"], contextWindow: 1000000, maxTokens: 65536, cost: { input: 2.5, output: 7.5, cacheRead: 0.5, cacheWrite: 3.125 }, enabled: true },
104
+ { id: "minimax-m3", name: "MiniMax-M3", reasoning: true, input: ["text", "image"], contextWindow: 1000000, maxTokens: 131072, cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 }, enabled: true },
105
+ { id: "kimi-k3", name: "Kimi K3 (2x usage)", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 131072, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 }, enabled: true },
89
106
  ],
90
107
  },
91
108
  {
@@ -97,8 +114,9 @@ export const BUILTIN_PROVIDERS: Provider[] = [
97
114
  hasAuth: false,
98
115
  authMethod: "none",
99
116
  models: [
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 },
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 },
117
+ { id: "anthropic/claude-sonnet-5", name: "Anthropic: Claude Sonnet 5", reasoning: true, input: ["text", "image"], contextWindow: 1000000, maxTokens: 128000, cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 }, enabled: false },
118
+ { id: "anthropic/claude-opus-4.5", name: "Anthropic: Claude Opus 4.5", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 64000, cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, enabled: false },
119
+ { id: "deepseek/deepseek-v4-flash", name: "DeepSeek: DeepSeek V4 Flash", reasoning: true, input: ["text"], contextWindow: 1048576, maxTokens: 393216, cost: { input: 0.14, output: 0.28, cacheRead: 0.028, cacheWrite: 0 }, enabled: false },
102
120
  ],
103
121
  },
104
122
  {
@@ -106,23 +124,27 @@ export const BUILTIN_PROVIDERS: Provider[] = [
106
124
  name: "Mistral",
107
125
  type: "builtin",
108
126
  api: "mistral-conversations",
109
- baseUrl: "https://api.mistral.ai/v1",
127
+ baseUrl: "https://api.mistral.ai",
110
128
  hasAuth: false,
111
129
  authMethod: "none",
112
130
  models: [
113
- { 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 },
131
+ { id: "mistral-large-latest", name: "Mistral Large (latest)", reasoning: false, input: ["text", "image"], contextWindow: 262144, maxTokens: 262144, cost: { input: 0.5, output: 1.5, cacheRead: 0.05, cacheWrite: 0 }, enabled: true },
132
+ { id: "mistral-medium-latest", name: "Mistral Medium (latest)", reasoning: true, input: ["text", "image"], contextWindow: 262144, maxTokens: 262144, cost: { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 }, enabled: false },
133
+ { id: "mistral-small-latest", name: "Mistral Small (latest)", reasoning: true, input: ["text", "image"], contextWindow: 256000, maxTokens: 256000, cost: { input: 0.15, output: 0.6, cacheRead: 0.015, cacheWrite: 0 }, enabled: false },
114
134
  ],
115
135
  },
116
136
  {
117
137
  id: "github-copilot",
118
138
  name: "GitHub Copilot",
119
139
  type: "builtin",
120
- api: "openai-completions",
121
- baseUrl: "https://api.githubcopilot.com",
140
+ api: "anthropic-messages",
141
+ baseUrl: "https://api.individual.githubcopilot.com",
122
142
  hasAuth: false,
123
143
  authMethod: "none",
124
144
  models: [
125
- { 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 },
145
+ { id: "claude-sonnet-5", name: "Claude Sonnet 5", reasoning: true, input: ["text", "image"], contextWindow: 1000000, maxTokens: 128000, cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 }, enabled: true },
146
+ { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", reasoning: true, input: ["text", "image"], contextWindow: 1050000, maxTokens: 128000, cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }, enabled: true },
147
+ { id: "gemini-3.5-flash", name: "Gemini 3.5 Flash", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 64000, cost: { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 }, enabled: true },
126
148
  ],
127
149
  },
128
150
  {
@@ -134,7 +156,8 @@ export const BUILTIN_PROVIDERS: Provider[] = [
134
156
  hasAuth: false,
135
157
  authMethod: "none",
136
158
  models: [
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 },
159
+ { id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B", reasoning: false, input: ["text"], contextWindow: 131072, maxTokens: 32768, cost: { input: 0.59, output: 0.79, cacheRead: 0, cacheWrite: 0 }, enabled: true },
160
+ { id: "meta-llama/llama-4-scout-17b-16e-instruct", name: "Llama 4 Scout 17B 16E", reasoning: false, input: ["text", "image"], contextWindow: 131072, maxTokens: 8192, cost: { input: 0.11, output: 0.34, cacheRead: 0, cacheWrite: 0 }, enabled: false },
138
161
  ],
139
162
  },
140
163
  ];
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ MODEL_CATALOG,
4
+ catalogEntryId,
5
+ catalogToModel,
6
+ findCatalogEntry,
7
+ searchCatalog,
8
+ } from "./model-catalog";
9
+
10
+ describe("model catalog integrity", () => {
11
+ it("every entry exposes a callable api id", () => {
12
+ for (const entry of MODEL_CATALOG) {
13
+ const id = catalogEntryId(entry);
14
+ expect(id, `${entry.name} has no id`).toBeTruthy();
15
+ // A trailing separator means the value is a match prefix, not a model id.
16
+ expect(id, `${entry.name} -> "${id}" is a prefix, not an id`).not.toMatch(/[-_.:]$/);
17
+ }
18
+ });
19
+
20
+ it("api ids are unique", () => {
21
+ const ids = MODEL_CATALOG.map(catalogEntryId);
22
+ expect(new Set(ids).size).toBe(ids.length);
23
+ });
24
+
25
+ it("picking a preset and re-detecting its id resolves to the same entry", () => {
26
+ for (const entry of MODEL_CATALOG) {
27
+ const id = catalogEntryId(entry);
28
+ expect(findCatalogEntry(id), `${id} round-trips to the wrong entry`).toBe(entry);
29
+ }
30
+ });
31
+
32
+ it("maxTokens never exceeds contextWindow", () => {
33
+ for (const entry of MODEL_CATALOG) {
34
+ expect(entry.maxTokens, `${entry.name}`).toBeLessThanOrEqual(entry.contextWindow);
35
+ }
36
+ });
37
+
38
+ it("every entry has a cost so usage stats are not silently free", () => {
39
+ const missing = MODEL_CATALOG.filter((e) => !e.cost).map((e) => e.name);
40
+ expect(missing).toEqual([]);
41
+ });
42
+
43
+ it("catalogToModel prefills the entry's api id", () => {
44
+ for (const entry of MODEL_CATALOG) {
45
+ expect(catalogToModel(entry).id).toBe(catalogEntryId(entry));
46
+ }
47
+ });
48
+
49
+ it("an empty query lists the whole catalog", () => {
50
+ expect(searchCatalog("", MODEL_CATALOG.length)).toHaveLength(MODEL_CATALOG.length);
51
+ });
52
+ });
53
+
54
+ describe("findCatalogEntry resolution", () => {
55
+ // Exact ids must beat broader family prefixes.
56
+ const cases: [string, string][] = [
57
+ ["claude-opus-4-5-20251101", "Claude Opus 4.5"],
58
+ ["claude-sonnet-4-5", "Claude Sonnet 4.5"],
59
+ ["claude-haiku-4-5", "Claude Haiku 4.5"],
60
+ ["anthropic/claude-sonnet-5", "Claude Sonnet 5"],
61
+ ["gpt-5", "GPT-5"],
62
+ ["gpt-5.1", "GPT-5.1"],
63
+ ["gpt-5.6-sol", "GPT-5.6 Sol"],
64
+ ["gpt-4o-mini", "GPT-4o Mini"],
65
+ ["o3", "o3"],
66
+ ["o3-mini", "o3-mini"],
67
+ ["o3-pro", "o3-pro"],
68
+ ["deepseek-v4-flash", "DeepSeek V4 Flash"],
69
+ ["deepseek-v4-flash-free", "DeepSeek V4 Flash (Free)"],
70
+ ["deepseek-v4-pro", "DeepSeek V4 Pro"],
71
+ ["glm-4.5", "GLM-4.5"],
72
+ ["glm-4.5-air", "GLM-4.5-Air"],
73
+ ["glm-5", "GLM-5"],
74
+ ["glm-5.2", "GLM-5.2"],
75
+ ["kimi-k2.6", "Kimi K2.6"],
76
+ ["kimi-k2.6-thinking", "Kimi K2.6 Thinking"],
77
+ ["kimi-k2.7-code", "Kimi K2.7 Code"],
78
+ ["qwen3-max", "Qwen3 Max"],
79
+ ["qwen3-coder-plus", "Qwen3 Coder Plus"],
80
+ ["qwen3-vl-plus", "Qwen3 VL Plus"],
81
+ ["grok-4.6", "Grok 4.6"],
82
+ ["gemini-2.5-flash", "Gemini 2.5 Flash"],
83
+ ["gemini-2.5-pro", "Gemini 2.5 Pro"],
84
+ ["mistral-large-latest", "Mistral Large 3"],
85
+ ];
86
+
87
+ it.each(cases)("%s resolves to %s", (id, name) => {
88
+ expect(findCatalogEntry(id)?.name).toBe(name);
89
+ });
90
+
91
+ it("returns undefined for an unknown id", () => {
92
+ expect(findCatalogEntry("totally-made-up-model-xyz")).toBeUndefined();
93
+ });
94
+ });
95
+
96
+ describe("vendor-documented values", () => {
97
+ // Regression guards for the values that were previously wrong. Each is
98
+ // sourced from the vendor's own pricing / model docs.
99
+ const expected: Record<string, { ctx: number; max: number; in: number; out: number }> = {
100
+ "claude-opus-4-5": { ctx: 200_000, max: 65_536, in: 5, out: 25 },
101
+ "claude-sonnet-4-5": { ctx: 200_000, max: 65_536, in: 3, out: 15 },
102
+ "gpt-5.1": { ctx: 400_000, max: 128_000, in: 1.25, out: 10 },
103
+ "o3-pro": { ctx: 200_000, max: 100_000, in: 20, out: 80 },
104
+ "gemini-2.5-flash": { ctx: 1_048_576, max: 65_536, in: 0.3, out: 2.5 },
105
+ "deepseek-v4-pro": { ctx: 1_048_576, max: 131_072, in: 1.32, out: 3.96 },
106
+ "glm-5.1": { ctx: 204_800, max: 131_072, in: 1.4, out: 4.4 },
107
+ "qwen3-max": { ctx: 262_144, max: 65_536, in: 1.2, out: 6 },
108
+ };
109
+
110
+ it.each(Object.entries(expected))("%s matches vendor docs", (id, want) => {
111
+ const entry = findCatalogEntry(id);
112
+ expect(entry).toBeDefined();
113
+ expect(entry!.contextWindow).toBe(want.ctx);
114
+ expect(entry!.maxTokens).toBe(want.max);
115
+ expect(entry!.cost?.input).toBe(want.in);
116
+ expect(entry!.cost?.output).toBe(want.out);
117
+ });
118
+
119
+ it("glm-5.2 is a Zhipu model, not SenseNova", () => {
120
+ expect(findCatalogEntry("glm-5.2")?.family).toBe("GLM");
121
+ });
122
+ });