aicodeswitch 1.4.1 → 1.5.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.
@@ -0,0 +1,809 @@
1
+ import Database from 'better-sqlite3';
2
+ import { Level } from 'level';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import CryptoJS from 'crypto-js';
6
+ import type {
7
+ Vendor,
8
+ APIService,
9
+ Route,
10
+ Rule,
11
+ RequestLog,
12
+ AccessLog,
13
+ ErrorLog,
14
+ AppConfig,
15
+ ExportData,
16
+ Statistics,
17
+ ContentType,
18
+ ServiceBlacklistEntry,
19
+ } from '../types';
20
+
21
+ export class DatabaseManager {
22
+ private db: Database.Database;
23
+ private logDb: Level<string, string>;
24
+ private accessLogDb: Level<string, string>;
25
+ private errorLogDb: Level<string, string>;
26
+ private blacklistDb: Level<string, string>;
27
+
28
+ constructor(dataPath: string) {
29
+ this.db = new Database(path.join(dataPath, 'app.db'));
30
+ this.logDb = new Level(path.join(dataPath, 'logs'), { valueEncoding: 'json' });
31
+ this.accessLogDb = new Level(path.join(dataPath, 'access-logs'), { valueEncoding: 'json' });
32
+ this.errorLogDb = new Level(path.join(dataPath, 'error-logs'), { valueEncoding: 'json' });
33
+ this.blacklistDb = new Level(path.join(dataPath, 'service-blacklist'), { valueEncoding: 'json' });
34
+ }
35
+
36
+ async initialize() {
37
+ this.createTables();
38
+ await this.ensureDefaultConfig();
39
+ }
40
+
41
+ private createTables() {
42
+ this.db.exec(`
43
+ CREATE TABLE IF NOT EXISTS vendors (
44
+ id TEXT PRIMARY KEY,
45
+ name TEXT NOT NULL,
46
+ description TEXT,
47
+ created_at INTEGER NOT NULL,
48
+ updated_at INTEGER NOT NULL
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS api_services (
52
+ id TEXT PRIMARY KEY,
53
+ vendor_id TEXT NOT NULL,
54
+ name TEXT NOT NULL,
55
+ api_url TEXT NOT NULL,
56
+ api_key TEXT NOT NULL,
57
+ timeout INTEGER,
58
+ source_type TEXT,
59
+ supported_models TEXT,
60
+ created_at INTEGER NOT NULL,
61
+ updated_at INTEGER NOT NULL,
62
+ FOREIGN KEY (vendor_id) REFERENCES vendors(id) ON DELETE CASCADE
63
+ );
64
+
65
+ CREATE TABLE IF NOT EXISTS routes (
66
+ id TEXT PRIMARY KEY,
67
+ name TEXT NOT NULL,
68
+ description TEXT,
69
+ target_type TEXT NOT NULL CHECK(target_type IN ('claude-code', 'codex')),
70
+ is_active INTEGER DEFAULT 0,
71
+ created_at INTEGER NOT NULL,
72
+ updated_at INTEGER NOT NULL
73
+ );
74
+
75
+ CREATE TABLE IF NOT EXISTS rules (
76
+ id TEXT PRIMARY KEY,
77
+ route_id TEXT NOT NULL,
78
+ content_type TEXT NOT NULL CHECK(content_type IN ('default', 'background', 'thinking', 'long-context', 'image-understanding', 'model-mapping')),
79
+ target_service_id TEXT NOT NULL,
80
+ target_model TEXT,
81
+ replaced_model TEXT,
82
+ sort_order INTEGER DEFAULT 0,
83
+ created_at INTEGER NOT NULL,
84
+ updated_at INTEGER NOT NULL,
85
+ FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE,
86
+ FOREIGN KEY (target_service_id) REFERENCES api_services(id) ON DELETE CASCADE
87
+ );
88
+
89
+ CREATE TABLE IF NOT EXISTS config (
90
+ key TEXT PRIMARY KEY,
91
+ value TEXT NOT NULL
92
+ );
93
+ `);
94
+ }
95
+
96
+ private async ensureDefaultConfig() {
97
+ const config = this.db.prepare('SELECT * FROM config WHERE key = ?').get('app_config');
98
+ if (!config) {
99
+ const defaultConfig: AppConfig = {
100
+ enableLogging: true,
101
+ logRetentionDays: 7,
102
+ maxLogSize: 1000,
103
+ apiKey: '',
104
+ enableFailover: true, // 默认启用智能故障切换
105
+ };
106
+ this.db.prepare('INSERT INTO config (key, value) VALUES (?, ?)').run(
107
+ 'app_config',
108
+ JSON.stringify(defaultConfig)
109
+ );
110
+ }
111
+ }
112
+
113
+ // Vendor operations
114
+ getVendors(): Vendor[] {
115
+ const rows = this.db.prepare('SELECT * FROM vendors ORDER BY created_at DESC').all();
116
+ return rows.map((row: any) => ({
117
+ id: row.id,
118
+ name: row.name,
119
+ description: row.description,
120
+ createdAt: row.created_at,
121
+ updatedAt: row.updated_at,
122
+ }));
123
+ }
124
+
125
+ createVendor(vendor: Omit<Vendor, 'id' | 'createdAt' | 'updatedAt'>): Vendor {
126
+ const id = crypto.randomUUID();
127
+ const now = Date.now();
128
+ this.db
129
+ .prepare('INSERT INTO vendors (id, name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?)')
130
+ .run(id, vendor.name, vendor.description || null, now, now);
131
+ return { ...vendor, id, createdAt: now, updatedAt: now };
132
+ }
133
+
134
+ updateVendor(id: string, vendor: Partial<Vendor>): boolean {
135
+ const now = Date.now();
136
+ const result = this.db
137
+ .prepare('UPDATE vendors SET name = ?, description = ?, updated_at = ? WHERE id = ?')
138
+ .run(vendor.name, vendor.description || null, now, id);
139
+ return result.changes > 0;
140
+ }
141
+
142
+ deleteVendor(id: string): boolean {
143
+ const result = this.db.prepare('DELETE FROM vendors WHERE id = ?').run(id);
144
+ return result.changes > 0;
145
+ }
146
+
147
+ // API Service operations
148
+ getAPIServices(vendorId?: string): APIService[] {
149
+ const query = vendorId
150
+ ? 'SELECT * FROM api_services WHERE vendor_id = ? ORDER BY created_at DESC'
151
+ : 'SELECT * FROM api_services ORDER BY created_at DESC';
152
+ const stmt = vendorId ? this.db.prepare(query).bind(vendorId) : this.db.prepare(query);
153
+ const rows = stmt.all();
154
+ return rows.map((row: any) => ({
155
+ id: row.id,
156
+ vendorId: row.vendor_id,
157
+ name: row.name,
158
+ apiUrl: row.api_url,
159
+ apiKey: row.api_key,
160
+
161
+ timeout: row.timeout,
162
+ sourceType: row.source_type,
163
+ supportedModels: row.supported_models ? row.supported_models.split(',').map((model: string) => model.trim()).filter((model: string) => model.length > 0) : undefined,
164
+ createdAt: row.created_at,
165
+ updatedAt: row.updated_at,
166
+ }));
167
+ }
168
+
169
+ createAPIService(service: Omit<APIService, 'id' | 'createdAt' | 'updatedAt'>): APIService {
170
+ const id = crypto.randomUUID();
171
+ const now = Date.now();
172
+ this.db
173
+ .prepare(
174
+ 'INSERT INTO api_services (id, vendor_id, name, api_url, api_key, timeout, source_type, supported_models, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
175
+ )
176
+ .run(
177
+ id,
178
+ service.vendorId,
179
+ service.name,
180
+ service.apiUrl,
181
+ service.apiKey,
182
+ service.timeout || null,
183
+ service.sourceType || null,
184
+ service.supportedModels ? service.supportedModels.join(',') : null,
185
+ now,
186
+ now
187
+ );
188
+ return { ...service, id, createdAt: now, updatedAt: now };
189
+ }
190
+
191
+ updateAPIService(id: string, service: Partial<APIService>): boolean {
192
+ const now = Date.now();
193
+ const result = this.db
194
+ .prepare(
195
+ 'UPDATE api_services SET name = ?, api_url = ?, api_key = ?, timeout = ?, source_type = ?, supported_models = ?, updated_at = ? WHERE id = ?'
196
+ )
197
+ .run(
198
+ service.name,
199
+ service.apiUrl,
200
+ service.apiKey,
201
+ service.timeout || null,
202
+ service.sourceType || null,
203
+ service.supportedModels ? service.supportedModels.join(',') : null,
204
+ now,
205
+ id
206
+ );
207
+ return result.changes > 0;
208
+ }
209
+
210
+ deleteAPIService(id: string): boolean {
211
+ const result = this.db.prepare('DELETE FROM api_services WHERE id = ?').run(id);
212
+ return result.changes > 0;
213
+ }
214
+
215
+ // Route operations
216
+ getRoutes(): Route[] {
217
+ const rows = this.db.prepare('SELECT * FROM routes ORDER BY created_at DESC').all();
218
+ return rows.map((row: any) => ({
219
+ id: row.id,
220
+ name: row.name,
221
+ description: row.description,
222
+ targetType: row.target_type,
223
+ isActive: row.is_active === 1,
224
+ createdAt: row.created_at,
225
+ updatedAt: row.updated_at,
226
+ }));
227
+ }
228
+
229
+ createRoute(route: Omit<Route, 'id' | 'createdAt' | 'updatedAt'>): Route {
230
+ const id = crypto.randomUUID();
231
+ const now = Date.now();
232
+ this.db
233
+ .prepare('INSERT INTO routes (id, name, description, target_type, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
234
+ .run(id, route.name, route.description || null, route.targetType, route.isActive ? 1 : 0, now, now);
235
+ return { ...route, id, createdAt: now, updatedAt: now };
236
+ }
237
+
238
+ updateRoute(id: string, route: Partial<Route>): boolean {
239
+ const now = Date.now();
240
+ const result = this.db
241
+ .prepare('UPDATE routes SET name = ?, description = ?, target_type = ?, updated_at = ? WHERE id = ?')
242
+ .run(route.name, route.description || null, route.targetType, now, id);
243
+ return result.changes > 0;
244
+ }
245
+
246
+ deleteRoute(id: string): boolean {
247
+ const result = this.db.prepare('DELETE FROM routes WHERE id = ?').run(id);
248
+ return result.changes > 0;
249
+ }
250
+
251
+ activateRoute(id: string): boolean {
252
+ const route = this.getRoutes().find(r => r.id === id);
253
+ if (route) {
254
+ this.db.prepare('UPDATE routes SET is_active = 0 WHERE target_type = ?').run(route.targetType);
255
+ const result = this.db.prepare('UPDATE routes SET is_active = 1 WHERE id = ?').run(id);
256
+ return result.changes > 0;
257
+ }
258
+ return false;
259
+ }
260
+
261
+ deactivateRoute(id: string): boolean {
262
+ const result = this.db.prepare('UPDATE routes SET is_active = 0 WHERE id = ?').run(id);
263
+ return result.changes > 0;
264
+ }
265
+
266
+ // Rule operations
267
+ getRules(routeId?: string): Rule[] {
268
+ const query = routeId
269
+ ? 'SELECT * FROM rules WHERE route_id = ? ORDER BY sort_order DESC, created_at DESC'
270
+ : 'SELECT * FROM rules ORDER BY sort_order DESC, created_at DESC';
271
+ const stmt = routeId ? this.db.prepare(query).bind(routeId) : this.db.prepare(query);
272
+ const rows = stmt.all();
273
+ return rows.map((row: any) => ({
274
+ id: row.id,
275
+ routeId: row.route_id,
276
+ contentType: row.content_type,
277
+ targetServiceId: row.target_service_id,
278
+ targetModel: row.target_model,
279
+ replacedModel: row.replaced_model,
280
+ sortOrder: row.sort_order,
281
+ createdAt: row.created_at,
282
+ updatedAt: row.updated_at,
283
+ }));
284
+ }
285
+
286
+ createRule(route: Omit<Rule, 'id' | 'createdAt' | 'updatedAt'>): Rule {
287
+ const id = crypto.randomUUID();
288
+ const now = Date.now();
289
+ this.db
290
+ .prepare(
291
+ 'INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
292
+ )
293
+ .run(
294
+ id,
295
+ route.routeId,
296
+ route.contentType,
297
+ route.targetServiceId,
298
+ route.targetModel || null,
299
+ route.replacedModel || null,
300
+ route.sortOrder || 0,
301
+ now,
302
+ now
303
+ );
304
+ return { ...route, id, createdAt: now, updatedAt: now };
305
+ }
306
+
307
+ updateRule(id: string, route: Partial<Rule>): boolean {
308
+ const now = Date.now();
309
+ const result = this.db
310
+ .prepare(
311
+ 'UPDATE rules SET content_type = ?, target_service_id = ?, target_model = ?, replaced_model = ?, sort_order = ?, updated_at = ? WHERE id = ?'
312
+ )
313
+ .run(
314
+ route.contentType,
315
+ route.targetServiceId,
316
+ route.targetModel || null,
317
+ route.replacedModel || null,
318
+ route.sortOrder || 0,
319
+ now,
320
+ id
321
+ );
322
+ return result.changes > 0;
323
+ }
324
+
325
+ deleteRule(id: string): boolean {
326
+ const result = this.db.prepare('DELETE FROM rules WHERE id = ?').run(id);
327
+ return result.changes > 0;
328
+ }
329
+
330
+ // Log operations
331
+ async addLog(log: Omit<RequestLog, 'id'>): Promise<void> {
332
+ const id = crypto.randomUUID();
333
+ await this.logDb.put(id, JSON.stringify({ ...log, id }));
334
+ }
335
+
336
+ async getLogs(limit: number = 100, offset: number = 0): Promise<RequestLog[]> {
337
+ const allLogs: RequestLog[] = [];
338
+ for await (const [, value] of this.logDb.iterator()) {
339
+ allLogs.push(JSON.parse(value));
340
+ }
341
+ // Sort by timestamp in descending order (newest first)
342
+ allLogs.sort((a, b) => b.timestamp - a.timestamp);
343
+ // Apply offset and limit
344
+ return allLogs.slice(offset, offset + limit);
345
+ }
346
+
347
+ async clearLogs(): Promise<void> {
348
+ await this.logDb.clear();
349
+ }
350
+
351
+ // Access log operations
352
+ async addAccessLog(log: Omit<AccessLog, 'id'>): Promise<string> {
353
+ const id = crypto.randomUUID();
354
+ await this.accessLogDb.put(id, JSON.stringify({ ...log, id }));
355
+ return id;
356
+ }
357
+
358
+ async updateAccessLog(id: string, data: Partial<AccessLog>): Promise<void> {
359
+ const log = await this.accessLogDb.get(id);
360
+ const updatedLog = { ...JSON.parse(log), ...data };
361
+ await this.accessLogDb.put(id, JSON.stringify(updatedLog));
362
+ }
363
+
364
+ async getAccessLogs(limit: number = 100, offset: number = 0): Promise<AccessLog[]> {
365
+ const allLogs: AccessLog[] = [];
366
+ for await (const [, value] of this.accessLogDb.iterator()) {
367
+ allLogs.push(JSON.parse(value));
368
+ }
369
+ // Sort by timestamp in descending order (newest first)
370
+ allLogs.sort((a, b) => b.timestamp - a.timestamp);
371
+ // Apply offset and limit
372
+ return allLogs.slice(offset, offset + limit);
373
+ }
374
+
375
+ async clearAccessLogs(): Promise<void> {
376
+ await this.accessLogDb.clear();
377
+ }
378
+
379
+ // Error log operations
380
+ async addErrorLog(log: Omit<ErrorLog, 'id'>): Promise<void> {
381
+ const id = crypto.randomUUID();
382
+ await this.errorLogDb.put(id, JSON.stringify({ ...log, id }));
383
+ }
384
+
385
+ async getErrorLogs(limit: number = 100, offset: number = 0): Promise<ErrorLog[]> {
386
+ const allLogs: ErrorLog[] = [];
387
+ for await (const [, value] of this.errorLogDb.iterator()) {
388
+ allLogs.push(JSON.parse(value));
389
+ }
390
+ // Sort by timestamp in descending order (newest first)
391
+ allLogs.sort((a, b) => b.timestamp - a.timestamp);
392
+ // Apply offset and limit
393
+ return allLogs.slice(offset, offset + limit);
394
+ }
395
+
396
+ async clearErrorLogs(): Promise<void> {
397
+ await this.errorLogDb.clear();
398
+ }
399
+
400
+ // Service blacklist operations
401
+ async isServiceBlacklisted(
402
+ serviceId: string,
403
+ routeId: string,
404
+ contentType: ContentType
405
+ ): Promise<boolean> {
406
+ const key = `${routeId}:${contentType}:${serviceId}`;
407
+ try {
408
+ const value = await this.blacklistDb.get(key);
409
+ const entry: ServiceBlacklistEntry = JSON.parse(value);
410
+
411
+ // 检查是否过期
412
+ if (Date.now() > entry.expiresAt) {
413
+ // 已过期,删除记录
414
+ await this.blacklistDb.del(key);
415
+ return false;
416
+ }
417
+
418
+ return true;
419
+ } catch (error: any) {
420
+ if (error.code === 'LEVEL_NOT_FOUND') {
421
+ return false;
422
+ }
423
+ throw error;
424
+ }
425
+ }
426
+
427
+ async addToBlacklist(
428
+ serviceId: string,
429
+ routeId: string,
430
+ contentType: ContentType,
431
+ errorMessage?: string,
432
+ statusCode?: number
433
+ ): Promise<void> {
434
+ const key = `${routeId}:${contentType}:${serviceId}`;
435
+ const now = Date.now();
436
+
437
+ try {
438
+ // 尝试读取现有记录
439
+ const existing = await this.blacklistDb.get(key);
440
+ const entry: ServiceBlacklistEntry = JSON.parse(existing);
441
+
442
+ // 更新现有记录
443
+ entry.blacklistedAt = now;
444
+ entry.expiresAt = now + 10 * 60 * 1000; // 10分钟
445
+ entry.errorCount++;
446
+ entry.lastError = errorMessage;
447
+ entry.lastStatusCode = statusCode;
448
+
449
+ await this.blacklistDb.put(key, JSON.stringify(entry));
450
+ } catch (error: any) {
451
+ if (error.code === 'LEVEL_NOT_FOUND') {
452
+ // 创建新记录
453
+ const entry: ServiceBlacklistEntry = {
454
+ serviceId,
455
+ routeId,
456
+ contentType,
457
+ blacklistedAt: now,
458
+ expiresAt: now + 10 * 60 * 1000,
459
+ errorCount: 1,
460
+ lastError: errorMessage,
461
+ lastStatusCode: statusCode,
462
+ };
463
+
464
+ await this.blacklistDb.put(key, JSON.stringify(entry));
465
+ } else {
466
+ throw error;
467
+ }
468
+ }
469
+ }
470
+
471
+ async cleanupExpiredBlacklist(): Promise<number> {
472
+ const now = Date.now();
473
+ let count = 0;
474
+
475
+ for await (const [key, value] of this.blacklistDb.iterator()) {
476
+ const entry: ServiceBlacklistEntry = JSON.parse(value);
477
+ if (now > entry.expiresAt) {
478
+ await this.blacklistDb.del(key);
479
+ count++;
480
+ }
481
+ }
482
+
483
+ return count;
484
+ }
485
+
486
+ // Config operations
487
+ getConfig(): AppConfig {
488
+ const row: any = this.db.prepare('SELECT value FROM config WHERE key = ?').get('app_config');
489
+ return row ? JSON.parse(row.value) : null as any;
490
+ }
491
+
492
+ updateConfig(config: AppConfig): boolean {
493
+ const result = this.db
494
+ .prepare('UPDATE config SET value = ? WHERE key = ?')
495
+ .run(JSON.stringify(config), 'app_config');
496
+ return result.changes > 0;
497
+ }
498
+
499
+ // Export/Import operations
500
+ async exportData(password: string): Promise<string> {
501
+ const exportData: ExportData = {
502
+ version: '1.0.0',
503
+ exportDate: Date.now(),
504
+ vendors: this.getVendors(),
505
+ apiServices: this.getAPIServices(),
506
+ routes: this.getRoutes(),
507
+ rules: this.getRules(),
508
+ config: this.getConfig(),
509
+ };
510
+
511
+ const jsonData = JSON.stringify(exportData);
512
+ const encrypted = CryptoJS.AES.encrypt(jsonData, password).toString();
513
+ return encrypted;
514
+ }
515
+
516
+ async importData(encryptedData: string, password: string): Promise<boolean> {
517
+ try {
518
+ const decrypted = CryptoJS.AES.decrypt(encryptedData, password);
519
+ const jsonData = decrypted.toString(CryptoJS.enc.Utf8);
520
+ const importData: ExportData = JSON.parse(jsonData);
521
+
522
+ // Clear existing data
523
+ this.db.prepare('DELETE FROM rules').run();
524
+ this.db.prepare('DELETE FROM routes').run();
525
+ this.db.prepare('DELETE FROM api_services').run();
526
+ this.db.prepare('DELETE FROM vendors').run();
527
+
528
+ // Import vendors
529
+ for (const vendor of importData.vendors) {
530
+ this.db
531
+ .prepare('INSERT INTO vendors (id, name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?)')
532
+ .run(vendor.id, vendor.name, vendor.description || null, vendor.createdAt, vendor.updatedAt);
533
+ }
534
+
535
+ // Import API services
536
+ for (const service of importData.apiServices) {
537
+ this.db
538
+ .prepare(
539
+ 'INSERT INTO api_services (id, vendor_id, name, api_url, api_key, timeout, source_type, supported_models, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
540
+ )
541
+ .run(
542
+ service.id,
543
+ service.vendorId,
544
+ service.name,
545
+ service.apiUrl,
546
+ service.apiKey,
547
+ service.timeout || null,
548
+ service.sourceType || null,
549
+ service.supportedModels ? service.supportedModels.join(',') : null,
550
+ service.createdAt,
551
+ service.updatedAt
552
+ );
553
+ }
554
+
555
+ // Import routes
556
+ for (const route of importData.routes) {
557
+ this.db
558
+ .prepare('INSERT INTO routes (id, name, description, target_type, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
559
+ .run(route.id, route.name, route.description || null, route.targetType, route.isActive ? 1 : 0, route.createdAt, route.updatedAt);
560
+ }
561
+
562
+ // Import rules
563
+ for (const rule of importData.rules) {
564
+ this.db
565
+ .prepare(
566
+ 'INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
567
+ )
568
+ .run(
569
+ rule.id,
570
+ rule.routeId,
571
+ rule.contentType || 'default',
572
+ rule.targetServiceId,
573
+ rule.targetModel || null,
574
+ rule.replacedModel || null,
575
+ rule.sortOrder || 0,
576
+ rule.createdAt,
577
+ rule.updatedAt
578
+ );
579
+ }
580
+
581
+ // Update config
582
+ this.updateConfig(importData.config);
583
+
584
+ return true;
585
+ } catch (error) {
586
+ console.error('Import error:', error);
587
+ return false;
588
+ }
589
+ }
590
+
591
+ // Statistics operations
592
+ async getStatistics(days: number = 30): Promise<Statistics> {
593
+ const now = Date.now();
594
+ const startTime = now - days * 24 * 60 * 60 * 1000;
595
+
596
+ // Get all logs within the time period
597
+ const allLogs: RequestLog[] = [];
598
+ for await (const [, value] of this.logDb.iterator()) {
599
+ const log = JSON.parse(value) as RequestLog;
600
+ if (log.timestamp >= startTime) {
601
+ allLogs.push(log);
602
+ }
603
+ }
604
+
605
+ // Get all error logs
606
+ const errorLogs: ErrorLog[] = [];
607
+ const recentErrorLogs: ErrorLog[] = [];
608
+ const recentTime = now - 24 * 60 * 60 * 1000; // 24 hours ago
609
+ for await (const [, value] of this.errorLogDb.iterator()) {
610
+ const log = JSON.parse(value) as ErrorLog;
611
+ errorLogs.push(log);
612
+ if (log.timestamp >= recentTime) {
613
+ recentErrorLogs.push(log);
614
+ }
615
+ }
616
+
617
+ // Get vendors and services for mapping
618
+ const vendors = this.getVendors();
619
+ const vendorMap = new Map(vendors.map(v => [v.id, v.name]));
620
+ const services = this.getAPIServices();
621
+ const serviceMap = new Map(services.map(s => [s.id, { name: s.name, vendorId: s.vendorId }]));
622
+
623
+ // Calculate overview
624
+ const totalRequests = allLogs.length;
625
+ const successRequests = allLogs.filter(log => log.statusCode && log.statusCode >= 200 && log.statusCode < 400).length;
626
+ const totalInputTokens = allLogs.reduce((sum, log) => sum + (log.usage?.inputTokens || 0), 0);
627
+ const totalOutputTokens = allLogs.reduce((sum, log) => sum + (log.usage?.outputTokens || 0), 0);
628
+ const totalCacheReadTokens = allLogs.reduce((sum, log) => sum + (log.usage?.cacheReadInputTokens || 0), 0);
629
+ const totalTokens = allLogs.reduce((sum, log) => {
630
+ if (log.usage?.totalTokens) return sum + log.usage.totalTokens;
631
+ return sum + (log.usage?.inputTokens || 0) + (log.usage?.outputTokens || 0);
632
+ }, 0);
633
+ const avgResponseTime = allLogs.length > 0
634
+ ? allLogs.reduce((sum, log) => sum + (log.responseTime || 0), 0) / allLogs.length
635
+ : 0;
636
+ const successRate = totalRequests > 0 ? (successRequests / totalRequests) * 100 : 0;
637
+
638
+ // Calculate coding time (estimate based on tokens and requests)
639
+ // Assume average reading speed: 250 tokens/minute, coding speed: 100 tokens/minute
640
+ const totalCodingTime = Math.round(totalInputTokens / 250 + totalOutputTokens / 100);
641
+
642
+ // Group by target type
643
+ const byTargetTypeMap = new Map<string, { requests: number; tokens: number; responseTime: number }>();
644
+ for (const log of allLogs) {
645
+ const key = log.targetType || 'unknown';
646
+ if (!byTargetTypeMap.has(key)) {
647
+ byTargetTypeMap.set(key, { requests: 0, tokens: 0, responseTime: 0 });
648
+ }
649
+ const stats = byTargetTypeMap.get(key)!;
650
+ stats.requests++;
651
+ stats.tokens += log.usage?.totalTokens || (log.usage?.inputTokens || 0) + (log.usage?.outputTokens || 0);
652
+ stats.responseTime += log.responseTime || 0;
653
+ }
654
+
655
+ const byTargetType = Array.from(byTargetTypeMap.entries()).map(([targetType, stats]) => ({
656
+ targetType: targetType as any,
657
+ totalRequests: stats.requests,
658
+ totalTokens: stats.tokens,
659
+ avgResponseTime: stats.requests > 0 ? Math.round(stats.responseTime / stats.requests) : 0,
660
+ }));
661
+
662
+ // Group by vendor
663
+ const byVendorMap = new Map<string, { requests: number; tokens: number; responseTime: number }>();
664
+ for (const log of allLogs) {
665
+ const key = log.vendorId || 'unknown';
666
+ if (!byVendorMap.has(key)) {
667
+ byVendorMap.set(key, { requests: 0, tokens: 0, responseTime: 0 });
668
+ }
669
+ const stats = byVendorMap.get(key)!;
670
+ stats.requests++;
671
+ stats.tokens += log.usage?.totalTokens || (log.usage?.inputTokens || 0) + (log.usage?.outputTokens || 0);
672
+ stats.responseTime += log.responseTime || 0;
673
+ }
674
+
675
+ const byVendor = Array.from(byVendorMap.entries()).map(([vendorId, stats]) => ({
676
+ vendorId,
677
+ vendorName: vendorMap.get(vendorId) || 'Unknown',
678
+ totalRequests: stats.requests,
679
+ totalTokens: stats.tokens,
680
+ avgResponseTime: stats.requests > 0 ? Math.round(stats.responseTime / stats.requests) : 0,
681
+ }));
682
+
683
+ // Group by service
684
+ const byServiceMap = new Map<string, { requests: number; tokens: number; responseTime: number }>();
685
+ for (const log of allLogs) {
686
+ const key = log.targetServiceId || 'unknown';
687
+ if (!byServiceMap.has(key)) {
688
+ byServiceMap.set(key, { requests: 0, tokens: 0, responseTime: 0 });
689
+ }
690
+ const stats = byServiceMap.get(key)!;
691
+ stats.requests++;
692
+ stats.tokens += log.usage?.totalTokens || (log.usage?.inputTokens || 0) + (log.usage?.outputTokens || 0);
693
+ stats.responseTime += log.responseTime || 0;
694
+ }
695
+
696
+ const byService = Array.from(byServiceMap.entries()).map(([serviceId, stats]) => {
697
+ const serviceInfo = serviceMap.get(serviceId);
698
+ return {
699
+ serviceId,
700
+ serviceName: serviceInfo?.name || 'Unknown',
701
+ vendorName: serviceInfo ? vendorMap.get(serviceInfo.vendorId) || 'Unknown' : 'Unknown',
702
+ totalRequests: stats.requests,
703
+ totalTokens: stats.tokens,
704
+ avgResponseTime: stats.requests > 0 ? Math.round(stats.responseTime / stats.requests) : 0,
705
+ };
706
+ });
707
+
708
+ // Group by model
709
+ const byModelMap = new Map<string, { requests: number; tokens: number; responseTime: number }>();
710
+ for (const log of allLogs) {
711
+ const key = log.targetModel || 'unknown';
712
+ if (!byModelMap.has(key)) {
713
+ byModelMap.set(key, { requests: 0, tokens: 0, responseTime: 0 });
714
+ }
715
+ const stats = byModelMap.get(key)!;
716
+ stats.requests++;
717
+ stats.tokens += log.usage?.totalTokens || (log.usage?.inputTokens || 0) + (log.usage?.outputTokens || 0);
718
+ stats.responseTime += log.responseTime || 0;
719
+ }
720
+
721
+ const byModel = Array.from(byModelMap.entries()).map(([modelName, stats]) => ({
722
+ modelName,
723
+ totalRequests: stats.requests,
724
+ totalTokens: stats.tokens,
725
+ avgResponseTime: stats.requests > 0 ? Math.round(stats.responseTime / stats.requests) : 0,
726
+ }));
727
+
728
+ // Timeline data (by day)
729
+ const timelineMap = new Map<string, { requests: number; tokens: number; inputTokens: number; outputTokens: number }>();
730
+ for (const log of allLogs) {
731
+ const date = new Date(log.timestamp).toISOString().split('T')[0];
732
+ if (!timelineMap.has(date)) {
733
+ timelineMap.set(date, { requests: 0, tokens: 0, inputTokens: 0, outputTokens: 0 });
734
+ }
735
+ const stats = timelineMap.get(date)!;
736
+ stats.requests++;
737
+ stats.inputTokens += log.usage?.inputTokens || 0;
738
+ stats.outputTokens += log.usage?.outputTokens || 0;
739
+ stats.tokens += log.usage?.totalTokens || (log.usage?.inputTokens || 0) + (log.usage?.outputTokens || 0);
740
+ }
741
+
742
+ const timeline = Array.from(timelineMap.entries())
743
+ .map(([date, stats]) => ({
744
+ date,
745
+ totalRequests: stats.requests,
746
+ totalTokens: stats.tokens,
747
+ totalInputTokens: stats.inputTokens,
748
+ totalOutputTokens: stats.outputTokens,
749
+ }))
750
+ .sort((a, b) => a.date.localeCompare(b.date));
751
+
752
+ // Content type distribution (infer from request patterns)
753
+ const contentTypeMap = new Map<string, number>();
754
+ for (const log of allLogs) {
755
+ // Infer content type from request characteristics
756
+ let contentType = 'default';
757
+ if (log.body && (log.body.includes('image') || log.body.includes('base64'))) {
758
+ contentType = 'image-understanding';
759
+ } else if (log.requestModel && log.requestModel.toLowerCase().includes('think')) {
760
+ contentType = 'thinking';
761
+ } else if (log.usage && log.usage.inputTokens > 12000) {
762
+ contentType = 'long-context';
763
+ }
764
+
765
+ contentTypeMap.set(contentType, (contentTypeMap.get(contentType) || 0) + 1);
766
+ }
767
+
768
+ const contentTypeDistribution = Array.from(contentTypeMap.entries()).map(([contentType, count]) => ({
769
+ contentType,
770
+ count,
771
+ percentage: totalRequests > 0 ? Math.round((count / totalRequests) * 100) : 0,
772
+ }));
773
+
774
+ return {
775
+ overview: {
776
+ totalRequests,
777
+ totalTokens,
778
+ totalInputTokens,
779
+ totalOutputTokens,
780
+ totalCacheReadTokens,
781
+ totalVendors: vendors.length,
782
+ totalServices: services.length,
783
+ totalRoutes: this.getRoutes().length,
784
+ totalRules: this.getRules().length,
785
+ avgResponseTime: Math.round(avgResponseTime),
786
+ successRate: Math.round(successRate * 10) / 10,
787
+ totalCodingTime,
788
+ },
789
+ byTargetType,
790
+ byVendor,
791
+ byService,
792
+ byModel,
793
+ timeline,
794
+ contentTypeDistribution,
795
+ errors: {
796
+ totalErrors: errorLogs.length,
797
+ recentErrors: recentErrorLogs.length,
798
+ },
799
+ };
800
+ }
801
+
802
+ close() {
803
+ this.db.close();
804
+ this.logDb.close();
805
+ this.accessLogDb.close();
806
+ this.errorLogDb.close();
807
+ this.blacklistDb.close();
808
+ }
809
+ }