UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

239 lines (238 loc) 10.2 kB
import { s as asFiniteNumber } from "./number-coercion-CLj0HTDM.js"; import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js"; import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { m as normalizeResolvedSecretInputString } from "./types.secrets-kC0nOetj.js"; import "./string-coerce-runtime-GQa0ehRA.js"; import "./secret-input-dpVVFmLG.js"; import { t as parseSpeechDirectiveNumberOverride } from "./directive-number-BuuWT1SS.js"; import "./speech-provider-D6qM0DDB.js"; import { a as resolveOpenAIProviderConfigRecord } from "./realtime-provider-shared-D19ceJ4h.js"; import { a as isValidOpenAIVoice, i as isValidOpenAIModel, n as OPENAI_TTS_MODELS, o as normalizeOpenAITtsBaseUrl, r as OPENAI_TTS_VOICES, s as openaiTTS, t as DEFAULT_OPENAI_BASE_URL } from "./tts-CTOmge4b.js"; //#region extensions/openai/speech-provider.ts const OPENAI_SPEECH_RESPONSE_FORMATS = [ "mp3", "opus", "wav" ]; function resolveOpenAISpeechApiKey(config) { return normalizeOptionalString(config.apiKey) ?? normalizeOptionalString(process.env.OPENAI_API_KEY); } function normalizeOpenAISpeechResponseFormat(value) { const next = normalizeOptionalLowercaseString(value); if (!next) return; if (OPENAI_SPEECH_RESPONSE_FORMATS.includes(next)) return next; throw new Error(`Invalid OpenAI speech responseFormat: ${next}`); } function isGroqSpeechBaseUrl(baseUrl) { try { const hostname = normalizeLowercaseStringOrEmpty(new URL(baseUrl).hostname); return hostname === "groq.com" || hostname.endsWith(".groq.com"); } catch { return false; } } function resolveSpeechResponseFormat(baseUrl, target, configuredFormat) { if (configuredFormat) return configuredFormat; if (isGroqSpeechBaseUrl(baseUrl)) return "wav"; return target === "voice-note" ? "opus" : "mp3"; } function responseFormatToFileExtension(format) { switch (format) { case "opus": return ".opus"; case "wav": return ".wav"; default: return ".mp3"; } } function readExtraBody(value) { const body = asOptionalRecord(value); if (!body || Object.keys(body).length === 0) return; return body; } function normalizeOpenAISpeechSpeed(value, baseUrl) { const speed = asFiniteNumber(value); if (speed === void 0) return; if (baseUrl !== void 0 && normalizeOpenAITtsBaseUrl(baseUrl) !== "https://api.openai.com/v1") return speed; return speed >= .25 && speed <= 4 ? speed : void 0; } function normalizeOpenAIProviderConfig(rawConfig) { const raw = resolveOpenAIProviderConfigRecord(rawConfig); const extraBody = readExtraBody(raw?.extraBody) ?? readExtraBody(raw?.extra_body); const baseUrl = normalizeOpenAITtsBaseUrl(normalizeOptionalString(raw?.baseUrl) ?? normalizeOptionalString(process.env.OPENAI_TTS_BASE_URL) ?? "https://api.openai.com/v1"); return { apiKey: normalizeResolvedSecretInputString({ value: raw?.apiKey, path: "tts.providers.openai.apiKey" }), baseUrl, model: normalizeOptionalString(raw?.model) ?? "gpt-4o-mini-tts", voice: normalizeOptionalString(raw?.voice) ?? "coral", speed: normalizeOpenAISpeechSpeed(raw?.speed, baseUrl), instructions: normalizeOptionalString(raw?.instructions), responseFormat: normalizeOpenAISpeechResponseFormat(raw?.responseFormat), extraBody }; } function readOpenAIProviderConfig(config) { const normalized = normalizeOpenAIProviderConfig({}); return { apiKey: normalizeOptionalString(config.apiKey) ?? normalized.apiKey, baseUrl: normalizeOptionalString(config.baseUrl) ?? normalized.baseUrl, model: normalizeOptionalString(config.model) ?? normalized.model, voice: normalizeOptionalString(config.voice) ?? normalized.voice, speed: normalizeOpenAISpeechSpeed(config.speed, normalizeOptionalString(config.baseUrl) ?? normalized.baseUrl) ?? normalized.speed, instructions: normalizeOptionalString(config.instructions) ?? normalized.instructions, responseFormat: normalizeOpenAISpeechResponseFormat(config.responseFormat) ?? normalized.responseFormat, extraBody: readExtraBody(config.extraBody) ?? readExtraBody(config.extra_body) }; } function readOpenAIOverrides(overrides, baseUrl) { if (!overrides) return {}; return { model: normalizeOptionalString(overrides.model), voice: normalizeOptionalString(overrides.voice), speed: normalizeOpenAISpeechSpeed(overrides.speed, baseUrl) }; } function isCustomOpenAITtsBaseUrl(baseUrl) { if (baseUrl !== void 0) return normalizeOpenAITtsBaseUrl(baseUrl) !== DEFAULT_OPENAI_BASE_URL; return normalizeOpenAITtsBaseUrl(process.env.OPENAI_TTS_BASE_URL) !== DEFAULT_OPENAI_BASE_URL; } function parseDirectiveToken(ctx) { const baseUrl = normalizeOptionalString(asOptionalRecord(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 } }; case "speed": case "openai_speed": case "openaispeed": { const customBaseUrl = isCustomOpenAITtsBaseUrl(baseUrl); return parseSpeechDirectiveNumberOverride({ ctx, overrideKey: "speed", range: customBaseUrl ? {} : { min: .25, max: 4 }, warning: (value) => customBaseUrl ? `invalid OpenAI-compatible speed "${value}"` : `invalid OpenAI speed "${value}" (0.25-4.0)` }); } default: return { handled: false }; } } function buildOpenAISpeechProvider() { return { id: "openai", label: "OpenAI", autoSelectOrder: 10, defaultModel: OPENAI_TTS_MODELS[0], models: OPENAI_TTS_MODELS, voices: OPENAI_TTS_VOICES, resolveConfig: ({ rawConfig }) => normalizeOpenAIProviderConfig(rawConfig), parseDirectiveToken, resolveTalkConfig: ({ baseTtsConfig, talkProviderConfig }) => { const base = normalizeOpenAIProviderConfig(baseTtsConfig); const responseFormat = normalizeOpenAISpeechResponseFormat(talkProviderConfig.responseFormat); const baseUrl = normalizeOptionalString(talkProviderConfig.baseUrl) ?? base.baseUrl; const speed = normalizeOpenAISpeechSpeed(talkProviderConfig.speed, baseUrl); return { ...base, ...talkProviderConfig.apiKey === void 0 ? {} : { apiKey: normalizeResolvedSecretInputString({ value: talkProviderConfig.apiKey, path: "talk.providers.openai.apiKey" }) }, ...normalizeOptionalString(talkProviderConfig.baseUrl) == null ? {} : { baseUrl }, ...normalizeOptionalString(talkProviderConfig.modelId) == null ? {} : { model: normalizeOptionalString(talkProviderConfig.modelId) }, ...normalizeOptionalString(talkProviderConfig.voiceId) == null ? {} : { voice: normalizeOptionalString(talkProviderConfig.voiceId) }, ...speed == null ? {} : { speed }, ...normalizeOptionalString(talkProviderConfig.instructions) == null ? {} : { instructions: normalizeOptionalString(talkProviderConfig.instructions) }, ...responseFormat == null ? {} : { responseFormat } }; }, resolveTalkOverrides: ({ params }) => ({ ...normalizeOptionalString(params.voiceId) == null ? {} : { voice: normalizeOptionalString(params.voiceId) }, ...normalizeOptionalString(params.modelId) == null ? {} : { model: normalizeOptionalString(params.modelId) }, ...asFiniteNumber(params.speed) == null ? {} : { speed: asFiniteNumber(params.speed) } }), listVoices: async () => OPENAI_TTS_VOICES.map((voice) => ({ id: voice, name: voice })), isConfigured: ({ providerConfig }) => Boolean(resolveOpenAISpeechApiKey(readOpenAIProviderConfig(providerConfig))), synthesize: async (req) => { const config = readOpenAIProviderConfig(req.providerConfig); const overrides = readOpenAIOverrides(req.providerOverrides, config.baseUrl); const apiKey = resolveOpenAISpeechApiKey(config); if (!apiKey) throw new Error("OpenAI API key missing"); const responseFormat = resolveSpeechResponseFormat(config.baseUrl, req.target, config.responseFormat); const { resolveGeneratedMediaMaxBytes } = await import("./plugin-sdk/media-generation-runtime.js"); const audioBuffer = await openaiTTS({ text: req.text, apiKey, baseUrl: config.baseUrl, model: overrides.model ?? config.model, voice: overrides.voice ?? config.voice, speed: overrides.speed ?? config.speed, instructions: config.instructions, responseFormat, extraBody: config.extraBody, timeoutMs: req.timeoutMs, maxBytes: resolveGeneratedMediaMaxBytes(req.cfg, "audio") }); const fileExtension = responseFormatToFileExtension(responseFormat); const { isVoiceMessageCompatibleAudio } = await import("./plugin-sdk/media-runtime.js"); return { audioBuffer, outputFormat: responseFormat, fileExtension, voiceCompatible: req.target === "voice-note" && isVoiceMessageCompatibleAudio({ fileName: `speech${fileExtension}` }) }; }, synthesizeTelephony: async (req) => { const config = readOpenAIProviderConfig(req.providerConfig); const overrides = readOpenAIOverrides(req.providerOverrides, config.baseUrl); const apiKey = resolveOpenAISpeechApiKey(config); if (!apiKey) throw new Error("OpenAI API key missing"); const outputFormat = "pcm"; const sampleRate = 24e3; const { resolveGeneratedMediaMaxBytes } = await import("./plugin-sdk/media-generation-runtime.js"); return { audioBuffer: await openaiTTS({ text: req.text, apiKey, baseUrl: config.baseUrl, model: overrides.model ?? config.model, voice: overrides.voice ?? config.voice, speed: overrides.speed ?? config.speed, instructions: config.instructions, responseFormat: outputFormat, extraBody: config.extraBody, timeoutMs: req.timeoutMs, maxBytes: resolveGeneratedMediaMaxBytes(req.cfg, "audio") }), outputFormat, sampleRate }; } }; } //#endregion export { buildOpenAISpeechProvider as t };