Files
studyos/server/routes/models.js
renato97 4ff4302a8c feat: implement 33 nice-to-have features + fix 37 code review bugs
5 SDD batches archived:
- Batch 1: UI Polish (10 features, 14 tasks)
- Batch 2: Study System (8 features, 23 tasks)
- Batch 3: Infrastructure (5 features, 22 tasks)
- Batch 4: AI Advanced (5 features, 30 tasks) — RAG with @xenova/transformers
- Batch 5: Core Features (5 features, 19 tasks)

37 bugs fixed from comprehensive code review (11 CRITICAL, 12 HIGH, 14 MEDIUM/LOW):
- SSE streaming now works (event.token check)
- API keys no longer exposed via GET /api/models
- FTS5 injection sanitized
- DB backup/restore with admin auth
- Buddy mode wired (buddy_meta column)
- Exam auto-submit stale closure fixed
- CSS variables aligned with design tokens
- Progress data corruption fixed
- WebSocket protocol auto-detection
- Tests infrastructure completed (vitest + node:test)
2026-06-08 18:18:47 -03:00

184 lines
5.9 KiB
JavaScript

const express = require('express');
const db = require('../db');
const { streamCompletion } = require('../lib/llm');
const router = express.Router();
const MODEL_PUBLIC_COLS = 'id, name, api_base, provider, is_default_main, is_default_fork, is_default_exam';
// GET /api/models — list all
router.get('/', (req, res) => {
try {
const rows = db.prepare(`SELECT ${MODEL_PUBLIC_COLS} FROM models ORDER BY id`).all();
res.json(rows);
} catch (err) {
console.error('[models] list error:', err.message);
res.status(500).json({ error: err.message });
}
});
// GET /api/models/:id — get single model (no api_key)
router.get('/:id', (req, res) => {
const id = parseInt(req.params.id, 10);
if (Number.isNaN(id)) {
return res.status(400).json({ error: 'Invalid model id' });
}
try {
const row = db.prepare(`SELECT ${MODEL_PUBLIC_COLS} FROM models WHERE id = ?`).get(id);
if (!row) {
return res.status(404).json({ error: 'Model not found' });
}
res.json(row);
} catch (err) {
console.error('[models] get error:', err.message);
res.status(500).json({ error: err.message });
}
});
// POST /api/models — create
router.post('/', (req, res) => {
const { name, api_base, api_key, provider, is_default_main, is_default_fork, is_default_exam } = req.body;
if (!name || !api_base || !provider) {
return res.status(400).json({ error: 'name, api_base, and provider are required' });
}
try {
const info = db.prepare(`
INSERT INTO models (name, api_base, api_key, provider, is_default_main, is_default_fork, is_default_exam)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
name,
api_base,
api_key || '',
provider,
is_default_main ? 1 : 0,
is_default_fork ? 1 : 0,
is_default_exam ? 1 : 0
);
// If setting a default flag, unset others for that role
const newId = info.lastInsertRowid;
if (is_default_main) unsetOtherDefaults(newId, 'is_default_main');
if (is_default_fork) unsetOtherDefaults(newId, 'is_default_fork');
if (is_default_exam) unsetOtherDefaults(newId, 'is_default_exam');
const row = db.prepare(`SELECT ${MODEL_PUBLIC_COLS} FROM models WHERE id = ?`).get(newId);
res.status(201).json(row);
} catch (err) {
console.error('[models] create error:', err.message);
res.status(500).json({ error: err.message });
}
});
// PUT /api/models/:id — update
router.put('/:id', (req, res) => {
const id = parseInt(req.params.id, 10);
if (Number.isNaN(id)) {
return res.status(400).json({ error: 'Invalid model id' });
}
const { name, api_base, api_key, provider, is_default_main, is_default_fork, is_default_exam } = req.body;
try {
const existing = db.prepare('SELECT * FROM models WHERE id = ?').get(id);
if (!existing) {
return res.status(404).json({ error: 'Model not found' });
}
db.prepare(`
UPDATE models SET
name = COALESCE(?, name),
api_base = COALESCE(?, api_base),
api_key = ?,
provider = COALESCE(?, provider),
is_default_main = COALESCE(?, is_default_main),
is_default_fork = COALESCE(?, is_default_fork),
is_default_exam = COALESCE(?, is_default_exam)
WHERE id = ?
`).run(
name || null,
api_base || null,
api_key !== undefined ? (api_key === null ? '' : api_key) : null,
provider || null,
is_default_main !== undefined ? (is_default_main ? 1 : 0) : null,
is_default_fork !== undefined ? (is_default_fork ? 1 : 0) : null,
is_default_exam !== undefined ? (is_default_exam ? 1 : 0) : null,
id
);
if (is_default_main) unsetOtherDefaults(id, 'is_default_main');
if (is_default_fork) unsetOtherDefaults(id, 'is_default_fork');
if (is_default_exam) unsetOtherDefaults(id, 'is_default_exam');
const row = db.prepare(`SELECT ${MODEL_PUBLIC_COLS} FROM models WHERE id = ?`).get(id);
res.json(row);
} catch (err) {
console.error('[models] update error:', err.message);
res.status(500).json({ error: err.message });
}
});
// DELETE /api/models/:id
router.delete('/:id', (req, res) => {
const id = parseInt(req.params.id, 10);
if (Number.isNaN(id)) {
return res.status(400).json({ error: 'Invalid model id' });
}
try {
// Reject if conversations reference this model
const convCount = db.prepare('SELECT COUNT(*) as count FROM conversations WHERE model_id = ?').get(id);
if (convCount.count > 0) {
return res.status(409).json({ error: 'Cannot delete model referenced by conversations' });
}
const info = db.prepare('DELETE FROM models WHERE id = ?').run(id);
if (info.changes === 0) {
return res.status(404).json({ error: 'Model not found' });
}
res.json({ deleted: true });
} catch (err) {
console.error('[models] delete error:', err.message);
res.status(500).json({ error: err.message });
}
});
// POST /api/models/:id/test — send "di hola" and return latency
router.post('/:id/test', async (req, res) => {
const id = parseInt(req.params.id, 10);
if (Number.isNaN(id)) {
return res.status(400).json({ error: 'Invalid model id' });
}
try {
const model = db.prepare('SELECT * FROM models WHERE id = ?').get(id);
if (!model) {
return res.status(404).json({ error: 'Model not found' });
}
const start = Date.now();
let fullText = '';
for await (const chunk of streamCompletion(model, [{ role: 'user', content: 'di hola' }], '')) {
if (chunk.error) {
return res.status(502).json({ error: chunk.error });
}
if (chunk.done) {
fullText = chunk.fullText;
}
}
const latency = Date.now() - start;
res.json({ latency_ms: latency, response: fullText.trim() });
} catch (err) {
console.error('[models] test error:', err.message);
res.status(500).json({ error: err.message });
}
});
function unsetOtherDefaults(exceptId, column) {
db.prepare(`UPDATE models SET ${column} = 0 WHERE id != ?`).run(exceptId);
}
module.exports = router;