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,112 @@
1
+ import { BaseAdapter } from '../base-adapter'
2
+ import type { ProviderInfo, ProviderAccount, NormalizedRequest, NormalizedResponse, NormalizedChunk, Model, HealthStatusType } from '../types'
3
+
4
+ export class GeminiAdapter extends BaseAdapter {
5
+ info: ProviderInfo
6
+
7
+ constructor(info?: Partial<ProviderInfo>) {
8
+ super()
9
+ this.info = {
10
+ id: info?.id || 'gemini',
11
+ name: info?.name || 'Gemini',
12
+ prefix: info?.prefix || 'gm/',
13
+ authType: info?.authType || 'api_key',
14
+ baseUrl: info?.baseUrl || 'https://generativelanguage.googleapis.com',
15
+ enabled: info?.enabled ?? true,
16
+ }
17
+ }
18
+
19
+ async fetchModels(): Promise<Model[]> {
20
+ try {
21
+ const data = await this.fetchJson<{ models?: Array<{ name: string; displayName?: string; inputTokenLimit?: number; outputTokenLimit?: number }> }>(
22
+ `${this.info.baseUrl}/v1beta/models`,
23
+ { headers: {} }
24
+ )
25
+ return (data.models || [])
26
+ .filter((m) => m.name.includes('gemini'))
27
+ .map((m) => {
28
+ const id = m.name.replace('models/', '')
29
+ return {
30
+ id: `${this.info.prefix}${id}`,
31
+ name: id,
32
+ displayName: m.displayName || id,
33
+ providerId: this.info.id,
34
+ pricingTier: 'pay_per_use' as const,
35
+ contextWindow: m.inputTokenLimit,
36
+ available: true,
37
+ }
38
+ })
39
+ } catch {
40
+ return [
41
+ { id: 'gm/gemini-2.5-flash', name: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', providerId: this.info.id, pricingTier: 'pay_per_use', contextWindow: 1048576, available: true },
42
+ { id: 'gm/gemini-2.5-pro', name: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', providerId: this.info.id, pricingTier: 'pay_per_use', contextWindow: 2097152, available: true },
43
+ ]
44
+ }
45
+ }
46
+
47
+ async chatCompletion(req: NormalizedRequest, account: ProviderAccount): Promise<NormalizedResponse> {
48
+ const startTime = Date.now()
49
+ const modelName = req.model.replace('gm/', '')
50
+ const contents = req.messages
51
+ .filter((m) => m.role !== 'system')
52
+ .map((m) => ({
53
+ role: m.role === 'assistant' ? 'model' : 'user',
54
+ parts: [{ text: m.content }],
55
+ }))
56
+
57
+ const systemInstruction = req.messages.find((m) => m.role === 'system')
58
+
59
+ const geminiBody: Record<string, unknown> = {
60
+ contents,
61
+ generationConfig: {
62
+ temperature: req.temperature,
63
+ maxOutputTokens: req.maxTokens,
64
+ },
65
+ }
66
+ if (systemInstruction) {
67
+ geminiBody.systemInstruction = { parts: [{ text: systemInstruction.content }] }
68
+ }
69
+
70
+ const raw = await this.fetchJson<{
71
+ candidates?: Array<{ content?: { parts?: Array<{ text?: string }> }; finishReason?: string }>
72
+ usageMetadata?: { promptTokenCount?: number; candidatesTokenCount?: number; totalTokenCount?: number }
73
+ }>(`${this.info.baseUrl}/v1beta/models/${modelName}:generateContent?key=${account.authData.apiKey}`, {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/json' },
76
+ body: JSON.stringify(geminiBody),
77
+ })
78
+
79
+ const content = raw.candidates?.[0]?.content?.parts?.[0]?.text || ''
80
+
81
+ return {
82
+ id: crypto.randomUUID(),
83
+ model: modelName,
84
+ choices: [{
85
+ index: 0,
86
+ message: { role: 'assistant', content },
87
+ finishReason: raw.candidates?.[0]?.finishReason || 'stop',
88
+ }],
89
+ usage: {
90
+ inputTokens: raw.usageMetadata?.promptTokenCount || 0,
91
+ outputTokens: raw.usageMetadata?.candidatesTokenCount || 0,
92
+ totalTokens: raw.usageMetadata?.totalTokenCount || 0,
93
+ },
94
+ latencyMs: Date.now() - startTime,
95
+ provider: this.info.name,
96
+ }
97
+ }
98
+
99
+ async *chatCompletionStream(): AsyncIterable<NormalizedChunk> {
100
+ yield { id: '', model: '', choices: [{ index: 0, delta: { content: 'Streaming not implemented for Gemini adapter yet' }, finishReason: 'stop' }] }
101
+ }
102
+
103
+ async healthCheck(): Promise<{ status: HealthStatusType; latencyMs: number }> {
104
+ const start = Date.now()
105
+ try {
106
+ await fetch(`${this.info.baseUrl}/v1beta/models?key=test`, { method: 'GET', signal: AbortSignal.timeout(10000) })
107
+ return { status: 'healthy', latencyMs: Date.now() - start }
108
+ } catch {
109
+ return { status: 'down', latencyMs: Date.now() - start }
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,122 @@
1
+ import { BaseAdapter } from '../base-adapter'
2
+ import type { ProviderInfo, ProviderAccount, NormalizedRequest, NormalizedResponse, NormalizedChunk, Model, HealthStatusType } from '../types'
3
+
4
+ export class OpenAIAdapter extends BaseAdapter {
5
+ info: ProviderInfo
6
+
7
+ constructor(info?: Partial<ProviderInfo>) {
8
+ super()
9
+ this.info = {
10
+ id: info?.id || 'openai',
11
+ name: info?.name || 'OpenAI',
12
+ prefix: info?.prefix || 'oa/',
13
+ authType: info?.authType || 'api_key',
14
+ baseUrl: info?.baseUrl || 'https://api.openai.com/v1',
15
+ enabled: info?.enabled ?? true,
16
+ }
17
+ }
18
+
19
+ async fetchModels(): Promise<Model[]> {
20
+ try {
21
+ const data = await this.fetchJson<{ data: Array<{ id: string }> }>(
22
+ `${this.info.baseUrl}/models`,
23
+ { headers: { 'Content-Type': 'application/json' } }
24
+ )
25
+ return data.data
26
+ .filter((m) => m.id.startsWith('gpt-') || m.id.startsWith('o1') || m.id.startsWith('o3'))
27
+ .map((m) => ({
28
+ id: `${this.info.prefix}${m.id}`,
29
+ name: m.id,
30
+ displayName: m.id,
31
+ providerId: this.info.id,
32
+ pricingTier: 'pay_per_use' as const,
33
+ available: true,
34
+ }))
35
+ } catch {
36
+ return []
37
+ }
38
+ }
39
+
40
+ async chatCompletion(req: NormalizedRequest, account: ProviderAccount): Promise<NormalizedResponse> {
41
+ const startTime = Date.now()
42
+ const body = this.createOpenAIRequest(req)
43
+
44
+ const raw = await this.fetchJson<{
45
+ id?: string
46
+ model?: string
47
+ choices?: Array<{ index?: number; message?: { role?: string; content?: string }; finish_reason?: string }>
48
+ usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number }
49
+ }>(`${this.info.baseUrl}/chat/completions`, {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ Authorization: `Bearer ${account.authData.apiKey}`,
54
+ },
55
+ body: JSON.stringify(body),
56
+ })
57
+
58
+ return this.parseOpenAIResponse(raw, this.info.name, startTime)
59
+ }
60
+
61
+ async *chatCompletionStream(req: NormalizedRequest, account: ProviderAccount): AsyncIterable<NormalizedChunk> {
62
+ const body = { ...this.createOpenAIRequest(req), stream: true }
63
+ const response = await this.fetchWithRetry(`${this.info.baseUrl}/chat/completions`, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ Authorization: `Bearer ${account.authData.apiKey}`,
68
+ },
69
+ body: JSON.stringify(body),
70
+ })
71
+
72
+ if (!response.body) return
73
+
74
+ const reader = response.body.getReader()
75
+ const decoder = new TextDecoder()
76
+ let buffer = ''
77
+
78
+ while (true) {
79
+ const { done, value } = await reader.read()
80
+ if (done) break
81
+
82
+ buffer += decoder.decode(value, { stream: true })
83
+ const lines = buffer.split('\n')
84
+ buffer = lines.pop() || ''
85
+
86
+ for (const line of lines) {
87
+ const trimmed = line.trim()
88
+ if (!trimmed.startsWith('data: ')) continue
89
+ const data = trimmed.slice(6)
90
+ if (data === '[DONE]') return
91
+
92
+ try {
93
+ const parsed = JSON.parse(data)
94
+ yield {
95
+ id: parsed.id || '',
96
+ model: parsed.model || '',
97
+ choices: (parsed.choices || []).map((c: Record<string, unknown>) => ({
98
+ index: (c.index as number) || 0,
99
+ delta: {
100
+ role: (c.delta as Record<string, unknown>)?.role as 'assistant' | undefined,
101
+ content: (c.delta as Record<string, unknown>)?.content as string | undefined,
102
+ },
103
+ finishReason: (c.finish_reason as string) || null,
104
+ })),
105
+ }
106
+ } catch {
107
+ continue
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ async healthCheck(): Promise<{ status: HealthStatusType; latencyMs: number }> {
114
+ const start = Date.now()
115
+ try {
116
+ await fetch(`${this.info.baseUrl}/models`, { method: 'GET', signal: AbortSignal.timeout(10000) })
117
+ return { status: 'healthy', latencyMs: Date.now() - start }
118
+ } catch {
119
+ return { status: 'down', latencyMs: Date.now() - start }
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,112 @@
1
+ import { BaseAdapter } from '../base-adapter'
2
+ import type { ProviderInfo, ProviderAccount, NormalizedRequest, NormalizedResponse, NormalizedChunk, Model, HealthStatusType } from '../types'
3
+
4
+ export class OpenRouterAdapter extends BaseAdapter {
5
+ info: ProviderInfo
6
+
7
+ constructor(info?: Partial<ProviderInfo>) {
8
+ super()
9
+ this.info = {
10
+ id: info?.id || 'openrouter',
11
+ name: info?.name || 'OpenRouter',
12
+ prefix: info?.prefix || 'or/',
13
+ authType: info?.authType || 'api_key',
14
+ baseUrl: info?.baseUrl || 'https://openrouter.ai/api/v1',
15
+ enabled: info?.enabled ?? true,
16
+ }
17
+ }
18
+
19
+ async fetchModels(): Promise<Model[]> {
20
+ try {
21
+ const data = await this.fetchJson<{ data: Array<{ id: string; name?: string; context_length?: number; pricing?: { prompt?: string; completion?: string } }> }>(
22
+ `${this.info.baseUrl}/models`,
23
+ { headers: { 'Content-Type': 'application/json' } }
24
+ )
25
+ return data.data.map((m) => ({
26
+ id: `${this.info.prefix}${m.id}`,
27
+ name: m.id,
28
+ displayName: m.name || m.id,
29
+ providerId: this.info.id,
30
+ pricingTier: 'pay_per_use' as const,
31
+ costPer1mInput: m.pricing?.prompt ? parseFloat(m.pricing.prompt) * 1_000_000 : undefined,
32
+ costPer1mOutput: m.pricing?.completion ? parseFloat(m.pricing.completion) * 1_000_000 : undefined,
33
+ contextWindow: m.context_length,
34
+ available: true,
35
+ }))
36
+ } catch {
37
+ return []
38
+ }
39
+ }
40
+
41
+ async chatCompletion(req: NormalizedRequest, account: ProviderAccount): Promise<NormalizedResponse> {
42
+ const startTime = Date.now()
43
+ const body = this.createOpenAIRequest(req)
44
+ const raw = await this.fetchJson<{
45
+ id?: string; model?: string
46
+ choices?: Array<{ index?: number; message?: { role?: string; content?: string }; finish_reason?: string }>
47
+ usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number }
48
+ }>(`${this.info.baseUrl}/chat/completions`, {
49
+ method: 'POST',
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ Authorization: `Bearer ${account.authData.apiKey}`,
53
+ 'HTTP-Referer': 'https://synapse.dev',
54
+ 'X-Title': 'Synapse',
55
+ },
56
+ body: JSON.stringify(body),
57
+ })
58
+ return this.parseOpenAIResponse(raw, this.info.name, startTime)
59
+ }
60
+
61
+ async *chatCompletionStream(req: NormalizedRequest, account: ProviderAccount): AsyncIterable<NormalizedChunk> {
62
+ const body = { ...this.createOpenAIRequest(req), stream: true }
63
+ const response = await this.fetchWithRetry(`${this.info.baseUrl}/chat/completions`, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ Authorization: `Bearer ${account.authData.apiKey}`,
68
+ 'HTTP-Referer': 'https://synapse.dev',
69
+ 'X-Title': 'Synapse',
70
+ },
71
+ body: JSON.stringify(body),
72
+ })
73
+ if (!response.body) return
74
+ const reader = response.body.getReader()
75
+ const decoder = new TextDecoder()
76
+ let buffer = ''
77
+ while (true) {
78
+ const { done, value } = await reader.read()
79
+ if (done) break
80
+ buffer += decoder.decode(value, { stream: true })
81
+ const lines = buffer.split('\n')
82
+ buffer = lines.pop() || ''
83
+ for (const line of lines) {
84
+ const trimmed = line.trim()
85
+ if (!trimmed.startsWith('data: ')) continue
86
+ const data = trimmed.slice(6)
87
+ if (data === '[DONE]') return
88
+ try {
89
+ const parsed = JSON.parse(data)
90
+ yield {
91
+ id: parsed.id || '', model: parsed.model || '',
92
+ choices: (parsed.choices || []).map((c: Record<string, unknown>) => ({
93
+ index: (c.index as number) || 0,
94
+ delta: { role: (c.delta as Record<string, unknown>)?.role as 'assistant' | undefined, content: (c.delta as Record<string, unknown>)?.content as string | undefined },
95
+ finishReason: (c.finish_reason as string) || null,
96
+ })),
97
+ }
98
+ } catch { continue }
99
+ }
100
+ }
101
+ }
102
+
103
+ async healthCheck(): Promise<{ status: HealthStatusType; latencyMs: number }> {
104
+ const start = Date.now()
105
+ try {
106
+ await fetch(`${this.info.baseUrl}/models`, { method: 'GET', signal: AbortSignal.timeout(10000) })
107
+ return { status: 'healthy', latencyMs: Date.now() - start }
108
+ } catch {
109
+ return { status: 'down', latencyMs: Date.now() - start }
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,122 @@
1
+ import type {
2
+ ProviderAdapter,
3
+ ProviderInfo,
4
+ ProviderAccount,
5
+ NormalizedRequest,
6
+ NormalizedResponse,
7
+ NormalizedChunk,
8
+ Model,
9
+ HealthStatusType,
10
+ Message,
11
+ } from './types'
12
+
13
+ export abstract class BaseAdapter implements ProviderAdapter {
14
+ abstract info: ProviderInfo
15
+
16
+ protected timeout = 30000
17
+ protected maxRetries = 3
18
+
19
+ abstract fetchModels(): Promise<Model[]>
20
+ abstract chatCompletion(req: NormalizedRequest, account: ProviderAccount): Promise<NormalizedResponse>
21
+ abstract chatCompletionStream(req: NormalizedRequest, account: ProviderAccount): AsyncIterable<NormalizedChunk>
22
+ abstract healthCheck(): Promise<{ status: HealthStatusType; latencyMs: number }>
23
+
24
+ protected async fetchWithRetry(
25
+ url: string,
26
+ options: RequestInit,
27
+ maxRetries = this.maxRetries
28
+ ): Promise<Response> {
29
+ let lastError: Error | null = null
30
+
31
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
32
+ try {
33
+ const controller = new AbortController()
34
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout)
35
+
36
+ const response = await fetch(url, {
37
+ ...options,
38
+ signal: controller.signal,
39
+ })
40
+
41
+ clearTimeout(timeoutId)
42
+
43
+ if (response.status === 429 && attempt < maxRetries - 1) {
44
+ const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000
45
+ await new Promise((resolve) => setTimeout(resolve, delay))
46
+ continue
47
+ }
48
+
49
+ return response
50
+ } catch (error) {
51
+ lastError = error as Error
52
+ if (attempt < maxRetries - 1) {
53
+ const delay = Math.pow(2, attempt) * 500
54
+ await new Promise((resolve) => setTimeout(resolve, delay))
55
+ }
56
+ }
57
+ }
58
+
59
+ throw lastError || new Error('Request failed after retries')
60
+ }
61
+
62
+ protected async fetchJson<T>(url: string, options: RequestInit): Promise<T> {
63
+ const response = await this.fetchWithRetry(url, options)
64
+ if (!response.ok) {
65
+ const body = await response.text().catch(() => '')
66
+ throw new Error(`HTTP ${response.status}: ${body}`)
67
+ }
68
+ return response.json() as Promise<T>
69
+ }
70
+
71
+ protected createOpenAIRequest(req: NormalizedRequest): Record<string, unknown> {
72
+ const body: Record<string, unknown> = {
73
+ model: req.model,
74
+ messages: req.messages.map((m) => ({
75
+ role: m.role,
76
+ content: m.content,
77
+ })),
78
+ stream: req.stream ?? false,
79
+ }
80
+
81
+ if (req.temperature !== undefined) body.temperature = req.temperature
82
+ if (req.maxTokens !== undefined) body.max_tokens = req.maxTokens
83
+ if (req.tools) body.tools = req.tools
84
+
85
+ return body
86
+ }
87
+
88
+ protected parseOpenAIResponse(raw: {
89
+ id?: string
90
+ model?: string
91
+ choices?: Array<{
92
+ index?: number
93
+ message?: { role?: string; content?: string; tool_calls?: unknown[] }
94
+ finish_reason?: string
95
+ }>
96
+ usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number }
97
+ }, providerName: string, startTime: number): NormalizedResponse {
98
+ const message: Message = {
99
+ role: 'assistant',
100
+ content: raw.choices?.[0]?.message?.content || '',
101
+ }
102
+
103
+ return {
104
+ id: raw.id || crypto.randomUUID(),
105
+ model: raw.model || '',
106
+ choices: [
107
+ {
108
+ index: raw.choices?.[0]?.index || 0,
109
+ message,
110
+ finishReason: raw.choices?.[0]?.finish_reason || 'stop',
111
+ },
112
+ ],
113
+ usage: {
114
+ inputTokens: raw.usage?.prompt_tokens || 0,
115
+ outputTokens: raw.usage?.completion_tokens || 0,
116
+ totalTokens: raw.usage?.total_tokens || 0,
117
+ },
118
+ latencyMs: Date.now() - startTime,
119
+ provider: providerName,
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,46 @@
1
+ import type { ProviderAdapter, MeshProvider, ProviderAccount, Model } from './types'
2
+
3
+ class ProviderRegistry {
4
+ private adapters = new Map<string, ProviderAdapter>()
5
+
6
+ register(adapter: ProviderAdapter): void {
7
+ this.adapters.set(adapter.info.prefix, adapter)
8
+ }
9
+
10
+ unregister(prefix: string): void {
11
+ this.adapters.delete(prefix)
12
+ }
13
+
14
+ get(prefix: string): ProviderAdapter | undefined {
15
+ return this.adapters.get(prefix)
16
+ }
17
+
18
+ list(): ProviderAdapter[] {
19
+ return Array.from(this.adapters.values())
20
+ }
21
+
22
+ resolveModel(modelId: string): { adapter: ProviderAdapter; modelName: string } | null {
23
+ const slashIndex = modelId.indexOf('/')
24
+ if (slashIndex === -1) return null
25
+
26
+ const prefix = modelId.substring(0, slashIndex) + '/'
27
+ const modelName = modelId.substring(slashIndex + 1)
28
+
29
+ const adapter = this.adapters.get(prefix)
30
+ if (!adapter) return null
31
+
32
+ return { adapter, modelName }
33
+ }
34
+
35
+ resolveAllProviders(modelName: string): Array<{ adapter: ProviderAdapter; localName: string }> {
36
+ const results: Array<{ adapter: ProviderAdapter; localName: string }> = []
37
+
38
+ for (const adapter of this.adapters.values()) {
39
+ results.push({ adapter, localName: modelName })
40
+ }
41
+
42
+ return results
43
+ }
44
+ }
45
+
46
+ export const registry = new ProviderRegistry()
@@ -0,0 +1,121 @@
1
+ export type AuthType = 'oauth' | 'api_key' | 'none' | 'service_account'
2
+ export type PricingTier = 'free' | 'cheap' | 'subscription' | 'pay_per_use'
3
+ export type HealthStatusType = 'healthy' | 'degraded' | 'down' | 'disabled'
4
+ export type TaskType = 'code' | 'chat' | 'reason' | 'review' | 'debug' | 'doc' | 'translate' | 'fast'
5
+
6
+ export interface Model {
7
+ id: string
8
+ name: string
9
+ displayName?: string
10
+ providerId: string
11
+ pricingTier?: PricingTier
12
+ costPer1mInput?: number
13
+ costPer1mOutput?: number
14
+ contextWindow?: number
15
+ capabilities?: string[]
16
+ available: boolean
17
+ }
18
+
19
+ export interface ProviderInfo {
20
+ id: string
21
+ name: string
22
+ prefix: string
23
+ authType: AuthType
24
+ baseUrl: string
25
+ enabled: boolean
26
+ }
27
+
28
+ export interface ProviderAccount {
29
+ id: string
30
+ providerId: string
31
+ label?: string
32
+ authData: Record<string, unknown>
33
+ enabled: boolean
34
+ priority: number
35
+ quotaUsedTokens: number
36
+ quotaLimitTokens?: number
37
+ quotaResetAt?: Date
38
+ lastUsedAt?: Date
39
+ }
40
+
41
+ export interface Message {
42
+ role: 'system' | 'user' | 'assistant' | 'tool'
43
+ content: string
44
+ toolCallId?: string
45
+ toolCalls?: ToolCall[]
46
+ }
47
+
48
+ export interface ToolCall {
49
+ id: string
50
+ type: 'function'
51
+ function: { name: string; arguments: string }
52
+ }
53
+
54
+ export interface NormalizedRequest {
55
+ model: string
56
+ messages: Message[]
57
+ temperature?: number
58
+ maxTokens?: number
59
+ stream?: boolean
60
+ tools?: unknown[]
61
+ metadata?: Record<string, unknown>
62
+ skillId?: string
63
+ taskType?: TaskType
64
+ }
65
+
66
+ export interface NormalizedResponse {
67
+ id: string
68
+ model: string
69
+ choices: Array<{
70
+ index: number
71
+ message: Message
72
+ finishReason: string
73
+ }>
74
+ usage: {
75
+ inputTokens: number
76
+ outputTokens: number
77
+ totalTokens: number
78
+ }
79
+ latencyMs: number
80
+ provider: string
81
+ }
82
+
83
+ export interface NormalizedChunk {
84
+ id: string
85
+ model: string
86
+ choices: Array<{
87
+ index: number
88
+ delta: Partial<Message>
89
+ finishReason: string | null
90
+ }>
91
+ usage?: {
92
+ inputTokens: number
93
+ outputTokens: number
94
+ totalTokens: number
95
+ }
96
+ }
97
+
98
+ export interface QuotaInfo {
99
+ usedTokens: number
100
+ limitTokens?: number
101
+ resetAt?: Date
102
+ unlimited: boolean
103
+ }
104
+
105
+ export interface ProviderAdapter {
106
+ info: ProviderInfo
107
+ fetchModels(): Promise<Model[]>
108
+ chatCompletion(req: NormalizedRequest, account: ProviderAccount): Promise<NormalizedResponse>
109
+ chatCompletionStream(req: NormalizedRequest, account: ProviderAccount): AsyncIterable<NormalizedChunk>
110
+ healthCheck(): Promise<{ status: HealthStatusType; latencyMs: number }>
111
+ getQuota?(account: ProviderAccount): Promise<QuotaInfo>
112
+ }
113
+
114
+ export interface MeshProvider {
115
+ provider: ProviderInfo
116
+ account: ProviderAccount
117
+ model: Model
118
+ health: HealthStatusType
119
+ cost: number
120
+ latency: number
121
+ }