From c8b2f746f18cebe2b4dd44a9f684b47246257f70 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 9 Jun 2026 11:20:37 +0200 Subject: [PATCH] fix: persist admin-chosen default model across redeploys The startup model seed in server/db/index.ts re-applied the JSON seed's is_default flag to existing rows on every boot. An admin-set default survived restarts but was clobbered back to the models.json default (Kimi) on redeploy, when a new image re-ran the seed. Stop touching is_default on conflict so the admin's choice persists; new DBs still get the seed default via INSERT. Add a safety net that restores exactly one default if none is set (fresh DB edge cases, or the chosen default being dropped from the seed). --- server/db/index.ts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/server/db/index.ts b/server/db/index.ts index b151847..c3e2096 100644 --- a/server/db/index.ts +++ b/server/db/index.ts @@ -771,19 +771,23 @@ db.exec(`CREATE INDEX IF NOT EXISTS idx_email_messages_chat ON email_messages(ch default?: boolean; multimodal?: boolean; pi?: { provider?: string; model?: string }; }>; - // Seed-driven fields only — never overwrite admin-edited pi routing on - // existing rows. New rows still get the seed's pi block as initial values. - // Upsert. On conflict we re-apply seed-canonical fields but only update - // pi routing when the row is still at the default (openrouter / no - // override) — this lets new pi blocks in models.json reach existing DBs - // without clobbering admin customizations. + // Seed-driven fields only — never overwrite admin-edited pi routing or the + // admin-chosen default model on existing rows. New rows still get the seed's + // values as initial state. + // Upsert. On conflict we re-apply seed-canonical fields but: + // - is_default is left untouched, so the admin's choice of default model + // survives redeploys (a new image re-runs this seed). New DBs still get + // the seed default via the INSERT path; a safety net below guarantees + // exactly one default exists. + // - pi routing is only updated when the row is still at the default + // (openrouter / no override) — this lets new pi blocks in models.json + // reach existing DBs without clobbering admin customizations. const insertModel = db.prepare( `INSERT INTO models (id, name, provider, is_default, multimodal, sort_order, pi_provider, pi_model_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = excluded.name, provider = excluded.provider, - is_default = excluded.is_default, multimodal = excluded.multimodal, sort_order = excluded.sort_order, pi_provider = CASE @@ -812,6 +816,17 @@ db.exec(`CREATE INDEX IF NOT EXISTS idx_email_messages_chat ON email_messages(ch const seedIds = seedModels.map((m) => m.id); const placeholders = seedIds.map(() => "?").join(","); db.prepare(`DELETE FROM models WHERE id NOT IN (${placeholders})`).run(...seedIds); + + // Safety net: guarantee exactly one default exists. We never touch + // is_default on existing rows above, so a brand-new DB (rows just inserted + // with the seed flag) is already fine — but if no default is set (e.g. the + // admin's chosen default was dropped from the seed and deleted), fall back + // to the seed's default model. + const hasDefault = db.prepare(`SELECT 1 FROM models WHERE is_default = 1 LIMIT 1`).get(); + if (!hasDefault) { + const fallbackId = seedModels.find((m) => m.default)?.id ?? seedModels[0]?.id; + if (fallbackId) db.prepare(`UPDATE models SET is_default = 1 WHERE id = ?`).run(fallbackId); + } } // ── Exports ──