ai-agent-router 0.1.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/.claude/commands/openspec/apply.md +23 -0
- package/.claude/commands/openspec/archive.md +27 -0
- package/.claude/commands/openspec/proposal.md +28 -0
- package/.claude/settings.local.json +12 -0
- package/.claude/skills/ui-ux-pro-max/SKILL.md +228 -0
- package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/prompts.csv +24 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.claude/skills/ui-ux-pro-max/data/styles.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc +0 -0
- package/.claude/skills/ui-ux-pro-max/scripts/core.py +238 -0
- package/.claude/skills/ui-ux-pro-max/scripts/search.py +61 -0
- package/.cursor/commands/openspec-apply.md +23 -0
- package/.cursor/commands/openspec-archive.md +27 -0
- package/.cursor/commands/openspec-proposal.md +28 -0
- package/.cursor/commands/ui-ux-pro-max.md +226 -0
- package/.eslintrc.json +3 -0
- package/.shared/ui-ux-pro-max/data/charts.csv +26 -0
- package/.shared/ui-ux-pro-max/data/colors.csv +97 -0
- package/.shared/ui-ux-pro-max/data/landing.csv +31 -0
- package/.shared/ui-ux-pro-max/data/products.csv +97 -0
- package/.shared/ui-ux-pro-max/data/prompts.csv +24 -0
- package/.shared/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.shared/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.shared/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.shared/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.shared/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.shared/ui-ux-pro-max/data/styles.csv +59 -0
- package/.shared/ui-ux-pro-max/data/typography.csv +58 -0
- package/.shared/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.shared/ui-ux-pro-max/scripts/core.py +238 -0
- package/.shared/ui-ux-pro-max/scripts/search.py +61 -0
- package/AGENTS.md +18 -0
- package/CLAUDE.md +18 -0
- package/IMPLEMENTATION.md +157 -0
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/.next/types/app/api/config/route.js +52 -0
- package/dist/.next/types/app/api/gateway/[...path]/route.js +52 -0
- package/dist/.next/types/app/api/gateway/route.js +52 -0
- package/dist/.next/types/app/api/logs/route.js +52 -0
- package/dist/.next/types/app/api/models/route.js +52 -0
- package/dist/.next/types/app/api/providers/route.js +52 -0
- package/dist/.next/types/app/api/providers/test/route.js +52 -0
- package/dist/.next/types/app/api/service/start/route.js +52 -0
- package/dist/.next/types/app/api/service/status/route.js +52 -0
- package/dist/.next/types/app/api/service/stop/route.js +52 -0
- package/dist/.next/types/app/layout.js +22 -0
- package/dist/.next/types/app/logs/page.js +22 -0
- package/dist/.next/types/app/models/page.js +22 -0
- package/dist/.next/types/app/page.js +22 -0
- package/dist/.next/types/app/providers/page.js +22 -0
- package/dist/src/app/api/config/route.js +43 -0
- package/dist/src/app/api/gateway/[...path]/route.js +83 -0
- package/dist/src/app/api/gateway/route.js +63 -0
- package/dist/src/app/api/logs/route.js +34 -0
- package/dist/src/app/api/models/route.js +152 -0
- package/dist/src/app/api/providers/route.js +118 -0
- package/dist/src/app/api/providers/test/route.js +154 -0
- package/dist/src/app/api/service/start/route.js +55 -0
- package/dist/src/app/api/service/status/route.js +17 -0
- package/dist/src/app/api/service/stop/route.js +20 -0
- package/dist/src/app/components/ConfirmDialog.jsx +31 -0
- package/dist/src/app/components/Nav.jsx +45 -0
- package/dist/src/app/components/Toast.jsx +37 -0
- package/dist/src/app/components/ToastProvider.jsx +21 -0
- package/dist/src/app/layout.jsx +13 -0
- package/dist/src/app/logs/page.jsx +210 -0
- package/dist/src/app/models/page.jsx +291 -0
- package/dist/src/app/page.jsx +236 -0
- package/dist/src/app/providers/page.jsx +402 -0
- package/dist/src/cli/index.js +90 -0
- package/dist/src/db/database.js +69 -0
- package/dist/src/db/queries.js +261 -0
- package/dist/src/db/schema.js +67 -0
- package/dist/src/server/crypto.js +22 -0
- package/dist/src/server/gateway-server.js +200 -0
- package/dist/src/server/gateway.js +76 -0
- package/dist/src/server/logger.js +72 -0
- package/dist/src/server/providers/anthropic.js +52 -0
- package/dist/src/server/providers/gemini.js +64 -0
- package/dist/src/server/providers/index.js +16 -0
- package/dist/src/server/providers/openai.js +86 -0
- package/dist/src/server/providers/types.js +1 -0
- package/dist/src/server/service-manager.js +286 -0
- package/docs/TODO.md +19 -0
- package/next.config.js +7 -0
- package/openspec/AGENTS.md +456 -0
- package/openspec/changes/add-logging/proposal.md +18 -0
- package/openspec/changes/add-logging/specs/core/spec.md +21 -0
- package/openspec/changes/add-logging/tasks.md +16 -0
- package/openspec/changes/add-provider-test-connection/proposal.md +22 -0
- package/openspec/changes/add-provider-test-connection/specs/model-provider/spec.md +68 -0
- package/openspec/changes/add-provider-test-connection/tasks.md +31 -0
- package/openspec/changes/improve-gateway-startup/design.md +137 -0
- package/openspec/changes/improve-gateway-startup/proposal.md +33 -0
- package/openspec/changes/improve-gateway-startup/specs/api-gateway/spec.md +94 -0
- package/openspec/changes/improve-gateway-startup/specs/web-ui/spec.md +67 -0
- package/openspec/changes/improve-gateway-startup/tasks.md +47 -0
- package/openspec/changes/init-api-gateway/design.md +185 -0
- package/openspec/changes/init-api-gateway/proposal.md +30 -0
- package/openspec/changes/init-api-gateway/specs/api-gateway/spec.md +42 -0
- package/openspec/changes/init-api-gateway/specs/cli-tool/spec.md +40 -0
- package/openspec/changes/init-api-gateway/specs/model-management/spec.md +47 -0
- package/openspec/changes/init-api-gateway/specs/model-provider/spec.md +33 -0
- package/openspec/changes/init-api-gateway/specs/request-logging/spec.md +54 -0
- package/openspec/changes/init-api-gateway/specs/web-ui/spec.md +49 -0
- package/openspec/changes/init-api-gateway/tasks.md +84 -0
- package/openspec/project.md +58 -0
- package/package.json +51 -0
- package/postcss.config.js +6 -0
- package/src/app/api/config/route.ts +62 -0
- package/src/app/api/gateway/[...path]/route.ts +118 -0
- package/src/app/api/gateway/route.ts +77 -0
- package/src/app/api/logs/route.ts +48 -0
- package/src/app/api/models/route.ts +210 -0
- package/src/app/api/providers/route.ts +162 -0
- package/src/app/api/providers/test/route.ts +182 -0
- package/src/app/api/service/start/route.ts +73 -0
- package/src/app/api/service/status/route.ts +22 -0
- package/src/app/api/service/stop/route.ts +27 -0
- package/src/app/components/ConfirmDialog.tsx +63 -0
- package/src/app/components/Nav.tsx +66 -0
- package/src/app/components/Toast.tsx +61 -0
- package/src/app/components/ToastProvider.tsx +43 -0
- package/src/app/globals.css +71 -0
- package/src/app/layout.tsx +22 -0
- package/src/app/logs/page.tsx +261 -0
- package/src/app/models/page.tsx +500 -0
- package/src/app/page.tsx +742 -0
- package/src/app/providers/page.tsx +558 -0
- package/src/cli/index.ts +95 -0
- package/src/db/database.ts +125 -0
- package/src/db/queries.ts +339 -0
- package/src/db/schema.ts +117 -0
- package/src/server/crypto.ts +48 -0
- package/src/server/gateway-server.ts +306 -0
- package/src/server/gateway.ts +163 -0
- package/src/server/logger.ts +96 -0
- package/src/server/providers/anthropic.ts +121 -0
- package/src/server/providers/gemini.ts +112 -0
- package/src/server/providers/index.ts +20 -0
- package/src/server/providers/openai.ts +235 -0
- package/src/server/providers/types.ts +20 -0
- package/src/server/service-manager.ts +321 -0
- package/tailwind.config.js +16 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { getDatabase } from './database.js';
|
|
2
|
+
// Provider queries
|
|
3
|
+
export function getAllProviders() {
|
|
4
|
+
const db = getDatabase();
|
|
5
|
+
return db.prepare('SELECT * FROM providers ORDER BY created_at DESC').all();
|
|
6
|
+
}
|
|
7
|
+
export function getProviderById(id) {
|
|
8
|
+
const db = getDatabase();
|
|
9
|
+
return db.prepare('SELECT * FROM providers WHERE id = ?').get(id);
|
|
10
|
+
}
|
|
11
|
+
export function createProvider(provider) {
|
|
12
|
+
const db = getDatabase();
|
|
13
|
+
const stmt = db.prepare(`
|
|
14
|
+
INSERT INTO providers (name, protocol, base_url, api_key, updated_at)
|
|
15
|
+
VALUES (?, ?, ?, ?, datetime('now'))
|
|
16
|
+
`);
|
|
17
|
+
const result = stmt.run(provider.name, provider.protocol, provider.base_url, provider.api_key);
|
|
18
|
+
return getProviderById(result.lastInsertRowid);
|
|
19
|
+
}
|
|
20
|
+
export function updateProvider(id, provider) {
|
|
21
|
+
const db = getDatabase();
|
|
22
|
+
const updates = [];
|
|
23
|
+
const values = [];
|
|
24
|
+
if (provider.name !== undefined) {
|
|
25
|
+
updates.push('name = ?');
|
|
26
|
+
values.push(provider.name);
|
|
27
|
+
}
|
|
28
|
+
if (provider.protocol !== undefined) {
|
|
29
|
+
updates.push('protocol = ?');
|
|
30
|
+
values.push(provider.protocol);
|
|
31
|
+
}
|
|
32
|
+
if (provider.base_url !== undefined) {
|
|
33
|
+
updates.push('base_url = ?');
|
|
34
|
+
values.push(provider.base_url);
|
|
35
|
+
}
|
|
36
|
+
if (provider.api_key !== undefined) {
|
|
37
|
+
updates.push('api_key = ?');
|
|
38
|
+
values.push(provider.api_key);
|
|
39
|
+
}
|
|
40
|
+
if (updates.length === 0) {
|
|
41
|
+
return getProviderById(id);
|
|
42
|
+
}
|
|
43
|
+
updates.push("updated_at = datetime('now')");
|
|
44
|
+
values.push(id);
|
|
45
|
+
const stmt = db.prepare(`UPDATE providers SET ${updates.join(', ')} WHERE id = ?`);
|
|
46
|
+
stmt.run(...values);
|
|
47
|
+
return getProviderById(id);
|
|
48
|
+
}
|
|
49
|
+
export function deleteProvider(id) {
|
|
50
|
+
const db = getDatabase();
|
|
51
|
+
const stmt = db.prepare('DELETE FROM providers WHERE id = ?');
|
|
52
|
+
const result = stmt.run(id);
|
|
53
|
+
return result.changes > 0;
|
|
54
|
+
}
|
|
55
|
+
// Model queries
|
|
56
|
+
export function getAllModels() {
|
|
57
|
+
const db = getDatabase();
|
|
58
|
+
return db.prepare(`
|
|
59
|
+
SELECT m.*, p.name as provider_name, p.protocol as provider_protocol
|
|
60
|
+
FROM models m
|
|
61
|
+
JOIN providers p ON m.provider_id = p.id
|
|
62
|
+
ORDER BY m.created_at DESC
|
|
63
|
+
`).all();
|
|
64
|
+
}
|
|
65
|
+
export function getModelsByProvider(providerId) {
|
|
66
|
+
const db = getDatabase();
|
|
67
|
+
return db.prepare('SELECT * FROM models WHERE provider_id = ? ORDER BY name').all(providerId);
|
|
68
|
+
}
|
|
69
|
+
export function getModelById(id) {
|
|
70
|
+
const db = getDatabase();
|
|
71
|
+
return db.prepare('SELECT * FROM models WHERE id = ?').get(id);
|
|
72
|
+
}
|
|
73
|
+
export function getModelByModelId(providerId, modelId) {
|
|
74
|
+
const db = getDatabase();
|
|
75
|
+
return db.prepare('SELECT * FROM models WHERE provider_id = ? AND model_id = ?').get(providerId, modelId);
|
|
76
|
+
}
|
|
77
|
+
export function createModel(model) {
|
|
78
|
+
const db = getDatabase();
|
|
79
|
+
const stmt = db.prepare(`
|
|
80
|
+
INSERT INTO models (provider_id, name, model_id, enabled, updated_at)
|
|
81
|
+
VALUES (?, ?, ?, ?, datetime('now'))
|
|
82
|
+
`);
|
|
83
|
+
const result = stmt.run(model.provider_id, model.name, model.model_id, model.enabled ? 1 : 0);
|
|
84
|
+
return getModelById(result.lastInsertRowid);
|
|
85
|
+
}
|
|
86
|
+
export function updateModel(id, model) {
|
|
87
|
+
const db = getDatabase();
|
|
88
|
+
const updates = [];
|
|
89
|
+
const values = [];
|
|
90
|
+
if (model.name !== undefined) {
|
|
91
|
+
updates.push('name = ?');
|
|
92
|
+
values.push(model.name);
|
|
93
|
+
}
|
|
94
|
+
if (model.model_id !== undefined) {
|
|
95
|
+
updates.push('model_id = ?');
|
|
96
|
+
values.push(model.model_id);
|
|
97
|
+
}
|
|
98
|
+
if (model.enabled !== undefined) {
|
|
99
|
+
updates.push('enabled = ?');
|
|
100
|
+
values.push(model.enabled ? 1 : 0);
|
|
101
|
+
}
|
|
102
|
+
if (model.provider_id !== undefined) {
|
|
103
|
+
updates.push('provider_id = ?');
|
|
104
|
+
values.push(model.provider_id);
|
|
105
|
+
}
|
|
106
|
+
if (updates.length === 0) {
|
|
107
|
+
return getModelById(id);
|
|
108
|
+
}
|
|
109
|
+
updates.push("updated_at = datetime('now')");
|
|
110
|
+
values.push(id);
|
|
111
|
+
const stmt = db.prepare(`UPDATE models SET ${updates.join(', ')} WHERE id = ?`);
|
|
112
|
+
stmt.run(...values);
|
|
113
|
+
return getModelById(id);
|
|
114
|
+
}
|
|
115
|
+
export function deleteModel(id) {
|
|
116
|
+
const db = getDatabase();
|
|
117
|
+
const stmt = db.prepare('DELETE FROM models WHERE id = ?');
|
|
118
|
+
const result = stmt.run(id);
|
|
119
|
+
return result.changes > 0;
|
|
120
|
+
}
|
|
121
|
+
export function getEnabledModels() {
|
|
122
|
+
const db = getDatabase();
|
|
123
|
+
return db.prepare(`
|
|
124
|
+
SELECT m.*, p.name as provider_name, p.protocol, p.base_url, p.api_key
|
|
125
|
+
FROM models m
|
|
126
|
+
JOIN providers p ON m.provider_id = p.id
|
|
127
|
+
WHERE m.enabled = 1
|
|
128
|
+
ORDER BY m.name
|
|
129
|
+
`).all();
|
|
130
|
+
}
|
|
131
|
+
// Request log queries
|
|
132
|
+
export function createRequestLog(log) {
|
|
133
|
+
const db = getDatabase();
|
|
134
|
+
const stmt = db.prepare(`
|
|
135
|
+
INSERT INTO request_logs (
|
|
136
|
+
model_id, request_method, request_path, request_headers,
|
|
137
|
+
request_query, request_body, response_status, response_body, response_time_ms
|
|
138
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
139
|
+
`);
|
|
140
|
+
const result = stmt.run(log.model_id, log.request_method, log.request_path, log.request_headers, log.request_query, log.request_body, log.response_status, log.response_body, log.response_time_ms);
|
|
141
|
+
return db.prepare('SELECT * FROM request_logs WHERE id = ?').get(result.lastInsertRowid);
|
|
142
|
+
}
|
|
143
|
+
export function getRequestLogs(limit = 100, offset = 0, modelId) {
|
|
144
|
+
const db = getDatabase();
|
|
145
|
+
if (modelId) {
|
|
146
|
+
return db.prepare(`
|
|
147
|
+
SELECT l.*, m.name as model_name, m.model_id, p.name as provider_name
|
|
148
|
+
FROM request_logs l
|
|
149
|
+
LEFT JOIN models m ON l.model_id = m.id
|
|
150
|
+
LEFT JOIN providers p ON m.provider_id = p.id
|
|
151
|
+
WHERE l.model_id = ?
|
|
152
|
+
ORDER BY l.created_at DESC
|
|
153
|
+
LIMIT ? OFFSET ?
|
|
154
|
+
`).all(modelId, limit, offset);
|
|
155
|
+
}
|
|
156
|
+
return db.prepare(`
|
|
157
|
+
SELECT l.*, m.name as model_name, m.model_id, p.name as provider_name
|
|
158
|
+
FROM request_logs l
|
|
159
|
+
LEFT JOIN models m ON l.model_id = m.id
|
|
160
|
+
LEFT JOIN providers p ON m.provider_id = p.id
|
|
161
|
+
ORDER BY l.created_at DESC
|
|
162
|
+
LIMIT ? OFFSET ?
|
|
163
|
+
`).all(limit, offset);
|
|
164
|
+
}
|
|
165
|
+
export function getRequestLogById(id) {
|
|
166
|
+
const db = getDatabase();
|
|
167
|
+
return db.prepare(`
|
|
168
|
+
SELECT l.*, m.name as model_name, m.model_id, p.name as provider_name
|
|
169
|
+
FROM request_logs l
|
|
170
|
+
LEFT JOIN models m ON l.model_id = m.id
|
|
171
|
+
LEFT JOIN providers p ON m.provider_id = p.id
|
|
172
|
+
WHERE l.id = ?
|
|
173
|
+
`).get(id);
|
|
174
|
+
}
|
|
175
|
+
export function getRequestLogCount(modelId) {
|
|
176
|
+
const db = getDatabase();
|
|
177
|
+
if (modelId) {
|
|
178
|
+
return db.prepare('SELECT COUNT(*) as count FROM request_logs WHERE model_id = ?').get(modelId).count;
|
|
179
|
+
}
|
|
180
|
+
return db.prepare('SELECT COUNT(*) as count FROM request_logs').get().count;
|
|
181
|
+
}
|
|
182
|
+
// Config queries
|
|
183
|
+
export function getConfig(key) {
|
|
184
|
+
const db = getDatabase();
|
|
185
|
+
return db.prepare('SELECT * FROM config WHERE key = ?').get(key);
|
|
186
|
+
}
|
|
187
|
+
export function setConfig(key, value) {
|
|
188
|
+
const db = getDatabase();
|
|
189
|
+
const stmt = db.prepare(`
|
|
190
|
+
INSERT INTO config (key, value, updated_at)
|
|
191
|
+
VALUES (?, ?, datetime('now'))
|
|
192
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
193
|
+
value = excluded.value,
|
|
194
|
+
updated_at = datetime('now')
|
|
195
|
+
`);
|
|
196
|
+
stmt.run(key, value);
|
|
197
|
+
return getConfig(key);
|
|
198
|
+
}
|
|
199
|
+
export function getAllConfig() {
|
|
200
|
+
const db = getDatabase();
|
|
201
|
+
const configs = db.prepare('SELECT key, value FROM config').all();
|
|
202
|
+
const result = {};
|
|
203
|
+
for (const config of configs) {
|
|
204
|
+
result[config.key] = config.value;
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
// Service status queries
|
|
209
|
+
export function getServiceStatus() {
|
|
210
|
+
const db = getDatabase();
|
|
211
|
+
return db.prepare('SELECT * FROM service_status ORDER BY id DESC LIMIT 1').get();
|
|
212
|
+
}
|
|
213
|
+
export function setServiceStatus(status) {
|
|
214
|
+
const db = getDatabase();
|
|
215
|
+
// Delete old status records (keep only one)
|
|
216
|
+
db.prepare('DELETE FROM service_status').run();
|
|
217
|
+
// Insert new status
|
|
218
|
+
const stmt = db.prepare(`
|
|
219
|
+
INSERT INTO service_status (status, port, pid, started_at, updated_at)
|
|
220
|
+
VALUES (?, ?, ?, ?, datetime('now'))
|
|
221
|
+
`);
|
|
222
|
+
const result = stmt.run(status.status, status.port, status.pid, status.started_at);
|
|
223
|
+
return db.prepare('SELECT * FROM service_status WHERE id = ?').get(result.lastInsertRowid);
|
|
224
|
+
}
|
|
225
|
+
export function updateServiceStatus(updates) {
|
|
226
|
+
const db = getDatabase();
|
|
227
|
+
const current = getServiceStatus();
|
|
228
|
+
if (!current) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const updateFields = [];
|
|
232
|
+
const values = [];
|
|
233
|
+
if (updates.status !== undefined) {
|
|
234
|
+
updateFields.push('status = ?');
|
|
235
|
+
values.push(updates.status);
|
|
236
|
+
}
|
|
237
|
+
if (updates.port !== undefined) {
|
|
238
|
+
updateFields.push('port = ?');
|
|
239
|
+
values.push(updates.port);
|
|
240
|
+
}
|
|
241
|
+
if (updates.pid !== undefined) {
|
|
242
|
+
updateFields.push('pid = ?');
|
|
243
|
+
values.push(updates.pid);
|
|
244
|
+
}
|
|
245
|
+
if (updates.started_at !== undefined) {
|
|
246
|
+
updateFields.push('started_at = ?');
|
|
247
|
+
values.push(updates.started_at);
|
|
248
|
+
}
|
|
249
|
+
if (updateFields.length === 0) {
|
|
250
|
+
return current;
|
|
251
|
+
}
|
|
252
|
+
updateFields.push("updated_at = datetime('now')");
|
|
253
|
+
values.push(current.id);
|
|
254
|
+
const stmt = db.prepare(`UPDATE service_status SET ${updateFields.join(', ')} WHERE id = ?`);
|
|
255
|
+
stmt.run(...values);
|
|
256
|
+
return getServiceStatus();
|
|
257
|
+
}
|
|
258
|
+
export function clearServiceStatus() {
|
|
259
|
+
const db = getDatabase();
|
|
260
|
+
db.prepare('DELETE FROM service_status').run();
|
|
261
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database schema definitions
|
|
3
|
+
*/
|
|
4
|
+
export const CREATE_TABLES_SQL = `
|
|
5
|
+
-- Providers table
|
|
6
|
+
CREATE TABLE IF NOT EXISTS providers (
|
|
7
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8
|
+
name TEXT NOT NULL,
|
|
9
|
+
protocol TEXT NOT NULL CHECK(protocol IN ('openai', 'anthropic', 'gemini')),
|
|
10
|
+
base_url TEXT NOT NULL,
|
|
11
|
+
api_key TEXT NOT NULL,
|
|
12
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
13
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
-- Models table
|
|
17
|
+
CREATE TABLE IF NOT EXISTS models (
|
|
18
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
19
|
+
provider_id INTEGER NOT NULL,
|
|
20
|
+
name TEXT NOT NULL,
|
|
21
|
+
model_id TEXT NOT NULL,
|
|
22
|
+
enabled BOOLEAN DEFAULT 1,
|
|
23
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
25
|
+
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE,
|
|
26
|
+
UNIQUE(provider_id, model_id)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
-- Request logs table
|
|
30
|
+
CREATE TABLE IF NOT EXISTS request_logs (
|
|
31
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
32
|
+
model_id INTEGER NOT NULL,
|
|
33
|
+
request_method TEXT NOT NULL,
|
|
34
|
+
request_path TEXT NOT NULL,
|
|
35
|
+
request_headers TEXT,
|
|
36
|
+
request_query TEXT,
|
|
37
|
+
request_body TEXT,
|
|
38
|
+
response_status INTEGER,
|
|
39
|
+
response_body TEXT,
|
|
40
|
+
response_time_ms INTEGER,
|
|
41
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
42
|
+
FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE SET NULL
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
-- Config table
|
|
46
|
+
CREATE TABLE IF NOT EXISTS config (
|
|
47
|
+
key TEXT PRIMARY KEY,
|
|
48
|
+
value TEXT NOT NULL,
|
|
49
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- Service status table
|
|
53
|
+
CREATE TABLE IF NOT EXISTS service_status (
|
|
54
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
55
|
+
status TEXT NOT NULL CHECK(status IN ('running', 'stopped')),
|
|
56
|
+
port INTEGER NOT NULL,
|
|
57
|
+
pid INTEGER,
|
|
58
|
+
started_at DATETIME,
|
|
59
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
-- Indexes
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_models_provider_id ON models(provider_id);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_models_enabled ON models(enabled);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_request_logs_model_id ON request_logs(model_id);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_request_logs_created_at ON request_logs(created_at);
|
|
67
|
+
`;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import CryptoJS from 'crypto-js';
|
|
2
|
+
// Simple encryption key - in production, use environment variable
|
|
3
|
+
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'default-encryption-key-change-in-production';
|
|
4
|
+
export function encryptApiKey(apiKey) {
|
|
5
|
+
return CryptoJS.AES.encrypt(apiKey, ENCRYPTION_KEY).toString();
|
|
6
|
+
}
|
|
7
|
+
export function decryptApiKey(encryptedApiKey) {
|
|
8
|
+
const bytes = CryptoJS.AES.decrypt(encryptedApiKey, ENCRYPTION_KEY);
|
|
9
|
+
return bytes.toString(CryptoJS.enc.Utf8);
|
|
10
|
+
}
|
|
11
|
+
export function maskApiKey(apiKey) {
|
|
12
|
+
if (!apiKey || apiKey.length < 8) {
|
|
13
|
+
return '***';
|
|
14
|
+
}
|
|
15
|
+
return apiKey.substring(0, 4) + '***' + apiKey.substring(apiKey.length - 4);
|
|
16
|
+
}
|
|
17
|
+
export function maskToken(token) {
|
|
18
|
+
if (!token || token.length < 10) {
|
|
19
|
+
return '***';
|
|
20
|
+
}
|
|
21
|
+
return token.substring(0, 6) + '***' + token.substring(token.length - 6);
|
|
22
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone Gateway Server
|
|
3
|
+
* This server only provides API gateway functionality for external clients
|
|
4
|
+
* It does NOT include the Web UI
|
|
5
|
+
*/
|
|
6
|
+
import http from 'http';
|
|
7
|
+
import { getDatabase, closeDatabase } from '../db/database.js';
|
|
8
|
+
import { handleGatewayRequest } from './gateway.js';
|
|
9
|
+
export class GatewayServer {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.server = null;
|
|
12
|
+
this.port = options.port;
|
|
13
|
+
this.hostname = options.hostname || 'localhost';
|
|
14
|
+
this.apiKey = options.apiKey || null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Validate API key if configured
|
|
18
|
+
*/
|
|
19
|
+
validateApiKey(authHeader) {
|
|
20
|
+
if (!this.apiKey) {
|
|
21
|
+
return true; // No API key configured, allow all requests
|
|
22
|
+
}
|
|
23
|
+
if (!authHeader) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
// Support both "Bearer <key>" and direct key
|
|
27
|
+
const key = authHeader.startsWith('Bearer ')
|
|
28
|
+
? authHeader.substring(7)
|
|
29
|
+
: authHeader;
|
|
30
|
+
return key === this.apiKey;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse request body
|
|
34
|
+
*/
|
|
35
|
+
async parseBody(req) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
let body = '';
|
|
38
|
+
req.on('data', (chunk) => {
|
|
39
|
+
body += chunk.toString();
|
|
40
|
+
});
|
|
41
|
+
req.on('end', () => {
|
|
42
|
+
if (!body) {
|
|
43
|
+
resolve(null);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
resolve(JSON.parse(body));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
resolve(body);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
req.on('error', reject);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Handle gateway request
|
|
58
|
+
*/
|
|
59
|
+
async handleRequest(req, res) {
|
|
60
|
+
// CORS headers
|
|
61
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
62
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
63
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
64
|
+
// Handle preflight
|
|
65
|
+
if (req.method === 'OPTIONS') {
|
|
66
|
+
res.writeHead(200);
|
|
67
|
+
res.end();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
// Validate API key if configured
|
|
72
|
+
const authHeader = req.headers.authorization || req.headers['x-api-key'] || null;
|
|
73
|
+
if (!this.validateApiKey(authHeader)) {
|
|
74
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
75
|
+
res.end(JSON.stringify({ error: { message: 'Invalid or missing API key' } }));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Parse URL
|
|
79
|
+
const url = new URL(req.url || '/', `http://${this.hostname}:${this.port}`);
|
|
80
|
+
const pathname = url.pathname;
|
|
81
|
+
// Parse body first to get model ID
|
|
82
|
+
let body = null;
|
|
83
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
84
|
+
body = await this.parseBody(req);
|
|
85
|
+
}
|
|
86
|
+
// Get model ID from query, body, or URL path
|
|
87
|
+
// Priority: query param > body > path segment
|
|
88
|
+
let modelId = url.searchParams.get('model') ||
|
|
89
|
+
url.searchParams.get('model_id') ||
|
|
90
|
+
null;
|
|
91
|
+
// Try to get from body if not in query
|
|
92
|
+
if (!modelId && body) {
|
|
93
|
+
modelId = body.model || body.model_id || null;
|
|
94
|
+
}
|
|
95
|
+
// Try to get from URL path (e.g., /v1/models/{model_id}/...)
|
|
96
|
+
if (!modelId && pathname) {
|
|
97
|
+
const pathMatch = pathname.match(/\/(?:v1|api\/gateway)\/models\/([^\/]+)/);
|
|
98
|
+
if (pathMatch) {
|
|
99
|
+
modelId = pathMatch[1];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!modelId) {
|
|
103
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
104
|
+
res.end(JSON.stringify({
|
|
105
|
+
error: {
|
|
106
|
+
message: 'Model ID not specified. Please provide model ID in query parameter (?model=xxx), request body, or URL path.'
|
|
107
|
+
}
|
|
108
|
+
}));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Build gateway request
|
|
112
|
+
const gatewayRequest = {
|
|
113
|
+
method: req.method || 'GET',
|
|
114
|
+
path: pathname,
|
|
115
|
+
headers: req.headers,
|
|
116
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
117
|
+
body: body,
|
|
118
|
+
};
|
|
119
|
+
// Handle the request
|
|
120
|
+
const response = await handleGatewayRequest(modelId, gatewayRequest);
|
|
121
|
+
// Send response
|
|
122
|
+
res.writeHead(response.status, response.headers);
|
|
123
|
+
res.end(JSON.stringify(response.body));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.error('Gateway server error:', error);
|
|
127
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
128
|
+
res.end(JSON.stringify({
|
|
129
|
+
error: {
|
|
130
|
+
message: error.message || 'Internal server error',
|
|
131
|
+
type: 'gateway_error',
|
|
132
|
+
},
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Start the gateway server
|
|
138
|
+
*/
|
|
139
|
+
async start() {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
// Initialize database
|
|
142
|
+
try {
|
|
143
|
+
getDatabase();
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
reject(new Error(`Failed to initialize database: ${error.message}`));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.server = http.createServer((req, res) => {
|
|
150
|
+
this.handleRequest(req, res).catch((error) => {
|
|
151
|
+
console.error('Unhandled request error:', error);
|
|
152
|
+
if (!res.headersSent) {
|
|
153
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
154
|
+
res.end(JSON.stringify({ error: { message: 'Internal server error' } }));
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
this.server.listen(this.port, this.hostname, () => {
|
|
159
|
+
console.log(`✓ Gateway server ready on http://${this.hostname}:${this.port}`);
|
|
160
|
+
console.log(` API Gateway: http://${this.hostname}:${this.port}/api/gateway`);
|
|
161
|
+
if (this.apiKey) {
|
|
162
|
+
console.log(` API Key authentication: Enabled`);
|
|
163
|
+
}
|
|
164
|
+
resolve();
|
|
165
|
+
});
|
|
166
|
+
this.server.on('error', (error) => {
|
|
167
|
+
if (error.code === 'EADDRINUSE') {
|
|
168
|
+
reject(new Error(`Port ${this.port} is already in use`));
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
reject(error);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
// Graceful shutdown
|
|
175
|
+
process.on('SIGTERM', () => {
|
|
176
|
+
this.stop();
|
|
177
|
+
});
|
|
178
|
+
process.on('SIGINT', () => {
|
|
179
|
+
this.stop();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Stop the gateway server
|
|
185
|
+
*/
|
|
186
|
+
async stop() {
|
|
187
|
+
return new Promise((resolve) => {
|
|
188
|
+
if (this.server) {
|
|
189
|
+
this.server.close(() => {
|
|
190
|
+
console.log('Gateway server closed');
|
|
191
|
+
closeDatabase();
|
|
192
|
+
resolve();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
resolve();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getEnabledModels } from '../db/queries.js';
|
|
2
|
+
import { getProviderAdapter } from './providers/index.js';
|
|
3
|
+
import { logRequest } from './logger.js';
|
|
4
|
+
export async function handleGatewayRequest(modelId, request) {
|
|
5
|
+
const startTime = Date.now();
|
|
6
|
+
try {
|
|
7
|
+
// Find the model
|
|
8
|
+
// First, try to find by model_id in enabled models
|
|
9
|
+
const enabledModels = getEnabledModels();
|
|
10
|
+
const model = enabledModels.find(m => m.model_id === modelId);
|
|
11
|
+
if (!model) {
|
|
12
|
+
return {
|
|
13
|
+
status: 404,
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
body: { error: { message: `Model ${modelId} not found or disabled` } },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// Get provider adapter
|
|
19
|
+
const adapter = getProviderAdapter(model.protocol);
|
|
20
|
+
// Forward the request
|
|
21
|
+
const response = await adapter.forwardRequest(model, request);
|
|
22
|
+
// Log the request
|
|
23
|
+
const responseTimeMs = Date.now() - startTime;
|
|
24
|
+
await logRequest({
|
|
25
|
+
modelId: model.id,
|
|
26
|
+
method: request.method,
|
|
27
|
+
path: request.path,
|
|
28
|
+
headers: request.headers,
|
|
29
|
+
query: request.query,
|
|
30
|
+
body: request.body,
|
|
31
|
+
}, {
|
|
32
|
+
status: response.status,
|
|
33
|
+
headers: response.headers,
|
|
34
|
+
body: response.body,
|
|
35
|
+
responseTimeMs,
|
|
36
|
+
});
|
|
37
|
+
return response;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const responseTimeMs = Date.now() - startTime;
|
|
41
|
+
const errorResponse = {
|
|
42
|
+
status: 500,
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: {
|
|
45
|
+
error: {
|
|
46
|
+
message: error.message || 'Internal server error',
|
|
47
|
+
type: 'gateway_error',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
// Try to log the error request if we have model info
|
|
52
|
+
try {
|
|
53
|
+
const enabledModels = getEnabledModels();
|
|
54
|
+
const model = enabledModels.find(m => m.model_id === modelId);
|
|
55
|
+
if (model) {
|
|
56
|
+
await logRequest({
|
|
57
|
+
modelId: model.id,
|
|
58
|
+
method: request.method,
|
|
59
|
+
path: request.path,
|
|
60
|
+
headers: request.headers,
|
|
61
|
+
query: request.query,
|
|
62
|
+
body: request.body,
|
|
63
|
+
}, {
|
|
64
|
+
status: errorResponse.status,
|
|
65
|
+
headers: errorResponse.headers,
|
|
66
|
+
body: errorResponse.body,
|
|
67
|
+
responseTimeMs,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (logError) {
|
|
72
|
+
console.error('Failed to log error request:', logError);
|
|
73
|
+
}
|
|
74
|
+
return errorResponse;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createRequestLog } from '../db/queries.js';
|
|
2
|
+
import { maskApiKey, maskToken } from './crypto.js';
|
|
3
|
+
export async function logRequest(request, response) {
|
|
4
|
+
try {
|
|
5
|
+
// Mask sensitive information
|
|
6
|
+
const maskedHeaders = maskSensitiveHeaders(request.headers);
|
|
7
|
+
const maskedBody = maskSensitiveData(request.body);
|
|
8
|
+
await createRequestLog({
|
|
9
|
+
model_id: request.modelId,
|
|
10
|
+
request_method: request.method,
|
|
11
|
+
request_path: request.path,
|
|
12
|
+
request_headers: JSON.stringify(maskedHeaders),
|
|
13
|
+
request_query: JSON.stringify(request.query),
|
|
14
|
+
request_body: JSON.stringify(maskedBody),
|
|
15
|
+
response_status: response.status,
|
|
16
|
+
response_body: JSON.stringify(response.body),
|
|
17
|
+
response_time_ms: response.responseTimeMs,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error('Failed to log request:', error);
|
|
22
|
+
// Don't throw - logging failure shouldn't break the request
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function maskSensitiveHeaders(headers) {
|
|
26
|
+
const masked = { ...headers };
|
|
27
|
+
// Mask API keys
|
|
28
|
+
if (masked['authorization']) {
|
|
29
|
+
const auth = masked['authorization'];
|
|
30
|
+
if (auth.startsWith('Bearer ')) {
|
|
31
|
+
masked['authorization'] = `Bearer ${maskToken(auth.substring(7))}`;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
masked['authorization'] = maskToken(auth);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (masked['x-api-key']) {
|
|
38
|
+
masked['x-api-key'] = maskApiKey(masked['x-api-key']);
|
|
39
|
+
}
|
|
40
|
+
if (masked['api-key']) {
|
|
41
|
+
masked['api-key'] = maskApiKey(masked['api-key']);
|
|
42
|
+
}
|
|
43
|
+
return masked;
|
|
44
|
+
}
|
|
45
|
+
function maskSensitiveData(data) {
|
|
46
|
+
if (!data || typeof data !== 'object') {
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(data)) {
|
|
50
|
+
return data.map(maskSensitiveData);
|
|
51
|
+
}
|
|
52
|
+
const masked = {};
|
|
53
|
+
for (const [key, value] of Object.entries(data)) {
|
|
54
|
+
const lowerKey = key.toLowerCase();
|
|
55
|
+
if (lowerKey.includes('api') && lowerKey.includes('key')) {
|
|
56
|
+
masked[key] = maskApiKey(String(value));
|
|
57
|
+
}
|
|
58
|
+
else if (lowerKey.includes('token')) {
|
|
59
|
+
masked[key] = maskToken(String(value));
|
|
60
|
+
}
|
|
61
|
+
else if (lowerKey === 'authorization') {
|
|
62
|
+
masked[key] = maskToken(String(value));
|
|
63
|
+
}
|
|
64
|
+
else if (typeof value === 'object' && value !== null) {
|
|
65
|
+
masked[key] = maskSensitiveData(value);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
masked[key] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return masked;
|
|
72
|
+
}
|