From 423532cbcbac41200b0c7609fc6cfec754f465d0 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Tue, 28 Jul 2026 15:29:26 +0000 Subject: [PATCH] Order override writes per model Saving twice quickly, or saving while the one-time backfill is still running, started independent requests with no sequencing, so the older response could commit last and resurrect the entry the newer one meant to replace or remove. An API-driven load then applies context or GPU settings the user has already changed, with nothing in the UI showing it. Writes now chain per override key. The chain hangs off the settled tail, so a failed write cannot cancel the next one, and only the last writer clears the slot so a queue that is still building keeps its order. Different models still overlap. Verified against the real module under node: two saves for one model with the first made slow commit oldest-first and never overlap, where the previous version committed them in the wrong order; a rejected write still lets the next succeed; and two models still run concurrently. --- .../model-picker/api/model-overrides.ts | 31 +++++++++++++++++++ tests/studio/test_model_picker_contracts.py | 17 ++++++++++ 2 files changed, 48 insertions(+) diff --git a/studio/frontend/src/features/model-picker/api/model-overrides.ts b/studio/frontend/src/features/model-picker/api/model-overrides.ts index d4e89985b4..bf8ad1aa58 100644 --- a/studio/frontend/src/features/model-picker/api/model-overrides.ts +++ b/studio/frontend/src/features/model-picker/api/model-overrides.ts @@ -123,10 +123,41 @@ function toApiOverride(config: PerModelConfig | null): ApiModelOverride { return payload; } +// One in-flight write per model, so writes for the same model commit in the +// order they were issued. Saving twice quickly, or saving while the one-time +// backfill is still running, otherwise leaves two independent requests racing: +// the older response can land last and resurrect the entry the newer one meant +// to replace or remove, and an API load then applies settings the user has +// already changed. Different models still overlap. +const writesByKey = new Map>(); + export async function putModelOverride( modelId: string, ggufVariant: string | null | undefined, config: PerModelConfig | null, +): Promise { + const key = modelOverrideKey(modelId, ggufVariant); + // Chain on the settled tail: a failed write must not cancel the next one. + const previous = writesByKey.get(key) ?? Promise.resolve(); + const write = previous + .catch(() => {}) + .then(() => sendModelOverride(modelId, ggufVariant, config)); + writesByKey.set(key, write); + try { + await write; + } finally { + // Only the last writer clears the slot, so a queue that is still building + // keeps its ordering. + if (writesByKey.get(key) === write) { + writesByKey.delete(key); + } + } +} + +async function sendModelOverride( + modelId: string, + ggufVariant: string | null | undefined, + config: PerModelConfig | null, ): Promise { const res = await authFetch(OVERRIDES_URL, { method: "PUT", diff --git a/tests/studio/test_model_picker_contracts.py b/tests/studio/test_model_picker_contracts.py index f146de5c0f..a22ee1f5c3 100644 --- a/tests/studio/test_model_picker_contracts.py +++ b/tests/studio/test_model_picker_contracts.py @@ -763,3 +763,20 @@ def test_monitor_overlay_does_not_pull_in_the_lazy_page(): shared = _read("features/api-monitor/lifecycle.ts") assert "export function isLifecycleEntry(" in shared assert "export function lifecycleLabel(" in shared + + +def test_override_writes_are_ordered_per_model(): + """Two saves for one model, or a save racing the one-time backfill, started + independent requests with no sequencing, so the older response could commit + last and resurrect the entry the newer one meant to replace. An API load + then applies settings the user has already changed. Different models still + overlap, so a slow write for one cannot hold up another. + """ + src = " ".join(_read("features/model-picker/api/model-overrides.ts").split()) + assert "const writesByKey = new Map>();" in src + # Keyed by the same override key the server stores under. + assert "const key = modelOverrideKey(modelId, ggufVariant);" in src + # Chained on the settled tail, so one failed write cannot cancel the next. + assert "previous .catch(() => {}) .then(() => sendModelOverride(" in src + # Only the last writer clears the slot, or a queue still building loses order. + assert "if (writesByKey.get(key) === write) { writesByKey.delete(key); }" in src