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, 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), ), }; }