synapse-gateway 2.0.0
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/README.md +385 -0
- package/bin/synapse.js +242 -0
- package/docs/PLAN.md +1723 -0
- package/docs/PRD.md +1799 -0
- package/drizzle.config.ts +12 -0
- package/next.config.ts +8 -0
- package/package.json +82 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/analytics/cost/route.ts +13 -0
- package/src/app/api/analytics/usage/route.ts +16 -0
- package/src/app/api/auth/login/route.ts +42 -0
- package/src/app/api/cache/route.ts +19 -0
- package/src/app/api/dashboard/route.ts +35 -0
- package/src/app/api/distill/route.ts +10 -0
- package/src/app/api/events/route.ts +54 -0
- package/src/app/api/health/route.ts +10 -0
- package/src/app/api/intelligence/forensics/route.ts +23 -0
- package/src/app/api/intelligence/neural-router/route.ts +23 -0
- package/src/app/api/keys/route.ts +34 -0
- package/src/app/api/mcp/route.ts +49 -0
- package/src/app/api/memory/route.ts +10 -0
- package/src/app/api/models/benchmark/route.ts +13 -0
- package/src/app/api/models/route.ts +39 -0
- package/src/app/api/namespace/route.ts +25 -0
- package/src/app/api/plugins/route.ts +41 -0
- package/src/app/api/providers/accounts/route.ts +91 -0
- package/src/app/api/providers/fetch-models/route.ts +52 -0
- package/src/app/api/providers/health/route.ts +10 -0
- package/src/app/api/providers/route.ts +46 -0
- package/src/app/api/routes/pipeline/route.ts +20 -0
- package/src/app/api/settings/route.ts +33 -0
- package/src/app/api/skills/route.ts +39 -0
- package/src/app/api/v1/chat/completions/route.ts +156 -0
- package/src/app/api/v1/models/route.ts +44 -0
- package/src/app/dashboard/intelligence/loading.tsx +14 -0
- package/src/app/dashboard/intelligence/page.tsx +125 -0
- package/src/app/dashboard/layout.tsx +143 -0
- package/src/app/dashboard/loading.tsx +17 -0
- package/src/app/dashboard/memory/loading.tsx +15 -0
- package/src/app/dashboard/memory/page.tsx +71 -0
- package/src/app/dashboard/models/loading.tsx +13 -0
- package/src/app/dashboard/models/page.tsx +107 -0
- package/src/app/dashboard/page.tsx +183 -0
- package/src/app/dashboard/playground/loading.tsx +17 -0
- package/src/app/dashboard/playground/page.tsx +212 -0
- package/src/app/dashboard/providers/loading.tsx +15 -0
- package/src/app/dashboard/providers/page.tsx +248 -0
- package/src/app/dashboard/routes/loading.tsx +15 -0
- package/src/app/dashboard/routes/page.tsx +72 -0
- package/src/app/dashboard/settings/loading.tsx +20 -0
- package/src/app/dashboard/settings/page.tsx +208 -0
- package/src/app/dashboard/skills/loading.tsx +26 -0
- package/src/app/dashboard/skills/page.tsx +137 -0
- package/src/app/dashboard/vault/loading.tsx +18 -0
- package/src/app/dashboard/vault/page.tsx +139 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +59 -0
- package/src/app/layout.tsx +32 -0
- package/src/app/login/page.tsx +87 -0
- package/src/app/page.tsx +5 -0
- package/src/components/ui/badge.tsx +32 -0
- package/src/components/ui/button.tsx +38 -0
- package/src/components/ui/card.tsx +50 -0
- package/src/components/ui/error-boundary.tsx +47 -0
- package/src/components/ui/index.ts +11 -0
- package/src/components/ui/input.tsx +26 -0
- package/src/components/ui/select.tsx +24 -0
- package/src/components/ui/skeleton.tsx +53 -0
- package/src/components/ui/toast.tsx +51 -0
- package/src/instrumentation.ts +6 -0
- package/src/lib/__tests__/auth.test.ts +42 -0
- package/src/lib/__tests__/format.test.ts +94 -0
- package/src/lib/__tests__/namespace.test.ts +102 -0
- package/src/lib/__tests__/squeezer.test.ts +93 -0
- package/src/lib/__tests__/utils.test.ts +28 -0
- package/src/lib/analytics/index.ts +187 -0
- package/src/lib/auth/guard.tsx +71 -0
- package/src/lib/auth/index.ts +105 -0
- package/src/lib/auth/middleware.ts +64 -0
- package/src/lib/benchmark/index.ts +137 -0
- package/src/lib/bootstrap.ts +122 -0
- package/src/lib/cache/index.ts +1 -0
- package/src/lib/cache/semantic.ts +211 -0
- package/src/lib/config/defaults.ts +61 -0
- package/src/lib/config/index.ts +72 -0
- package/src/lib/config/schema.ts +63 -0
- package/src/lib/db/index.ts +22 -0
- package/src/lib/db/migrate.ts +327 -0
- package/src/lib/db/schema.ts +303 -0
- package/src/lib/distiller/index.ts +331 -0
- package/src/lib/fallback/index.ts +153 -0
- package/src/lib/forensics/index.ts +188 -0
- package/src/lib/format/anthropic.ts +139 -0
- package/src/lib/format/gemini.ts +130 -0
- package/src/lib/format/index.ts +3 -0
- package/src/lib/format/openai.ts +78 -0
- package/src/lib/health/index.ts +158 -0
- package/src/lib/mcp/builtin.ts +83 -0
- package/src/lib/mcp/index.ts +1 -0
- package/src/lib/mcp/registry.ts +49 -0
- package/src/lib/memory/index.ts +3 -0
- package/src/lib/memory/store.ts +215 -0
- package/src/lib/memory/types.ts +56 -0
- package/src/lib/namespace/index.ts +89 -0
- package/src/lib/neural/features.ts +74 -0
- package/src/lib/neural/index.ts +85 -0
- package/src/lib/neural/strategies.ts +124 -0
- package/src/lib/pipeline/index.ts +84 -0
- package/src/lib/pipeline/types.ts +77 -0
- package/src/lib/plugins/builtin.ts +79 -0
- package/src/lib/plugins/index.ts +65 -0
- package/src/lib/prediction/index.ts +113 -0
- package/src/lib/providers/api-key/anthropic.ts +96 -0
- package/src/lib/providers/api-key/deepseek.ts +108 -0
- package/src/lib/providers/api-key/gemini.ts +112 -0
- package/src/lib/providers/api-key/openai.ts +122 -0
- package/src/lib/providers/api-key/openrouter.ts +112 -0
- package/src/lib/providers/base-adapter.ts +122 -0
- package/src/lib/providers/registry.ts +46 -0
- package/src/lib/providers/types.ts +121 -0
- package/src/lib/router/index.ts +82 -0
- package/src/lib/skills/forge.ts +57 -0
- package/src/lib/skills/index.ts +3 -0
- package/src/lib/skills/registry.ts +195 -0
- package/src/lib/skills/types.ts +44 -0
- package/src/lib/squeezer/index.ts +158 -0
- package/src/lib/utils/cn.ts +6 -0
- package/src/lib/utils/logger.ts +16 -0
- package/src/middleware.ts +60 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef } from 'react'
|
|
4
|
+
import { Send, Trash2 } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
interface Message {
|
|
7
|
+
role: 'user' | 'assistant' | 'system'
|
|
8
|
+
content: string
|
|
9
|
+
model?: string
|
|
10
|
+
tokens?: number
|
|
11
|
+
streaming?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ModelOption {
|
|
15
|
+
id: string
|
|
16
|
+
name: string
|
|
17
|
+
providerId: string
|
|
18
|
+
available: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function PlaygroundPage() {
|
|
22
|
+
const [messages, setMessages] = useState<Message[]>([])
|
|
23
|
+
const [input, setInput] = useState('')
|
|
24
|
+
const [model, setModel] = useState('best')
|
|
25
|
+
const [models, setModels] = useState<ModelOption[]>([])
|
|
26
|
+
const [loading, setLoading] = useState(false)
|
|
27
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
fetch('/api/models').then((r) => r.json()).then((data) => {
|
|
31
|
+
const all = (data as { models: ModelOption[] }).models || []
|
|
32
|
+
setModels(all)
|
|
33
|
+
if (all.length > 0 && model === 'best') setModel(all[0].name)
|
|
34
|
+
}).catch(() => {})
|
|
35
|
+
}, [])
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (scrollRef.current) {
|
|
39
|
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
|
40
|
+
}
|
|
41
|
+
}, [messages])
|
|
42
|
+
|
|
43
|
+
async function handleSend() {
|
|
44
|
+
if (!input.trim() || loading) return
|
|
45
|
+
|
|
46
|
+
const userMsg: Message = { role: 'user', content: input.trim() }
|
|
47
|
+
const allMessages = [...messages, userMsg]
|
|
48
|
+
setMessages(allMessages)
|
|
49
|
+
setInput('')
|
|
50
|
+
setLoading(true)
|
|
51
|
+
|
|
52
|
+
const assistantIdx = allMessages.length
|
|
53
|
+
setMessages((prev) => [...prev, { role: 'assistant', content: '', streaming: true }])
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch('/api/v1/chat/completions', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
model,
|
|
61
|
+
messages: allMessages.map((m) => ({ role: m.role, content: m.content })),
|
|
62
|
+
stream: true,
|
|
63
|
+
}),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (!res.ok || !res.body) {
|
|
67
|
+
const errData = await res.json().catch(() => ({ error: { message: 'Request failed' } }))
|
|
68
|
+
setMessages((prev) => {
|
|
69
|
+
const updated = [...prev]
|
|
70
|
+
updated[assistantIdx] = { role: 'assistant', content: errData.error?.message || 'Request failed' }
|
|
71
|
+
return updated
|
|
72
|
+
})
|
|
73
|
+
setLoading(false)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const reader = res.body.getReader()
|
|
78
|
+
const decoder = new TextDecoder()
|
|
79
|
+
let buffer = ''
|
|
80
|
+
let fullContent = ''
|
|
81
|
+
let modelUsed = model
|
|
82
|
+
|
|
83
|
+
while (true) {
|
|
84
|
+
const { done, value } = await reader.read()
|
|
85
|
+
if (done) break
|
|
86
|
+
|
|
87
|
+
buffer += decoder.decode(value, { stream: true })
|
|
88
|
+
const lines = buffer.split('\n')
|
|
89
|
+
buffer = lines.pop() || ''
|
|
90
|
+
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
const trimmed = line.trim()
|
|
93
|
+
if (!trimmed.startsWith('data: ')) continue
|
|
94
|
+
const data = trimmed.slice(6)
|
|
95
|
+
if (data === '[DONE]') continue
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(data)
|
|
99
|
+
if (parsed.error) {
|
|
100
|
+
fullContent += parsed.error.message || JSON.stringify(parsed.error)
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
const delta = parsed.choices?.[0]?.delta?.content
|
|
104
|
+
if (delta) fullContent += delta
|
|
105
|
+
if (parsed.model) modelUsed = parsed.model
|
|
106
|
+
} catch { continue }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
setMessages((prev) => {
|
|
110
|
+
const updated = [...prev]
|
|
111
|
+
updated[assistantIdx] = { role: 'assistant', content: fullContent, model: modelUsed, streaming: true }
|
|
112
|
+
return updated
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
setMessages((prev) => {
|
|
117
|
+
const updated = [...prev]
|
|
118
|
+
updated[assistantIdx] = { role: 'assistant', content: fullContent, model: modelUsed, streaming: false }
|
|
119
|
+
return updated
|
|
120
|
+
})
|
|
121
|
+
} catch {
|
|
122
|
+
setMessages((prev) => {
|
|
123
|
+
const updated = [...prev]
|
|
124
|
+
updated[assistantIdx] = { role: 'assistant', content: 'Network error: Failed to connect' }
|
|
125
|
+
return updated
|
|
126
|
+
})
|
|
127
|
+
} finally {
|
|
128
|
+
setLoading(false)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const namespaceAliases = [
|
|
133
|
+
{ value: 'best', label: 'Best (auto)' },
|
|
134
|
+
{ value: 'fast', label: 'Fast (auto)' },
|
|
135
|
+
{ value: 'cheap', label: 'Cheap (auto)' },
|
|
136
|
+
{ value: 'reasoning', label: 'Reasoning (auto)' },
|
|
137
|
+
{ value: 'code', label: 'Code (auto)' },
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className="flex flex-col h-[calc(100vh-8rem)]">
|
|
142
|
+
<div className="flex items-center justify-between mb-4">
|
|
143
|
+
<div className="flex items-center gap-3">
|
|
144
|
+
<select
|
|
145
|
+
value={model}
|
|
146
|
+
onChange={(e) => setModel(e.target.value)}
|
|
147
|
+
className="bg-muted border border-border rounded-md text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-ring max-w-xs"
|
|
148
|
+
>
|
|
149
|
+
<optgroup label="Namespace Aliases">
|
|
150
|
+
{namespaceAliases.map((a) => (
|
|
151
|
+
<option key={a.value} value={a.value}>{a.label}</option>
|
|
152
|
+
))}
|
|
153
|
+
</optgroup>
|
|
154
|
+
<optgroup label="Models">
|
|
155
|
+
{models.map((m) => (
|
|
156
|
+
<option key={m.id} value={m.name} disabled={!m.available}>
|
|
157
|
+
{m.name} ({m.providerId})
|
|
158
|
+
</option>
|
|
159
|
+
))}
|
|
160
|
+
</optgroup>
|
|
161
|
+
</select>
|
|
162
|
+
</div>
|
|
163
|
+
<button
|
|
164
|
+
onClick={() => setMessages([])}
|
|
165
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground border border-border rounded-md hover:bg-muted transition-colors"
|
|
166
|
+
>
|
|
167
|
+
<Trash2 className="h-3.5 w-3.5" /> Clear
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div ref={scrollRef} className="flex-1 overflow-y-auto bg-card border border-border rounded-lg p-4 space-y-4 mb-4">
|
|
172
|
+
{messages.length === 0 && (
|
|
173
|
+
<div className="flex items-center justify-center h-full text-sm text-muted-foreground">
|
|
174
|
+
Send a message to start testing
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
{messages.map((m, i) => (
|
|
178
|
+
<div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
179
|
+
<div className={`max-w-[80%] rounded-lg px-4 py-2.5 text-sm ${m.role === 'user' ? 'bg-primary/10 text-foreground' : 'bg-muted text-foreground'}`}>
|
|
180
|
+
<p className="whitespace-pre-wrap">{m.content || '\u00A0'}{m.streaming && <span className="animate-pulse">|</span>}</p>
|
|
181
|
+
{m.model && !m.streaming && (
|
|
182
|
+
<div className="text-xs text-muted-foreground mt-1.5 flex items-center gap-2">
|
|
183
|
+
<code className="font-mono">{m.model}</code>
|
|
184
|
+
{m.tokens && <span>{m.tokens} tokens</span>}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div className="flex gap-2">
|
|
193
|
+
<input
|
|
194
|
+
type="text"
|
|
195
|
+
value={input}
|
|
196
|
+
onChange={(e) => setInput(e.target.value)}
|
|
197
|
+
onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSend()}
|
|
198
|
+
placeholder="Type a message..."
|
|
199
|
+
className="flex-1 bg-muted border border-border rounded-md px-4 py-2.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
|
200
|
+
disabled={loading}
|
|
201
|
+
/>
|
|
202
|
+
<button
|
|
203
|
+
onClick={handleSend}
|
|
204
|
+
disabled={loading || !input.trim()}
|
|
205
|
+
className="flex items-center gap-2 px-4 py-2.5 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
206
|
+
>
|
|
207
|
+
<Send className="h-4 w-4" /> Send
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CardSkeleton } from '@/components/ui/skeleton'
|
|
2
|
+
|
|
3
|
+
export default function ProvidersLoading() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="space-y-6">
|
|
6
|
+
<div className="flex items-center justify-between">
|
|
7
|
+
<div className="h-4 w-64 animate-pulse rounded bg-muted" />
|
|
8
|
+
<div className="h-8 w-32 animate-pulse rounded bg-muted" />
|
|
9
|
+
</div>
|
|
10
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
11
|
+
<CardSkeleton key={i} />
|
|
12
|
+
))}
|
|
13
|
+
</div>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { Server, Plus, Eye, EyeOff, Trash2, RefreshCw, Key } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
interface ProviderData {
|
|
7
|
+
providers: Array<{
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
prefix: string
|
|
11
|
+
type: string
|
|
12
|
+
enabled: boolean
|
|
13
|
+
config: string
|
|
14
|
+
createdAt: string
|
|
15
|
+
health: {
|
|
16
|
+
status: string
|
|
17
|
+
avgLatencyMs: number | null
|
|
18
|
+
successRate: number | null
|
|
19
|
+
errorRate: number | null
|
|
20
|
+
} | null
|
|
21
|
+
}>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface AccountData {
|
|
25
|
+
id: string
|
|
26
|
+
providerId: string
|
|
27
|
+
label: string | null
|
|
28
|
+
enabled: boolean
|
|
29
|
+
priority: number
|
|
30
|
+
hasApiKey: boolean
|
|
31
|
+
lastUsedAt: string | null
|
|
32
|
+
createdAt: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function ProvidersPage() {
|
|
36
|
+
const [providers, setProviders] = useState<ProviderData['providers']>([])
|
|
37
|
+
const [accounts, setAccounts] = useState<AccountData[]>([])
|
|
38
|
+
const [expanded, setExpanded] = useState<string | null>(null)
|
|
39
|
+
const [showAddAccount, setShowAddAccount] = useState<string | null>(null)
|
|
40
|
+
const [accountForm, setAccountForm] = useState({ label: '', apiKey: '' })
|
|
41
|
+
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({})
|
|
42
|
+
const [fetching, setFetching] = useState<Record<string, boolean>>({})
|
|
43
|
+
|
|
44
|
+
function loadData() {
|
|
45
|
+
Promise.all([
|
|
46
|
+
fetch('/api/providers').then((r) => r.json()).catch(() => ({ providers: [] })),
|
|
47
|
+
fetch('/api/providers/accounts').then((r) => r.json()).catch(() => ({ accounts: [] })),
|
|
48
|
+
]).then(([pData, aData]) => {
|
|
49
|
+
setProviders((pData as ProviderData).providers || [])
|
|
50
|
+
setAccounts((aData as { accounts: AccountData[] }).accounts || [])
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
useEffect(() => { loadData() }, [])
|
|
55
|
+
|
|
56
|
+
async function handleAddAccount(providerId: string) {
|
|
57
|
+
if (!accountForm.apiKey) return
|
|
58
|
+
await fetch('/api/providers/accounts', {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'Content-Type': 'application/json' },
|
|
61
|
+
body: JSON.stringify({ providerId, label: accountForm.label, apiKey: accountForm.apiKey }),
|
|
62
|
+
})
|
|
63
|
+
setShowAddAccount(null)
|
|
64
|
+
setAccountForm({ label: '', apiKey: '' })
|
|
65
|
+
loadData()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function handleDeleteAccount(id: string) {
|
|
69
|
+
await fetch('/api/providers/accounts', {
|
|
70
|
+
method: 'DELETE',
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
body: JSON.stringify({ id }),
|
|
73
|
+
})
|
|
74
|
+
loadData()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function handleFetchModels(providerId: string) {
|
|
78
|
+
setFetching((prev) => ({ ...prev, [providerId]: true }))
|
|
79
|
+
try {
|
|
80
|
+
await fetch('/api/providers/fetch-models', {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({ providerId }),
|
|
84
|
+
})
|
|
85
|
+
} finally {
|
|
86
|
+
setFetching((prev) => ({ ...prev, [providerId]: false }))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function handleToggleAccount(id: string, enabled: boolean) {
|
|
91
|
+
await fetch('/api/providers/accounts', {
|
|
92
|
+
method: 'PUT',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({ id, enabled }),
|
|
95
|
+
})
|
|
96
|
+
loadData()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="space-y-6">
|
|
101
|
+
<p className="text-sm text-muted-foreground">Configure AI providers, manage API keys, and fetch available models</p>
|
|
102
|
+
|
|
103
|
+
{providers.length === 0 ? (
|
|
104
|
+
<div className="bg-card border border-border rounded-lg p-8 text-center text-sm text-muted-foreground">
|
|
105
|
+
No providers registered. Bootstrap runs on startup.
|
|
106
|
+
</div>
|
|
107
|
+
) : (
|
|
108
|
+
<div className="space-y-3">
|
|
109
|
+
{providers.map((p) => {
|
|
110
|
+
const providerAccounts = accounts.filter((a) => a.providerId === p.id)
|
|
111
|
+
const isExpanded = expanded === p.id
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div key={p.id} className="bg-card border border-border rounded-lg">
|
|
115
|
+
<div className="p-4">
|
|
116
|
+
<div className="flex items-start justify-between">
|
|
117
|
+
<div className="flex items-center gap-3">
|
|
118
|
+
<Server className="h-4 w-4 text-primary shrink-0" />
|
|
119
|
+
<div>
|
|
120
|
+
<div className="flex items-center gap-2">
|
|
121
|
+
<h3 className="font-medium text-sm">{p.name}</h3>
|
|
122
|
+
<span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded">{p.prefix}</span>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
|
|
125
|
+
<span>Type: {p.type}</span>
|
|
126
|
+
<span>{providerAccounts.length} account(s)</span>
|
|
127
|
+
{p.health && (
|
|
128
|
+
<>
|
|
129
|
+
<span className={p.health.status === 'healthy' ? 'text-primary' : 'text-destructive'}>
|
|
130
|
+
{p.health.status}
|
|
131
|
+
</span>
|
|
132
|
+
{p.health.avgLatencyMs != null && <span>{p.health.avgLatencyMs}ms</span>}
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="flex items-center gap-2">
|
|
140
|
+
<span className={`text-xs px-2 py-0.5 rounded-full ${p.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}>
|
|
141
|
+
{p.enabled ? 'Enabled' : 'Disabled'}
|
|
142
|
+
</span>
|
|
143
|
+
<button
|
|
144
|
+
onClick={() => setExpanded(isExpanded ? null : p.id)}
|
|
145
|
+
className="px-2 py-1 text-xs border border-border rounded hover:bg-muted transition-colors"
|
|
146
|
+
>
|
|
147
|
+
{isExpanded ? 'Collapse' : 'Manage'}
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{isExpanded && (
|
|
154
|
+
<div className="border-t border-border p-4 space-y-3">
|
|
155
|
+
<div className="flex items-center justify-between">
|
|
156
|
+
<h4 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Accounts</h4>
|
|
157
|
+
<div className="flex items-center gap-2">
|
|
158
|
+
<button
|
|
159
|
+
onClick={() => handleFetchModels(p.id)}
|
|
160
|
+
disabled={fetching[p.id]}
|
|
161
|
+
className="flex items-center gap-1.5 px-2 py-1 text-xs border border-border rounded hover:bg-muted transition-colors disabled:opacity-50"
|
|
162
|
+
>
|
|
163
|
+
<RefreshCw className={`h-3 w-3 ${fetching[p.id] ? 'animate-spin' : ''}`} />
|
|
164
|
+
Fetch Models
|
|
165
|
+
</button>
|
|
166
|
+
<button
|
|
167
|
+
onClick={() => setShowAddAccount(showAddAccount === p.id ? null : p.id)}
|
|
168
|
+
className="flex items-center gap-1.5 px-2 py-1 text-xs bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors"
|
|
169
|
+
>
|
|
170
|
+
<Plus className="h-3 w-3" /> Add Account
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{showAddAccount === p.id && (
|
|
176
|
+
<div className="bg-muted/50 border border-border rounded-md p-3 space-y-2">
|
|
177
|
+
<input
|
|
178
|
+
placeholder="Label (e.g. Production)"
|
|
179
|
+
value={accountForm.label}
|
|
180
|
+
onChange={(e) => setAccountForm({ ...accountForm, label: e.target.value })}
|
|
181
|
+
className="w-full bg-background border border-border rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
|
182
|
+
/>
|
|
183
|
+
<div className="relative">
|
|
184
|
+
<input
|
|
185
|
+
type={showApiKey[`new-${p.id}`] ? 'text' : 'password'}
|
|
186
|
+
placeholder="API Key"
|
|
187
|
+
value={accountForm.apiKey}
|
|
188
|
+
onChange={(e) => setAccountForm({ ...accountForm, apiKey: e.target.value })}
|
|
189
|
+
className="w-full bg-background border border-border rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring pr-10"
|
|
190
|
+
/>
|
|
191
|
+
<button
|
|
192
|
+
onClick={() => setShowApiKey((prev) => ({ ...prev, [`new-${p.id}`]: !prev[`new-${p.id}`] }))}
|
|
193
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
194
|
+
>
|
|
195
|
+
{showApiKey[`new-${p.id}`] ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
<div className="flex gap-2">
|
|
199
|
+
<button onClick={() => handleAddAccount(p.id)} disabled={!accountForm.apiKey} className="px-3 py-1 bg-primary text-primary-foreground rounded text-xs hover:bg-primary/90 disabled:opacity-50">Save</button>
|
|
200
|
+
<button onClick={() => { setShowAddAccount(null); setAccountForm({ label: '', apiKey: '' }) }} className="px-3 py-1 border border-border rounded text-xs hover:bg-muted">Cancel</button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
{providerAccounts.length === 0 ? (
|
|
206
|
+
<div className="text-xs text-muted-foreground py-2">No accounts configured. Add an API key to start using this provider.</div>
|
|
207
|
+
) : (
|
|
208
|
+
providerAccounts.map((a) => (
|
|
209
|
+
<div key={a.id} className="flex items-center justify-between bg-muted/30 border border-border rounded-md px-3 py-2">
|
|
210
|
+
<div className="flex items-center gap-3">
|
|
211
|
+
<Key className="h-3.5 w-3.5 text-muted-foreground" />
|
|
212
|
+
<div>
|
|
213
|
+
<span className="text-sm">{a.label || 'Unnamed'}</span>
|
|
214
|
+
<span className="ml-2 text-xs text-muted-foreground">
|
|
215
|
+
{a.hasApiKey ? 'Key configured' : 'No key'}
|
|
216
|
+
</span>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
<div className="flex items-center gap-2">
|
|
220
|
+
<span className="text-xs text-muted-foreground">
|
|
221
|
+
{a.lastUsedAt ? `Used ${new Date(a.lastUsedAt).toLocaleDateString()}` : 'Never used'}
|
|
222
|
+
</span>
|
|
223
|
+
<button
|
|
224
|
+
onClick={() => handleToggleAccount(a.id, !a.enabled)}
|
|
225
|
+
className={`text-xs px-2 py-0.5 rounded-full ${a.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}
|
|
226
|
+
>
|
|
227
|
+
{a.enabled ? 'Active' : 'Disabled'}
|
|
228
|
+
</button>
|
|
229
|
+
<button
|
|
230
|
+
onClick={() => handleDeleteAccount(a.id)}
|
|
231
|
+
className="p-1 hover:bg-destructive/10 rounded text-muted-foreground hover:text-destructive transition-colors"
|
|
232
|
+
>
|
|
233
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
))
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
)
|
|
243
|
+
})}
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CardSkeleton } from '@/components/ui/skeleton'
|
|
2
|
+
|
|
3
|
+
export default function RoutesLoading() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="space-y-6">
|
|
6
|
+
<div className="flex items-center justify-between">
|
|
7
|
+
<div className="h-4 w-64 animate-pulse rounded bg-muted" />
|
|
8
|
+
<div className="h-8 w-32 animate-pulse rounded bg-muted" />
|
|
9
|
+
</div>
|
|
10
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
11
|
+
<CardSkeleton key={i} />
|
|
12
|
+
))}
|
|
13
|
+
</div>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { Route, Plus, ArrowRight } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
interface Pipeline {
|
|
7
|
+
id: string
|
|
8
|
+
name: string
|
|
9
|
+
description: string | null
|
|
10
|
+
nodes: Array<{ id: string; type: string; config: Record<string, unknown> }>
|
|
11
|
+
connections: Array<{ from: string; to: string }>
|
|
12
|
+
enabled: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function RoutesPage() {
|
|
16
|
+
const [pipelines, setPipelines] = useState<Pipeline[]>([])
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
fetch('/api/routes/pipeline').then((r) => r.json()).then((data) => setPipelines((data as { pipelines: Pipeline[] }).pipelines || [])).catch(() => {})
|
|
20
|
+
}, [])
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="space-y-6">
|
|
24
|
+
<div className="flex items-center justify-between">
|
|
25
|
+
<p className="text-sm text-muted-foreground">Configure routing pipelines and fallback chains</p>
|
|
26
|
+
<button className="flex items-center gap-2 px-3 py-1.5 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 transition-colors">
|
|
27
|
+
<Plus className="h-4 w-4" /> Add Pipeline
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
{pipelines.length === 0 ? (
|
|
32
|
+
<div className="bg-card border border-border rounded-lg p-8 text-center text-sm text-muted-foreground">
|
|
33
|
+
No pipelines configured. The default pipeline processes requests through provider routing with automatic fallback.
|
|
34
|
+
</div>
|
|
35
|
+
) : (
|
|
36
|
+
<div className="space-y-3">
|
|
37
|
+
{pipelines.map((p) => (
|
|
38
|
+
<div key={p.id} className="bg-card border border-border rounded-lg p-4">
|
|
39
|
+
<div className="flex items-center gap-3 mb-3">
|
|
40
|
+
<Route className="h-4 w-4 text-primary shrink-0" />
|
|
41
|
+
<h3 className="font-medium text-sm flex-1">{p.name}</h3>
|
|
42
|
+
<span className="text-xs text-muted-foreground">
|
|
43
|
+
{p.nodes?.length || 0} nodes
|
|
44
|
+
</span>
|
|
45
|
+
<span className={`text-xs px-2 py-0.5 rounded-full ${p.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}>
|
|
46
|
+
{p.enabled ? 'Active' : 'Disabled'}
|
|
47
|
+
</span>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{p.description && (
|
|
51
|
+
<p className="text-xs text-muted-foreground mb-3">{p.description}</p>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
{p.nodes && p.nodes.length > 0 && (
|
|
55
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
56
|
+
{p.nodes.map((n, i) => (
|
|
57
|
+
<span key={n.id} className="flex items-center gap-1">
|
|
58
|
+
{i > 0 && <ArrowRight className="h-3 w-3 text-muted-foreground" />}
|
|
59
|
+
<code className="text-xs font-mono bg-muted px-2 py-1 rounded">
|
|
60
|
+
{n.type}
|
|
61
|
+
</code>
|
|
62
|
+
</span>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export default function SettingsLoading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex gap-6 h-[calc(100vh-8rem)]">
|
|
4
|
+
<div className="w-48 shrink-0 space-y-2">
|
|
5
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
6
|
+
<div key={i} className="h-9 w-full animate-pulse rounded bg-muted" />
|
|
7
|
+
))}
|
|
8
|
+
</div>
|
|
9
|
+
<div className="flex-1 bg-card border border-border rounded-lg p-6 space-y-6">
|
|
10
|
+
<div className="h-6 w-48 animate-pulse rounded bg-muted" />
|
|
11
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
12
|
+
<div key={i} className="space-y-2">
|
|
13
|
+
<div className="h-4 w-32 animate-pulse rounded bg-muted" />
|
|
14
|
+
<div className="h-9 w-40 animate-pulse rounded bg-muted" />
|
|
15
|
+
</div>
|
|
16
|
+
))}
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|