Spaces:
Runtime error
Runtime error
| import { Router } from 'express'; | |
| import type { Request, Response } from 'express'; | |
| import { z } from 'zod'; | |
| import { getDb } from '../db/index.js'; | |
| import { resolveProvider } from '../providers/index.js'; | |
| import { encrypt, decrypt, maskKey } from '../lib/crypto.js'; | |
| export const keysRouter = Router(); | |
| // Active providers — must match providers/index.ts registrations + shared/types.ts Platform. | |
| // Moonshot and MiniMax direct integrations were dropped in V4. HuggingFace | |
| // was dropped in V4 and re-added in V13 via the router.huggingface.co route. | |
| // SambaNova was dropped in V23 (free tier permanently retired). | |
| const PLATFORMS = [ | |
| 'google', 'groq', 'cerebras', 'nvidia', 'mistral', | |
| 'openrouter', 'github', 'cohere', 'cloudflare', 'zhipu', 'ollama', | |
| 'kilo', 'pollinations', 'llm7', 'huggingface', 'opencode', 'custom', | |
| ] as const; | |
| // `key` is optional so keyless providers (Kilo's anonymous gateway) can be added | |
| // without one; the handler enforces a non-empty key for everyone else. | |
| const addKeySchema = z.object({ | |
| platform: z.enum(PLATFORMS), | |
| key: z.string().optional(), | |
| label: z.string().optional(), | |
| }); | |
| const updateKeySchema = z.object({ | |
| enabled: z.boolean().optional(), | |
| label: z.string().optional(), | |
| }).refine(data => data.enabled !== undefined || data.label !== undefined, { | |
| message: 'At least one of enabled or label must be provided', | |
| }); | |
| // List all keys (masked) | |
| keysRouter.get('/', (_req: Request, res: Response) => { | |
| const db = getDb(); | |
| const rows = db.prepare('SELECT * FROM api_keys ORDER BY created_at DESC').all() as any[]; | |
| const keys = rows.map(row => { | |
| let maskedKey = '****'; | |
| try { | |
| const realKey = decrypt(row.encrypted_key, row.iv, row.auth_tag); | |
| maskedKey = maskKey(realKey); | |
| } catch { | |
| maskedKey = '[decrypt failed]'; | |
| } | |
| return { | |
| id: row.id, | |
| platform: row.platform, | |
| label: row.label, | |
| maskedKey, | |
| baseUrl: row.base_url ?? null, | |
| status: row.status, | |
| enabled: row.enabled === 1, | |
| createdAt: row.created_at, | |
| lastCheckedAt: row.last_checked_at, | |
| }; | |
| }); | |
| res.json(keys); | |
| }); | |
| // Add a key | |
| keysRouter.post('/', (req: Request, res: Response) => { | |
| const parsed = addKeySchema.safeParse(req.body); | |
| if (!parsed.success) { | |
| res.status(400).json({ error: { message: parsed.error.errors.map(e => e.message).join(', ') } }); | |
| return; | |
| } | |
| const { platform, label } = parsed.data; | |
| const isKeyless = resolveProvider(platform)?.keyless === true; | |
| const rawKey = parsed.data.key?.trim() ?? ''; | |
| if (!isKeyless && !rawKey) { | |
| res.status(400).json({ error: { message: 'key is required' } }); | |
| return; | |
| } | |
| // Keyless providers (Kilo anon) store a sentinel so routing sees the platform | |
| // as configured; the provider omits the auth header on outgoing calls. | |
| const keyToStore = isKeyless ? (rawKey || 'no-key') : rawKey; | |
| const db = getDb(); | |
| // A keyless provider needs only one sentinel row — re-enable an existing one | |
| // instead of piling up duplicates each time the user clicks "Add". | |
| if (isKeyless) { | |
| const existing = db.prepare('SELECT id FROM api_keys WHERE platform = ? LIMIT 1').get(platform) as { id: number } | undefined; | |
| if (existing) { | |
| db.prepare("UPDATE api_keys SET enabled = 1, status = 'unknown' WHERE id = ?").run(existing.id); | |
| res.status(200).json({ | |
| id: existing.id, | |
| platform, | |
| label: label ?? '', | |
| maskedKey: maskKey(keyToStore), | |
| status: 'unknown', | |
| enabled: true, | |
| }); | |
| return; | |
| } | |
| } | |
| const { encrypted, iv, authTag } = encrypt(keyToStore); | |
| const result = db.prepare(` | |
| INSERT INTO api_keys (platform, label, encrypted_key, iv, auth_tag, status, enabled) | |
| VALUES (?, ?, ?, ?, ?, 'unknown', 1) | |
| `).run(platform, label ?? '', encrypted, iv, authTag); | |
| res.status(201).json({ | |
| id: result.lastInsertRowid, | |
| platform, | |
| label: label ?? '', | |
| maskedKey: maskKey(keyToStore), | |
| status: 'unknown', | |
| enabled: true, | |
| }); | |
| }); | |
| // ── Custom OpenAI-compatible providers (#117, #212) ─────────────────────── | |
| // User-configured endpoints (llama.cpp / LM Studio / vLLM / Ollama / any | |
| // OpenAI-compatible base_url). Each DISTINCT base_url gets its own 'custom' | |
| // api_keys row, and every registered model binds to its endpoint's key via | |
| // models.key_id — so several custom providers coexist without overwriting | |
| // each other (#212). Re-submitting an existing base_url updates its key/label; | |
| // re-registering an existing model id re-binds it to the submitted endpoint. | |
| const customProviderSchema = z.object({ | |
| baseUrl: z.string().url('baseUrl must be a valid URL'), | |
| model: z.string().min(1, 'model is required'), | |
| displayName: z.string().optional(), | |
| apiKey: z.string().optional(), | |
| label: z.string().optional(), | |
| }); | |
| keysRouter.post('/custom', (req: Request, res: Response) => { | |
| const parsed = customProviderSchema.safeParse(req.body); | |
| if (!parsed.success) { | |
| res.status(400).json({ error: { message: parsed.error.errors.map(e => e.message).join(', ') } }); | |
| return; | |
| } | |
| const baseUrl = parsed.data.baseUrl.trim().replace(/\/+$/, ''); | |
| const modelId = parsed.data.model.trim(); | |
| const displayName = (parsed.data.displayName ?? modelId).trim(); | |
| // Local servers often need no key; keep a sentinel so there's always a bearer. | |
| const rawKey = parsed.data.apiKey?.trim() || 'no-key'; | |
| const label = parsed.data.label ?? 'Custom'; | |
| const db = getDb(); | |
| const upsert = db.transaction(() => { | |
| // One 'custom' key row PER ENDPOINT (matched on base_url). Re-submitting | |
| // the same endpoint updates its key/label; a new base_url gets its own | |
| // row instead of clobbering the previous provider. (#212) | |
| const existing = db.prepare("SELECT id FROM api_keys WHERE platform = 'custom' AND base_url = ? LIMIT 1") | |
| .get(baseUrl) as { id: number } | undefined; | |
| let keyId: number; | |
| if (existing) { | |
| const { encrypted, iv, authTag } = encrypt(rawKey); | |
| db.prepare("UPDATE api_keys SET label = ?, encrypted_key = ?, iv = ?, auth_tag = ?, status = 'unknown', enabled = 1 WHERE id = ?") | |
| .run(label, encrypted, iv, authTag, existing.id); | |
| keyId = existing.id; | |
| } else { | |
| const { encrypted, iv, authTag } = encrypt(rawKey); | |
| const r = db.prepare(` | |
| INSERT INTO api_keys (platform, label, encrypted_key, iv, auth_tag, status, enabled, base_url) | |
| VALUES ('custom', ?, ?, ?, ?, 'unknown', 1, ?) | |
| `).run(label, encrypted, iv, authTag, baseUrl); | |
| keyId = Number(r.lastInsertRowid); | |
| } | |
| // Register the model bound to THIS endpoint's key. Custom models carry no | |
| // rate limits and sort last in the intelligence preset (size_label tier). | |
| // Re-registering an existing model id re-binds it (model ids are unique | |
| // per platform, so one id can't live on two endpoints at once). | |
| db.prepare(` | |
| INSERT INTO models | |
| (platform, model_id, display_name, intelligence_rank, speed_rank, size_label, | |
| rpm_limit, rpd_limit, tpm_limit, tpd_limit, monthly_token_budget, context_window, enabled, key_id) | |
| VALUES ('custom', ?, ?, 50, 50, 'Custom', NULL, NULL, NULL, NULL, '', NULL, 1, ?) | |
| ON CONFLICT(platform, model_id) | |
| DO UPDATE SET display_name = excluded.display_name, key_id = excluded.key_id, enabled = 1 | |
| `).run(modelId, displayName, keyId); | |
| const modelRow = db.prepare("SELECT id FROM models WHERE platform = 'custom' AND model_id = ?").get(modelId) as { id: number }; | |
| // Append to the fallback chain if not already present. | |
| const inChain = db.prepare('SELECT 1 FROM fallback_config WHERE model_db_id = ?').get(modelRow.id); | |
| if (!inChain) { | |
| const max = db.prepare('SELECT COALESCE(MAX(priority), 0) AS m FROM fallback_config').get() as { m: number }; | |
| db.prepare('INSERT INTO fallback_config (model_db_id, priority, enabled) VALUES (?, ?, 1)').run(modelRow.id, max.m + 1); | |
| } | |
| return { keyId, modelDbId: modelRow.id }; | |
| }); | |
| const { keyId, modelDbId } = upsert(); | |
| res.status(201).json({ | |
| success: true, | |
| keyId, | |
| modelDbId, | |
| platform: 'custom', | |
| baseUrl, | |
| model: modelId, | |
| displayName, | |
| maskedKey: maskKey(rawKey), | |
| }); | |
| }); | |
| // Delete a key | |
| keysRouter.delete('/:id', (req: Request, res: Response) => { | |
| const id = parseInt(req.params.id as string, 10); | |
| if (isNaN(id)) { | |
| res.status(400).json({ error: { message: 'Invalid key ID' } }); | |
| return; | |
| } | |
| const db = getDb(); | |
| const row = db.prepare('SELECT platform FROM api_keys WHERE id = ?').get(id) as { platform: string } | undefined; | |
| if (!row) { | |
| res.status(404).json({ error: { message: 'Key not found' } }); | |
| return; | |
| } | |
| const remove = db.transaction(() => { | |
| db.prepare('DELETE FROM api_keys WHERE id = ?').run(id); | |
| // Custom models exist only because POST /custom registered them alongside | |
| // their endpoint key (#117) — they can't route without it. Cascade away | |
| // the models bound to THIS endpoint (#212); other custom providers keep | |
| // theirs. Legacy rows (key_id NULL) are swept once no custom keys remain, | |
| // so they never linger in the fallback chain forever (#189). | |
| if (row.platform === 'custom') { | |
| db.prepare("DELETE FROM fallback_config WHERE model_db_id IN (SELECT id FROM models WHERE platform = 'custom' AND key_id = ?)").run(id); | |
| db.prepare("DELETE FROM models WHERE platform = 'custom' AND key_id = ?").run(id); | |
| const remaining = db.prepare("SELECT COUNT(*) AS n FROM api_keys WHERE platform = 'custom'").get() as { n: number }; | |
| if (remaining.n === 0) { | |
| db.prepare("DELETE FROM fallback_config WHERE model_db_id IN (SELECT id FROM models WHERE platform = 'custom')").run(); | |
| db.prepare("DELETE FROM models WHERE platform = 'custom'").run(); | |
| } | |
| } | |
| }); | |
| remove(); | |
| res.json({ success: true }); | |
| }); | |
| // Toggle all keys for a platform | |
| keysRouter.patch('/platform/:platform', (req: Request, res: Response) => { | |
| const platform = req.params.platform as string; | |
| if (!(PLATFORMS as readonly string[]).includes(platform)) { | |
| res.status(400).json({ error: { message: `Invalid platform '${platform}'` } }); | |
| return; | |
| } | |
| const { enabled } = req.body; | |
| if (typeof enabled !== 'boolean') { | |
| res.status(400).json({ error: { message: 'enabled must be a boolean' } }); | |
| return; | |
| } | |
| const db = getDb(); | |
| const result = db.prepare('UPDATE api_keys SET enabled = ? WHERE platform = ?').run(enabled ? 1 : 0, platform); | |
| res.json({ success: true, enabled, updatedKeys: result.changes }); | |
| }); | |
| // Update key (toggle enable/disable or edit label) | |
| keysRouter.patch('/:id', (req: Request, res: Response) => { | |
| const id = parseInt(req.params.id as string, 10); | |
| if (isNaN(id)) { | |
| res.status(400).json({ error: { message: 'Invalid key ID' } }); | |
| return; | |
| } | |
| const parsed = updateKeySchema.safeParse(req.body); | |
| if (!parsed.success) { | |
| res.status(400).json({ error: { message: parsed.error.errors.map(e => e.message).join(', ') } }); | |
| return; | |
| } | |
| const { enabled, label } = parsed.data; | |
| const updates: string[] = []; | |
| const values: (string | number)[] = []; | |
| if (enabled !== undefined) { | |
| updates.push('enabled = ?'); | |
| values.push(enabled ? 1 : 0); | |
| } | |
| if (label !== undefined) { | |
| updates.push('label = ?'); | |
| values.push(label); | |
| } | |
| values.push(id); | |
| const db = getDb(); | |
| const result = db.prepare(`UPDATE api_keys SET ${updates.join(', ')} WHERE id = ?`).run(...values); | |
| if (result.changes === 0) { | |
| res.status(404).json({ error: { message: 'Key not found' } }); | |
| return; | |
| } | |
| const response: Record<string, unknown> = { success: true }; | |
| if (enabled !== undefined) response.enabled = enabled; | |
| if (label !== undefined) response.label = label; | |
| res.json(response); | |
| }); | |