From e44b9521b2d8dd5e2d0015aeb2480e2ef27968be Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 14:57:09 -0700 Subject: [PATCH 1/2] test(web): a model swap never declares the new offering twice (CL-8591) --- .../src/settings/myra-model-redeploy.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/apps/web/src/settings/myra-model-redeploy.test.ts b/apps/web/src/settings/myra-model-redeploy.test.ts index 0d8c9788b..c8cefa917 100644 --- a/apps/web/src/settings/myra-model-redeploy.test.ts +++ b/apps/web/src/settings/myra-model-redeploy.test.ts @@ -45,6 +45,33 @@ describe("swapDeclaredOffering", () => { expect(result?.sourceOfferingIds).toEqual(["off_new", "off_b", "off_c"]); }); + test("drops the duplicate when the new offering is already declared", () => { + // The new offering is minted before the current list is read, so the + // swap would otherwise declare it twice and the hub rejects that. + const before = { + sourceOfferingIds: ["off_old", "off_new"], + defaultSourceOfferingId: "off_old", + declaredSources: [ + { provider: "openai-compatible" as const, model: "qwen2.5:7b" }, + { provider: "openai-compatible" as const, model: "llama3.2:1b" }, + ], + }; + + const result = swapDeclaredOffering( + before, + "off_old", + "off_new", + "openai-compatible", + "llama3.2:1b", + ); + + expect(result).toEqual({ + sourceOfferingIds: ["off_new"], + defaultSourceOfferingId: "off_new", + declaredSources: [{ provider: "openai-compatible", model: "llama3.2:1b" }], + }); + }); + test("returns null when the old offering id isn't declared at all", () => { const result = swapDeclaredOffering( BEFORE, From 389817d18127a960b6b9f6db3810f82904bccb1b Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 14:57:09 -0700 Subject: [PATCH 2/2] fix(web): a model change declares each offering once so the redeploy is accepted (CL-8591) --- apps/web/src/settings/myra-model-redeploy.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/web/src/settings/myra-model-redeploy.ts b/apps/web/src/settings/myra-model-redeploy.ts index 9d47e65e9..c6b223135 100644 --- a/apps/web/src/settings/myra-model-redeploy.ts +++ b/apps/web/src/settings/myra-model-redeploy.ts @@ -67,16 +67,23 @@ export function swapDeclaredOffering( } | null { const index = before.sourceOfferingIds.indexOf(oldOfferingId); if (index === -1) return null; + const swappedIds = before.sourceOfferingIds.map((id) => + id === oldOfferingId ? newOfferingId : id, + ); + const swappedSources = before.declaredSources.map((source, position) => + position === index ? { provider, model: newCanonicalName } : source, + ); + // The new offering is minted before the current list is read, so it can + // already be present; the hub rejects a chain that names an id twice. + const firstSeen = (id: string, position: number) => swappedIds.indexOf(id) === position; return { - sourceOfferingIds: before.sourceOfferingIds.map((id) => - id === oldOfferingId ? newOfferingId : id, - ), + sourceOfferingIds: swappedIds.filter(firstSeen), defaultSourceOfferingId: before.defaultSourceOfferingId === oldOfferingId ? newOfferingId : before.defaultSourceOfferingId, - declaredSources: before.declaredSources.map((source, position) => - position === index ? { provider, model: newCanonicalName } : source, + declaredSources: swappedSources.filter((_, position) => + firstSeen(swappedIds[position] ?? "", position), ), }; }