ldrouter 1.5.1 → 1.6.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 (60) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/package.json +13 -2
  3. package/dist/cli.js +0 -13
  4. package/dist/server/app.js +0 -138
  5. package/dist/server/auth/api-key.js +0 -75
  6. package/dist/server/auth/crypto.js +0 -94
  7. package/dist/server/auth/ids.js +0 -40
  8. package/dist/server/auth/middleware.js +0 -36
  9. package/dist/server/auth/recovery.js +0 -11
  10. package/dist/server/caching/store.js +0 -119
  11. package/dist/server/config/index.js +0 -96
  12. package/dist/server/db/index.js +0 -64
  13. package/dist/server/db/migrate.js +0 -408
  14. package/dist/server/db/repositories/audit.js +0 -75
  15. package/dist/server/db/repositories/settings.js +0 -63
  16. package/dist/server/db/schema.js +0 -396
  17. package/dist/server/errors.js +0 -65
  18. package/dist/server/gateway/runner.js +0 -745
  19. package/dist/server/logging/logger.js +0 -35
  20. package/dist/server/maintenance/retention.js +0 -48
  21. package/dist/server/metrics/registry.js +0 -169
  22. package/dist/server/protocols/anthropic.js +0 -154
  23. package/dist/server/protocols/canonical.js +0 -201
  24. package/dist/server/providers/index.js +0 -89
  25. package/dist/server/routes/admin/aliases.js +0 -98
  26. package/dist/server/routes/admin/api-keys.js +0 -194
  27. package/dist/server/routes/admin/audit.js +0 -19
  28. package/dist/server/routes/admin/auth.js +0 -124
  29. package/dist/server/routes/admin/backup.js +0 -113
  30. package/dist/server/routes/admin/combos.js +0 -198
  31. package/dist/server/routes/admin/dashboard.js +0 -55
  32. package/dist/server/routes/admin/models.js +0 -178
  33. package/dist/server/routes/admin/providers.js +0 -212
  34. package/dist/server/routes/admin/requests.js +0 -156
  35. package/dist/server/routes/admin/settings.js +0 -197
  36. package/dist/server/routes/admin/setup.js +0 -80
  37. package/dist/server/routes/admin/stats.js +0 -180
  38. package/dist/server/routes/admin.js +0 -39
  39. package/dist/server/routes/gateway/anthropic.js +0 -112
  40. package/dist/server/routes/gateway/openai.js +0 -257
  41. package/dist/server/routes/gateway.js +0 -7
  42. package/dist/server/routes/health.js +0 -27
  43. package/dist/server/routing/capabilities.js +0 -52
  44. package/dist/server/routing/circuit.js +0 -37
  45. package/dist/server/routing/combo.js +0 -100
  46. package/dist/server/routing/quota.js +0 -51
  47. package/dist/server/routing/ratelimit.js +0 -58
  48. package/dist/server/routing/resolver.js +0 -43
  49. package/dist/server/security/redact.js +0 -111
  50. package/dist/server/selfupdate/index.js +0 -154
  51. package/dist/server/upstream/client.js +0 -179
  52. package/dist/server/util/cidr.js +0 -91
  53. package/dist/server/util/client-ip.js +0 -15
  54. package/dist/server/util/stable-json.js +0 -19
  55. package/dist/shared/types.js +0 -2
  56. package/dist/web/assets/index-COSbvF8Z.css +0 -1
  57. package/dist/web/assets/index-DbnEzuxq.js +0 -251
  58. package/dist/web/favicon.png +0 -0
  59. package/dist/web/index.html +0 -15
  60. package/dist/web/logo.png +0 -0
