From 0fd25deb50defbe61ba168f10895880e37201644 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 16 Sep 2026 23:08:33 +0200 Subject: [PATCH] fix: the workflow image model is saved, not just accepted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PUT handler read budget and fallbacks and never looked at body.model, so picking a different model, pressing Save and refreshing put the old one back. The frontend had already been fixed to send it for both workflows; only the write was missing, which is why the card reported "Saved" and the database kept the previous value. image_model is now written when the field is present. Blank is a real value for my_resources ("use the Clinical Assistant's model"), so an empty string is stored rather than skipped, and only an absent field leaves it alone. The fallback de-duplication compared against the saved primary rather than the one this request is setting, so a model promoted to primary and left in the chain in the same save survived as its own fallback — a paid retry against the same refusal. --- src/routes/generatedImages.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/routes/generatedImages.js b/src/routes/generatedImages.js index 2c27f39c..ad24dff7 100644 --- a/src/routes/generatedImages.js +++ b/src/routes/generatedImages.js @@ -167,6 +167,17 @@ router.put('/admin/image-settings/:workflow', adminMiddleware, async (req, res) if (!IMAGE_WORKFLOWS.includes(workflow)) throw images.failure(404, 'Workflow not found'); const budget = images.budgetLimit(req.body.budget); + // The primary model — the field this endpoint used to accept and drop, so a + // saved selection never survived a refresh. Blank is a real value for + // my_resources ("use the Clinical Assistant's model"), so an empty string is + // written rather than skipped, and only an absent field leaves it alone. + let model = null; + if (req.body.model !== undefined) { + const id = String(req.body.model || '').trim(); + if (id && !MODEL_ID.test(id)) throw images.failure(400, 'Not a model ID: ' + id.slice(0, 60)); + model = id; + } + // Fallbacks are optional, and only sent when the caller means to change // them: an absent field leaves the saved chain alone rather than clearing it. let fallbacks = null; @@ -178,8 +189,10 @@ router.put('/admin/image-settings/:workflow', adminMiddleware, async (req, res) if (!MODEL_ID.test(id)) throw images.failure(400, 'Not a model ID: ' + id.slice(0, 60)); } // The primary counts toward the cap, and naming it again would pay twice - // for the same refusal. - const primary = String(await db.getSetting(workflow + '.image_model') || ''); + // for the same refusal. Compare against the model this request is setting, + // not the one it replaces, or a model promoted to primary in the same save + // survives as its own fallback. + const primary = model !== null ? model : String(await db.getSetting(workflow + '.image_model') || ''); fallbacks = fallbacks.filter((id, i) => id !== primary && fallbacks.indexOf(id) === i); if (fallbacks.length > images.MAX_IMAGE_MODELS - 1) { throw images.failure(400, 'At most ' + (images.MAX_IMAGE_MODELS - 1) + @@ -191,6 +204,7 @@ router.put('/admin/image-settings/:workflow', adminMiddleware, async (req, res) try { await client.query('BEGIN'); const changes = { image_budget: budget }; + if (model !== null) changes.image_model = model; if (fallbacks !== null) changes.fallback_image_models = fallbacks.join(','); for (const [key, value] of Object.entries(changes)) { await client.query('INSERT INTO app_settings(key,value) VALUES($1,$2) ON CONFLICT(key) DO UPDATE SET value=$2,updated_at=NOW()', [workflow + '.' + key, String(value)]);