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.
Files changed (135) hide show
  1. package/README.md +385 -0
  2. package/bin/synapse.js +242 -0
  3. package/docs/PLAN.md +1723 -0
  4. package/docs/PRD.md +1799 -0
  5. package/drizzle.config.ts +12 -0
  6. package/next.config.ts +8 -0
  7. package/package.json +82 -0
  8. package/postcss.config.mjs +7 -0
  9. package/public/file.svg +1 -0
  10. package/public/globe.svg +1 -0
  11. package/public/next.svg +1 -0
  12. package/public/vercel.svg +1 -0
  13. package/public/window.svg +1 -0
  14. package/src/app/api/analytics/cost/route.ts +13 -0
  15. package/src/app/api/analytics/usage/route.ts +16 -0
  16. package/src/app/api/auth/login/route.ts +42 -0
  17. package/src/app/api/cache/route.ts +19 -0
  18. package/src/app/api/dashboard/route.ts +35 -0
  19. package/src/app/api/distill/route.ts +10 -0
  20. package/src/app/api/events/route.ts +54 -0
  21. package/src/app/api/health/route.ts +10 -0
  22. package/src/app/api/intelligence/forensics/route.ts +23 -0
  23. package/src/app/api/intelligence/neural-router/route.ts +23 -0
  24. package/src/app/api/keys/route.ts +34 -0
  25. package/src/app/api/mcp/route.ts +49 -0
  26. package/src/app/api/memory/route.ts +10 -0
  27. package/src/app/api/models/benchmark/route.ts +13 -0
  28. package/src/app/api/models/route.ts +39 -0
  29. package/src/app/api/namespace/route.ts +25 -0
  30. package/src/app/api/plugins/route.ts +41 -0
  31. package/src/app/api/providers/accounts/route.ts +91 -0
  32. package/src/app/api/providers/fetch-models/route.ts +52 -0
  33. package/src/app/api/providers/health/route.ts +10 -0
  34. package/src/app/api/providers/route.ts +46 -0
  35. package/src/app/api/routes/pipeline/route.ts +20 -0
  36. package/src/app/api/settings/route.ts +33 -0
  37. package/src/app/api/skills/route.ts +39 -0
  38. package/src/app/api/v1/chat/completions/route.ts +156 -0
  39. package/src/app/api/v1/models/route.ts +44 -0
  40. package/src/app/dashboard/intelligence/loading.tsx +14 -0
  41. package/src/app/dashboard/intelligence/page.tsx +125 -0
  42. package/src/app/dashboard/layout.tsx +143 -0
  43. package/src/app/dashboard/loading.tsx +17 -0
  44. package/src/app/dashboard/memory/loading.tsx +15 -0
  45. package/src/app/dashboard/memory/page.tsx +71 -0
  46. package/src/app/dashboard/models/loading.tsx +13 -0
  47. package/src/app/dashboard/models/page.tsx +107 -0
  48. package/src/app/dashboard/page.tsx +183 -0
  49. package/src/app/dashboard/playground/loading.tsx +17 -0
  50. package/src/app/dashboard/playground/page.tsx +212 -0
  51. package/src/app/dashboard/providers/loading.tsx +15 -0
  52. package/src/app/dashboard/providers/page.tsx +248 -0
  53. package/src/app/dashboard/routes/loading.tsx +15 -0
  54. package/src/app/dashboard/routes/page.tsx +72 -0
  55. package/src/app/dashboard/settings/loading.tsx +20 -0
  56. package/src/app/dashboard/settings/page.tsx +208 -0
  57. package/src/app/dashboard/skills/loading.tsx +26 -0
  58. package/src/app/dashboard/skills/page.tsx +137 -0
  59. package/src/app/dashboard/vault/loading.tsx +18 -0
  60. package/src/app/dashboard/vault/page.tsx +139 -0
  61. package/src/app/favicon.ico +0 -0
  62. package/src/app/globals.css +59 -0
  63. package/src/app/layout.tsx +32 -0
  64. package/src/app/login/page.tsx +87 -0
  65. package/src/app/page.tsx +5 -0
  66. package/src/components/ui/badge.tsx +32 -0
  67. package/src/components/ui/button.tsx +38 -0
  68. package/src/components/ui/card.tsx +50 -0
  69. package/src/components/ui/error-boundary.tsx +47 -0
  70. package/src/components/ui/index.ts +11 -0
  71. package/src/components/ui/input.tsx +26 -0
  72. package/src/components/ui/select.tsx +24 -0
  73. package/src/components/ui/skeleton.tsx +53 -0
  74. package/src/components/ui/toast.tsx +51 -0
  75. package/src/instrumentation.ts +6 -0
  76. package/src/lib/__tests__/auth.test.ts +42 -0
  77. package/src/lib/__tests__/format.test.ts +94 -0
  78. package/src/lib/__tests__/namespace.test.ts +102 -0
  79. package/src/lib/__tests__/squeezer.test.ts +93 -0
  80. package/src/lib/__tests__/utils.test.ts +28 -0
  81. package/src/lib/analytics/index.ts +187 -0
  82. package/src/lib/auth/guard.tsx +71 -0
  83. package/src/lib/auth/index.ts +105 -0
  84. package/src/lib/auth/middleware.ts +64 -0
  85. package/src/lib/benchmark/index.ts +137 -0
  86. package/src/lib/bootstrap.ts +122 -0
  87. package/src/lib/cache/index.ts +1 -0
  88. package/src/lib/cache/semantic.ts +211 -0
  89. package/src/lib/config/defaults.ts +61 -0
  90. package/src/lib/config/index.ts +72 -0
  91. package/src/lib/config/schema.ts +63 -0
  92. package/src/lib/db/index.ts +22 -0
  93. package/src/lib/db/migrate.ts +327 -0
  94. package/src/lib/db/schema.ts +303 -0
  95. package/src/lib/distiller/index.ts +331 -0
  96. package/src/lib/fallback/index.ts +153 -0
  97. package/src/lib/forensics/index.ts +188 -0
  98. package/src/lib/format/anthropic.ts +139 -0
  99. package/src/lib/format/gemini.ts +130 -0
  100. package/src/lib/format/index.ts +3 -0
  101. package/src/lib/format/openai.ts +78 -0
  102. package/src/lib/health/index.ts +158 -0
  103. package/src/lib/mcp/builtin.ts +83 -0
  104. package/src/lib/mcp/index.ts +1 -0
  105. package/src/lib/mcp/registry.ts +49 -0
  106. package/src/lib/memory/index.ts +3 -0
  107. package/src/lib/memory/store.ts +215 -0
  108. package/src/lib/memory/types.ts +56 -0
  109. package/src/lib/namespace/index.ts +89 -0
  110. package/src/lib/neural/features.ts +74 -0
  111. package/src/lib/neural/index.ts +85 -0
  112. package/src/lib/neural/strategies.ts +124 -0
  113. package/src/lib/pipeline/index.ts +84 -0
  114. package/src/lib/pipeline/types.ts +77 -0
  115. package/src/lib/plugins/builtin.ts +79 -0
  116. package/src/lib/plugins/index.ts +65 -0
  117. package/src/lib/prediction/index.ts +113 -0
  118. package/src/lib/providers/api-key/anthropic.ts +96 -0
  119. package/src/lib/providers/api-key/deepseek.ts +108 -0
  120. package/src/lib/providers/api-key/gemini.ts +112 -0
  121. package/src/lib/providers/api-key/openai.ts +122 -0
  122. package/src/lib/providers/api-key/openrouter.ts +112 -0
  123. package/src/lib/providers/base-adapter.ts +122 -0
  124. package/src/lib/providers/registry.ts +46 -0
  125. package/src/lib/providers/types.ts +121 -0
  126. package/src/lib/router/index.ts +82 -0
  127. package/src/lib/skills/forge.ts +57 -0
  128. package/src/lib/skills/index.ts +3 -0
  129. package/src/lib/skills/registry.ts +195 -0
  130. package/src/lib/skills/types.ts +44 -0
  131. package/src/lib/squeezer/index.ts +158 -0
  132. package/src/lib/utils/cn.ts +6 -0
  133. package/src/lib/utils/logger.ts +16 -0
  134. package/src/middleware.ts +60 -0
  135. 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
+ }