Add admin LLM model setting; rename Pi to LLM in the UI; drop table Review column
Test and deploy / test-and-deploy (push) Successful in 49s

- New server/llm.js consolidates ModelRuntime + model selection: admin
  setting (app_settings.llm_model) > LLM_MODEL/PREFILL_MODEL env > first
  available; used by both prefill and roast evaluation
- GET/PUT /api/admin/llm lists configured models and stores the choice
  (validated against the list; empty = auto; audited); admin page gains
  an LLM section with a model picker
- All user-facing 'Pi agent' wording is now 'LLM'; no_model error message
  no longer references the pi CLI
- /roasts table: Review column removed (review lives in the detail view)

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Shane Maynard
2026-08-08 22:12:42 -04:00
co-authored by Claude Fable 5
parent eb82263ead
commit 5efaeb63c9
14 changed files with 344 additions and 87 deletions
+72 -3
View File
@@ -7,6 +7,7 @@ import { runPrefill } from "./prefill.js";
import { parseAlog } from "./alog.js";
import { listAlogLibrary, readAlogFromLibrary } from "./alog-library.js";
import { evaluateRoast as defaultEvaluateRoast } from "./evaluate-roast.js";
import { listAvailableModels as defaultListModels } from "./llm.js";
import { sendMail } from "./mailer.js";
import { coerceSession, computeTotalScore, blankSession } from "../shared/cupping.js";
import { computeMachineProfile } from "../shared/learn.js";
@@ -47,8 +48,9 @@ export function createApp({
db,
root,
env = process.env,
// Injectable so tests can stub the Pi-agent call; production always uses the real one.
// Injectable so tests can stub the LLM calls; production always uses the real ones.
evaluateRoast = defaultEvaluateRoast,
listModels = defaultListModels,
} = {}) {
const app = express();
const production = env.NODE_ENV === "production";
@@ -1119,6 +1121,13 @@ export function createApp({
},
);
// Admin-chosen LLM model ("provider:id"), empty = auto. Read per call so a change takes
// effect immediately without restarting the app.
const llmModelSetting = async () =>
(
await db.query("SELECT value FROM app_settings WHERE key='llm_model'")
).rows[0]?.value || "";
// ─── Actual roasts (finished .alog uploads) ────────────────────────────
const toRoastRow = (row, { full = false } = {}) => {
const parsed = row.parsed || {};
@@ -1154,7 +1163,11 @@ export function createApp({
).rows[0];
if (!row) return;
try {
const evaluation = await evaluateRoast(row.parsed, row.plan ?? null);
const evaluation = await evaluateRoast(
row.parsed,
row.plan ?? null,
await llmModelSetting(),
);
await db.query(
"UPDATE actual_roasts SET evaluation=$1, evaluation_status='done', evaluation_error=NULL, updated_at=now() WHERE id=$2",
[evaluation, roastId],
@@ -1727,6 +1740,59 @@ export function createApp({
res.json({ ok: true, links });
},
);
// The LLM model picker: which configured model runs prefill and roast reviews. Listing can
// legitimately fail (no models configured yet) — the GET still succeeds so the admin page can
// say so instead of erroring, and only a non-empty PUT requires the list for validation.
app.get("/api/admin/llm", requireAuth, admin, async (req, res, next) => {
try {
const current = await llmModelSetting();
let models = [];
let modelsError = null;
try {
models = await listModels();
} catch (e) {
modelsError = "llm_unavailable";
}
res.json({ ok: true, current, models, modelsError });
} catch (e) {
next(e);
}
});
app.put(
"/api/admin/llm",
requireAuth,
csrf,
admin,
async (req, res, next) => {
try {
const model = req.body.model;
if (typeof model !== "string" || model.length > 200)
return res.status(400).json({ ok: false, code: "bad_request" });
if (model !== "") {
let models;
try {
models = await listModels();
} catch (e) {
return res
.status(503)
.json({ ok: false, code: "llm_unavailable" });
}
if (!models.some((m) => m.key === model))
return res
.status(400)
.json({ ok: false, code: "unknown_model" });
}
await db.query(
"UPDATE app_settings SET value=$1 WHERE key='llm_model'",
[model],
);
await audit(req.user.id, "llm_model_changed", model || "auto");
res.json({ ok: true, current: model });
} catch (e) {
next(e);
}
},
);
app.put(
"/api/admin/signup-enabled",
requireAuth,
@@ -1837,7 +1903,10 @@ export function createApp({
.status(400)
.json({ ok: false, code: "bad_url", error: "Missing url." });
try {
const result = await runPrefill(await fetchPageText(url));
const result = await runPrefill(
await fetchPageText(url),
await llmModelSetting(),
);
res.json({ ok: true, ...result });
} catch (err) {
const code = err.code ?? "prefill_failed";