Nryn215's picture
Upload folder using huggingface_hub
077865a verified
Raw
History Blame Contribute Delete
12.2 kB
import { Router } 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',
];
// `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, res) => {
const db = getDb();
const rows = db.prepare('SELECT * FROM api_keys ORDER BY created_at DESC').all();
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, res) => {
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);
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, res) => {
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);
let keyId;
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);
// 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();
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, res) => {
const id = parseInt(req.params.id, 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);
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();
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, res) => {
const platform = req.params.platform;
if (!PLATFORMS.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, res) => {
const id = parseInt(req.params.id, 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 = [];
const values = [];
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 = { success: true };
if (enabled !== undefined)
response.enabled = enabled;
if (label !== undefined)
response.label = label;
res.json(response);
});
//# sourceMappingURL=keys.js.map