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,340 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* aigetwey console — warm near-black surfaces, soft lime accent, floating
|
|
5
|
+
* icon-rail. Dark-only. Depth from diffuse shadows, not hard lines.
|
|
6
|
+
*/
|
|
7
|
+
@theme {
|
|
8
|
+
--color-bg: #0f0f0e;
|
|
9
|
+
--color-bg-alt: #141413;
|
|
10
|
+
--color-surface: #1a1a18;
|
|
11
|
+
--color-surface-2: #222220;
|
|
12
|
+
--color-surface-3: #2c2c29;
|
|
13
|
+
--color-sidebar: #131312;
|
|
14
|
+
|
|
15
|
+
--color-border: #2a2a27;
|
|
16
|
+
--color-border-subtle: #1f1f1d;
|
|
17
|
+
|
|
18
|
+
--color-text: #ededec;
|
|
19
|
+
--color-text-muted: #8d8d86;
|
|
20
|
+
--color-text-subtle: #5c5c56;
|
|
21
|
+
|
|
22
|
+
--color-accent: #cbe85a;
|
|
23
|
+
--color-accent-hover: #d6f06a;
|
|
24
|
+
--color-accent-ink: #14140f;
|
|
25
|
+
--color-accent-soft: #3a3f1c;
|
|
26
|
+
|
|
27
|
+
--color-success: #7dd87f;
|
|
28
|
+
--color-warning: #e8c55a;
|
|
29
|
+
--color-danger: #e8806a;
|
|
30
|
+
--color-info: #7fb0e8;
|
|
31
|
+
|
|
32
|
+
--radius-brand: 12px;
|
|
33
|
+
--radius-brand-lg: 16px;
|
|
34
|
+
|
|
35
|
+
--shadow-soft: 0 1px 2px 0 rgba(0, 0, 0, 0.4);
|
|
36
|
+
--shadow-warm: 0 4px 16px -4px rgba(203, 232, 90, 0.18);
|
|
37
|
+
--shadow-elevated: 0 16px 40px -8px rgba(0, 0, 0, 0.55);
|
|
38
|
+
|
|
39
|
+
--font-sans: var(--font-inter), -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
|
|
40
|
+
--font-mono: var(--font-jetbrains-mono), ui-monospace, "SF Mono", Menlo, monospace;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
* {
|
|
44
|
+
box-sizing: border-box;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
html.light {
|
|
48
|
+
--color-bg: #fafaf9;
|
|
49
|
+
--color-bg-alt: #f5f5f3;
|
|
50
|
+
--color-surface: #ffffff;
|
|
51
|
+
--color-surface-2: #f0f0ee;
|
|
52
|
+
--color-surface-3: #e8e8e5;
|
|
53
|
+
--color-sidebar: #f7f7f6;
|
|
54
|
+
--color-border: #e0e0dc;
|
|
55
|
+
--color-border-subtle: #eaeae7;
|
|
56
|
+
--color-text: #1a1a18;
|
|
57
|
+
--color-text-muted: #6b6b65;
|
|
58
|
+
--color-text-subtle: #9b9b94;
|
|
59
|
+
--color-accent: #6b8f20;
|
|
60
|
+
--color-accent-hover: #7ba025;
|
|
61
|
+
--color-accent-ink: #ffffff;
|
|
62
|
+
--color-accent-soft: #eef5d8;
|
|
63
|
+
--shadow-soft: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
|
|
64
|
+
--shadow-warm: 0 4px 16px -4px rgba(107, 143, 32, 0.12);
|
|
65
|
+
--shadow-elevated: 0 16px 40px -8px rgba(0, 0, 0, 0.12);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
html,
|
|
69
|
+
body {
|
|
70
|
+
margin: 0;
|
|
71
|
+
padding: 0;
|
|
72
|
+
background: var(--color-bg);
|
|
73
|
+
color: var(--color-text);
|
|
74
|
+
font-family: var(--font-sans);
|
|
75
|
+
-webkit-font-smoothing: antialiased;
|
|
76
|
+
text-rendering: optimizeLegibility;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Material Symbols — hold the icon font's variation axes steady */
|
|
80
|
+
.material-symbols-outlined {
|
|
81
|
+
font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24;
|
|
82
|
+
font-size: 20px;
|
|
83
|
+
line-height: 1;
|
|
84
|
+
user-select: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* tabular figures wherever numbers live */
|
|
88
|
+
.tnum {
|
|
89
|
+
font-family: var(--font-mono);
|
|
90
|
+
font-variant-numeric: tabular-nums;
|
|
91
|
+
font-feature-settings: "tnum";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
a {
|
|
95
|
+
color: inherit;
|
|
96
|
+
text-decoration: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
a:focus-visible,
|
|
100
|
+
button:focus-visible,
|
|
101
|
+
input:focus-visible,
|
|
102
|
+
textarea:focus-visible,
|
|
103
|
+
select:focus-visible {
|
|
104
|
+
outline: 2px solid var(--color-accent);
|
|
105
|
+
outline-offset: 2px;
|
|
106
|
+
border-radius: 6px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.dark select option {
|
|
110
|
+
background-color: var(--color-surface);
|
|
111
|
+
color: var(--color-text);
|
|
112
|
+
}
|
|
113
|
+
html.light select option {
|
|
114
|
+
background-color: #ffffff;
|
|
115
|
+
color: #1a1a18;
|
|
116
|
+
}
|
|
117
|
+
html.light .rail-icon-active {
|
|
118
|
+
color: #ffffff;
|
|
119
|
+
background: #1a1a18;
|
|
120
|
+
}
|
|
121
|
+
html.light .rail-icon-active:hover {
|
|
122
|
+
color: #ffffff;
|
|
123
|
+
background: #333330;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* status lamp — green when live, red pulse on cooldown */
|
|
127
|
+
.lamp {
|
|
128
|
+
display: inline-block;
|
|
129
|
+
width: 8px;
|
|
130
|
+
height: 8px;
|
|
131
|
+
border-radius: 50%;
|
|
132
|
+
flex: none;
|
|
133
|
+
}
|
|
134
|
+
.lamp-live {
|
|
135
|
+
background: var(--color-success);
|
|
136
|
+
box-shadow: 0 0 6px 1px color-mix(in srgb, var(--color-success) 60%, transparent);
|
|
137
|
+
}
|
|
138
|
+
.lamp-idle {
|
|
139
|
+
background: var(--color-text-subtle);
|
|
140
|
+
}
|
|
141
|
+
.lamp-down {
|
|
142
|
+
background: var(--color-danger);
|
|
143
|
+
box-shadow: 0 0 6px 1px color-mix(in srgb, var(--color-danger) 60%, transparent);
|
|
144
|
+
animation: pulse 1.4s ease-in-out infinite;
|
|
145
|
+
}
|
|
146
|
+
@keyframes pulse {
|
|
147
|
+
0%,
|
|
148
|
+
100% {
|
|
149
|
+
opacity: 1;
|
|
150
|
+
}
|
|
151
|
+
50% {
|
|
152
|
+
opacity: 0.35;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
@media (prefers-reduced-motion: reduce) {
|
|
156
|
+
.lamp-down {
|
|
157
|
+
animation: none;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* console shell — floating icon-island rail + main column with sticky top bar */
|
|
162
|
+
.console-grid {
|
|
163
|
+
/* rail is position:fixed (out of flow), so reserve its lane with padding
|
|
164
|
+
instead of a grid track — a track would let the main column collapse into it. */
|
|
165
|
+
min-height: 100vh;
|
|
166
|
+
padding-left: 108px;
|
|
167
|
+
}
|
|
168
|
+
.console-rail {
|
|
169
|
+
/* fixed + viewport-centered so the island stays put on long, scrollable pages
|
|
170
|
+
(sticky+align-center centered to the FULL content height, scrolling away). */
|
|
171
|
+
position: fixed;
|
|
172
|
+
left: 32px;
|
|
173
|
+
top: 50%;
|
|
174
|
+
transform: translateY(-50%);
|
|
175
|
+
z-index: 30;
|
|
176
|
+
/* explicit width — fixed shrinks to content (42px icon) otherwise; 76px keeps
|
|
177
|
+
the original island proportion the old 108px grid track gave it. */
|
|
178
|
+
width: 76px;
|
|
179
|
+
/* no overflow clip — hover tooltips (::after) extend past the rail to the right;
|
|
180
|
+
overflow:auto would scroll/clip them and spawn a stray scrollbar. */
|
|
181
|
+
background: var(--color-sidebar);
|
|
182
|
+
border: 1px solid var(--color-border-subtle);
|
|
183
|
+
border-radius: 24px;
|
|
184
|
+
box-shadow: var(--shadow-soft);
|
|
185
|
+
padding: 1.25rem 0;
|
|
186
|
+
display: flex;
|
|
187
|
+
flex-direction: column;
|
|
188
|
+
align-items: center;
|
|
189
|
+
gap: 1rem;
|
|
190
|
+
}
|
|
191
|
+
.rail-divider {
|
|
192
|
+
width: 26px;
|
|
193
|
+
height: 1px;
|
|
194
|
+
background: var(--color-border);
|
|
195
|
+
margin: 0.2rem 0 0.4rem;
|
|
196
|
+
flex: none;
|
|
197
|
+
}
|
|
198
|
+
.console-col {
|
|
199
|
+
display: flex;
|
|
200
|
+
flex-direction: column;
|
|
201
|
+
min-width: 0;
|
|
202
|
+
}
|
|
203
|
+
.console-topbar {
|
|
204
|
+
position: sticky;
|
|
205
|
+
top: 0;
|
|
206
|
+
z-index: 20;
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
gap: 1rem;
|
|
210
|
+
padding: 0 1.5rem;
|
|
211
|
+
height: 56px;
|
|
212
|
+
background: var(--color-bg);
|
|
213
|
+
}
|
|
214
|
+
.console-main {
|
|
215
|
+
padding: 2rem 3rem 4rem;
|
|
216
|
+
min-width: 0;
|
|
217
|
+
width: 100%;
|
|
218
|
+
max-width: 1600px;
|
|
219
|
+
margin: 0 auto;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* circular icon buttons in the rail */
|
|
223
|
+
.rail-icon {
|
|
224
|
+
position: relative;
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
width: 42px;
|
|
229
|
+
height: 42px;
|
|
230
|
+
border-radius: 50%;
|
|
231
|
+
background: var(--color-surface);
|
|
232
|
+
color: var(--color-text-subtle);
|
|
233
|
+
transition: color 0.15s ease, background 0.15s ease;
|
|
234
|
+
}
|
|
235
|
+
.rail-icon:hover {
|
|
236
|
+
color: var(--color-text);
|
|
237
|
+
background: var(--color-surface-3);
|
|
238
|
+
}
|
|
239
|
+
.rail-icon-active {
|
|
240
|
+
color: var(--color-bg);
|
|
241
|
+
background: var(--color-text);
|
|
242
|
+
}
|
|
243
|
+
.rail-icon-active:hover {
|
|
244
|
+
color: var(--color-bg);
|
|
245
|
+
background: #ffffff;
|
|
246
|
+
}
|
|
247
|
+
.rail-brand {
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
justify-content: center;
|
|
251
|
+
width: 42px;
|
|
252
|
+
height: 42px;
|
|
253
|
+
border-radius: 13px;
|
|
254
|
+
background: var(--color-accent);
|
|
255
|
+
color: var(--color-accent-ink);
|
|
256
|
+
font-weight: 700;
|
|
257
|
+
font-size: 17px;
|
|
258
|
+
box-shadow: var(--shadow-warm);
|
|
259
|
+
flex: none;
|
|
260
|
+
}
|
|
261
|
+
/* tooltip on hover */
|
|
262
|
+
.rail-icon[data-label]::after {
|
|
263
|
+
content: attr(data-label);
|
|
264
|
+
position: absolute;
|
|
265
|
+
left: calc(100% + 12px);
|
|
266
|
+
top: 50%;
|
|
267
|
+
transform: translateY(-50%);
|
|
268
|
+
white-space: nowrap;
|
|
269
|
+
background: var(--color-surface-3);
|
|
270
|
+
color: var(--color-text);
|
|
271
|
+
font-size: 12px;
|
|
272
|
+
padding: 4px 9px;
|
|
273
|
+
border-radius: 7px;
|
|
274
|
+
opacity: 0;
|
|
275
|
+
pointer-events: none;
|
|
276
|
+
transition: opacity 0.12s ease;
|
|
277
|
+
z-index: 40;
|
|
278
|
+
box-shadow: var(--shadow-soft);
|
|
279
|
+
}
|
|
280
|
+
.rail-icon:hover[data-label]::after {
|
|
281
|
+
opacity: 1;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* KPI chip in the top bar */
|
|
285
|
+
.kpi {
|
|
286
|
+
display: flex;
|
|
287
|
+
align-items: center;
|
|
288
|
+
gap: 7px;
|
|
289
|
+
padding: 5px 11px;
|
|
290
|
+
border-radius: 999px;
|
|
291
|
+
background: var(--color-surface);
|
|
292
|
+
border: 1px solid var(--color-border);
|
|
293
|
+
font-size: 12px;
|
|
294
|
+
white-space: nowrap;
|
|
295
|
+
}
|
|
296
|
+
.kpi-k {
|
|
297
|
+
color: var(--color-text-subtle);
|
|
298
|
+
}
|
|
299
|
+
.kpi-v {
|
|
300
|
+
color: var(--color-text);
|
|
301
|
+
font-family: var(--font-mono);
|
|
302
|
+
font-variant-numeric: tabular-nums;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
@media (max-width: 760px) {
|
|
306
|
+
.console-grid {
|
|
307
|
+
grid-template-columns: 72px 1fr;
|
|
308
|
+
}
|
|
309
|
+
.console-main {
|
|
310
|
+
padding: 1.25rem 1.1rem 3rem;
|
|
311
|
+
}
|
|
312
|
+
.console-topbar {
|
|
313
|
+
padding: 0 1rem;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.table-wrap {
|
|
318
|
+
overflow-x: auto;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* Tailwind preflight leaves a plain <button> with the default arrow cursor, so
|
|
322
|
+
* raw toggles/pills/icon buttons gave no hover affordance. Pointer for every
|
|
323
|
+
* enabled interactive control; disabled keeps not-allowed. */
|
|
324
|
+
button:not(:disabled),
|
|
325
|
+
[role="button"]:not([aria-disabled="true"]),
|
|
326
|
+
label:has(> input[type="checkbox"]:not(:disabled)) {
|
|
327
|
+
cursor: pointer;
|
|
328
|
+
}
|
|
329
|
+
button:disabled {
|
|
330
|
+
cursor: not-allowed;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
::selection {
|
|
334
|
+
background: color-mix(in srgb, var(--color-accent) 30%, transparent);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@keyframes slideIn {
|
|
338
|
+
from { opacity: 0; transform: translateX(20px); }
|
|
339
|
+
to { opacity: 1; transform: translateX(0); }
|
|
340
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
|
2
|
+
<rect width="512" height="512" rx="116" fill="#0f0f0e"/>
|
|
3
|
+
<text x="120" y="338" font-family="ui-sans-serif, Arial, sans-serif" font-size="260" font-weight="800" text-anchor="middle" fill="#cbe85a">a</text>
|
|
4
|
+
<g fill="none" stroke="#cbe85a" stroke-width="32" stroke-linecap="round" stroke-linejoin="round">
|
|
5
|
+
<polyline points="270,180 350,256 270,332"/>
|
|
6
|
+
<polyline points="338,180 418,256 338,332"/>
|
|
7
|
+
</g>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Inter, JetBrains_Mono } from "next/font/google";
|
|
3
|
+
import { ThemeProvider } from "@/components/ThemeProvider";
|
|
4
|
+
import { ToastProvider } from "@/components/ToastProvider";
|
|
5
|
+
import "./globals.css";
|
|
6
|
+
|
|
7
|
+
const inter = Inter({ subsets: ["latin"], variable: "--font-inter", display: "swap" });
|
|
8
|
+
const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-jetbrains-mono", display: "swap" });
|
|
9
|
+
|
|
10
|
+
export const metadata: Metadata = {
|
|
11
|
+
title: "aigetwey",
|
|
12
|
+
description: "Console for the aigetwey AI gateway",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
16
|
+
return (
|
|
17
|
+
<html lang="en" className={`dark ${inter.variable} ${jetbrainsMono.variable}`}>
|
|
18
|
+
<head>
|
|
19
|
+
<link
|
|
20
|
+
rel="stylesheet"
|
|
21
|
+
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=block"
|
|
22
|
+
/>
|
|
23
|
+
<script dangerouslySetInnerHTML={{ __html: `try{var t=localStorage.getItem("theme");if(t==="light")document.documentElement.className=document.documentElement.className.replace("dark","light")}catch(e){}` }} />
|
|
24
|
+
</head>
|
|
25
|
+
<body><ThemeProvider><ToastProvider>{children}</ToastProvider></ThemeProvider></body>
|
|
26
|
+
</html>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { Button, Input, Field } from "@/components/Button";
|
|
6
|
+
|
|
7
|
+
export default function LoginPage() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const [password, setPassword] = useState("");
|
|
10
|
+
const [error, setError] = useState("");
|
|
11
|
+
const [busy, setBusy] = useState(false);
|
|
12
|
+
|
|
13
|
+
async function submit(e: React.FormEvent) {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
setBusy(true);
|
|
16
|
+
setError("");
|
|
17
|
+
const res = await fetch("/api/login", {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "content-type": "application/json" },
|
|
20
|
+
body: JSON.stringify({ password }),
|
|
21
|
+
});
|
|
22
|
+
if (res.ok) {
|
|
23
|
+
router.replace("/");
|
|
24
|
+
router.refresh();
|
|
25
|
+
} else {
|
|
26
|
+
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
27
|
+
setError(body.error ?? "login failed");
|
|
28
|
+
setBusy(false);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<main className="grid min-h-screen place-items-center p-6">
|
|
34
|
+
<form
|
|
35
|
+
onSubmit={submit}
|
|
36
|
+
className="w-full max-w-[360px] rounded-brand-lg border border-border bg-surface p-7 shadow-elevated"
|
|
37
|
+
>
|
|
38
|
+
<div className="mb-5 flex items-center gap-2.5">
|
|
39
|
+
<span className="flex h-7 w-7 flex-none items-center justify-center rounded-brand bg-accent text-[14px] font-bold text-accent-ink shadow-warm">
|
|
40
|
+
a
|
|
41
|
+
</span>
|
|
42
|
+
<span className="text-[16px] font-semibold tracking-tight text-text">aigetwey</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<h1 className="text-[19px] font-semibold tracking-tight text-text">Welcome back</h1>
|
|
46
|
+
<p className="mb-6 mt-1 text-[13px] text-text-muted">Enter the admin password to continue.</p>
|
|
47
|
+
|
|
48
|
+
<Field label="Password">
|
|
49
|
+
<Input type="password" value={password} autoFocus onChange={(e) => setPassword(e.target.value)} />
|
|
50
|
+
</Field>
|
|
51
|
+
|
|
52
|
+
{error && <div className="mt-2.5 text-[12px] text-danger">{error}</div>}
|
|
53
|
+
|
|
54
|
+
<Button type="submit" disabled={busy} className="mt-6 w-full">
|
|
55
|
+
{busy ? "Connecting…" : "Connect"}
|
|
56
|
+
</Button>
|
|
57
|
+
</form>
|
|
58
|
+
</main>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
export interface SeriesPoint {
|
|
6
|
+
ts: number;
|
|
7
|
+
requests: number;
|
|
8
|
+
tokens_in: number;
|
|
9
|
+
tokens_out: number;
|
|
10
|
+
cost: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Metric = "tokens" | "cost" | "requests";
|
|
14
|
+
|
|
15
|
+
const METRICS: { key: Metric; label: string }[] = [
|
|
16
|
+
{ key: "tokens", label: "Tokens" },
|
|
17
|
+
{ key: "cost", label: "Cost" },
|
|
18
|
+
{ key: "requests", label: "Requests" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function valueOf(p: SeriesPoint, m: Metric): number {
|
|
22
|
+
if (m === "tokens") return p.tokens_in + p.tokens_out;
|
|
23
|
+
if (m === "cost") return p.cost;
|
|
24
|
+
return p.requests;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fmtVal(v: number, m: Metric): string {
|
|
28
|
+
if (m === "cost") return v < 0.01 ? `$${v.toFixed(4)}` : `$${v.toFixed(2)}`;
|
|
29
|
+
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
|
|
30
|
+
if (v >= 1_000) return `${(v / 1_000).toFixed(1)}K`;
|
|
31
|
+
return String(Math.round(v));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fmtTime(ts: number): string {
|
|
35
|
+
return new Date(ts).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Inline-SVG area chart with a metric toggle. No charting dependency. */
|
|
39
|
+
export function AreaChart({ series }: { series: SeriesPoint[] }) {
|
|
40
|
+
const [metric, setMetric] = useState<Metric>("tokens");
|
|
41
|
+
|
|
42
|
+
const W = 1000;
|
|
43
|
+
const H = 240;
|
|
44
|
+
const padX = 8;
|
|
45
|
+
const padY = 16;
|
|
46
|
+
|
|
47
|
+
const vals = series.map((p) => valueOf(p, metric));
|
|
48
|
+
const max = Math.max(1, ...vals);
|
|
49
|
+
const n = series.length;
|
|
50
|
+
|
|
51
|
+
const x = (i: number) => (n <= 1 ? padX : padX + (i / (n - 1)) * (W - padX * 2));
|
|
52
|
+
const y = (v: number) => H - padY - (v / max) * (H - padY * 2);
|
|
53
|
+
|
|
54
|
+
const line = vals.map((v, i) => `${i === 0 ? "M" : "L"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
|
|
55
|
+
const area = `${line} L${x(n - 1).toFixed(1)},${H - padY} L${x(0).toFixed(1)},${H - padY} Z`;
|
|
56
|
+
|
|
57
|
+
const peak = vals.length ? vals.indexOf(Math.max(...vals)) : -1;
|
|
58
|
+
const total = vals.reduce((a, b) => a + b, 0);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="overflow-hidden rounded-brand-lg border border-border bg-surface shadow-soft">
|
|
62
|
+
<header className="flex items-center justify-between gap-3 border-b border-border-subtle px-4 py-3">
|
|
63
|
+
<div className="flex items-center gap-2.5">
|
|
64
|
+
<span className="text-[13px] font-semibold text-text">Over time</span>
|
|
65
|
+
<span className="tnum text-[12px] text-text-muted">{fmtVal(total, metric)} total</span>
|
|
66
|
+
</div>
|
|
67
|
+
<div className="flex items-center gap-1">
|
|
68
|
+
{METRICS.map((m) => (
|
|
69
|
+
<button
|
|
70
|
+
key={m.key}
|
|
71
|
+
onClick={() => setMetric(m.key)}
|
|
72
|
+
className={`rounded-full px-3 py-1 text-[12px] font-medium transition-colors ${
|
|
73
|
+
metric === m.key ? "bg-surface-2 text-text" : "text-text-muted hover:text-text"
|
|
74
|
+
}`}
|
|
75
|
+
>
|
|
76
|
+
{m.label}
|
|
77
|
+
</button>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
</header>
|
|
81
|
+
|
|
82
|
+
<div className="px-2 py-3">
|
|
83
|
+
{n === 0 || total === 0 ? (
|
|
84
|
+
<div className="flex h-[200px] items-center justify-center text-[13px] text-text-muted">
|
|
85
|
+
No activity in this range.
|
|
86
|
+
</div>
|
|
87
|
+
) : (
|
|
88
|
+
<svg viewBox={`0 0 ${W} ${H}`} className="h-[200px] w-full" preserveAspectRatio="none">
|
|
89
|
+
<defs>
|
|
90
|
+
<linearGradient id="areaFill" x1="0" y1="0" x2="0" y2="1">
|
|
91
|
+
<stop offset="0%" stopColor="var(--color-accent)" stopOpacity="0.28" />
|
|
92
|
+
<stop offset="100%" stopColor="var(--color-accent)" stopOpacity="0" />
|
|
93
|
+
</linearGradient>
|
|
94
|
+
</defs>
|
|
95
|
+
<path d={area} fill="url(#areaFill)" />
|
|
96
|
+
<path d={line} fill="none" stroke="var(--color-accent)" strokeWidth="2" vectorEffect="non-scaling-stroke" />
|
|
97
|
+
{peak >= 0 && (
|
|
98
|
+
<circle cx={x(peak)} cy={y(vals[peak]!)} r="3" fill="var(--color-accent)" vectorEffect="non-scaling-stroke" />
|
|
99
|
+
)}
|
|
100
|
+
</svg>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{n > 0 && total > 0 && peak >= 0 && (
|
|
105
|
+
<div className="flex justify-between border-t border-border-subtle px-4 py-2 tnum text-[10px] text-text-subtle">
|
|
106
|
+
<span>{fmtTime(series[0]!.ts)}</span>
|
|
107
|
+
<span>
|
|
108
|
+
peak {fmtVal(vals[peak]!, metric)} @ {fmtTime(series[peak]!.ts)}
|
|
109
|
+
</span>
|
|
110
|
+
<span>{fmtTime(series[n - 1]!.ts)}</span>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type Tone = "live" | "down" | "warn" | "info" | "neutral";
|
|
2
|
+
|
|
3
|
+
const TONES: Record<Tone, string> = {
|
|
4
|
+
live: "bg-success/12 text-success",
|
|
5
|
+
down: "bg-danger/12 text-danger",
|
|
6
|
+
warn: "bg-warning/12 text-warning",
|
|
7
|
+
info: "bg-info/12 text-info",
|
|
8
|
+
neutral: "bg-surface-2 text-text-muted",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function Badge({
|
|
12
|
+
tone = "neutral",
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
}: {
|
|
16
|
+
tone?: Tone;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<span
|
|
22
|
+
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium ${TONES[tone]}${className ? ` ${className}` : ""}`}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</span>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Format badge for a provider's wire format. */
|
|
30
|
+
export function FormatBadge({ format }: { format: "openai" | "anthropic" | "gemini" }) {
|
|
31
|
+
return <Badge tone="info">{format}</Badge>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
type Variant = "primary" | "ghost" | "danger";
|
|
4
|
+
|
|
5
|
+
const VARIANTS: Record<Variant, string> = {
|
|
6
|
+
primary: "bg-accent text-accent-ink shadow-warm hover:bg-accent-hover border border-transparent font-semibold",
|
|
7
|
+
ghost: "bg-transparent text-text-muted border border-border hover:text-text hover:border-text-subtle",
|
|
8
|
+
danger: "bg-transparent text-text-muted border border-border hover:text-danger hover:border-danger/50",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function Button({
|
|
12
|
+
variant = "primary",
|
|
13
|
+
className,
|
|
14
|
+
...props
|
|
15
|
+
}: ButtonHTMLAttributes<HTMLButtonElement> & { variant?: Variant }) {
|
|
16
|
+
return (
|
|
17
|
+
<button
|
|
18
|
+
className={`inline-flex items-center justify-center gap-1.5 rounded-brand px-3.5 py-2 text-[13px] font-medium transition-colors duration-150 cursor-pointer disabled:opacity-45 disabled:cursor-not-allowed ${VARIANTS[variant]}${className ? ` ${className}` : ""}`}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Input({ className, ...props }: React.InputHTMLAttributes<HTMLInputElement>) {
|
|
25
|
+
return (
|
|
26
|
+
<input
|
|
27
|
+
className={`w-full rounded-brand border border-border bg-bg px-3 py-2 text-[13px] text-text placeholder:text-text-subtle focus:border-accent focus:outline-none transition-colors${className ? ` ${className}` : ""}`}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function Select({ className, ...props }: React.SelectHTMLAttributes<HTMLSelectElement>) {
|
|
34
|
+
return (
|
|
35
|
+
<select
|
|
36
|
+
className={`w-full rounded-brand border border-border bg-bg px-3 py-2 text-[13px] text-text focus:border-accent focus:outline-none transition-colors${className ? ` ${className}` : ""}`}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function Field({
|
|
43
|
+
label,
|
|
44
|
+
hint,
|
|
45
|
+
children,
|
|
46
|
+
}: {
|
|
47
|
+
label: string;
|
|
48
|
+
hint?: string;
|
|
49
|
+
children: React.ReactNode;
|
|
50
|
+
}) {
|
|
51
|
+
return (
|
|
52
|
+
<label className="flex flex-col gap-1.5">
|
|
53
|
+
<span className="text-[11px] font-medium uppercase tracking-wider text-text-subtle">
|
|
54
|
+
{label}
|
|
55
|
+
{hint && <span className="ml-1.5 lowercase tracking-normal text-text-subtle/70">· {hint}</span>}
|
|
56
|
+
</span>
|
|
57
|
+
{children}
|
|
58
|
+
</label>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { getCapabilitiesForModel, type Caps } from "@/lib/capabilities";
|
|
4
|
+
import { Icon } from "@/components/Icon";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Per-model capability icons, aigetwey's own CapacityBadges + CAPACITY_META.
|
|
8
|
+
* Caps are DERIVED from the model id (not stored per-model) via the same resolver
|
|
9
|
+
* aigetwey uses, so badges match wherever the model appears. Only set caps render.
|
|
10
|
+
*/
|
|
11
|
+
const CAPACITY_META: Record<string, { icon: string; label: string; desc: string; color: string }> = {
|
|
12
|
+
vision: { icon: "visibility", label: "Vision", desc: "Supports image input", color: "text-info" },
|
|
13
|
+
reasoning: { icon: "neurology", label: "Reasoning", desc: "Supports reasoning / thinking", color: "text-warning" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function CapacityBadges({
|
|
17
|
+
model,
|
|
18
|
+
provider = null,
|
|
19
|
+
size = 15,
|
|
20
|
+
className = "",
|
|
21
|
+
}: {
|
|
22
|
+
model: string;
|
|
23
|
+
provider?: string | null;
|
|
24
|
+
size?: number;
|
|
25
|
+
className?: string;
|
|
26
|
+
}) {
|
|
27
|
+
const caps = getCapabilitiesForModel(provider, model);
|
|
28
|
+
const active = Object.keys(CAPACITY_META).filter((k) => caps[k as keyof Caps]);
|
|
29
|
+
if (active.length === 0) return null;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<span className={`inline-flex flex-none items-center gap-0.5 ${className}`}>
|
|
33
|
+
{active.map((k) => (
|
|
34
|
+
<span key={k} title={`${CAPACITY_META[k]!.label} — ${CAPACITY_META[k]!.desc}`} className="leading-none">
|
|
35
|
+
<Icon name={CAPACITY_META[k]!.icon} size={size} className={CAPACITY_META[k]!.color} />
|
|
36
|
+
</span>
|
|
37
|
+
))}
|
|
38
|
+
</span>
|
|
39
|
+
);
|
|
40
|
+
}
|