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.
Files changed (166) hide show
  1. package/.claude/commands/openspec/apply.md +23 -0
  2. package/.claude/commands/openspec/archive.md +27 -0
  3. package/.claude/commands/openspec/proposal.md +28 -0
  4. package/.claude/settings.local.json +12 -0
  5. package/.claude/skills/ui-ux-pro-max/SKILL.md +228 -0
  6. package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
  7. package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
  8. package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
  9. package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
  10. package/.claude/skills/ui-ux-pro-max/data/prompts.csv +24 -0
  11. package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  12. package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  13. package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  14. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  15. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  16. package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  17. package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  18. package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  19. package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  20. package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  21. package/.claude/skills/ui-ux-pro-max/data/styles.csv +59 -0
  22. package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
  23. package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  24. package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc +0 -0
  25. package/.claude/skills/ui-ux-pro-max/scripts/core.py +238 -0
  26. package/.claude/skills/ui-ux-pro-max/scripts/search.py +61 -0
  27. package/.cursor/commands/openspec-apply.md +23 -0
  28. package/.cursor/commands/openspec-archive.md +27 -0
  29. package/.cursor/commands/openspec-proposal.md +28 -0
  30. package/.cursor/commands/ui-ux-pro-max.md +226 -0
  31. package/.eslintrc.json +3 -0
  32. package/.shared/ui-ux-pro-max/data/charts.csv +26 -0
  33. package/.shared/ui-ux-pro-max/data/colors.csv +97 -0
  34. package/.shared/ui-ux-pro-max/data/landing.csv +31 -0
  35. package/.shared/ui-ux-pro-max/data/products.csv +97 -0
  36. package/.shared/ui-ux-pro-max/data/prompts.csv +24 -0
  37. package/.shared/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  38. package/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  39. package/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  40. package/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  41. package/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  42. package/.shared/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  43. package/.shared/ui-ux-pro-max/data/stacks/react.csv +54 -0
  44. package/.shared/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  45. package/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  46. package/.shared/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  47. package/.shared/ui-ux-pro-max/data/styles.csv +59 -0
  48. package/.shared/ui-ux-pro-max/data/typography.csv +58 -0
  49. package/.shared/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  50. package/.shared/ui-ux-pro-max/scripts/core.py +238 -0
  51. package/.shared/ui-ux-pro-max/scripts/search.py +61 -0
  52. package/AGENTS.md +18 -0
  53. package/CLAUDE.md +18 -0
  54. package/IMPLEMENTATION.md +157 -0
  55. package/LICENSE +21 -0
  56. package/README.md +165 -0
  57. package/dist/.next/types/app/api/config/route.js +52 -0
  58. package/dist/.next/types/app/api/gateway/[...path]/route.js +52 -0
  59. package/dist/.next/types/app/api/gateway/route.js +52 -0
  60. package/dist/.next/types/app/api/logs/route.js +52 -0
  61. package/dist/.next/types/app/api/models/route.js +52 -0
  62. package/dist/.next/types/app/api/providers/route.js +52 -0
  63. package/dist/.next/types/app/api/providers/test/route.js +52 -0
  64. package/dist/.next/types/app/api/service/start/route.js +52 -0
  65. package/dist/.next/types/app/api/service/status/route.js +52 -0
  66. package/dist/.next/types/app/api/service/stop/route.js +52 -0
  67. package/dist/.next/types/app/layout.js +22 -0
  68. package/dist/.next/types/app/logs/page.js +22 -0
  69. package/dist/.next/types/app/models/page.js +22 -0
  70. package/dist/.next/types/app/page.js +22 -0
  71. package/dist/.next/types/app/providers/page.js +22 -0
  72. package/dist/src/app/api/config/route.js +43 -0
  73. package/dist/src/app/api/gateway/[...path]/route.js +83 -0
  74. package/dist/src/app/api/gateway/route.js +63 -0
  75. package/dist/src/app/api/logs/route.js +34 -0
  76. package/dist/src/app/api/models/route.js +152 -0
  77. package/dist/src/app/api/providers/route.js +118 -0
  78. package/dist/src/app/api/providers/test/route.js +154 -0
  79. package/dist/src/app/api/service/start/route.js +55 -0
  80. package/dist/src/app/api/service/status/route.js +17 -0
  81. package/dist/src/app/api/service/stop/route.js +20 -0
  82. package/dist/src/app/components/ConfirmDialog.jsx +31 -0
  83. package/dist/src/app/components/Nav.jsx +45 -0
  84. package/dist/src/app/components/Toast.jsx +37 -0
  85. package/dist/src/app/components/ToastProvider.jsx +21 -0
  86. package/dist/src/app/layout.jsx +13 -0
  87. package/dist/src/app/logs/page.jsx +210 -0
  88. package/dist/src/app/models/page.jsx +291 -0
  89. package/dist/src/app/page.jsx +236 -0
  90. package/dist/src/app/providers/page.jsx +402 -0
  91. package/dist/src/cli/index.js +90 -0
  92. package/dist/src/db/database.js +69 -0
  93. package/dist/src/db/queries.js +261 -0
  94. package/dist/src/db/schema.js +67 -0
  95. package/dist/src/server/crypto.js +22 -0
  96. package/dist/src/server/gateway-server.js +200 -0
  97. package/dist/src/server/gateway.js +76 -0
  98. package/dist/src/server/logger.js +72 -0
  99. package/dist/src/server/providers/anthropic.js +52 -0
  100. package/dist/src/server/providers/gemini.js +64 -0
  101. package/dist/src/server/providers/index.js +16 -0
  102. package/dist/src/server/providers/openai.js +86 -0
  103. package/dist/src/server/providers/types.js +1 -0
  104. package/dist/src/server/service-manager.js +286 -0
  105. package/docs/TODO.md +19 -0
  106. package/next.config.js +7 -0
  107. package/openspec/AGENTS.md +456 -0
  108. package/openspec/changes/add-logging/proposal.md +18 -0
  109. package/openspec/changes/add-logging/specs/core/spec.md +21 -0
  110. package/openspec/changes/add-logging/tasks.md +16 -0
  111. package/openspec/changes/add-provider-test-connection/proposal.md +22 -0
  112. package/openspec/changes/add-provider-test-connection/specs/model-provider/spec.md +68 -0
  113. package/openspec/changes/add-provider-test-connection/tasks.md +31 -0
  114. package/openspec/changes/improve-gateway-startup/design.md +137 -0
  115. package/openspec/changes/improve-gateway-startup/proposal.md +33 -0
  116. package/openspec/changes/improve-gateway-startup/specs/api-gateway/spec.md +94 -0
  117. package/openspec/changes/improve-gateway-startup/specs/web-ui/spec.md +67 -0
  118. package/openspec/changes/improve-gateway-startup/tasks.md +47 -0
  119. package/openspec/changes/init-api-gateway/design.md +185 -0
  120. package/openspec/changes/init-api-gateway/proposal.md +30 -0
  121. package/openspec/changes/init-api-gateway/specs/api-gateway/spec.md +42 -0
  122. package/openspec/changes/init-api-gateway/specs/cli-tool/spec.md +40 -0
  123. package/openspec/changes/init-api-gateway/specs/model-management/spec.md +47 -0
  124. package/openspec/changes/init-api-gateway/specs/model-provider/spec.md +33 -0
  125. package/openspec/changes/init-api-gateway/specs/request-logging/spec.md +54 -0
  126. package/openspec/changes/init-api-gateway/specs/web-ui/spec.md +49 -0
  127. package/openspec/changes/init-api-gateway/tasks.md +84 -0
  128. package/openspec/project.md +58 -0
  129. package/package.json +51 -0
  130. package/postcss.config.js +6 -0
  131. package/src/app/api/config/route.ts +62 -0
  132. package/src/app/api/gateway/[...path]/route.ts +118 -0
  133. package/src/app/api/gateway/route.ts +77 -0
  134. package/src/app/api/logs/route.ts +48 -0
  135. package/src/app/api/models/route.ts +210 -0
  136. package/src/app/api/providers/route.ts +162 -0
  137. package/src/app/api/providers/test/route.ts +182 -0
  138. package/src/app/api/service/start/route.ts +73 -0
  139. package/src/app/api/service/status/route.ts +22 -0
  140. package/src/app/api/service/stop/route.ts +27 -0
  141. package/src/app/components/ConfirmDialog.tsx +63 -0
  142. package/src/app/components/Nav.tsx +66 -0
  143. package/src/app/components/Toast.tsx +61 -0
  144. package/src/app/components/ToastProvider.tsx +43 -0
  145. package/src/app/globals.css +71 -0
  146. package/src/app/layout.tsx +22 -0
  147. package/src/app/logs/page.tsx +261 -0
  148. package/src/app/models/page.tsx +500 -0
  149. package/src/app/page.tsx +742 -0
  150. package/src/app/providers/page.tsx +558 -0
  151. package/src/cli/index.ts +95 -0
  152. package/src/db/database.ts +125 -0
  153. package/src/db/queries.ts +339 -0
  154. package/src/db/schema.ts +117 -0
  155. package/src/server/crypto.ts +48 -0
  156. package/src/server/gateway-server.ts +306 -0
  157. package/src/server/gateway.ts +163 -0
  158. package/src/server/logger.ts +96 -0
  159. package/src/server/providers/anthropic.ts +121 -0
  160. package/src/server/providers/gemini.ts +112 -0
  161. package/src/server/providers/index.ts +20 -0
  162. package/src/server/providers/openai.ts +235 -0
  163. package/src/server/providers/types.ts +20 -0
  164. package/src/server/service-manager.ts +321 -0
  165. package/tailwind.config.js +16 -0
  166. package/tsconfig.json +29 -0
