From 2c6d099b01e0700ff2f4f3c821b0c87a79c1387d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Mar 2026 22:27:17 +0000 Subject: [PATCH] refactor: plugin-own speech provider config --- docs/.generated/plugin-sdk-api-baseline.json | 174 +++---- docs/.generated/plugin-sdk-api-baseline.jsonl | 174 +++---- extensions/discord/src/voice/manager.ts | 9 +- extensions/elevenlabs/speech-provider.ts | 474 ++++++++++++++++-- extensions/microsoft/speech-provider.ts | 139 ++++- extensions/openai/index.test.ts | 39 +- extensions/openai/speech-provider.ts | 200 +++++++- extensions/openai/tts.ts | 13 +- .../voice-call/src/providers/tts-openai.ts | 6 +- scripts/generate-bundled-plugin-metadata.mjs | 52 +- src/auto-reply/commands-registry.shared.ts | 2 +- src/auto-reply/reply/commands-tts.ts | 48 +- src/gateway/server-methods/talk.ts | 195 ++----- src/gateway/server-methods/tts.ts | 32 +- .../bundled-plugin-entries.generated.ts | 390 ++++++++------ src/plugin-sdk/speech-core.ts | 20 +- src/plugin-sdk/speech.ts | 9 +- src/plugin-sdk/voice-call.ts | 1 - src/plugins/bundled-plugin-entries.ts | 4 +- .../bundled-plugin-metadata.generated.ts | 18 +- src/plugins/types.ts | 13 + src/tts/directives.ts | 160 ++++++ src/tts/provider-registry.test.ts | 25 +- src/tts/provider-registry.ts | 17 +- src/tts/provider-types.ts | 76 ++- src/tts/tts-core.ts | 336 +------------ src/tts/tts.test.ts | 59 ++- src/tts/tts.ts | 335 +++++-------- 28 files changed, 1791 insertions(+), 1229 deletions(-) create mode 100644 src/tts/directives.ts diff --git a/docs/.generated/plugin-sdk-api-baseline.json b/docs/.generated/plugin-sdk-api-baseline.json index 220d24625e9..6a1f775f883 100644 --- a/docs/.generated/plugin-sdk-api-baseline.json +++ b/docs/.generated/plugin-sdk-api-baseline.json @@ -244,7 +244,7 @@ "exportName": "CliBackendPlugin", "kind": "type", "source": { - "line": 1292, + "line": 1305, "path": "src/plugins/types.ts" } }, @@ -388,7 +388,7 @@ "exportName": "MediaUnderstandingProviderPlugin", "kind": "type", "source": { - "line": 951, + "line": 964, "path": "src/plugins/types.ts" } }, @@ -406,7 +406,7 @@ "exportName": "OpenClawPluginApi", "kind": "type", "source": { - "line": 1336, + "line": 1349, "path": "src/plugins/types.ts" } }, @@ -415,7 +415,7 @@ "exportName": "OpenClawPluginConfigSchema", "kind": "type", "source": { - "line": 88, + "line": 94, "path": "src/plugins/types.ts" } }, @@ -424,7 +424,7 @@ "exportName": "PluginLogger", "kind": "type", "source": { - "line": 59, + "line": 65, "path": "src/plugins/types.ts" } }, @@ -442,7 +442,7 @@ "exportName": "ProviderAuthContext", "kind": "type", "source": { - "line": 155, + "line": 161, "path": "src/plugins/types.ts" } }, @@ -451,7 +451,7 @@ "exportName": "ProviderAuthResult", "kind": "type", "source": { - "line": 140, + "line": 146, "path": "src/plugins/types.ts" } }, @@ -460,7 +460,7 @@ "exportName": "ProviderRuntimeModel", "kind": "type", "source": { - "line": 295, + "line": 301, "path": "src/plugins/types.ts" } }, @@ -514,7 +514,7 @@ "exportName": "SpeechProviderPlugin", "kind": "type", "source": { - "line": 933, + "line": 939, "path": "src/plugins/types.ts" } }, @@ -3405,7 +3405,7 @@ "exportName": "MediaUnderstandingProviderPlugin", "kind": "type", "source": { - "line": 951, + "line": 964, "path": "src/plugins/types.ts" } }, @@ -3423,7 +3423,7 @@ "exportName": "OpenClawPluginApi", "kind": "type", "source": { - "line": 1336, + "line": 1349, "path": "src/plugins/types.ts" } }, @@ -3432,7 +3432,7 @@ "exportName": "OpenClawPluginCommandDefinition", "kind": "type", "source": { - "line": 1068, + "line": 1081, "path": "src/plugins/types.ts" } }, @@ -3441,7 +3441,7 @@ "exportName": "OpenClawPluginConfigSchema", "kind": "type", "source": { - "line": 88, + "line": 94, "path": "src/plugins/types.ts" } }, @@ -3450,7 +3450,7 @@ "exportName": "OpenClawPluginDefinition", "kind": "type", "source": { - "line": 1318, + "line": 1331, "path": "src/plugins/types.ts" } }, @@ -3459,7 +3459,7 @@ "exportName": "OpenClawPluginService", "kind": "type", "source": { - "line": 1285, + "line": 1298, "path": "src/plugins/types.ts" } }, @@ -3468,7 +3468,7 @@ "exportName": "OpenClawPluginServiceContext", "kind": "type", "source": { - "line": 1277, + "line": 1290, "path": "src/plugins/types.ts" } }, @@ -3477,7 +3477,7 @@ "exportName": "OpenClawPluginToolContext", "kind": "type", "source": { - "line": 103, + "line": 109, "path": "src/plugins/types.ts" } }, @@ -3486,7 +3486,7 @@ "exportName": "OpenClawPluginToolFactory", "kind": "type", "source": { - "line": 120, + "line": 126, "path": "src/plugins/types.ts" } }, @@ -3495,7 +3495,7 @@ "exportName": "PluginCommandContext", "kind": "type", "source": { - "line": 966, + "line": 979, "path": "src/plugins/types.ts" } }, @@ -3504,7 +3504,7 @@ "exportName": "PluginInteractiveTelegramHandlerContext", "kind": "type", "source": { - "line": 1097, + "line": 1110, "path": "src/plugins/types.ts" } }, @@ -3513,7 +3513,7 @@ "exportName": "PluginLogger", "kind": "type", "source": { - "line": 59, + "line": 65, "path": "src/plugins/types.ts" } }, @@ -3531,7 +3531,7 @@ "exportName": "ProviderAugmentModelCatalogContext", "kind": "type", "source": { - "line": 571, + "line": 577, "path": "src/plugins/types.ts" } }, @@ -3540,7 +3540,7 @@ "exportName": "ProviderAuthContext", "kind": "type", "source": { - "line": 155, + "line": 161, "path": "src/plugins/types.ts" } }, @@ -3549,7 +3549,7 @@ "exportName": "ProviderAuthDoctorHintContext", "kind": "type", "source": { - "line": 446, + "line": 452, "path": "src/plugins/types.ts" } }, @@ -3558,7 +3558,7 @@ "exportName": "ProviderAuthMethod", "kind": "type", "source": { - "line": 233, + "line": 239, "path": "src/plugins/types.ts" } }, @@ -3567,7 +3567,7 @@ "exportName": "ProviderAuthMethodNonInteractiveContext", "kind": "type", "source": { - "line": 217, + "line": 223, "path": "src/plugins/types.ts" } }, @@ -3576,7 +3576,7 @@ "exportName": "ProviderAuthResult", "kind": "type", "source": { - "line": 140, + "line": 146, "path": "src/plugins/types.ts" } }, @@ -3585,7 +3585,7 @@ "exportName": "ProviderBuildMissingAuthMessageContext", "kind": "type", "source": { - "line": 499, + "line": 505, "path": "src/plugins/types.ts" } }, @@ -3594,7 +3594,7 @@ "exportName": "ProviderBuiltInModelSuppressionContext", "kind": "type", "source": { - "line": 515, + "line": 521, "path": "src/plugins/types.ts" } }, @@ -3603,7 +3603,7 @@ "exportName": "ProviderBuiltInModelSuppressionResult", "kind": "type", "source": { - "line": 524, + "line": 530, "path": "src/plugins/types.ts" } }, @@ -3612,7 +3612,7 @@ "exportName": "ProviderCacheTtlEligibilityContext", "kind": "type", "source": { - "line": 487, + "line": 493, "path": "src/plugins/types.ts" } }, @@ -3621,7 +3621,7 @@ "exportName": "ProviderCatalogContext", "kind": "type", "source": { - "line": 254, + "line": 260, "path": "src/plugins/types.ts" } }, @@ -3630,7 +3630,7 @@ "exportName": "ProviderCatalogResult", "kind": "type", "source": { - "line": 277, + "line": 283, "path": "src/plugins/types.ts" } }, @@ -3639,7 +3639,7 @@ "exportName": "ProviderDefaultThinkingPolicyContext", "kind": "type", "source": { - "line": 548, + "line": 554, "path": "src/plugins/types.ts" } }, @@ -3648,7 +3648,7 @@ "exportName": "ProviderDiscoveryContext", "kind": "type", "source": { - "line": 587, + "line": 593, "path": "src/plugins/types.ts" } }, @@ -3657,7 +3657,7 @@ "exportName": "ProviderFetchUsageSnapshotContext", "kind": "type", "source": { - "line": 427, + "line": 433, "path": "src/plugins/types.ts" } }, @@ -3666,7 +3666,7 @@ "exportName": "ProviderModernModelPolicyContext", "kind": "type", "source": { - "line": 558, + "line": 564, "path": "src/plugins/types.ts" } }, @@ -3675,7 +3675,7 @@ "exportName": "ProviderNormalizeResolvedModelContext", "kind": "type", "source": { - "line": 338, + "line": 344, "path": "src/plugins/types.ts" } }, @@ -3684,7 +3684,7 @@ "exportName": "ProviderPreparedRuntimeAuth", "kind": "type", "source": { - "line": 374, + "line": 380, "path": "src/plugins/types.ts" } }, @@ -3693,7 +3693,7 @@ "exportName": "ProviderPrepareDynamicModelContext", "kind": "type", "source": { - "line": 329, + "line": 335, "path": "src/plugins/types.ts" } }, @@ -3702,7 +3702,7 @@ "exportName": "ProviderPrepareExtraParamsContext", "kind": "type", "source": { - "line": 460, + "line": 466, "path": "src/plugins/types.ts" } }, @@ -3711,7 +3711,7 @@ "exportName": "ProviderPrepareRuntimeAuthContext", "kind": "type", "source": { - "line": 353, + "line": 359, "path": "src/plugins/types.ts" } }, @@ -3720,7 +3720,7 @@ "exportName": "ProviderResolvedUsageAuth", "kind": "type", "source": { - "line": 414, + "line": 420, "path": "src/plugins/types.ts" } }, @@ -3729,7 +3729,7 @@ "exportName": "ProviderResolveDynamicModelContext", "kind": "type", "source": { - "line": 312, + "line": 318, "path": "src/plugins/types.ts" } }, @@ -3738,7 +3738,7 @@ "exportName": "ProviderResolveUsageAuthContext", "kind": "type", "source": { - "line": 395, + "line": 401, "path": "src/plugins/types.ts" } }, @@ -3747,7 +3747,7 @@ "exportName": "ProviderRuntimeModel", "kind": "type", "source": { - "line": 295, + "line": 301, "path": "src/plugins/types.ts" } }, @@ -3756,7 +3756,7 @@ "exportName": "ProviderThinkingPolicyContext", "kind": "type", "source": { - "line": 536, + "line": 542, "path": "src/plugins/types.ts" } }, @@ -3774,7 +3774,7 @@ "exportName": "ProviderWrapStreamFnContext", "kind": "type", "source": { - "line": 477, + "line": 483, "path": "src/plugins/types.ts" } }, @@ -3819,7 +3819,7 @@ "exportName": "SpeechProviderPlugin", "kind": "type", "source": { - "line": 933, + "line": 939, "path": "src/plugins/types.ts" } }, @@ -3911,7 +3911,7 @@ "exportName": "MediaUnderstandingProviderPlugin", "kind": "type", "source": { - "line": 951, + "line": 964, "path": "src/plugins/types.ts" } }, @@ -3929,7 +3929,7 @@ "exportName": "OpenClawPluginApi", "kind": "type", "source": { - "line": 1336, + "line": 1349, "path": "src/plugins/types.ts" } }, @@ -3938,7 +3938,7 @@ "exportName": "OpenClawPluginCommandDefinition", "kind": "type", "source": { - "line": 1068, + "line": 1081, "path": "src/plugins/types.ts" } }, @@ -3947,7 +3947,7 @@ "exportName": "OpenClawPluginConfigSchema", "kind": "type", "source": { - "line": 88, + "line": 94, "path": "src/plugins/types.ts" } }, @@ -3956,7 +3956,7 @@ "exportName": "OpenClawPluginDefinition", "kind": "type", "source": { - "line": 1318, + "line": 1331, "path": "src/plugins/types.ts" } }, @@ -3965,7 +3965,7 @@ "exportName": "OpenClawPluginService", "kind": "type", "source": { - "line": 1285, + "line": 1298, "path": "src/plugins/types.ts" } }, @@ -3974,7 +3974,7 @@ "exportName": "OpenClawPluginServiceContext", "kind": "type", "source": { - "line": 1277, + "line": 1290, "path": "src/plugins/types.ts" } }, @@ -3983,7 +3983,7 @@ "exportName": "PluginCommandContext", "kind": "type", "source": { - "line": 966, + "line": 979, "path": "src/plugins/types.ts" } }, @@ -3992,7 +3992,7 @@ "exportName": "PluginInteractiveTelegramHandlerContext", "kind": "type", "source": { - "line": 1097, + "line": 1110, "path": "src/plugins/types.ts" } }, @@ -4001,7 +4001,7 @@ "exportName": "PluginLogger", "kind": "type", "source": { - "line": 59, + "line": 65, "path": "src/plugins/types.ts" } }, @@ -4010,7 +4010,7 @@ "exportName": "ProviderAugmentModelCatalogContext", "kind": "type", "source": { - "line": 571, + "line": 577, "path": "src/plugins/types.ts" } }, @@ -4019,7 +4019,7 @@ "exportName": "ProviderAuthContext", "kind": "type", "source": { - "line": 155, + "line": 161, "path": "src/plugins/types.ts" } }, @@ -4028,7 +4028,7 @@ "exportName": "ProviderAuthDoctorHintContext", "kind": "type", "source": { - "line": 446, + "line": 452, "path": "src/plugins/types.ts" } }, @@ -4037,7 +4037,7 @@ "exportName": "ProviderAuthMethod", "kind": "type", "source": { - "line": 233, + "line": 239, "path": "src/plugins/types.ts" } }, @@ -4046,7 +4046,7 @@ "exportName": "ProviderAuthMethodNonInteractiveContext", "kind": "type", "source": { - "line": 217, + "line": 223, "path": "src/plugins/types.ts" } }, @@ -4055,7 +4055,7 @@ "exportName": "ProviderAuthResult", "kind": "type", "source": { - "line": 140, + "line": 146, "path": "src/plugins/types.ts" } }, @@ -4064,7 +4064,7 @@ "exportName": "ProviderBuildMissingAuthMessageContext", "kind": "type", "source": { - "line": 499, + "line": 505, "path": "src/plugins/types.ts" } }, @@ -4073,7 +4073,7 @@ "exportName": "ProviderBuiltInModelSuppressionContext", "kind": "type", "source": { - "line": 515, + "line": 521, "path": "src/plugins/types.ts" } }, @@ -4082,7 +4082,7 @@ "exportName": "ProviderBuiltInModelSuppressionResult", "kind": "type", "source": { - "line": 524, + "line": 530, "path": "src/plugins/types.ts" } }, @@ -4091,7 +4091,7 @@ "exportName": "ProviderCacheTtlEligibilityContext", "kind": "type", "source": { - "line": 487, + "line": 493, "path": "src/plugins/types.ts" } }, @@ -4100,7 +4100,7 @@ "exportName": "ProviderCatalogContext", "kind": "type", "source": { - "line": 254, + "line": 260, "path": "src/plugins/types.ts" } }, @@ -4109,7 +4109,7 @@ "exportName": "ProviderCatalogResult", "kind": "type", "source": { - "line": 277, + "line": 283, "path": "src/plugins/types.ts" } }, @@ -4118,7 +4118,7 @@ "exportName": "ProviderDefaultThinkingPolicyContext", "kind": "type", "source": { - "line": 548, + "line": 554, "path": "src/plugins/types.ts" } }, @@ -4127,7 +4127,7 @@ "exportName": "ProviderDiscoveryContext", "kind": "type", "source": { - "line": 587, + "line": 593, "path": "src/plugins/types.ts" } }, @@ -4136,7 +4136,7 @@ "exportName": "ProviderFetchUsageSnapshotContext", "kind": "type", "source": { - "line": 427, + "line": 433, "path": "src/plugins/types.ts" } }, @@ -4145,7 +4145,7 @@ "exportName": "ProviderModernModelPolicyContext", "kind": "type", "source": { - "line": 558, + "line": 564, "path": "src/plugins/types.ts" } }, @@ -4154,7 +4154,7 @@ "exportName": "ProviderNormalizeResolvedModelContext", "kind": "type", "source": { - "line": 338, + "line": 344, "path": "src/plugins/types.ts" } }, @@ -4163,7 +4163,7 @@ "exportName": "ProviderPreparedRuntimeAuth", "kind": "type", "source": { - "line": 374, + "line": 380, "path": "src/plugins/types.ts" } }, @@ -4172,7 +4172,7 @@ "exportName": "ProviderPrepareDynamicModelContext", "kind": "type", "source": { - "line": 329, + "line": 335, "path": "src/plugins/types.ts" } }, @@ -4181,7 +4181,7 @@ "exportName": "ProviderPrepareExtraParamsContext", "kind": "type", "source": { - "line": 460, + "line": 466, "path": "src/plugins/types.ts" } }, @@ -4190,7 +4190,7 @@ "exportName": "ProviderPrepareRuntimeAuthContext", "kind": "type", "source": { - "line": 353, + "line": 359, "path": "src/plugins/types.ts" } }, @@ -4199,7 +4199,7 @@ "exportName": "ProviderResolvedUsageAuth", "kind": "type", "source": { - "line": 414, + "line": 420, "path": "src/plugins/types.ts" } }, @@ -4208,7 +4208,7 @@ "exportName": "ProviderResolveDynamicModelContext", "kind": "type", "source": { - "line": 312, + "line": 318, "path": "src/plugins/types.ts" } }, @@ -4217,7 +4217,7 @@ "exportName": "ProviderResolveUsageAuthContext", "kind": "type", "source": { - "line": 395, + "line": 401, "path": "src/plugins/types.ts" } }, @@ -4226,7 +4226,7 @@ "exportName": "ProviderRuntimeModel", "kind": "type", "source": { - "line": 295, + "line": 301, "path": "src/plugins/types.ts" } }, @@ -4235,7 +4235,7 @@ "exportName": "ProviderThinkingPolicyContext", "kind": "type", "source": { - "line": 536, + "line": 542, "path": "src/plugins/types.ts" } }, @@ -4244,7 +4244,7 @@ "exportName": "ProviderWrapStreamFnContext", "kind": "type", "source": { - "line": 477, + "line": 483, "path": "src/plugins/types.ts" } }, @@ -4253,7 +4253,7 @@ "exportName": "SpeechProviderPlugin", "kind": "type", "source": { - "line": 933, + "line": 939, "path": "src/plugins/types.ts" } } diff --git a/docs/.generated/plugin-sdk-api-baseline.jsonl b/docs/.generated/plugin-sdk-api-baseline.jsonl index ab024a272a9..c44b826f780 100644 --- a/docs/.generated/plugin-sdk-api-baseline.jsonl +++ b/docs/.generated/plugin-sdk-api-baseline.jsonl @@ -25,7 +25,7 @@ {"declaration":"export type ChannelStatusIssue = ChannelStatusIssue;","entrypoint":"index","exportName":"ChannelStatusIssue","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":100,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"index","exportName":"ClawdbotConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"} {"declaration":"export type CliBackendConfig = CliBackendConfig;","entrypoint":"index","exportName":"CliBackendConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":47,"sourcePath":"src/config/types.agent-defaults.ts"} -{"declaration":"export type CliBackendPlugin = CliBackendPlugin;","entrypoint":"index","exportName":"CliBackendPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1292,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type CliBackendPlugin = CliBackendPlugin;","entrypoint":"index","exportName":"CliBackendPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1305,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type CompiledConfiguredBinding = CompiledConfiguredBinding;","entrypoint":"index","exportName":"CompiledConfiguredBinding","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":38,"sourcePath":"src/channels/plugins/binding-types.ts"} {"declaration":"export type ConfiguredBindingConversation = ConversationRef;","entrypoint":"index","exportName":"ConfiguredBindingConversation","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":13,"sourcePath":"src/channels/plugins/binding-types.ts"} {"declaration":"export type ConfiguredBindingResolution = ConfiguredBindingResolution;","entrypoint":"index","exportName":"ConfiguredBindingResolution","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":49,"sourcePath":"src/channels/plugins/binding-types.ts"} @@ -41,21 +41,21 @@ {"declaration":"export type ImageGenerationResolution = ImageGenerationResolution;","entrypoint":"index","exportName":"ImageGenerationResolution","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":12,"sourcePath":"src/image-generation/types.ts"} {"declaration":"export type ImageGenerationResult = ImageGenerationResult;","entrypoint":"index","exportName":"ImageGenerationResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":36,"sourcePath":"src/image-generation/types.ts"} {"declaration":"export type ImageGenerationSourceImage = ImageGenerationSourceImage;","entrypoint":"index","exportName":"ImageGenerationSourceImage","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":14,"sourcePath":"src/image-generation/types.ts"} -{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"index","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":951,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"index","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":964,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"index","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"} -{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"index","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1336,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"index","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":88,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"index","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":59,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"index","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1349,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"index","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":94,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"index","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":65,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"index","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":54,"sourcePath":"src/plugins/runtime/types.ts"} -{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"index","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":155,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"index","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":140,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"index","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":295,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"index","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":161,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"index","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":146,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"index","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":301,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type ReplyPayload = ReplyPayload;","entrypoint":"index","exportName":"ReplyPayload","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":76,"sourcePath":"src/auto-reply/types.ts"} {"declaration":"export type RuntimeEnv = RuntimeEnv;","entrypoint":"index","exportName":"RuntimeEnv","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":4,"sourcePath":"src/runtime.ts"} {"declaration":"export type RuntimeLogger = RuntimeLogger;","entrypoint":"index","exportName":"RuntimeLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/plugins/runtime/types-core.ts"} {"declaration":"export type SecretInput = SecretInput;","entrypoint":"index","exportName":"SecretInput","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":16,"sourcePath":"src/config/types.secrets.ts"} {"declaration":"export type SecretRef = SecretRef;","entrypoint":"index","exportName":"SecretRef","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":10,"sourcePath":"src/config/types.secrets.ts"} -{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"index","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":933,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"index","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":939,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type StatefulBindingTargetDescriptor = StatefulBindingTargetDescriptor;","entrypoint":"index","exportName":"StatefulBindingTargetDescriptor","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":17,"sourcePath":"src/channels/plugins/binding-types.ts"} {"declaration":"export type StatefulBindingTargetDriver = StatefulBindingTargetDriver;","entrypoint":"index","exportName":"StatefulBindingTargetDriver","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":15,"sourcePath":"src/channels/plugins/stateful-target-drivers.ts"} {"declaration":"export type StatefulBindingTargetReadyResult = StatefulBindingTargetReadyResult;","entrypoint":"index","exportName":"StatefulBindingTargetReadyResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/channels/plugins/stateful-target-drivers.ts"} @@ -374,53 +374,53 @@ {"declaration":"export type ChannelPlugin = ChannelPlugin;","entrypoint":"core","exportName":"ChannelPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":55,"sourcePath":"src/channels/plugins/types.plugin.ts"} {"declaration":"export type GatewayBindUrlResult = GatewayBindUrlResult;","entrypoint":"core","exportName":"GatewayBindUrlResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1,"sourcePath":"src/shared/gateway-bind-url.ts"} {"declaration":"export type GatewayRequestHandlerOptions = GatewayRequestHandlerOptions;","entrypoint":"core","exportName":"GatewayRequestHandlerOptions","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":112,"sourcePath":"src/gateway/server-methods/types.ts"} -{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"core","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":951,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"core","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":964,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"core","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"} -{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1336,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1068,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":88,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1318,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1285,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"core","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1277,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"core","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":103,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"core","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":120,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"core","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":966,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"core","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1097,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"core","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":59,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1349,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1081,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":94,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1331,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1298,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"core","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1290,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"core","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":109,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"core","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":126,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"core","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":979,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"core","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1110,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"core","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":65,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"core","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":54,"sourcePath":"src/plugins/runtime/types.ts"} -{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"core","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":571,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"core","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":155,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"core","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":446,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"core","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":233,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"core","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":217,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"core","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":140,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"core","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":499,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":515,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":524,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"core","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":487,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":254,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"core","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":277,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":548,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":587,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"core","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":427,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"core","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"core","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":338,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"core","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":374,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":329,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"core","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":460,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"core","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":353,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"core","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":414,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":312,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"core","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":395,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"core","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":295,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":536,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"core","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":577,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"core","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":161,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"core","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":452,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"core","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":239,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"core","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":223,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"core","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":146,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"core","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":505,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":521,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":530,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"core","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":493,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":260,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"core","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":283,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":554,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":593,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"core","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":433,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"core","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":564,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"core","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":344,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"core","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":380,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":335,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"core","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":466,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"core","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":359,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"core","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":420,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":318,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"core","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":401,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"core","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":301,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":542,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type ProviderUsageSnapshot = ProviderUsageSnapshot;","entrypoint":"core","exportName":"ProviderUsageSnapshot","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/infra/provider-usage.types.ts"} -{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"core","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":477,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"core","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":483,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type RoutePeer = RoutePeer;","entrypoint":"core","exportName":"RoutePeer","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":21,"sourcePath":"src/routing/resolve-route.ts"} {"declaration":"export type RoutePeerKind = ChatType;","entrypoint":"core","exportName":"RoutePeerKind","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":19,"sourcePath":"src/routing/resolve-route.ts"} {"declaration":"export type SecretFileReadOptions = SecretFileReadOptions;","entrypoint":"core","exportName":"SecretFileReadOptions","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/infra/secret-file.ts"} {"declaration":"export type SecretFileReadResult = SecretFileReadResult;","entrypoint":"core","exportName":"SecretFileReadResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":12,"sourcePath":"src/infra/secret-file.ts"} -{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"core","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":933,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"core","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":939,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type TailscaleStatusCommandResult = TailscaleStatusCommandResult;","entrypoint":"core","exportName":"TailscaleStatusCommandResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1,"sourcePath":"src/shared/tailscale-status.ts"} {"declaration":"export type TailscaleStatusCommandRunner = TailscaleStatusCommandRunner;","entrypoint":"core","exportName":"TailscaleStatusCommandRunner","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":6,"sourcePath":"src/shared/tailscale-status.ts"} {"declaration":"export type UsageProviderId = UsageProviderId;","entrypoint":"core","exportName":"UsageProviderId","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":20,"sourcePath":"src/infra/provider-usage.types.ts"} @@ -430,45 +430,45 @@ {"declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"plugin-entry","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export","sourceLine":88,"sourcePath":"src/plugin-sdk/plugin-entry.ts"} {"declaration":"export function emptyPluginConfigSchema(): OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"emptyPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export","sourceLine":13,"sourcePath":"src/plugins/config-schema.ts"} {"declaration":"export type AnyAgentTool = AnyAgentTool;","entrypoint":"plugin-entry","exportName":"AnyAgentTool","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":9,"sourcePath":"src/agents/tools/common.ts"} -{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"plugin-entry","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":951,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"plugin-entry","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":964,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"plugin-entry","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"} -{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1336,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1068,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":88,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1318,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"plugin-entry","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1285,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1277,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"plugin-entry","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":966,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"plugin-entry","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1097,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"plugin-entry","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":59,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":571,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":155,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":446,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":233,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":217,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"plugin-entry","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":140,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"plugin-entry","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":499,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":515,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":524,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"plugin-entry","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":487,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":254,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"plugin-entry","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":277,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":548,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":587,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"plugin-entry","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":427,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":338,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"plugin-entry","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":374,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":329,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":460,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":353,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"plugin-entry","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":414,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":312,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":395,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"plugin-entry","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":295,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":536,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"plugin-entry","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":477,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"plugin-entry","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":933,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1349,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1081,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":94,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1331,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"plugin-entry","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1298,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1290,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"plugin-entry","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":979,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"plugin-entry","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1110,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"plugin-entry","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":65,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":577,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":161,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":452,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":239,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":223,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"plugin-entry","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":146,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"plugin-entry","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":505,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":521,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":530,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"plugin-entry","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":493,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":260,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"plugin-entry","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":283,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":554,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":593,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"plugin-entry","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":433,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":564,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":344,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"plugin-entry","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":380,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":335,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":466,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":359,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"plugin-entry","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":420,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":318,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":401,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"plugin-entry","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":301,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":542,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"plugin-entry","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":483,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"plugin-entry","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":939,"sourcePath":"src/plugins/types.ts"} {"category":"provider","entrypoint":"provider-onboard","importSpecifier":"openclaw/plugin-sdk/provider-onboard","recordType":"module","sourceLine":1,"sourcePath":"src/plugin-sdk/provider-onboard.ts"} {"declaration":"export function applyAgentDefaultModelPrimary(cfg: OpenClawConfig, primary: string): OpenClawConfig;","entrypoint":"provider-onboard","exportName":"applyAgentDefaultModelPrimary","importSpecifier":"openclaw/plugin-sdk/provider-onboard","kind":"function","recordType":"export","sourceLine":76,"sourcePath":"src/plugins/provider-onboarding-config.ts"} {"declaration":"export function applyCloudflareAiGatewayConfig(cfg: OpenClawConfig, params?: { accountId?: string | undefined; gatewayId?: string | undefined; } | undefined): OpenClawConfig;","entrypoint":"provider-onboard","exportName":"applyCloudflareAiGatewayConfig","importSpecifier":"openclaw/plugin-sdk/provider-onboard","kind":"function","recordType":"export","sourceLine":85,"sourcePath":"extensions/cloudflare-ai-gateway/onboard.ts"} diff --git a/extensions/discord/src/voice/manager.ts b/extensions/discord/src/voice/manager.ts index c7160a06929..1c29888495d 100644 --- a/extensions/discord/src/voice/manager.ts +++ b/extensions/discord/src/voice/manager.ts @@ -645,11 +645,10 @@ export class DiscordVoiceManager { cfg: this.params.cfg, override: this.params.discordConfig.voice?.tts, }); - const directive = parseTtsDirectives( - replyText, - ttsConfig.modelOverrides, - ttsConfig.openai.baseUrl, - ); + const directive = parseTtsDirectives(replyText, ttsConfig.modelOverrides, { + cfg: ttsCfg, + providerConfigs: ttsConfig.providerConfigs, + }); const speakText = directive.overrides.ttsText ?? directive.cleanedText.trim(); if (!speakText) { logVoiceVerbose( diff --git a/extensions/elevenlabs/speech-provider.ts b/extensions/elevenlabs/speech-provider.ts index 24e8298ad0d..5a30cd47b6b 100644 --- a/extensions/elevenlabs/speech-provider.ts +++ b/extensions/elevenlabs/speech-provider.ts @@ -1,16 +1,309 @@ -import type { SpeechProviderPlugin } from "openclaw/plugin-sdk/core"; -import type { SpeechVoiceOption } from "openclaw/plugin-sdk/speech"; +import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input"; +import type { + SpeechDirectiveTokenParseContext, + SpeechProviderConfig, + SpeechProviderOverrides, + SpeechProviderPlugin, + SpeechVoiceOption, +} from "openclaw/plugin-sdk/speech-core"; +import { + normalizeApplyTextNormalization, + normalizeLanguageCode, + normalizeSeed, + requireInRange, +} from "openclaw/plugin-sdk/speech-core"; import { elevenLabsTTS } from "./tts.js"; +const DEFAULT_ELEVENLABS_BASE_URL = "https://api.elevenlabs.io"; +const DEFAULT_ELEVENLABS_VOICE_ID = "pMsXgVXv3BLzUgSXRplE"; +const DEFAULT_ELEVENLABS_MODEL_ID = "eleven_multilingual_v2"; +const DEFAULT_ELEVENLABS_VOICE_SETTINGS = { + stability: 0.5, + similarityBoost: 0.75, + style: 0.0, + useSpeakerBoost: true, + speed: 1.0, +}; + const ELEVENLABS_TTS_MODELS = [ "eleven_multilingual_v2", "eleven_turbo_v2_5", "eleven_monolingual_v1", ] as const; +type ElevenLabsProviderConfig = { + apiKey?: string; + baseUrl: string; + voiceId: string; + modelId: string; + seed?: number; + applyTextNormalization?: "auto" | "on" | "off"; + languageCode?: string; + voiceSettings: { + stability: number; + similarityBoost: number; + style: number; + useSpeakerBoost: boolean; + speed: number; + }; +}; + +function trimToUndefined(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function asNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +function asBoolean(value: unknown): boolean | undefined { + return typeof value === "boolean" ? value : undefined; +} + +function asObject(value: unknown): Record | undefined { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +function parseBooleanValue(value: string): boolean | undefined { + const normalized = value.trim().toLowerCase(); + if (["true", "1", "yes", "on"].includes(normalized)) { + return true; + } + if (["false", "0", "no", "off"].includes(normalized)) { + return false; + } + return undefined; +} + +function parseNumberValue(value: string): number | undefined { + const parsed = Number.parseFloat(value); + return Number.isFinite(parsed) ? parsed : undefined; +} + +export function isValidVoiceId(voiceId: string): boolean { + return /^[a-zA-Z0-9]{10,40}$/.test(voiceId); +} + function normalizeElevenLabsBaseUrl(baseUrl: string | undefined): string { const trimmed = baseUrl?.trim(); - return trimmed?.replace(/\/+$/, "") || "https://api.elevenlabs.io"; + return trimmed?.replace(/\/+$/, "") || DEFAULT_ELEVENLABS_BASE_URL; +} + +function normalizeElevenLabsProviderConfig( + rawConfig: Record, +): ElevenLabsProviderConfig { + const raw = asObject(rawConfig.elevenlabs); + const rawVoiceSettings = asObject(raw?.voiceSettings); + return { + apiKey: normalizeResolvedSecretInputString({ + value: raw?.apiKey, + path: "messages.tts.elevenlabs.apiKey", + }), + baseUrl: normalizeElevenLabsBaseUrl(trimToUndefined(raw?.baseUrl)), + voiceId: trimToUndefined(raw?.voiceId) ?? DEFAULT_ELEVENLABS_VOICE_ID, + modelId: trimToUndefined(raw?.modelId) ?? DEFAULT_ELEVENLABS_MODEL_ID, + seed: asNumber(raw?.seed), + applyTextNormalization: trimToUndefined(raw?.applyTextNormalization) as + | "auto" + | "on" + | "off" + | undefined, + languageCode: trimToUndefined(raw?.languageCode), + voiceSettings: { + stability: + asNumber(rawVoiceSettings?.stability) ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.stability, + similarityBoost: + asNumber(rawVoiceSettings?.similarityBoost) ?? + DEFAULT_ELEVENLABS_VOICE_SETTINGS.similarityBoost, + style: asNumber(rawVoiceSettings?.style) ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.style, + useSpeakerBoost: + asBoolean(rawVoiceSettings?.useSpeakerBoost) ?? + DEFAULT_ELEVENLABS_VOICE_SETTINGS.useSpeakerBoost, + speed: asNumber(rawVoiceSettings?.speed) ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.speed, + }, + }; +} + +function readElevenLabsProviderConfig(config: SpeechProviderConfig): ElevenLabsProviderConfig { + const defaults = normalizeElevenLabsProviderConfig({}); + const voiceSettings = asObject(config.voiceSettings); + return { + apiKey: trimToUndefined(config.apiKey) ?? defaults.apiKey, + baseUrl: normalizeElevenLabsBaseUrl(trimToUndefined(config.baseUrl) ?? defaults.baseUrl), + voiceId: trimToUndefined(config.voiceId) ?? defaults.voiceId, + modelId: trimToUndefined(config.modelId) ?? defaults.modelId, + seed: asNumber(config.seed) ?? defaults.seed, + applyTextNormalization: + (trimToUndefined(config.applyTextNormalization) as "auto" | "on" | "off" | undefined) ?? + defaults.applyTextNormalization, + languageCode: trimToUndefined(config.languageCode) ?? defaults.languageCode, + voiceSettings: { + stability: asNumber(voiceSettings?.stability) ?? defaults.voiceSettings.stability, + similarityBoost: + asNumber(voiceSettings?.similarityBoost) ?? defaults.voiceSettings.similarityBoost, + style: asNumber(voiceSettings?.style) ?? defaults.voiceSettings.style, + useSpeakerBoost: + asBoolean(voiceSettings?.useSpeakerBoost) ?? defaults.voiceSettings.useSpeakerBoost, + speed: asNumber(voiceSettings?.speed) ?? defaults.voiceSettings.speed, + }, + }; +} + +function mergeVoiceSettingsOverride( + ctx: SpeechDirectiveTokenParseContext, + next: Record, +): SpeechProviderOverrides { + return { + ...(ctx.currentOverrides ?? {}), + voiceSettings: { + ...(asObject(ctx.currentOverrides?.voiceSettings) ?? {}), + ...next, + }, + }; +} + +function parseDirectiveToken(ctx: SpeechDirectiveTokenParseContext) { + try { + switch (ctx.key) { + case "voiceid": + case "voice_id": + case "elevenlabs_voice": + case "elevenlabsvoice": + if (!ctx.policy.allowVoice) { + return { handled: true }; + } + if (!isValidVoiceId(ctx.value)) { + return { handled: true, warnings: [`invalid ElevenLabs voiceId "${ctx.value}"`] }; + } + return { + handled: true, + overrides: { ...(ctx.currentOverrides ?? {}), voiceId: ctx.value }, + }; + case "model": + case "modelid": + case "model_id": + case "elevenlabs_model": + case "elevenlabsmodel": + if (!ctx.policy.allowModelId) { + return { handled: true }; + } + return { + handled: true, + overrides: { ...(ctx.currentOverrides ?? {}), modelId: ctx.value }, + }; + case "stability": { + if (!ctx.policy.allowVoiceSettings) { + return { handled: true }; + } + const value = parseNumberValue(ctx.value); + if (value == null) { + return { handled: true, warnings: ["invalid stability value"] }; + } + requireInRange(value, 0, 1, "stability"); + return { handled: true, overrides: mergeVoiceSettingsOverride(ctx, { stability: value }) }; + } + case "similarity": + case "similarityboost": + case "similarity_boost": { + if (!ctx.policy.allowVoiceSettings) { + return { handled: true }; + } + const value = parseNumberValue(ctx.value); + if (value == null) { + return { handled: true, warnings: ["invalid similarityBoost value"] }; + } + requireInRange(value, 0, 1, "similarityBoost"); + return { + handled: true, + overrides: mergeVoiceSettingsOverride(ctx, { similarityBoost: value }), + }; + } + case "style": { + if (!ctx.policy.allowVoiceSettings) { + return { handled: true }; + } + const value = parseNumberValue(ctx.value); + if (value == null) { + return { handled: true, warnings: ["invalid style value"] }; + } + requireInRange(value, 0, 1, "style"); + return { handled: true, overrides: mergeVoiceSettingsOverride(ctx, { style: value }) }; + } + case "speed": { + if (!ctx.policy.allowVoiceSettings) { + return { handled: true }; + } + const value = parseNumberValue(ctx.value); + if (value == null) { + return { handled: true, warnings: ["invalid speed value"] }; + } + requireInRange(value, 0.5, 2, "speed"); + return { handled: true, overrides: mergeVoiceSettingsOverride(ctx, { speed: value }) }; + } + case "speakerboost": + case "speaker_boost": + case "usespeakerboost": + case "use_speaker_boost": { + if (!ctx.policy.allowVoiceSettings) { + return { handled: true }; + } + const value = parseBooleanValue(ctx.value); + if (value == null) { + return { handled: true, warnings: ["invalid useSpeakerBoost value"] }; + } + return { + handled: true, + overrides: mergeVoiceSettingsOverride(ctx, { useSpeakerBoost: value }), + }; + } + case "normalize": + case "applytextnormalization": + case "apply_text_normalization": + if (!ctx.policy.allowNormalization) { + return { handled: true }; + } + return { + handled: true, + overrides: { + ...(ctx.currentOverrides ?? {}), + applyTextNormalization: normalizeApplyTextNormalization(ctx.value), + }, + }; + case "language": + case "languagecode": + case "language_code": + if (!ctx.policy.allowNormalization) { + return { handled: true }; + } + return { + handled: true, + overrides: { + ...(ctx.currentOverrides ?? {}), + languageCode: normalizeLanguageCode(ctx.value), + }, + }; + case "seed": + if (!ctx.policy.allowSeed) { + return { handled: true }; + } + return { + handled: true, + overrides: { + ...(ctx.currentOverrides ?? {}), + seed: normalizeSeed(Number.parseInt(ctx.value, 10)), + }, + }; + default: + return { handled: false }; + } + } catch (error) { + return { + handled: true, + warnings: [error instanceof Error ? error.message : String(error)], + }; + } } export async function listElevenLabsVoices(params: { @@ -49,49 +342,164 @@ export function buildElevenLabsSpeechProvider(): SpeechProviderPlugin { return { id: "elevenlabs", label: "ElevenLabs", + autoSelectOrder: 20, models: ELEVENLABS_TTS_MODELS, + resolveConfig: ({ rawConfig }) => normalizeElevenLabsProviderConfig(rawConfig), + parseDirectiveToken, + resolveTalkConfig: ({ baseTtsConfig, talkProviderConfig }) => { + const base = normalizeElevenLabsProviderConfig(baseTtsConfig); + const talkVoiceSettings = asObject(talkProviderConfig.voiceSettings); + return { + ...base, + ...(talkProviderConfig.apiKey === undefined + ? {} + : { + apiKey: normalizeResolvedSecretInputString({ + value: talkProviderConfig.apiKey, + path: "talk.providers.elevenlabs.apiKey", + }), + }), + ...(trimToUndefined(talkProviderConfig.baseUrl) == null + ? {} + : { baseUrl: normalizeElevenLabsBaseUrl(trimToUndefined(talkProviderConfig.baseUrl)) }), + ...(trimToUndefined(talkProviderConfig.voiceId) == null + ? {} + : { voiceId: trimToUndefined(talkProviderConfig.voiceId) }), + ...(trimToUndefined(talkProviderConfig.modelId) == null + ? {} + : { modelId: trimToUndefined(talkProviderConfig.modelId) }), + ...(asNumber(talkProviderConfig.seed) == null + ? {} + : { seed: asNumber(talkProviderConfig.seed) }), + ...(trimToUndefined(talkProviderConfig.applyTextNormalization) == null + ? {} + : { + applyTextNormalization: normalizeApplyTextNormalization( + trimToUndefined(talkProviderConfig.applyTextNormalization), + ), + }), + ...(trimToUndefined(talkProviderConfig.languageCode) == null + ? {} + : { + languageCode: normalizeLanguageCode(trimToUndefined(talkProviderConfig.languageCode)), + }), + voiceSettings: { + ...base.voiceSettings, + ...(asNumber(talkVoiceSettings?.stability) == null + ? {} + : { stability: asNumber(talkVoiceSettings?.stability) }), + ...(asNumber(talkVoiceSettings?.similarityBoost) == null + ? {} + : { similarityBoost: asNumber(talkVoiceSettings?.similarityBoost) }), + ...(asNumber(talkVoiceSettings?.style) == null + ? {} + : { style: asNumber(talkVoiceSettings?.style) }), + ...(asBoolean(talkVoiceSettings?.useSpeakerBoost) == null + ? {} + : { useSpeakerBoost: asBoolean(talkVoiceSettings?.useSpeakerBoost) }), + ...(asNumber(talkVoiceSettings?.speed) == null + ? {} + : { speed: asNumber(talkVoiceSettings?.speed) }), + }, + }; + }, + resolveTalkOverrides: ({ params }) => { + const normalize = trimToUndefined(params.normalize); + const language = trimToUndefined(params.language)?.toLowerCase(); + const voiceSettings = { + ...(asNumber(params.speed) == null ? {} : { speed: asNumber(params.speed) }), + ...(asNumber(params.stability) == null ? {} : { stability: asNumber(params.stability) }), + ...(asNumber(params.similarity) == null + ? {} + : { similarityBoost: asNumber(params.similarity) }), + ...(asNumber(params.style) == null ? {} : { style: asNumber(params.style) }), + ...(asBoolean(params.speakerBoost) == null + ? {} + : { useSpeakerBoost: asBoolean(params.speakerBoost) }), + }; + return { + ...(trimToUndefined(params.voiceId) == null + ? {} + : { voiceId: trimToUndefined(params.voiceId) }), + ...(trimToUndefined(params.modelId) == null + ? {} + : { modelId: trimToUndefined(params.modelId) }), + ...(trimToUndefined(params.outputFormat) == null + ? {} + : { outputFormat: trimToUndefined(params.outputFormat) }), + ...(asNumber(params.seed) == null ? {} : { seed: asNumber(params.seed) }), + ...(normalize == null + ? {} + : { applyTextNormalization: normalizeApplyTextNormalization(normalize) }), + ...(language == null ? {} : { languageCode: normalizeLanguageCode(language) }), + ...(Object.keys(voiceSettings).length === 0 ? {} : { voiceSettings }), + }; + }, listVoices: async (req) => { + const config = req.providerConfig + ? readElevenLabsProviderConfig(req.providerConfig) + : undefined; const apiKey = - req.apiKey || - req.config?.elevenlabs.apiKey || - process.env.ELEVENLABS_API_KEY || - process.env.XI_API_KEY; + req.apiKey || config?.apiKey || process.env.ELEVENLABS_API_KEY || process.env.XI_API_KEY; if (!apiKey) { throw new Error("ElevenLabs API key missing"); } return listElevenLabsVoices({ apiKey, - baseUrl: req.baseUrl ?? req.config?.elevenlabs.baseUrl, + baseUrl: req.baseUrl ?? config?.baseUrl, }); }, - isConfigured: ({ config }) => - Boolean(config.elevenlabs.apiKey || process.env.ELEVENLABS_API_KEY || process.env.XI_API_KEY), + isConfigured: ({ providerConfig }) => + Boolean( + readElevenLabsProviderConfig(providerConfig).apiKey || + process.env.ELEVENLABS_API_KEY || + process.env.XI_API_KEY, + ), synthesize: async (req) => { - const apiKey = - req.config.elevenlabs.apiKey || process.env.ELEVENLABS_API_KEY || process.env.XI_API_KEY; + const config = readElevenLabsProviderConfig(req.providerConfig); + const overrides = req.providerOverrides ?? {}; + const apiKey = config.apiKey || process.env.ELEVENLABS_API_KEY || process.env.XI_API_KEY; if (!apiKey) { throw new Error("ElevenLabs API key missing"); } const outputFormat = - req.overrides?.elevenlabs?.outputFormat ?? + trimToUndefined(overrides.outputFormat) ?? (req.target === "voice-note" ? "opus_48000_64" : "mp3_44100_128"); + const overrideVoiceSettings = asObject(overrides.voiceSettings); const audioBuffer = await elevenLabsTTS({ text: req.text, apiKey, - baseUrl: req.config.elevenlabs.baseUrl, - voiceId: req.overrides?.elevenlabs?.voiceId ?? req.config.elevenlabs.voiceId, - modelId: req.overrides?.elevenlabs?.modelId ?? req.config.elevenlabs.modelId, + baseUrl: config.baseUrl, + voiceId: trimToUndefined(overrides.voiceId) ?? config.voiceId, + modelId: trimToUndefined(overrides.modelId) ?? config.modelId, outputFormat, - seed: req.overrides?.elevenlabs?.seed ?? req.config.elevenlabs.seed, + seed: asNumber(overrides.seed) ?? config.seed, applyTextNormalization: - req.overrides?.elevenlabs?.applyTextNormalization ?? - req.config.elevenlabs.applyTextNormalization, - languageCode: req.overrides?.elevenlabs?.languageCode ?? req.config.elevenlabs.languageCode, + (trimToUndefined(overrides.applyTextNormalization) as + | "auto" + | "on" + | "off" + | undefined) ?? config.applyTextNormalization, + languageCode: trimToUndefined(overrides.languageCode) ?? config.languageCode, voiceSettings: { - ...req.config.elevenlabs.voiceSettings, - ...req.overrides?.elevenlabs?.voiceSettings, + ...config.voiceSettings, + ...(asNumber(overrideVoiceSettings?.stability) == null + ? {} + : { stability: asNumber(overrideVoiceSettings?.stability) }), + ...(asNumber(overrideVoiceSettings?.similarityBoost) == null + ? {} + : { similarityBoost: asNumber(overrideVoiceSettings?.similarityBoost) }), + ...(asNumber(overrideVoiceSettings?.style) == null + ? {} + : { style: asNumber(overrideVoiceSettings?.style) }), + ...(asBoolean(overrideVoiceSettings?.useSpeakerBoost) == null + ? {} + : { useSpeakerBoost: asBoolean(overrideVoiceSettings?.useSpeakerBoost) }), + ...(asNumber(overrideVoiceSettings?.speed) == null + ? {} + : { speed: asNumber(overrideVoiceSettings?.speed) }), }, - timeoutMs: req.config.timeoutMs, + timeoutMs: req.timeoutMs, }); return { audioBuffer, @@ -101,8 +509,8 @@ export function buildElevenLabsSpeechProvider(): SpeechProviderPlugin { }; }, synthesizeTelephony: async (req) => { - const apiKey = - req.config.elevenlabs.apiKey || process.env.ELEVENLABS_API_KEY || process.env.XI_API_KEY; + const config = readElevenLabsProviderConfig(req.providerConfig); + const apiKey = config.apiKey || process.env.ELEVENLABS_API_KEY || process.env.XI_API_KEY; if (!apiKey) { throw new Error("ElevenLabs API key missing"); } @@ -111,15 +519,15 @@ export function buildElevenLabsSpeechProvider(): SpeechProviderPlugin { const audioBuffer = await elevenLabsTTS({ text: req.text, apiKey, - baseUrl: req.config.elevenlabs.baseUrl, - voiceId: req.config.elevenlabs.voiceId, - modelId: req.config.elevenlabs.modelId, + baseUrl: config.baseUrl, + voiceId: config.voiceId, + modelId: config.modelId, outputFormat, - seed: req.config.elevenlabs.seed, - applyTextNormalization: req.config.elevenlabs.applyTextNormalization, - languageCode: req.config.elevenlabs.languageCode, - voiceSettings: req.config.elevenlabs.voiceSettings, - timeoutMs: req.config.timeoutMs, + seed: config.seed, + applyTextNormalization: config.applyTextNormalization, + languageCode: config.languageCode, + voiceSettings: config.voiceSettings, + timeoutMs: req.timeoutMs, }); return { audioBuffer, outputFormat, sampleRate }; }, diff --git a/extensions/microsoft/speech-provider.ts b/extensions/microsoft/speech-provider.ts index 3967a0c62d2..27317210095 100644 --- a/extensions/microsoft/speech-provider.ts +++ b/extensions/microsoft/speech-provider.ts @@ -5,14 +5,33 @@ import { TRUSTED_CLIENT_TOKEN, generateSecMsGecToken, } from "node-edge-tts/dist/drm.js"; -import type { SpeechProviderPlugin } from "openclaw/plugin-sdk/core"; import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/llm-task"; import { isVoiceCompatibleAudio } from "openclaw/plugin-sdk/media-runtime"; -import type { SpeechVoiceOption } from "openclaw/plugin-sdk/speech"; +import type { + SpeechProviderConfig, + SpeechProviderPlugin, + SpeechVoiceOption, +} from "openclaw/plugin-sdk/speech-core"; import { edgeTTS, inferEdgeExtension } from "./tts.js"; +const DEFAULT_EDGE_VOICE = "en-US-MichelleNeural"; +const DEFAULT_EDGE_LANG = "en-US"; const DEFAULT_EDGE_OUTPUT_FORMAT = "audio-24khz-48kbitrate-mono-mp3"; +type MicrosoftProviderConfig = { + enabled: boolean; + voice: string; + lang: string; + outputFormat: string; + outputFormatConfigured: boolean; + pitch?: string; + rate?: string; + volume?: string; + saveSubtitles: boolean; + proxy?: string; + timeoutMs?: number; +}; + type MicrosoftVoiceListEntry = { ShortName?: string; FriendlyName?: string; @@ -24,6 +43,64 @@ type MicrosoftVoiceListEntry = { }; }; +function trimToUndefined(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function asBoolean(value: unknown): boolean | undefined { + return typeof value === "boolean" ? value : undefined; +} + +function asNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +function asObject(value: unknown): Record | undefined { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +function normalizeMicrosoftProviderConfig( + rawConfig: Record, +): MicrosoftProviderConfig { + const rawEdge = asObject(rawConfig.edge); + const rawMicrosoft = asObject(rawConfig.microsoft); + const raw = { ...(rawEdge ?? {}), ...(rawMicrosoft ?? {}) }; + const outputFormat = trimToUndefined(raw.outputFormat); + return { + enabled: asBoolean(raw.enabled) ?? true, + voice: trimToUndefined(raw.voice) ?? DEFAULT_EDGE_VOICE, + lang: trimToUndefined(raw.lang) ?? DEFAULT_EDGE_LANG, + outputFormat: outputFormat ?? DEFAULT_EDGE_OUTPUT_FORMAT, + outputFormatConfigured: Boolean(outputFormat), + pitch: trimToUndefined(raw.pitch), + rate: trimToUndefined(raw.rate), + volume: trimToUndefined(raw.volume), + saveSubtitles: asBoolean(raw.saveSubtitles) ?? false, + proxy: trimToUndefined(raw.proxy), + timeoutMs: asNumber(raw.timeoutMs), + }; +} + +function readMicrosoftProviderConfig(config: SpeechProviderConfig): MicrosoftProviderConfig { + const defaults = normalizeMicrosoftProviderConfig({}); + return { + enabled: asBoolean(config.enabled) ?? defaults.enabled, + voice: trimToUndefined(config.voice) ?? defaults.voice, + lang: trimToUndefined(config.lang) ?? defaults.lang, + outputFormat: trimToUndefined(config.outputFormat) ?? defaults.outputFormat, + outputFormatConfigured: + asBoolean(config.outputFormatConfigured) ?? defaults.outputFormatConfigured, + pitch: trimToUndefined(config.pitch) ?? defaults.pitch, + rate: trimToUndefined(config.rate) ?? defaults.rate, + volume: trimToUndefined(config.volume) ?? defaults.volume, + saveSubtitles: asBoolean(config.saveSubtitles) ?? defaults.saveSubtitles, + proxy: trimToUndefined(config.proxy) ?? defaults.proxy, + timeoutMs: asNumber(config.timeoutMs) ?? defaults.timeoutMs, + }; +} + function buildMicrosoftVoiceHeaders(): Record { const major = CHROMIUM_FULL_VERSION.split(".")[0] || "0"; return { @@ -77,13 +154,57 @@ export function buildMicrosoftSpeechProvider(): SpeechProviderPlugin { id: "microsoft", label: "Microsoft", aliases: ["edge"], + autoSelectOrder: 30, + resolveConfig: ({ rawConfig }) => normalizeMicrosoftProviderConfig(rawConfig), + resolveTalkConfig: ({ baseTtsConfig, talkProviderConfig }) => { + const base = normalizeMicrosoftProviderConfig(baseTtsConfig); + return { + ...base, + enabled: true, + ...(trimToUndefined(talkProviderConfig.voiceId) == null + ? {} + : { voice: trimToUndefined(talkProviderConfig.voiceId) }), + ...(trimToUndefined(talkProviderConfig.languageCode) == null + ? {} + : { lang: trimToUndefined(talkProviderConfig.languageCode) }), + ...(trimToUndefined(talkProviderConfig.outputFormat) == null + ? {} + : { outputFormat: trimToUndefined(talkProviderConfig.outputFormat) }), + ...(trimToUndefined(talkProviderConfig.pitch) == null + ? {} + : { pitch: trimToUndefined(talkProviderConfig.pitch) }), + ...(trimToUndefined(talkProviderConfig.rate) == null + ? {} + : { rate: trimToUndefined(talkProviderConfig.rate) }), + ...(trimToUndefined(talkProviderConfig.volume) == null + ? {} + : { volume: trimToUndefined(talkProviderConfig.volume) }), + ...(trimToUndefined(talkProviderConfig.proxy) == null + ? {} + : { proxy: trimToUndefined(talkProviderConfig.proxy) }), + ...(asNumber(talkProviderConfig.timeoutMs) == null + ? {} + : { timeoutMs: asNumber(talkProviderConfig.timeoutMs) }), + }; + }, + resolveTalkOverrides: ({ params }) => ({ + ...(trimToUndefined(params.voiceId) == null + ? {} + : { voice: trimToUndefined(params.voiceId) }), + ...(trimToUndefined(params.outputFormat) == null + ? {} + : { outputFormat: trimToUndefined(params.outputFormat) }), + }), listVoices: async () => await listMicrosoftVoices(), - isConfigured: ({ config }) => config.edge.enabled, + isConfigured: ({ providerConfig }) => readMicrosoftProviderConfig(providerConfig).enabled, synthesize: async (req) => { + const config = readMicrosoftProviderConfig(req.providerConfig); const tempRoot = resolvePreferredOpenClawTmpDir(); mkdirSync(tempRoot, { recursive: true, mode: 0o700 }); const tempDir = mkdtempSync(path.join(tempRoot, "tts-microsoft-")); - let outputFormat = req.overrides?.microsoft?.outputFormat ?? req.config.edge.outputFormat; + const overrideVoice = trimToUndefined(req.providerOverrides?.voice); + let outputFormat = + trimToUndefined(req.providerOverrides?.outputFormat) ?? config.outputFormat; const fallbackOutputFormat = outputFormat !== DEFAULT_EDGE_OUTPUT_FORMAT ? DEFAULT_EDGE_OUTPUT_FORMAT : undefined; @@ -95,11 +216,11 @@ export function buildMicrosoftSpeechProvider(): SpeechProviderPlugin { text: req.text, outputPath, config: { - ...req.config.edge, - voice: req.overrides?.microsoft?.voice ?? req.config.edge.voice, + ...config, + voice: overrideVoice ?? config.voice, outputFormat: format, }, - timeoutMs: req.config.timeoutMs, + timeoutMs: req.timeoutMs, }); const audioBuffer = readFileSync(outputPath); return { @@ -112,9 +233,9 @@ export function buildMicrosoftSpeechProvider(): SpeechProviderPlugin { try { return await runEdge(outputFormat); - } catch (err) { + } catch (error) { if (!fallbackOutputFormat || fallbackOutputFormat === outputFormat) { - throw err; + throw error; } outputFormat = fallbackOutputFormat; return await runEdge(outputFormat); diff --git a/extensions/openai/index.test.ts b/extensions/openai/index.test.ts index fec3b9416c2..ea5d2b08d68 100644 --- a/extensions/openai/index.test.ts +++ b/extensions/openai/index.test.ts @@ -137,32 +137,14 @@ function createLiveTtsConfig(): ResolvedTtsConfig { allowNormalization: true, allowSeed: true, }, - elevenlabs: { - baseUrl: "https://api.elevenlabs.io", - voiceId: "", - modelId: "eleven_multilingual_v2", - voiceSettings: { - stability: 0.5, - similarityBoost: 0.75, - style: 0, - useSpeakerBoost: true, - speed: 1, + providerConfigs: { + openai: { + apiKey: OPENAI_API_KEY, + baseUrl: "https://api.openai.com/v1", + model: "gpt-4o-mini-tts", + voice: "alloy", }, }, - openai: { - apiKey: OPENAI_API_KEY, - baseUrl: "https://api.openai.com/v1", - model: "gpt-4o-mini-tts", - voice: "alloy", - }, - edge: { - enabled: false, - voice: "en-US-AriaNeural", - lang: "en-US", - outputFormat: "audio-24khz-48kbitrate-mono-mp3", - outputFormatConfigured: false, - saveSubtitles: false, - }, maxTextLength: 4_000, timeoutMs: 30_000, }; @@ -358,8 +340,9 @@ describeLive("openai plugin live", () => { const audioFile = await speechProvider.synthesize({ text: "OpenClaw integration test OK.", cfg, - config: ttsConfig, + providerConfig: ttsConfig.providerConfigs.openai ?? {}, target: "audio-file", + timeoutMs: ttsConfig.timeoutMs, }); expect(audioFile.outputFormat).toBe("mp3"); expect(audioFile.fileExtension).toBe(".mp3"); @@ -368,7 +351,8 @@ describeLive("openai plugin live", () => { const telephony = await speechProvider.synthesizeTelephony?.({ text: "Telephony check OK.", cfg, - config: ttsConfig, + providerConfig: ttsConfig.providerConfigs.openai ?? {}, + timeoutMs: ttsConfig.timeoutMs, }); expect(telephony?.outputFormat).toBe("pcm"); expect(telephony?.sampleRate).toBe(24_000); @@ -386,8 +370,9 @@ describeLive("openai plugin live", () => { const synthesized = await speechProvider.synthesize({ text: "OpenClaw integration test OK.", cfg, - config: ttsConfig, + providerConfig: ttsConfig.providerConfigs.openai ?? {}, target: "audio-file", + timeoutMs: ttsConfig.timeoutMs, }); const transcription = await mediaProvider.transcribeAudio?.({ diff --git a/extensions/openai/speech-provider.ts b/extensions/openai/speech-provider.ts index 91b8008db9c..28b15707c6e 100644 --- a/extensions/openai/speech-provider.ts +++ b/extensions/openai/speech-provider.ts @@ -1,16 +1,181 @@ -import type { SpeechProviderPlugin } from "openclaw/plugin-sdk/core"; -import { OPENAI_TTS_MODELS, OPENAI_TTS_VOICES, openaiTTS } from "./tts.js"; +import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input"; +import type { + SpeechDirectiveTokenParseContext, + SpeechProviderConfig, + SpeechProviderOverrides, + SpeechProviderPlugin, +} from "openclaw/plugin-sdk/speech-core"; +import { + DEFAULT_OPENAI_BASE_URL, + isValidOpenAIModel, + isValidOpenAIVoice, + normalizeOpenAITtsBaseUrl, + OPENAI_TTS_MODELS, + OPENAI_TTS_VOICES, + openaiTTS, +} from "./tts.js"; + +type OpenAITtsProviderConfig = { + apiKey?: string; + baseUrl: string; + model: string; + voice: string; + speed?: number; + instructions?: string; +}; + +type OpenAITtsProviderOverrides = { + model?: string; + voice?: string; + speed?: number; +}; + +function trimToUndefined(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function asNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +function asObject(value: unknown): Record | undefined { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +function normalizeOpenAIProviderConfig( + rawConfig: Record, +): OpenAITtsProviderConfig { + const raw = asObject(rawConfig.openai); + return { + apiKey: normalizeResolvedSecretInputString({ + value: raw?.apiKey, + path: "messages.tts.openai.apiKey", + }), + baseUrl: normalizeOpenAITtsBaseUrl( + trimToUndefined(raw?.baseUrl) ?? + trimToUndefined(process.env.OPENAI_TTS_BASE_URL) ?? + DEFAULT_OPENAI_BASE_URL, + ), + model: trimToUndefined(raw?.model) ?? "gpt-4o-mini-tts", + voice: trimToUndefined(raw?.voice) ?? "coral", + speed: asNumber(raw?.speed), + instructions: trimToUndefined(raw?.instructions), + }; +} + +function readOpenAIProviderConfig(config: SpeechProviderConfig): OpenAITtsProviderConfig { + const normalized = normalizeOpenAIProviderConfig({}); + return { + apiKey: trimToUndefined(config.apiKey) ?? normalized.apiKey, + baseUrl: trimToUndefined(config.baseUrl) ?? normalized.baseUrl, + model: trimToUndefined(config.model) ?? normalized.model, + voice: trimToUndefined(config.voice) ?? normalized.voice, + speed: asNumber(config.speed) ?? normalized.speed, + instructions: trimToUndefined(config.instructions) ?? normalized.instructions, + }; +} + +function readOpenAIOverrides( + overrides: SpeechProviderOverrides | undefined, +): OpenAITtsProviderOverrides { + if (!overrides) { + return {}; + } + return { + model: trimToUndefined(overrides.model), + voice: trimToUndefined(overrides.voice), + speed: asNumber(overrides.speed), + }; +} + +function parseDirectiveToken(ctx: SpeechDirectiveTokenParseContext): { + handled: boolean; + overrides?: SpeechProviderOverrides; + warnings?: string[]; +} { + const baseUrl = trimToUndefined(ctx.providerConfig?.baseUrl); + switch (ctx.key) { + case "voice": + case "openai_voice": + case "openaivoice": + if (!ctx.policy.allowVoice) { + return { handled: true }; + } + if (!isValidOpenAIVoice(ctx.value, baseUrl)) { + return { handled: true, warnings: [`invalid OpenAI voice "${ctx.value}"`] }; + } + return { handled: true, overrides: { voice: ctx.value } }; + case "model": + case "openai_model": + case "openaimodel": + if (!ctx.policy.allowModelId) { + return { handled: true }; + } + if (!isValidOpenAIModel(ctx.value, baseUrl)) { + return { handled: false }; + } + return { handled: true, overrides: { model: ctx.value } }; + default: + return { handled: false }; + } +} export function buildOpenAISpeechProvider(): SpeechProviderPlugin { return { id: "openai", label: "OpenAI", + autoSelectOrder: 10, models: OPENAI_TTS_MODELS, voices: OPENAI_TTS_VOICES, + resolveConfig: ({ rawConfig }) => normalizeOpenAIProviderConfig(rawConfig), + parseDirectiveToken, + resolveTalkConfig: ({ baseTtsConfig, talkProviderConfig }) => { + const base = normalizeOpenAIProviderConfig(baseTtsConfig); + return { + ...base, + ...(talkProviderConfig.apiKey === undefined + ? {} + : { + apiKey: normalizeResolvedSecretInputString({ + value: talkProviderConfig.apiKey, + path: "talk.providers.openai.apiKey", + }), + }), + ...(trimToUndefined(talkProviderConfig.baseUrl) == null + ? {} + : { baseUrl: trimToUndefined(talkProviderConfig.baseUrl) }), + ...(trimToUndefined(talkProviderConfig.modelId) == null + ? {} + : { model: trimToUndefined(talkProviderConfig.modelId) }), + ...(trimToUndefined(talkProviderConfig.voiceId) == null + ? {} + : { voice: trimToUndefined(talkProviderConfig.voiceId) }), + ...(asNumber(talkProviderConfig.speed) == null + ? {} + : { speed: asNumber(talkProviderConfig.speed) }), + ...(trimToUndefined(talkProviderConfig.instructions) == null + ? {} + : { instructions: trimToUndefined(talkProviderConfig.instructions) }), + }; + }, + resolveTalkOverrides: ({ params }) => ({ + ...(trimToUndefined(params.voiceId) == null + ? {} + : { voice: trimToUndefined(params.voiceId) }), + ...(trimToUndefined(params.modelId) == null + ? {} + : { model: trimToUndefined(params.modelId) }), + ...(asNumber(params.speed) == null ? {} : { speed: asNumber(params.speed) }), + }), listVoices: async () => OPENAI_TTS_VOICES.map((voice) => ({ id: voice, name: voice })), - isConfigured: ({ config }) => Boolean(config.openai.apiKey || process.env.OPENAI_API_KEY), + isConfigured: ({ providerConfig }) => + Boolean(readOpenAIProviderConfig(providerConfig).apiKey || process.env.OPENAI_API_KEY), synthesize: async (req) => { - const apiKey = req.config.openai.apiKey || process.env.OPENAI_API_KEY; + const config = readOpenAIProviderConfig(req.providerConfig); + const overrides = readOpenAIOverrides(req.providerOverrides); + const apiKey = config.apiKey || process.env.OPENAI_API_KEY; if (!apiKey) { throw new Error("OpenAI API key missing"); } @@ -18,13 +183,13 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin { const audioBuffer = await openaiTTS({ text: req.text, apiKey, - baseUrl: req.config.openai.baseUrl, - model: req.overrides?.openai?.model ?? req.config.openai.model, - voice: req.overrides?.openai?.voice ?? req.config.openai.voice, - speed: req.overrides?.openai?.speed ?? req.config.openai.speed, - instructions: req.config.openai.instructions, + baseUrl: config.baseUrl, + model: overrides.model ?? config.model, + voice: overrides.voice ?? config.voice, + speed: overrides.speed ?? config.speed, + instructions: config.instructions, responseFormat, - timeoutMs: req.config.timeoutMs, + timeoutMs: req.timeoutMs, }); return { audioBuffer, @@ -34,7 +199,8 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin { }; }, synthesizeTelephony: async (req) => { - const apiKey = req.config.openai.apiKey || process.env.OPENAI_API_KEY; + const config = readOpenAIProviderConfig(req.providerConfig); + const apiKey = config.apiKey || process.env.OPENAI_API_KEY; if (!apiKey) { throw new Error("OpenAI API key missing"); } @@ -43,13 +209,13 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin { const audioBuffer = await openaiTTS({ text: req.text, apiKey, - baseUrl: req.config.openai.baseUrl, - model: req.config.openai.model, - voice: req.config.openai.voice, - speed: req.config.openai.speed, - instructions: req.config.openai.instructions, + baseUrl: config.baseUrl, + model: config.model, + voice: config.voice, + speed: config.speed, + instructions: config.instructions, responseFormat: outputFormat, - timeoutMs: req.config.timeoutMs, + timeoutMs: req.timeoutMs, }); return { audioBuffer, outputFormat, sampleRate }; }, diff --git a/extensions/openai/tts.ts b/extensions/openai/tts.ts index 52a288fef6f..edc23baa674 100644 --- a/extensions/openai/tts.ts +++ b/extensions/openai/tts.ts @@ -1,4 +1,4 @@ -const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"; +export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"; export const OPENAI_TTS_MODELS = ["gpt-4o-mini-tts", "tts-1", "tts-1-hd"] as const; @@ -21,7 +21,7 @@ export const OPENAI_TTS_VOICES = [ type OpenAiTtsVoice = (typeof OPENAI_TTS_VOICES)[number]; -function normalizeOpenAITtsBaseUrl(baseUrl?: string): string { +export function normalizeOpenAITtsBaseUrl(baseUrl?: string): string { const trimmed = baseUrl?.trim(); if (!trimmed) { return DEFAULT_OPENAI_BASE_URL; @@ -36,21 +36,24 @@ function isCustomOpenAIEndpoint(baseUrl?: string): boolean { return normalizeOpenAITtsBaseUrl(process.env.OPENAI_TTS_BASE_URL) !== DEFAULT_OPENAI_BASE_URL; } -function isValidOpenAIModel(model: string, baseUrl?: string): boolean { +export function isValidOpenAIModel(model: string, baseUrl?: string): boolean { if (isCustomOpenAIEndpoint(baseUrl)) { return true; } return OPENAI_TTS_MODELS.includes(model as (typeof OPENAI_TTS_MODELS)[number]); } -function isValidOpenAIVoice(voice: string, baseUrl?: string): voice is OpenAiTtsVoice { +export function isValidOpenAIVoice(voice: string, baseUrl?: string): voice is OpenAiTtsVoice { if (isCustomOpenAIEndpoint(baseUrl)) { return true; } return OPENAI_TTS_VOICES.includes(voice as OpenAiTtsVoice); } -function resolveOpenAITtsInstructions(model: string, instructions?: string): string | undefined { +export function resolveOpenAITtsInstructions( + model: string, + instructions?: string, +): string | undefined { const next = instructions?.trim(); return next && model.includes("gpt-4o-mini-tts") ? next : undefined; } diff --git a/extensions/voice-call/src/providers/tts-openai.ts b/extensions/voice-call/src/providers/tts-openai.ts index 9ed4e92e796..1fdc7a147d3 100644 --- a/extensions/voice-call/src/providers/tts-openai.ts +++ b/extensions/voice-call/src/providers/tts-openai.ts @@ -1,4 +1,3 @@ -import { resolveOpenAITtsInstructions } from "../../api.js"; import { convertPcmToMulaw8k } from "../telephony-audio.js"; /** @@ -72,6 +71,11 @@ function trimToUndefined(value: string | undefined): string | undefined { return trimmed ? trimmed : undefined; } +function resolveOpenAITtsInstructions(model: string, instructions?: string): string | undefined { + const next = trimToUndefined(instructions); + return next && model.includes("gpt-4o-mini-tts") ? next : undefined; +} + /** * OpenAI TTS Provider for generating speech audio. */ diff --git a/scripts/generate-bundled-plugin-metadata.mjs b/scripts/generate-bundled-plugin-metadata.mjs index 903f5a96324..f78525c288b 100644 --- a/scripts/generate-bundled-plugin-metadata.mjs +++ b/scripts/generate-bundled-plugin-metadata.mjs @@ -8,13 +8,6 @@ const DEFAULT_OUTPUT_PATH = "src/plugins/bundled-plugin-metadata.generated.ts"; const DEFAULT_ENTRIES_OUTPUT_PATH = "src/generated/bundled-plugin-entries.generated.ts"; const MANIFEST_KEY = "openclaw"; const FORMATTER_CWD = path.resolve(import.meta.dirname, ".."); -const CANONICAL_PACKAGE_ID_ALIASES = { - "elevenlabs-speech": "elevenlabs", - "microsoft-speech": "microsoft", - "ollama-provider": "ollama", - "sglang-provider": "sglang", - "vllm-provider": "vllm", -}; function rewriteEntryToBuiltPath(entry) { if (typeof entry !== "string" || entry.trim().length === 0) { @@ -24,8 +17,12 @@ function rewriteEntryToBuiltPath(entry) { return normalized.replace(/\.[^.]+$/u, ".js"); } -function deriveIdHint({ filePath, packageName, hasMultipleExtensions }) { +function deriveIdHint({ filePath, manifestId, packageName, hasMultipleExtensions }) { const base = path.basename(filePath, path.extname(filePath)); + const normalizedManifestId = manifestId?.trim(); + if (normalizedManifestId) { + return hasMultipleExtensions ? `${normalizedManifestId}/${base}` : normalizedManifestId; + } const rawPackageName = packageName?.trim(); if (!rawPackageName) { return base; @@ -34,11 +31,10 @@ function deriveIdHint({ filePath, packageName, hasMultipleExtensions }) { const unscoped = rawPackageName.includes("/") ? (rawPackageName.split("/").pop() ?? rawPackageName) : rawPackageName; - const canonicalPackageId = CANONICAL_PACKAGE_ID_ALIASES[unscoped] ?? unscoped; const normalizedPackageId = - canonicalPackageId.endsWith("-provider") && canonicalPackageId.length > "-provider".length - ? canonicalPackageId.slice(0, -"-provider".length) - : canonicalPackageId; + unscoped.endsWith("-provider") && unscoped.length > "-provider".length + ? unscoped.slice(0, -"-provider".length) + : unscoped; if (!hasMultipleExtensions) { return normalizedPackageId; @@ -184,6 +180,7 @@ export function collectBundledPluginMetadata(params = {}) { dirName: source.dirName, idHint: deriveIdHint({ filePath: sourceEntry, + manifestId: manifest.id, packageName: typeof packageJson.name === "string" ? packageJson.name : undefined, hasMultipleExtensions: extensions.length > 1, }), @@ -219,19 +216,34 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = ${JSON.stringify(entries, null, export function renderBundledPluginEntriesModule(entries) { const imports = entries .map((entry) => { - const identifier = toIdentifier(entry.dirName); const importPath = normalizeGeneratedImportPath(entry.dirName, entry.source.built); - return `import ${identifier} from "${importPath}";`; + return ` import("${importPath}")`; }) - .join("\n"); - const identifiers = entries.map((entry) => toIdentifier(entry.dirName)).join(",\n "); + .join(",\n"); + const bindings = entries + .map((entry) => { + const identifier = toIdentifier(entry.dirName); + return `${identifier}Module`; + }) + .join(",\n "); + const identifiers = entries + .map((entry) => { + const identifier = toIdentifier(entry.dirName); + return `${identifier}Module.default`; + }) + .join(",\n "); return `// Auto-generated by ${GENERATED_BY}. Do not edit directly. +export async function loadGeneratedBundledPluginEntries() { + const [ + ${bindings} + ] = await Promise.all([ ${imports} - -export const GENERATED_BUNDLED_PLUGIN_ENTRIES = [ - ${identifiers} -] as const; + ]); + return [ + ${identifiers} + ] as const; +} `; } diff --git a/src/auto-reply/commands-registry.shared.ts b/src/auto-reply/commands-registry.shared.ts index 5edf83ebe8a..41f49cd3ff2 100644 --- a/src/auto-reply/commands-registry.shared.ts +++ b/src/auto-reply/commands-registry.shared.ts @@ -257,7 +257,7 @@ export function buildBuiltinChatCommands(): ChatCommandDefinition[] { "• On – Enable TTS for responses\n" + "• Off – Disable TTS\n" + "• Status – Show current settings\n" + - "• Provider – Set voice provider (edge, elevenlabs, openai)\n" + + "• Provider – Show or set the voice provider\n" + "• Limit – Set max characters for TTS\n" + "• Summary – Toggle AI summary for long texts\n" + "• Audio – Generate TTS from custom text\n" + diff --git a/src/auto-reply/reply/commands-tts.ts b/src/auto-reply/reply/commands-tts.ts index 67023318bdd..0712a65679f 100644 --- a/src/auto-reply/reply/commands-tts.ts +++ b/src/auto-reply/reply/commands-tts.ts @@ -1,13 +1,17 @@ import { logVerbose } from "../../globals.js"; -import { getSpeechProvider, normalizeSpeechProviderId } from "../../tts/provider-registry.js"; import { + canonicalizeSpeechProviderId, + getSpeechProvider, + listSpeechProviders, +} from "../../tts/provider-registry.js"; +import { + getResolvedSpeechProviderConfig, getLastTtsAttempt, getTtsMaxLength, getTtsProvider, isSummarizationEnabled, isTtsEnabled, isTtsProviderConfigured, - resolveTtsApiKey, resolveTtsConfig, resolveTtsPrefsPath, setLastTtsAttempt, @@ -55,15 +59,13 @@ function ttsUsage(): ReplyPayload { `• /tts summary [on|off] — View/change auto-summary\n` + `• /tts audio — Generate audio from text\n\n` + `**Providers:**\n` + - `• microsoft — Microsoft Edge-backed speech (default fallback)\n` + - `• openai — High quality (requires API key)\n` + - `• elevenlabs — Premium voices (requires API key)\n\n` + + `Use /tts provider to list the registered speech providers and their status.\n\n` + `**Text Limit (default: 1500, max: 4096):**\n` + `When text exceeds the limit:\n` + `• Summary ON: AI summarizes, then generates audio\n` + `• Summary OFF: Truncates text, then generates audio\n\n` + `**Examples:**\n` + - `/tts provider microsoft\n` + + `/tts provider \n` + `/tts limit 2000\n` + `/tts audio Hello, this is a test!`, }; @@ -160,30 +162,44 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand if (action === "provider") { const currentProvider = getTtsProvider(config, prefsPath); if (!args.trim()) { - const hasOpenAI = Boolean(resolveTtsApiKey(config, "openai")); - const hasElevenLabs = Boolean(resolveTtsApiKey(config, "elevenlabs")); - const hasMicrosoft = isTtsProviderConfigured(config, "microsoft", params.cfg); + const providers = listSpeechProviders(params.cfg); return { shouldContinue: false, reply: { text: `🎙️ TTS provider\n` + `Primary: ${currentProvider}\n` + - `OpenAI key: ${hasOpenAI ? "✅" : "❌"}\n` + - `ElevenLabs key: ${hasElevenLabs ? "✅" : "❌"}\n` + - `Microsoft enabled: ${hasMicrosoft ? "✅" : "❌"}\n` + - `Usage: /tts provider openai | elevenlabs | microsoft`, + providers + .map( + (provider) => + `${provider.label}: ${ + provider.isConfigured({ + cfg: params.cfg, + providerConfig: getResolvedSpeechProviderConfig( + config, + provider.id, + params.cfg, + ), + timeoutMs: config.timeoutMs, + }) + ? "✅" + : "❌" + }`, + ) + .join("\n") + + `\nUsage: /tts provider `, }, }; } const requested = args.trim().toLowerCase(); - if (requested !== "edge" && !getSpeechProvider(requested, params.cfg)) { + const resolvedProvider = getSpeechProvider(requested, params.cfg); + if (!resolvedProvider) { return { shouldContinue: false, reply: ttsUsage() }; } - const nextProvider = normalizeSpeechProviderId(requested) ?? requested; - setTtsProvider(prefsPath, requested); + const nextProvider = canonicalizeSpeechProviderId(requested, params.cfg) ?? resolvedProvider.id; + setTtsProvider(prefsPath, nextProvider); return { shouldContinue: false, reply: { text: `✅ TTS provider set to ${nextProvider}.` }, diff --git a/src/gateway/server-methods/talk.ts b/src/gateway/server-methods/talk.ts index 3930dc4c4ca..63d10191b19 100644 --- a/src/gateway/server-methods/talk.ts +++ b/src/gateway/server-methods/talk.ts @@ -3,7 +3,7 @@ import { redactConfigObject } from "../../config/redact-snapshot.js"; import { buildTalkConfigResponse, resolveActiveTalkProviderConfig } from "../../config/talk.js"; import type { TalkProviderConfig } from "../../config/types.gateway.js"; import type { OpenClawConfig, TtsConfig } from "../../config/types.js"; -import { normalizeSpeechProviderId } from "../../tts/provider-registry.js"; +import { canonicalizeSpeechProviderId, getSpeechProvider } from "../../tts/provider-registry.js"; import { synthesizeSpeech, type TtsDirectiveOverrides } from "../../tts/tts.js"; import { ErrorCodes, @@ -18,7 +18,6 @@ import type { GatewayRequestHandlers } from "./types.js"; const ADMIN_SCOPE = "operator.admin"; const TALK_SECRETS_SCOPE = "operator.talk.secrets"; -type ElevenLabsVoiceSettings = NonNullable["voiceSettings"]>; function canReadTalkSecrets(client: { connect?: { scopes?: string[] } } | null): boolean { const scopes = Array.isArray(client?.connect?.scopes) ? client.connect.scopes : []; @@ -33,27 +32,6 @@ function trimString(value: unknown): string | undefined { return trimmed.length > 0 ? trimmed : undefined; } -function finiteNumber(value: unknown): number | undefined { - return typeof value === "number" && Number.isFinite(value) ? value : undefined; -} - -function optionalBoolean(value: unknown): boolean | undefined { - return typeof value === "boolean" ? value : undefined; -} - -function plainObject(value: unknown): Record | undefined { - return typeof value === "object" && value !== null && !Array.isArray(value) - ? (value as Record) - : undefined; -} - -function normalizeTextNormalization(value: unknown): "auto" | "on" | "off" | undefined { - const normalized = trimString(value)?.toLowerCase(); - return normalized === "auto" || normalized === "on" || normalized === "off" - ? normalized - : undefined; -} - function normalizeAliasKey(value: string): string { return value.trim().toLowerCase(); } @@ -78,100 +56,39 @@ function resolveTalkVoiceId( return requested; } -function readTalkVoiceSettings( - providerConfig: TalkProviderConfig, -): ElevenLabsVoiceSettings | undefined { - const source = plainObject(providerConfig.voiceSettings); - if (!source) { - return undefined; - } - const stability = finiteNumber(source.stability); - const similarityBoost = finiteNumber(source.similarityBoost); - const style = finiteNumber(source.style); - const useSpeakerBoost = optionalBoolean(source.useSpeakerBoost); - const speed = finiteNumber(source.speed); - const voiceSettings = { - ...(stability == null ? {} : { stability }), - ...(similarityBoost == null ? {} : { similarityBoost }), - ...(style == null ? {} : { style }), - ...(useSpeakerBoost == null ? {} : { useSpeakerBoost }), - ...(speed == null ? {} : { speed }), - }; - return Object.keys(voiceSettings).length > 0 ? voiceSettings : undefined; -} - function buildTalkTtsConfig( config: OpenClawConfig, ): | { cfg: OpenClawConfig; provider: string; providerConfig: TalkProviderConfig } | { error: string } { const resolved = resolveActiveTalkProviderConfig(config.talk); - const provider = normalizeSpeechProviderId(resolved?.provider); + const provider = canonicalizeSpeechProviderId(resolved?.provider, config); if (!resolved || !provider) { return { error: "talk.speak unavailable: talk provider not configured" }; } + const speechProvider = getSpeechProvider(provider, config); + if (!speechProvider) { + return { + error: `talk.speak unavailable: speech provider "${provider}" does not support Talk mode`, + }; + } + const baseTts = config.messages?.tts ?? {}; const providerConfig = resolved.config; + const resolvedProviderConfig = + speechProvider.resolveTalkConfig?.({ + cfg: config, + baseTtsConfig: baseTts as Record, + talkProviderConfig: providerConfig, + timeoutMs: baseTts.timeoutMs ?? 30_000, + }) ?? providerConfig; const talkTts: TtsConfig = { ...baseTts, auto: "always", provider, + [provider]: resolvedProviderConfig, }; - const baseUrl = trimString(providerConfig.baseUrl); - const voiceId = trimString(providerConfig.voiceId); - const modelId = trimString(providerConfig.modelId); - const languageCode = trimString(providerConfig.languageCode); - - if (provider === "elevenlabs") { - const seed = finiteNumber(providerConfig.seed); - const applyTextNormalization = normalizeTextNormalization( - providerConfig.applyTextNormalization, - ); - const voiceSettings = readTalkVoiceSettings(providerConfig); - talkTts.elevenlabs = { - ...baseTts.elevenlabs, - ...(providerConfig.apiKey === undefined ? {} : { apiKey: providerConfig.apiKey }), - ...(baseUrl == null ? {} : { baseUrl }), - ...(voiceId == null ? {} : { voiceId }), - ...(modelId == null ? {} : { modelId }), - ...(seed == null ? {} : { seed }), - ...(applyTextNormalization == null ? {} : { applyTextNormalization }), - ...(languageCode == null ? {} : { languageCode }), - ...(voiceSettings == null ? {} : { voiceSettings }), - }; - } else if (provider === "openai") { - const speed = finiteNumber(providerConfig.speed); - const instructions = trimString(providerConfig.instructions); - talkTts.openai = { - ...baseTts.openai, - ...(providerConfig.apiKey === undefined ? {} : { apiKey: providerConfig.apiKey }), - ...(baseUrl == null ? {} : { baseUrl }), - ...(modelId == null ? {} : { model: modelId }), - ...(voiceId == null ? {} : { voice: voiceId }), - ...(speed == null ? {} : { speed }), - ...(instructions == null ? {} : { instructions }), - }; - } else if (provider === "microsoft") { - const outputFormat = trimString(providerConfig.outputFormat); - const pitch = trimString(providerConfig.pitch); - const rate = trimString(providerConfig.rate); - const volume = trimString(providerConfig.volume); - const proxy = trimString(providerConfig.proxy); - const timeoutMs = finiteNumber(providerConfig.timeoutMs); - talkTts.microsoft = { - ...baseTts.microsoft, - enabled: true, - ...(voiceId == null ? {} : { voice: voiceId }), - ...(languageCode == null ? {} : { lang: languageCode }), - ...(outputFormat == null ? {} : { outputFormat }), - ...(pitch == null ? {} : { pitch }), - ...(rate == null ? {} : { rate }), - ...(volume == null ? {} : { volume }), - ...(proxy == null ? {} : { proxy }), - ...(timeoutMs == null ? {} : { timeoutMs }), - }; - } return { provider, @@ -189,60 +106,31 @@ function buildTalkTtsConfig( function buildTalkSpeakOverrides( provider: string, providerConfig: TalkProviderConfig, + config: OpenClawConfig, params: Record, ): TtsDirectiveOverrides { - const voiceId = resolveTalkVoiceId(providerConfig, trimString(params.voiceId)); - const modelId = trimString(params.modelId); - const outputFormat = trimString(params.outputFormat); - const speed = finiteNumber(params.speed); - const seed = finiteNumber(params.seed); - const normalize = normalizeTextNormalization(params.normalize); - const language = trimString(params.language)?.toLowerCase(); - const overrides: TtsDirectiveOverrides = { provider }; - - if (provider === "elevenlabs") { - const voiceSettings = { - ...(speed == null ? {} : { speed }), - ...(finiteNumber(params.stability) == null - ? {} - : { stability: finiteNumber(params.stability) }), - ...(finiteNumber(params.similarity) == null - ? {} - : { similarityBoost: finiteNumber(params.similarity) }), - ...(finiteNumber(params.style) == null ? {} : { style: finiteNumber(params.style) }), - ...(optionalBoolean(params.speakerBoost) == null - ? {} - : { useSpeakerBoost: optionalBoolean(params.speakerBoost) }), - }; - overrides.elevenlabs = { - ...(voiceId == null ? {} : { voiceId }), - ...(modelId == null ? {} : { modelId }), - ...(outputFormat == null ? {} : { outputFormat }), - ...(seed == null ? {} : { seed }), - ...(normalize == null ? {} : { applyTextNormalization: normalize }), - ...(language == null ? {} : { languageCode: language }), - ...(Object.keys(voiceSettings).length === 0 ? {} : { voiceSettings }), - }; - return overrides; + const speechProvider = getSpeechProvider(provider, config); + if (!speechProvider?.resolveTalkOverrides) { + return { provider }; } - - if (provider === "openai") { - overrides.openai = { - ...(voiceId == null ? {} : { voice: voiceId }), - ...(modelId == null ? {} : { model: modelId }), - ...(speed == null ? {} : { speed }), - }; - return overrides; + const providerOverrides = speechProvider.resolveTalkOverrides({ + talkProviderConfig: providerConfig, + params: { + ...params, + ...(resolveTalkVoiceId(providerConfig, trimString(params.voiceId)) == null + ? {} + : { voiceId: resolveTalkVoiceId(providerConfig, trimString(params.voiceId)) }), + }, + }); + if (!providerOverrides || Object.keys(providerOverrides).length === 0) { + return { provider }; } - - if (provider === "microsoft") { - overrides.microsoft = { - ...(voiceId == null ? {} : { voice: voiceId }), - ...(outputFormat == null ? {} : { outputFormat }), - }; - } - - return overrides; + return { + provider, + providerOverrides: { + [provider]: providerOverrides, + }, + }; } function inferMimeType( @@ -350,7 +238,12 @@ export const talkHandlers: GatewayRequestHandlers = { return; } - const overrides = buildTalkSpeakOverrides(setup.provider, setup.providerConfig, params); + const overrides = buildTalkSpeakOverrides( + setup.provider, + setup.providerConfig, + snapshot.config, + params, + ); const result = await synthesizeSpeech({ text, cfg: setup.cfg, diff --git a/src/gateway/server-methods/tts.ts b/src/gateway/server-methods/tts.ts index 38934afbae9..28345c53e8c 100644 --- a/src/gateway/server-methods/tts.ts +++ b/src/gateway/server-methods/tts.ts @@ -1,15 +1,15 @@ import { loadConfig } from "../../config/config.js"; import { + canonicalizeSpeechProviderId, getSpeechProvider, listSpeechProviders, - normalizeSpeechProviderId, } from "../../tts/provider-registry.js"; import { + getResolvedSpeechProviderConfig, getTtsProvider, isTtsEnabled, isTtsProviderConfigured, resolveTtsAutoMode, - resolveTtsApiKey, resolveTtsConfig, resolveTtsPrefsPath, resolveTtsProviderOrder, @@ -32,6 +32,15 @@ export const ttsHandlers: GatewayRequestHandlers = { const fallbackProviders = resolveTtsProviderOrder(provider, cfg) .slice(1) .filter((candidate) => isTtsProviderConfigured(config, candidate, cfg)); + const providerStates = listSpeechProviders(cfg).map((candidate) => ({ + id: candidate.id, + label: candidate.label, + configured: candidate.isConfigured({ + cfg, + providerConfig: getResolvedSpeechProviderConfig(config, candidate.id, cfg), + timeoutMs: config.timeoutMs, + }), + })); respond(true, { enabled: isTtsEnabled(config, prefsPath), auto: autoMode, @@ -39,9 +48,7 @@ export const ttsHandlers: GatewayRequestHandlers = { fallbackProvider: fallbackProviders[0] ?? null, fallbackProviders, prefsPath, - hasOpenAIKey: Boolean(resolveTtsApiKey(config, "openai")), - hasElevenLabsKey: Boolean(resolveTtsApiKey(config, "elevenlabs")), - microsoftEnabled: isTtsProviderConfigured(config, "microsoft", cfg), + providerStates, }); } catch (err) { respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err))); @@ -102,17 +109,18 @@ export const ttsHandlers: GatewayRequestHandlers = { } }, "tts.setProvider": async ({ params, respond }) => { - const provider = normalizeSpeechProviderId( - typeof params.provider === "string" ? params.provider.trim() : "", - ); const cfg = loadConfig(); + const provider = canonicalizeSpeechProviderId( + typeof params.provider === "string" ? params.provider.trim() : "", + cfg, + ); if (!provider || !getSpeechProvider(provider, cfg)) { respond( false, undefined, errorShape( ErrorCodes.INVALID_REQUEST, - "Invalid provider. Use a registered TTS provider id such as openai, elevenlabs, or microsoft.", + "Invalid provider. Use a registered TTS provider id.", ), ); return; @@ -135,7 +143,11 @@ export const ttsHandlers: GatewayRequestHandlers = { providers: listSpeechProviders(cfg).map((provider) => ({ id: provider.id, name: provider.label, - configured: provider.isConfigured({ cfg, config }), + configured: provider.isConfigured({ + cfg, + providerConfig: getResolvedSpeechProviderConfig(config, provider.id, cfg), + timeoutMs: config.timeoutMs, + }), models: [...(provider.models ?? [])], voices: [...(provider.voices ?? [])], })), diff --git a/src/generated/bundled-plugin-entries.generated.ts b/src/generated/bundled-plugin-entries.generated.ts index 67c003247f0..6bf331088e5 100644 --- a/src/generated/bundled-plugin-entries.generated.ts +++ b/src/generated/bundled-plugin-entries.generated.ts @@ -1,157 +1,237 @@ // Auto-generated by scripts/generate-bundled-plugin-metadata.mjs. Do not edit directly. -import acpxPlugin from "../../extensions/acpx/index.js"; -import amazonBedrockPlugin from "../../extensions/amazon-bedrock/index.js"; -import anthropicPlugin from "../../extensions/anthropic/index.js"; -import bluebubblesPlugin from "../../extensions/bluebubbles/index.js"; -import bravePlugin from "../../extensions/brave/index.js"; -import byteplusPlugin from "../../extensions/byteplus/index.js"; -import chutesPlugin from "../../extensions/chutes/index.js"; -import cloudflareAiGatewayPlugin from "../../extensions/cloudflare-ai-gateway/index.js"; -import copilotProxyPlugin from "../../extensions/copilot-proxy/index.js"; -import deepgramPlugin from "../../extensions/deepgram/index.js"; -import deepseekPlugin from "../../extensions/deepseek/index.js"; -import diagnosticsOtelPlugin from "../../extensions/diagnostics-otel/index.js"; -import diffsPlugin from "../../extensions/diffs/index.js"; -import discordPlugin from "../../extensions/discord/index.js"; -import duckduckgoPlugin from "../../extensions/duckduckgo/index.js"; -import elevenlabsPlugin from "../../extensions/elevenlabs/index.js"; -import exaPlugin from "../../extensions/exa/index.js"; -import falPlugin from "../../extensions/fal/index.js"; -import feishuPlugin from "../../extensions/feishu/index.js"; -import firecrawlPlugin from "../../extensions/firecrawl/index.js"; -import githubCopilotPlugin from "../../extensions/github-copilot/index.js"; -import googlePlugin from "../../extensions/google/index.js"; -import googlechatPlugin from "../../extensions/googlechat/index.js"; -import groqPlugin from "../../extensions/groq/index.js"; -import huggingfacePlugin from "../../extensions/huggingface/index.js"; -import imessagePlugin from "../../extensions/imessage/index.js"; -import ircPlugin from "../../extensions/irc/index.js"; -import kilocodePlugin from "../../extensions/kilocode/index.js"; -import kimiCodingPlugin from "../../extensions/kimi-coding/index.js"; -import linePlugin from "../../extensions/line/index.js"; -import llmTaskPlugin from "../../extensions/llm-task/index.js"; -import lobsterPlugin from "../../extensions/lobster/index.js"; -import matrixPlugin from "../../extensions/matrix/index.js"; -import mattermostPlugin from "../../extensions/mattermost/index.js"; -import memoryCorePlugin from "../../extensions/memory-core/index.js"; -import memoryLancedbPlugin from "../../extensions/memory-lancedb/index.js"; -import microsoftFoundryPlugin from "../../extensions/microsoft-foundry/index.js"; -import microsoftPlugin from "../../extensions/microsoft/index.js"; -import minimaxPlugin from "../../extensions/minimax/index.js"; -import mistralPlugin from "../../extensions/mistral/index.js"; -import modelstudioPlugin from "../../extensions/modelstudio/index.js"; -import moonshotPlugin from "../../extensions/moonshot/index.js"; -import msteamsPlugin from "../../extensions/msteams/index.js"; -import nextcloudTalkPlugin from "../../extensions/nextcloud-talk/index.js"; -import nostrPlugin from "../../extensions/nostr/index.js"; -import nvidiaPlugin from "../../extensions/nvidia/index.js"; -import ollamaPlugin from "../../extensions/ollama/index.js"; -import openProsePlugin from "../../extensions/open-prose/index.js"; -import openaiPlugin from "../../extensions/openai/index.js"; -import opencodeGoPlugin from "../../extensions/opencode-go/index.js"; -import opencodePlugin from "../../extensions/opencode/index.js"; -import openrouterPlugin from "../../extensions/openrouter/index.js"; -import openshellPlugin from "../../extensions/openshell/index.js"; -import perplexityPlugin from "../../extensions/perplexity/index.js"; -import qianfanPlugin from "../../extensions/qianfan/index.js"; -import sglangPlugin from "../../extensions/sglang/index.js"; -import signalPlugin from "../../extensions/signal/index.js"; -import slackPlugin from "../../extensions/slack/index.js"; -import synologyChatPlugin from "../../extensions/synology-chat/index.js"; -import syntheticPlugin from "../../extensions/synthetic/index.js"; -import tavilyPlugin from "../../extensions/tavily/index.js"; -import telegramPlugin from "../../extensions/telegram/index.js"; -import tlonPlugin from "../../extensions/tlon/index.js"; -import togetherPlugin from "../../extensions/together/index.js"; -import twitchPlugin from "../../extensions/twitch/index.js"; -import venicePlugin from "../../extensions/venice/index.js"; -import vercelAiGatewayPlugin from "../../extensions/vercel-ai-gateway/index.js"; -import vllmPlugin from "../../extensions/vllm/index.js"; -import voiceCallPlugin from "../../extensions/voice-call/index.js"; -import volcenginePlugin from "../../extensions/volcengine/index.js"; -import whatsappPlugin from "../../extensions/whatsapp/index.js"; -import xaiPlugin from "../../extensions/xai/index.js"; -import xiaomiPlugin from "../../extensions/xiaomi/index.js"; -import zaiPlugin from "../../extensions/zai/index.js"; -import zaloPlugin from "../../extensions/zalo/index.js"; -import zalouserPlugin from "../../extensions/zalouser/index.js"; - -export const GENERATED_BUNDLED_PLUGIN_ENTRIES = [ - acpxPlugin, - amazonBedrockPlugin, - anthropicPlugin, - bluebubblesPlugin, - bravePlugin, - byteplusPlugin, - chutesPlugin, - cloudflareAiGatewayPlugin, - copilotProxyPlugin, - deepgramPlugin, - deepseekPlugin, - diagnosticsOtelPlugin, - diffsPlugin, - discordPlugin, - duckduckgoPlugin, - elevenlabsPlugin, - exaPlugin, - falPlugin, - feishuPlugin, - firecrawlPlugin, - githubCopilotPlugin, - googlePlugin, - googlechatPlugin, - groqPlugin, - huggingfacePlugin, - imessagePlugin, - ircPlugin, - kilocodePlugin, - kimiCodingPlugin, - linePlugin, - llmTaskPlugin, - lobsterPlugin, - matrixPlugin, - mattermostPlugin, - memoryCorePlugin, - memoryLancedbPlugin, - microsoftPlugin, - microsoftFoundryPlugin, - minimaxPlugin, - mistralPlugin, - modelstudioPlugin, - moonshotPlugin, - msteamsPlugin, - nextcloudTalkPlugin, - nostrPlugin, - nvidiaPlugin, - ollamaPlugin, - openProsePlugin, - openaiPlugin, - opencodePlugin, - opencodeGoPlugin, - openrouterPlugin, - openshellPlugin, - perplexityPlugin, - qianfanPlugin, - sglangPlugin, - signalPlugin, - slackPlugin, - synologyChatPlugin, - syntheticPlugin, - tavilyPlugin, - telegramPlugin, - tlonPlugin, - togetherPlugin, - twitchPlugin, - venicePlugin, - vercelAiGatewayPlugin, - vllmPlugin, - voiceCallPlugin, - volcenginePlugin, - whatsappPlugin, - xaiPlugin, - xiaomiPlugin, - zaiPlugin, - zaloPlugin, - zalouserPlugin, -] as const; +export async function loadGeneratedBundledPluginEntries() { + const [ + acpxPluginModule, + amazonBedrockPluginModule, + anthropicPluginModule, + bluebubblesPluginModule, + bravePluginModule, + byteplusPluginModule, + chutesPluginModule, + cloudflareAiGatewayPluginModule, + copilotProxyPluginModule, + deepgramPluginModule, + deepseekPluginModule, + diagnosticsOtelPluginModule, + diffsPluginModule, + discordPluginModule, + duckduckgoPluginModule, + elevenlabsPluginModule, + exaPluginModule, + falPluginModule, + feishuPluginModule, + firecrawlPluginModule, + githubCopilotPluginModule, + googlePluginModule, + googlechatPluginModule, + groqPluginModule, + huggingfacePluginModule, + imessagePluginModule, + ircPluginModule, + kilocodePluginModule, + kimiCodingPluginModule, + linePluginModule, + llmTaskPluginModule, + lobsterPluginModule, + matrixPluginModule, + mattermostPluginModule, + memoryCorePluginModule, + memoryLancedbPluginModule, + microsoftPluginModule, + microsoftFoundryPluginModule, + minimaxPluginModule, + mistralPluginModule, + modelstudioPluginModule, + moonshotPluginModule, + msteamsPluginModule, + nextcloudTalkPluginModule, + nostrPluginModule, + nvidiaPluginModule, + ollamaPluginModule, + openProsePluginModule, + openaiPluginModule, + opencodePluginModule, + opencodeGoPluginModule, + openrouterPluginModule, + openshellPluginModule, + perplexityPluginModule, + qianfanPluginModule, + sglangPluginModule, + signalPluginModule, + slackPluginModule, + synologyChatPluginModule, + syntheticPluginModule, + tavilyPluginModule, + telegramPluginModule, + tlonPluginModule, + togetherPluginModule, + twitchPluginModule, + venicePluginModule, + vercelAiGatewayPluginModule, + vllmPluginModule, + voiceCallPluginModule, + volcenginePluginModule, + whatsappPluginModule, + xaiPluginModule, + xiaomiPluginModule, + zaiPluginModule, + zaloPluginModule, + zalouserPluginModule, + ] = await Promise.all([ + import("../../extensions/acpx/index.js"), + import("../../extensions/amazon-bedrock/index.js"), + import("../../extensions/anthropic/index.js"), + import("../../extensions/bluebubbles/index.js"), + import("../../extensions/brave/index.js"), + import("../../extensions/byteplus/index.js"), + import("../../extensions/chutes/index.js"), + import("../../extensions/cloudflare-ai-gateway/index.js"), + import("../../extensions/copilot-proxy/index.js"), + import("../../extensions/deepgram/index.js"), + import("../../extensions/deepseek/index.js"), + import("../../extensions/diagnostics-otel/index.js"), + import("../../extensions/diffs/index.js"), + import("../../extensions/discord/index.js"), + import("../../extensions/duckduckgo/index.js"), + import("../../extensions/elevenlabs/index.js"), + import("../../extensions/exa/index.js"), + import("../../extensions/fal/index.js"), + import("../../extensions/feishu/index.js"), + import("../../extensions/firecrawl/index.js"), + import("../../extensions/github-copilot/index.js"), + import("../../extensions/google/index.js"), + import("../../extensions/googlechat/index.js"), + import("../../extensions/groq/index.js"), + import("../../extensions/huggingface/index.js"), + import("../../extensions/imessage/index.js"), + import("../../extensions/irc/index.js"), + import("../../extensions/kilocode/index.js"), + import("../../extensions/kimi-coding/index.js"), + import("../../extensions/line/index.js"), + import("../../extensions/llm-task/index.js"), + import("../../extensions/lobster/index.js"), + import("../../extensions/matrix/index.js"), + import("../../extensions/mattermost/index.js"), + import("../../extensions/memory-core/index.js"), + import("../../extensions/memory-lancedb/index.js"), + import("../../extensions/microsoft/index.js"), + import("../../extensions/microsoft-foundry/index.js"), + import("../../extensions/minimax/index.js"), + import("../../extensions/mistral/index.js"), + import("../../extensions/modelstudio/index.js"), + import("../../extensions/moonshot/index.js"), + import("../../extensions/msteams/index.js"), + import("../../extensions/nextcloud-talk/index.js"), + import("../../extensions/nostr/index.js"), + import("../../extensions/nvidia/index.js"), + import("../../extensions/ollama/index.js"), + import("../../extensions/open-prose/index.js"), + import("../../extensions/openai/index.js"), + import("../../extensions/opencode/index.js"), + import("../../extensions/opencode-go/index.js"), + import("../../extensions/openrouter/index.js"), + import("../../extensions/openshell/index.js"), + import("../../extensions/perplexity/index.js"), + import("../../extensions/qianfan/index.js"), + import("../../extensions/sglang/index.js"), + import("../../extensions/signal/index.js"), + import("../../extensions/slack/index.js"), + import("../../extensions/synology-chat/index.js"), + import("../../extensions/synthetic/index.js"), + import("../../extensions/tavily/index.js"), + import("../../extensions/telegram/index.js"), + import("../../extensions/tlon/index.js"), + import("../../extensions/together/index.js"), + import("../../extensions/twitch/index.js"), + import("../../extensions/venice/index.js"), + import("../../extensions/vercel-ai-gateway/index.js"), + import("../../extensions/vllm/index.js"), + import("../../extensions/voice-call/index.js"), + import("../../extensions/volcengine/index.js"), + import("../../extensions/whatsapp/index.js"), + import("../../extensions/xai/index.js"), + import("../../extensions/xiaomi/index.js"), + import("../../extensions/zai/index.js"), + import("../../extensions/zalo/index.js"), + import("../../extensions/zalouser/index.js"), + ]); + return [ + acpxPluginModule.default, + amazonBedrockPluginModule.default, + anthropicPluginModule.default, + bluebubblesPluginModule.default, + bravePluginModule.default, + byteplusPluginModule.default, + chutesPluginModule.default, + cloudflareAiGatewayPluginModule.default, + copilotProxyPluginModule.default, + deepgramPluginModule.default, + deepseekPluginModule.default, + diagnosticsOtelPluginModule.default, + diffsPluginModule.default, + discordPluginModule.default, + duckduckgoPluginModule.default, + elevenlabsPluginModule.default, + exaPluginModule.default, + falPluginModule.default, + feishuPluginModule.default, + firecrawlPluginModule.default, + githubCopilotPluginModule.default, + googlePluginModule.default, + googlechatPluginModule.default, + groqPluginModule.default, + huggingfacePluginModule.default, + imessagePluginModule.default, + ircPluginModule.default, + kilocodePluginModule.default, + kimiCodingPluginModule.default, + linePluginModule.default, + llmTaskPluginModule.default, + lobsterPluginModule.default, + matrixPluginModule.default, + mattermostPluginModule.default, + memoryCorePluginModule.default, + memoryLancedbPluginModule.default, + microsoftPluginModule.default, + microsoftFoundryPluginModule.default, + minimaxPluginModule.default, + mistralPluginModule.default, + modelstudioPluginModule.default, + moonshotPluginModule.default, + msteamsPluginModule.default, + nextcloudTalkPluginModule.default, + nostrPluginModule.default, + nvidiaPluginModule.default, + ollamaPluginModule.default, + openProsePluginModule.default, + openaiPluginModule.default, + opencodePluginModule.default, + opencodeGoPluginModule.default, + openrouterPluginModule.default, + openshellPluginModule.default, + perplexityPluginModule.default, + qianfanPluginModule.default, + sglangPluginModule.default, + signalPluginModule.default, + slackPluginModule.default, + synologyChatPluginModule.default, + syntheticPluginModule.default, + tavilyPluginModule.default, + telegramPluginModule.default, + tlonPluginModule.default, + togetherPluginModule.default, + twitchPluginModule.default, + venicePluginModule.default, + vercelAiGatewayPluginModule.default, + vllmPluginModule.default, + voiceCallPluginModule.default, + volcenginePluginModule.default, + whatsappPluginModule.default, + xaiPluginModule.default, + xiaomiPluginModule.default, + zaiPluginModule.default, + zaloPluginModule.default, + zalouserPluginModule.default, + ] as const; +} diff --git a/src/plugin-sdk/speech-core.ts b/src/plugin-sdk/speech-core.ts index 46274a62931..8db7a983fcb 100644 --- a/src/plugin-sdk/speech-core.ts +++ b/src/plugin-sdk/speech-core.ts @@ -1,12 +1,28 @@ // Shared speech-provider implementation helpers for bundled and third-party plugins. export type { SpeechProviderPlugin } from "../plugins/types.js"; -export type { SpeechVoiceOption } from "../tts/provider-types.js"; +export type { + SpeechDirectiveTokenParseContext, + SpeechDirectiveTokenParseResult, + SpeechListVoicesRequest, + SpeechModelOverridePolicy, + SpeechProviderConfig, + SpeechProviderConfiguredContext, + SpeechProviderResolveConfigContext, + SpeechProviderResolveTalkConfigContext, + SpeechProviderResolveTalkOverridesContext, + SpeechProviderOverrides, + SpeechSynthesisRequest, + SpeechTelephonySynthesisRequest, + SpeechVoiceOption, + TtsDirectiveOverrides, + TtsDirectiveParseResult, +} from "../tts/provider-types.js"; export { normalizeApplyTextNormalization, normalizeLanguageCode, normalizeSeed, - parseTtsDirectives, requireInRange, } from "../tts/tts-core.js"; +export { parseTtsDirectives } from "../tts/directives.js"; diff --git a/src/plugin-sdk/speech.ts b/src/plugin-sdk/speech.ts index a98e98103e0..9033a70af8f 100644 --- a/src/plugin-sdk/speech.ts +++ b/src/plugin-sdk/speech.ts @@ -1,4 +1,9 @@ // Public speech helpers for bundled or third-party plugins. -export { parseTtsDirectives } from "../tts/tts-core.js"; -export type { SpeechVoiceOption } from "../tts/provider-types.js"; +export { parseTtsDirectives } from "../tts/directives.js"; +export type { + SpeechModelOverridePolicy, + SpeechVoiceOption, + TtsDirectiveOverrides, + TtsDirectiveParseResult, +} from "../tts/provider-types.js"; diff --git a/src/plugin-sdk/voice-call.ts b/src/plugin-sdk/voice-call.ts index 2df9528111f..2d8c3a6e493 100644 --- a/src/plugin-sdk/voice-call.ts +++ b/src/plugin-sdk/voice-call.ts @@ -8,7 +8,6 @@ export { TtsModeSchema, TtsProviderSchema, } from "../config/zod-schema.core.js"; -export { resolveOpenAITtsInstructions } from "../tts/tts-core.js"; export type { GatewayRequestHandlerOptions } from "../gateway/server-methods/types.js"; export { isRequestBodyLimitError, diff --git a/src/plugins/bundled-plugin-entries.ts b/src/plugins/bundled-plugin-entries.ts index 31f1d1a8e75..771a960ee20 100644 --- a/src/plugins/bundled-plugin-entries.ts +++ b/src/plugins/bundled-plugin-entries.ts @@ -1,4 +1,4 @@ -import { GENERATED_BUNDLED_PLUGIN_ENTRIES } from "../generated/bundled-plugin-entries.generated.js"; +import { loadGeneratedBundledPluginEntries } from "../generated/bundled-plugin-entries.generated.js"; import type { OpenClawPluginDefinition } from "./types.js"; type BundledRegistrablePlugin = OpenClawPluginDefinition & { @@ -7,4 +7,4 @@ type BundledRegistrablePlugin = OpenClawPluginDefinition & { }; export const BUNDLED_PLUGIN_ENTRIES = - GENERATED_BUNDLED_PLUGIN_ENTRIES as unknown as readonly BundledRegistrablePlugin[]; + (await loadGeneratedBundledPluginEntries()) as unknown as readonly BundledRegistrablePlugin[]; diff --git a/src/plugins/bundled-plugin-metadata.generated.ts b/src/plugins/bundled-plugin-metadata.generated.ts index ea68157c475..c18ccbcbe6e 100644 --- a/src/plugins/bundled-plugin-metadata.generated.ts +++ b/src/plugins/bundled-plugin-metadata.generated.ts @@ -249,7 +249,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ }, { dirName: "brave", - idHint: "brave-plugin", + idHint: "brave", source: { source: "./index.ts", built: "index.js", @@ -820,7 +820,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ }, { dirName: "duckduckgo", - idHint: "duckduckgo-plugin", + idHint: "duckduckgo", source: { source: "./index.ts", built: "index.js", @@ -889,7 +889,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ }, { dirName: "exa", - idHint: "exa-plugin", + idHint: "exa", source: { source: "./index.ts", built: "index.js", @@ -1021,7 +1021,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ }, { dirName: "firecrawl", - idHint: "firecrawl-plugin", + idHint: "firecrawl", source: { source: "./index.ts", built: "index.js", @@ -1109,7 +1109,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ }, { dirName: "google", - idHint: "google-plugin", + idHint: "google", source: { source: "./index.ts", built: "index.js", @@ -2556,7 +2556,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ }, { dirName: "openshell", - idHint: "openshell-sandbox", + idHint: "openshell", source: { source: "./index.ts", built: "index.js", @@ -2670,7 +2670,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ }, { dirName: "perplexity", - idHint: "perplexity-plugin", + idHint: "perplexity", source: { source: "./index.ts", built: "index.js", @@ -2966,7 +2966,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ }, { dirName: "tavily", - idHint: "tavily-plugin", + idHint: "tavily", source: { source: "./index.ts", built: "index.js", @@ -4007,7 +4007,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ }, { dirName: "xai", - idHint: "xai-plugin", + idHint: "xai", source: { source: "./index.ts", built: "index.js", diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 7ef2a8053f6..27d362f9412 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -32,7 +32,13 @@ import type { MediaUnderstandingProvider } from "../media-understanding/types.js import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeWebSearchMetadata } from "../secrets/runtime-web-tools.types.js"; import type { + SpeechDirectiveTokenParseContext, + SpeechDirectiveTokenParseResult, SpeechProviderConfiguredContext, + SpeechProviderConfig, + SpeechProviderResolveConfigContext, + SpeechProviderResolveTalkConfigContext, + SpeechProviderResolveTalkOverridesContext, SpeechListVoicesRequest, SpeechProviderId, SpeechSynthesisRequest, @@ -939,8 +945,15 @@ export type SpeechProviderPlugin = { id: SpeechProviderId; label: string; aliases?: string[]; + autoSelectOrder?: number; models?: readonly string[]; voices?: readonly string[]; + resolveConfig?: (ctx: SpeechProviderResolveConfigContext) => SpeechProviderConfig; + parseDirectiveToken?: (ctx: SpeechDirectiveTokenParseContext) => SpeechDirectiveTokenParseResult; + resolveTalkConfig?: (ctx: SpeechProviderResolveTalkConfigContext) => SpeechProviderConfig; + resolveTalkOverrides?: ( + ctx: SpeechProviderResolveTalkOverridesContext, + ) => SpeechProviderConfig | undefined; isConfigured: (ctx: SpeechProviderConfiguredContext) => boolean; synthesize: (req: SpeechSynthesisRequest) => Promise; synthesizeTelephony?: ( diff --git a/src/tts/directives.ts b/src/tts/directives.ts new file mode 100644 index 00000000000..df993ba1bed --- /dev/null +++ b/src/tts/directives.ts @@ -0,0 +1,160 @@ +import type { OpenClawConfig } from "../config/config.js"; +import type { SpeechProviderPlugin } from "../plugins/types.js"; +import { listSpeechProviders } from "./provider-registry.js"; +import type { + SpeechModelOverridePolicy, + SpeechProviderConfig, + SpeechProviderOverrides, + TtsDirectiveOverrides, + TtsDirectiveParseResult, +} from "./provider-types.js"; + +type ParseTtsDirectiveOptions = { + cfg?: OpenClawConfig; + providers?: readonly SpeechProviderPlugin[]; + providerConfigs?: Record; + openaiBaseUrl?: string; +}; + +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); +} + +function resolveDirectiveProviderConfig( + provider: SpeechProviderPlugin, + options?: ParseTtsDirectiveOptions, +): SpeechProviderConfig | undefined { + const explicit = options?.providerConfigs?.[provider.id]; + if (explicit) { + return explicit; + } + if (provider.id === "openai" && options?.openaiBaseUrl) { + return { baseUrl: options.openaiBaseUrl }; + } + return undefined; +} + +function mergeProviderOverrides( + target: TtsDirectiveOverrides, + providerId: string, + next: SpeechProviderOverrides, +): void { + target.providerOverrides = { + ...target.providerOverrides, + [providerId]: { + ...target.providerOverrides?.[providerId], + ...next, + }, + }; +} + +function resolveLegacyOptions( + optionsOrOpenaiBaseUrl?: ParseTtsDirectiveOptions | string, +): ParseTtsDirectiveOptions | undefined { + if (typeof optionsOrOpenaiBaseUrl === "string") { + return { openaiBaseUrl: optionsOrOpenaiBaseUrl }; + } + return optionsOrOpenaiBaseUrl; +} + +export function parseTtsDirectives( + text: string, + policy: SpeechModelOverridePolicy, + optionsOrOpenaiBaseUrl?: ParseTtsDirectiveOptions | string, +): TtsDirectiveParseResult { + if (!policy.enabled) { + return { cleanedText: text, overrides: {}, warnings: [], hasDirective: false }; + } + + const options = resolveLegacyOptions(optionsOrOpenaiBaseUrl); + const providers = resolveDirectiveProviders(options); + const overrides: TtsDirectiveOverrides = {}; + const warnings: string[] = []; + let cleanedText = text; + let hasDirective = false; + + const blockRegex = /\[\[tts:text\]\]([\s\S]*?)\[\[\/tts:text\]\]/gi; + cleanedText = cleanedText.replace(blockRegex, (_match, inner: string) => { + hasDirective = true; + if (policy.allowText && overrides.ttsText == null) { + overrides.ttsText = inner.trim(); + } + return ""; + }); + + const directiveRegex = /\[\[tts:([^\]]+)\]\]/gi; + cleanedText = cleanedText.replace(directiveRegex, (_match, body: string) => { + hasDirective = true; + const tokens = body.split(/\s+/).filter(Boolean); + for (const token of tokens) { + const eqIndex = token.indexOf("="); + if (eqIndex === -1) { + continue; + } + const rawKey = token.slice(0, eqIndex).trim(); + const rawValue = token.slice(eqIndex + 1).trim(); + if (!rawKey || !rawValue) { + continue; + } + const key = rawKey.toLowerCase(); + if (key === "provider") { + if (policy.allowProvider) { + const providerId = rawValue.trim().toLowerCase(); + if (providerId) { + overrides.provider = providerId; + } else { + warnings.push("invalid provider id"); + } + } + continue; + } + + let handled = false; + for (const provider of providers) { + const parsed = provider.parseDirectiveToken?.({ + key, + value: rawValue, + policy, + providerConfig: resolveDirectiveProviderConfig(provider, options), + currentOverrides: overrides.providerOverrides?.[provider.id], + }); + if (!parsed?.handled) { + continue; + } + handled = true; + if (parsed.overrides) { + mergeProviderOverrides(overrides, provider.id, parsed.overrides); + } + if (parsed.warnings?.length) { + warnings.push(...parsed.warnings); + } + break; + } + + if (!handled) { + continue; + } + } + return ""; + }); + + return { + cleanedText, + ttsText: overrides.ttsText, + hasDirective, + overrides, + warnings, + }; +} diff --git a/src/tts/provider-registry.test.ts b/src/tts/provider-registry.test.ts index 5a084466e55..55b5e9cf28a 100644 --- a/src/tts/provider-registry.test.ts +++ b/src/tts/provider-registry.test.ts @@ -13,6 +13,7 @@ vi.mock("../plugins/loader.js", () => ({ let getSpeechProvider: typeof import("./provider-registry.js").getSpeechProvider; let listSpeechProviders: typeof import("./provider-registry.js").listSpeechProviders; +let canonicalizeSpeechProviderId: typeof import("./provider-registry.js").canonicalizeSpeechProviderId; let normalizeSpeechProviderId: typeof import("./provider-registry.js").normalizeSpeechProviderId; function createSpeechProvider(id: string, aliases?: string[]): SpeechProviderPlugin { @@ -36,8 +37,12 @@ describe("speech provider registry", () => { resetPluginRuntimeStateForTest(); loadOpenClawPluginsMock.mockReset(); loadOpenClawPluginsMock.mockReturnValue(createEmptyPluginRegistry()); - ({ getSpeechProvider, listSpeechProviders, normalizeSpeechProviderId } = - await import("./provider-registry.js")); + ({ + getSpeechProvider, + listSpeechProviders, + canonicalizeSpeechProviderId, + normalizeSpeechProviderId, + } = await import("./provider-registry.js")); }); afterEach(() => { @@ -86,7 +91,19 @@ describe("speech provider registry", () => { expect(getSpeechProvider("openai")).toBeUndefined(); }); - it("normalizes the legacy edge alias to microsoft", () => { - expect(normalizeSpeechProviderId("edge")).toBe("microsoft"); + it("canonicalizes the legacy edge alias to microsoft", () => { + setActivePluginRegistry({ + ...createEmptyPluginRegistry(), + speechProviders: [ + { + pluginId: "test-microsoft", + source: "test", + provider: createSpeechProvider("microsoft", ["edge"]), + }, + ], + }); + + expect(normalizeSpeechProviderId("edge")).toBe("edge"); + expect(canonicalizeSpeechProviderId("edge")).toBe("microsoft"); }); }); diff --git a/src/tts/provider-registry.ts b/src/tts/provider-registry.ts index 5fc6485e066..f782e9a9b86 100644 --- a/src/tts/provider-registry.ts +++ b/src/tts/provider-registry.ts @@ -12,11 +12,7 @@ function trimToUndefined(value: string | undefined): string | undefined { export function normalizeSpeechProviderId( providerId: string | undefined, ): SpeechProviderId | undefined { - const normalized = trimToUndefined(providerId); - if (!normalized) { - return undefined; - } - return normalized === "edge" ? "microsoft" : normalized; + return trimToUndefined(providerId); } function resolveSpeechProviderPluginEntries(cfg?: OpenClawConfig): SpeechProviderPlugin[] { @@ -70,3 +66,14 @@ export function getSpeechProvider( } return buildProviderMaps(cfg).aliases.get(normalized); } + +export function canonicalizeSpeechProviderId( + providerId: string | undefined, + cfg?: OpenClawConfig, +): SpeechProviderId | undefined { + const normalized = normalizeSpeechProviderId(providerId); + if (!normalized) { + return undefined; + } + return getSpeechProvider(normalized, cfg)?.id ?? normalized; +} diff --git a/src/tts/provider-types.ts b/src/tts/provider-types.ts index c0640b63614..3c3c916c3bb 100644 --- a/src/tts/provider-types.ts +++ b/src/tts/provider-types.ts @@ -1,21 +1,52 @@ import type { OpenClawConfig } from "../config/config.js"; -import type { ResolvedTtsConfig, TtsDirectiveOverrides } from "./tts.js"; +import type { TalkProviderConfig } from "../config/types.gateway.js"; export type SpeechProviderId = string; export type SpeechSynthesisTarget = "audio-file" | "voice-note"; +export type SpeechProviderConfig = Record; + +export type SpeechProviderOverrides = Record; + +export type SpeechModelOverridePolicy = { + enabled: boolean; + allowText: boolean; + allowProvider: boolean; + allowVoice: boolean; + allowModelId: boolean; + allowVoiceSettings: boolean; + allowNormalization: boolean; + allowSeed: boolean; +}; + +export type TtsDirectiveOverrides = { + ttsText?: string; + provider?: SpeechProviderId; + providerOverrides?: Record; +}; + +export type TtsDirectiveParseResult = { + cleanedText: string; + ttsText?: string; + hasDirective: boolean; + overrides: TtsDirectiveOverrides; + warnings: string[]; +}; + export type SpeechProviderConfiguredContext = { cfg?: OpenClawConfig; - config: ResolvedTtsConfig; + providerConfig: SpeechProviderConfig; + timeoutMs: number; }; export type SpeechSynthesisRequest = { text: string; cfg: OpenClawConfig; - config: ResolvedTtsConfig; + providerConfig: SpeechProviderConfig; target: SpeechSynthesisTarget; - overrides?: TtsDirectiveOverrides; + providerOverrides?: SpeechProviderOverrides; + timeoutMs: number; }; export type SpeechSynthesisResult = { @@ -28,7 +59,8 @@ export type SpeechSynthesisResult = { export type SpeechTelephonySynthesisRequest = { text: string; cfg: OpenClawConfig; - config: ResolvedTtsConfig; + providerConfig: SpeechProviderConfig; + timeoutMs: number; }; export type SpeechTelephonySynthesisResult = { @@ -49,7 +81,39 @@ export type SpeechVoiceOption = { export type SpeechListVoicesRequest = { cfg?: OpenClawConfig; - config?: ResolvedTtsConfig; + providerConfig?: SpeechProviderConfig; apiKey?: string; baseUrl?: string; }; + +export type SpeechProviderResolveConfigContext = { + cfg: OpenClawConfig; + rawConfig: Record; + timeoutMs: number; +}; + +export type SpeechDirectiveTokenParseContext = { + key: string; + value: string; + policy: SpeechModelOverridePolicy; + providerConfig?: SpeechProviderConfig; + currentOverrides?: SpeechProviderOverrides; +}; + +export type SpeechDirectiveTokenParseResult = { + handled: boolean; + overrides?: SpeechProviderOverrides; + warnings?: string[]; +}; + +export type SpeechProviderResolveTalkConfigContext = { + cfg: OpenClawConfig; + baseTtsConfig: Record; + talkProviderConfig: TalkProviderConfig; + timeoutMs: number; +}; + +export type SpeechProviderResolveTalkOverridesContext = { + talkProviderConfig: TalkProviderConfig; + params: Record; +}; diff --git a/src/tts/tts-core.ts b/src/tts/tts-core.ts index 8f7cc4d8170..1176f593bc7 100644 --- a/src/tts/tts-core.ts +++ b/src/tts/tts-core.ts @@ -10,33 +10,10 @@ import { import { resolveModelAsync } from "../agents/pi-embedded-runner/model.js"; import { prepareModelForSimpleCompletion } from "../agents/simple-completion-transport.js"; import type { OpenClawConfig } from "../config/config.js"; -import type { - ResolvedTtsConfig, - ResolvedTtsModelOverrides, - TtsDirectiveOverrides, - TtsDirectiveParseResult, -} from "./tts.js"; +import type { ResolvedTtsConfig } from "./tts.js"; -export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"; const TEMP_FILE_CLEANUP_DELAY_MS = 5 * 60 * 1000; // 5 minutes -export function isValidVoiceId(voiceId: string): boolean { - return /^[a-zA-Z0-9]{10,40}$/.test(voiceId); -} - -function normalizeOpenAITtsBaseUrl(baseUrl?: string): string { - const trimmed = baseUrl?.trim(); - if (!trimmed) { - return DEFAULT_OPENAI_BASE_URL; - } - return trimmed.replace(/\/+$/, ""); -} - -function trimToUndefined(value?: string): string | undefined { - const trimmed = value?.trim(); - return trimmed ? trimmed : undefined; -} - export function requireInRange(value: number, min: number, max: number, label: string): void { if (!Number.isFinite(value) || value < min || value > max) { throw new Error(`${label} must be between ${min} and ${max}`); @@ -78,317 +55,6 @@ export function normalizeSeed(seed?: number): number | undefined { return next; } -function parseBooleanValue(value: string): boolean | undefined { - const normalized = value.trim().toLowerCase(); - if (["true", "1", "yes", "on"].includes(normalized)) { - return true; - } - if (["false", "0", "no", "off"].includes(normalized)) { - return false; - } - return undefined; -} - -function parseNumberValue(value: string): number | undefined { - const parsed = Number.parseFloat(value); - return Number.isFinite(parsed) ? parsed : undefined; -} - -export function parseTtsDirectives( - text: string, - policy: ResolvedTtsModelOverrides, - openaiBaseUrl?: string, -): TtsDirectiveParseResult { - if (!policy.enabled) { - return { cleanedText: text, overrides: {}, warnings: [], hasDirective: false }; - } - - const overrides: TtsDirectiveOverrides = {}; - const warnings: string[] = []; - let cleanedText = text; - let hasDirective = false; - - const blockRegex = /\[\[tts:text\]\]([\s\S]*?)\[\[\/tts:text\]\]/gi; - cleanedText = cleanedText.replace(blockRegex, (_match, inner: string) => { - hasDirective = true; - if (policy.allowText && overrides.ttsText == null) { - overrides.ttsText = inner.trim(); - } - return ""; - }); - - const directiveRegex = /\[\[tts:([^\]]+)\]\]/gi; - cleanedText = cleanedText.replace(directiveRegex, (_match, body: string) => { - hasDirective = true; - const tokens = body.split(/\s+/).filter(Boolean); - for (const token of tokens) { - const eqIndex = token.indexOf("="); - if (eqIndex === -1) { - continue; - } - const rawKey = token.slice(0, eqIndex).trim(); - const rawValue = token.slice(eqIndex + 1).trim(); - if (!rawKey || !rawValue) { - continue; - } - const key = rawKey.toLowerCase(); - try { - switch (key) { - case "provider": - if (!policy.allowProvider) { - break; - } - { - const providerId = rawValue.trim().toLowerCase(); - if (providerId) { - overrides.provider = providerId; - } else { - warnings.push("invalid provider id"); - } - } - break; - case "voice": - case "openai_voice": - case "openaivoice": - if (!policy.allowVoice) { - break; - } - if (isValidOpenAIVoice(rawValue, openaiBaseUrl)) { - overrides.openai = { ...overrides.openai, voice: rawValue }; - } else { - warnings.push(`invalid OpenAI voice "${rawValue}"`); - } - break; - case "voiceid": - case "voice_id": - case "elevenlabs_voice": - case "elevenlabsvoice": - if (!policy.allowVoice) { - break; - } - if (isValidVoiceId(rawValue)) { - overrides.elevenlabs = { ...overrides.elevenlabs, voiceId: rawValue }; - } else { - warnings.push(`invalid ElevenLabs voiceId "${rawValue}"`); - } - break; - case "model": - case "modelid": - case "model_id": - case "elevenlabs_model": - case "elevenlabsmodel": - case "openai_model": - case "openaimodel": - if (!policy.allowModelId) { - break; - } - if (isValidOpenAIModel(rawValue, openaiBaseUrl)) { - overrides.openai = { ...overrides.openai, model: rawValue }; - } else { - overrides.elevenlabs = { ...overrides.elevenlabs, modelId: rawValue }; - } - break; - case "stability": - if (!policy.allowVoiceSettings) { - break; - } - { - const value = parseNumberValue(rawValue); - if (value == null) { - warnings.push("invalid stability value"); - break; - } - requireInRange(value, 0, 1, "stability"); - overrides.elevenlabs = { - ...overrides.elevenlabs, - voiceSettings: { ...overrides.elevenlabs?.voiceSettings, stability: value }, - }; - } - break; - case "similarity": - case "similarityboost": - case "similarity_boost": - if (!policy.allowVoiceSettings) { - break; - } - { - const value = parseNumberValue(rawValue); - if (value == null) { - warnings.push("invalid similarityBoost value"); - break; - } - requireInRange(value, 0, 1, "similarityBoost"); - overrides.elevenlabs = { - ...overrides.elevenlabs, - voiceSettings: { ...overrides.elevenlabs?.voiceSettings, similarityBoost: value }, - }; - } - break; - case "style": - if (!policy.allowVoiceSettings) { - break; - } - { - const value = parseNumberValue(rawValue); - if (value == null) { - warnings.push("invalid style value"); - break; - } - requireInRange(value, 0, 1, "style"); - overrides.elevenlabs = { - ...overrides.elevenlabs, - voiceSettings: { ...overrides.elevenlabs?.voiceSettings, style: value }, - }; - } - break; - case "speed": - if (!policy.allowVoiceSettings) { - break; - } - { - const value = parseNumberValue(rawValue); - if (value == null) { - warnings.push("invalid speed value"); - break; - } - requireInRange(value, 0.5, 2, "speed"); - overrides.elevenlabs = { - ...overrides.elevenlabs, - voiceSettings: { ...overrides.elevenlabs?.voiceSettings, speed: value }, - }; - } - break; - case "speakerboost": - case "speaker_boost": - case "usespeakerboost": - case "use_speaker_boost": - if (!policy.allowVoiceSettings) { - break; - } - { - const value = parseBooleanValue(rawValue); - if (value == null) { - warnings.push("invalid useSpeakerBoost value"); - break; - } - overrides.elevenlabs = { - ...overrides.elevenlabs, - voiceSettings: { ...overrides.elevenlabs?.voiceSettings, useSpeakerBoost: value }, - }; - } - break; - case "normalize": - case "applytextnormalization": - case "apply_text_normalization": - if (!policy.allowNormalization) { - break; - } - overrides.elevenlabs = { - ...overrides.elevenlabs, - applyTextNormalization: normalizeApplyTextNormalization(rawValue), - }; - break; - case "language": - case "languagecode": - case "language_code": - if (!policy.allowNormalization) { - break; - } - overrides.elevenlabs = { - ...overrides.elevenlabs, - languageCode: normalizeLanguageCode(rawValue), - }; - break; - case "seed": - if (!policy.allowSeed) { - break; - } - overrides.elevenlabs = { - ...overrides.elevenlabs, - seed: normalizeSeed(Number.parseInt(rawValue, 10)), - }; - break; - default: - break; - } - } catch (err) { - warnings.push((err as Error).message); - } - } - return ""; - }); - - return { - cleanedText, - ttsText: overrides.ttsText, - hasDirective, - overrides, - warnings, - }; -} - -export const OPENAI_TTS_MODELS = ["gpt-4o-mini-tts", "tts-1", "tts-1-hd"] as const; - -/** - * Custom OpenAI-compatible TTS endpoint. - * When set, model/voice validation is relaxed to allow non-OpenAI models. - * Example: OPENAI_TTS_BASE_URL=http://localhost:8880/v1 - * - * Note: Read at runtime (not module load) to support config.env loading. - */ -function getOpenAITtsBaseUrl(): string { - return normalizeOpenAITtsBaseUrl(process.env.OPENAI_TTS_BASE_URL); -} - -function isCustomOpenAIEndpoint(baseUrl?: string): boolean { - if (baseUrl != null) { - return normalizeOpenAITtsBaseUrl(baseUrl) !== DEFAULT_OPENAI_BASE_URL; - } - return getOpenAITtsBaseUrl() !== DEFAULT_OPENAI_BASE_URL; -} -export const OPENAI_TTS_VOICES = [ - "alloy", - "ash", - "ballad", - "cedar", - "coral", - "echo", - "fable", - "juniper", - "marin", - "onyx", - "nova", - "sage", - "shimmer", - "verse", -] as const; - -type OpenAiTtsVoice = (typeof OPENAI_TTS_VOICES)[number]; - -export function isValidOpenAIModel(model: string, baseUrl?: string): boolean { - // Allow any model when using custom endpoint (e.g., Kokoro, LocalAI) - if (isCustomOpenAIEndpoint(baseUrl)) { - return true; - } - return OPENAI_TTS_MODELS.includes(model as (typeof OPENAI_TTS_MODELS)[number]); -} - -export function resolveOpenAITtsInstructions( - model: string, - instructions?: string, -): string | undefined { - const next = trimToUndefined(instructions); - return next && model.includes("gpt-4o-mini-tts") ? next : undefined; -} - -export function isValidOpenAIVoice(voice: string, baseUrl?: string): voice is OpenAiTtsVoice { - // Allow any voice when using custom endpoint (e.g., Kokoro Chinese voices) - if (isCustomOpenAIEndpoint(baseUrl)) { - return true; - } - return OPENAI_TTS_VOICES.includes(voice as OpenAiTtsVoice); -} - type SummarizeResult = { summary: string; latencyMs: number; diff --git a/src/tts/tts.test.ts b/src/tts/tts.test.ts index cf91e1b66a2..d78e9494bca 100644 --- a/src/tts/tts.test.ts +++ b/src/tts/tts.test.ts @@ -1,8 +1,18 @@ import type { AssistantMessage } from "@mariozechner/pi-ai"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { buildElevenLabsSpeechProvider } from "../../extensions/elevenlabs/speech-provider.ts"; +import { + buildElevenLabsSpeechProvider, + isValidVoiceId, +} from "../../extensions/elevenlabs/speech-provider.ts"; import { buildMicrosoftSpeechProvider } from "../../extensions/microsoft/speech-provider.ts"; import { buildOpenAISpeechProvider } from "../../extensions/openai/speech-provider.ts"; +import { + isValidOpenAIModel, + isValidOpenAIVoice, + OPENAI_TTS_MODELS, + OPENAI_TTS_VOICES, + resolveOpenAITtsInstructions, +} from "../../extensions/openai/tts.ts"; import type { OpenClawConfig } from "../config/config.js"; import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; @@ -73,17 +83,11 @@ vi.mock("../agents/custom-api-registry.js", () => ({ const { _test, resolveTtsConfig, maybeApplyTtsToPayload, getTtsProvider } = tts; const { - isValidVoiceId, - isValidOpenAIVoice, - isValidOpenAIModel, - OPENAI_TTS_MODELS, - OPENAI_TTS_VOICES, parseTtsDirectives, - resolveOpenAITtsInstructions, resolveModelOverridePolicy, summarizeText, resolveOutputFormat, - resolveEdgeOutputFormat, + getResolvedSpeechProviderConfig, } = _test; const mockAssistantMessage = (content: AssistantMessage["content"]): AssistantMessage => ({ @@ -317,7 +321,10 @@ describe("tts", () => { ] as const; for (const testCase of cases) { const config = resolveTtsConfig(testCase.cfg); - expect(resolveEdgeOutputFormat(config), testCase.name).toBe(testCase.expected); + const providerConfig = getResolvedSpeechProviderConfig(config, "microsoft") as { + outputFormat?: string; + }; + expect(providerConfig.outputFormat, testCase.name).toBe(testCase.expected); } }); }); @@ -329,13 +336,19 @@ describe("tts", () => { "Hello [[tts:provider=elevenlabs voiceId=pMsXgVXv3BLzUgSXRplE stability=0.4 speed=1.1]] world\n\n" + "[[tts:text]](laughs) Read the song once more.[[/tts:text]]"; const result = parseTtsDirectives(input, policy); + const elevenlabsOverrides = result.overrides.providerOverrides?.elevenlabs as + | { + voiceId?: string; + voiceSettings?: { stability?: number; speed?: number }; + } + | undefined; expect(result.cleanedText).not.toContain("[[tts:"); expect(result.ttsText).toBe("(laughs) Read the song once more."); expect(result.overrides.provider).toBe("elevenlabs"); - expect(result.overrides.elevenlabs?.voiceId).toBe("pMsXgVXv3BLzUgSXRplE"); - expect(result.overrides.elevenlabs?.voiceSettings?.stability).toBe(0.4); - expect(result.overrides.elevenlabs?.voiceSettings?.speed).toBe(1.1); + expect(elevenlabsOverrides?.voiceId).toBe("pMsXgVXv3BLzUgSXRplE"); + expect(elevenlabsOverrides?.voiceSettings?.stability).toBe(0.4); + expect(elevenlabsOverrides?.voiceSettings?.speed).toBe(1.1); }); it("accepts edge as a legacy microsoft provider override", () => { @@ -350,9 +363,12 @@ describe("tts", () => { const policy = resolveModelOverridePolicy({ enabled: true }); const input = "Hello [[tts:provider=edge voice=alloy]] world"; const result = parseTtsDirectives(input, policy); + const openaiOverrides = result.overrides.providerOverrides?.openai as + | { voice?: string } + | undefined; expect(result.overrides.provider).toBeUndefined(); - expect(result.overrides.openai?.voice).toBe("alloy"); + expect(openaiOverrides?.voice).toBe("alloy"); }); it("keeps text intact when overrides are disabled", () => { @@ -370,9 +386,12 @@ describe("tts", () => { const customBaseUrl = "http://localhost:8880/v1"; const result = parseTtsDirectives(input, policy, customBaseUrl); + const openaiOverrides = result.overrides.providerOverrides?.openai as + | { voice?: string; model?: string } + | undefined; - expect(result.overrides.openai?.voice).toBe("kokoro-chinese"); - expect(result.overrides.openai?.model).toBe("kokoro-v1"); + expect(openaiOverrides?.voice).toBe("kokoro-chinese"); + expect(openaiOverrides?.model).toBe("kokoro-v1"); expect(result.warnings).toHaveLength(0); }); @@ -382,8 +401,11 @@ describe("tts", () => { const defaultBaseUrl = "https://api.openai.com/v1"; const result = parseTtsDirectives(input, policy, defaultBaseUrl); + const openaiOverrides = result.overrides.providerOverrides?.openai as + | { voice?: string } + | undefined; - expect(result.overrides.openai?.voice).toBeUndefined(); + expect(openaiOverrides?.voice).toBeUndefined(); expect(result.warnings).toContain('invalid OpenAI voice "kokoro-chinese"'); }); }); @@ -704,7 +726,10 @@ describe("tts", () => { ] as const) { withEnv(testCase.env, () => { const config = resolveTtsConfig(testCase.cfg); - expect(config.openai.baseUrl, testCase.name).toBe(testCase.expected); + const openaiConfig = getResolvedSpeechProviderConfig(config, "openai") as { + baseUrl?: string; + }; + expect(openaiConfig.baseUrl, testCase.name).toBe(testCase.expected); }); } }); diff --git a/src/tts/tts.ts b/src/tts/tts.ts index 10663d66c76..9e41f780761 100644 --- a/src/tts/tts.ts +++ b/src/tts/tts.ts @@ -14,7 +14,6 @@ import type { ReplyPayload } from "../auto-reply/types.js"; import { normalizeChannelId } from "../channels/plugins/index.js"; import type { ChannelId } from "../channels/plugins/types.js"; import type { OpenClawConfig } from "../config/config.js"; -import { normalizeResolvedSecretInputString } from "../config/types.secrets.js"; import type { TtsConfig, TtsAutoMode, @@ -24,52 +23,30 @@ import type { } from "../config/types.tts.js"; import { logVerbose } from "../globals.js"; import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; -import { - OPENAI_DEFAULT_TTS_MODEL as DEFAULT_OPENAI_MODEL, - OPENAI_DEFAULT_TTS_VOICE as DEFAULT_OPENAI_VOICE, -} from "../plugins/provider-model-defaults.js"; import { stripMarkdown } from "../shared/text/strip-markdown.js"; import { CONFIG_DIR, resolveUserPath } from "../utils.js"; +import { parseTtsDirectives } from "./directives.js"; import { + canonicalizeSpeechProviderId, getSpeechProvider, listSpeechProviders, - normalizeSpeechProviderId, } from "./provider-registry.js"; -import type { SpeechVoiceOption } from "./provider-types.js"; -import { - DEFAULT_OPENAI_BASE_URL, - isValidOpenAIModel, - isValidOpenAIVoice, - isValidVoiceId, - OPENAI_TTS_MODELS, - OPENAI_TTS_VOICES, - resolveOpenAITtsInstructions, - parseTtsDirectives, - scheduleCleanup, - summarizeText, -} from "./tts-core.js"; -export { OPENAI_TTS_MODELS, OPENAI_TTS_VOICES } from "./tts-core.js"; +import type { + SpeechModelOverridePolicy, + SpeechProviderConfig, + SpeechVoiceOption, + TtsDirectiveOverrides, + TtsDirectiveParseResult, +} from "./provider-types.js"; +import { scheduleCleanup, summarizeText } from "./tts-core.js"; + +export type { TtsDirectiveOverrides, TtsDirectiveParseResult } from "./provider-types.js"; const DEFAULT_TIMEOUT_MS = 30_000; const DEFAULT_TTS_MAX_LENGTH = 1500; const DEFAULT_TTS_SUMMARIZE = true; const DEFAULT_MAX_TEXT_LENGTH = 4096; -const DEFAULT_ELEVENLABS_BASE_URL = "https://api.elevenlabs.io"; -const DEFAULT_ELEVENLABS_VOICE_ID = "pMsXgVXv3BLzUgSXRplE"; -const DEFAULT_ELEVENLABS_MODEL_ID = "eleven_multilingual_v2"; -const DEFAULT_EDGE_VOICE = "en-US-MichelleNeural"; -const DEFAULT_EDGE_LANG = "en-US"; -const DEFAULT_EDGE_OUTPUT_FORMAT = "audio-24khz-48kbitrate-mono-mp3"; - -const DEFAULT_ELEVENLABS_VOICE_SETTINGS = { - stability: 0.5, - similarityBoost: 0.75, - style: 0.0, - useSpeakerBoost: true, - speed: 1.0, -}; - const OPUS_OUTPUT = { openai: "opus" as const, // ElevenLabs output formats use codec_sample_rate_bitrate naming. @@ -95,43 +72,7 @@ export type ResolvedTtsConfig = { providerSource: "config" | "default"; summaryModel?: string; modelOverrides: ResolvedTtsModelOverrides; - elevenlabs: { - apiKey?: string; - baseUrl: string; - voiceId: string; - modelId: string; - seed?: number; - applyTextNormalization?: "auto" | "on" | "off"; - languageCode?: string; - voiceSettings: { - stability: number; - similarityBoost: number; - style: number; - useSpeakerBoost: boolean; - speed: number; - }; - }; - openai: { - apiKey?: string; - baseUrl: string; - model: string; - voice: string; - speed?: number; - instructions?: string; - }; - edge: { - enabled: boolean; - voice: string; - lang: string; - outputFormat: string; - outputFormatConfigured: boolean; - pitch?: string; - rate?: string; - volume?: string; - saveSubtitles: boolean; - proxy?: string; - timeoutMs?: number; - }; + providerConfigs: Record; prefsPath?: string; maxTextLength: number; timeoutMs: number; @@ -147,47 +88,7 @@ type TtsUserPrefs = { }; }; -export type ResolvedTtsModelOverrides = { - enabled: boolean; - allowText: boolean; - allowProvider: boolean; - allowVoice: boolean; - allowModelId: boolean; - allowVoiceSettings: boolean; - allowNormalization: boolean; - allowSeed: boolean; -}; - -export type TtsDirectiveOverrides = { - ttsText?: string; - provider?: TtsProvider; - openai?: { - voice?: string; - model?: string; - speed?: number; - }; - elevenlabs?: { - voiceId?: string; - modelId?: string; - outputFormat?: string; - seed?: number; - applyTextNormalization?: "auto" | "on" | "off"; - languageCode?: string; - voiceSettings?: Partial; - }; - microsoft?: { - voice?: string; - outputFormat?: string; - }; -}; - -export type TtsDirectiveParseResult = { - cleanedText: string; - ttsText?: string; - hasDirective: boolean; - overrides: TtsDirectiveOverrides; - warnings: string[]; -}; +export type ResolvedTtsModelOverrides = SpeechModelOverridePolicy; export type TtsResult = { success: boolean; @@ -273,75 +174,66 @@ function resolveModelOverridePolicy( }; } +function sortSpeechProvidersForAutoSelection(cfg?: OpenClawConfig) { + return 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); + }); +} + +function asProviderConfig(value: unknown): SpeechProviderConfig { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as SpeechProviderConfig) + : {}; +} + +function resolveSpeechProviderConfigs( + raw: TtsConfig, + cfg: OpenClawConfig, + timeoutMs: number, +): Record { + const providerConfigs: Record = {}; + for (const provider of listSpeechProviders(cfg)) { + providerConfigs[provider.id] = + provider.resolveConfig?.({ + cfg, + rawConfig: raw as Record, + timeoutMs, + }) ?? asProviderConfig((raw as Record)[provider.id]); + } + return providerConfigs; +} + +export function getResolvedSpeechProviderConfig( + config: ResolvedTtsConfig, + providerId: string, + cfg?: OpenClawConfig, +): SpeechProviderConfig { + const canonical = + canonicalizeSpeechProviderId(providerId, cfg) ?? providerId.trim().toLowerCase(); + return config.providerConfigs[canonical] ?? {}; +} + export function resolveTtsConfig(cfg: OpenClawConfig): ResolvedTtsConfig { const raw: TtsConfig = cfg.messages?.tts ?? {}; const providerSource = raw.provider ? "config" : "default"; - const rawMicrosoft = { ...raw.edge, ...raw.microsoft }; - const edgeOutputFormat = rawMicrosoft.outputFormat?.trim(); + const timeoutMs = raw.timeoutMs ?? DEFAULT_TIMEOUT_MS; const auto = normalizeTtsAutoMode(raw.auto) ?? (raw.enabled ? "always" : "off"); return { auto, mode: raw.mode ?? "final", - provider: normalizeSpeechProviderId(raw.provider) ?? "microsoft", + provider: canonicalizeSpeechProviderId(raw.provider, cfg) ?? "microsoft", providerSource, summaryModel: raw.summaryModel?.trim() || undefined, modelOverrides: resolveModelOverridePolicy(raw.modelOverrides), - elevenlabs: { - apiKey: normalizeResolvedSecretInputString({ - value: raw.elevenlabs?.apiKey, - path: "messages.tts.elevenlabs.apiKey", - }), - baseUrl: raw.elevenlabs?.baseUrl?.trim() || DEFAULT_ELEVENLABS_BASE_URL, - voiceId: raw.elevenlabs?.voiceId ?? DEFAULT_ELEVENLABS_VOICE_ID, - modelId: raw.elevenlabs?.modelId ?? DEFAULT_ELEVENLABS_MODEL_ID, - seed: raw.elevenlabs?.seed, - applyTextNormalization: raw.elevenlabs?.applyTextNormalization, - languageCode: raw.elevenlabs?.languageCode, - voiceSettings: { - stability: - raw.elevenlabs?.voiceSettings?.stability ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.stability, - similarityBoost: - raw.elevenlabs?.voiceSettings?.similarityBoost ?? - DEFAULT_ELEVENLABS_VOICE_SETTINGS.similarityBoost, - style: raw.elevenlabs?.voiceSettings?.style ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.style, - useSpeakerBoost: - raw.elevenlabs?.voiceSettings?.useSpeakerBoost ?? - DEFAULT_ELEVENLABS_VOICE_SETTINGS.useSpeakerBoost, - speed: raw.elevenlabs?.voiceSettings?.speed ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.speed, - }, - }, - openai: { - apiKey: normalizeResolvedSecretInputString({ - value: raw.openai?.apiKey, - path: "messages.tts.openai.apiKey", - }), - // Config > env var > default; strip trailing slashes for consistency. - baseUrl: ( - raw.openai?.baseUrl?.trim() || - process.env.OPENAI_TTS_BASE_URL?.trim() || - DEFAULT_OPENAI_BASE_URL - ).replace(/\/+$/, ""), - model: raw.openai?.model ?? DEFAULT_OPENAI_MODEL, - voice: raw.openai?.voice ?? DEFAULT_OPENAI_VOICE, - speed: raw.openai?.speed, - instructions: raw.openai?.instructions?.trim() || undefined, - }, - edge: { - enabled: rawMicrosoft.enabled ?? true, - voice: rawMicrosoft.voice?.trim() || DEFAULT_EDGE_VOICE, - lang: rawMicrosoft.lang?.trim() || DEFAULT_EDGE_LANG, - outputFormat: edgeOutputFormat || DEFAULT_EDGE_OUTPUT_FORMAT, - outputFormatConfigured: Boolean(edgeOutputFormat), - pitch: rawMicrosoft.pitch?.trim() || undefined, - rate: rawMicrosoft.rate?.trim() || undefined, - volume: rawMicrosoft.volume?.trim() || undefined, - saveSubtitles: rawMicrosoft.saveSubtitles ?? false, - proxy: rawMicrosoft.proxy?.trim() || undefined, - timeoutMs: rawMicrosoft.timeoutMs, - }, + providerConfigs: resolveSpeechProviderConfigs(raw, cfg, timeoutMs), prefsPath: raw.prefsPath, maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH, - timeoutMs: raw.timeoutMs ?? DEFAULT_TIMEOUT_MS, + timeoutMs, }; } @@ -464,26 +356,30 @@ export function setTtsEnabled(prefsPath: string, enabled: boolean): void { export function getTtsProvider(config: ResolvedTtsConfig, prefsPath: string): TtsProvider { const prefs = readPrefs(prefsPath); - const prefsProvider = normalizeSpeechProviderId(prefs.tts?.provider); + const prefsProvider = canonicalizeSpeechProviderId(prefs.tts?.provider); if (prefsProvider) { return prefsProvider; } if (config.providerSource === "config") { - return normalizeSpeechProviderId(config.provider) ?? config.provider; + return canonicalizeSpeechProviderId(config.provider) ?? config.provider; } - if (resolveTtsApiKey(config, "openai")) { - return "openai"; - } - if (resolveTtsApiKey(config, "elevenlabs")) { - return "elevenlabs"; + for (const provider of sortSpeechProvidersForAutoSelection()) { + if ( + provider.isConfigured({ + providerConfig: config.providerConfigs[provider.id] ?? {}, + timeoutMs: config.timeoutMs, + }) + ) { + return provider.id; + } } return "microsoft"; } export function setTtsProvider(prefsPath: string, provider: TtsProvider): void { updatePrefs(prefsPath, (prefs) => { - prefs.tts = { ...prefs.tts, provider: normalizeSpeechProviderId(provider) ?? provider }; + prefs.tts = { ...prefs.tts, provider: canonicalizeSpeechProviderId(provider) ?? provider }; }); } @@ -531,36 +427,11 @@ function resolveChannelId(channel: string | undefined): ChannelId | null { return channel ? normalizeChannelId(channel) : null; } -function resolveEdgeOutputFormat(config: ResolvedTtsConfig): string { - return config.edge.outputFormat; -} - -export function resolveTtsApiKey( - config: ResolvedTtsConfig, - provider: TtsProvider, -): string | undefined { - const normalizedProvider = normalizeSpeechProviderId(provider); - if (normalizedProvider === "elevenlabs") { - return config.elevenlabs.apiKey || process.env.ELEVENLABS_API_KEY || process.env.XI_API_KEY; - } - if (normalizedProvider === "openai") { - return config.openai.apiKey || process.env.OPENAI_API_KEY; - } - return undefined; -} - -export const TTS_PROVIDERS = ["openai", "elevenlabs", "microsoft"] as const; - export function resolveTtsProviderOrder(primary: TtsProvider, cfg?: OpenClawConfig): TtsProvider[] { - const normalizedPrimary = normalizeSpeechProviderId(primary) ?? primary; + const normalizedPrimary = canonicalizeSpeechProviderId(primary, cfg) ?? primary; const ordered = new Set([normalizedPrimary]); - for (const provider of TTS_PROVIDERS) { - if (provider !== normalizedPrimary) { - ordered.add(provider); - } - } - for (const provider of listSpeechProviders(cfg)) { - const normalized = normalizeSpeechProviderId(provider.id) ?? provider.id; + for (const provider of sortSpeechProvidersForAutoSelection(cfg)) { + const normalized = provider.id; if (normalized !== normalizedPrimary) { ordered.add(normalized); } @@ -574,7 +445,16 @@ export function isTtsProviderConfigured( cfg?: OpenClawConfig, ): boolean { const resolvedProvider = getSpeechProvider(provider, cfg); - return resolvedProvider?.isConfigured({ cfg, config }) ?? false; + if (!resolvedProvider) { + return false; + } + return ( + resolvedProvider.isConfigured({ + cfg, + providerConfig: getResolvedSpeechProviderConfig(config, resolvedProvider.id, cfg), + timeoutMs: config.timeoutMs, + }) ?? false + ); } function formatTtsProviderError(provider: TtsProvider, err: unknown): string { @@ -604,7 +484,18 @@ function resolveReadySpeechProvider(params: { params.errors.push(`${params.provider}: no provider registered`); return null; } - if (!resolvedProvider.isConfigured({ cfg: params.cfg, config: params.config })) { + const providerConfig = getResolvedSpeechProviderConfig( + params.config, + resolvedProvider.id, + params.cfg, + ); + if ( + !resolvedProvider.isConfigured({ + cfg: params.cfg, + providerConfig, + timeoutMs: params.config.timeoutMs, + }) + ) { params.errors.push(`${params.provider}: not configured`); return null; } @@ -638,7 +529,8 @@ function resolveTtsRequestSetup(params: { } const userProvider = getTtsProvider(config, prefsPath); - const provider = normalizeSpeechProviderId(params.providerOverride) ?? userProvider; + const provider = + canonicalizeSpeechProviderId(params.providerOverride, params.cfg) ?? userProvider; return { config, providers: params.disableFallback ? [provider] : resolveTtsProviderOrder(provider, params.cfg), @@ -715,9 +607,10 @@ export async function synthesizeSpeech(params: { const synthesis = await resolvedProvider.synthesize({ text: params.text, cfg: params.cfg, - config, + providerConfig: getResolvedSpeechProviderConfig(config, resolvedProvider.id, params.cfg), target, - overrides: params.overrides, + providerOverrides: params.overrides?.providerOverrides?.[resolvedProvider.id], + timeoutMs: config.timeoutMs, }); return { success: true, @@ -770,7 +663,8 @@ export async function textToSpeechTelephony(params: { const synthesis = await resolvedProvider.synthesizeTelephony({ text: params.text, cfg: params.cfg, - config, + providerConfig: getResolvedSpeechProviderConfig(config, resolvedProvider.id, params.cfg), + timeoutMs: config.timeoutMs, }); return { @@ -796,7 +690,7 @@ export async function listSpeechVoices(params: { apiKey?: string; baseUrl?: string; }): Promise { - const provider = normalizeSpeechProviderId(params.provider); + const provider = canonicalizeSpeechProviderId(params.provider, params.cfg); if (!provider) { throw new Error("speech provider id is required"); } @@ -813,7 +707,7 @@ export async function listSpeechVoices(params: { } return await resolvedProvider.listVoices({ cfg: params.cfg, - config, + providerConfig: getResolvedSpeechProviderConfig(config, resolvedProvider.id, params.cfg), apiKey: params.apiKey, baseUrl: params.baseUrl, }); @@ -844,7 +738,10 @@ export async function maybeApplyTtsToPayload(params: { const reply = resolveSendableOutboundReplyParts(params.payload); const text = reply.text; - const directives = parseTtsDirectives(text, config.modelOverrides, config.openai.baseUrl); + const directives = parseTtsDirectives(text, config.modelOverrides, { + cfg: params.cfg, + providerConfigs: config.providerConfigs, + }); if (directives.warnings.length > 0) { logVerbose(`TTS: ignored directive overrides (${directives.warnings.join("; ")})`); } @@ -971,15 +868,9 @@ export async function maybeApplyTtsToPayload(params: { } export const _test = { - isValidVoiceId, - isValidOpenAIVoice, - isValidOpenAIModel, - OPENAI_TTS_MODELS, - OPENAI_TTS_VOICES, - resolveOpenAITtsInstructions, parseTtsDirectives, resolveModelOverridePolicy, summarizeText, resolveOutputFormat, - resolveEdgeOutputFormat, + getResolvedSpeechProviderConfig, };