@raingor/pi-web-switch 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.html +4 -4
- package/index.html +2 -2
- package/package.json +5 -3
- package/public/manifest.webmanifest +2 -2
- package/public/sw.js +51 -51
- package/server/pi-reader.ts +326 -19
- package/src/App.tsx +2 -0
- package/src/components/dashboard/DashboardPage.tsx +341 -27
- package/src/components/layout/AppShell.tsx +45 -13
- package/src/components/layout/Sidebar.tsx +78 -74
- package/src/components/providers/ProvidersModelsPage.tsx +125 -131
- package/src/components/sessions/MemoryPage.tsx +32 -12
- package/src/components/sessions/SessionsPage.tsx +18 -4
- package/src/components/settings/SettingsPage.tsx +6 -1
- package/src/components/speedtest/ModelSpeedTestPage.tsx +429 -0
- package/src/components/ui/EmptyState.tsx +7 -6
- package/src/components/ui/Modal.tsx +33 -25
- package/src/components/ui/StatCard.tsx +10 -13
- package/src/data/builtin-providers.test.ts +109 -0
- package/src/data/builtin-providers.ts +67 -44
- package/src/data/model-catalog.test.ts +122 -0
- package/src/data/model-catalog.ts +697 -478
- package/src/index.css +624 -210
- package/src/lib/translations/en.ts +88 -9
- package/src/lib/translations/ja.ts +88 -9
- package/src/lib/translations/zh-CN.ts +88 -9
- package/src/lib/translations/zh-TW.ts +87 -9
- package/src/main.tsx +99 -22
- package/vite.config.ts +62 -137
- package/src/data/mock-config.ts +0 -247
- package/src/data/mock-usage.ts +0 -151
|
@@ -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
|
-
//
|
|
3
|
-
//
|
|
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
|
|
20
|
+
baseUrl: "https://api.anthropic.com",
|
|
14
21
|
hasAuth: true,
|
|
15
22
|
authMethod: "env",
|
|
16
23
|
models: [
|
|
17
|
-
{ id: "claude-
|
|
18
|
-
{ id: "claude-sonnet-
|
|
19
|
-
{ id: "claude-opus-4", name: "Claude 4
|
|
20
|
-
{ id: "claude-
|
|
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-
|
|
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-
|
|
33
|
-
{ id: "gpt-
|
|
34
|
-
{ id: "gpt-5.
|
|
35
|
-
{ id: "
|
|
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
|
|
52
|
+
baseUrl: "https://api.deepseek.com",
|
|
44
53
|
hasAuth: true,
|
|
45
54
|
authMethod: "env",
|
|
46
55
|
models: [
|
|
47
|
-
{ id: "deepseek-
|
|
48
|
-
{ id: "deepseek-
|
|
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: "
|
|
53
|
-
name: "
|
|
61
|
+
id: "google",
|
|
62
|
+
name: "Google",
|
|
54
63
|
type: "builtin",
|
|
55
|
-
api: "
|
|
64
|
+
api: "google-generative-ai",
|
|
65
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
56
66
|
hasAuth: true,
|
|
57
|
-
authMethod: "
|
|
67
|
+
authMethod: "env",
|
|
58
68
|
models: [
|
|
59
|
-
{ id: "
|
|
60
|
-
{ id: "
|
|
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
|
|
65
|
-
name: "OpenCode
|
|
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:
|
|
72
|
-
{ id: "deepseek-v4-
|
|
73
|
-
{ id: "
|
|
74
|
-
{ id: "
|
|
75
|
-
{ id: "
|
|
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: "
|
|
80
|
-
name: "
|
|
92
|
+
id: "opencode-go",
|
|
93
|
+
name: "OpenCode Zen Go",
|
|
81
94
|
type: "builtin",
|
|
82
|
-
api: "
|
|
83
|
-
baseUrl: "https://
|
|
95
|
+
api: "openai-completions",
|
|
96
|
+
baseUrl: "https://opencode.ai/zen/go/v1",
|
|
84
97
|
hasAuth: true,
|
|
85
|
-
authMethod: "
|
|
98
|
+
authMethod: "file",
|
|
86
99
|
models: [
|
|
87
|
-
{ id: "
|
|
88
|
-
{ id: "
|
|
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: "
|
|
101
|
-
{ id: "
|
|
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
|
|
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:
|
|
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: "
|
|
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: "
|
|
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:
|
|
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
|
+
});
|