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,475 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { RichCard, CardTitle } from "@/components/RichCard";
|
|
6
|
+
import { Badge } from "@/components/Badge";
|
|
7
|
+
import { Button, Select } from "@/components/Button";
|
|
8
|
+
import { ModelPicker, type ModelGroup } from "@/components/ModelPicker";
|
|
9
|
+
import { Icon } from "@/components/Icon";
|
|
10
|
+
import { Empty } from "@/components/ui";
|
|
11
|
+
import { adminApi, cliConfig, type CliStatus } from "@/lib/client";
|
|
12
|
+
import { toolById } from "@/lib/cliTools";
|
|
13
|
+
import { modalitiesForModel } from "@/lib/capabilities";
|
|
14
|
+
import type { EndpointPayload, MaskedConfig } from "@/lib/gateway";
|
|
15
|
+
|
|
16
|
+
/** Step-by-step setup for one CLI tool, with copy-ready env (real key inlined). */
|
|
17
|
+
export function ToolDetail({ id }: { id: string }) {
|
|
18
|
+
const router = useRouter();
|
|
19
|
+
const tool = toolById(id);
|
|
20
|
+
const [ep, setEp] = useState<EndpointPayload | null>(null);
|
|
21
|
+
const [combos, setCombos] = useState<string[]>([]);
|
|
22
|
+
const [keyIdx, setKeyIdx] = useState(0);
|
|
23
|
+
const [realKey, setRealKey] = useState("");
|
|
24
|
+
const [error, setError] = useState("");
|
|
25
|
+
const [cli, setCli] = useState<CliStatus | null>(null);
|
|
26
|
+
const [cliBusy, setCliBusy] = useState<"" | "apply" | "reset">("");
|
|
27
|
+
const [cliMsg, setCliMsg] = useState("");
|
|
28
|
+
const [allModels, setAllModels] = useState<string[]>([]);
|
|
29
|
+
const [groups, setGroups] = useState<ModelGroup[]>([]);
|
|
30
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
31
|
+
const [picked, setPicked] = useState<string[]>([]); // openai tools: chosen models
|
|
32
|
+
const [active, setActive] = useState(""); // openai tools: default/active model
|
|
33
|
+
const [slots, setSlots] = useState({ opus: "", sonnet: "", haiku: "" }); // claude
|
|
34
|
+
const isAnthropic = tool?.format === "anthropic";
|
|
35
|
+
|
|
36
|
+
function togglePicked(v: string) {
|
|
37
|
+
if (picked.includes(v)) {
|
|
38
|
+
setPicked((p) => p.filter((x) => x !== v));
|
|
39
|
+
if (active === v) setActive("");
|
|
40
|
+
} else {
|
|
41
|
+
setPicked((p) => [...p, v]);
|
|
42
|
+
if (!active) setActive(v);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const loadCli = useCallback(async () => {
|
|
47
|
+
if (!tool?.autoConfig) return;
|
|
48
|
+
const r = await cliConfig.status(tool.id);
|
|
49
|
+
setCli(r.data);
|
|
50
|
+
}, [tool]);
|
|
51
|
+
useEffect(() => { void loadCli(); }, [loadCli]);
|
|
52
|
+
|
|
53
|
+
// seed the editable selection from whatever is already in the tool's config.
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!cli?.installed) return;
|
|
56
|
+
if (isAnthropic && cli.modelSlots) {
|
|
57
|
+
setSlots({ opus: cli.modelSlots.opus ?? "", sonnet: cli.modelSlots.sonnet ?? "", haiku: cli.modelSlots.haiku ?? "" });
|
|
58
|
+
} else if (!isAnthropic && cli.models && cli.models.length > 0) {
|
|
59
|
+
setPicked(cli.models);
|
|
60
|
+
setActive(cli.activeModel ?? cli.models[0] ?? "");
|
|
61
|
+
}
|
|
62
|
+
}, [cli, isAnthropic]);
|
|
63
|
+
|
|
64
|
+
async function applyCli() {
|
|
65
|
+
if (!tool || !ep) return;
|
|
66
|
+
setCliMsg("");
|
|
67
|
+
const baseUrl = `http://127.0.0.1:${ep.port}`; // gateway root; opencode route appends /v1
|
|
68
|
+
const key = ep.keys.length ? realKey || undefined : undefined;
|
|
69
|
+
if (isAnthropic) {
|
|
70
|
+
const m: Record<string, string> = {};
|
|
71
|
+
if (slots.opus) m.opus = slots.opus;
|
|
72
|
+
if (slots.sonnet) m.sonnet = slots.sonnet;
|
|
73
|
+
if (slots.haiku) m.haiku = slots.haiku;
|
|
74
|
+
setCliBusy("apply");
|
|
75
|
+
const r = await cliConfig.apply(tool.id, { base: baseUrl, key, models: m });
|
|
76
|
+
setCliBusy("");
|
|
77
|
+
setCliMsg(r.ok ? "Wrote config ✓" : r.error ?? "failed");
|
|
78
|
+
if (r.ok) void loadCli();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (picked.length === 0) { setCliMsg("add at least one model"); return; }
|
|
82
|
+
setCliBusy("apply");
|
|
83
|
+
const r = await cliConfig.apply(tool.id, { base: baseUrl, key, models: picked, active });
|
|
84
|
+
setCliBusy("");
|
|
85
|
+
setCliMsg(r.ok ? "Wrote config ✓" : r.error ?? "failed");
|
|
86
|
+
if (r.ok) void loadCli();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function resetCli() {
|
|
90
|
+
if (!tool) return;
|
|
91
|
+
setCliBusy("reset");
|
|
92
|
+
setCliMsg("");
|
|
93
|
+
const r = await cliConfig.reset(tool.id);
|
|
94
|
+
setCliBusy("");
|
|
95
|
+
if (r.ok) { setCliMsg("Removed gateway config ✓"); void loadCli(); }
|
|
96
|
+
else setCliMsg(r.error ?? "failed");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
void (async () => {
|
|
101
|
+
const [epRes, cfgRes] = await Promise.all([
|
|
102
|
+
fetch("/api/gw/admin/endpoint"),
|
|
103
|
+
fetch("/api/gw/admin/config"),
|
|
104
|
+
]);
|
|
105
|
+
if (!epRes.ok) {
|
|
106
|
+
setError("could not reach the gateway");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
setEp((await epRes.json()) as EndpointPayload);
|
|
110
|
+
if (cfgRes.ok) {
|
|
111
|
+
const cfg = (await cfgRes.json()) as MaskedConfig;
|
|
112
|
+
const aliases = cfg.models.map((m) => m.alias);
|
|
113
|
+
setCombos(aliases);
|
|
114
|
+
// everything callable: combo aliases + every provider/model ref.
|
|
115
|
+
const refs = cfg.providers.flatMap((p) => p.models.map((m) => `${p.id}/${m.id}`));
|
|
116
|
+
setAllModels([...aliases, ...refs]);
|
|
117
|
+
// grouped for the picker: Combos first, then one group per provider.
|
|
118
|
+
const grps: ModelGroup[] = [];
|
|
119
|
+
if (aliases.length) grps.push({ label: "Combos", items: aliases.map((a) => ({ value: a, label: a })) });
|
|
120
|
+
for (const p of cfg.providers) {
|
|
121
|
+
if (p.models.length) grps.push({ label: p.id, items: p.models.map((m) => ({ value: `${p.id}/${m.id}`, label: `${p.id}/${m.id}` })) });
|
|
122
|
+
}
|
|
123
|
+
setGroups(grps);
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
// reveal the selected gateway key so the env block is copy-ready (the whole
|
|
129
|
+
// point of this page is to paste a working config locally).
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!ep || ep.keys.length === 0) return;
|
|
132
|
+
void adminApi.revealServerKey(keyIdx).then((r) => setRealKey(r.ok ? r.data?.key ?? "" : ""));
|
|
133
|
+
}, [ep, keyIdx]);
|
|
134
|
+
|
|
135
|
+
if (!tool) return <Empty>Unknown tool.</Empty>;
|
|
136
|
+
if (error) return <Empty>{error}</Empty>;
|
|
137
|
+
if (!ep) return <Empty>Loading…</Empty>;
|
|
138
|
+
|
|
139
|
+
const base = `http://127.0.0.1:${ep.port}`;
|
|
140
|
+
const env = tool.env(base, realKey);
|
|
141
|
+
const block = env.map((e) => `export ${e.name}="${e.value}"`).join("\n");
|
|
142
|
+
|
|
143
|
+
// opencode reads models from ~/.config/opencode/opencode.json, not shell env.
|
|
144
|
+
// Show the exact JSON the auto-apply would MERGE — so the models are visible and
|
|
145
|
+
// it's clear nothing else gets replaced (other providers + your other keys stay).
|
|
146
|
+
const ocModels = picked.length ? picked : cli?.models ?? [];
|
|
147
|
+
const opencodeJson =
|
|
148
|
+
tool.id === "opencode"
|
|
149
|
+
? JSON.stringify(
|
|
150
|
+
{
|
|
151
|
+
provider: {
|
|
152
|
+
aigetwey: {
|
|
153
|
+
npm: "@ai-sdk/openai-compatible",
|
|
154
|
+
options: { baseURL: `${base}/v1`, apiKey: realKey || "aigetwey" },
|
|
155
|
+
models: Object.fromEntries(ocModels.map((m) => [m, { name: m, modalities: modalitiesForModel(m) }])),
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
model: `aigetwey/${active || ocModels[0] || ""}`,
|
|
159
|
+
},
|
|
160
|
+
null,
|
|
161
|
+
2,
|
|
162
|
+
)
|
|
163
|
+
: null;
|
|
164
|
+
|
|
165
|
+
// Claude Code reads its config from ~/.claude/settings.json — show the exact
|
|
166
|
+
// env block the auto-apply writes (merged into the existing settings).
|
|
167
|
+
const claudeJson =
|
|
168
|
+
tool.id === "claude-code"
|
|
169
|
+
? JSON.stringify(
|
|
170
|
+
{
|
|
171
|
+
hasCompletedOnboarding: true,
|
|
172
|
+
env: {
|
|
173
|
+
ANTHROPIC_BASE_URL: base,
|
|
174
|
+
API_TIMEOUT_MS: "600000",
|
|
175
|
+
...(realKey ? { ANTHROPIC_AUTH_TOKEN: realKey } : {}),
|
|
176
|
+
...(slots.opus ? { ANTHROPIC_DEFAULT_OPUS_MODEL: slots.opus } : {}),
|
|
177
|
+
...(slots.sonnet ? { ANTHROPIC_DEFAULT_SONNET_MODEL: slots.sonnet } : {}),
|
|
178
|
+
...(slots.haiku ? { ANTHROPIC_DEFAULT_HAIKU_MODEL: slots.haiku } : {}),
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
null,
|
|
182
|
+
2,
|
|
183
|
+
)
|
|
184
|
+
: null;
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div>
|
|
188
|
+
<button onClick={() => router.push("/tools")} className="mb-4 inline-flex items-center gap-1 text-[12px] text-text-muted hover:text-text">
|
|
189
|
+
<Icon name="arrow_back" size={15} /> CLI Tools
|
|
190
|
+
</button>
|
|
191
|
+
|
|
192
|
+
<div className="mb-6 flex items-center gap-3">
|
|
193
|
+
<span className="flex h-9 w-9 items-center justify-center rounded-brand bg-surface-2 text-text-muted">
|
|
194
|
+
<Icon name={tool.icon} size={20} />
|
|
195
|
+
</span>
|
|
196
|
+
<h1 className="text-[22px] font-semibold tracking-tight text-text">{tool.name}</h1>
|
|
197
|
+
<Badge tone="info">{tool.format}</Badge>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div className="grid gap-4 lg:grid-cols-2">
|
|
201
|
+
{tool.autoConfig && (
|
|
202
|
+
<RichCard
|
|
203
|
+
className="lg:col-span-2"
|
|
204
|
+
header={
|
|
205
|
+
<>
|
|
206
|
+
<CardTitle title="Local setup" sub="detect this tool on your machine and write its config for you" />
|
|
207
|
+
{cli && (
|
|
208
|
+
<Badge tone={!cli.installed ? "neutral" : cli.configured ? "live" : "warn"}>
|
|
209
|
+
{!cli.installed ? "not detected" : cli.configured ? "configured" : "detected"}
|
|
210
|
+
</Badge>
|
|
211
|
+
)}
|
|
212
|
+
</>
|
|
213
|
+
}
|
|
214
|
+
>
|
|
215
|
+
{!cli ? (
|
|
216
|
+
<p className="text-[12.5px] text-text-subtle">Checking your machine…</p>
|
|
217
|
+
) : !cli.installed ? (
|
|
218
|
+
<p className="text-[12.5px] text-text-muted">
|
|
219
|
+
Not found on this machine. Install it (above) or paste the manual env below — then re-open this page.
|
|
220
|
+
</p>
|
|
221
|
+
) : (
|
|
222
|
+
<div className="space-y-3">
|
|
223
|
+
<SetupRow label="Endpoint">
|
|
224
|
+
<span className="tnum text-[12.5px] text-text">{isAnthropic ? base : `${base}/v1`}</span>
|
|
225
|
+
</SetupRow>
|
|
226
|
+
|
|
227
|
+
{ep.keys.length > 0 && (
|
|
228
|
+
<SetupRow label="API Key">
|
|
229
|
+
<Select value={String(keyIdx)} onChange={(e) => setKeyIdx(Number(e.target.value))} className="max-w-[260px]">
|
|
230
|
+
{ep.keys.map((k, i) => (
|
|
231
|
+
<option key={i} value={i}>{k.name || `key ${i + 1}`}</option>
|
|
232
|
+
))}
|
|
233
|
+
</Select>
|
|
234
|
+
</SetupRow>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
{isAnthropic ? (
|
|
238
|
+
<SetupRow label="Models" top>
|
|
239
|
+
<div className="flex flex-col gap-2">
|
|
240
|
+
{(["opus", "sonnet", "haiku"] as const).map((slot) => (
|
|
241
|
+
<div key={slot} className="flex items-center gap-2">
|
|
242
|
+
<span className="w-16 text-[12px] capitalize text-text-subtle">{slot}</span>
|
|
243
|
+
<Select value={slots[slot]} onChange={(e) => setSlots((s) => ({ ...s, [slot]: e.target.value }))} className="flex-1">
|
|
244
|
+
<option value="">— none —</option>
|
|
245
|
+
{allModels.map((m) => <option key={m} value={m}>{m}</option>)}
|
|
246
|
+
</Select>
|
|
247
|
+
</div>
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
</SetupRow>
|
|
251
|
+
) : (
|
|
252
|
+
<SetupRow label="Models" top>
|
|
253
|
+
<div>
|
|
254
|
+
<div className="flex min-h-[34px] flex-wrap gap-1.5 rounded-brand border border-border-subtle bg-bg px-2 py-1.5">
|
|
255
|
+
{picked.length === 0 ? (
|
|
256
|
+
<span className="text-[12px] text-text-subtle">No models — add one below.</span>
|
|
257
|
+
) : (
|
|
258
|
+
picked.map((m) => (
|
|
259
|
+
<span
|
|
260
|
+
key={m}
|
|
261
|
+
onClick={() => setActive((a) => (a === m ? "" : m))}
|
|
262
|
+
title={m === active ? "active model — click to clear" : "click to set active"}
|
|
263
|
+
className={`inline-flex cursor-pointer items-center gap-1 rounded px-2 py-0.5 text-[12px] transition-colors ${
|
|
264
|
+
m === active ? "border border-accent bg-accent-soft text-accent" : "border border-transparent bg-surface-2 text-text-muted hover:border-border"
|
|
265
|
+
}`}
|
|
266
|
+
>
|
|
267
|
+
{m === active && <Icon name="star" size={11} />}
|
|
268
|
+
<span className="tnum">{m}</span>
|
|
269
|
+
<button
|
|
270
|
+
onClick={(e) => { e.stopPropagation(); setPicked((p) => p.filter((x) => x !== m)); setActive((a) => (a === m ? "" : a)); }}
|
|
271
|
+
className="hover:text-danger"
|
|
272
|
+
aria-label={`Remove ${m}`}
|
|
273
|
+
>
|
|
274
|
+
<Icon name="close" size={12} />
|
|
275
|
+
</button>
|
|
276
|
+
</span>
|
|
277
|
+
))
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
<div className="mt-1.5 flex items-center gap-2">
|
|
281
|
+
<Button type="button" variant="ghost" onClick={() => setPickerOpen(true)}>
|
|
282
|
+
<Icon name="add" size={15} /> Add models
|
|
283
|
+
</Button>
|
|
284
|
+
<span className="text-[11.5px] text-text-subtle">
|
|
285
|
+
{active ? <>active: <span className="tnum text-accent">{active}</span></> : picked.length ? "click a chip to set active" : ""}
|
|
286
|
+
</span>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
</SetupRow>
|
|
290
|
+
)}
|
|
291
|
+
|
|
292
|
+
{cli.configured && cli.baseUrl && (
|
|
293
|
+
<SetupRow label="Current">
|
|
294
|
+
<span className="tnum text-[11.5px] text-text-subtle">{cli.baseUrl}</span>
|
|
295
|
+
</SetupRow>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
<div className="flex items-center gap-2 border-t border-border-subtle pt-3">
|
|
299
|
+
<Button onClick={applyCli} disabled={cliBusy === "apply"}>
|
|
300
|
+
<Icon name={cliBusy === "apply" ? "progress_activity" : "bolt"} size={15} />
|
|
301
|
+
{cliBusy === "apply" ? "Applying…" : cli.configured ? "Re-apply" : "Apply config"}
|
|
302
|
+
</Button>
|
|
303
|
+
{cli.configured && (
|
|
304
|
+
<Button variant="ghost" onClick={resetCli} disabled={cliBusy === "reset"}>
|
|
305
|
+
{cliBusy === "reset" ? "Removing…" : "Reset"}
|
|
306
|
+
</Button>
|
|
307
|
+
)}
|
|
308
|
+
{cliMsg && <span className="text-[12px] text-text-subtle">{cliMsg}</span>}
|
|
309
|
+
{cli.path && <span className="ml-auto truncate tnum text-[11px] text-text-subtle">{cli.path}</span>}
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
</RichCard>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{tool.install && (
|
|
317
|
+
<RichCard header={<CardTitle title="Install" />}>
|
|
318
|
+
<CopyBlock text={tool.install} />
|
|
319
|
+
</RichCard>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
<RichCard
|
|
323
|
+
className={tool.install ? "" : "lg:col-span-2"}
|
|
324
|
+
header={
|
|
325
|
+
<>
|
|
326
|
+
<CardTitle title="Environment" sub="copy into your shell" />
|
|
327
|
+
{ep.keys.length > 1 && (
|
|
328
|
+
<Select value={String(keyIdx)} onChange={(e) => setKeyIdx(Number(e.target.value))} className="max-w-[180px]">
|
|
329
|
+
{ep.keys.map((k, i) => (
|
|
330
|
+
<option key={i} value={i}>{k.name || `key ${i + 1}`}</option>
|
|
331
|
+
))}
|
|
332
|
+
</Select>
|
|
333
|
+
)}
|
|
334
|
+
</>
|
|
335
|
+
}
|
|
336
|
+
>
|
|
337
|
+
<CopyBlock text={block} />
|
|
338
|
+
{ep.keys.length === 0 ? (
|
|
339
|
+
<p className="mt-3 text-[12px] text-warning">
|
|
340
|
+
No gateway key set — auth is disabled. Add one under Endpoint, then it appears here.
|
|
341
|
+
</p>
|
|
342
|
+
) : (
|
|
343
|
+
<p className="mt-3 text-[12px] text-text-subtle">
|
|
344
|
+
Using key <span className="text-text-muted">{ep.keys[keyIdx]?.name || `#${keyIdx + 1}`}</span>. The real value is filled in above.
|
|
345
|
+
</p>
|
|
346
|
+
)}
|
|
347
|
+
</RichCard>
|
|
348
|
+
|
|
349
|
+
{opencodeJson && (
|
|
350
|
+
<RichCard
|
|
351
|
+
className="lg:col-span-2"
|
|
352
|
+
header={<CardTitle title="Manual config" sub="merge into ~/.config/opencode/opencode.json — every model listed" />}
|
|
353
|
+
>
|
|
354
|
+
{ocModels.length === 0 ? (
|
|
355
|
+
<p className="text-[12.5px] text-text-muted">Add models above to see them listed here.</p>
|
|
356
|
+
) : (
|
|
357
|
+
<CopyBlock text={opencodeJson} />
|
|
358
|
+
)}
|
|
359
|
+
<p className="mt-3 text-[12px] text-text-subtle">
|
|
360
|
+
Apply does the same merge for you — it keeps any other providers and existing models, only adding these.
|
|
361
|
+
</p>
|
|
362
|
+
</RichCard>
|
|
363
|
+
)}
|
|
364
|
+
|
|
365
|
+
{claudeJson && (
|
|
366
|
+
<RichCard
|
|
367
|
+
className="lg:col-span-2"
|
|
368
|
+
header={<CardTitle title="Manual config" sub="merge the env block into ~/.claude/settings.json" />}
|
|
369
|
+
>
|
|
370
|
+
<CopyBlock text={claudeJson} />
|
|
371
|
+
<p className="mt-3 text-[12px] text-text-subtle">
|
|
372
|
+
Apply does the same merge for you — it keeps the rest of your settings, only writing these env keys.
|
|
373
|
+
</p>
|
|
374
|
+
</RichCard>
|
|
375
|
+
)}
|
|
376
|
+
|
|
377
|
+
{!tool.autoConfig && (
|
|
378
|
+
<RichCard
|
|
379
|
+
className="lg:col-span-2"
|
|
380
|
+
header={<CardTitle title="Models to call" sub="name a combo exactly this — the tool will hit it" />}
|
|
381
|
+
>
|
|
382
|
+
<div className="space-y-1.5">
|
|
383
|
+
{tool.slots.map((s) => {
|
|
384
|
+
const exists = combos.includes(s.alias);
|
|
385
|
+
return (
|
|
386
|
+
<div key={s.alias} className="flex items-center gap-3 rounded-brand border border-border-subtle px-3 py-2">
|
|
387
|
+
<span className="w-32 flex-none text-[12px] text-text-subtle">{s.label}</span>
|
|
388
|
+
<Icon name="arrow_forward" size={14} className="flex-none text-text-subtle" />
|
|
389
|
+
<span className="tnum truncate text-[13px] text-text">{s.alias}</span>
|
|
390
|
+
<span className="ml-auto flex flex-none items-center gap-2">
|
|
391
|
+
{exists ? (
|
|
392
|
+
<Badge tone="live">ready</Badge>
|
|
393
|
+
) : (
|
|
394
|
+
<>
|
|
395
|
+
<Badge tone="warn">missing</Badge>
|
|
396
|
+
<button
|
|
397
|
+
type="button"
|
|
398
|
+
onClick={() => router.push("/combos")}
|
|
399
|
+
className="inline-flex items-center gap-1 text-[12px] text-accent hover:underline"
|
|
400
|
+
>
|
|
401
|
+
<Icon name="add" size={13} /> create
|
|
402
|
+
</button>
|
|
403
|
+
</>
|
|
404
|
+
)}
|
|
405
|
+
</span>
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
})}
|
|
409
|
+
</div>
|
|
410
|
+
{combos.length > 0 && (
|
|
411
|
+
<p className="mt-2.5 text-[11px] text-text-subtle">
|
|
412
|
+
Your combos: <span className="tnum text-text-muted">{combos.join(", ")}</span>
|
|
413
|
+
</p>
|
|
414
|
+
)}
|
|
415
|
+
</RichCard>
|
|
416
|
+
)}
|
|
417
|
+
|
|
418
|
+
<RichCard className="lg:col-span-2" header={<CardTitle title="Steps" />}>
|
|
419
|
+
<ol className="space-y-2.5">
|
|
420
|
+
{tool.steps.map((s, i) => (
|
|
421
|
+
<li key={i} className="flex gap-2.5 text-[13px] text-text-muted">
|
|
422
|
+
<span className="flex h-5 w-5 flex-none items-center justify-center rounded-full bg-surface-2 tnum text-[11px] text-text">
|
|
423
|
+
{i + 1}
|
|
424
|
+
</span>
|
|
425
|
+
{s}
|
|
426
|
+
</li>
|
|
427
|
+
))}
|
|
428
|
+
</ol>
|
|
429
|
+
</RichCard>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
{pickerOpen && (
|
|
433
|
+
<ModelPicker
|
|
434
|
+
title="Add models"
|
|
435
|
+
note="Click a model to add it, click again to remove. Then hit Apply."
|
|
436
|
+
groups={groups}
|
|
437
|
+
selected={picked}
|
|
438
|
+
onToggle={togglePicked}
|
|
439
|
+
onClose={() => setPickerOpen(false)}
|
|
440
|
+
/>
|
|
441
|
+
)}
|
|
442
|
+
</div>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** label → control row used by the Local setup card (matches aigetwey's layout). */
|
|
447
|
+
function SetupRow({ label, children, top }: { label: string; children: React.ReactNode; top?: boolean }) {
|
|
448
|
+
return (
|
|
449
|
+
<div className={`grid grid-cols-[7rem_1fr] gap-3 ${top ? "items-start" : "items-center"}`}>
|
|
450
|
+
<span className={`text-[12px] font-medium text-text-subtle ${top ? "pt-1.5" : ""}`}>{label}</span>
|
|
451
|
+
<div className="min-w-0">{children}</div>
|
|
452
|
+
</div>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function CopyBlock({ text }: { text: string }) {
|
|
457
|
+
const [copied, setCopied] = useState(false);
|
|
458
|
+
return (
|
|
459
|
+
<div className="relative">
|
|
460
|
+
<pre className="overflow-x-auto rounded-brand border border-border-subtle bg-bg px-3 py-2.5 font-mono text-[12.5px] leading-relaxed text-text">
|
|
461
|
+
{text}
|
|
462
|
+
</pre>
|
|
463
|
+
<button
|
|
464
|
+
onClick={() => {
|
|
465
|
+
void navigator.clipboard.writeText(text);
|
|
466
|
+
setCopied(true);
|
|
467
|
+
setTimeout(() => setCopied(false), 1200);
|
|
468
|
+
}}
|
|
469
|
+
className="absolute right-2 top-2 flex items-center gap-1 rounded-brand border border-border bg-surface px-2 py-1 text-[11px] text-text-muted hover:text-text"
|
|
470
|
+
>
|
|
471
|
+
<Icon name={copied ? "check" : "content_copy"} size={13} /> {copied ? "copied" : "copy"}
|
|
472
|
+
</button>
|
|
473
|
+
</div>
|
|
474
|
+
);
|
|
475
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { Icon } from "./Icon";
|
|
6
|
+
import { useTheme } from "./ThemeProvider";
|
|
7
|
+
import { ConfirmModal } from "./ConfirmModal";
|
|
8
|
+
import { adminApi } from "@/lib/client";
|
|
9
|
+
|
|
10
|
+
const LABELS: Record<string, string> = {
|
|
11
|
+
"": "Endpoint & Key",
|
|
12
|
+
endpoint: "Endpoint & Key",
|
|
13
|
+
providers: "Providers",
|
|
14
|
+
combos: "Combos",
|
|
15
|
+
usage: "Usage",
|
|
16
|
+
quota: "Quota Tracker",
|
|
17
|
+
tools: "CLI Tools",
|
|
18
|
+
console: "Server Console",
|
|
19
|
+
config: "Settings",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function TopBar() {
|
|
23
|
+
const path = usePathname();
|
|
24
|
+
const { theme, toggle } = useTheme();
|
|
25
|
+
const seg = path === "/" ? "" : (path.split("/")[1] ?? "");
|
|
26
|
+
const current = LABELS[seg] ?? seg;
|
|
27
|
+
|
|
28
|
+
const [version, setVersion] = useState<{ current: string; latest: string | null; updateAvailable: boolean } | null>(null);
|
|
29
|
+
const [confirmShutdown, setConfirmShutdown] = useState(false);
|
|
30
|
+
const [stopped, setStopped] = useState(false);
|
|
31
|
+
const [busy, setBusy] = useState(false);
|
|
32
|
+
|
|
33
|
+
// version + update check on mount, via aigetwey's sidebar npm poll.
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
void adminApi.version().then((r) => {
|
|
36
|
+
if (r.ok && r.data) setVersion(r.data);
|
|
37
|
+
});
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
async function doShutdown() {
|
|
41
|
+
setBusy(true);
|
|
42
|
+
await adminApi.shutdown();
|
|
43
|
+
// the gateway exits ~300ms after replying; reflect it in the UI.
|
|
44
|
+
setStopped(true);
|
|
45
|
+
setConfirmShutdown(false);
|
|
46
|
+
setBusy(false);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<header className="console-topbar">
|
|
51
|
+
<div className="flex items-center gap-2 text-[13px]">
|
|
52
|
+
<span className="text-text-subtle">aigetwey</span>
|
|
53
|
+
<span className="text-text-subtle">/</span>
|
|
54
|
+
<span className="font-medium text-text">{current}</span>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div className="ml-auto flex items-center gap-2.5">
|
|
58
|
+
{version && (
|
|
59
|
+
<span
|
|
60
|
+
className="flex items-center gap-1 text-[11px] text-text-subtle"
|
|
61
|
+
title={
|
|
62
|
+
version.updateAvailable
|
|
63
|
+
? `Update available: v${version.latest}`
|
|
64
|
+
: "You're on the latest version"
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
v{version.current}
|
|
68
|
+
{version.updateAvailable && (
|
|
69
|
+
<span className="flex items-center gap-1 rounded-full bg-warning/12 px-1.5 py-0.5 text-warning">
|
|
70
|
+
<Icon name="arrow_upward" size={11} />v{version.latest}
|
|
71
|
+
</span>
|
|
72
|
+
)}
|
|
73
|
+
</span>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
<button
|
|
77
|
+
onClick={toggle}
|
|
78
|
+
className="flex h-8 w-8 items-center justify-center rounded-full text-text-subtle transition-colors hover:text-text"
|
|
79
|
+
aria-label="Toggle theme"
|
|
80
|
+
title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
|
81
|
+
>
|
|
82
|
+
<Icon name={theme === "dark" ? "light_mode" : "dark_mode"} size={18} />
|
|
83
|
+
</button>
|
|
84
|
+
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => setConfirmShutdown(true)}
|
|
87
|
+
className="flex h-8 w-8 items-center justify-center rounded-full text-text-subtle transition-colors hover:text-danger"
|
|
88
|
+
aria-label="Shut down gateway"
|
|
89
|
+
title="Shut down the gateway"
|
|
90
|
+
>
|
|
91
|
+
<Icon name="power_settings_new" size={18} />
|
|
92
|
+
</button>
|
|
93
|
+
|
|
94
|
+
<div className="flex items-center gap-2 rounded-full border border-border bg-surface py-1 pl-1 pr-3">
|
|
95
|
+
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-accent text-[11px] font-bold text-accent-ink">
|
|
96
|
+
A
|
|
97
|
+
</span>
|
|
98
|
+
<span className="text-[12px] text-text-muted">admin</span>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{confirmShutdown && (
|
|
103
|
+
<ConfirmModal
|
|
104
|
+
title="Shut down gateway?"
|
|
105
|
+
message="The gateway process will stop and all requests will fail until you restart it (run.sh). The dashboard stays up but can't reach the gateway."
|
|
106
|
+
confirmLabel="Shut down"
|
|
107
|
+
busy={busy}
|
|
108
|
+
onConfirm={doShutdown}
|
|
109
|
+
onCancel={() => setConfirmShutdown(false)}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{stopped && (
|
|
114
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-6">
|
|
115
|
+
<div className="w-full max-w-sm rounded-brand-lg border border-border bg-surface p-5 text-center shadow-elevated">
|
|
116
|
+
<span className="mx-auto mb-2 flex h-9 w-9 items-center justify-center rounded-full bg-danger/10 text-danger">
|
|
117
|
+
<Icon name="power_settings_new" size={20} />
|
|
118
|
+
</span>
|
|
119
|
+
<h2 className="text-[15px] font-semibold text-text">Gateway stopped</h2>
|
|
120
|
+
<p className="mt-1 text-[12.5px] text-text-muted">
|
|
121
|
+
Restart it with <code className="rounded bg-surface-2 px-1">run.sh</code>, then reload this page.
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</header>
|
|
127
|
+
);
|
|
128
|
+
}
|