fix: the workflow image model is saved, not just accepted

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.
This commit is contained in:
Daniel 2026-09-16 23:08:33 +02:00
parent 87c69ce151
commit 0fd25deb50

View file

@ -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)]);