@@ -0,0 +1,125 @@
1
+ import Database from 'better-sqlite3';
2
+ import { CREATE_TABLES_SQL } from './schema';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import os from 'os';
6
+
7
+ const DB_PATH = process.env.DB_PATH || path.join(os.homedir(), '.aar', 'gateway.db');
8
+
9
+ let dbInstance: Database.Database | null = null;
10
+
11
+ export function getDatabase(): Database.Database {
12
+ if (!dbInstance) {
13
+ try {
14
+ // Ensure directory exists
15
+ const dbDir = path.dirname(DB_PATH);
16
+ if (!fs.existsSync(dbDir)) {
17
+ fs.mkdirSync(dbDir, { recursive: true });
18
+ }
19
+
20
+ dbInstance = new Database(DB_PATH, {
21
+ // Add timeout for busy operations to avoid blocking
22
+ timeout: 5000,
23
+ });
24
+
25
+ // Enable WAL mode for better concurrency
26
+ dbInstance.pragma('journal_mode = WAL');
27
+ // Set busy timeout to handle concurrent access
28
+ dbInstance.pragma('busy_timeout = 5000');
29
+
30
+ // Initialize schema with error handling for concurrent access
31
+ try {
32
+ dbInstance.exec(CREATE_TABLES_SQL);
33
+
34
+ // Migration: Allow NULL model_id in request_logs table (for gateway requests)
35
+ // SQLite doesn't support ALTER COLUMN, so we need to recreate the table
36
+ try {
37
+ // Check if request_logs table exists and has NOT NULL constraint
38
+ const tableInfo = dbInstance.prepare("PRAGMA table_info(request_logs)").all() as any[];
39
+ const modelIdColumn = tableInfo.find(col => col.name === 'model_id');
40
+
41
+ if (modelIdColumn && modelIdColumn.notnull === 1) {
42
+ // Table exists with NOT NULL constraint, need to migrate
43
+ console.log('[Database] Migrating request_logs table to allow NULL model_id...');
44
+
45
+ // Create new table with NULL allowed
46
+ dbInstance.exec(`
47
+ CREATE TABLE IF NOT EXISTS request_logs_new (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ model_id INTEGER,
50
+ request_method TEXT NOT NULL,
51
+ request_path TEXT NOT NULL,
52
+ request_headers TEXT,
53
+ request_query TEXT,
54
+ request_body TEXT,
55
+ response_status INTEGER,
56
+ response_body TEXT,
57
+ response_time_ms INTEGER,
58
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
59
+ FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE SET NULL
60
+ );
61
+
62
+ -- Copy data from old table
63
+ INSERT INTO request_logs_new
64
+ SELECT * FROM request_logs;
65
+
66
+ -- Drop old table
67
+ DROP TABLE request_logs;
68
+
69
+ -- Rename new table
70
+ ALTER TABLE request_logs_new RENAME TO request_logs;
71
+ `);
72
+
73
+ console.log('[Database] Migration completed successfully');
74
+ }
75
+ } catch (migrationError: any) {
76
+ // Migration errors are not critical, just log them
77
+ // The table might already be migrated or in use
78
+ if (!migrationError.message.includes('no such table') &&
79
+ !migrationError.message.includes('already exists')) {
80
+ console.warn('[Database] Migration warning (non-critical):', migrationError.message);
81
+ }
82
+ }
83
+ } catch (schemaError: any) {
84
+ // Ignore "table already exists" errors (concurrent initialization)
85
+ if (!schemaError.message.includes('already exists') &&
86
+ !schemaError.message.includes('duplicate')) {
87
+ throw schemaError;
88
+ }
89
+ }
90
+ } catch (error: any) {
91
+ console.error('Database initialization error:', error);
92
+ // Don't throw if it's a busy/locked error, retry might work
93
+ if (error.message && error.message.includes('database is locked')) {
94
+ // Wait a bit and retry once
95
+ setTimeout(() => {
96
+ if (!dbInstance) {
97
+ try {
98
+ dbInstance = new Database(DB_PATH, { timeout: 5000 });
99
+ dbInstance.pragma('journal_mode = WAL');
100
+ dbInstance.pragma('busy_timeout = 5000');
101
+ } catch (retryError) {
102
+ console.error('Database retry failed:', retryError);
103
+ }
104
+ }
105
+ }, 100);
106
+ // Return a temporary instance or throw
107
+ throw new Error(`Database is temporarily locked: ${error.message}`);
108
+ }
109
+ throw new Error(`Failed to initialize database: ${error.message}`);
110
+ }
111
+ }
112
+
113
+ return dbInstance;
114
+ }
115
+
116
+ export function closeDatabase(): void {
117
+ if (dbInstance) {
118
+ dbInstance.close();
119
+ dbInstance = null;
120
+ }
121
+ }
122
+
123
+ export function getDbPath(): string {
124
+ return DB_PATH;
125
+ }
@@ -0,0 +1,339 @@
1
+ import { getDatabase } from './database';
2
+ import type { Provider, Model, RequestLog, Config, ServiceStatus } from './schema';
3
+
4
+ // Provider queries
5
+ export function getAllProviders(): Provider[] {
6
+ const db = getDatabase();
7
+ return db.prepare('SELECT * FROM providers ORDER BY created_at DESC').all() as Provider[];
8
+ }
9
+
10
+ export function getProviderById(id: number): Provider | null {
11
+ const db = getDatabase();
12
+ return db.prepare('SELECT * FROM providers WHERE id = ?').get(id) as Provider | null;
13
+ }
14
+
15
+ export function createProvider(provider: Omit<Provider, 'id' | 'created_at' | 'updated_at'>): Provider {
16
+ const db = getDatabase();
17
+ const stmt = db.prepare(`
18
+ INSERT INTO providers (name, protocol, base_url, api_key, updated_at)
19
+ VALUES (?, ?, ?, ?, datetime('now'))
20
+ `);
21
+ const result = stmt.run(provider.name, provider.protocol, provider.base_url, provider.api_key);
22
+ return getProviderById(result.lastInsertRowid as number)!;
23
+ }
24
+
25
+ export function updateProvider(id: number, provider: Partial<Omit<Provider, 'id' | 'created_at'>>): Provider | null {
26
+ const db = getDatabase();
27
+ const updates: string[] = [];
28
+ const values: any[] = [];
29
+
30
+ if (provider.name !== undefined) {
31
+ updates.push('name = ?');
32
+ values.push(provider.name);
33
+ }
34
+ if (provider.protocol !== undefined) {
35
+ updates.push('protocol = ?');
36
+ values.push(provider.protocol);
37
+ }
38
+ if (provider.base_url !== undefined) {
39
+ updates.push('base_url = ?');
40
+ values.push(provider.base_url);
41
+ }
42
+ if (provider.api_key !== undefined) {
43
+ updates.push('api_key = ?');
44
+ values.push(provider.api_key);
45
+ }
46
+
47
+ if (updates.length === 0) {
48
+ return getProviderById(id);
49
+ }
50
+
51
+ updates.push("updated_at = datetime('now')");
52
+ values.push(id);
53
+
54
+ const stmt = db.prepare(`UPDATE providers SET ${updates.join(', ')} WHERE id = ?`);
55
+ stmt.run(...values);
56
+ return getProviderById(id);
57
+ }
58
+
59
+ export function deleteProvider(id: number): boolean {
60
+ const db = getDatabase();
61
+ const stmt = db.prepare('DELETE FROM providers WHERE id = ?');
62
+ const result = stmt.run(id);
63
+ return result.changes > 0;
64
+ }
65
+
66
+ // Model queries
67
+ export function getAllModels(): Model[] {
68
+ const db = getDatabase();
69
+ return db.prepare(`
70
+ SELECT m.*, p.name as provider_name, p.protocol as provider_protocol
71
+ FROM models m
72
+ JOIN providers p ON m.provider_id = p.id
73
+ ORDER BY m.created_at DESC
74
+ `).all() as any[];
75
+ }
76
+
77
+ export function getModelsByProvider(providerId: number): Model[] {
78
+ const db = getDatabase();
79
+ return db.prepare('SELECT * FROM models WHERE provider_id = ? ORDER BY name').all(providerId) as Model[];
80
+ }
81
+
82
+ export function getModelById(id: number): Model | null {
83
+ const db = getDatabase();
84
+ return db.prepare('SELECT * FROM models WHERE id = ?').get(id) as Model | null;
85
+ }
86
+
87
+ export function getModelByModelId(providerId: number, modelId: string): Model | null {
88
+ const db = getDatabase();
89
+ return db.prepare('SELECT * FROM models WHERE provider_id = ? AND model_id = ?').get(providerId, modelId) as Model | null;
90
+ }
91
+
92
+ export function createModel(model: Omit<Model, 'id' | 'created_at' | 'updated_at'>): Model {
93
+ const db = getDatabase();
94
+ const stmt = db.prepare(`
95
+ INSERT INTO models (provider_id, name, model_id, enabled, updated_at)
96
+ VALUES (?, ?, ?, ?, datetime('now'))
97
+ `);
98
+ const result = stmt.run(model.provider_id, model.name, model.model_id, model.enabled ? 1 : 0);
99
+ return getModelById(result.lastInsertRowid as number)!;
100
+ }
101
+
102
+ export function updateModel(id: number, model: Partial<Omit<Model, 'id' | 'created_at'>>): Model | null {
103
+ const db = getDatabase();
104
+ const updates: string[] = [];
105
+ const values: any[] = [];
106
+
107
+ if (model.name !== undefined) {
108
+ updates.push('name = ?');
109
+ values.push(model.name);
110
+ }
111
+ if (model.model_id !== undefined) {
112
+ updates.push('model_id = ?');
113
+ values.push(model.model_id);
114
+ }
115
+ if (model.enabled !== undefined) {
116
+ updates.push('enabled = ?');
117
+ values.push(model.enabled ? 1 : 0);
118
+ }
119
+ if (model.provider_id !== undefined) {
120
+ updates.push('provider_id = ?');
121
+ values.push(model.provider_id);
122
+ }
123
+
124
+ if (updates.length === 0) {
125
+ return getModelById(id);
126
+ }
127
+
128
+ updates.push("updated_at = datetime('now')");
129
+ values.push(id);
130
+
131
+ const stmt = db.prepare(`UPDATE models SET ${updates.join(', ')} WHERE id = ?`);
132
+ stmt.run(...values);
133
+ return getModelById(id);
134
+ }
135
+
136
+ export function deleteModel(id: number): boolean {
137
+ const db = getDatabase();
138
+ const stmt = db.prepare('DELETE FROM models WHERE id = ?');
139
+ const result = stmt.run(id);
140
+ return result.changes > 0;
141
+ }
142
+
143
+ export function getEnabledModels(): Model[] {
144
+ const db = getDatabase();
145
+ return db.prepare(`
146
+ SELECT m.*, p.name as provider_name, p.protocol, p.base_url, p.api_key
147
+ FROM models m
148
+ JOIN providers p ON m.provider_id = p.id
149
+ WHERE m.enabled = 1
150
+ ORDER BY m.name
151
+ `).all() as any[];
152
+ }
153
+
154
+ // Request log queries
155
+ export function createRequestLog(log: Omit<RequestLog, 'id' | 'created_at'>): RequestLog {
156
+ const db = getDatabase();
157
+
158
+ // Try to insert with model_id (for regular requests)
159
+ // If model_id is null or doesn't exist, insert with NULL (for gateway requests)
160
+ const stmt = db.prepare(`
161
+ INSERT INTO request_logs (
162
+ model_id, request_method, request_path, request_headers,
163
+ request_query, request_body, response_status, response_body, response_time_ms
164
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
165
+ `);
166
+
167
+ try {
168
+ const result = stmt.run(
169
+ log.model_id || null, // Allow NULL for gateway requests
170
+ log.request_method,
171
+ log.request_path,
172
+ log.request_headers,
173
+ log.request_query,
174
+ log.request_body,
175
+ log.response_status,
176
+ log.response_body,
177
+ log.response_time_ms
178
+ );
179
+ return db.prepare('SELECT * FROM request_logs WHERE id = ?').get(result.lastInsertRowid) as RequestLog;
180
+ } catch (error: any) {
181
+ // If foreign key constraint fails, try again with NULL model_id
182
+ if (error.code === 'SQLITE_CONSTRAINT_FOREIGNKEY' && log.model_id) {
183
+ console.warn(`[RequestLog] Foreign key constraint failed for model_id ${log.model_id}, retrying with NULL`);
184
+ const result = stmt.run(
185
+ null, // Use NULL for gateway requests
186
+ log.request_method,
187
+ log.request_path,
188
+ log.request_headers,
189
+ log.request_query,
190
+ log.request_body,
191
+ log.response_status,
192
+ log.response_body,
193
+ log.response_time_ms
194
+ );
195
+ return db.prepare('SELECT * FROM request_logs WHERE id = ?').get(result.lastInsertRowid) as RequestLog;
196
+ }
197
+ throw error;
198
+ }
199
+ }
200
+
201
+ export function getRequestLogs(limit: number = 100, offset: number = 0, modelId?: number): RequestLog[] {
202
+ const db = getDatabase();
203
+ if (modelId) {
204
+ return db.prepare(`
205
+ SELECT l.*, m.name as model_name, m.model_id, p.name as provider_name
206
+ FROM request_logs l
207
+ LEFT JOIN models m ON l.model_id = m.id
208
+ LEFT JOIN providers p ON m.provider_id = p.id
209
+ WHERE l.model_id = ?
210
+ ORDER BY l.created_at DESC
211
+ LIMIT ? OFFSET ?
212
+ `).all(modelId, limit, offset) as any[];
213
+ }
214
+ return db.prepare(`
215
+ SELECT l.*, m.name as model_name, m.model_id, p.name as provider_name
216
+ FROM request_logs l
217
+ LEFT JOIN models m ON l.model_id = m.id
218
+ LEFT JOIN providers p ON m.provider_id = p.id
219
+ ORDER BY l.created_at DESC
220
+ LIMIT ? OFFSET ?
221
+ `).all(limit, offset) as any[];
222
+ }
223
+
224
+ export function getRequestLogById(id: number): RequestLog | null {
225
+ const db = getDatabase();
226
+ return db.prepare(`
227
+ SELECT l.*, m.name as model_name, m.model_id, p.name as provider_name
228
+ FROM request_logs l
229
+ LEFT JOIN models m ON l.model_id = m.id
230
+ LEFT JOIN providers p ON m.provider_id = p.id
231
+ WHERE l.id = ?
232
+ `).get(id) as any | null;
233
+ }
234
+
235
+ export function getRequestLogCount(modelId?: number): number {
236
+ const db = getDatabase();
237
+ if (modelId) {
238
+ return (db.prepare('SELECT COUNT(*) as count FROM request_logs WHERE model_id = ?').get(modelId) as any).count;
239
+ }
240
+ return (db.prepare('SELECT COUNT(*) as count FROM request_logs').get() as any).count;
241
+ }
242
+
243
+ // Config queries
244
+ export function getConfig(key: string): Config | null {
245
+ const db = getDatabase();
246
+ return db.prepare('SELECT * FROM config WHERE key = ?').get(key) as Config | null;
247
+ }
248
+
249
+ export function setConfig(key: string, value: string): Config {
250
+ const db = getDatabase();
251
+ const stmt = db.prepare(`
252
+ INSERT INTO config (key, value, updated_at)
253
+ VALUES (?, ?, datetime('now'))
254
+ ON CONFLICT(key) DO UPDATE SET
255
+ value = excluded.value,
256
+ updated_at = datetime('now')
257
+ `);
258
+ stmt.run(key, value);
259
+ return getConfig(key)!;
260
+ }
261
+
262
+ export function getAllConfig(): Record<string, string> {
263
+ const db = getDatabase();
264
+ const configs = db.prepare('SELECT key, value FROM config').all() as Config[];
265
+ const result: Record<string, string> = {};
266
+ for (const config of configs) {
267
+ result[config.key] = config.value;
268
+ }
269
+ return result;
270
+ }
271
+
272
+ // Service status queries
273
+ export function getServiceStatus(): ServiceStatus | null {
274
+ const db = getDatabase();
275
+ return db.prepare('SELECT * FROM service_status ORDER BY id DESC LIMIT 1').get() as ServiceStatus | null;
276
+ }
277
+
278
+ export function setServiceStatus(status: Omit<ServiceStatus, 'id' | 'updated_at'>): ServiceStatus {
279
+ const db = getDatabase();
280
+ // Delete old status records (keep only one)
281
+ db.prepare('DELETE FROM service_status').run();
282
+
283
+ // Insert new status
284
+ const stmt = db.prepare(`
285
+ INSERT INTO service_status (status, port, pid, started_at, updated_at)
286
+ VALUES (?, ?, ?, ?, datetime('now'))
287
+ `);
288
+ const result = stmt.run(
289
+ status.status,
290
+ status.port,
291
+ status.pid,
292
+ status.started_at
293
+ );
294
+ return db.prepare('SELECT * FROM service_status WHERE id = ?').get(result.lastInsertRowid) as ServiceStatus;
295
+ }
296
+
297
+ export function updateServiceStatus(updates: Partial<Omit<ServiceStatus, 'id' | 'updated_at'>>): ServiceStatus | null {
298
+ const db = getDatabase();
299
+ const current = getServiceStatus();
300
+ if (!current) {
301
+ return null;
302
+ }
303
+
304
+ const updateFields: string[] = [];
305
+ const values: any[] = [];
306
+
307
+ if (updates.status !== undefined) {
308
+ updateFields.push('status = ?');
309
+ values.push(updates.status);
310
+ }
311
+ if (updates.port !== undefined) {
312
+ updateFields.push('port = ?');
313
+ values.push(updates.port);
314
+ }
315
+ if (updates.pid !== undefined) {
316
+ updateFields.push('pid = ?');
317
+ values.push(updates.pid);
318
+ }
319
+ if (updates.started_at !== undefined) {
320
+ updateFields.push('started_at = ?');
321
+ values.push(updates.started_at);
322
+ }
323
+
324
+ if (updateFields.length === 0) {
325
+ return current;
326
+ }
327
+
328
+ updateFields.push("updated_at = datetime('now')");
329
+ values.push(current.id);
330
+
331
+ const stmt = db.prepare(`UPDATE service_status SET ${updateFields.join(', ')} WHERE id = ?`);
332
+ stmt.run(...values);
333
+ return getServiceStatus();
334
+ }
335
+
336
+ export function clearServiceStatus(): void {
337
+ const db = getDatabase();
338
+ db.prepare('DELETE FROM service_status').run();
339
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Database schema definitions
3
+ */
4
+
5
+ export interface Provider {
6
+ id: number;
7
+ name: string;
8
+ protocol: 'openai' | 'anthropic' | 'gemini';
9
+ base_url: string;
10
+ api_key: string; // encrypted
11
+ created_at: string;
12
+ updated_at: string;
13
+ }
14
+
15
+ export interface Model {
16
+ id: number;
17
+ provider_id: number;
18
+ name: string;
19
+ model_id: string;
20
+ enabled: boolean;
21
+ created_at: string;
22
+ updated_at: string;
23
+ }
24
+
25
+ export interface RequestLog {
26
+ id: number;
27
+ model_id: number;
28
+ request_method: string;
29
+ request_path: string;
30
+ request_headers: string; // JSON string
31
+ request_query: string; // JSON string
32
+ request_body: string; // JSON string
33
+ response_status: number;
34
+ response_body: string; // JSON string
35
+ response_time_ms: number;
36
+ created_at: string;
37
+ }
38
+
39
+ export interface Config {
40
+ key: string;
41
+ value: string; // JSON string
42
+ updated_at: string;
43
+ }
44
+
45
+ export interface ServiceStatus {
46
+ id: number;
47
+ status: 'running' | 'stopped';
48
+ port: number;
49
+ pid: number | null;
50
+ started_at: string | null;
51
+ updated_at: string;
52
+ }
53
+
54
+ export const CREATE_TABLES_SQL = `
55
+ -- Providers table
56
+ CREATE TABLE IF NOT EXISTS providers (
57
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ name TEXT NOT NULL,
59
+ protocol TEXT NOT NULL CHECK(protocol IN ('openai', 'anthropic', 'gemini')),
60
+ base_url TEXT NOT NULL,
61
+ api_key TEXT NOT NULL,
62
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
63
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
64
+ );
65
+
66
+ -- Models table
67
+ CREATE TABLE IF NOT EXISTS models (
68
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
69
+ provider_id INTEGER NOT NULL,
70
+ name TEXT NOT NULL,
71
+ model_id TEXT NOT NULL,
72
+ enabled BOOLEAN DEFAULT 1,
73
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
74
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
75
+ FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE,
76
+ UNIQUE(provider_id, model_id)
77
+ );
78
+
79
+ -- Request logs table
80
+ CREATE TABLE IF NOT EXISTS request_logs (
81
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
82
+ model_id INTEGER,
83
+ request_method TEXT NOT NULL,
84
+ request_path TEXT NOT NULL,
85
+ request_headers TEXT,
86
+ request_query TEXT,
87
+ request_body TEXT,
88
+ response_status INTEGER,
89
+ response_body TEXT,
90
+ response_time_ms INTEGER,
91
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
92
+ FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE SET NULL
93
+ );
94
+
95
+ -- Config table
96
+ CREATE TABLE IF NOT EXISTS config (
97
+ key TEXT PRIMARY KEY,
98
+ value TEXT NOT NULL,
99
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
100
+ );
101
+
102
+ -- Service status table
103
+ CREATE TABLE IF NOT EXISTS service_status (
104
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
105
+ status TEXT NOT NULL CHECK(status IN ('running', 'stopped')),
106
+ port INTEGER NOT NULL,
107
+ pid INTEGER,
108
+ started_at DATETIME,
109
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
110
+ );
111
+
112
+ -- Indexes
113
+ CREATE INDEX IF NOT EXISTS idx_models_provider_id ON models(provider_id);
114
+ CREATE INDEX IF NOT EXISTS idx_models_enabled ON models(enabled);
115
+ CREATE INDEX IF NOT EXISTS idx_request_logs_model_id ON request_logs(model_id);
116
+ CREATE INDEX IF NOT EXISTS idx_request_logs_created_at ON request_logs(created_at);
117
+ `;
@@ -0,0 +1,48 @@
1
+ import CryptoJS from 'crypto-js';
2
+
3
+ // Simple encryption key - in production, use environment variable
4
+ const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'default-encryption-key-change-in-production';
5
+
6
+ export function encryptApiKey(apiKey: string): string {
7
+ return CryptoJS.AES.encrypt(apiKey, ENCRYPTION_KEY).toString();
8
+ }
9
+
10
+ export function decryptApiKey(encryptedApiKey: string): string {
11
+ if (!encryptedApiKey || encryptedApiKey.trim() === '') {
12
+ throw new Error('Encrypted API key is empty');
13
+ }
14
+
15
+ try {
16
+ const bytes = CryptoJS.AES.decrypt(encryptedApiKey, ENCRYPTION_KEY);
17
+ const decrypted = bytes.toString(CryptoJS.enc.Utf8);
18
+
19
+ if (!decrypted || decrypted.trim() === '') {
20
+ // If decryption returns empty, the key might not be encrypted
21
+ // Try to return the original value (for backward compatibility with unencrypted keys)
22
+ console.warn('[Crypto] Decryption returned empty string, key might not be encrypted');
23
+ return encryptedApiKey;
24
+ }
25
+
26
+ return decrypted;
27
+ } catch (error: any) {
28
+ console.error('[Crypto] Decryption error:', error.message);
29
+ // If decryption fails, the key might not be encrypted (backward compatibility)
30
+ // Return the original value and let the API call fail with proper error
31
+ console.warn('[Crypto] Decryption failed, assuming key is not encrypted (backward compatibility)');
32
+ return encryptedApiKey;
33
+ }
34
+ }
35
+
36
+ export function maskApiKey(apiKey: string): string {
37
+ if (!apiKey || apiKey.length < 8) {
38
+ return '***';
39
+ }
40
+ return apiKey.substring(0, 4) + '***' + apiKey.substring(apiKey.length - 4);
41
+ }
42
+
43
+ export function maskToken(token: string): string {
44
+ if (!token || token.length < 10) {
45
+ return '***';
46
+ }
47
+ return token.substring(0, 6) + '***' + token.substring(token.length - 6);
48
+ }