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,215 @@
|
|
|
1
|
+
import { db } from '../db'
|
|
2
|
+
import { episodes, knowledge, proceduralRules } from '../db/schema'
|
|
3
|
+
import { eq, desc, sql } from 'drizzle-orm'
|
|
4
|
+
import type { MemorySearchResult, MemoryType } from './types'
|
|
5
|
+
import { logger } from '../utils/logger'
|
|
6
|
+
import { v4 as uuid } from 'uuid'
|
|
7
|
+
|
|
8
|
+
export interface EpisodeInput {
|
|
9
|
+
eventType: string
|
|
10
|
+
model?: string
|
|
11
|
+
provider?: string
|
|
12
|
+
taskType?: string
|
|
13
|
+
inputTokens?: number
|
|
14
|
+
outputTokens?: number
|
|
15
|
+
tokensSaved?: number
|
|
16
|
+
latencyMs?: number
|
|
17
|
+
cost?: number
|
|
18
|
+
qualityScore?: number
|
|
19
|
+
outcome?: string
|
|
20
|
+
metadata?: Record<string, unknown>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface KnowledgeInput {
|
|
24
|
+
category: string
|
|
25
|
+
subject: string
|
|
26
|
+
key: string
|
|
27
|
+
value: string
|
|
28
|
+
confidence?: number
|
|
29
|
+
source?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RuleInput {
|
|
33
|
+
category: string
|
|
34
|
+
condition: string
|
|
35
|
+
action: string
|
|
36
|
+
confidence?: number
|
|
37
|
+
priority?: number
|
|
38
|
+
status?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class MemoryStore {
|
|
42
|
+
async storeEpisode(ep: EpisodeInput): Promise<string> {
|
|
43
|
+
const id = uuid()
|
|
44
|
+
try {
|
|
45
|
+
await db.insert(episodes).values({
|
|
46
|
+
id,
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
eventType: ep.eventType,
|
|
49
|
+
model: ep.model || null,
|
|
50
|
+
provider: ep.provider || null,
|
|
51
|
+
taskType: ep.taskType || null,
|
|
52
|
+
inputTokens: ep.inputTokens ?? null,
|
|
53
|
+
outputTokens: ep.outputTokens ?? null,
|
|
54
|
+
tokensSaved: ep.tokensSaved ?? null,
|
|
55
|
+
latencyMs: ep.latencyMs ?? null,
|
|
56
|
+
cost: ep.cost ?? null,
|
|
57
|
+
qualityScore: ep.qualityScore ?? null,
|
|
58
|
+
outcome: ep.outcome || null,
|
|
59
|
+
metadata: ep.metadata ? JSON.stringify(ep.metadata) : '{}',
|
|
60
|
+
})
|
|
61
|
+
} catch (err) {
|
|
62
|
+
logger.warn({ error: (err as Error).message }, 'Failed to store episode')
|
|
63
|
+
}
|
|
64
|
+
return id
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getRecentEpisodes(limit = 50) {
|
|
68
|
+
try {
|
|
69
|
+
return await db.select().from(episodes).orderBy(desc(episodes.createdAt)).limit(limit)
|
|
70
|
+
} catch {
|
|
71
|
+
return []
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async storeKnowledge(k: KnowledgeInput): Promise<string> {
|
|
76
|
+
const id = uuid()
|
|
77
|
+
try {
|
|
78
|
+
const existing = await db.select().from(knowledge)
|
|
79
|
+
.where(sql`${knowledge.category} = ${k.category} AND ${knowledge.subject} = ${k.subject} AND ${knowledge.key} = ${k.key}`)
|
|
80
|
+
.limit(1)
|
|
81
|
+
|
|
82
|
+
if (existing.length > 0) {
|
|
83
|
+
await db.update(knowledge).set({
|
|
84
|
+
value: k.value,
|
|
85
|
+
confidence: k.confidence ?? 0,
|
|
86
|
+
source: k.source || 'distilled',
|
|
87
|
+
lastUpdated: new Date().toISOString(),
|
|
88
|
+
sampleCount: sql`${knowledge.sampleCount} + 1`,
|
|
89
|
+
}).where(eq(knowledge.id, existing[0].id))
|
|
90
|
+
return existing[0].id
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await db.insert(knowledge).values({
|
|
94
|
+
id,
|
|
95
|
+
category: k.category,
|
|
96
|
+
subject: k.subject,
|
|
97
|
+
key: k.key,
|
|
98
|
+
value: k.value,
|
|
99
|
+
confidence: k.confidence ?? 0,
|
|
100
|
+
source: k.source || 'distilled',
|
|
101
|
+
})
|
|
102
|
+
} catch (err) {
|
|
103
|
+
logger.warn({ error: (err as Error).message }, 'Failed to store knowledge')
|
|
104
|
+
}
|
|
105
|
+
return id
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async getKnowledge(category: string, subject: string, key: string) {
|
|
109
|
+
try {
|
|
110
|
+
const rows = await db.select().from(knowledge)
|
|
111
|
+
.where(sql`${knowledge.category} = ${category} AND ${knowledge.subject} = ${subject} AND ${knowledge.key} = ${key}`)
|
|
112
|
+
.limit(1)
|
|
113
|
+
return rows[0] || null
|
|
114
|
+
} catch {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async searchKnowledge(query: string, limit = 10) {
|
|
120
|
+
try {
|
|
121
|
+
return await db.select().from(knowledge)
|
|
122
|
+
.where(sql`${knowledge.value} LIKE ${`%${query}%`} OR ${knowledge.key} LIKE ${`%${query}%`}`)
|
|
123
|
+
.orderBy(desc(knowledge.confidence))
|
|
124
|
+
.limit(limit)
|
|
125
|
+
} catch {
|
|
126
|
+
return []
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async storeRule(rule: RuleInput): Promise<string> {
|
|
131
|
+
const id = uuid()
|
|
132
|
+
try {
|
|
133
|
+
await db.insert(proceduralRules).values({
|
|
134
|
+
id,
|
|
135
|
+
category: rule.category,
|
|
136
|
+
condition: rule.condition,
|
|
137
|
+
action: rule.action,
|
|
138
|
+
confidence: rule.confidence ?? 0,
|
|
139
|
+
priority: rule.priority ?? 0,
|
|
140
|
+
status: rule.status || 'auto_generated',
|
|
141
|
+
})
|
|
142
|
+
} catch (err) {
|
|
143
|
+
logger.warn({ error: (err as Error).message }, 'Failed to store rule')
|
|
144
|
+
}
|
|
145
|
+
return id
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async getApplicableRules(context: string) {
|
|
149
|
+
try {
|
|
150
|
+
const rows = await db.select().from(proceduralRules)
|
|
151
|
+
.where(sql`${proceduralRules.condition} LIKE ${`%${context}%`}`)
|
|
152
|
+
.orderBy(desc(proceduralRules.priority))
|
|
153
|
+
.limit(5)
|
|
154
|
+
|
|
155
|
+
for (const row of rows) {
|
|
156
|
+
await db.update(proceduralRules).set({
|
|
157
|
+
applyCount: sql`${proceduralRules.applyCount} + 1`,
|
|
158
|
+
lastAppliedAt: new Date().toISOString(),
|
|
159
|
+
}).where(eq(proceduralRules.id, row.id))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return rows
|
|
163
|
+
} catch {
|
|
164
|
+
return []
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async search(query: string, types?: MemoryType[], limit = 20): Promise<MemorySearchResult[]> {
|
|
169
|
+
const results: MemorySearchResult[] = []
|
|
170
|
+
const searchTypes = types || ['episodic', 'semantic', 'procedural']
|
|
171
|
+
|
|
172
|
+
if (searchTypes.includes('semantic')) {
|
|
173
|
+
const items = await this.searchKnowledge(query, limit)
|
|
174
|
+
results.push(...items.map((k) => ({
|
|
175
|
+
id: k.id,
|
|
176
|
+
type: 'semantic' as MemoryType,
|
|
177
|
+
content: `${k.key}: ${k.value}`,
|
|
178
|
+
relevance: k.confidence ?? 0,
|
|
179
|
+
metadata: { category: k.category, source: k.source },
|
|
180
|
+
})))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (searchTypes.includes('procedural')) {
|
|
184
|
+
const rules = await this.getApplicableRules(query)
|
|
185
|
+
results.push(...rules.map((r) => ({
|
|
186
|
+
id: r.id,
|
|
187
|
+
type: 'procedural' as MemoryType,
|
|
188
|
+
content: `${r.category}: ${r.condition} → ${r.action}`,
|
|
189
|
+
relevance: r.confidence ?? 0,
|
|
190
|
+
metadata: { action: r.action, priority: r.priority },
|
|
191
|
+
})))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return results.sort((a, b) => b.relevance - a.relevance).slice(0, limit)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async getStats() {
|
|
198
|
+
try {
|
|
199
|
+
const [epCount] = await db.select({ count: sql<number>`count(*)` }).from(episodes)
|
|
200
|
+
const [knCount] = await db.select({ count: sql<number>`count(*)` }).from(knowledge)
|
|
201
|
+
const [ruCount] = await db.select({ count: sql<number>`count(*)` }).from(proceduralRules)
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
episodes: epCount?.count ?? 0,
|
|
205
|
+
knowledge: knCount?.count ?? 0,
|
|
206
|
+
rules: ruCount?.count ?? 0,
|
|
207
|
+
totalSize: 'N/A',
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
return { episodes: 0, knowledge: 0, rules: 0, totalSize: 'N/A' }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const memoryStore = new MemoryStore()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type MemoryType = 'episodic' | 'semantic' | 'procedural'
|
|
2
|
+
|
|
3
|
+
export interface EpisodicMemory {
|
|
4
|
+
id: string
|
|
5
|
+
userId?: string
|
|
6
|
+
sessionId?: string
|
|
7
|
+
input: string
|
|
8
|
+
output: string
|
|
9
|
+
model: string
|
|
10
|
+
provider: string
|
|
11
|
+
taskType?: string
|
|
12
|
+
skillId?: string
|
|
13
|
+
tokensUsed: number
|
|
14
|
+
cost: number
|
|
15
|
+
latencyMs: number
|
|
16
|
+
rating?: number
|
|
17
|
+
tags?: string[]
|
|
18
|
+
createdAt: Date
|
|
19
|
+
expiresAt?: Date
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SemanticMemory {
|
|
23
|
+
id: string
|
|
24
|
+
key: string
|
|
25
|
+
value: string
|
|
26
|
+
source: string
|
|
27
|
+
category: string
|
|
28
|
+
confidence: number
|
|
29
|
+
accessCount: number
|
|
30
|
+
lastAccessedAt: Date
|
|
31
|
+
createdAt: Date
|
|
32
|
+
updatedAt: Date
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ProceduralMemory {
|
|
36
|
+
id: string
|
|
37
|
+
name: string
|
|
38
|
+
description: string
|
|
39
|
+
rule: string
|
|
40
|
+
condition: string
|
|
41
|
+
action: string
|
|
42
|
+
priority: number
|
|
43
|
+
confidence: number
|
|
44
|
+
hitCount: number
|
|
45
|
+
enabled: boolean
|
|
46
|
+
createdAt: Date
|
|
47
|
+
updatedAt: Date
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface MemorySearchResult {
|
|
51
|
+
id: string
|
|
52
|
+
type: MemoryType
|
|
53
|
+
content: string
|
|
54
|
+
relevance: number
|
|
55
|
+
metadata?: Record<string, unknown>
|
|
56
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export type TaskType = 'code' | 'chat' | 'reason' | 'review' | 'debug' | 'doc' | 'translate' | 'fast' | 'creative' | 'math'
|
|
2
|
+
|
|
3
|
+
export interface NamespaceAlias {
|
|
4
|
+
alias: string
|
|
5
|
+
resolvesTo: string
|
|
6
|
+
description: string
|
|
7
|
+
taskTypes: TaskType[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const BUILTIN_ALIASES: NamespaceAlias[] = [
|
|
11
|
+
{ alias: 'best', resolvesTo: 'an/claude-sonnet-4', description: 'Best overall model', taskTypes: ['code', 'chat', 'reason'] },
|
|
12
|
+
{ alias: 'fast', resolvesTo: 'oa/gpt-4o-mini', description: 'Fastest response', taskTypes: ['fast', 'chat'] },
|
|
13
|
+
{ alias: 'smart', resolvesTo: 'an/claude-opus-4-7', description: 'Most capable', taskTypes: ['reason', 'code', 'review'] },
|
|
14
|
+
{ alias: 'cheap', resolvesTo: 'ds/deepseek-chat', description: 'Cheapest option', taskTypes: ['chat', 'doc', 'translate'] },
|
|
15
|
+
{ alias: 'code', resolvesTo: 'an/claude-sonnet-4', description: 'Best for code generation', taskTypes: ['code', 'debug', 'review'] },
|
|
16
|
+
{ alias: 'reason', resolvesTo: 'oa/o3', description: 'Best for reasoning', taskTypes: ['reason', 'math'] },
|
|
17
|
+
{ alias: 'creative', resolvesTo: 'an/claude-sonnet-4', description: 'Best for creative writing', taskTypes: ['creative'] },
|
|
18
|
+
{ alias: 'long', resolvesTo: 'gm/gemini-2.5-pro', description: 'Best for long context (1M tokens)', taskTypes: ['chat', 'doc', 'review'] },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
export const TASK_TYPE_MODELS: Record<TaskType, string[]> = {
|
|
22
|
+
code: ['an/claude-sonnet-4', 'oa/gpt-4o', 'ds/deepseek-chat'],
|
|
23
|
+
chat: ['oa/gpt-4o-mini', 'an/claude-haiku-3.5', 'gm/gemini-2.5-flash'],
|
|
24
|
+
reason: ['oa/o3', 'an/claude-opus-4-7', 'ds/deepseek-reasoner'],
|
|
25
|
+
review: ['an/claude-sonnet-4', 'oa/gpt-4o', 'an/claude-opus-4-7'],
|
|
26
|
+
debug: ['an/claude-sonnet-4', 'oa/gpt-4o', 'ds/deepseek-chat'],
|
|
27
|
+
doc: ['oa/gpt-4o-mini', 'an/claude-haiku-3.5', 'gm/gemini-2.5-flash'],
|
|
28
|
+
translate: ['oa/gpt-4o-mini', 'ds/deepseek-chat', 'gm/gemini-2.5-flash'],
|
|
29
|
+
fast: ['oa/gpt-4o-mini', 'an/claude-haiku-3.5', 'gm/gemini-2.5-flash'],
|
|
30
|
+
creative: ['an/claude-sonnet-4', 'oa/gpt-4o', 'gm/gemini-2.5-pro'],
|
|
31
|
+
math: ['oa/o3', 'ds/deepseek-reasoner', 'oa/o3-mini'],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class NamespaceResolver {
|
|
35
|
+
private aliases = new Map<string, NamespaceAlias>()
|
|
36
|
+
private customAliases = new Map<string, NamespaceAlias>()
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
39
|
+
for (const alias of BUILTIN_ALIASES) {
|
|
40
|
+
this.aliases.set(alias.alias, alias)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
resolve(modelId: string): string[] {
|
|
45
|
+
const lower = modelId.toLowerCase().trim()
|
|
46
|
+
|
|
47
|
+
const builtin = this.aliases.get(lower)
|
|
48
|
+
if (builtin) return [builtin.resolvesTo]
|
|
49
|
+
|
|
50
|
+
const custom = this.customAliases.get(lower)
|
|
51
|
+
if (custom) return [custom.resolvesTo]
|
|
52
|
+
|
|
53
|
+
if (lower.includes('/')) return [modelId]
|
|
54
|
+
|
|
55
|
+
return [modelId]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
resolveForTask(taskType: TaskType): string[] {
|
|
59
|
+
return TASK_TYPE_MODELS[taskType] || TASK_TYPE_MODELS.chat
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
detectTaskType(content: string): TaskType {
|
|
63
|
+
const lower = content.toLowerCase()
|
|
64
|
+
|
|
65
|
+
if (/(?:function|class|import |export |const |async |return |=>|```)/.test(lower)) return 'code'
|
|
66
|
+
if (/(?:explain why|reason about|prove|logical|deduce)/.test(lower)) return 'reason'
|
|
67
|
+
if (/(?:review|feedback|critique|improve)/.test(lower)) return 'review'
|
|
68
|
+
if (/(?:bug|error|fix|debug|stack trace|exception)/.test(lower)) return 'debug'
|
|
69
|
+
if (/(?:document|docs|readme|explain how|tutorial)/.test(lower)) return 'doc'
|
|
70
|
+
if (/(?:translate|traducir| Übersetzen)/.test(lower)) return 'translate'
|
|
71
|
+
if (/(?:write|story|poem|creative|imagine)/.test(lower)) return 'creative'
|
|
72
|
+
if (/(?:calcul|solve|equation|math|integral)/.test(lower)) return 'math'
|
|
73
|
+
return 'chat'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
addAlias(alias: NamespaceAlias) {
|
|
77
|
+
this.customAliases.set(alias.alias, alias)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
removeAlias(alias: string) {
|
|
81
|
+
this.customAliases.delete(alias)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
listAliases(): NamespaceAlias[] {
|
|
85
|
+
return [...this.aliases.values(), ...this.customAliases.values()]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const namespaceResolver = new NamespaceResolver()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface RequestFeatures {
|
|
2
|
+
hourOfDay: number
|
|
3
|
+
dayOfWeek: number
|
|
4
|
+
promptTokenCount: number
|
|
5
|
+
hasCode: boolean
|
|
6
|
+
hasTools: boolean
|
|
7
|
+
isStreaming: boolean
|
|
8
|
+
temperature: number
|
|
9
|
+
taskType: string
|
|
10
|
+
modelFamily: string
|
|
11
|
+
contextFillRatio: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RoutingPrediction {
|
|
15
|
+
provider: string
|
|
16
|
+
model: string
|
|
17
|
+
confidence: number
|
|
18
|
+
expectedLatencyMs: number
|
|
19
|
+
expectedCost: number
|
|
20
|
+
strategy: 'neural' | 'hybrid' | 'rule_based'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RoutingStrategy {
|
|
24
|
+
name: string
|
|
25
|
+
select(features: RequestFeatures, candidates: RoutingCandidate[]): RoutingPrediction
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RoutingCandidate {
|
|
29
|
+
providerId: string
|
|
30
|
+
model: string
|
|
31
|
+
health: number
|
|
32
|
+
latency: number
|
|
33
|
+
cost: number
|
|
34
|
+
successRate: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function extractFeatures(request: {
|
|
38
|
+
messages?: Array<{ content: string; role: string }>
|
|
39
|
+
model?: string
|
|
40
|
+
stream?: boolean
|
|
41
|
+
temperature?: number
|
|
42
|
+
maxTokens?: number
|
|
43
|
+
tools?: unknown[]
|
|
44
|
+
taskType?: string
|
|
45
|
+
}): RequestFeatures {
|
|
46
|
+
const now = new Date()
|
|
47
|
+
const content = (request.messages || []).map((m) => m.content).join(' ')
|
|
48
|
+
const estimatedTokens = Math.ceil(content.length / 4)
|
|
49
|
+
|
|
50
|
+
const hasCode = /(?:function|class|const |let |var |import |export |def |async |return |=>|```)/.test(content)
|
|
51
|
+
const hasTools = (request.tools?.length ?? 0) > 0
|
|
52
|
+
|
|
53
|
+
const modelFamily = (() => {
|
|
54
|
+
const m = (request.model || '').toLowerCase()
|
|
55
|
+
if (m.includes('gpt') || m.includes('o1') || m.includes('o3')) return 'openai'
|
|
56
|
+
if (m.includes('claude')) return 'anthropic'
|
|
57
|
+
if (m.includes('gemini')) return 'gemini'
|
|
58
|
+
if (m.includes('deepseek')) return 'deepseek'
|
|
59
|
+
return 'unknown'
|
|
60
|
+
})()
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
hourOfDay: now.getHours(),
|
|
64
|
+
dayOfWeek: now.getDay(),
|
|
65
|
+
promptTokenCount: estimatedTokens,
|
|
66
|
+
hasCode,
|
|
67
|
+
hasTools,
|
|
68
|
+
isStreaming: request.stream ?? false,
|
|
69
|
+
temperature: request.temperature ?? 0.7,
|
|
70
|
+
taskType: request.taskType || (hasCode ? 'code' : 'chat'),
|
|
71
|
+
modelFamily,
|
|
72
|
+
contextFillRatio: Math.min(estimatedTokens / 128000, 1),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { RequestFeatures, RoutingPrediction, RoutingCandidate } from './features'
|
|
2
|
+
import { PriorityStrategy, LatencyStrategy, CostStrategy, RoundRobinStrategy, HybridStrategy } from './strategies'
|
|
3
|
+
import { db } from '../db'
|
|
4
|
+
import { routerTrainingData } from '../db/schema'
|
|
5
|
+
import { sql } from 'drizzle-orm'
|
|
6
|
+
import { logger } from '../utils/logger'
|
|
7
|
+
import { v4 as uuid } from 'uuid'
|
|
8
|
+
|
|
9
|
+
type StrategyName = 'priority' | 'latency' | 'cost' | 'round_robin' | 'hybrid'
|
|
10
|
+
|
|
11
|
+
class NeuralRouter {
|
|
12
|
+
private strategies = new Map<string, PriorityStrategy | LatencyStrategy | CostStrategy | RoundRobinStrategy | HybridStrategy>()
|
|
13
|
+
private defaultStrategy: StrategyName = 'hybrid'
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
this.strategies.set('priority', new PriorityStrategy())
|
|
17
|
+
this.strategies.set('latency', new LatencyStrategy())
|
|
18
|
+
this.strategies.set('cost', new CostStrategy())
|
|
19
|
+
this.strategies.set('round_robin', new RoundRobinStrategy())
|
|
20
|
+
this.strategies.set('hybrid', new HybridStrategy())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
route(features: RequestFeatures, candidates: RoutingCandidate[], strategy?: StrategyName): RoutingPrediction {
|
|
24
|
+
const strat = this.strategies.get(strategy || this.defaultStrategy)
|
|
25
|
+
if (!strat) {
|
|
26
|
+
return this.strategies.get('priority')!.select(features, candidates)
|
|
27
|
+
}
|
|
28
|
+
return strat.select(features, candidates)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setDefaultStrategy(name: StrategyName) {
|
|
32
|
+
if (this.strategies.has(name)) {
|
|
33
|
+
this.defaultStrategy = name
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getDefaultStrategy(): StrategyName {
|
|
38
|
+
return this.defaultStrategy
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async recordOutcome(data: {
|
|
42
|
+
features: RequestFeatures
|
|
43
|
+
selectedProvider: string
|
|
44
|
+
selectedModel: string
|
|
45
|
+
outcome: 'success' | 'timeout' | 'error' | 'rate_limited'
|
|
46
|
+
latencyMs?: number
|
|
47
|
+
cost?: number
|
|
48
|
+
qualityScore?: number
|
|
49
|
+
}) {
|
|
50
|
+
try {
|
|
51
|
+
await db.insert(routerTrainingData).values({
|
|
52
|
+
id: uuid(),
|
|
53
|
+
features: JSON.stringify(data.features),
|
|
54
|
+
selectedProvider: data.selectedProvider,
|
|
55
|
+
selectedModel: data.selectedModel,
|
|
56
|
+
outcome: data.outcome,
|
|
57
|
+
latencyMs: data.latencyMs ?? null,
|
|
58
|
+
cost: data.cost ?? null,
|
|
59
|
+
qualityScore: data.qualityScore ?? null,
|
|
60
|
+
})
|
|
61
|
+
} catch (err) {
|
|
62
|
+
logger.warn({ error: (err as Error).message }, 'Failed to record routing outcome')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async getAccuracyStats() {
|
|
67
|
+
try {
|
|
68
|
+
const [total] = await db.select({ count: sql<number>`count(*)` }).from(routerTrainingData)
|
|
69
|
+
const [success] = await db.select({ count: sql<number>`count(*)` }).from(routerTrainingData)
|
|
70
|
+
.where(sql`${routerTrainingData.outcome} = 'success'`)
|
|
71
|
+
|
|
72
|
+
const totalRequests = total?.count ?? 0
|
|
73
|
+
const successCount = success?.count ?? 0
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
totalSamples: totalRequests,
|
|
77
|
+
accuracy: totalRequests > 0 ? successCount / totalRequests : 0,
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
return { totalSamples: 0, accuracy: 0 }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const neuralRouter = new NeuralRouter()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { RequestFeatures, RoutingPrediction, RoutingStrategy, RoutingCandidate } from './features'
|
|
2
|
+
|
|
3
|
+
export class PriorityStrategy implements RoutingStrategy {
|
|
4
|
+
name = 'priority'
|
|
5
|
+
select(_features: RequestFeatures, candidates: RoutingCandidate[]): RoutingPrediction {
|
|
6
|
+
const sorted = [...candidates].sort((a, b) => {
|
|
7
|
+
if (a.health !== b.health) return b.health - a.health
|
|
8
|
+
return a.cost - b.cost
|
|
9
|
+
})
|
|
10
|
+
const best = sorted[0]
|
|
11
|
+
return {
|
|
12
|
+
provider: best.providerId,
|
|
13
|
+
model: best.model,
|
|
14
|
+
confidence: 0.5,
|
|
15
|
+
expectedLatencyMs: best.latency,
|
|
16
|
+
expectedCost: best.cost,
|
|
17
|
+
strategy: 'rule_based',
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class LatencyStrategy implements RoutingStrategy {
|
|
23
|
+
name = 'latency'
|
|
24
|
+
select(_features: RequestFeatures, candidates: RoutingCandidate[]): RoutingPrediction {
|
|
25
|
+
const healthy = candidates.filter((c) => c.health > 0.5)
|
|
26
|
+
const sorted = [...(healthy.length > 0 ? healthy : candidates)].sort((a, b) => a.latency - b.latency)
|
|
27
|
+
const best = sorted[0]
|
|
28
|
+
return {
|
|
29
|
+
provider: best.providerId,
|
|
30
|
+
model: best.model,
|
|
31
|
+
confidence: 0.6,
|
|
32
|
+
expectedLatencyMs: best.latency,
|
|
33
|
+
expectedCost: best.cost,
|
|
34
|
+
strategy: 'rule_based',
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class CostStrategy implements RoutingStrategy {
|
|
40
|
+
name = 'cost'
|
|
41
|
+
select(_features: RequestFeatures, candidates: RoutingCandidate[]): RoutingPrediction {
|
|
42
|
+
const healthy = candidates.filter((c) => c.health > 0.5)
|
|
43
|
+
const sorted = [...(healthy.length > 0 ? healthy : candidates)].sort((a, b) => a.cost - b.cost)
|
|
44
|
+
const best = sorted[0]
|
|
45
|
+
return {
|
|
46
|
+
provider: best.providerId,
|
|
47
|
+
model: best.model,
|
|
48
|
+
confidence: 0.6,
|
|
49
|
+
expectedLatencyMs: best.latency,
|
|
50
|
+
expectedCost: best.cost,
|
|
51
|
+
strategy: 'rule_based',
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class RoundRobinStrategy implements RoutingStrategy {
|
|
57
|
+
name = 'round_robin'
|
|
58
|
+
private counter = 0
|
|
59
|
+
select(_features: RequestFeatures, candidates: RoutingCandidate[]): RoutingPrediction {
|
|
60
|
+
const idx = this.counter % candidates.length
|
|
61
|
+
this.counter++
|
|
62
|
+
const pick = candidates[idx]
|
|
63
|
+
return {
|
|
64
|
+
provider: pick.providerId,
|
|
65
|
+
model: pick.model,
|
|
66
|
+
confidence: 0.4,
|
|
67
|
+
expectedLatencyMs: pick.latency,
|
|
68
|
+
expectedCost: pick.cost,
|
|
69
|
+
strategy: 'rule_based',
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class HybridStrategy implements RoutingStrategy {
|
|
75
|
+
name = 'hybrid'
|
|
76
|
+
|
|
77
|
+
constructor(
|
|
78
|
+
private neuralWeight = 0.6,
|
|
79
|
+
private ruleWeight = 0.4,
|
|
80
|
+
) {}
|
|
81
|
+
|
|
82
|
+
select(features: RequestFeatures, candidates: RoutingCandidate[]): RoutingPrediction {
|
|
83
|
+
const scored = candidates.map((c) => {
|
|
84
|
+
const healthScore = c.health
|
|
85
|
+
const latencyScore = 1 - Math.min(c.latency / 5000, 1)
|
|
86
|
+
const costScore = 1 - Math.min(c.cost / 0.1, 1)
|
|
87
|
+
const successScore = c.successRate
|
|
88
|
+
|
|
89
|
+
const neuralBonus = this.getNeuralBonus(features, c)
|
|
90
|
+
const ruleScore = (healthScore * 0.3 + latencyScore * 0.3 + costScore * 0.2 + successScore * 0.2)
|
|
91
|
+
const finalScore = ruleScore * this.ruleWeight + neuralBonus * this.neuralWeight
|
|
92
|
+
|
|
93
|
+
return { candidate: c, score: finalScore }
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
scored.sort((a, b) => b.score - a.score)
|
|
97
|
+
const best = scored[0]
|
|
98
|
+
|
|
99
|
+
const confidence = Math.min(best.score, 0.95)
|
|
100
|
+
const strategy: 'neural' | 'hybrid' | 'rule_based' = confidence > 0.8 ? 'neural' : confidence > 0.5 ? 'hybrid' : 'rule_based'
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
provider: best.candidate.providerId,
|
|
104
|
+
model: best.candidate.model,
|
|
105
|
+
confidence,
|
|
106
|
+
expectedLatencyMs: best.candidate.latency,
|
|
107
|
+
expectedCost: best.candidate.cost,
|
|
108
|
+
strategy,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private getNeuralBonus(features: RequestFeatures, candidate: RoutingCandidate): number {
|
|
113
|
+
let bonus = 0.5
|
|
114
|
+
|
|
115
|
+
if (features.hasCode && candidate.providerId.includes('anthropic')) bonus += 0.15
|
|
116
|
+
if (features.hasCode && candidate.providerId.includes('openai')) bonus += 0.1
|
|
117
|
+
if (features.taskType === 'reason' && candidate.providerId.includes('openai')) bonus += 0.15
|
|
118
|
+
if (features.contextFillRatio > 0.8 && candidate.providerId.includes('gemini')) bonus += 0.2
|
|
119
|
+
if (features.temperature < 0.3 && candidate.providerId.includes('openai')) bonus += 0.1
|
|
120
|
+
if (features.hourOfDay >= 9 && features.hourOfDay <= 17 && candidate.cost < 0.005) bonus += 0.05
|
|
121
|
+
|
|
122
|
+
return Math.min(bonus, 1)
|
|
123
|
+
}
|
|
124
|
+
}
|