ldrouter 1.5.1
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/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/cli.js +13 -0
- package/dist/server/app.js +138 -0
- package/dist/server/auth/api-key.js +75 -0
- package/dist/server/auth/crypto.js +94 -0
- package/dist/server/auth/ids.js +40 -0
- package/dist/server/auth/middleware.js +36 -0
- package/dist/server/auth/recovery.js +11 -0
- package/dist/server/caching/store.js +119 -0
- package/dist/server/config/index.js +96 -0
- package/dist/server/db/index.js +64 -0
- package/dist/server/db/migrate.js +408 -0
- package/dist/server/db/repositories/audit.js +75 -0
- package/dist/server/db/repositories/settings.js +63 -0
- package/dist/server/db/schema.js +396 -0
- package/dist/server/errors.js +65 -0
- package/dist/server/gateway/runner.js +745 -0
- package/dist/server/logging/logger.js +35 -0
- package/dist/server/maintenance/retention.js +48 -0
- package/dist/server/metrics/registry.js +169 -0
- package/dist/server/protocols/anthropic.js +154 -0
- package/dist/server/protocols/canonical.js +201 -0
- package/dist/server/providers/index.js +89 -0
- package/dist/server/routes/admin/aliases.js +98 -0
- package/dist/server/routes/admin/api-keys.js +194 -0
- package/dist/server/routes/admin/audit.js +19 -0
- package/dist/server/routes/admin/auth.js +124 -0
- package/dist/server/routes/admin/backup.js +113 -0
- package/dist/server/routes/admin/combos.js +198 -0
- package/dist/server/routes/admin/dashboard.js +55 -0
- package/dist/server/routes/admin/models.js +178 -0
- package/dist/server/routes/admin/providers.js +212 -0
- package/dist/server/routes/admin/requests.js +156 -0
- package/dist/server/routes/admin/settings.js +197 -0
- package/dist/server/routes/admin/setup.js +80 -0
- package/dist/server/routes/admin/stats.js +180 -0
- package/dist/server/routes/admin.js +39 -0
- package/dist/server/routes/gateway/anthropic.js +112 -0
- package/dist/server/routes/gateway/openai.js +257 -0
- package/dist/server/routes/gateway.js +7 -0
- package/dist/server/routes/health.js +27 -0
- package/dist/server/routing/capabilities.js +52 -0
- package/dist/server/routing/circuit.js +37 -0
- package/dist/server/routing/combo.js +100 -0
- package/dist/server/routing/quota.js +51 -0
- package/dist/server/routing/ratelimit.js +58 -0
- package/dist/server/routing/resolver.js +43 -0
- package/dist/server/security/redact.js +111 -0
- package/dist/server/selfupdate/index.js +154 -0
- package/dist/server/upstream/client.js +179 -0
- package/dist/server/util/cidr.js +91 -0
- package/dist/server/util/client-ip.js +15 -0
- package/dist/server/util/stable-json.js +19 -0
- package/dist/shared/types.js +2 -0
- package/dist/web/assets/index-COSbvF8Z.css +1 -0
- package/dist/web/assets/index-DbnEzuxq.js +251 -0
- package/dist/web/favicon.png +0 -0
- package/dist/web/index.html +15 -0
- package/dist/web/logo.png +0 -0
- package/migrations/0001_initial_schema.sql +323 -0
- package/migrations/0002_source_api_key_secrets.sql +7 -0
- package/package.json +117 -0
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<meta name="color-scheme" content="light dark" />
|
|
8
|
+
<title>LateDev Router</title>
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DbnEzuxq.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-COSbvF8Z.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
Binary file
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS app_settings (
|
|
2
|
+
id INTEGER PRIMARY KEY,
|
|
3
|
+
setup_complete INTEGER NOT NULL DEFAULT 0,
|
|
4
|
+
retention_days INTEGER NOT NULL DEFAULT 30,
|
|
5
|
+
content_log_mode TEXT NOT NULL DEFAULT 'metadata',
|
|
6
|
+
db_size_limit_mb INTEGER NOT NULL DEFAULT 2048,
|
|
7
|
+
trust_proxy_hops INTEGER NOT NULL DEFAULT 0,
|
|
8
|
+
schema_version INTEGER NOT NULL DEFAULT 0,
|
|
9
|
+
app_version TEXT NOT NULL DEFAULT '0.0.0',
|
|
10
|
+
gateway_cache_enabled INTEGER NOT NULL DEFAULT 0,
|
|
11
|
+
gateway_cache_default_ttl_seconds INTEGER NOT NULL DEFAULT 300,
|
|
12
|
+
gateway_cache_max_size_mb INTEGER NOT NULL DEFAULT 256,
|
|
13
|
+
master_key_version INTEGER NOT NULL DEFAULT 1,
|
|
14
|
+
master_key_configured INTEGER NOT NULL DEFAULT 0,
|
|
15
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS admin_account (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
username TEXT NOT NULL UNIQUE,
|
|
21
|
+
password_hash TEXT NOT NULL,
|
|
22
|
+
totp_enabled INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
totp_secret_encrypted TEXT,
|
|
24
|
+
totp_secret_nonce TEXT,
|
|
25
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
26
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
27
|
+
last_login_at TEXT
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS admin_recovery_codes (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
admin_id TEXT NOT NULL REFERENCES admin_account(id) ON DELETE CASCADE,
|
|
33
|
+
code_hash TEXT NOT NULL,
|
|
34
|
+
used_at TEXT,
|
|
35
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
36
|
+
);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_recovery_admin ON admin_recovery_codes(admin_id);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS admin_sessions (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
token_digest TEXT NOT NULL UNIQUE,
|
|
42
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
43
|
+
expires_at TEXT NOT NULL,
|
|
44
|
+
last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
45
|
+
ip TEXT,
|
|
46
|
+
user_agent TEXT
|
|
47
|
+
);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_session_expires ON admin_sessions(expires_at);
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS login_attempts (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
username TEXT NOT NULL,
|
|
53
|
+
ip TEXT NOT NULL,
|
|
54
|
+
success INTEGER NOT NULL,
|
|
55
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
56
|
+
);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_login_user_time ON login_attempts(username, created_at);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_login_ip_time ON login_attempts(ip, created_at);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE IF NOT EXISTS providers (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
name TEXT NOT NULL,
|
|
63
|
+
slug TEXT NOT NULL UNIQUE,
|
|
64
|
+
type TEXT NOT NULL CHECK (type IN ('openai','anthropic')),
|
|
65
|
+
base_url TEXT NOT NULL,
|
|
66
|
+
encrypted_api_key TEXT NOT NULL,
|
|
67
|
+
api_key_nonce TEXT NOT NULL,
|
|
68
|
+
api_key_version INTEGER NOT NULL DEFAULT 1,
|
|
69
|
+
custom_headers_encrypted TEXT,
|
|
70
|
+
custom_headers_nonce TEXT,
|
|
71
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
72
|
+
connect_timeout_ms INTEGER NOT NULL DEFAULT 10000,
|
|
73
|
+
first_token_timeout_ms INTEGER NOT NULL DEFAULT 30000,
|
|
74
|
+
stream_idle_timeout_ms INTEGER NOT NULL DEFAULT 60000,
|
|
75
|
+
total_timeout_ms INTEGER NOT NULL DEFAULT 180000,
|
|
76
|
+
max_retries INTEGER NOT NULL DEFAULT 2,
|
|
77
|
+
retry_base_ms INTEGER NOT NULL DEFAULT 500,
|
|
78
|
+
retry_max_ms INTEGER NOT NULL DEFAULT 8000,
|
|
79
|
+
cb_failure_threshold INTEGER NOT NULL DEFAULT 5,
|
|
80
|
+
cb_cooldown_seconds INTEGER NOT NULL DEFAULT 60,
|
|
81
|
+
health_state TEXT NOT NULL DEFAULT 'unknown' CHECK (health_state IN ('healthy','degraded','down','circuit_open','unknown')),
|
|
82
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
83
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE TABLE IF NOT EXISTS models (
|
|
87
|
+
id TEXT PRIMARY KEY,
|
|
88
|
+
provider_id TEXT NOT NULL REFERENCES providers(id) ON DELETE RESTRICT,
|
|
89
|
+
upstream_model_id TEXT NOT NULL,
|
|
90
|
+
public_model_id TEXT NOT NULL UNIQUE,
|
|
91
|
+
display_name TEXT NOT NULL,
|
|
92
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
93
|
+
upstream_available INTEGER NOT NULL DEFAULT 1,
|
|
94
|
+
capabilities_json TEXT NOT NULL DEFAULT '{}',
|
|
95
|
+
max_context_tokens INTEGER,
|
|
96
|
+
max_output_tokens INTEGER,
|
|
97
|
+
discovered_metadata_json TEXT,
|
|
98
|
+
cache_override_enabled INTEGER,
|
|
99
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
100
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
101
|
+
last_seen_upstream_at TEXT
|
|
102
|
+
);
|
|
103
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_model_public ON models(public_model_id);
|
|
104
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_provider_upstream ON models(provider_id, upstream_model_id);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_model_enabled ON models(enabled);
|
|
106
|
+
|
|
107
|
+
CREATE TABLE IF NOT EXISTS combos (
|
|
108
|
+
id TEXT PRIMARY KEY,
|
|
109
|
+
name TEXT NOT NULL,
|
|
110
|
+
slug TEXT NOT NULL UNIQUE,
|
|
111
|
+
public_model_id TEXT NOT NULL UNIQUE,
|
|
112
|
+
mode TEXT NOT NULL CHECK (mode IN ('fallback','weighted_round_robin')),
|
|
113
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
114
|
+
max_total_attempts INTEGER NOT NULL DEFAULT 3,
|
|
115
|
+
fallback_on_connection INTEGER NOT NULL DEFAULT 1,
|
|
116
|
+
fallback_on_connect_timeout INTEGER NOT NULL DEFAULT 1,
|
|
117
|
+
fallback_on_first_token_timeout INTEGER NOT NULL DEFAULT 1,
|
|
118
|
+
fallback_on_408 INTEGER NOT NULL DEFAULT 1,
|
|
119
|
+
fallback_on_429 INTEGER NOT NULL DEFAULT 1,
|
|
120
|
+
fallback_on_5xx INTEGER NOT NULL DEFAULT 1,
|
|
121
|
+
cache_override_enabled INTEGER,
|
|
122
|
+
config_version INTEGER NOT NULL DEFAULT 1,
|
|
123
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
124
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
CREATE TABLE IF NOT EXISTS combo_members (
|
|
128
|
+
id TEXT PRIMARY KEY,
|
|
129
|
+
combo_id TEXT NOT NULL REFERENCES combos(id) ON DELETE CASCADE,
|
|
130
|
+
model_id TEXT NOT NULL REFERENCES models(id) ON DELETE RESTRICT,
|
|
131
|
+
position INTEGER NOT NULL,
|
|
132
|
+
weight INTEGER NOT NULL DEFAULT 1,
|
|
133
|
+
enabled INTEGER NOT NULL DEFAULT 1
|
|
134
|
+
);
|
|
135
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_combo_model ON combo_members(combo_id, model_id);
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_combo_pos ON combo_members(combo_id, position);
|
|
137
|
+
|
|
138
|
+
CREATE TABLE IF NOT EXISTS model_aliases (
|
|
139
|
+
id TEXT PRIMARY KEY,
|
|
140
|
+
alias TEXT NOT NULL UNIQUE,
|
|
141
|
+
target_kind TEXT NOT NULL CHECK (target_kind IN ('model','combo')),
|
|
142
|
+
target_id TEXT NOT NULL,
|
|
143
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
144
|
+
config_version INTEGER NOT NULL DEFAULT 1,
|
|
145
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
146
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
150
|
+
id TEXT PRIMARY KEY,
|
|
151
|
+
name TEXT NOT NULL,
|
|
152
|
+
key_prefix TEXT NOT NULL,
|
|
153
|
+
key_digest TEXT NOT NULL UNIQUE,
|
|
154
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
155
|
+
expires_at TEXT,
|
|
156
|
+
rpm_limit INTEGER,
|
|
157
|
+
tpm_limit INTEGER,
|
|
158
|
+
daily_token_limit INTEGER,
|
|
159
|
+
monthly_token_limit INTEGER,
|
|
160
|
+
max_concurrent INTEGER,
|
|
161
|
+
max_output_tokens_per_request INTEGER,
|
|
162
|
+
allow_all_models INTEGER NOT NULL DEFAULT 0,
|
|
163
|
+
cache_override_enabled INTEGER,
|
|
164
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
165
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
166
|
+
last_used_at TEXT
|
|
167
|
+
);
|
|
168
|
+
CREATE INDEX IF NOT EXISTS idx_key_prefix ON api_keys(key_prefix);
|
|
169
|
+
|
|
170
|
+
CREATE TABLE IF NOT EXISTS api_key_model_permissions (
|
|
171
|
+
id TEXT PRIMARY KEY,
|
|
172
|
+
api_key_id TEXT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
|
|
173
|
+
target_kind TEXT NOT NULL CHECK (target_kind IN ('model','combo','alias')),
|
|
174
|
+
target_id TEXT NOT NULL
|
|
175
|
+
);
|
|
176
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_key_target ON api_key_model_permissions(api_key_id, target_kind, target_id);
|
|
177
|
+
|
|
178
|
+
CREATE TABLE IF NOT EXISTS api_key_ip_rules (
|
|
179
|
+
id TEXT PRIMARY KEY,
|
|
180
|
+
api_key_id TEXT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
|
|
181
|
+
mode TEXT NOT NULL CHECK (mode IN ('allow','deny')),
|
|
182
|
+
cidr TEXT NOT NULL,
|
|
183
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
184
|
+
);
|
|
185
|
+
CREATE INDEX IF NOT EXISTS idx_ip_rule_key ON api_key_ip_rules(api_key_id);
|
|
186
|
+
|
|
187
|
+
CREATE TABLE IF NOT EXISTS requests (
|
|
188
|
+
id TEXT PRIMARY KEY,
|
|
189
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
190
|
+
completed_at TEXT,
|
|
191
|
+
api_key_id TEXT,
|
|
192
|
+
key_prefix_snapshot TEXT,
|
|
193
|
+
client_ip TEXT NOT NULL,
|
|
194
|
+
protocol TEXT NOT NULL CHECK (protocol IN ('openai','anthropic')),
|
|
195
|
+
endpoint TEXT NOT NULL,
|
|
196
|
+
requested_model TEXT NOT NULL,
|
|
197
|
+
resolved_target_kind TEXT NOT NULL CHECK (resolved_target_kind IN ('model','combo','alias','unknown')),
|
|
198
|
+
resolved_target_id TEXT,
|
|
199
|
+
final_model_id TEXT,
|
|
200
|
+
streaming INTEGER NOT NULL,
|
|
201
|
+
http_status INTEGER NOT NULL,
|
|
202
|
+
success INTEGER NOT NULL,
|
|
203
|
+
total_latency_ms INTEGER NOT NULL DEFAULT 0,
|
|
204
|
+
ttft_ms INTEGER,
|
|
205
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
206
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
207
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
208
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
209
|
+
reasoning_tokens INTEGER NOT NULL DEFAULT 0,
|
|
210
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
211
|
+
attempts_count INTEGER NOT NULL DEFAULT 0,
|
|
212
|
+
error_type TEXT,
|
|
213
|
+
error_message TEXT,
|
|
214
|
+
request_payload_json TEXT,
|
|
215
|
+
response_payload_json TEXT,
|
|
216
|
+
gateway_cache_hit INTEGER NOT NULL DEFAULT 0,
|
|
217
|
+
partial_stream INTEGER NOT NULL DEFAULT 0
|
|
218
|
+
);
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_request_created ON requests(created_at);
|
|
220
|
+
CREATE INDEX IF NOT EXISTS idx_request_success ON requests(success, created_at);
|
|
221
|
+
CREATE INDEX IF NOT EXISTS idx_request_apikey ON requests(api_key_id, created_at);
|
|
222
|
+
CREATE INDEX IF NOT EXISTS idx_request_final_model ON requests(final_model_id, created_at);
|
|
223
|
+
CREATE INDEX IF NOT EXISTS idx_request_requested ON requests(requested_model, created_at);
|
|
224
|
+
CREATE INDEX IF NOT EXISTS idx_request_protocol ON requests(protocol, created_at);
|
|
225
|
+
|
|
226
|
+
CREATE TABLE IF NOT EXISTS request_attempts (
|
|
227
|
+
id TEXT PRIMARY KEY,
|
|
228
|
+
request_id TEXT NOT NULL REFERENCES requests(id) ON DELETE CASCADE,
|
|
229
|
+
attempt_number INTEGER NOT NULL,
|
|
230
|
+
provider_id TEXT NOT NULL,
|
|
231
|
+
model_id TEXT NOT NULL,
|
|
232
|
+
started_at TEXT NOT NULL,
|
|
233
|
+
completed_at TEXT,
|
|
234
|
+
status_code INTEGER,
|
|
235
|
+
success INTEGER NOT NULL,
|
|
236
|
+
latency_ms INTEGER NOT NULL DEFAULT 0,
|
|
237
|
+
ttft_ms INTEGER,
|
|
238
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
239
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
240
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
241
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
242
|
+
reasoning_tokens INTEGER NOT NULL DEFAULT 0,
|
|
243
|
+
stream_started INTEGER NOT NULL DEFAULT 0,
|
|
244
|
+
partial_response INTEGER NOT NULL DEFAULT 0,
|
|
245
|
+
selection_reason TEXT NOT NULL,
|
|
246
|
+
failure_reason TEXT,
|
|
247
|
+
error_message TEXT,
|
|
248
|
+
upstream_request_id TEXT
|
|
249
|
+
);
|
|
250
|
+
CREATE INDEX IF NOT EXISTS idx_attempt_request ON request_attempts(request_id, attempt_number);
|
|
251
|
+
CREATE INDEX IF NOT EXISTS idx_attempt_provider_model ON request_attempts(provider_id, model_id, started_at);
|
|
252
|
+
|
|
253
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
254
|
+
id TEXT PRIMARY KEY,
|
|
255
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
256
|
+
action TEXT NOT NULL,
|
|
257
|
+
actor TEXT NOT NULL DEFAULT 'admin',
|
|
258
|
+
ip TEXT,
|
|
259
|
+
success INTEGER NOT NULL,
|
|
260
|
+
target_type TEXT,
|
|
261
|
+
target_id TEXT,
|
|
262
|
+
target_name TEXT,
|
|
263
|
+
metadata_json TEXT
|
|
264
|
+
);
|
|
265
|
+
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_logs(created_at);
|
|
266
|
+
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action, created_at);
|
|
267
|
+
|
|
268
|
+
CREATE TABLE IF NOT EXISTS response_cache (
|
|
269
|
+
id TEXT PRIMARY KEY,
|
|
270
|
+
cache_key TEXT NOT NULL UNIQUE,
|
|
271
|
+
target_kind TEXT NOT NULL CHECK (target_kind IN ('model','combo','alias')),
|
|
272
|
+
target_id TEXT NOT NULL,
|
|
273
|
+
target_config_version INTEGER NOT NULL DEFAULT 1,
|
|
274
|
+
protocol TEXT NOT NULL CHECK (protocol IN ('openai','anthropic')),
|
|
275
|
+
response_json TEXT NOT NULL,
|
|
276
|
+
usage_json TEXT,
|
|
277
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
278
|
+
expires_at TEXT NOT NULL,
|
|
279
|
+
last_hit_at TEXT,
|
|
280
|
+
hit_count INTEGER NOT NULL DEFAULT 0,
|
|
281
|
+
bytes INTEGER NOT NULL DEFAULT 0
|
|
282
|
+
);
|
|
283
|
+
CREATE INDEX IF NOT EXISTS idx_cache_expires ON response_cache(expires_at);
|
|
284
|
+
CREATE INDEX IF NOT EXISTS idx_cache_target ON response_cache(target_kind, target_id);
|
|
285
|
+
|
|
286
|
+
CREATE TABLE IF NOT EXISTS usage_daily (
|
|
287
|
+
day TEXT NOT NULL,
|
|
288
|
+
api_key_id TEXT NOT NULL,
|
|
289
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
290
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
291
|
+
total_tokens INTEGER NOT NULL DEFAULT 0
|
|
292
|
+
);
|
|
293
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_usage_day_key ON usage_daily(day, api_key_id);
|
|
294
|
+
|
|
295
|
+
CREATE TABLE IF NOT EXISTS usage_monthly (
|
|
296
|
+
month TEXT NOT NULL,
|
|
297
|
+
api_key_id TEXT NOT NULL,
|
|
298
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
299
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
300
|
+
total_tokens INTEGER NOT NULL DEFAULT 0
|
|
301
|
+
);
|
|
302
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_usage_month_key ON usage_monthly(month, api_key_id);
|
|
303
|
+
|
|
304
|
+
CREATE TABLE IF NOT EXISTS backups (
|
|
305
|
+
id TEXT PRIMARY KEY,
|
|
306
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
307
|
+
schema_version INTEGER NOT NULL,
|
|
308
|
+
app_version TEXT NOT NULL,
|
|
309
|
+
size_bytes INTEGER NOT NULL,
|
|
310
|
+
checksum TEXT NOT NULL,
|
|
311
|
+
path TEXT NOT NULL,
|
|
312
|
+
notes TEXT
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
CREATE TABLE IF NOT EXISTS csrf_tokens (
|
|
316
|
+
id TEXT PRIMARY KEY,
|
|
317
|
+
session_id TEXT NOT NULL,
|
|
318
|
+
token TEXT NOT NULL,
|
|
319
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
320
|
+
expires_at TEXT NOT NULL
|
|
321
|
+
);
|
|
322
|
+
CREATE INDEX IF NOT EXISTS idx_csrf_session ON csrf_tokens(session_id);
|
|
323
|
+
CREATE INDEX IF NOT EXISTS idx_csrf_expires ON csrf_tokens(expires_at);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- 0002_source_api_key_secrets.sql
|
|
2
|
+
-- Add columns for encrypted API key secret storage.
|
|
3
|
+
-- Allows admin to retrieve the full key after creation (encrypted at rest).
|
|
4
|
+
|
|
5
|
+
ALTER TABLE api_keys ADD COLUMN key_secret_encrypted TEXT;
|
|
6
|
+
ALTER TABLE api_keys ADD COLUMN key_secret_nonce TEXT;
|
|
7
|
+
ALTER TABLE api_keys ADD COLUMN key_secret_version INTEGER NOT NULL DEFAULT 1;
|
package/package.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ldrouter",
|
|
3
|
+
"version": "1.5.1",
|
|
4
|
+
"description": "LateDev Router — lightweight self-hosted LLM gateway with admin UI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "dist/server/app.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"ldrouter": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"migrations"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"llm",
|
|
20
|
+
"gateway",
|
|
21
|
+
"router",
|
|
22
|
+
"openai",
|
|
23
|
+
"anthropic",
|
|
24
|
+
"proxy",
|
|
25
|
+
"self-hosted"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "node --import tsx/esm scripts/dev.ts",
|
|
29
|
+
"build": "pnpm run build:web && pnpm run build:server",
|
|
30
|
+
"build:server": "tsc -p tsconfig.build.json && node --import tsx/esm scripts/fix-imports.mts",
|
|
31
|
+
"build:web": "vite build",
|
|
32
|
+
"start": "node dist/cli.js",
|
|
33
|
+
"lint": "eslint . --max-warnings=0",
|
|
34
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"test:e2e": "playwright test",
|
|
38
|
+
"db:migrate": "node --import tsx/esm scripts/migrate.ts",
|
|
39
|
+
"migrate": "node --import tsx/esm scripts/migrate.ts",
|
|
40
|
+
"prepack": "pnpm run build"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@fastify/cookie": "^11.0.2",
|
|
44
|
+
"@fastify/cors": "^10.0.2",
|
|
45
|
+
"@fastify/helmet": "^13.0.1",
|
|
46
|
+
"@fastify/multipart": "^9.0.3",
|
|
47
|
+
"@fastify/static": "^8.1.1",
|
|
48
|
+
"@radix-ui/react-accordion": "^1.2.2",
|
|
49
|
+
"@radix-ui/react-alert-dialog": "^1.1.23",
|
|
50
|
+
"@radix-ui/react-avatar": "^1.1.2",
|
|
51
|
+
"@radix-ui/react-checkbox": "^1.1.3",
|
|
52
|
+
"@radix-ui/react-dialog": "^1.1.4",
|
|
53
|
+
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
|
54
|
+
"@radix-ui/react-label": "^2.1.1",
|
|
55
|
+
"@radix-ui/react-popover": "^1.1.4",
|
|
56
|
+
"@radix-ui/react-select": "^2.1.4",
|
|
57
|
+
"@radix-ui/react-separator": "^1.1.1",
|
|
58
|
+
"@radix-ui/react-slot": "^1.1.1",
|
|
59
|
+
"@radix-ui/react-switch": "^1.1.2",
|
|
60
|
+
"@radix-ui/react-tabs": "^1.1.2",
|
|
61
|
+
"@radix-ui/react-tooltip": "^1.1.6",
|
|
62
|
+
"argon2": "^0.43.0",
|
|
63
|
+
"better-sqlite3": "^11.7.0",
|
|
64
|
+
"class-variance-authority": "^0.7.1",
|
|
65
|
+
"clsx": "^2.1.1",
|
|
66
|
+
"drizzle-orm": "^0.36.4",
|
|
67
|
+
"execa": "^10.0.1",
|
|
68
|
+
"fastify": "^5.2.1",
|
|
69
|
+
"ipaddr.js": "^2.2.0",
|
|
70
|
+
"lucide-react": "^0.469.0",
|
|
71
|
+
"pino": "^9.5.0",
|
|
72
|
+
"pino-pretty": "^13.0.0",
|
|
73
|
+
"qrcode": "^1.5.4",
|
|
74
|
+
"semver": "^7.8.5",
|
|
75
|
+
"sonner": "^1.7.1",
|
|
76
|
+
"speakeasy": "^2.0.0",
|
|
77
|
+
"tailwind-merge": "^2.6.0",
|
|
78
|
+
"tailwindcss-animate": "^1.0.7",
|
|
79
|
+
"zod": "^3.24.1"
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"@eslint/js": "^10.0.1",
|
|
83
|
+
"@playwright/test": "^1.50.0",
|
|
84
|
+
"@types/better-sqlite3": "^7.6.12",
|
|
85
|
+
"@types/node": "^22.10.5",
|
|
86
|
+
"@types/pngjs": "^6.0.5",
|
|
87
|
+
"@types/qrcode": "^1.5.5",
|
|
88
|
+
"@types/react": "^19.0.0",
|
|
89
|
+
"@types/react-dom": "^19.0.0",
|
|
90
|
+
"@types/semver": "^7.8.0",
|
|
91
|
+
"@types/speakeasy": "^2.0.10",
|
|
92
|
+
"@typescript-eslint/eslint-plugin": "^8.20.0",
|
|
93
|
+
"@typescript-eslint/parser": "^8.20.0",
|
|
94
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
95
|
+
"autoprefixer": "^10.4.20",
|
|
96
|
+
"drizzle-kit": "^0.30.1",
|
|
97
|
+
"eslint": "^9.18.0",
|
|
98
|
+
"eslint-plugin-react": "^7.37.4",
|
|
99
|
+
"eslint-plugin-react-hooks": "^5.1.0",
|
|
100
|
+
"globals": "^17.11.0",
|
|
101
|
+
"happy-dom": "^16.5.3",
|
|
102
|
+
"pngjs": "^7.0.0",
|
|
103
|
+
"postcss": "^8.5.1",
|
|
104
|
+
"react": "^19.0.0",
|
|
105
|
+
"react-dom": "^19.0.0",
|
|
106
|
+
"react-router-dom": "^7.1.1",
|
|
107
|
+
"tailwindcss": "^3.4.17",
|
|
108
|
+
"tsx": "^4.19.2",
|
|
109
|
+
"typescript": "^5.7.3",
|
|
110
|
+
"vite": "^6.0.7",
|
|
111
|
+
"vitest": "^2.1.8"
|
|
112
|
+
},
|
|
113
|
+
"engines": {
|
|
114
|
+
"node": ">=22.0.0"
|
|
115
|
+
},
|
|
116
|
+
"packageManager": "pnpm@11.24.0"
|
|
117
|
+
}
|