refactor: dedupe android talk config parsing

This commit is contained in:
Peter Steinberger
2026-03-08 16:26:35 +00:00
parent 2ed644f5d3
commit cee2f3e8b4
4 changed files with 72 additions and 60 deletions

View File

@@ -4,9 +4,16 @@ import ai.openclaw.app.normalizeMainKey
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.booleanOrNull import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.contentOrNull
internal data class TalkProviderConfigSelection(
val provider: String,
val config: JsonObject,
val normalizedPayload: Boolean,
)
internal data class TalkModeGatewayConfigState( internal data class TalkModeGatewayConfigState(
val activeProvider: String, val activeProvider: String,
val normalizedPayload: Boolean, val normalizedPayload: Boolean,
@@ -22,6 +29,8 @@ internal data class TalkModeGatewayConfigState(
) )
internal object TalkModeGatewayConfigParser { internal object TalkModeGatewayConfigParser {
private const val defaultTalkProvider = "elevenlabs"
fun parse( fun parse(
config: JsonObject?, config: JsonObject?,
defaultProvider: String, defaultProvider: String,
@@ -32,7 +41,7 @@ internal object TalkModeGatewayConfigParser {
envKey: String?, envKey: String?,
): TalkModeGatewayConfigState { ): TalkModeGatewayConfigState {
val talk = config?.get("talk").asObjectOrNull() val talk = config?.get("talk").asObjectOrNull()
val selection = TalkModeManager.selectTalkProviderConfig(talk) val selection = selectTalkProviderConfig(talk)
val activeProvider = selection?.provider ?: defaultProvider val activeProvider = selection?.provider ?: defaultProvider
val activeConfig = selection?.config val activeConfig = selection?.config
val sessionCfg = config?.get("session").asObjectOrNull() val sessionCfg = config?.get("session").asObjectOrNull()
@@ -48,7 +57,7 @@ internal object TalkModeGatewayConfigParser {
activeConfig?.get("outputFormat")?.asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() } activeConfig?.get("outputFormat")?.asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
val key = activeConfig?.get("apiKey")?.asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() } val key = activeConfig?.get("apiKey")?.asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
val interrupt = talk?.get("interruptOnSpeech")?.asBooleanOrNull() val interrupt = talk?.get("interruptOnSpeech")?.asBooleanOrNull()
val silenceTimeoutMs = TalkModeManager.resolvedSilenceTimeoutMs(talk) val silenceTimeoutMs = resolvedSilenceTimeoutMs(talk)
return TalkModeGatewayConfigState( return TalkModeGatewayConfigState(
activeProvider = activeProvider, activeProvider = activeProvider,
@@ -91,6 +100,48 @@ internal object TalkModeGatewayConfigParser {
interruptOnSpeech = null, interruptOnSpeech = null,
silenceTimeoutMs = TalkDefaults.defaultSilenceTimeoutMs, silenceTimeoutMs = TalkDefaults.defaultSilenceTimeoutMs,
) )
fun selectTalkProviderConfig(talk: JsonObject?): TalkProviderConfigSelection? {
if (talk == null) return null
selectResolvedTalkProviderConfig(talk)?.let { return it }
val rawProvider = talk["provider"].asStringOrNull()
val rawProviders = talk["providers"].asObjectOrNull()
val hasNormalizedPayload = rawProvider != null || rawProviders != null
if (hasNormalizedPayload) {
return null
}
return TalkProviderConfigSelection(
provider = defaultTalkProvider,
config = talk,
normalizedPayload = false,
)
}
fun resolvedSilenceTimeoutMs(talk: JsonObject?): Long {
val fallback = TalkDefaults.defaultSilenceTimeoutMs
val primitive = talk?.get("silenceTimeoutMs") as? JsonPrimitive ?: return fallback
if (primitive.isString) return fallback
val timeout = primitive.content.toDoubleOrNull() ?: return fallback
if (timeout <= 0 || timeout % 1.0 != 0.0 || timeout > Long.MAX_VALUE.toDouble()) {
return fallback
}
return timeout.toLong()
}
private fun selectResolvedTalkProviderConfig(talk: JsonObject): TalkProviderConfigSelection? {
val resolved = talk["resolved"].asObjectOrNull() ?: return null
val providerId = normalizeTalkProviderId(resolved["provider"].asStringOrNull()) ?: return null
return TalkProviderConfigSelection(
provider = providerId,
config = resolved["config"].asObjectOrNull() ?: buildJsonObject {},
normalizedPayload = true,
)
}
private fun normalizeTalkProviderId(raw: String?): String? {
val trimmed = raw?.trim()?.lowercase().orEmpty()
return trimmed.takeIf { it.isNotEmpty() }
}
} }
private fun normalizeTalkAliasKey(value: String): String = private fun normalizeTalkAliasKey(value: String): String =

View File

@@ -64,54 +64,6 @@ class TalkModeManager(
private const val chatFinalWaitWithSubscribeMs = 45_000L private const val chatFinalWaitWithSubscribeMs = 45_000L
private const val chatFinalWaitWithoutSubscribeMs = 6_000L private const val chatFinalWaitWithoutSubscribeMs = 6_000L
private const val maxCachedRunCompletions = 128 private const val maxCachedRunCompletions = 128
internal data class TalkProviderConfigSelection(
val provider: String,
val config: JsonObject,
val normalizedPayload: Boolean,
)
private fun normalizeTalkProviderId(raw: String?): String? {
val trimmed = raw?.trim()?.lowercase().orEmpty()
return trimmed.takeIf { it.isNotEmpty() }
}
private fun selectResolvedTalkProviderConfig(talk: JsonObject): TalkProviderConfigSelection? {
val resolved = talk["resolved"].asObjectOrNull() ?: return null
val providerId = normalizeTalkProviderId(resolved["provider"].asStringOrNull()) ?: return null
return TalkProviderConfigSelection(
provider = providerId,
config = resolved["config"].asObjectOrNull() ?: buildJsonObject {},
normalizedPayload = true,
)
}
internal fun selectTalkProviderConfig(talk: JsonObject?): TalkProviderConfigSelection? {
if (talk == null) return null
selectResolvedTalkProviderConfig(talk)?.let { return it }
val rawProvider = talk["provider"].asStringOrNull()
val rawProviders = talk["providers"].asObjectOrNull()
val hasNormalizedPayload = rawProvider != null || rawProviders != null
if (hasNormalizedPayload) {
return null
}
return TalkProviderConfigSelection(
provider = defaultTalkProvider,
config = talk,
normalizedPayload = false,
)
}
internal fun resolvedSilenceTimeoutMs(talk: JsonObject?): Long {
val fallback = TalkDefaults.defaultSilenceTimeoutMs
val primitive = talk?.get("silenceTimeoutMs") as? JsonPrimitive ?: return fallback
if (primitive.isString) return fallback
val timeout = primitive.content.toDoubleOrNull() ?: return fallback
if (timeout <= 0 || timeout % 1.0 != 0.0 || timeout > Long.MAX_VALUE.toDouble()) {
return fallback
}
return timeout.toLong()
}
} }
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())

View File

@@ -38,7 +38,7 @@ class TalkModeConfigContractTest {
@Test @Test
fun selectionFixtures() { fun selectionFixtures() {
for (fixture in loadFixtures().selectionCases) { for (fixture in loadFixtures().selectionCases) {
val selection = TalkModeManager.selectTalkProviderConfig(fixture.talk) val selection = TalkModeGatewayConfigParser.selectTalkProviderConfig(fixture.talk)
val expected = fixture.expectedSelection val expected = fixture.expectedSelection
if (expected == null) { if (expected == null) {
assertNull(fixture.id, selection) assertNull(fixture.id, selection)

View File

@@ -36,7 +36,7 @@ class TalkModeConfigParsingTest {
) )
.jsonObject .jsonObject
val selection = TalkModeManager.selectTalkProviderConfig(talk) val selection = TalkModeGatewayConfigParser.selectTalkProviderConfig(talk)
assertNotNull(selection) assertNotNull(selection)
assertEquals("elevenlabs", selection?.provider) assertEquals("elevenlabs", selection?.provider)
assertTrue(selection?.normalizedPayload == true) assertTrue(selection?.normalizedPayload == true)
@@ -61,7 +61,7 @@ class TalkModeConfigParsingTest {
) )
.jsonObject .jsonObject
val selection = TalkModeManager.selectTalkProviderConfig(talk) val selection = TalkModeGatewayConfigParser.selectTalkProviderConfig(talk)
assertEquals(null, selection) assertEquals(null, selection)
} }
@@ -82,7 +82,7 @@ class TalkModeConfigParsingTest {
) )
.jsonObject .jsonObject
val selection = TalkModeManager.selectTalkProviderConfig(talk) val selection = TalkModeGatewayConfigParser.selectTalkProviderConfig(talk)
assertEquals(null, selection) assertEquals(null, selection)
} }
@@ -105,7 +105,7 @@ class TalkModeConfigParsingTest {
) )
.jsonObject .jsonObject
val selection = TalkModeManager.selectTalkProviderConfig(talk) val selection = TalkModeGatewayConfigParser.selectTalkProviderConfig(talk)
assertEquals(null, selection) assertEquals(null, selection)
} }
@@ -118,7 +118,7 @@ class TalkModeConfigParsingTest {
put("apiKey", legacyApiKey) // pragma: allowlist secret put("apiKey", legacyApiKey) // pragma: allowlist secret
} }
val selection = TalkModeManager.selectTalkProviderConfig(talk) val selection = TalkModeGatewayConfigParser.selectTalkProviderConfig(talk)
assertNotNull(selection) assertNotNull(selection)
assertEquals("elevenlabs", selection?.provider) assertEquals("elevenlabs", selection?.provider)
assertTrue(selection?.normalizedPayload == false) assertTrue(selection?.normalizedPayload == false)
@@ -130,25 +130,34 @@ class TalkModeConfigParsingTest {
fun readsConfiguredSilenceTimeoutMs() { fun readsConfiguredSilenceTimeoutMs() {
val talk = buildJsonObject { put("silenceTimeoutMs", 1500) } val talk = buildJsonObject { put("silenceTimeoutMs", 1500) }
assertEquals(1500L, TalkModeManager.resolvedSilenceTimeoutMs(talk)) assertEquals(1500L, TalkModeGatewayConfigParser.resolvedSilenceTimeoutMs(talk))
} }
@Test @Test
fun defaultsSilenceTimeoutMsWhenMissing() { fun defaultsSilenceTimeoutMsWhenMissing() {
assertEquals(TalkDefaults.defaultSilenceTimeoutMs, TalkModeManager.resolvedSilenceTimeoutMs(null)) assertEquals(
TalkDefaults.defaultSilenceTimeoutMs,
TalkModeGatewayConfigParser.resolvedSilenceTimeoutMs(null),
)
} }
@Test @Test
fun defaultsSilenceTimeoutMsWhenInvalid() { fun defaultsSilenceTimeoutMsWhenInvalid() {
val talk = buildJsonObject { put("silenceTimeoutMs", 0) } val talk = buildJsonObject { put("silenceTimeoutMs", 0) }
assertEquals(TalkDefaults.defaultSilenceTimeoutMs, TalkModeManager.resolvedSilenceTimeoutMs(talk)) assertEquals(
TalkDefaults.defaultSilenceTimeoutMs,
TalkModeGatewayConfigParser.resolvedSilenceTimeoutMs(talk),
)
} }
@Test @Test
fun defaultsSilenceTimeoutMsWhenString() { fun defaultsSilenceTimeoutMsWhenString() {
val talk = buildJsonObject { put("silenceTimeoutMs", "1500") } val talk = buildJsonObject { put("silenceTimeoutMs", "1500") }
assertEquals(TalkDefaults.defaultSilenceTimeoutMs, TalkModeManager.resolvedSilenceTimeoutMs(talk)) assertEquals(
TalkDefaults.defaultSilenceTimeoutMs,
TalkModeGatewayConfigParser.resolvedSilenceTimeoutMs(talk),
)
} }
} }