@@ -1,63 +0,0 @@
1
- // Repository: application settings (singleton row in app_settings table).
2
- import { getDb, schema } from '../index.js';
3
- import { eq } from 'drizzle-orm';
4
- function rowToSettings(row) {
5
- return {
6
- setupComplete: row.setupComplete,
7
- retentionDays: row.retentionDays,
8
- retentionMode: 'custom',
9
- customRetentionDays: row.retentionDays,
10
- contentLogMode: row.contentLogMode,
11
- dbSizeLimitMb: row.dbSizeLimitMb,
12
- trustProxyHops: row.trustProxyHops,
13
- schemaVersion: row.schemaVersion,
14
- appVersion: row.appVersion,
15
- gatewayCacheEnabled: row.gatewayCacheEnabled,
16
- gatewayCacheDefaultTtlSeconds: row.gatewayCacheDefaultTtlSeconds,
17
- gatewayCacheMaxSizeMb: row.gatewayCacheMaxSizeMb,
18
- masterKeyConfigured: row.masterKeyConfigured,
19
- masterKeyVersion: row.masterKeyVersion,
20
- };
21
- }
22
- export function getSettings() {
23
- const db = getDb();
24
- const row = db.select().from(schema.appSettings).where(eq(schema.appSettings.id, 1)).get();
25
- if (!row) {
26
- // Bootstrap if somehow missing.
27
- db.insert(schema.appSettings).values({ id: 1 }).run();
28
- const again = db.select().from(schema.appSettings).where(eq(schema.appSettings.id, 1)).get();
29
- if (!again)
30
- throw new Error('app_settings bootstrap failed');
31
- return rowToSettings(again);
32
- }
33
- return rowToSettings(row);
34
- }
35
- export function updateSettings(patch) {
36
- const db = getDb();
37
- const update = {
38
- updatedAt: new Date().toISOString(),
39
- };
40
- if (patch.setupComplete !== undefined)
41
- update.setupComplete = patch.setupComplete;
42
- if (patch.retentionDays !== undefined)
43
- update.retentionDays = patch.retentionDays;
44
- if (patch.contentLogMode !== undefined)
45
- update.contentLogMode = patch.contentLogMode;
46
- if (patch.dbSizeLimitMb !== undefined)
47
- update.dbSizeLimitMb = patch.dbSizeLimitMb;
48
- if (patch.trustProxyHops !== undefined)
49
- update.trustProxyHops = patch.trustProxyHops;
50
- if (patch.gatewayCacheEnabled !== undefined)
51
- update.gatewayCacheEnabled = patch.gatewayCacheEnabled;
52
- if (patch.gatewayCacheDefaultTtlSeconds !== undefined)
53
- update.gatewayCacheDefaultTtlSeconds = patch.gatewayCacheDefaultTtlSeconds;
54
- if (patch.gatewayCacheMaxSizeMb !== undefined)
55
- update.gatewayCacheMaxSizeMb = patch.gatewayCacheMaxSizeMb;
56
- if (patch.masterKeyConfigured !== undefined)
57
- update.masterKeyConfigured = patch.masterKeyConfigured;
58
- db.update(schema.appSettings).set(update).where(eq(schema.appSettings.id, 1)).run();
59
- return getSettings();
60
- }
61
- export function markSetupComplete() {
62
- updateSettings({ setupComplete: true });
63
- }
@@ -1,396 +0,0 @@
1
- // Drizzle ORM schema for LateDev Router SQLite database.
2
- // Source of truth: docs/02-DATA-MODEL.md
3
- import { sql } from 'drizzle-orm';
4
- import { sqliteTable, text, integer, index, uniqueIndex } from 'drizzle-orm/sqlite-core';
5
- // ============================================================================
6
- // Application settings (singleton)
7
- // ============================================================================
8
- export const appSettings = sqliteTable('app_settings', {
9
- id: integer('id').primaryKey(),
10
- setupComplete: integer('setup_complete', { mode: 'boolean' }).notNull().notNull().default(false),
11
- retentionDays: integer('retention_days').notNull().default(30),
12
- contentLogMode: text('content_log_mode').notNull().default('metadata'),
13
- dbSizeLimitMb: integer('db_size_limit_mb').notNull().default(2048),
14
- trustProxyHops: integer('trust_proxy_hops').notNull().default(0),
15
- schemaVersion: integer('schema_version').notNull().default(0),
16
- appVersion: text('app_version').notNull().default('0.0.0'),
17
- gatewayCacheEnabled: integer('gateway_cache_enabled', { mode: 'boolean' }).notNull().notNull().default(false),
18
- gatewayCacheDefaultTtlSeconds: integer('gateway_cache_default_ttl_seconds').notNull().default(300),
19
- gatewayCacheMaxSizeMb: integer('gateway_cache_max_size_mb').notNull().default(256),
20
- masterKeyVersion: integer('master_key_version').notNull().default(1),
21
- masterKeyConfigured: integer('master_key_configured', { mode: 'boolean' }).notNull().notNull().default(false),
22
- updatedAt: text('updated_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
23
- });
24
- // ============================================================================
25
- // Admin account + sessions
26
- // ============================================================================
27
- export const adminAccount = sqliteTable('admin_account', {
28
- id: text('id').primaryKey(),
29
- username: text('username').notNull().unique(),
30
- passwordHash: text('password_hash').notNull(),
31
- totpEnabled: integer('totp_enabled', { mode: 'boolean' }).notNull().notNull().default(false),
32
- totpSecretEncrypted: text('totp_secret_encrypted'),
33
- totpSecretNonce: text('totp_secret_nonce'),
34
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
35
- updatedAt: text('updated_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
36
- lastLoginAt: text('last_login_at'),
37
- });
38
- export const adminRecoveryCodes = sqliteTable('admin_recovery_codes', {
39
- id: text('id').primaryKey(),
40
- adminId: text('admin_id')
41
- .notNull()
42
- .references(() => adminAccount.id, { onDelete: 'cascade' }),
43
- codeHash: text('code_hash').notNull(),
44
- usedAt: text('used_at'),
45
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
46
- }, (t) => ({
47
- adminIdx: index('idx_recovery_admin').on(t.adminId),
48
- }));
49
- export const adminSessions = sqliteTable('admin_sessions', {
50
- id: text('id').primaryKey(),
51
- tokenDigest: text('token_digest').notNull().unique(),
52
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
53
- expiresAt: text('expires_at').notNull(),
54
- lastSeenAt: text('last_seen_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
55
- ip: text('ip'),
56
- userAgent: text('user_agent'),
57
- }, (t) => ({
58
- expiresIdx: index('idx_session_expires').on(t.expiresAt),
59
- }));
60
- export const loginAttempts = sqliteTable('login_attempts', {
61
- id: text('id').primaryKey(),
62
- username: text('username').notNull(),
63
- ip: text('ip').notNull(),
64
- success: integer('success', { mode: 'boolean' }).notNull(),
65
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
66
- }, (t) => ({
67
- userIdx: index('idx_login_user_time').on(t.username, t.createdAt),
68
- ipIdx: index('idx_login_ip_time').on(t.ip, t.createdAt),
69
- }));
70
- // ============================================================================
71
- // Providers
72
- // ============================================================================
73
- export const providers = sqliteTable('providers', {
74
- id: text('id').primaryKey(),
75
- name: text('name').notNull(),
76
- slug: text('slug').notNull().unique(),
77
- type: text('type', { enum: ['openai', 'anthropic'] }).notNull(),
78
- baseUrl: text('base_url').notNull(),
79
- encryptedApiKey: text('encrypted_api_key').notNull(),
80
- apiKeyNonce: text('api_key_nonce').notNull(),
81
- apiKeyVersion: integer('api_key_version').notNull().default(1),
82
- customHeadersEncrypted: text('custom_headers_encrypted'),
83
- customHeadersNonce: text('custom_headers_nonce'),
84
- enabled: integer('enabled', { mode: 'boolean' }).notNull().notNull().default(true),
85
- connectTimeoutMs: integer('connect_timeout_ms').notNull().default(10000),
86
- firstTokenTimeoutMs: integer('first_token_timeout_ms').notNull().default(30000),
87
- streamIdleTimeoutMs: integer('stream_idle_timeout_ms').notNull().default(60000),
88
- totalTimeoutMs: integer('total_timeout_ms').notNull().default(180000),
89
- maxRetries: integer('max_retries').notNull().default(2),
90
- retryBaseMs: integer('retry_base_ms').notNull().default(500),
91
- retryMaxMs: integer('retry_max_ms').notNull().default(8000),
92
- cbFailureThreshold: integer('cb_failure_threshold').notNull().default(5),
93
- cbCooldownSeconds: integer('cb_cooldown_seconds').notNull().default(60),
94
- healthState: text('health_state', { enum: ['healthy', 'degraded', 'down', 'circuit_open', 'unknown'] })
95
- .notNull()
96
- .default('unknown'),
97
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
98
- updatedAt: text('updated_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
99
- }, (t) => ({
100
- slugIdx: uniqueIndex('uniq_provider_slug').on(t.slug),
101
- }));
102
- // ============================================================================
103
- // Models
104
- // ============================================================================
105
- export const models = sqliteTable('models', {
106
- id: text('id').primaryKey(),
107
- providerId: text('provider_id')
108
- .notNull()
109
- .references(() => providers.id, { onDelete: 'restrict' }),
110
- upstreamModelId: text('upstream_model_id').notNull(),
111
- publicModelId: text('public_model_id').notNull().unique(),
112
- displayName: text('display_name').notNull(),
113
- enabled: integer('enabled', { mode: 'boolean' }).notNull().notNull().default(true),
114
- upstreamAvailable: integer('upstream_available', { mode: 'boolean' }).notNull().notNull().default(true),
115
- capabilitiesJson: text('capabilities_json').notNull().default('{}'),
116
- maxContextTokens: integer('max_context_tokens'),
117
- maxOutputTokens: integer('max_output_tokens'),
118
- discoveredMetadataJson: text('discovered_metadata_json'),
119
- cacheOverrideEnabled: integer('cache_override_enabled', { mode: 'boolean' }),
120
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
121
- updatedAt: text('updated_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
122
- lastSeenUpstreamAt: text('last_seen_upstream_at'),
123
- }, (t) => ({
124
- publicIdx: uniqueIndex('uniq_model_public').on(t.publicModelId),
125
- providerUpstreamIdx: uniqueIndex('uniq_provider_upstream').on(t.providerId, t.upstreamModelId),
126
- enabledIdx: index('idx_model_enabled').on(t.enabled),
127
- }));
128
- // ============================================================================
129
- // Combos
130
- // ============================================================================
131
- export const combos = sqliteTable('combos', {
132
- id: text('id').primaryKey(),
133
- name: text('name').notNull(),
134
- slug: text('slug').notNull().unique(),
135
- publicModelId: text('public_model_id').notNull().unique(),
136
- mode: text('mode', { enum: ['fallback', 'weighted_round_robin'] }).notNull(),
137
- enabled: integer('enabled', { mode: 'boolean' }).notNull().notNull().default(true),
138
- maxTotalAttempts: integer('max_total_attempts').notNull().default(3),
139
- fallbackOnConnection: integer('fallback_on_connection', { mode: 'boolean' }).notNull().notNull().default(true),
140
- fallbackOnConnectTimeout: integer('fallback_on_connect_timeout', { mode: 'boolean' }).notNull().notNull().default(true),
141
- fallbackOnFirstTokenTimeout: integer('fallback_on_first_token_timeout', { mode: 'boolean' }).notNull().notNull().default(true),
142
- fallbackOn408: integer('fallback_on_408', { mode: 'boolean' }).notNull().notNull().default(true),
143
- fallbackOn429: integer('fallback_on_429', { mode: 'boolean' }).notNull().notNull().default(true),
144
- fallbackOn5xx: integer('fallback_on_5xx', { mode: 'boolean' }).notNull().notNull().default(true),
145
- cacheOverrideEnabled: integer('cache_override_enabled', { mode: 'boolean' }),
146
- configVersion: integer('config_version').notNull().default(1),
147
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
148
- updatedAt: text('updated_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
149
- }, (t) => ({
150
- slugIdx: uniqueIndex('uniq_combo_slug').on(t.slug),
151
- publicIdx: uniqueIndex('uniq_combo_public').on(t.publicModelId),
152
- }));
153
- export const comboMembers = sqliteTable('combo_members', {
154
- id: text('id').primaryKey(),
155
- comboId: text('combo_id')
156
- .notNull()
157
- .references(() => combos.id, { onDelete: 'cascade' }),
158
- modelId: text('model_id')
159
- .notNull()
160
- .references(() => models.id, { onDelete: 'restrict' }),
161
- position: integer('position').notNull(),
162
- weight: integer('weight').notNull().default(1),
163
- enabled: integer('enabled', { mode: 'boolean' }).notNull().notNull().default(true),
164
- }, (t) => ({
165
- uniqMember: uniqueIndex('uniq_combo_model').on(t.comboId, t.modelId),
166
- posIdx: index('idx_combo_pos').on(t.comboId, t.position),
167
- }));
168
- // ============================================================================
169
- // Aliases
170
- // ============================================================================
171
- export const modelAliases = sqliteTable('model_aliases', {
172
- id: text('id').primaryKey(),
173
- alias: text('alias').notNull().unique(),
174
- targetKind: text('target_kind', { enum: ['model', 'combo'] }).notNull(),
175
- targetId: text('target_id').notNull(),
176
- enabled: integer('enabled', { mode: 'boolean' }).notNull().notNull().default(true),
177
- configVersion: integer('config_version').notNull().default(1),
178
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
179
- updatedAt: text('updated_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
180
- }, (t) => ({
181
- aliasIdx: uniqueIndex('uniq_alias').on(t.alias),
182
- }));
183
- // ============================================================================
184
- // API keys (gateway keys with ld- prefix)
185
- // ============================================================================
186
- export const apiKeys = sqliteTable('api_keys', {
187
- id: text('id').primaryKey(),
188
- name: text('name').notNull(),
189
- keyPrefix: text('key_prefix').notNull(),
190
- keyDigest: text('key_digest').notNull().unique(),
191
- keySecretEncrypted: text('key_secret_encrypted'),
192
- keySecretNonce: text('key_secret_nonce'),
193
- keySecretVersion: integer('key_secret_version').notNull().default(1),
194
- enabled: integer('enabled', { mode: 'boolean' }).notNull().notNull().default(true),
195
- expiresAt: text('expires_at'),
196
- rpmLimit: integer('rpm_limit'),
197
- tpmLimit: integer('tpm_limit'),
198
- dailyTokenLimit: integer('daily_token_limit'),
199
- monthlyTokenLimit: integer('monthly_token_limit'),
200
- maxConcurrent: integer('max_concurrent'),
201
- maxOutputTokensPerRequest: integer('max_output_tokens_per_request'),
202
- allowAllModels: integer('allow_all_models', { mode: 'boolean' }).notNull().notNull().default(false),
203
- cacheOverrideEnabled: integer('cache_override_enabled', { mode: 'boolean' }),
204
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
205
- updatedAt: text('updated_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
206
- lastUsedAt: text('last_used_at'),
207
- }, (t) => ({
208
- digestIdx: uniqueIndex('uniq_key_digest').on(t.keyDigest),
209
- prefixIdx: index('idx_key_prefix').on(t.keyPrefix),
210
- }));
211
- export const apiKeyModelPermissions = sqliteTable('api_key_model_permissions', {
212
- id: text('id').primaryKey(),
213
- apiKeyId: text('api_key_id')
214
- .notNull()
215
- .references(() => apiKeys.id, { onDelete: 'cascade' }),
216
- targetKind: text('target_kind', { enum: ['model', 'combo', 'alias'] }).notNull(),
217
- targetId: text('target_id').notNull(),
218
- }, (t) => ({
219
- uniqPerm: uniqueIndex('uniq_key_target').on(t.apiKeyId, t.targetKind, t.targetId),
220
- }));
221
- export const apiKeyIpRules = sqliteTable('api_key_ip_rules', {
222
- id: text('id').primaryKey(),
223
- apiKeyId: text('api_key_id')
224
- .notNull()
225
- .references(() => apiKeys.id, { onDelete: 'cascade' }),
226
- mode: text('mode', { enum: ['allow', 'deny'] }).notNull(),
227
- cidr: text('cidr').notNull(),
228
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
229
- }, (t) => ({
230
- keyIdx: index('idx_ip_rule_key').on(t.apiKeyId),
231
- }));
232
- // ============================================================================
233
- // Request logs + attempts
234
- // ============================================================================
235
- export const requests = sqliteTable('requests', {
236
- id: text('id').primaryKey(),
237
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
238
- completedAt: text('completed_at'),
239
- apiKeyId: text('api_key_id'),
240
- keyPrefixSnapshot: text('key_prefix_snapshot'),
241
- clientIp: text('client_ip').notNull(),
242
- protocol: text('protocol', { enum: ['openai', 'anthropic'] }).notNull(),
243
- endpoint: text('endpoint').notNull(),
244
- requestedModel: text('requested_model').notNull(),
245
- resolvedTargetKind: text('resolved_target_kind', { enum: ['model', 'combo', 'alias', 'unknown'] }).notNull(),
246
- resolvedTargetId: text('resolved_target_id'),
247
- finalModelId: text('final_model_id'),
248
- streaming: integer('streaming', { mode: 'boolean' }).notNull(),
249
- httpStatus: integer('http_status').notNull(),
250
- success: integer('success', { mode: 'boolean' }).notNull(),
251
- totalLatencyMs: integer('total_latency_ms').notNull().default(0),
252
- ttftMs: integer('ttft_ms'),
253
- inputTokens: integer('input_tokens').notNull().default(0),
254
- outputTokens: integer('output_tokens').notNull().default(0),
255
- cacheReadTokens: integer('cache_read_tokens').notNull().default(0),
256
- cacheWriteTokens: integer('cache_write_tokens').notNull().default(0),
257
- reasoningTokens: integer('reasoning_tokens').notNull().default(0),
258
- totalTokens: integer('total_tokens').notNull().default(0),
259
- attemptsCount: integer('attempts_count').notNull().default(0),
260
- errorType: text('error_type'),
261
- errorMessage: text('error_message'),
262
- requestPayloadJson: text('request_payload_json'),
263
- responsePayloadJson: text('response_payload_json'),
264
- gatewayCacheHit: integer('gateway_cache_hit', { mode: 'boolean' }).notNull().notNull().default(false),
265
- partialStream: integer('partial_stream', { mode: 'boolean' }).notNull().notNull().default(false),
266
- }, (t) => ({
267
- createdIdx: index('idx_request_created').on(t.createdAt),
268
- successIdx: index('idx_request_success').on(t.success, t.createdAt),
269
- apiKeyIdx: index('idx_request_apikey').on(t.apiKeyId, t.createdAt),
270
- finalModelIdx: index('idx_request_final_model').on(t.finalModelId, t.createdAt),
271
- requestedIdx: index('idx_request_requested').on(t.requestedModel, t.createdAt),
272
- protocolIdx: index('idx_request_protocol').on(t.protocol, t.createdAt),
273
- }));
274
- export const requestAttempts = sqliteTable('request_attempts', {
275
- id: text('id').primaryKey(),
276
- requestId: text('request_id')
277
- .notNull()
278
- .references(() => requests.id, { onDelete: 'cascade' }),
279
- attemptNumber: integer('attempt_number').notNull(),
280
- providerId: text('provider_id').notNull(),
281
- modelId: text('model_id').notNull(),
282
- startedAt: text('started_at').notNull(),
283
- completedAt: text('completed_at'),
284
- statusCode: integer('status_code'),
285
- success: integer('success', { mode: 'boolean' }).notNull(),
286
- latencyMs: integer('latency_ms').notNull().default(0),
287
- ttftMs: integer('ttft_ms'),
288
- inputTokens: integer('input_tokens').notNull().default(0),
289
- outputTokens: integer('output_tokens').notNull().default(0),
290
- cacheReadTokens: integer('cache_read_tokens').notNull().default(0),
291
- cacheWriteTokens: integer('cache_write_tokens').notNull().default(0),
292
- reasoningTokens: integer('reasoning_tokens').notNull().default(0),
293
- streamStarted: integer('stream_started', { mode: 'boolean' }).notNull().notNull().default(false),
294
- partialResponse: integer('partial_response', { mode: 'boolean' }).notNull().notNull().default(false),
295
- selectionReason: text('selection_reason').notNull(),
296
- failureReason: text('failure_reason'),
297
- errorMessage: text('error_message'),
298
- upstreamRequestId: text('upstream_request_id'),
299
- }, (t) => ({
300
- requestIdx: index('idx_attempt_request').on(t.requestId, t.attemptNumber),
301
- providerModelIdx: index('idx_attempt_provider_model').on(t.providerId, t.modelId, t.startedAt),
302
- }));
303
- // ============================================================================
304
- // Audit logs (immutable, excluded from request retention)
305
- // ============================================================================
306
- export const auditLogs = sqliteTable('audit_logs', {
307
- id: text('id').primaryKey(),
308
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
309
- action: text('action').notNull(),
310
- actor: text('actor').notNull().default('admin'),
311
- ip: text('ip'),
312
- success: integer('success', { mode: 'boolean' }).notNull(),
313
- targetType: text('target_type'),
314
- targetId: text('target_id'),
315
- targetName: text('target_name'),
316
- metadataJson: text('metadata_json'),
317
- }, (t) => ({
318
- createdIdx: index('idx_audit_created').on(t.createdAt),
319
- actionIdx: index('idx_audit_action').on(t.action, t.createdAt),
320
- }));
321
- // ============================================================================
322
- // Gateway response cache (canonical, exact-key)
323
- // ============================================================================
324
- export const responseCache = sqliteTable('response_cache', {
325
- id: text('id').primaryKey(),
326
- cacheKey: text('cache_key').notNull().unique(),
327
- targetKind: text('target_kind', { enum: ['model', 'combo', 'alias'] }).notNull(),
328
- targetId: text('target_id').notNull(),
329
- targetConfigVersion: integer('target_config_version').notNull().default(1),
330
- protocol: text('protocol', { enum: ['openai', 'anthropic'] }).notNull(),
331
- responseJson: text('response_json').notNull(),
332
- usageJson: text('usage_json'),
333
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
334
- expiresAt: text('expires_at').notNull(),
335
- lastHitAt: text('last_hit_at'),
336
- hitCount: integer('hit_count').notNull().default(0),
337
- bytes: integer('bytes').notNull().default(0),
338
- }, (t) => ({
339
- expiresIdx: index('idx_cache_expires').on(t.expiresAt),
340
- targetIdx: index('idx_cache_target').on(t.targetKind, t.targetId),
341
- }));
342
- // ============================================================================
343
- // Daily usage aggregates (for daily/monthly quota + statistics fast path)
344
- // ============================================================================
345
- export const usageDaily = sqliteTable('usage_daily', {
346
- day: text('day').notNull(), // YYYY-MM-DD
347
- apiKeyId: text('api_key_id').notNull(),
348
- inputTokens: integer('input_tokens').notNull().default(0),
349
- outputTokens: integer('output_tokens').notNull().default(0),
350
- totalTokens: integer('total_tokens').notNull().default(0),
351
- }, (t) => ({
352
- pk: uniqueIndex('uniq_usage_day_key').on(t.day, t.apiKeyId),
353
- }));
354
- export const usageMonthly = sqliteTable('usage_monthly', {
355
- month: text('month').notNull(), // YYYY-MM
356
- apiKeyId: text('api_key_id').notNull(),
357
- inputTokens: integer('input_tokens').notNull().default(0),
358
- outputTokens: integer('output_tokens').notNull().default(0),
359
- totalTokens: integer('total_tokens').notNull().default(0),
360
- }, (t) => ({
361
- pk: uniqueIndex('uniq_usage_month_key').on(t.month, t.apiKeyId),
362
- }));
363
- // ============================================================================
364
- // Backups
365
- // ============================================================================
366
- export const backups = sqliteTable('backups', {
367
- id: text('id').primaryKey(),
368
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
369
- schemaVersion: integer('schema_version').notNull(),
370
- appVersion: text('app_version').notNull(),
371
- sizeBytes: integer('size_bytes').notNull(),
372
- checksum: text('checksum').notNull(),
373
- path: text('path').notNull(),
374
- notes: text('notes'),
375
- });
376
- // ============================================================================
377
- // CSRF tokens (stateful per-session CSRF reference)
378
- // ============================================================================
379
- export const csrfTokens = sqliteTable('csrf_tokens', {
380
- id: text('id').primaryKey(),
381
- sessionId: text('session_id').notNull(),
382
- token: text('token').notNull(),
383
- createdAt: text('created_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
384
- expiresAt: text('expires_at').notNull(),
385
- }, (t) => ({
386
- sessionIdx: index('idx_csrf_session').on(t.sessionId),
387
- expiresIdx: index('idx_csrf_expires').on(t.expiresAt),
388
- }));
389
- // ============================================================================
390
- // Schema version tracking (for backup compatibility checks)
391
- // ============================================================================
392
- export const schemaMigrations = sqliteTable('schema_migrations', {
393
- version: integer('version').primaryKey(),
394
- name: text('name').notNull(),
395
- appliedAt: text('applied_at').notNull().default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`),
396
- });
@@ -1,65 +0,0 @@
1
- // Canonical gateway error categories. Maps to OpenAI / Anthropic error envelopes.
2
- export class GatewayError extends Error {
3
- type;
4
- status;
5
- code;
6
- safe;
7
- cause;
8
- constructor(type, message, opts = {}) {
9
- super(message);
10
- this.type = type;
11
- this.status = opts.status ?? defaultStatusFor(type);
12
- this.code = opts.code ?? type;
13
- this.safe = opts.safe ?? true;
14
- this.cause = opts.cause;
15
- this.name = 'GatewayError';
16
- }
17
- }
18
- export function defaultStatusFor(t) {
19
- switch (t) {
20
- case 'authentication_error':
21
- return 401;
22
- case 'permission_error':
23
- return 403;
24
- case 'model_not_found':
25
- return 404;
26
- case 'invalid_request_error':
27
- return 400;
28
- case 'capability_not_supported':
29
- return 400;
30
- case 'rate_limit_error':
31
- return 429;
32
- case 'timeout_error':
33
- return 504;
34
- case 'upstream_auth_error':
35
- return 502;
36
- case 'upstream_rate_limit':
37
- return 529;
38
- case 'upstream_unavailable':
39
- return 502;
40
- case 'upstream_error':
41
- return 502;
42
- case 'gateway_error':
43
- default:
44
- return 500;
45
- }
46
- }
47
- /** OpenAI-compatible error envelope. */
48
- export function toOpenAIError(g, requestId) {
49
- return {
50
- error: {
51
- message: g.safe ? g.message : 'Gateway error',
52
- type: g.type,
53
- ...(g.code ? { code: g.code } : {}),
54
- ...(requestId ? { request_id: requestId } : {}),
55
- },
56
- };
57
- }
58
- /** Anthropic-compatible error envelope. */
59
- export function toAnthropicError(g, requestId) {
60
- return {
61
- type: 'error',
62
- error: { type: g.type, message: g.safe ? g.message : 'Gateway error' },
63
- ...(requestId ? { request_id: requestId } : {}),
64
- };
65
- }