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,91 @@
|
|
|
1
|
+
import { db } from '@/lib/db'
|
|
2
|
+
import { providerAccounts } from '@/lib/db/schema'
|
|
3
|
+
import { eq } from 'drizzle-orm'
|
|
4
|
+
|
|
5
|
+
export async function GET(request: Request) {
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(request.url)
|
|
8
|
+
const providerId = url.searchParams.get('providerId')
|
|
9
|
+
|
|
10
|
+
const accounts = providerId
|
|
11
|
+
? await db.select().from(providerAccounts).where(eq(providerAccounts.providerId, providerId))
|
|
12
|
+
: await db.select().from(providerAccounts)
|
|
13
|
+
|
|
14
|
+
return Response.json({
|
|
15
|
+
accounts: accounts.map((a) => ({
|
|
16
|
+
id: a.id,
|
|
17
|
+
providerId: a.providerId,
|
|
18
|
+
label: a.label,
|
|
19
|
+
enabled: a.enabled,
|
|
20
|
+
priority: a.priority,
|
|
21
|
+
hasApiKey: !!(a.authData && a.authData !== '{}'),
|
|
22
|
+
lastUsedAt: a.lastUsedAt,
|
|
23
|
+
createdAt: a.createdAt,
|
|
24
|
+
})),
|
|
25
|
+
})
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function POST(request: Request) {
|
|
32
|
+
try {
|
|
33
|
+
const body = await request.json()
|
|
34
|
+
const { providerId, label, apiKey, enabled, priority } = body
|
|
35
|
+
|
|
36
|
+
if (!providerId || !apiKey) {
|
|
37
|
+
return Response.json({ error: 'providerId and apiKey are required' }, { status: 400 })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const id = crypto.randomUUID()
|
|
41
|
+
await db.insert(providerAccounts).values({
|
|
42
|
+
id,
|
|
43
|
+
providerId,
|
|
44
|
+
label: label || null,
|
|
45
|
+
authData: JSON.stringify({ apiKey }),
|
|
46
|
+
enabled: enabled ?? true,
|
|
47
|
+
priority: priority ?? 0,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return Response.json({ success: true, id })
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function PUT(request: Request) {
|
|
57
|
+
try {
|
|
58
|
+
const body = await request.json()
|
|
59
|
+
const { id, label, apiKey, enabled, priority } = body
|
|
60
|
+
|
|
61
|
+
if (!id) {
|
|
62
|
+
return Response.json({ error: 'id is required' }, { status: 400 })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const updates: Record<string, unknown> = {
|
|
66
|
+
updatedAt: new Date().toISOString(),
|
|
67
|
+
}
|
|
68
|
+
if (label !== undefined) updates.label = label
|
|
69
|
+
if (apiKey !== undefined) updates.authData = JSON.stringify({ apiKey })
|
|
70
|
+
if (enabled !== undefined) updates.enabled = enabled
|
|
71
|
+
if (priority !== undefined) updates.priority = priority
|
|
72
|
+
|
|
73
|
+
await db.update(providerAccounts).set(updates).where(eq(providerAccounts.id, id))
|
|
74
|
+
|
|
75
|
+
return Response.json({ success: true })
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function DELETE(request: Request) {
|
|
82
|
+
try {
|
|
83
|
+
const { id } = await request.json()
|
|
84
|
+
if (!id) return Response.json({ error: 'id is required' }, { status: 400 })
|
|
85
|
+
|
|
86
|
+
await db.delete(providerAccounts).where(eq(providerAccounts.id, id))
|
|
87
|
+
return Response.json({ success: true })
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { registry } from '@/lib/providers/registry'
|
|
2
|
+
import { db } from '@/lib/db'
|
|
3
|
+
import { models } from '@/lib/db/schema'
|
|
4
|
+
import { eq } from 'drizzle-orm'
|
|
5
|
+
|
|
6
|
+
export async function POST(request: Request) {
|
|
7
|
+
try {
|
|
8
|
+
const { providerId } = await request.json()
|
|
9
|
+
if (!providerId) {
|
|
10
|
+
return Response.json({ error: 'providerId is required' }, { status: 400 })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const adapters = registry.list()
|
|
14
|
+
const adapter = adapters.find((a) => a.info.id === providerId)
|
|
15
|
+
if (!adapter) {
|
|
16
|
+
return Response.json({ error: `Provider not found: ${providerId}` }, { status: 404 })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const fetchedModels = await adapter.fetchModels()
|
|
20
|
+
|
|
21
|
+
let upserted = 0
|
|
22
|
+
for (const m of fetchedModels) {
|
|
23
|
+
const modelId = m.id.includes('/') ? m.id : `${adapter.info.prefix}${m.id}`
|
|
24
|
+
try {
|
|
25
|
+
await db.insert(models).values({
|
|
26
|
+
id: modelId,
|
|
27
|
+
providerId: adapter.info.id,
|
|
28
|
+
name: m.name,
|
|
29
|
+
displayName: m.displayName || null,
|
|
30
|
+
pricingTier: m.pricingTier || null,
|
|
31
|
+
costPer1mInput: m.costPer1mInput ?? null,
|
|
32
|
+
costPer1mOutput: m.costPer1mOutput ?? null,
|
|
33
|
+
contextWindow: m.contextWindow ?? null,
|
|
34
|
+
capabilities: m.capabilities ? JSON.stringify(m.capabilities) : '[]',
|
|
35
|
+
available: m.available,
|
|
36
|
+
}).onConflictDoUpdate({
|
|
37
|
+
target: models.id,
|
|
38
|
+
set: {
|
|
39
|
+
displayName: m.displayName || null,
|
|
40
|
+
available: m.available,
|
|
41
|
+
lastCheckedAt: new Date().toISOString(),
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
upserted++
|
|
45
|
+
} catch {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return Response.json({ success: true, fetched: fetchedModels.length, upserted })
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { healthChecker } from '@/lib/health'
|
|
2
|
+
|
|
3
|
+
export async function GET() {
|
|
4
|
+
try {
|
|
5
|
+
const health = await healthChecker.getProviderHealth()
|
|
6
|
+
return Response.json({ health })
|
|
7
|
+
} catch (err) {
|
|
8
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { db } from '@/lib/db'
|
|
2
|
+
import { providers, providerAccounts, providerHealth } from '@/lib/db/schema'
|
|
3
|
+
import { eq } from 'drizzle-orm'
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
try {
|
|
7
|
+
const allProviders = await db.select().from(providers)
|
|
8
|
+
const allAccounts = await db.select().from(providerAccounts)
|
|
9
|
+
const allHealth = await db.select().from(providerHealth)
|
|
10
|
+
|
|
11
|
+
const result = allProviders.map((p) => ({
|
|
12
|
+
...p,
|
|
13
|
+
accounts: allAccounts.filter((a) => a.providerId === p.id),
|
|
14
|
+
health: allHealth.find((h) => h.providerId === p.id) || null,
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
return Response.json({ providers: result })
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function POST(request: Request) {
|
|
24
|
+
try {
|
|
25
|
+
const body = await request.json()
|
|
26
|
+
const { id, name, prefix, type, config, enabled, priority } = body
|
|
27
|
+
|
|
28
|
+
if (!id || !name || !prefix || !type) {
|
|
29
|
+
return Response.json({ error: 'id, name, prefix, type are required' }, { status: 400 })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await db.insert(providers).values({
|
|
33
|
+
id,
|
|
34
|
+
name,
|
|
35
|
+
prefix,
|
|
36
|
+
type,
|
|
37
|
+
config: config || '{}',
|
|
38
|
+
enabled: enabled ?? true,
|
|
39
|
+
priority: priority ?? 0,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return Response.json({ success: true, id })
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { pipelineEngine } from '@/lib/pipeline'
|
|
2
|
+
|
|
3
|
+
export async function GET() {
|
|
4
|
+
try {
|
|
5
|
+
const pipelines = await pipelineEngine.list()
|
|
6
|
+
return Response.json({ pipelines })
|
|
7
|
+
} catch (err) {
|
|
8
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function POST(request: Request) {
|
|
13
|
+
try {
|
|
14
|
+
const body = await request.json()
|
|
15
|
+
const id = await pipelineEngine.create(body)
|
|
16
|
+
return Response.json({ success: true, id })
|
|
17
|
+
} catch (err) {
|
|
18
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { db } from '@/lib/db'
|
|
2
|
+
import { settings } from '@/lib/db/schema'
|
|
3
|
+
import { eq } from 'drizzle-orm'
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
try {
|
|
7
|
+
const allSettings = await db.select().from(settings)
|
|
8
|
+
const config: Record<string, string> = {}
|
|
9
|
+
for (const s of allSettings) {
|
|
10
|
+
config[s.key] = s.value
|
|
11
|
+
}
|
|
12
|
+
return Response.json({ settings: config })
|
|
13
|
+
} catch (err) {
|
|
14
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function PUT(request: Request) {
|
|
19
|
+
try {
|
|
20
|
+
const body = await request.json()
|
|
21
|
+
for (const [key, value] of Object.entries(body)) {
|
|
22
|
+
if (typeof value === 'string') {
|
|
23
|
+
await db.insert(settings).values({ key, value }).onConflictDoUpdate({
|
|
24
|
+
target: settings.key,
|
|
25
|
+
set: { value, updatedAt: new Date().toISOString() },
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return Response.json({ success: true })
|
|
30
|
+
} catch (err) {
|
|
31
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { skillRegistry } from '@/lib/skills/registry'
|
|
2
|
+
import { skillForge } from '@/lib/skills/forge'
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
try {
|
|
6
|
+
const skills = await skillRegistry.listSkills()
|
|
7
|
+
const groups = await skillRegistry.listGroups()
|
|
8
|
+
return Response.json({ skills, groups })
|
|
9
|
+
} catch (err) {
|
|
10
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function POST(request: Request) {
|
|
15
|
+
try {
|
|
16
|
+
const body = await request.json()
|
|
17
|
+
|
|
18
|
+
if (body.action === 'create') {
|
|
19
|
+
const skillId = await skillForge.createFromRecipe({
|
|
20
|
+
name: body.name,
|
|
21
|
+
description: body.description || '',
|
|
22
|
+
basePrompt: body.systemPrompt,
|
|
23
|
+
examples: body.examples || [],
|
|
24
|
+
constraints: body.constraints,
|
|
25
|
+
preferredModel: body.preferredModel,
|
|
26
|
+
})
|
|
27
|
+
return Response.json({ success: true, skillId })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (body.action === 'import_openclaw') {
|
|
31
|
+
const skillId = await skillForge.importFromOpenClaw(body)
|
|
32
|
+
return Response.json({ success: true, skillId })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Response.json({ error: 'Unknown action' }, { status: 400 })
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return Response.json({ error: (err as Error).message }, { status: 500 })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { routeRequest, routeRequestStream } from '@/lib/router'
|
|
3
|
+
import { db } from '@/lib/db'
|
|
4
|
+
import { providerAccounts, requestLogs } from '@/lib/db/schema'
|
|
5
|
+
import type { ProviderAccount } from '@/lib/providers/types'
|
|
6
|
+
import { logger } from '@/lib/utils/logger'
|
|
7
|
+
import { v4 as uuid } from 'uuid'
|
|
8
|
+
|
|
9
|
+
function getCorsHeaders() {
|
|
10
|
+
return {
|
|
11
|
+
'Access-Control-Allow-Origin': '*',
|
|
12
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
13
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-Key',
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function OPTIONS() {
|
|
18
|
+
return NextResponse.json({}, { headers: getCorsHeaders() })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function POST(request: Request) {
|
|
22
|
+
const requestId = uuid()
|
|
23
|
+
const startTime = Date.now()
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const body = await request.json()
|
|
27
|
+
const { model, messages, temperature, max_tokens, maxTokens, stream, tools } = body
|
|
28
|
+
|
|
29
|
+
if (!model || !messages || !Array.isArray(messages)) {
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ error: { type: 'invalid_request', message: 'model and messages are required' } },
|
|
32
|
+
{ status: 400, headers: getCorsHeaders() }
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalizedReq = {
|
|
37
|
+
model: model as string,
|
|
38
|
+
messages: messages as Array<{ role: 'system' | 'user' | 'assistant' | 'tool'; content: string }>,
|
|
39
|
+
temperature: temperature as number | undefined,
|
|
40
|
+
maxTokens: (maxTokens || max_tokens) as number | undefined,
|
|
41
|
+
stream: stream as boolean | undefined,
|
|
42
|
+
tools: tools as unknown[] | undefined,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const accountCache = new Map<string, ProviderAccount | null>()
|
|
46
|
+
const getAccount = (providerId: string): ProviderAccount | null => {
|
|
47
|
+
if (!accountCache.has(providerId)) return null
|
|
48
|
+
return accountCache.get(providerId) ?? null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const allAccounts = await db.select().from(providerAccounts)
|
|
52
|
+
for (const row of allAccounts) {
|
|
53
|
+
let authData: Record<string, unknown> = {}
|
|
54
|
+
try { authData = JSON.parse(row.authData || '{}') } catch {}
|
|
55
|
+
const account: ProviderAccount = {
|
|
56
|
+
id: row.id,
|
|
57
|
+
providerId: row.providerId,
|
|
58
|
+
label: row.label || undefined,
|
|
59
|
+
authData,
|
|
60
|
+
enabled: row.enabled,
|
|
61
|
+
priority: row.priority,
|
|
62
|
+
quotaUsedTokens: row.quotaUsedTokens,
|
|
63
|
+
quotaLimitTokens: row.quotaLimitTokens ?? undefined,
|
|
64
|
+
quotaResetAt: row.quotaResetAt ? new Date(row.quotaResetAt) : undefined,
|
|
65
|
+
lastUsedAt: row.lastUsedAt ? new Date(row.lastUsedAt) : undefined,
|
|
66
|
+
}
|
|
67
|
+
accountCache.set(row.providerId, account)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (stream) {
|
|
71
|
+
const encoder = new TextEncoder()
|
|
72
|
+
const streamResult = new ReadableStream({
|
|
73
|
+
async start(controller) {
|
|
74
|
+
try {
|
|
75
|
+
const routeStream = await routeRequestStream(normalizedReq, getAccount)
|
|
76
|
+
for await (const chunk of routeStream) {
|
|
77
|
+
const data = JSON.stringify({
|
|
78
|
+
id: chunk.id || requestId,
|
|
79
|
+
object: 'chat.completion.chunk',
|
|
80
|
+
model: routeStream.model,
|
|
81
|
+
choices: chunk.choices.map((c: { index: number; delta: Record<string, unknown>; finishReason: string | null }) => ({
|
|
82
|
+
index: c.index,
|
|
83
|
+
delta: c.delta,
|
|
84
|
+
finish_reason: c.finishReason,
|
|
85
|
+
})),
|
|
86
|
+
})
|
|
87
|
+
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
|
|
88
|
+
}
|
|
89
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
|
90
|
+
controller.close()
|
|
91
|
+
} catch (err) {
|
|
92
|
+
const errorData = JSON.stringify({ error: { type: 'stream_error', message: (err as Error).message } })
|
|
93
|
+
controller.enqueue(encoder.encode(`data: ${errorData}\n\n`))
|
|
94
|
+
controller.close()
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return new Response(streamResult, {
|
|
100
|
+
headers: {
|
|
101
|
+
'Content-Type': 'text/event-stream',
|
|
102
|
+
'Cache-Control': 'no-cache',
|
|
103
|
+
Connection: 'keep-alive',
|
|
104
|
+
...getCorsHeaders(),
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const result = await routeRequest(normalizedReq, getAccount)
|
|
110
|
+
const latencyMs = Date.now() - startTime
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await db.insert(requestLogs).values({
|
|
114
|
+
id: uuid(),
|
|
115
|
+
requestId,
|
|
116
|
+
model: result.model,
|
|
117
|
+
providerId: result.provider,
|
|
118
|
+
inputTokens: result.response.usage.inputTokens,
|
|
119
|
+
outputTokens: result.response.usage.outputTokens,
|
|
120
|
+
tokensSaved: 0,
|
|
121
|
+
cost: 0,
|
|
122
|
+
latencyMs,
|
|
123
|
+
statusCode: 200,
|
|
124
|
+
fallbackUsed: result.fallbackUsed,
|
|
125
|
+
cached: false,
|
|
126
|
+
})
|
|
127
|
+
} catch (logErr) {
|
|
128
|
+
logger.warn({ error: (logErr as Error).message }, 'Failed to log request')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const openaiResponse = {
|
|
132
|
+
id: result.response.id,
|
|
133
|
+
object: 'chat.completion',
|
|
134
|
+
created: Math.floor(Date.now() / 1000),
|
|
135
|
+
model: result.model,
|
|
136
|
+
choices: result.response.choices.map((c: { index: number; message: { role: string; content: string }; finishReason: string }) => ({
|
|
137
|
+
index: c.index,
|
|
138
|
+
message: { role: c.message.role, content: c.message.content },
|
|
139
|
+
finish_reason: c.finishReason,
|
|
140
|
+
})),
|
|
141
|
+
usage: {
|
|
142
|
+
prompt_tokens: result.response.usage.inputTokens,
|
|
143
|
+
completion_tokens: result.response.usage.outputTokens,
|
|
144
|
+
total_tokens: result.response.usage.totalTokens,
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return NextResponse.json(openaiResponse, { headers: getCorsHeaders() })
|
|
149
|
+
} catch (err) {
|
|
150
|
+
logger.error({ error: (err as Error).message, requestId }, 'Chat completion failed')
|
|
151
|
+
return NextResponse.json(
|
|
152
|
+
{ error: { type: 'server_error', message: (err as Error).message } },
|
|
153
|
+
{ status: 500, headers: getCorsHeaders() }
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { registry } from '@/lib/providers/registry'
|
|
3
|
+
|
|
4
|
+
function getCorsHeaders() {
|
|
5
|
+
return {
|
|
6
|
+
'Access-Control-Allow-Origin': '*',
|
|
7
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
8
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-Key',
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function OPTIONS() {
|
|
13
|
+
return NextResponse.json({}, { headers: getCorsHeaders() })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function GET() {
|
|
17
|
+
const allModels: Array<{
|
|
18
|
+
id: string
|
|
19
|
+
object: string
|
|
20
|
+
created: number
|
|
21
|
+
owned_by: string
|
|
22
|
+
}> = []
|
|
23
|
+
|
|
24
|
+
for (const adapter of registry.list()) {
|
|
25
|
+
try {
|
|
26
|
+
const models = await adapter.fetchModels()
|
|
27
|
+
for (const model of models) {
|
|
28
|
+
allModels.push({
|
|
29
|
+
id: model.id,
|
|
30
|
+
object: 'model',
|
|
31
|
+
created: Math.floor(Date.now() / 1000),
|
|
32
|
+
owned_by: adapter.info.name,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return NextResponse.json({
|
|
41
|
+
object: 'list',
|
|
42
|
+
data: allModels,
|
|
43
|
+
}, { headers: getCorsHeaders() })
|
|
44
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { StatsSkeleton } from '@/components/ui/skeleton'
|
|
2
|
+
|
|
3
|
+
export default function IntelligenceLoading() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="space-y-6">
|
|
6
|
+
<div className="h-4 w-64 animate-pulse rounded bg-muted" />
|
|
7
|
+
<StatsSkeleton />
|
|
8
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
9
|
+
<div className="bg-card border border-border rounded-lg p-4 h-64 animate-pulse" />
|
|
10
|
+
<div className="bg-card border border-border rounded-lg p-4 h-64 animate-pulse" />
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { Sparkles, Brain, TrendingUp, Lightbulb } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
interface NeuralRouterData {
|
|
7
|
+
defaultStrategy: string
|
|
8
|
+
totalSamples: number
|
|
9
|
+
accuracy: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ForensicsData {
|
|
13
|
+
recentFailures: Array<Record<string, unknown>>
|
|
14
|
+
slowRequests: Array<Record<string, unknown>>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function IntelligencePage() {
|
|
18
|
+
const [routerData, setRouterData] = useState<NeuralRouterData | null>(null)
|
|
19
|
+
const [forensicsData, setForensicsData] = useState<ForensicsData | null>(null)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
Promise.all([
|
|
23
|
+
fetch('/api/intelligence/neural-router').then((r) => r.json()).catch(() => null),
|
|
24
|
+
fetch('/api/intelligence/forensics').then((r) => r.json()).catch(() => null),
|
|
25
|
+
]).then(([router, forensics]) => {
|
|
26
|
+
if (router) setRouterData(router as NeuralRouterData)
|
|
27
|
+
if (forensics) setForensicsData(forensics as ForensicsData)
|
|
28
|
+
})
|
|
29
|
+
}, [])
|
|
30
|
+
|
|
31
|
+
const metrics = [
|
|
32
|
+
{ label: 'Routing Strategy', value: routerData?.defaultStrategy || '—', icon: Brain },
|
|
33
|
+
{ label: 'Training Samples', value: (routerData?.totalSamples || 0).toLocaleString(), icon: TrendingUp },
|
|
34
|
+
{ label: 'Router Accuracy', value: routerData ? `${(routerData.accuracy * 100).toFixed(1)}%` : '—', icon: Sparkles },
|
|
35
|
+
{ label: 'Recent Failures', value: (forensicsData?.recentFailures?.length || 0).toString(), icon: Lightbulb },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="space-y-6">
|
|
40
|
+
<p className="text-sm text-muted-foreground">Self-learning engine, neural router, and request forensics</p>
|
|
41
|
+
|
|
42
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
43
|
+
{metrics.map((m) => (
|
|
44
|
+
<div key={m.label} className="bg-card border border-border rounded-lg p-4">
|
|
45
|
+
<div className="flex items-center justify-between mb-2">
|
|
46
|
+
<span className="text-xs text-muted-foreground uppercase tracking-wider">{m.label}</span>
|
|
47
|
+
<m.icon className="h-4 w-4 text-accent" />
|
|
48
|
+
</div>
|
|
49
|
+
<span className="text-2xl font-semibold">{m.value}</span>
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
55
|
+
<div className="bg-card border border-border rounded-lg">
|
|
56
|
+
<div className="px-4 py-3 border-b border-border flex items-center justify-between">
|
|
57
|
+
<h2 className="text-sm font-medium">Neural Router</h2>
|
|
58
|
+
<button
|
|
59
|
+
onClick={async () => {
|
|
60
|
+
const strategy = routerData?.defaultStrategy === 'hybrid' ? 'priority' : 'hybrid'
|
|
61
|
+
await fetch('/api/intelligence/neural-router', {
|
|
62
|
+
method: 'PUT',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({ strategy }),
|
|
65
|
+
})
|
|
66
|
+
setRouterData((prev) => prev ? { ...prev, defaultStrategy: strategy } : null)
|
|
67
|
+
}}
|
|
68
|
+
className="text-xs px-2 py-1 border border-border rounded hover:bg-muted"
|
|
69
|
+
>
|
|
70
|
+
Toggle Strategy
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="p-4 space-y-3">
|
|
74
|
+
<div className="flex justify-between text-sm">
|
|
75
|
+
<span className="text-muted-foreground">Strategy</span>
|
|
76
|
+
<span className="font-medium capitalize">{routerData?.defaultStrategy || '—'}</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="flex justify-between text-sm">
|
|
79
|
+
<span className="text-muted-foreground">Samples</span>
|
|
80
|
+
<span>{(routerData?.totalSamples || 0).toLocaleString()}</span>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="flex justify-between text-sm">
|
|
83
|
+
<span className="text-muted-foreground">Accuracy</span>
|
|
84
|
+
<span className={routerData && routerData.accuracy > 0.8 ? 'text-primary' : 'text-yellow-500'}>
|
|
85
|
+
{routerData ? `${(routerData.accuracy * 100).toFixed(1)}%` : 'N/A'}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
<div className="flex justify-between text-sm">
|
|
89
|
+
<span className="text-muted-foreground">Status</span>
|
|
90
|
+
<span className="text-primary flex items-center gap-1.5">
|
|
91
|
+
<span className="h-1.5 w-1.5 rounded-full bg-primary animate-pulse" />
|
|
92
|
+
Active
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="bg-card border border-border rounded-lg">
|
|
99
|
+
<div className="px-4 py-3 border-b border-border">
|
|
100
|
+
<h2 className="text-sm font-medium">Request Forensics</h2>
|
|
101
|
+
</div>
|
|
102
|
+
<div className="p-4 space-y-3">
|
|
103
|
+
<div className="flex justify-between text-sm">
|
|
104
|
+
<span className="text-muted-foreground">Recent Failures</span>
|
|
105
|
+
<span>{forensicsData?.recentFailures?.length || 0}</span>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="flex justify-between text-sm">
|
|
108
|
+
<span className="text-muted-foreground">Slow Requests (>3s)</span>
|
|
109
|
+
<span>{forensicsData?.slowRequests?.length || 0}</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div className="text-xs text-muted-foreground mt-4">
|
|
112
|
+
Forensic analysis runs automatically on each request, providing root cause analysis, latency breakdowns, and optimization suggestions.
|
|
113
|
+
</div>
|
|
114
|
+
<button
|
|
115
|
+
onClick={async () => { await fetch('/api/distill', { method: 'POST' }) }}
|
|
116
|
+
className="w-full px-3 py-1.5 bg-accent/10 text-accent border border-accent/20 rounded-md text-sm hover:bg-accent/20 transition-colors"
|
|
117
|
+
>
|
|
118
|
+
Trigger Distillation Now
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|