diff --git a/services/agent-runtime/src/pi-runtime-models.ts b/services/agent-runtime/src/pi-runtime-models.ts index 36548cbf8..77e3d2d3f 100644 --- a/services/agent-runtime/src/pi-runtime-models.ts +++ b/services/agent-runtime/src/pi-runtime-models.ts @@ -189,6 +189,22 @@ function normalizeBackendUrl(value: string): string { return value.trim().replace(/\/+$/, ""); } +function controllerUrlIdentity(value: string): string { + const normalized = normalizeBackendUrl(value); + try { + const parsed = new URL(normalized); + const hostname = ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname.toLowerCase()) + ? "loopback" + : parsed.hostname.toLowerCase(); + const port = + parsed.port || (parsed.protocol === "https:" ? "443" : parsed.protocol === "http:" ? "80" : ""); + const pathname = parsed.pathname.replace(/\/+$/, ""); + return `${parsed.protocol}//${hostname}:${port}${pathname}`; + } catch { + return normalized; + } +} + function normalizeControllerInput(input: PiControllerModelsRequest): PiControllerConfig | null { const url = normalizeBackendUrl(input.url || ""); if (!url) return null; @@ -201,19 +217,28 @@ function normalizeControllerInput(input: PiControllerModelsRequest): PiControlle }; } -function mergeControllers( +export function mergeControllers( settings: ApiSettings, requested: PiControllerModelsRequest[] = [], ): PiControllerConfig[] { const requestedController = requested .map(normalizeControllerInput) .find((controller): controller is PiControllerConfig => controller !== null); - if (requestedController) return [requestedController]; const primary = normalizeControllerInput({ url: settings.backendUrl, apiKey: settings.apiKey, name: "primary", }); + if (requestedController) { + if ( + !requestedController.apiKey && + primary?.apiKey && + controllerUrlIdentity(requestedController.url) === controllerUrlIdentity(primary.url) + ) { + return [{ ...requestedController, apiKey: primary.apiKey }]; + } + return [requestedController]; + } return primary ? [primary] : []; } diff --git a/services/agent-runtime/test/pi-runtime-models.test.ts b/services/agent-runtime/test/pi-runtime-models.test.ts new file mode 100644 index 000000000..c6297fcb3 --- /dev/null +++ b/services/agent-runtime/test/pi-runtime-models.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { mergeControllers } from "../src/pi-runtime-models"; + +const settings = { + backendUrl: "http://127.0.0.1:8080", + apiKey: "saved-controller-key", + voiceUrl: "", + voiceModel: "whisper-large-v3-turbo", +}; + +describe("mergeControllers", () => { + it("uses the persisted key for the matching requested controller", () => { + expect(mergeControllers(settings, [{ url: "http://127.0.0.1:8080" }])).toEqual([ + { url: "http://127.0.0.1:8080", apiKey: "saved-controller-key" }, + ]); + }); + + it("treats loopback aliases as the same controller", () => { + expect(mergeControllers(settings, [{ url: "http://localhost:8080" }])).toEqual([ + { url: "http://localhost:8080", apiKey: "saved-controller-key" }, + ]); + }); + + it("does not send a persisted key to a different controller", () => { + expect(mergeControllers(settings, [{ url: "http://example.test:8080" }])).toEqual([ + { url: "http://example.test:8080", apiKey: "" }, + ]); + }); + + it("keeps an explicitly requested key", () => { + expect( + mergeControllers(settings, [ + { url: "http://localhost:8080", apiKey: "requested-controller-key" }, + ]), + ).toEqual([{ url: "http://localhost:8080", apiKey: "requested-controller-key" }]); + }); +});