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,82 @@
|
|
|
1
|
+
import { registry } from '../providers/registry'
|
|
2
|
+
import type { NormalizedRequest, NormalizedResponse, ProviderAccount } from '../providers/types'
|
|
3
|
+
import { logger } from '../utils/logger'
|
|
4
|
+
|
|
5
|
+
export interface RouteResult {
|
|
6
|
+
response: NormalizedResponse
|
|
7
|
+
provider: string
|
|
8
|
+
model: string
|
|
9
|
+
fallbackUsed: boolean
|
|
10
|
+
fallbackChain?: string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function routeRequest(
|
|
14
|
+
req: NormalizedRequest,
|
|
15
|
+
getAccount: (providerId: string) => ProviderAccount | null
|
|
16
|
+
): Promise<RouteResult> {
|
|
17
|
+
const resolved = registry.resolveModel(req.model)
|
|
18
|
+
if (!resolved) {
|
|
19
|
+
if (req.model.includes('/')) {
|
|
20
|
+
throw new Error(`No provider found for model: ${req.model}`)
|
|
21
|
+
}
|
|
22
|
+
const adapters = registry.list()
|
|
23
|
+
if (adapters.length === 0) {
|
|
24
|
+
throw new Error('No providers registered')
|
|
25
|
+
}
|
|
26
|
+
const fallbackChain: string[] = []
|
|
27
|
+
for (const adapter of adapters) {
|
|
28
|
+
const account = getAccount(adapter.info.id)
|
|
29
|
+
if (!account || !account.enabled) continue
|
|
30
|
+
try {
|
|
31
|
+
const modifiedReq = { ...req, model: req.model }
|
|
32
|
+
const response = await adapter.chatCompletion(modifiedReq, account)
|
|
33
|
+
return { response, provider: adapter.info.name, model: req.model, fallbackUsed: true, fallbackChain }
|
|
34
|
+
} catch (err) {
|
|
35
|
+
fallbackChain.push(`${adapter.info.name}: ${(err as Error).message}`)
|
|
36
|
+
logger.warn({ provider: adapter.info.name, error: (err as Error).message }, 'Provider failed, trying next')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`All providers failed: ${fallbackChain.join(' | ')}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { adapter, modelName } = resolved
|
|
43
|
+
const account = getAccount(adapter.info.id)
|
|
44
|
+
if (!account || !account.enabled) {
|
|
45
|
+
throw new Error(`No enabled account for provider: ${adapter.info.name}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const modifiedReq = { ...req, model: modelName }
|
|
49
|
+
const response = await adapter.chatCompletion(modifiedReq, account)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
response,
|
|
53
|
+
provider: adapter.info.name,
|
|
54
|
+
model: req.model,
|
|
55
|
+
fallbackUsed: false,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function routeRequestStream(
|
|
60
|
+
req: NormalizedRequest,
|
|
61
|
+
getAccount: (providerId: string) => ProviderAccount | null
|
|
62
|
+
): Promise<AsyncIterable<import('../providers/types').NormalizedChunk> & { provider: string; model: string }> {
|
|
63
|
+
const resolved = registry.resolveModel(req.model)
|
|
64
|
+
if (!resolved) {
|
|
65
|
+
throw new Error(`No provider found for model: ${req.model}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { adapter, modelName } = resolved
|
|
69
|
+
const account = getAccount(adapter.info.id)
|
|
70
|
+
if (!account || !account.enabled) {
|
|
71
|
+
throw new Error(`No enabled account for provider: ${adapter.info.name}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const modifiedReq = { ...req, model: modelName }
|
|
75
|
+
const stream = adapter.chatCompletionStream(modifiedReq, account)
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
...stream,
|
|
79
|
+
provider: adapter.info.name,
|
|
80
|
+
model: req.model,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { db } from '../db'
|
|
2
|
+
import { skills } from '../db/schema'
|
|
3
|
+
import type { SkillForgeRecipe } from './types'
|
|
4
|
+
import { skillRegistry } from './registry'
|
|
5
|
+
import { v4 as uuid } from 'uuid'
|
|
6
|
+
|
|
7
|
+
export class SkillForge {
|
|
8
|
+
async createFromRecipe(recipe: SkillForgeRecipe): Promise<string> {
|
|
9
|
+
const systemPrompt = this.buildSystemPrompt(recipe)
|
|
10
|
+
const skillId = uuid()
|
|
11
|
+
|
|
12
|
+
await skillRegistry.createSkill({
|
|
13
|
+
id: skillId,
|
|
14
|
+
name: recipe.name,
|
|
15
|
+
description: recipe.description,
|
|
16
|
+
systemPrompt,
|
|
17
|
+
preferredModel: recipe.preferredModel,
|
|
18
|
+
enabled: true,
|
|
19
|
+
groupId: null,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
return skillId
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async importFromOpenClaw(data: {
|
|
26
|
+
name: string
|
|
27
|
+
description: string
|
|
28
|
+
prompt: string
|
|
29
|
+
examples?: Array<{ input: string; output: string }>
|
|
30
|
+
}): Promise<string> {
|
|
31
|
+
return this.createFromRecipe({
|
|
32
|
+
name: data.name,
|
|
33
|
+
description: data.description,
|
|
34
|
+
basePrompt: data.prompt,
|
|
35
|
+
examples: data.examples || [],
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private buildSystemPrompt(recipe: SkillForgeRecipe): string {
|
|
40
|
+
let prompt = recipe.basePrompt
|
|
41
|
+
|
|
42
|
+
if (recipe.constraints && recipe.constraints.length > 0) {
|
|
43
|
+
prompt += '\n\nConstraints:\n' + recipe.constraints.map((c) => `- ${c}`).join('\n')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (recipe.examples.length > 0) {
|
|
47
|
+
prompt += '\n\nExamples:\n'
|
|
48
|
+
for (const ex of recipe.examples) {
|
|
49
|
+
prompt += `\nInput: ${ex.input}\nOutput: ${ex.output}\n`
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return prompt
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const skillForge = new SkillForge()
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { db } from '../db'
|
|
2
|
+
import { skills, skillGroups } from '../db/schema'
|
|
3
|
+
import { eq } from 'drizzle-orm'
|
|
4
|
+
import type { Skill, SkillGroup, RotationStrategy, SkillRotationResult } from './types'
|
|
5
|
+
import { logger } from '../utils/logger'
|
|
6
|
+
|
|
7
|
+
class SkillRegistry {
|
|
8
|
+
private skillCache = new Map<string, Skill>()
|
|
9
|
+
private groupCache = new Map<string, SkillGroup>()
|
|
10
|
+
private loaded = false
|
|
11
|
+
|
|
12
|
+
async load() {
|
|
13
|
+
if (this.loaded) return
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const allSkills = await db.select().from(skills)
|
|
17
|
+
const allGroups = await db.select().from(skillGroups)
|
|
18
|
+
|
|
19
|
+
this.skillCache.clear()
|
|
20
|
+
this.groupCache.clear()
|
|
21
|
+
|
|
22
|
+
for (const s of allSkills) {
|
|
23
|
+
const rotationConfig = s.rotationConfig ? JSON.parse(s.rotationConfig) : {}
|
|
24
|
+
this.skillCache.set(s.id, {
|
|
25
|
+
id: s.id,
|
|
26
|
+
name: s.name,
|
|
27
|
+
description: s.description || '',
|
|
28
|
+
systemPrompt: s.systemPrompt,
|
|
29
|
+
groupId: s.groupId,
|
|
30
|
+
temperature: rotationConfig.temperature ?? undefined,
|
|
31
|
+
maxTokens: rotationConfig.maxTokens ?? undefined,
|
|
32
|
+
preferredModel: rotationConfig.preferredModel ?? undefined,
|
|
33
|
+
enabled: s.enabled,
|
|
34
|
+
metadata: { usageCount: s.usageCount, qualityScore: s.qualityScore, tags: JSON.parse(s.tags) },
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const g of allGroups) {
|
|
39
|
+
const taskTypes: string[] = g.taskTypes ? JSON.parse(g.taskTypes) : []
|
|
40
|
+
this.groupCache.set(g.id, {
|
|
41
|
+
id: g.id,
|
|
42
|
+
name: g.name,
|
|
43
|
+
description: g.description || '',
|
|
44
|
+
skillIds: taskTypes,
|
|
45
|
+
rotationStrategy: (g.rotationStrategy as RotationStrategy) || 'round_robin',
|
|
46
|
+
enabled: g.enabled,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.loaded = true
|
|
51
|
+
logger.info({ skills: this.skillCache.size, groups: this.groupCache.size }, 'Skills loaded')
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logger.warn({ error: (err as Error).message }, 'Failed to load skills from DB, using empty registry')
|
|
54
|
+
this.loaded = true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getSkill(id: string): Promise<Skill | null> {
|
|
59
|
+
await this.load()
|
|
60
|
+
return this.skillCache.get(id) || null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async listSkills(): Promise<Skill[]> {
|
|
64
|
+
await this.load()
|
|
65
|
+
return Array.from(this.skillCache.values())
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async listEnabledSkills(): Promise<Skill[]> {
|
|
69
|
+
await this.load()
|
|
70
|
+
return Array.from(this.skillCache.values()).filter((s) => s.enabled)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async getGroup(id: string): Promise<SkillGroup | null> {
|
|
74
|
+
await this.load()
|
|
75
|
+
return this.groupCache.get(id) || null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async listGroups(): Promise<SkillGroup[]> {
|
|
79
|
+
await this.load()
|
|
80
|
+
return Array.from(this.groupCache.values())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async createSkill(skill: Skill): Promise<void> {
|
|
84
|
+
const rotationConfig: Record<string, unknown> = {}
|
|
85
|
+
if (skill.temperature !== undefined) rotationConfig.temperature = skill.temperature
|
|
86
|
+
if (skill.maxTokens !== undefined) rotationConfig.maxTokens = skill.maxTokens
|
|
87
|
+
if (skill.preferredModel !== undefined) rotationConfig.preferredModel = skill.preferredModel
|
|
88
|
+
|
|
89
|
+
await db.insert(skills).values({
|
|
90
|
+
id: skill.id,
|
|
91
|
+
name: skill.name,
|
|
92
|
+
description: skill.description,
|
|
93
|
+
systemPrompt: skill.systemPrompt,
|
|
94
|
+
groupId: skill.groupId,
|
|
95
|
+
rotationConfig: JSON.stringify(rotationConfig),
|
|
96
|
+
enabled: skill.enabled,
|
|
97
|
+
})
|
|
98
|
+
this.skillCache.set(skill.id, skill)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async createGroup(group: SkillGroup): Promise<void> {
|
|
102
|
+
await db.insert(skillGroups).values({
|
|
103
|
+
id: group.id,
|
|
104
|
+
name: group.name,
|
|
105
|
+
description: group.description,
|
|
106
|
+
taskTypes: JSON.stringify(group.skillIds),
|
|
107
|
+
rotationStrategy: group.rotationStrategy,
|
|
108
|
+
enabled: group.enabled,
|
|
109
|
+
})
|
|
110
|
+
this.groupCache.set(group.id, group)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async updateSkill(id: string, updates: Partial<Skill>): Promise<void> {
|
|
114
|
+
const existing = this.skillCache.get(id)
|
|
115
|
+
if (!existing) throw new Error(`Skill not found: ${id}`)
|
|
116
|
+
|
|
117
|
+
const updated = { ...existing, ...updates }
|
|
118
|
+
const rotationConfig: Record<string, unknown> = {}
|
|
119
|
+
if (updated.temperature !== undefined) rotationConfig.temperature = updated.temperature
|
|
120
|
+
if (updated.maxTokens !== undefined) rotationConfig.maxTokens = updated.maxTokens
|
|
121
|
+
if (updated.preferredModel !== undefined) rotationConfig.preferredModel = updated.preferredModel
|
|
122
|
+
|
|
123
|
+
await db.update(skills).set({
|
|
124
|
+
name: updated.name,
|
|
125
|
+
description: updated.description,
|
|
126
|
+
systemPrompt: updated.systemPrompt,
|
|
127
|
+
groupId: updated.groupId,
|
|
128
|
+
rotationConfig: JSON.stringify(rotationConfig),
|
|
129
|
+
enabled: updated.enabled,
|
|
130
|
+
}).where(eq(skills.id, id))
|
|
131
|
+
this.skillCache.set(id, updated)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async deleteSkill(id: string): Promise<void> {
|
|
135
|
+
await db.delete(skills).where(eq(skills.id, id))
|
|
136
|
+
this.skillCache.delete(id)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async rotate(groupId: string, taskType?: string): Promise<SkillRotationResult> {
|
|
140
|
+
await this.load()
|
|
141
|
+
const group = this.groupCache.get(groupId)
|
|
142
|
+
if (!group) throw new Error(`Group not found: ${groupId}`)
|
|
143
|
+
if (!group.enabled) throw new Error(`Group "${group.name}" is disabled`)
|
|
144
|
+
|
|
145
|
+
const enabledSkills = group.skillIds
|
|
146
|
+
.map((sid) => this.skillCache.get(sid))
|
|
147
|
+
.filter((s): s is Skill => !!s && s.enabled)
|
|
148
|
+
|
|
149
|
+
if (enabledSkills.length === 0) throw new Error(`No enabled skills in group "${group.name}"`)
|
|
150
|
+
if (enabledSkills.length === 1) {
|
|
151
|
+
return { skill: enabledSkills[0], group, strategy: group.rotationStrategy, confidence: 1 }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const selected = this.applyStrategy(enabledSkills, group.rotationStrategy, taskType)
|
|
155
|
+
return { skill: selected, group, strategy: group.rotationStrategy, confidence: 0.8 }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private applyStrategy(candidates: Skill[], strategy: RotationStrategy, taskType?: string): Skill {
|
|
159
|
+
switch (strategy) {
|
|
160
|
+
case 'round_robin': {
|
|
161
|
+
const idx = Date.now() % candidates.length
|
|
162
|
+
return candidates[idx]
|
|
163
|
+
}
|
|
164
|
+
case 'quality_based':
|
|
165
|
+
return candidates[0]
|
|
166
|
+
case 'weighted_random': {
|
|
167
|
+
return candidates[Math.floor(Math.random() * candidates.length)]
|
|
168
|
+
}
|
|
169
|
+
case 'task_match': {
|
|
170
|
+
if (taskType) {
|
|
171
|
+
const match = candidates.find((s) =>
|
|
172
|
+
s.name.toLowerCase().includes(taskType.toLowerCase()) ||
|
|
173
|
+
s.description.toLowerCase().includes(taskType.toLowerCase())
|
|
174
|
+
)
|
|
175
|
+
if (match) return match
|
|
176
|
+
}
|
|
177
|
+
return candidates[0]
|
|
178
|
+
}
|
|
179
|
+
case 'schedule': {
|
|
180
|
+
const idx = Math.floor(Date.now() / 3600000) % candidates.length
|
|
181
|
+
return candidates[idx]
|
|
182
|
+
}
|
|
183
|
+
default:
|
|
184
|
+
return candidates[0]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
invalidate() {
|
|
189
|
+
this.loaded = false
|
|
190
|
+
this.skillCache.clear()
|
|
191
|
+
this.groupCache.clear()
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const skillRegistry = new SkillRegistry()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type RotationStrategy = 'task_match' | 'round_robin' | 'quality_based' | 'schedule' | 'weighted_random'
|
|
2
|
+
|
|
3
|
+
export interface Skill {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
description: string
|
|
7
|
+
systemPrompt: string
|
|
8
|
+
groupId: string | null
|
|
9
|
+
temperature?: number
|
|
10
|
+
maxTokens?: number
|
|
11
|
+
preferredModel?: string
|
|
12
|
+
enabled: boolean
|
|
13
|
+
metadata?: Record<string, unknown>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SkillGroup {
|
|
17
|
+
id: string
|
|
18
|
+
name: string
|
|
19
|
+
description: string
|
|
20
|
+
skillIds: string[]
|
|
21
|
+
rotationStrategy: RotationStrategy
|
|
22
|
+
enabled: boolean
|
|
23
|
+
schedule?: {
|
|
24
|
+
cron: string
|
|
25
|
+
timezone: string
|
|
26
|
+
}
|
|
27
|
+
weights?: Record<string, number>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SkillRotationResult {
|
|
31
|
+
skill: Skill
|
|
32
|
+
group: SkillGroup | null
|
|
33
|
+
strategy: RotationStrategy
|
|
34
|
+
confidence: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SkillForgeRecipe {
|
|
38
|
+
name: string
|
|
39
|
+
description: string
|
|
40
|
+
basePrompt: string
|
|
41
|
+
examples: Array<{ input: string; output: string }>
|
|
42
|
+
constraints?: string[]
|
|
43
|
+
preferredModel?: string
|
|
44
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export type CompressionLevel = 'none' | 'light' | 'balanced' | 'aggressive'
|
|
2
|
+
|
|
3
|
+
export interface SqueezeResult {
|
|
4
|
+
originalTokens: number
|
|
5
|
+
squeezedTokens: number
|
|
6
|
+
savingsPercent: number
|
|
7
|
+
level: CompressionLevel
|
|
8
|
+
filters: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SqueezeContext {
|
|
12
|
+
content: string
|
|
13
|
+
contentType: 'code' | 'text' | 'mixed'
|
|
14
|
+
contextWindow: number
|
|
15
|
+
usedTokens: number
|
|
16
|
+
targetTokens?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CODE_PATTERNS = /(?:function|class|const |let |var |import |export |def |async |return |=>|```|\{|\}|\[|\])/
|
|
20
|
+
const COMMENT_PATTERNS = /(?:\/\/.*$|\/\*[\s\S]*?\*\/|#.*$)/gm
|
|
21
|
+
const WHITESPACE_MULTI = /\n{3,}/g
|
|
22
|
+
const TRAILING_WS = /[ \t]+$/gm
|
|
23
|
+
const EMPTY_LINES = /^\s*\n/gm
|
|
24
|
+
const CONSOLE_LOG = /console\.(log|debug|info|warn|error)\([^)]*\);?\n?/g
|
|
25
|
+
const TYPE_IMPORTS = /^import\s+type\s+.*$;\s*$/gm
|
|
26
|
+
const EMPTY_BLOCKS = /\{\s*\}/g
|
|
27
|
+
|
|
28
|
+
function classifyContent(content: string): 'code' | 'text' | 'mixed' {
|
|
29
|
+
const lines = content.split('\n')
|
|
30
|
+
let codeLines = 0
|
|
31
|
+
let textLines = 0
|
|
32
|
+
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (CODE_PATTERNS.test(line)) codeLines++
|
|
35
|
+
else if (line.trim()) textLines++
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (codeLines > textLines * 2) return 'code'
|
|
39
|
+
if (textLines > codeLines * 2) return 'text'
|
|
40
|
+
return 'mixed'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function estimateTokens(text: string): number {
|
|
44
|
+
return Math.ceil(text.length / 4)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function removeComments(content: string): string {
|
|
48
|
+
return content.replace(COMMENT_PATTERNS, '')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collapseWhitespace(content: string): string {
|
|
52
|
+
return content
|
|
53
|
+
.replace(TRAILING_WS, '')
|
|
54
|
+
.replace(WHITESPACE_MULTI, '\n\n')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function removeConsoleLogs(content: string): string {
|
|
58
|
+
return content.replace(CONSOLE_LOG, '')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function removeTypeImports(content: string): string {
|
|
62
|
+
return content.replace(TYPE_IMPORTS, '')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collapseEmptyBlocks(content: string): string {
|
|
66
|
+
return content.replace(EMPTY_BLOCKS, '{}')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function removeEmptyLines(content: string): string {
|
|
70
|
+
return content.replace(EMPTY_LINES, '')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function smartTruncate(content: string, targetTokens: number): string {
|
|
74
|
+
const currentTokens = estimateTokens(content)
|
|
75
|
+
if (currentTokens <= targetTokens) return content
|
|
76
|
+
|
|
77
|
+
const ratio = targetTokens / currentTokens
|
|
78
|
+
const targetChars = Math.floor(content.length * ratio)
|
|
79
|
+
|
|
80
|
+
const lines = content.split('\n')
|
|
81
|
+
const result: string[] = []
|
|
82
|
+
let charCount = 0
|
|
83
|
+
|
|
84
|
+
for (const line of lines) {
|
|
85
|
+
if (charCount + line.length > targetChars) break
|
|
86
|
+
result.push(line)
|
|
87
|
+
charCount += line.length + 1
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result.join('\n')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function squeeze(ctx: SqueezeContext, level: CompressionLevel = 'balanced'): SqueezeResult {
|
|
94
|
+
const originalTokens = estimateTokens(ctx.content)
|
|
95
|
+
if (level === 'none') {
|
|
96
|
+
return { originalTokens, squeezedTokens: originalTokens, savingsPercent: 0, level: 'none', filters: [] }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let content = ctx.content
|
|
100
|
+
const contentType = classifyContent(content)
|
|
101
|
+
const appliedFilters: string[] = []
|
|
102
|
+
|
|
103
|
+
if (level === 'light') {
|
|
104
|
+
content = collapseWhitespace(content)
|
|
105
|
+
appliedFilters.push('collapse_whitespace')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (level === 'balanced' || level === 'aggressive') {
|
|
109
|
+
content = collapseWhitespace(content)
|
|
110
|
+
appliedFilters.push('collapse_whitespace')
|
|
111
|
+
|
|
112
|
+
if (contentType === 'code' || contentType === 'mixed') {
|
|
113
|
+
content = removeComments(content)
|
|
114
|
+
appliedFilters.push('remove_comments')
|
|
115
|
+
|
|
116
|
+
content = removeConsoleLogs(content)
|
|
117
|
+
appliedFilters.push('remove_console_logs')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (level === 'balanced') {
|
|
121
|
+
content = collapseEmptyBlocks(content)
|
|
122
|
+
appliedFilters.push('collapse_empty_blocks')
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (level === 'aggressive') {
|
|
127
|
+
if (contentType === 'code' || contentType === 'mixed') {
|
|
128
|
+
content = removeTypeImports(content)
|
|
129
|
+
appliedFilters.push('remove_type_imports')
|
|
130
|
+
|
|
131
|
+
content = removeEmptyLines(content)
|
|
132
|
+
appliedFilters.push('remove_empty_lines')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const contextRemaining = ctx.contextWindow - ctx.usedTokens
|
|
136
|
+
const targetTokens = Math.min(
|
|
137
|
+
ctx.targetTokens || contextRemaining,
|
|
138
|
+
contextRemaining,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if (estimateTokens(content) > targetTokens) {
|
|
142
|
+
content = smartTruncate(content, targetTokens)
|
|
143
|
+
appliedFilters.push('smart_truncate')
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const squeezedTokens = estimateTokens(content)
|
|
148
|
+
const savingsPercent = originalTokens > 0 ? ((originalTokens - squeezedTokens) / originalTokens) * 100 : 0
|
|
149
|
+
|
|
150
|
+
return { originalTokens, squeezedTokens, savingsPercent, level, filters: appliedFilters }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function selectCompressionLevel(contextFillRatio: number): CompressionLevel {
|
|
154
|
+
if (contextFillRatio < 0.5) return 'none'
|
|
155
|
+
if (contextFillRatio < 0.7) return 'light'
|
|
156
|
+
if (contextFillRatio < 0.85) return 'balanced'
|
|
157
|
+
return 'aggressive'
|
|
158
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import pino from 'pino'
|
|
2
|
+
import { getConfig } from '../config'
|
|
3
|
+
|
|
4
|
+
const config = getConfig()
|
|
5
|
+
|
|
6
|
+
export const logger = pino({
|
|
7
|
+
level: config.logLevel || 'info',
|
|
8
|
+
transport: {
|
|
9
|
+
target: 'pino/file',
|
|
10
|
+
options: { destination: 1 },
|
|
11
|
+
},
|
|
12
|
+
formatters: {
|
|
13
|
+
level: (label) => ({ level: label }),
|
|
14
|
+
},
|
|
15
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
16
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { NextRequest } from 'next/server'
|
|
3
|
+
|
|
4
|
+
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
|
|
5
|
+
const WINDOW_MS = 60_000
|
|
6
|
+
const MAX_REQUESTS = 100
|
|
7
|
+
|
|
8
|
+
export function middleware(request: NextRequest) {
|
|
9
|
+
if (request.nextUrl.pathname.startsWith('/api/')) {
|
|
10
|
+
return handleApiMiddleware(request)
|
|
11
|
+
}
|
|
12
|
+
return NextResponse.next()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function handleApiMiddleware(request: NextRequest) {
|
|
16
|
+
const headers = new Headers()
|
|
17
|
+
|
|
18
|
+
headers.set('Access-Control-Allow-Origin', '*')
|
|
19
|
+
headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
|
20
|
+
headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key')
|
|
21
|
+
headers.set('Access-Control-Max-Age', '86400')
|
|
22
|
+
|
|
23
|
+
if (request.method === 'OPTIONS') {
|
|
24
|
+
return new NextResponse(null, { status: 204, headers })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
headers.set('X-Request-Id', crypto.randomUUID())
|
|
28
|
+
|
|
29
|
+
const clientIp = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
|
|
30
|
+
const key = `${clientIp}:${request.nextUrl.pathname}`
|
|
31
|
+
const now = Date.now()
|
|
32
|
+
|
|
33
|
+
const entry = rateLimitMap.get(key)
|
|
34
|
+
if (!entry || now > entry.resetAt) {
|
|
35
|
+
rateLimitMap.set(key, { count: 1, resetAt: now + WINDOW_MS })
|
|
36
|
+
} else {
|
|
37
|
+
entry.count++
|
|
38
|
+
if (entry.count > MAX_REQUESTS) {
|
|
39
|
+
headers.set('Retry-After', String(Math.ceil((entry.resetAt - now) / 1000)))
|
|
40
|
+
return NextResponse.json(
|
|
41
|
+
{ error: { type: 'rate_limit_exceeded', message: 'Too many requests' } },
|
|
42
|
+
{ status: 429, headers },
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
headers.set('X-RateLimit-Limit', String(MAX_REQUESTS))
|
|
48
|
+
headers.set('X-RateLimit-Remaining', String(Math.max(0, MAX_REQUESTS - (entry?.count || 1))))
|
|
49
|
+
|
|
50
|
+
const response = NextResponse.next()
|
|
51
|
+
for (const [k, v] of headers.entries()) {
|
|
52
|
+
response.headers.set(k, v)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return response
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const config = {
|
|
59
|
+
matcher: ['/api/:path*'],
|
|
60
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"next-env.d.ts",
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
".next/types/**/*.ts",
|
|
30
|
+
".next/dev/types/**/*.ts",
|
|
31
|
+
"**/*.mts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": ["node_modules"]
|
|
34
|
+
}
|