From 73294a85f2b0d5669c7bcefa8f4fcdc9ec4fce0c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 25 Aug 2026 14:16:10 -0700 Subject: [PATCH] refactor(tts): consolidate speech provider ordering (#129508) Amp-Thread-ID: https://ampcode.com/threads/T-01a037b7-785b-75eb-8426-ad67094e61d4 Co-authored-by: Amp --- src/tts/directives.ts | 16 +++------------- src/tts/provider-registry-core.ts | 13 +++++++++++++ src/tts/provider-registry.test.ts | 5 +++-- src/tts/tts-provider-resolution.ts | 10 ++-------- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/tts/directives.ts b/src/tts/directives.ts index 93f79dee90e7..fb45199a98e4 100644 --- a/src/tts/directives.ts +++ b/src/tts/directives.ts @@ -4,6 +4,7 @@ import type { OpenClawConfig } from "../config/types.js"; import type { AssistantDeliveryTtsFacts } from "../llm/types.js"; import type { SpeechProviderPlugin } from "../plugins/types.js"; import { extractTtsDirectiveFacts } from "./directive-facts.js"; +import { compareSpeechProviderOrder } from "./provider-registry-core.js"; import { listSpeechProviders } from "./provider-registry.js"; import type { SpeechModelOverridePolicy, @@ -31,20 +32,9 @@ type TtsDirectiveTextStreamCleaner = { hasBufferedDirectiveText: () => boolean; }; -function buildProviderOrder(left: SpeechProviderPlugin, right: SpeechProviderPlugin): number { - const leftOrder = left.autoSelectOrder ?? Number.MAX_SAFE_INTEGER; - const rightOrder = right.autoSelectOrder ?? Number.MAX_SAFE_INTEGER; - if (leftOrder !== rightOrder) { - return leftOrder - rightOrder; - } - return left.id.localeCompare(right.id); -} - function resolveDirectiveProviders(options?: ParseTtsDirectiveOptions): SpeechProviderPlugin[] { - if (options?.providers) { - return [...options.providers].toSorted(buildProviderOrder); - } - return listSpeechProviders(options?.cfg).toSorted(buildProviderOrder); + const providers = options?.providers ?? listSpeechProviders(options?.cfg); + return providers.toSorted(compareSpeechProviderOrder); } function resolveDirectiveProviderConfig( diff --git a/src/tts/provider-registry-core.ts b/src/tts/provider-registry-core.ts index 907c18ee70fa..dc3df3cdcb01 100644 --- a/src/tts/provider-registry-core.ts +++ b/src/tts/provider-registry-core.ts @@ -20,6 +20,19 @@ export function normalizeSpeechProviderId( return normalizeCapabilityProviderId(providerId); } +/** Order speech providers by priority and provider ID for deterministic equal-priority fallback. */ +export function compareSpeechProviderOrder( + left: SpeechProviderPlugin, + right: SpeechProviderPlugin, +): number { + const leftOrder = left.autoSelectOrder ?? Number.MAX_SAFE_INTEGER; + const rightOrder = right.autoSelectOrder ?? Number.MAX_SAFE_INTEGER; + if (leftOrder !== rightOrder) { + return leftOrder - rightOrder; + } + return left.id.localeCompare(right.id); +} + /** Create a registry facade with canonical listing, alias lookup, and ID canonicalization. */ export function createSpeechProviderRegistry(resolver: SpeechProviderRegistryResolver) { const buildResolvedProviderMaps = (cfg?: OpenClawConfig) => diff --git a/src/tts/provider-registry.test.ts b/src/tts/provider-registry.test.ts index 7df00e597209..8c7ea3f5a89b 100644 --- a/src/tts/provider-registry.test.ts +++ b/src/tts/provider-registry.test.ts @@ -96,10 +96,11 @@ describe("speech provider registry", () => { expect(registry.canonicalizeSpeechProviderId("edge")).toBe("microsoft"); }); - it("resolves fallback order and aliases from a supplied provider inventory", () => { + it("resolves deterministic fallback order and aliases from a supplied provider inventory", () => { const inventory = [ { ...createSpeechProvider("openai", ["oai"]), autoSelectOrder: 5 }, { ...createSpeechProvider("google"), autoSelectOrder: 1 }, + { ...createSpeechProvider("azure"), autoSelectOrder: 1 }, { ...createSpeechProvider("elevenlabs"), autoSelectOrder: 3 }, ]; @@ -109,7 +110,7 @@ describe("speech provider registry", () => { undefined, inventory, ), - ).toEqual(["openai", "google", "elevenlabs"]); + ).toEqual(["openai", "azure", "google", "elevenlabs"]); }); it("selects the first configured provider entirely from prepared facts", () => { diff --git a/src/tts/tts-provider-resolution.ts b/src/tts/tts-provider-resolution.ts index a2f048c18c1c..e39c979cde85 100644 --- a/src/tts/tts-provider-resolution.ts +++ b/src/tts/tts-provider-resolution.ts @@ -10,6 +10,7 @@ import type { TtsProvider, } from "../config/types.js"; import type { SpeechProviderPlugin } from "../plugins/types.js"; +import { compareSpeechProviderOrder } from "./provider-registry-core.js"; import { canonicalizeSpeechProviderId, getSpeechProvider, @@ -64,14 +65,7 @@ function sortSpeechProvidersForAutoSelection( cfg?: OpenClawConfig, providers?: readonly SpeechProviderPlugin[], ) { - return [...(providers ?? listSpeechProviders(cfg))].toSorted((left, right) => { - const leftOrder = left.autoSelectOrder ?? Number.MAX_SAFE_INTEGER; - const rightOrder = right.autoSelectOrder ?? Number.MAX_SAFE_INTEGER; - if (leftOrder !== rightOrder) { - return leftOrder - rightOrder; - } - return left.id.localeCompare(right.id); - }); + return [...(providers ?? listSpeechProviders(cfg))].toSorted(compareSpeechProviderOrder); } function canonicalizeSpeechProviderIdFromInventory(