aigetwey 1.0.1
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/CHANGELOG.md +84 -0
- package/LICENSE +21 -0
- package/README.md +302 -0
- package/assets/logo.svg +8 -0
- package/assets/screenshot.png +0 -0
- package/assets/wordmark.svg +9 -0
- package/config.example.yaml +56 -0
- package/dashboard/.env.example +12 -0
- package/dashboard/next-env.d.ts +6 -0
- package/dashboard/next.config.ts +12 -0
- package/dashboard/package-lock.json +1771 -0
- package/dashboard/package.json +29 -0
- package/dashboard/postcss.config.mjs +5 -0
- package/dashboard/src/app/(console)/combos/page.tsx +10 -0
- package/dashboard/src/app/(console)/config/page.tsx +5 -0
- package/dashboard/src/app/(console)/console/page.tsx +92 -0
- package/dashboard/src/app/(console)/endpoint/page.tsx +5 -0
- package/dashboard/src/app/(console)/layout.tsx +17 -0
- package/dashboard/src/app/(console)/page.tsx +8 -0
- package/dashboard/src/app/(console)/providers/[id]/page.tsx +6 -0
- package/dashboard/src/app/(console)/providers/page.tsx +5 -0
- package/dashboard/src/app/(console)/quota/page.tsx +5 -0
- package/dashboard/src/app/(console)/tools/[id]/page.tsx +6 -0
- package/dashboard/src/app/(console)/tools/page.tsx +5 -0
- package/dashboard/src/app/(console)/usage/page.tsx +24 -0
- package/dashboard/src/app/api/cli-detect/[tool]/route.ts +253 -0
- package/dashboard/src/app/api/gw/[...path]/route.ts +89 -0
- package/dashboard/src/app/api/login/route.ts +30 -0
- package/dashboard/src/app/api/logout/route.ts +9 -0
- package/dashboard/src/app/api/password/route.ts +34 -0
- package/dashboard/src/app/globals.css +340 -0
- package/dashboard/src/app/icon.svg +8 -0
- package/dashboard/src/app/layout.tsx +28 -0
- package/dashboard/src/app/login/page.tsx +60 -0
- package/dashboard/src/components/AreaChart.tsx +115 -0
- package/dashboard/src/components/Badge.tsx +32 -0
- package/dashboard/src/components/Button.tsx +60 -0
- package/dashboard/src/components/CapacityBadges.tsx +40 -0
- package/dashboard/src/components/Checkbox.tsx +40 -0
- package/dashboard/src/components/CliToolConfig.tsx +63 -0
- package/dashboard/src/components/ConfigEditor.tsx +199 -0
- package/dashboard/src/components/ConfirmModal.tsx +36 -0
- package/dashboard/src/components/CooldownTimer.tsx +42 -0
- package/dashboard/src/components/EndpointView.tsx +439 -0
- package/dashboard/src/components/Icon.tsx +25 -0
- package/dashboard/src/components/KeyReveal.tsx +78 -0
- package/dashboard/src/components/Lamp.tsx +8 -0
- package/dashboard/src/components/LogTable.tsx +223 -0
- package/dashboard/src/components/LogoutButton.tsx +20 -0
- package/dashboard/src/components/ModelPicker.tsx +121 -0
- package/dashboard/src/components/ModelSelectModal.tsx +126 -0
- package/dashboard/src/components/PasswordEditor.tsx +86 -0
- package/dashboard/src/components/PricingEditor.tsx +171 -0
- package/dashboard/src/components/ProviderDetail.tsx +566 -0
- package/dashboard/src/components/ProviderManager.tsx +311 -0
- package/dashboard/src/components/QuotaView.tsx +78 -0
- package/dashboard/src/components/Rail.tsx +82 -0
- package/dashboard/src/components/RichCard.tsx +46 -0
- package/dashboard/src/components/RoutingView.tsx +329 -0
- package/dashboard/src/components/ThemeProvider.tsx +36 -0
- package/dashboard/src/components/ToastProvider.tsx +58 -0
- package/dashboard/src/components/ToolDetail.tsx +475 -0
- package/dashboard/src/components/TopBar.tsx +128 -0
- package/dashboard/src/components/UsageView.tsx +151 -0
- package/dashboard/src/components/ui.tsx +54 -0
- package/dashboard/src/lib/capabilities.ts +318 -0
- package/dashboard/src/lib/cliTools.ts +120 -0
- package/dashboard/src/lib/client.ts +190 -0
- package/dashboard/src/lib/gateway.ts +269 -0
- package/dashboard/src/lib/session.ts +71 -0
- package/dashboard/src/middleware.ts +37 -0
- package/dashboard/tsconfig.json +21 -0
- package/dist/adapters/anthropic.js +289 -0
- package/dist/adapters/anthropic.js.map +1 -0
- package/dist/adapters/gemini.js +268 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/openai.js +13 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/cli/tray/autostart.js +152 -0
- package/dist/cli/tray/autostart.js.map +1 -0
- package/dist/cli/tray/icon.js +4 -0
- package/dist/cli/tray/icon.js.map +1 -0
- package/dist/cli/tray/tray.js +141 -0
- package/dist/cli/tray/tray.js.map +1 -0
- package/dist/cli/tray/trayRuntime.js +91 -0
- package/dist/cli/tray/trayRuntime.js.map +1 -0
- package/dist/cli.js +361 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +728 -0
- package/dist/config.js.map +1 -0
- package/dist/core/authStore.js +78 -0
- package/dist/core/authStore.js.map +1 -0
- package/dist/core/canonical.js +9 -0
- package/dist/core/canonical.js.map +1 -0
- package/dist/core/console-buffer.js +25 -0
- package/dist/core/console-buffer.js.map +1 -0
- package/dist/core/fallback.js +62 -0
- package/dist/core/fallback.js.map +1 -0
- package/dist/core/handler.js +174 -0
- package/dist/core/handler.js.map +1 -0
- package/dist/core/keypool.js +105 -0
- package/dist/core/keypool.js.map +1 -0
- package/dist/core/quota.js +165 -0
- package/dist/core/quota.js.map +1 -0
- package/dist/core/state.js +52 -0
- package/dist/core/state.js.map +1 -0
- package/dist/db.js +193 -0
- package/dist/db.js.map +1 -0
- package/dist/headroom/compress.js +44 -0
- package/dist/headroom/compress.js.map +1 -0
- package/dist/headroom/detect.js +108 -0
- package/dist/headroom/detect.js.map +1 -0
- package/dist/headroom/process.js +158 -0
- package/dist/headroom/process.js.map +1 -0
- package/dist/inject/caveman.js +30 -0
- package/dist/inject/caveman.js.map +1 -0
- package/dist/inject/index.js +24 -0
- package/dist/inject/index.js.map +1 -0
- package/dist/inject/ponytail.js +19 -0
- package/dist/inject/ponytail.js.map +1 -0
- package/dist/middleware/auth.js +66 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/providers/capabilities.js +246 -0
- package/dist/providers/capabilities.js.map +1 -0
- package/dist/providers/free.js +43 -0
- package/dist/providers/free.js.map +1 -0
- package/dist/providers/pricing.js +224 -0
- package/dist/providers/pricing.js.map +1 -0
- package/dist/providers/vertex.js +97 -0
- package/dist/providers/vertex.js.map +1 -0
- package/dist/routes/admin.js +622 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/health.js +4 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.js +12 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/v1.js +75 -0
- package/dist/routes/v1.js.map +1 -0
- package/dist/rtk/detect.js +50 -0
- package/dist/rtk/detect.js.map +1 -0
- package/dist/rtk/filters.js +85 -0
- package/dist/rtk/filters.js.map +1 -0
- package/dist/rtk/index.js +39 -0
- package/dist/rtk/index.js.map +1 -0
- package/dist/server.js +100 -0
- package/dist/server.js.map +1 -0
- package/dist/stream/anthropic-stream.js +239 -0
- package/dist/stream/anthropic-stream.js.map +1 -0
- package/dist/stream/chunk.js +7 -0
- package/dist/stream/chunk.js.map +1 -0
- package/dist/stream/gemini-stream.js +135 -0
- package/dist/stream/gemini-stream.js.map +1 -0
- package/dist/stream/index.js +12 -0
- package/dist/stream/index.js.map +1 -0
- package/dist/stream/openai-stream.js +34 -0
- package/dist/stream/openai-stream.js.map +1 -0
- package/dist/stream/sse.js +64 -0
- package/dist/stream/sse.js.map +1 -0
- package/dist/translator/thinking.js +70 -0
- package/dist/translator/thinking.js.map +1 -0
- package/dist/translator/thinkingUnified.js +322 -0
- package/dist/translator/thinkingUnified.js.map +1 -0
- package/dist/upstream/client.js +120 -0
- package/dist/upstream/client.js.map +1 -0
- package/package.json +76 -0
- package/run.sh +27 -0
- package/src/adapters/anthropic.ts +377 -0
- package/src/adapters/gemini.ts +341 -0
- package/src/adapters/index.ts +17 -0
- package/src/adapters/openai.ts +22 -0
- package/src/cli/tray/autostart.ts +133 -0
- package/src/cli/tray/icon.ts +4 -0
- package/src/cli/tray/tray.ts +156 -0
- package/src/cli/tray/trayRuntime.ts +90 -0
- package/src/cli.ts +379 -0
- package/src/config.ts +777 -0
- package/src/core/authStore.ts +86 -0
- package/src/core/canonical.ts +93 -0
- package/src/core/console-buffer.ts +39 -0
- package/src/core/fallback.ts +116 -0
- package/src/core/handler.ts +236 -0
- package/src/core/keypool.ts +152 -0
- package/src/core/quota.ts +214 -0
- package/src/core/state.ts +65 -0
- package/src/db.ts +280 -0
- package/src/headroom/compress.ts +78 -0
- package/src/headroom/detect.ts +119 -0
- package/src/headroom/process.ts +166 -0
- package/src/inject/caveman.ts +35 -0
- package/src/inject/index.ts +46 -0
- package/src/inject/ponytail.ts +31 -0
- package/src/middleware/auth.ts +76 -0
- package/src/providers/capabilities.ts +297 -0
- package/src/providers/free.ts +53 -0
- package/src/providers/pricing.ts +261 -0
- package/src/providers/vertex.ts +117 -0
- package/src/routes/admin.ts +716 -0
- package/src/routes/health.ts +5 -0
- package/src/routes/index.ts +24 -0
- package/src/routes/v1.ts +87 -0
- package/src/rtk/detect.ts +55 -0
- package/src/rtk/filters.ts +94 -0
- package/src/rtk/index.ts +58 -0
- package/src/server.ts +108 -0
- package/src/stream/anthropic-stream.ts +310 -0
- package/src/stream/chunk.ts +46 -0
- package/src/stream/gemini-stream.ts +158 -0
- package/src/stream/index.ts +23 -0
- package/src/stream/openai-stream.ts +41 -0
- package/src/stream/sse.ts +72 -0
- package/src/translator/thinking.ts +64 -0
- package/src/translator/thinkingUnified.ts +319 -0
- package/src/upstream/client.ts +155 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from "react";
|
|
4
|
+
import { AreaChart, type SeriesPoint } from "@/components/AreaChart";
|
|
5
|
+
import { Stat, fmt, Empty } from "@/components/ui";
|
|
6
|
+
import { RichCard } from "@/components/RichCard";
|
|
7
|
+
import type { UsageSummary } from "@/lib/gateway";
|
|
8
|
+
|
|
9
|
+
type Window = { label: string; ms: number; bucketMs: number };
|
|
10
|
+
|
|
11
|
+
// window -> (lookback, chart bucket size). Buckets keep ~24-48 points per range.
|
|
12
|
+
const WINDOWS: Window[] = [
|
|
13
|
+
{ label: "24h", ms: 24 * 3600_000, bucketMs: 3600_000 },
|
|
14
|
+
{ label: "7d", ms: 7 * 86400_000, bucketMs: 6 * 3600_000 },
|
|
15
|
+
{ label: "30d", ms: 30 * 86400_000, bucketMs: 86400_000 },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function UsageView() {
|
|
19
|
+
const [win, setWin] = useState<Window>(WINDOWS[0]!);
|
|
20
|
+
const [summary, setSummary] = useState<UsageSummary | null>(null);
|
|
21
|
+
const [series, setSeries] = useState<SeriesPoint[]>([]);
|
|
22
|
+
const [error, setError] = useState("");
|
|
23
|
+
const [loading, setLoading] = useState(true);
|
|
24
|
+
|
|
25
|
+
const load = useCallback(async (w: Window) => {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError("");
|
|
28
|
+
const since = Date.now() - w.ms;
|
|
29
|
+
const [sumRes, serRes] = await Promise.all([
|
|
30
|
+
fetch(`/api/gw/admin/usage?since=${since}`),
|
|
31
|
+
fetch(`/api/gw/admin/usage/series?since=${since}&bucket=${w.bucketMs}`),
|
|
32
|
+
]);
|
|
33
|
+
if (!sumRes.ok) {
|
|
34
|
+
setError("could not load usage");
|
|
35
|
+
setLoading(false);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
setSummary((await sumRes.json()) as UsageSummary);
|
|
39
|
+
const ser = serRes.ok ? ((await serRes.json()) as { series: SeriesPoint[] }) : { series: [] };
|
|
40
|
+
setSeries(ser.series);
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
void load(win);
|
|
46
|
+
}, [win, load]);
|
|
47
|
+
|
|
48
|
+
const total = summary?.total;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div>
|
|
52
|
+
<div className="mb-6 flex items-center justify-between gap-3">
|
|
53
|
+
<div>
|
|
54
|
+
<h1 className="text-[22px] font-semibold tracking-tight text-text">Usage</h1>
|
|
55
|
+
<p className="mt-1 text-[13px] text-text-muted">Tokens and cost across providers and models.</p>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="flex items-center gap-1 rounded-full border border-border bg-surface p-1">
|
|
58
|
+
{WINDOWS.map((w) => (
|
|
59
|
+
<button
|
|
60
|
+
key={w.label}
|
|
61
|
+
onClick={() => setWin(w)}
|
|
62
|
+
className={`rounded-full px-3 py-1 text-[12px] font-medium transition-colors ${
|
|
63
|
+
win.label === w.label ? "bg-surface-2 text-text" : "text-text-muted hover:text-text"
|
|
64
|
+
}`}
|
|
65
|
+
>
|
|
66
|
+
{w.label}
|
|
67
|
+
</button>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{error ? (
|
|
73
|
+
<Empty>{error}</Empty>
|
|
74
|
+
) : (
|
|
75
|
+
<>
|
|
76
|
+
<div className="mb-5 grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
77
|
+
<Stat label="Requests" value={fmt.int(total?.requests ?? 0)} />
|
|
78
|
+
<Stat label="Tokens in" value={fmt.compact(total?.tokens_in ?? 0)} />
|
|
79
|
+
<Stat label="Tokens out" value={fmt.compact(total?.tokens_out ?? 0)} />
|
|
80
|
+
<Stat label="Cost" value={fmt.cost(total?.cost ?? 0)} />
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div className="mb-5">
|
|
84
|
+
<AreaChart series={series} />
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div className="grid gap-4 lg:grid-cols-2">
|
|
88
|
+
<RichCard header={<span className="text-[13px] font-semibold text-text">By provider</span>}>
|
|
89
|
+
{summary?.by_provider.length ? (
|
|
90
|
+
<BreakdownTable
|
|
91
|
+
rows={summary.by_provider.map((p) => ({ label: p.provider, ...p }))}
|
|
92
|
+
loading={loading}
|
|
93
|
+
/>
|
|
94
|
+
) : (
|
|
95
|
+
<Empty>No usage in this window.</Empty>
|
|
96
|
+
)}
|
|
97
|
+
</RichCard>
|
|
98
|
+
<RichCard header={<span className="text-[13px] font-semibold text-text">By model</span>}>
|
|
99
|
+
{summary?.by_model.length ? (
|
|
100
|
+
<BreakdownTable
|
|
101
|
+
rows={summary.by_model.map((m) => ({ label: `${m.alias} → ${m.model}`, ...m }))}
|
|
102
|
+
loading={loading}
|
|
103
|
+
/>
|
|
104
|
+
) : (
|
|
105
|
+
<Empty>No usage in this window.</Empty>
|
|
106
|
+
)}
|
|
107
|
+
</RichCard>
|
|
108
|
+
</div>
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function BreakdownTable({
|
|
116
|
+
rows,
|
|
117
|
+
loading,
|
|
118
|
+
}: {
|
|
119
|
+
rows: Array<{ label: string; requests: number; tokens_in: number; tokens_out: number; cost: number }>;
|
|
120
|
+
loading: boolean;
|
|
121
|
+
}) {
|
|
122
|
+
return (
|
|
123
|
+
<div className={`table-wrap ${loading ? "opacity-50" : ""}`}>
|
|
124
|
+
<table className="w-full border-collapse">
|
|
125
|
+
<thead>
|
|
126
|
+
<tr className="text-text-subtle">
|
|
127
|
+
{["", "Reqs", "In", "Out", "Cost"].map((h, i) => (
|
|
128
|
+
<th
|
|
129
|
+
key={h + i}
|
|
130
|
+
className={`pb-2 text-[10px] font-medium uppercase tracking-wider ${i === 0 ? "text-left" : "text-right"}`}
|
|
131
|
+
>
|
|
132
|
+
{h}
|
|
133
|
+
</th>
|
|
134
|
+
))}
|
|
135
|
+
</tr>
|
|
136
|
+
</thead>
|
|
137
|
+
<tbody>
|
|
138
|
+
{rows.map((r) => (
|
|
139
|
+
<tr key={r.label} className="border-t border-border-subtle">
|
|
140
|
+
<td className="py-2 text-[12.5px] text-text">{r.label}</td>
|
|
141
|
+
<td className="py-2 text-right tnum text-[12.5px] text-text-muted">{fmt.int(r.requests)}</td>
|
|
142
|
+
<td className="py-2 text-right tnum text-[12.5px] text-text-muted">{fmt.compact(r.tokens_in)}</td>
|
|
143
|
+
<td className="py-2 text-right tnum text-[12.5px] text-text-muted">{fmt.compact(r.tokens_out)}</td>
|
|
144
|
+
<td className="py-2 text-right tnum text-[12.5px] text-text">{fmt.cost(r.cost)}</td>
|
|
145
|
+
</tr>
|
|
146
|
+
))}
|
|
147
|
+
</tbody>
|
|
148
|
+
</table>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Small formatting helpers shared across the console. */
|
|
2
|
+
export const fmt = {
|
|
3
|
+
int(n: number): string {
|
|
4
|
+
return n.toLocaleString("en-US");
|
|
5
|
+
},
|
|
6
|
+
/** compact number: 1.2K, 3.4M */
|
|
7
|
+
compact(n: number): string {
|
|
8
|
+
if (Math.abs(n) >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
9
|
+
if (Math.abs(n) >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
10
|
+
return String(Math.round(n));
|
|
11
|
+
},
|
|
12
|
+
cost(n: number): string {
|
|
13
|
+
if (n === 0) return "$0";
|
|
14
|
+
return n < 0.01 ? `$${n.toFixed(4)}` : `$${n.toFixed(2)}`;
|
|
15
|
+
},
|
|
16
|
+
time(ts: number): string {
|
|
17
|
+
return new Date(ts).toLocaleString("en-US", { hour12: false });
|
|
18
|
+
},
|
|
19
|
+
/** "3m", "2h", "5d" — coarse relative age */
|
|
20
|
+
ago(ts: number): string {
|
|
21
|
+
const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
|
|
22
|
+
if (s < 60) return `${s}s`;
|
|
23
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`;
|
|
24
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h`;
|
|
25
|
+
return `${Math.floor(s / 86400)}d`;
|
|
26
|
+
},
|
|
27
|
+
/** ms duration -> "Xs", "Xm Ys", "Xh Ym" for quota/cooldown countdowns */
|
|
28
|
+
duration(ms: number): string {
|
|
29
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
30
|
+
if (s < 60) return `${s}s`;
|
|
31
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
32
|
+
const h = Math.floor(s / 3600);
|
|
33
|
+
const m = Math.floor((s % 3600) / 60);
|
|
34
|
+
if (h < 24) return `${h}h ${m}m`;
|
|
35
|
+
const d = Math.floor(h / 24);
|
|
36
|
+
return `${d}d ${h % 24}h`;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** A labelled stat block — number over a caption. */
|
|
41
|
+
export function Stat({ label, value, sub }: { label: string; value: React.ReactNode; sub?: string }) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="rounded-brand-lg border border-border bg-surface px-4 py-3.5 shadow-soft">
|
|
44
|
+
<div className="text-[11px] font-medium uppercase tracking-wider text-text-subtle">{label}</div>
|
|
45
|
+
<div className="mt-1 tnum text-[22px] font-semibold tracking-tight text-text">{value}</div>
|
|
46
|
+
{sub && <div className="mt-0.5 text-[12px] text-text-muted">{sub}</div>}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Empty-state hint inside a card body. */
|
|
52
|
+
export function Empty({ children }: { children: React.ReactNode }) {
|
|
53
|
+
return <div className="px-1 py-8 text-center text-[13px] text-text-muted">{children}</div>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// Model capabilities — what each model can read/do beyond plain text.
|
|
2
|
+
//
|
|
3
|
+
// Fallback order (first match wins), result merged over DEFAULT_CAPABILITIES:
|
|
4
|
+
// 1. PROVIDER_CAPABILITIES[provider][model] — provider-specific override
|
|
5
|
+
// 2. MODEL_CAPABILITIES[model] — canonical exact id (handles exceptions)
|
|
6
|
+
// 3. PATTERN_CAPABILITIES — glob match, ordered specific -> generic
|
|
7
|
+
// 4. DEFAULT_CAPABILITIES — safe floor (always returned)
|
|
8
|
+
//
|
|
9
|
+
// ── HOW TO ADD / UPDATE A MODEL ──────────────────────────────────────
|
|
10
|
+
// Authoritative data source: https://models.dev/api.json (145 providers, 4000+
|
|
11
|
+
// models, MIT). Each model exposes the exact fields we map below:
|
|
12
|
+
// modalities.input ["text","image","pdf","audio","video"] -> vision / pdf / audioInput / videoInput
|
|
13
|
+
// modalities.output ["text","image","audio"] -> imageOutput / audioOutput
|
|
14
|
+
// reasoning -> reasoning tool_call -> tools
|
|
15
|
+
// limit.context -> contextWindow limit.output -> maxOutput
|
|
16
|
+
// Look up the model id, then:
|
|
17
|
+
// • If a PATTERN below already covers it correctly -> nothing to do.
|
|
18
|
+
// • If it is an exception (pattern would mis-match) -> add an exact entry to
|
|
19
|
+
// MODEL_CAPABILITIES (only the fields that differ from DEFAULT).
|
|
20
|
+
// • If a whole new family -> add an ordered PATTERN (specific before generic).
|
|
21
|
+
// NOTE: models.dev has NO "search" flag (web search is a runtime tool, not a
|
|
22
|
+
// model spec); set `search` from vendor docs (Claude 4.x+, GPT-5.x/4o, Gemini
|
|
23
|
+
// 2.0+, Grok, Perplexity). Verify with: curl -s https://models.dev/api.json
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Safe floor — every resolved result is merged over this so consumers
|
|
28
|
+
* never need null-checks. Most modern LLMs meet these limits.
|
|
29
|
+
*/
|
|
30
|
+
export interface Caps {
|
|
31
|
+
vision: boolean;
|
|
32
|
+
pdf: boolean;
|
|
33
|
+
audioInput: boolean;
|
|
34
|
+
videoInput: boolean;
|
|
35
|
+
imageOutput: boolean;
|
|
36
|
+
audioOutput: boolean;
|
|
37
|
+
search: boolean;
|
|
38
|
+
tools: boolean;
|
|
39
|
+
reasoning: boolean;
|
|
40
|
+
thinkingFormat: string | null;
|
|
41
|
+
thinkingCanDisable: boolean;
|
|
42
|
+
thinkingRange: { min: number; max: number } | null;
|
|
43
|
+
contextWindow: number;
|
|
44
|
+
maxOutput: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Glob (* = wildcard) match, anchored + case-insensitive. aigetwey's
|
|
49
|
+
* pricing.matchPattern so capabilities resolve identically.
|
|
50
|
+
*/
|
|
51
|
+
export function matchPattern(pattern: string, model: string): boolean {
|
|
52
|
+
const regex = new RegExp(
|
|
53
|
+
"^" + pattern.split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$",
|
|
54
|
+
"i",
|
|
55
|
+
);
|
|
56
|
+
return regex.test(model);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const DEFAULT_CAPABILITIES: Caps = {
|
|
60
|
+
// input modalities
|
|
61
|
+
vision: false, // read images
|
|
62
|
+
pdf: false, // read PDF / documents
|
|
63
|
+
audioInput: false, // read audio
|
|
64
|
+
videoInput: false, // read video
|
|
65
|
+
// output modalities
|
|
66
|
+
imageOutput: false, // generate images
|
|
67
|
+
audioOutput: false, // generate audio
|
|
68
|
+
// features
|
|
69
|
+
search: false, // built-in web search tool / grounding
|
|
70
|
+
tools: true, // function / tool calling
|
|
71
|
+
reasoning: false, // thinking / reasoning
|
|
72
|
+
// thinking wire format (only meaningful when reasoning:true). null → derive from transport.format.
|
|
73
|
+
// enum: openai|claude-adaptive|claude-budget|gemini-level|gemini-budget|zai|qwen|deepseek|kimi|minimax|hunyuan|step
|
|
74
|
+
thinkingFormat: null,
|
|
75
|
+
thinkingCanDisable: true, // false → model cannot turn thinking off (clamp to min instead of disable)
|
|
76
|
+
thinkingRange: null, // { min, max } for budget formats; null = no clamp
|
|
77
|
+
// limits (tokens)
|
|
78
|
+
contextWindow: 200000,
|
|
79
|
+
maxOutput: 64000,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// User-added model metadata can carry dashboard service kinds instead of the
|
|
83
|
+
// runtime capability names used here. Map those typed model kinds into input /
|
|
84
|
+
// output capabilities so custom vision models are not treated as text-only.
|
|
85
|
+
const SERVICE_KIND_CAPABILITIES: Record<string, Partial<Caps>> = {
|
|
86
|
+
imageToText: { vision: true },
|
|
87
|
+
image: { imageOutput: true },
|
|
88
|
+
stt: { audioInput: true },
|
|
89
|
+
tts: { audioOutput: true },
|
|
90
|
+
embedding: { tools: false },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export function capabilitiesFromServiceKind(kind: string): Partial<Caps> | null {
|
|
94
|
+
return SERVICE_KIND_CAPABILITIES[kind] || null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Canonical exact-id overrides — used for exceptions that patterns would
|
|
99
|
+
* otherwise mis-match. Only declare deltas vs DEFAULT.
|
|
100
|
+
*/
|
|
101
|
+
export const MODEL_CAPABILITIES: Record<string, Partial<Caps>> = {
|
|
102
|
+
// Claude 4.6/4.7 have 1M context + adaptive thinking (override generic claude pattern)
|
|
103
|
+
"claude-opus-4.6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
|
|
104
|
+
"claude-opus-4.7": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
|
|
105
|
+
"claude-opus-4-6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
|
|
106
|
+
"claude-sonnet-4.6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 64000 },
|
|
107
|
+
"claude-sonnet-4-6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 64000 },
|
|
108
|
+
|
|
109
|
+
// Gemini image-gen / OpenAI image / xai image variants
|
|
110
|
+
"gpt-image-1": { imageOutput: true, tools: false },
|
|
111
|
+
|
|
112
|
+
// GLM vision variant (text GLM has no vision)
|
|
113
|
+
"glm-4.6v": { vision: true, reasoning: true, thinkingFormat: "zai", contextWindow: 128000 },
|
|
114
|
+
|
|
115
|
+
// Qwen plain coder/text (no vision) — registry "vision-model" / "coder-model" aliases
|
|
116
|
+
"vision-model": { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 },
|
|
117
|
+
"coder-model": { reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Provider-specific capability overrides. Keyed by provider alias/id.
|
|
122
|
+
*/
|
|
123
|
+
export const PROVIDER_CAPABILITIES: Record<string, Record<string, Partial<Caps>>> = {
|
|
124
|
+
// CodeBuddy.cn — authoritative per-model metadata from the gateway's model
|
|
125
|
+
// config (contextWindow=maxInputTokens, maxOutput=maxOutputTokens, vision=
|
|
126
|
+
// supportsImages). Every model reasons via OpenAI-style reasoning_effort
|
|
127
|
+
// (see registry thinkingFormat). `onlyReasoning` models can't turn thinking
|
|
128
|
+
// off → thinkingCanDisable:false (clamped to minimal instead of disabled).
|
|
129
|
+
"codebuddy-cn": {
|
|
130
|
+
"glm-5.2": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 48000 },
|
|
131
|
+
"glm-5.1": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 },
|
|
132
|
+
"glm-5.0": { reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 48000 },
|
|
133
|
+
"glm-5.0-turbo": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 },
|
|
134
|
+
"glm-5v-turbo": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 38000 },
|
|
135
|
+
"glm-4.7": { reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 48000 },
|
|
136
|
+
"minimax-m3": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 512000, maxOutput: 48000 },
|
|
137
|
+
"minimax-m2.7": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 },
|
|
138
|
+
"kimi-k2.7": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 256000, maxOutput: 32000 },
|
|
139
|
+
"kimi-k2.6": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 256000, maxOutput: 32000 },
|
|
140
|
+
"kimi-k2.5": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 164000, maxOutput: 32000 },
|
|
141
|
+
"hy3-preview": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 192000, maxOutput: 64000 },
|
|
142
|
+
"deepseek-v4-pro": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 },
|
|
143
|
+
"deepseek-v4-flash": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 },
|
|
144
|
+
"deepseek-v3-2-volc": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 96000, maxOutput: 32000 },
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Pattern fallback — glob (* = wildcard), matched case-insensitively and
|
|
150
|
+
* anchored (^...$) so a pattern must match the full model id. ORDER MATTERS:
|
|
151
|
+
* vision/specific variants first, text-only/generic families last, to avoid
|
|
152
|
+
* a broad family pattern swallowing an exception (e.g. glm-4.6v vs glm-5).
|
|
153
|
+
*/
|
|
154
|
+
export const PATTERN_CAPABILITIES: Array<{ pattern: string; caps: Partial<Caps> }> = [
|
|
155
|
+
// ── Claude (4.6+ = adaptive thinking; older/haiku = budget) ──────
|
|
156
|
+
{ pattern: "*claude*opus-4.6*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
|
|
157
|
+
{ pattern: "*claude*opus-4.7*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
|
|
158
|
+
{ pattern: "*claude*opus-4.8*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
|
|
159
|
+
{ pattern: "*claude*sonnet-4.6*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
|
|
160
|
+
{ pattern: "*claude*sonnet-4.7*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
|
|
161
|
+
{ pattern: "*claude*haiku*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } },
|
|
162
|
+
{ pattern: "*claude*opus*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } },
|
|
163
|
+
{ pattern: "*claude*sonnet*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } },
|
|
164
|
+
{ pattern: "*claude*fable*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget", contextWindow: 1000000, maxOutput: 128000 } },
|
|
165
|
+
{ pattern: "*claude*mythos*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget", contextWindow: 1000000, maxOutput: 128000 } },
|
|
166
|
+
{ pattern: "*claude-3*", caps: { vision: true } },
|
|
167
|
+
{ pattern: "*claude*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } },
|
|
168
|
+
|
|
169
|
+
// ── Gemini (all 2.0+ multimodal + google_search grounding, 1M ctx) ─
|
|
170
|
+
{ pattern: "*gemini*image*", caps: { vision: true, imageOutput: true, contextWindow: 1048576 } },
|
|
171
|
+
{ pattern: "*gemini-3*pro*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65535 } },
|
|
172
|
+
{ pattern: "*gemini-3*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65536 } },
|
|
173
|
+
{ pattern: "*gemini-2.5*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-budget", thinkingRange: { min: 0, max: 24576 }, contextWindow: 1048576, maxOutput: 65536 } },
|
|
174
|
+
{ pattern: "*gemini-2*", caps: { vision: true, audioInput: true, videoInput: true, search: true, contextWindow: 1048576, maxOutput: 65536 } },
|
|
175
|
+
{ pattern: "*gemini*", caps: { vision: true, search: true, contextWindow: 1048576 } },
|
|
176
|
+
{ pattern: "*gemma*", caps: { vision: true, contextWindow: 128000 } },
|
|
177
|
+
{ pattern: "*nanobanana*", caps: { vision: true, imageOutput: true } },
|
|
178
|
+
|
|
179
|
+
// ── OpenAI GPT-5.x (vision + thinking + web search) ──────────────
|
|
180
|
+
{ pattern: "*gpt-5*image*", caps: { imageOutput: true } },
|
|
181
|
+
{ pattern: "*gpt-5*codex*", caps: { reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 400000, maxOutput: 128000 } },
|
|
182
|
+
{ pattern: "*gpt-5*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 400000, maxOutput: 128000 } },
|
|
183
|
+
{ pattern: "*gpt-4o*", caps: { vision: true, search: true, contextWindow: 128000, maxOutput: 16384 } },
|
|
184
|
+
{ pattern: "*gpt-4.1*", caps: { vision: true, contextWindow: 1000000, maxOutput: 32768 } },
|
|
185
|
+
{ pattern: "*gpt-4-turbo*", caps: { vision: true, contextWindow: 128000 } },
|
|
186
|
+
{ pattern: "*gpt-4*", caps: { contextWindow: 128000 } },
|
|
187
|
+
{ pattern: "*gpt-3.5*", caps: { contextWindow: 16385, maxOutput: 4096 } },
|
|
188
|
+
{ pattern: "*gpt-oss*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 128000 } },
|
|
189
|
+
|
|
190
|
+
// ── OpenAI o-series (reasoning, vision) ──────────────────────────
|
|
191
|
+
{ pattern: "*o1-mini*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 128000 } },
|
|
192
|
+
{ pattern: "*o1*", caps: { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 100000 } },
|
|
193
|
+
{ pattern: "*o3*", caps: { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 100000 } },
|
|
194
|
+
{ pattern: "*o4*", caps: { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 100000 } },
|
|
195
|
+
|
|
196
|
+
// ── Grok (vision + Live Search) ──────────────────────────────────
|
|
197
|
+
{ pattern: "*grok*image*", caps: { imageOutput: true } },
|
|
198
|
+
{ pattern: "*grok-code*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 256000 } },
|
|
199
|
+
{ pattern: "*grok-4*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 256000 } },
|
|
200
|
+
{ pattern: "*grok-3*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 131072 } },
|
|
201
|
+
{ pattern: "*grok*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 256000 } },
|
|
202
|
+
|
|
203
|
+
// ── Qwen (enable_thinking + thinking_budget; QwQ = thinking-only) ─
|
|
204
|
+
{ pattern: "*qwen*vl*", caps: { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } },
|
|
205
|
+
{ pattern: "*qwen*max*", caps: { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000, maxOutput: 65536 } },
|
|
206
|
+
{ pattern: "*qwen*plus*", caps: { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000, maxOutput: 65536 } },
|
|
207
|
+
{ pattern: "*qwen*235b*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } },
|
|
208
|
+
{ pattern: "*qwen*coder*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 } },
|
|
209
|
+
{ pattern: "*qwq*", caps: { reasoning: true, thinkingFormat: "qwen", thinkingCanDisable: false, contextWindow: 131072 } },
|
|
210
|
+
{ pattern: "*qwen*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } },
|
|
211
|
+
|
|
212
|
+
// ── Kimi (enabled→reasoning_effort; K2.7-code cannot disable) ─────
|
|
213
|
+
{ pattern: "*kimi*k2.7*code*", caps: { vision: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 262144 } },
|
|
214
|
+
{ pattern: "*kimi*k2*", caps: { vision: true, reasoning: true, thinkingFormat: "kimi", contextWindow: 262144, maxOutput: 262144 } },
|
|
215
|
+
{ pattern: "*kimi*", caps: { reasoning: true, thinkingFormat: "kimi", contextWindow: 262144 } },
|
|
216
|
+
|
|
217
|
+
// ── GLM / Z.ai (thinking.enabled; disable via enable_thinking:false) ─
|
|
218
|
+
{ pattern: "*glm-5*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } },
|
|
219
|
+
{ pattern: "*glm-4.7*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } },
|
|
220
|
+
{ pattern: "*glm-4*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000 } },
|
|
221
|
+
{ pattern: "*glm*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000 } },
|
|
222
|
+
|
|
223
|
+
// ── DeepSeek (thinking.enabled + reasoning_effort; r1 = thinking-only) ─
|
|
224
|
+
{ pattern: "*deepseek-v4*", caps: { reasoning: true, thinkingFormat: "deepseek", contextWindow: 1000000, maxOutput: 384000 } },
|
|
225
|
+
{ pattern: "*reasoner*", caps: { reasoning: true, thinkingFormat: "deepseek", thinkingCanDisable: false, contextWindow: 128000 } },
|
|
226
|
+
{ pattern: "*deepseek-r*", caps: { reasoning: true, thinkingFormat: "deepseek", thinkingCanDisable: false, contextWindow: 128000 } },
|
|
227
|
+
{ pattern: "*deepseek-chat*", caps: { contextWindow: 128000 } },
|
|
228
|
+
{ pattern: "*deepseek*", caps: { reasoning: true, thinkingFormat: "deepseek", contextWindow: 128000 } },
|
|
229
|
+
|
|
230
|
+
// ── MiniMax (M3 = adaptive; M2.x cannot disable) ─────────────────
|
|
231
|
+
{ pattern: "*minimax*image*", caps: { imageOutput: true } },
|
|
232
|
+
{ pattern: "*minimax-m3*", caps: { vision: true, reasoning: true, thinkingFormat: "minimax", contextWindow: 1048576, maxOutput: 512000 } },
|
|
233
|
+
{ pattern: "*minimax-m2.7*", caps: { reasoning: true, thinkingFormat: "minimax", thinkingCanDisable: false, contextWindow: 204800, maxOutput: 131072 } },
|
|
234
|
+
{ pattern: "*minimax*", caps: { reasoning: true, thinkingFormat: "minimax", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 131072 } },
|
|
235
|
+
|
|
236
|
+
// ── Xiaomi MiMo (vision, 1M / 262K ctx) ──────────────────────────
|
|
237
|
+
{ pattern: "*mimo*v2.5*", caps: { vision: true, contextWindow: 1048576, maxOutput: 131072 } },
|
|
238
|
+
{ pattern: "*mimo*omni*", caps: { vision: true, audioInput: true, contextWindow: 262144, maxOutput: 131072 } },
|
|
239
|
+
{ pattern: "*mimo*", caps: { vision: true, contextWindow: 262144, maxOutput: 131072 } },
|
|
240
|
+
|
|
241
|
+
// ── Llama (4 = vision/1M; 3.x = text-only/128K) ──────────────────
|
|
242
|
+
{ pattern: "*llama-4*", caps: { vision: true, contextWindow: 1000000 } },
|
|
243
|
+
{ pattern: "*llama*", caps: { contextWindow: 128000 } },
|
|
244
|
+
|
|
245
|
+
// ── Mistral (Large 3 = vision/256K; codestral text) ──────────────
|
|
246
|
+
{ pattern: "*codestral*", caps: { contextWindow: 256000 } },
|
|
247
|
+
{ pattern: "*mistral-large*", caps: { vision: true, contextWindow: 256000 } },
|
|
248
|
+
{ pattern: "*mistral*", caps: { contextWindow: 128000 } },
|
|
249
|
+
|
|
250
|
+
// ── Cohere (Command A Vision = vision; others text) ──────────────
|
|
251
|
+
{ pattern: "*command-a-vision*", caps: { vision: true, contextWindow: 128000 } },
|
|
252
|
+
{ pattern: "*command*", caps: { contextWindow: 128000 } },
|
|
253
|
+
|
|
254
|
+
// ── Perplexity (web search native) ───────────────────────────────
|
|
255
|
+
{ pattern: "*sonar*", caps: { search: true, contextWindow: 128000 } },
|
|
256
|
+
{ pattern: "*pplx*", caps: { search: true, contextWindow: 128000 } },
|
|
257
|
+
{ pattern: "*perplexity*", caps: { search: true, contextWindow: 128000 } },
|
|
258
|
+
|
|
259
|
+
// ── Others ───────────────────────────────────────────────────────
|
|
260
|
+
{ pattern: "*hunyuan*", caps: { reasoning: true, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } },
|
|
261
|
+
{ pattern: "hy3*", caps: { reasoning: true, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } },
|
|
262
|
+
{ pattern: "*step-*", caps: { reasoning: true, thinkingFormat: "step", contextWindow: 128000 } },
|
|
263
|
+
{ pattern: "*nemotron*", caps: { reasoning: true, contextWindow: 128000 } },
|
|
264
|
+
{ pattern: "*ling-*", caps: { reasoning: true, contextWindow: 128000 } },
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Resolve capabilities for a model using the 4-step fallback chain,
|
|
269
|
+
* merged over DEFAULT_CAPABILITIES so the result is always complete.
|
|
270
|
+
*
|
|
271
|
+
* @param {string} provider
|
|
272
|
+
* @param {string} model
|
|
273
|
+
* @returns {object} full capabilities object
|
|
274
|
+
*/
|
|
275
|
+
/**
|
|
276
|
+
* models.dev-style modalities for a model ref, derived from its capabilities.
|
|
277
|
+
* Ref may be a `provider/model` (vendor prefix tolerated) or a bare combo alias.
|
|
278
|
+
* Mirrors what opencode/aigetwey store per model in opencode.json.
|
|
279
|
+
*/
|
|
280
|
+
export function modalitiesForModel(ref: string): { input: string[]; output: string[] } {
|
|
281
|
+
const slash = ref.indexOf("/");
|
|
282
|
+
const provider = slash > 0 ? ref.slice(0, slash) : null;
|
|
283
|
+
const model = slash > 0 ? ref.slice(slash + 1) : ref;
|
|
284
|
+
const c = getCapabilitiesForModel(provider, model);
|
|
285
|
+
const input = ["text"];
|
|
286
|
+
if (c.vision) input.push("image");
|
|
287
|
+
if (c.pdf) input.push("pdf");
|
|
288
|
+
if (c.audioInput) input.push("audio");
|
|
289
|
+
if (c.videoInput) input.push("video");
|
|
290
|
+
const output = ["text"];
|
|
291
|
+
if (c.imageOutput) output.push("image");
|
|
292
|
+
if (c.audioOutput) output.push("audio");
|
|
293
|
+
return { input, output };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function getCapabilitiesForModel(provider: string | null, model: string): Caps {
|
|
297
|
+
if (!model) return { ...DEFAULT_CAPABILITIES };
|
|
298
|
+
|
|
299
|
+
// 1. Provider-specific override
|
|
300
|
+
if (provider && PROVIDER_CAPABILITIES[provider]?.[model]) {
|
|
301
|
+
return { ...DEFAULT_CAPABILITIES, ...PROVIDER_CAPABILITIES[provider][model] };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 2. Canonical exact (strip vendor prefix: "anthropic/claude-opus-4.7" -> "claude-opus-4.7")
|
|
305
|
+
const baseModel = (model.includes("/") ? model.split("/").pop() : model) ?? model;
|
|
306
|
+
if (MODEL_CAPABILITIES[baseModel]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[baseModel] };
|
|
307
|
+
if (MODEL_CAPABILITIES[model]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[model] };
|
|
308
|
+
|
|
309
|
+
// 3. Pattern match (first match wins)
|
|
310
|
+
for (const { pattern, caps } of PATTERN_CAPABILITIES) {
|
|
311
|
+
if (matchPattern(pattern, baseModel) || matchPattern(pattern, model)) {
|
|
312
|
+
return { ...DEFAULT_CAPABILITIES, ...caps };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 4. Floor
|
|
317
|
+
return { ...DEFAULT_CAPABILITIES };
|
|
318
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI tool setup definitions. Each tool talks to the gateway in either OpenAI or
|
|
3
|
+
* Anthropic wire format; the gateway exposes both on the same port, so setup is
|
|
4
|
+
* just pointing the tool's base_url + key at us. `env` builders take the live
|
|
5
|
+
* gateway base URL + a gateway key and return ready-to-copy environment lines.
|
|
6
|
+
*
|
|
7
|
+
* `slots` are the model names this tool calls. Because our gateway routes by the
|
|
8
|
+
* combo alias (the alias IS the model name), the detail page checks whether a
|
|
9
|
+
* combo with each slot's alias exists, and prompts to create the missing ones.
|
|
10
|
+
*/
|
|
11
|
+
export type ToolFormat = "openai" | "anthropic";
|
|
12
|
+
|
|
13
|
+
export interface CliTool {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
/** Material Symbols ligature shown on the tool card + detail header. */
|
|
17
|
+
icon: string;
|
|
18
|
+
format: ToolFormat;
|
|
19
|
+
blurb: string;
|
|
20
|
+
/** true when the dashboard can detect + write this tool's local config file. */
|
|
21
|
+
autoConfig?: boolean;
|
|
22
|
+
/** one-line install command, when the tool ships via a package manager. */
|
|
23
|
+
install?: string;
|
|
24
|
+
/** model names the tool sends — pair each with a combo of the same name. */
|
|
25
|
+
slots: { label: string; alias: string }[];
|
|
26
|
+
/** environment variables to set, given the gateway base + a key. */
|
|
27
|
+
env: (base: string, key: string) => Array<{ name: string; value: string }>;
|
|
28
|
+
/** extra free-form steps shown on the detail page. */
|
|
29
|
+
steps: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const KEY = (k: string) => k || "<your-gateway-key>";
|
|
33
|
+
|
|
34
|
+
export const CLI_TOOLS: CliTool[] = [
|
|
35
|
+
{
|
|
36
|
+
id: "claude-code",
|
|
37
|
+
name: "Claude Code",
|
|
38
|
+
icon: "smart_toy",
|
|
39
|
+
format: "anthropic",
|
|
40
|
+
blurb: "Anthropic CLI. Point its base URL + key at the gateway.",
|
|
41
|
+
autoConfig: true,
|
|
42
|
+
install: "npm i -g @anthropic-ai/claude-code",
|
|
43
|
+
slots: [
|
|
44
|
+
{ label: "Opus · heavy", alias: "claude-opus-4-1" },
|
|
45
|
+
{ label: "Sonnet · default", alias: "claude-sonnet-4-6" },
|
|
46
|
+
{ label: "Haiku · fast", alias: "claude-haiku-4-5" },
|
|
47
|
+
],
|
|
48
|
+
env: (base, key) => [
|
|
49
|
+
{ name: "ANTHROPIC_BASE_URL", value: base },
|
|
50
|
+
{ name: "ANTHROPIC_API_KEY", value: KEY(key) },
|
|
51
|
+
],
|
|
52
|
+
steps: [
|
|
53
|
+
"Export the two variables in the shell you run `claude` from.",
|
|
54
|
+
"Create a combo named like each slot above so Claude Code's model ids resolve.",
|
|
55
|
+
"The gateway translates Anthropic ↔ provider format, so any provider works behind it.",
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "codex",
|
|
60
|
+
name: "Codex",
|
|
61
|
+
icon: "code",
|
|
62
|
+
format: "openai",
|
|
63
|
+
blurb: "OpenAI-compatible. Use the /v1 base URL.",
|
|
64
|
+
install: "npm i -g @openai/codex",
|
|
65
|
+
slots: [{ label: "Model", alias: "gpt-5" }],
|
|
66
|
+
env: (base, key) => [
|
|
67
|
+
{ name: "OPENAI_BASE_URL", value: `${base}/v1` },
|
|
68
|
+
{ name: "OPENAI_API_KEY", value: KEY(key) },
|
|
69
|
+
],
|
|
70
|
+
steps: ["Set the base URL to the gateway's /v1 path.", "Create a combo named like the slot above, then use it as the model."],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "opencode",
|
|
74
|
+
name: "opencode",
|
|
75
|
+
icon: "code_blocks",
|
|
76
|
+
format: "openai",
|
|
77
|
+
blurb: "OpenAI-compatible provider. Set base_url to /v1.",
|
|
78
|
+
autoConfig: true,
|
|
79
|
+
install: "curl -fsSL https://opencode.ai/install | bash",
|
|
80
|
+
slots: [{ label: "Model", alias: "gpt-5" }],
|
|
81
|
+
env: (base, key) => [
|
|
82
|
+
{ name: "OPENAI_BASE_URL", value: `${base}/v1` },
|
|
83
|
+
{ name: "OPENAI_API_KEY", value: KEY(key) },
|
|
84
|
+
],
|
|
85
|
+
steps: ["Add an OpenAI-compatible provider with the gateway /v1 base URL.", "Pick a combo alias as the model."],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "cursor",
|
|
89
|
+
name: "Cursor",
|
|
90
|
+
icon: "edit_square",
|
|
91
|
+
format: "openai",
|
|
92
|
+
blurb: "OpenAI-compatible. Override the base URL in settings.",
|
|
93
|
+
slots: [{ label: "Model", alias: "gpt-5" }],
|
|
94
|
+
env: (base, key) => [
|
|
95
|
+
{ name: "Base URL", value: `${base}/v1` },
|
|
96
|
+
{ name: "API Key", value: KEY(key) },
|
|
97
|
+
],
|
|
98
|
+
steps: [
|
|
99
|
+
"Settings → Models → OpenAI API Key → override base URL with the gateway /v1.",
|
|
100
|
+
"Add your combo aliases as custom model names.",
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "cline",
|
|
105
|
+
name: "Cline",
|
|
106
|
+
icon: "extension",
|
|
107
|
+
format: "openai",
|
|
108
|
+
blurb: "OpenAI-compatible VS Code agent.",
|
|
109
|
+
slots: [{ label: "Model", alias: "gpt-5" }],
|
|
110
|
+
env: (base, key) => [
|
|
111
|
+
{ name: "Base URL", value: `${base}/v1` },
|
|
112
|
+
{ name: "API Key", value: KEY(key) },
|
|
113
|
+
],
|
|
114
|
+
steps: ["Choose the OpenAI-compatible provider.", "Set the base URL to the gateway /v1 and use a combo alias."],
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
export function toolById(id: string): CliTool | undefined {
|
|
119
|
+
return CLI_TOOLS.find((t) => t.id === id);
|
|
120
|
+
}
|