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,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
|
+
}
|
package/src/db/schema.ts
ADDED
|
@@ -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
|
+
}
|