diff --git a/src/extension-host/runtime-backend-arbitration.ts b/src/extension-host/runtime-backend-arbitration.ts index 41f64905be3..e65ad902de2 100644 --- a/src/extension-host/runtime-backend-arbitration.ts +++ b/src/extension-host/runtime-backend-arbitration.ts @@ -43,3 +43,22 @@ export function resolveExtensionHostRuntimeBackendOrderByArbitration(params: { ...ordered.filter((backendId) => backendId !== params.preferredBackendId), ]; } + +export function resolveExtensionHostRuntimeBackendFallbackChainByArbitration(params: { + entries: readonly ExtensionHostRuntimeBackendCatalogEntry[]; + subsystemId: ExtensionHostRuntimeBackendSubsystemId; + preferredBackendId: string; + include?: ExtensionHostRuntimeBackendArbitrationPredicate; +}): readonly string[] { + return resolveExtensionHostRuntimeBackendOrderByArbitration(params); +} + +export function resolveExtensionHostDefaultRuntimeBackendIdByArbitration(params: { + entries: readonly ExtensionHostRuntimeBackendCatalogEntry[]; + subsystemId: ExtensionHostRuntimeBackendSubsystemId; + include?: ExtensionHostRuntimeBackendArbitrationPredicate; + fallbackBackendId?: string; +}): string | undefined { + const ordered = listExtensionHostRuntimeBackendIdsByArbitration(params); + return ordered[0] ?? params.fallbackBackendId; +} diff --git a/src/extension-host/tts-runtime-policy.test.ts b/src/extension-host/tts-runtime-policy.test.ts new file mode 100644 index 00000000000..cab948fecf3 --- /dev/null +++ b/src/extension-host/tts-runtime-policy.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it, vi } from "vitest"; +import { + resolveExtensionHostDefaultTtsProvider, + resolveExtensionHostTtsFallbackProviders, +} from "./tts-runtime-policy.js"; + +vi.mock("./runtime-backend-catalog.js", () => ({ + listExtensionHostTtsRuntimeBackendCatalogEntries: vi.fn(() => [ + { + id: "capability.runtime-backend:tts:openai", + family: "capability.runtime-backend", + subsystemId: "tts", + backendId: "openai", + source: "builtin", + defaultRank: 0, + selectorKeys: ["openai"], + capabilities: ["tts.synthesis", "tts.telephony"], + }, + { + id: "capability.runtime-backend:tts:elevenlabs", + family: "capability.runtime-backend", + subsystemId: "tts", + backendId: "elevenlabs", + source: "builtin", + defaultRank: 1, + selectorKeys: ["elevenlabs"], + capabilities: ["tts.synthesis", "tts.telephony"], + }, + { + id: "capability.runtime-backend:tts:edge", + family: "capability.runtime-backend", + subsystemId: "tts", + backendId: "edge", + source: "builtin", + defaultRank: 2, + selectorKeys: ["edge"], + capabilities: ["tts.synthesis"], + }, + ]), +})); + +vi.mock("./tts-runtime-registry.js", () => ({ + isExtensionHostTtsProviderConfigured: vi.fn( + ( + config: { + configured?: string[]; + }, + provider: string, + ) => config.configured?.includes(provider) ?? false, + ), +})); + +describe("tts-runtime-policy", () => { + it("selects the highest-ranked configured provider by default", () => { + expect( + resolveExtensionHostDefaultTtsProvider({ + configured: ["elevenlabs", "edge"], + } as never), + ).toBe("elevenlabs"); + }); + + it("falls back to edge when no configured provider is available", () => { + expect(resolveExtensionHostDefaultTtsProvider({ configured: [] } as never)).toBe("edge"); + }); + + it("keeps the preferred provider first while filtering fallback providers by configuration", () => { + expect( + resolveExtensionHostTtsFallbackProviders({ + config: { configured: ["openai", "edge"] } as never, + preferredProvider: "elevenlabs", + }), + ).toEqual(["elevenlabs", "openai", "edge"]); + }); +}); diff --git a/src/extension-host/tts-runtime-policy.ts b/src/extension-host/tts-runtime-policy.ts new file mode 100644 index 00000000000..3a27c395be0 --- /dev/null +++ b/src/extension-host/tts-runtime-policy.ts @@ -0,0 +1,39 @@ +import type { TtsProvider } from "../config/types.tts.js"; +import { + resolveExtensionHostDefaultRuntimeBackendIdByArbitration, + resolveExtensionHostRuntimeBackendFallbackChainByArbitration, +} from "./runtime-backend-arbitration.js"; +import { + listExtensionHostTtsRuntimeBackendCatalogEntries, + type ExtensionHostRuntimeBackendCatalogEntry, +} from "./runtime-backend-catalog.js"; +import type { ResolvedTtsConfig } from "./tts-config.js"; +import { isExtensionHostTtsProviderConfigured } from "./tts-runtime-registry.js"; + +function isConfiguredTtsRuntimeBackend( + config: ResolvedTtsConfig, + entry: ExtensionHostRuntimeBackendCatalogEntry, +): boolean { + return isExtensionHostTtsProviderConfigured(config, entry.backendId as TtsProvider); +} + +export function resolveExtensionHostDefaultTtsProvider(config: ResolvedTtsConfig): TtsProvider { + return (resolveExtensionHostDefaultRuntimeBackendIdByArbitration({ + entries: listExtensionHostTtsRuntimeBackendCatalogEntries(), + subsystemId: "tts", + include: (entry) => isConfiguredTtsRuntimeBackend(config, entry), + fallbackBackendId: "edge", + }) ?? "edge") as TtsProvider; +} + +export function resolveExtensionHostTtsFallbackProviders(params: { + config: ResolvedTtsConfig; + preferredProvider: TtsProvider; +}): readonly TtsProvider[] { + return resolveExtensionHostRuntimeBackendFallbackChainByArbitration({ + entries: listExtensionHostTtsRuntimeBackendCatalogEntries(), + subsystemId: "tts", + preferredBackendId: params.preferredProvider, + include: (entry) => isConfiguredTtsRuntimeBackend(params.config, entry), + }).map((backendId) => backendId as TtsProvider); +} diff --git a/src/extension-host/tts-runtime-setup.test.ts b/src/extension-host/tts-runtime-setup.test.ts index a96eb8fcc75..23a2d4863ee 100644 --- a/src/extension-host/tts-runtime-setup.test.ts +++ b/src/extension-host/tts-runtime-setup.test.ts @@ -15,6 +15,38 @@ vi.mock("./runtime-backend-catalog.js", () => ({ (candidate, index, items) => items.indexOf(candidate) === index, ), ), + listExtensionHostTtsRuntimeBackendCatalogEntries: vi.fn(() => [ + { + id: "capability.runtime-backend:tts:openai", + family: "capability.runtime-backend", + subsystemId: "tts", + backendId: "openai", + source: "builtin", + defaultRank: 0, + selectorKeys: ["openai"], + capabilities: ["tts.synthesis", "tts.telephony"], + }, + { + id: "capability.runtime-backend:tts:elevenlabs", + family: "capability.runtime-backend", + subsystemId: "tts", + backendId: "elevenlabs", + source: "builtin", + defaultRank: 1, + selectorKeys: ["elevenlabs"], + capabilities: ["tts.synthesis", "tts.telephony"], + }, + { + id: "capability.runtime-backend:tts:edge", + family: "capability.runtime-backend", + subsystemId: "tts", + backendId: "edge", + source: "builtin", + defaultRank: 2, + selectorKeys: ["edge"], + capabilities: ["tts.synthesis"], + }, + ]), })); const tempDirs: string[] = []; @@ -114,7 +146,7 @@ describe("tts-runtime-setup", () => { }); }); - it("uses the override provider to build the host-owned fallback order", () => { + it("uses the override provider to build the host-owned configured fallback order", () => { const config = createResolvedConfig({ provider: "edge", providerSource: "config", @@ -130,7 +162,7 @@ describe("tts-runtime-setup", () => { }), ).toEqual({ config, - providers: ["elevenlabs", "openai", "edge"], + providers: ["elevenlabs", "edge"], }); }); }); diff --git a/src/extension-host/tts-runtime-setup.ts b/src/extension-host/tts-runtime-setup.ts index 7886011f631..21ba32761b9 100644 --- a/src/extension-host/tts-runtime-setup.ts +++ b/src/extension-host/tts-runtime-setup.ts @@ -1,8 +1,10 @@ import { existsSync, readFileSync } from "node:fs"; import type { TtsProvider } from "../config/types.tts.js"; -import { resolveExtensionHostTtsRuntimeBackendOrder } from "./runtime-backend-catalog.js"; import type { ResolvedTtsConfig } from "./tts-config.js"; -import { resolveExtensionHostTtsApiKey } from "./tts-runtime-registry.js"; +import { + resolveExtensionHostDefaultTtsProvider, + resolveExtensionHostTtsFallbackProviders, +} from "./tts-runtime-policy.js"; type TtsUserPrefs = { tts?: { @@ -35,13 +37,7 @@ export function resolveExtensionHostTtsProvider( return config.provider; } - if (resolveExtensionHostTtsApiKey(config, "openai")) { - return "openai"; - } - if (resolveExtensionHostTtsApiKey(config, "elevenlabs")) { - return "elevenlabs"; - } - return "edge"; + return resolveExtensionHostDefaultTtsProvider(config); } export function resolveExtensionHostTtsRequestSetup(params: { @@ -67,6 +63,11 @@ export function resolveExtensionHostTtsRequestSetup(params: { params.providerOverride ?? resolveExtensionHostTtsProvider(params.config, params.prefsPath); return { config: params.config, - providers: [...resolveExtensionHostTtsRuntimeBackendOrder(provider)], + providers: [ + ...resolveExtensionHostTtsFallbackProviders({ + config: params.config, + preferredProvider: provider, + }), + ], }; } diff --git a/src/extension-host/tts-status.ts b/src/extension-host/tts-status.ts index f86a8ea2c02..abb7fdc08a2 100644 --- a/src/extension-host/tts-status.ts +++ b/src/extension-host/tts-status.ts @@ -1,5 +1,4 @@ import type { TtsProvider } from "../config/types.tts.js"; -import { resolveExtensionHostTtsRuntimeBackendOrder } from "./runtime-backend-catalog.js"; import type { ResolvedTtsConfig } from "./tts-config.js"; import { getExtensionHostTtsMaxLength, @@ -7,6 +6,7 @@ import { isExtensionHostTtsSummarizationEnabled, resolveExtensionHostTtsAutoMode, } from "./tts-preferences.js"; +import { resolveExtensionHostTtsFallbackProviders } from "./tts-runtime-policy.js"; import { isExtensionHostTtsProviderConfigured, resolveExtensionHostTtsApiKey, @@ -57,9 +57,10 @@ export function resolveExtensionHostTtsStatusSnapshot(params: { }): ExtensionHostTtsStatusSnapshot { const { config, prefsPath } = params; const provider = resolveExtensionHostTtsProvider(config, prefsPath); - const fallbackProviders = resolveExtensionHostTtsRuntimeBackendOrder(provider) - .slice(1) - .filter((candidate) => isExtensionHostTtsProviderConfigured(config, candidate)); + const fallbackProviders = resolveExtensionHostTtsFallbackProviders({ + config, + preferredProvider: provider, + }).slice(1); return { enabled: isExtensionHostTtsEnabled(config, prefsPath), auto: resolveExtensionHostTtsAutoMode({ config, prefsPath }),