diff --git a/src/server/management/routing-profile-routes.ts b/src/server/management/routing-profile-routes.ts index 36687c01b3..79a1cf0115 100644 --- a/src/server/management/routing-profile-routes.ts +++ b/src/server/management/routing-profile-routes.ts @@ -175,10 +175,10 @@ function modelMapMigrationCollision( const map = config.claudeCode?.modelMap; if (!map) return null; if (oldPublicModel === newPublicModel) return null; + if (!Object.hasOwn(map, oldPublicModel)) return null; const oldTarget = map[oldPublicModel]; - if (oldTarget === undefined) return null; + if (!Object.hasOwn(map, newPublicModel)) return null; const newTarget = map[newPublicModel]; - if (newTarget === undefined) return null; if (oldTarget === newTarget) return null; return `modelMap already maps \"${newPublicModel}\" to \"${newTarget}\"; renaming \"${oldPublicModel}\" (→ \"${newTarget}\") would drop one mapping. Resolve the conflict and retry.`; } diff --git a/tests/routing-profile-management-editor.test.ts b/tests/routing-profile-management-editor.test.ts index 08fc82aa27..859148b6ca 100644 --- a/tests/routing-profile-management-editor.test.ts +++ b/tests/routing-profile-management-editor.test.ts @@ -453,6 +453,38 @@ describe("routing profile management editor API", () => { expect(saves).toBe(0); }); + test("PUT update ignores inherited modelMap properties when checking alias collisions", async () => { + const config = baseConfig(); + config.claudeCode = { + enabled: false, + modelMap: { "ocx/fast": "a/m1" }, + }; + let saves = 0; + const req = new ManagementRequest("http://localhost/api/routing-profiles", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + id: "fast", + mode: "update", + profile: { + alias: "constructor", + candidates: [{ provider: "a", model: "m1" }], + }, + }), + }); + const response = await handleManagementAPI( + req, + new URL(req.url), + config, + deps(() => { saves += 1; }), + ); + + expect(response?.status).toBe(200); + expect(config.routingProfiles?.fast).toMatchObject({ alias: "constructor" }); + expect(config.claudeCode?.modelMap).toEqual({ constructor: "a/m1" }); + expect(saves).toBe(1); + }); + test("DELETE removes a profile, persists, and refreshes the catalog", async () => { const config = baseConfig(); let saves = 0;