feat: model lists and prices that follow each AI tool, a Settings tab per tool, live lane status (#40)

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.
This commit is contained in:
2026-10-01 13:39:17 +07:00
parent c5aba094bd
commit f887c16309
51 changed files with 2343 additions and 203 deletions
+10 -2
View File
@@ -147,12 +147,15 @@ function buildCodexArgv({
/**
* Any catalog tool without a hand-written launcher: `[bin, ...args, ...resume?,
* ...bypass?, ...prompt]`. Every piece but the prompt comes from the catalog
* ...bypass?, ...prompt]` (`[flag, model]` right after the fixed args when the
* catalog declares a model flag). Every piece but the prompt and the
* validated model comes from the catalog
* file, and the prompt is one argv element — or, for `after-start`, nothing
* here: spawnRun pastes it into the pane once the CLI is up.
*/
function buildCatalogArgv(entry, bin, { resumeLast, bypass, initialPrompt }) {
function buildCatalogArgv(entry, bin, { model, resumeLast, bypass, initialPrompt }) {
const argv = [bin, ...entry.args];
if (model && entry.model) argv.push(entry.model.flag, model);
if (resumeLast && entry.resume) argv.push(...entry.resume);
if (bypass && entry.bypass) argv.push(...entry.bypass);
const p = entry.prompt;
@@ -239,6 +242,11 @@ function spawnRun(args) {
`permissionMode must be one of: ${Array.from(ALLOWED_PERMISSION_MODES).join(", ")}`
);
}
// A model is one argv element after a model flag; one starting with `-`
// would be read as a flag of its own.
if (model != null && model !== "" && !aiTools.validModel(model)) {
throw makeErr("EBADMODEL", "model is not a valid model id");
}
if (effort != null && effort !== "" && !EFFORT_LEVELS.has(effort)) {
throw makeErr("EBADEFFORT", `effort must be one of: ${Array.from(EFFORT_LEVELS).join(", ")}`);
}