f887c16309
Pricing: the LiteLLM sync now adds every current Claude / GPT (>= 5) /
Gemini (>= 3) chat model before it is used and keeps its own rows
(model_pricing.source = 'sync', the Auto badge) at the published rate. A
user's row (any PUT) and a deleted pattern (model_pricing_dismissed) are
never touched; a deleted built-in row is not re-seeded at the next start; a
reset re-applies the list at once; retired Claude models are priced only
once used.
Models: GET /api/ai-tools/:id/models reads each tool's own list (Codex's
models_cache.json, Claude's aliases named after the newest version plus
priced versions, a catalog tool's vendor rows plus the models its sessions
used). A catalog `model: {flag, vendor?}` field passes `<flag> <id>`
(validated: never a flag or shell text); a per-tool default model starts
each run, and a resume keeps the session's own model.
Settings: every installed, enabled catalog tool without its own tab gets
one (AiToolConfig.tsx): path, homepage, Update, sessions/cost/lanes,
default model, vendor pricing, and the config files its release catalog
lists, edited over the opened sha, syntax-checked, backed up, written
atomically; credential-looking names refused. Antigravity keeps its hooks
card there.
Lanes: a just-started run no longer reads "dead" (age counts the run's
start while the old session is still bound); a second dashboard on the
same database re-broadcasts the lanes the other changed (PRAGMA
data_version, every 2 s); a lane start with a bad model answers 400.
DB: every migration runs under a cross-process lock (<db>-migrate.lock), so
two dashboards started at once on a fresh database both come up.
43 lines
1.6 KiB
JavaScript
43 lines
1.6 KiB
JavaScript
/**
|
|
* @file Two dashboards started at the same instant on a FRESH database must
|
|
* both come up: server/db.js runs its probe-then-ALTER migrations under one
|
|
* cross-process lock, so the second waits and then finds every column already
|
|
* there instead of dying on "duplicate column name".
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const { it } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const { spawn } = require("node:child_process");
|
|
|
|
const DB_MODULE = path.join(__dirname, "..", "db.js");
|
|
|
|
function openDb(dbPath, dataDir) {
|
|
return new Promise((resolve) => {
|
|
const child = spawn(process.execPath, ["-e", `require(${JSON.stringify(DB_MODULE)})`], {
|
|
env: { ...process.env, DASHBOARD_DB_PATH: dbPath, DASHBOARD_DATA_DIR: dataDir },
|
|
stdio: ["ignore", "ignore", "pipe"],
|
|
});
|
|
let err = "";
|
|
child.stderr.on("data", (d) => (err += d));
|
|
child.on("exit", (code) => resolve({ code, err }));
|
|
});
|
|
}
|
|
|
|
it("lets several processes open one fresh database at once", async () => {
|
|
for (let round = 0; round < 3; round++) {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "db-concurrent-"));
|
|
try {
|
|
const results = await Promise.all(
|
|
Array.from({ length: 4 }, () => openDb(path.join(dir, "dashboard.db"), dir))
|
|
);
|
|
for (const r of results) assert.equal(r.code, 0, r.err.split("\n").slice(0, 6).join("\n"));
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|