From 8d3d742c6a2f641c103c0c2b7705224b6e902802 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 16:08:22 +0000 Subject: [PATCH] refactor: require canonical talk resolved payload --- .../ai/openclaw/app/voice/TalkModeManager.kt | 27 +++------------ .../app/voice/TalkModeConfigParsingTest.kt | 5 +-- apps/ios/Sources/Voice/TalkModeManager.swift | 2 +- .../Tests/Logic/TalkConfigParsingTests.swift | 5 ++- .../Sources/OpenClaw/TalkModeRuntime.swift | 5 ++- .../TalkModeConfigParsingTests.swift | 6 ++-- .../OpenClawKit/TalkConfigParsing.swift | 34 ++----------------- .../TalkConfigParsingTests.swift | 6 ++-- 8 files changed, 18 insertions(+), 72 deletions(-) diff --git a/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt b/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt index 98823e0b216..3bd52a3a0a0 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt @@ -93,29 +93,7 @@ class TalkModeManager( val rawProviders = talk["providers"].asObjectOrNull() val hasNormalizedPayload = rawProvider != null || rawProviders != null if (hasNormalizedPayload) { - val providers = - rawProviders?.entries?.mapNotNull { (key, value) -> - val providerId = normalizeTalkProviderId(key) ?: return@mapNotNull null - val providerConfig = value.asObjectOrNull() ?: return@mapNotNull null - providerId to providerConfig - }?.toMap().orEmpty() - val explicitProviderId = normalizeTalkProviderId(rawProvider) - if (explicitProviderId != null) { - if (providers.isNotEmpty() && providers[explicitProviderId] == null) { - return null - } - return TalkProviderConfigSelection( - provider = explicitProviderId, - config = providers[explicitProviderId] ?: buildJsonObject {}, - normalizedPayload = true, - ) - } - val providerId = providers.keys.singleOrNull() ?: return null - return TalkProviderConfigSelection( - provider = providerId, - config = providers[providerId] ?: buildJsonObject {}, - normalizedPayload = true, - ) + return null } return TalkProviderConfigSelection( provider = defaultTalkProvider, @@ -1425,6 +1403,9 @@ class TalkModeManager( val config = root?.get("config").asObjectOrNull() val talk = config?.get("talk").asObjectOrNull() val selection = selectTalkProviderConfig(talk) + if (talk != null && selection == null) { + Log.w(tag, "talk config ignored: normalized payload missing talk.resolved") + } val activeProvider = selection?.provider ?: defaultTalkProvider val activeConfig = selection?.config val sessionCfg = config?.get("session").asObjectOrNull() diff --git a/apps/android/app/src/test/java/ai/openclaw/app/voice/TalkModeConfigParsingTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/voice/TalkModeConfigParsingTest.kt index 04fae5be2c7..4ccee1cdf69 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/voice/TalkModeConfigParsingTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/voice/TalkModeConfigParsingTest.kt @@ -62,10 +62,7 @@ class TalkModeConfigParsingTest { .jsonObject val selection = TalkModeManager.selectTalkProviderConfig(talk) - assertNotNull(selection) - assertEquals("elevenlabs", selection?.provider) - assertTrue(selection?.normalizedPayload == true) - assertEquals("voice-normalized", selection?.config?.get("voiceId")?.jsonPrimitive?.content) + assertEquals(null, selection) } @Test diff --git a/apps/ios/Sources/Voice/TalkModeManager.swift b/apps/ios/Sources/Voice/TalkModeManager.swift index ad972af7f45..9e963c17a16 100644 --- a/apps/ios/Sources/Voice/TalkModeManager.swift +++ b/apps/ios/Sources/Voice/TalkModeManager.swift @@ -1996,7 +1996,7 @@ extension TalkModeManager { let selection = Self.selectTalkProviderConfig(talk) if talk != nil, selection == nil { GatewayDiagnostics.log( - "talk config ignored: legacy payload unsupported on iOS beta; expected talk.provider/providers") + "talk config ignored: normalized payload missing talk.resolved") } let activeProvider = selection?.provider ?? Self.defaultTalkProvider let activeConfig = selection?.config diff --git a/apps/ios/Tests/Logic/TalkConfigParsingTests.swift b/apps/ios/Tests/Logic/TalkConfigParsingTests.swift index 6c99b85dea2..c7fb9b0e209 100644 --- a/apps/ios/Tests/Logic/TalkConfigParsingTests.swift +++ b/apps/ios/Tests/Logic/TalkConfigParsingTests.swift @@ -5,7 +5,7 @@ import Testing private let iOSSilenceTimeoutMs = 900 @Suite struct TalkConfigParsingTests { - @Test func prefersNormalizedTalkProviderPayload() { + @Test func rejectsNormalizedTalkProviderPayloadWithoutResolved() { let talk: [String: Any] = [ "provider": "elevenlabs", "providers": [ @@ -20,8 +20,7 @@ private let iOSSilenceTimeoutMs = 900 TalkConfigParsing.bridgeFoundationDictionary(talk), defaultProvider: "elevenlabs", allowLegacyFallback: false) - #expect(selection?.provider == "elevenlabs") - #expect(selection?.config["voiceId"]?.stringValue == "voice-normalized") + #expect(selection == nil) } @Test func ignoresLegacyTalkFieldsWhenNormalizedPayloadMissing() { diff --git a/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift b/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift index ad5d7e3a886..8013135b330 100644 --- a/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift +++ b/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift @@ -831,6 +831,9 @@ extension TalkModeRuntime { timeoutMs: 8000) let talk = snap.config?["talk"]?.dictionaryValue let selection = Self.selectTalkProviderConfig(talk) + if talk != nil, selection == nil { + self.ttsLogger.info("talk config ignored: normalized payload missing talk.resolved") + } let activeProvider = selection?.provider ?? Self.defaultTalkProvider let activeConfig = selection?.config let silenceTimeoutMs = Self.resolvedSilenceTimeoutMs(talk) @@ -870,7 +873,7 @@ extension TalkModeRuntime { self.ttsLogger .info("talk provider \(activeProvider, privacy: .public) unsupported; using system voice") } else if selection?.normalizedPayload == true { - self.ttsLogger.info("talk config provider elevenlabs") + self.ttsLogger.info("talk config provider from talk.resolved") } return TalkRuntimeConfig( voiceId: resolvedVoice, diff --git a/apps/macos/Tests/OpenClawIPCTests/TalkModeConfigParsingTests.swift b/apps/macos/Tests/OpenClawIPCTests/TalkModeConfigParsingTests.swift index 15797cebf9e..9409e110689 100644 --- a/apps/macos/Tests/OpenClawIPCTests/TalkModeConfigParsingTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/TalkModeConfigParsingTests.swift @@ -3,7 +3,7 @@ import Testing @testable import OpenClaw struct TalkModeConfigParsingTests { - @Test func `prefers normalized talk provider payload`() { + @Test func `rejects normalized talk provider payload without resolved`() { let talk: [String: AnyCodable] = [ "provider": AnyCodable("elevenlabs"), "providers": AnyCodable([ @@ -15,9 +15,7 @@ struct TalkModeConfigParsingTests { ] let selection = TalkModeRuntime.selectTalkProviderConfig(talk) - #expect(selection?.provider == "elevenlabs") - #expect(selection?.normalizedPayload == true) - #expect(selection?.config["voiceId"]?.stringValue == "voice-normalized") + #expect(selection == nil) } @Test func `falls back to legacy talk fields when normalized payload missing`() { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkConfigParsing.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkConfigParsing.swift index 0d5ade70e4c..6bdd6b9f244 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkConfigParsing.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkConfigParsing.swift @@ -26,28 +26,9 @@ public enum TalkConfigParsing { if let resolvedSelection = self.resolvedProviderConfig(talk) { return resolvedSelection } - let rawProvider = talk["provider"]?.stringValue - let rawProviders = talk["providers"] - let hasNormalizedPayload = rawProvider != nil || rawProviders != nil + let hasNormalizedPayload = talk["provider"] != nil || talk["providers"] != nil if hasNormalizedPayload { - let normalizedProviders = self.normalizedTalkProviders(rawProviders) - let explicitProviderID = self.normalizedTalkProviderID(rawProvider) - if let explicitProviderID { - if !normalizedProviders.isEmpty, normalizedProviders[explicitProviderID] == nil { - return nil - } - return TalkProviderConfigSelection( - provider: explicitProviderID, - config: normalizedProviders[explicitProviderID] ?? [:], - normalizedPayload: true) - } - guard normalizedProviders.count == 1, let providerID = normalizedProviders.keys.first else { - return nil - } - return TalkProviderConfigSelection( - provider: providerID, - config: normalizedProviders[providerID] ?? [:], - normalizedPayload: true) + return nil } guard allowLegacyFallback else { return nil } return TalkProviderConfigSelection( @@ -92,15 +73,4 @@ public enum TalkConfigParsing { config: resolved["config"]?.dictionaryValue ?? [:], normalizedPayload: true) } - - private static func normalizedTalkProviders(_ raw: AnyCodable?) -> [String: [String: AnyCodable]] { - guard let providerMap = raw?.dictionaryValue else { return [:] } - return providerMap.reduce(into: [String: [String: AnyCodable]]()) { acc, entry in - guard - let providerID = self.normalizedTalkProviderID(entry.key), - let providerConfig = entry.value.dictionaryValue - else { return } - acc[providerID] = providerConfig - } - } } diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigParsingTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigParsingTests.swift index f710a3497cf..5a8d5dd11d3 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigParsingTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigParsingTests.swift @@ -24,7 +24,7 @@ struct TalkConfigParsingTests { #expect(selection?.config["voiceId"]?.stringValue == "voice-resolved") } - @Test func prefersNormalizedTalkProviderPayload() { + @Test func rejectsNormalizedTalkProviderPayloadWithoutResolved() { let talk: [String: AnyCodable] = [ "provider": AnyCodable("elevenlabs"), "providers": AnyCodable([ @@ -36,9 +36,7 @@ struct TalkConfigParsingTests { ] let selection = TalkConfigParsing.selectProviderConfig(talk, defaultProvider: "elevenlabs") - #expect(selection?.provider == "elevenlabs") - #expect(selection?.normalizedPayload == true) - #expect(selection?.config["voiceId"]?.stringValue == "voice-normalized") + #expect(selection == nil) } @Test func fallsBackToLegacyTalkFieldsWhenNormalizedPayloadMissing() {