openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
236 lines (235 loc) • 9.87 kB
JavaScript
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 } from "./string-coerce-CIXf7egm.js";
import { m as normalizeResolvedSecretInputString } from "./types.secrets-kC0nOetj.js";
import "./secret-input-dpVVFmLG.js";
//#region src/tts/openai-compatible-speech-provider.ts
function resolveConfiguredSpeechVoice(config) {
return normalizeOptionalString(config?.speakerVoice) ?? normalizeOptionalString(config?.speakerVoiceId) ?? normalizeOptionalString(config?.voice) ?? normalizeOptionalString(config?.voiceId);
}
function normalizeResponseFormat(params) {
const next = normalizeOptionalLowercaseString(params.value);
if (!next) return;
if (params.responseFormats.includes(next)) return next;
throw new Error(`Invalid ${params.providerLabel} speech responseFormat: ${next}`);
}
function responseFormatToFileExtension(format) {
return `.${format}`;
}
function trimTrailingBaseUrl(value, fallback) {
return (normalizeOptionalString(value) ?? fallback).replace(/\/+$/u, "");
}
function normalizeBaseUrl(params) {
const normalized = trimTrailingBaseUrl(params.value, params.fallback);
if (params.policy?.kind !== "canonical") return normalized;
const canonical = trimTrailingBaseUrl(params.fallback, params.fallback);
return new Set([canonical, ...params.policy.aliases ?? []].map((entry) => trimTrailingBaseUrl(entry, canonical))).has(normalized) || !params.policy.allowCustom ? canonical : normalized;
}
function resolveProviderConfigRecord(rawConfig, providerConfigKey) {
const providers = asOptionalRecord(rawConfig.providers);
return asOptionalRecord(providers?.[providerConfigKey]) ?? asOptionalRecord(rawConfig[providerConfigKey]);
}
function readModelProviderConfig(cfg, providerConfigKey) {
const root = asOptionalRecord(cfg);
const models = asOptionalRecord(root?.models);
const providers = asOptionalRecord(models?.providers);
return asOptionalRecord(providers?.[providerConfigKey]);
}
function readSpeechOverrides(overrides) {
if (!overrides) return {};
return {
model: normalizeOptionalString(overrides.model ?? overrides.modelId),
voice: normalizeOptionalString(overrides.voice ?? overrides.voiceId),
speed: asFiniteNumber(overrides.speed)
};
}
function parseDirectiveToken(ctx, providerConfigKey) {
const compactProviderKey = providerConfigKey.replace(/[^a-z0-9]+/giu, "").toLowerCase();
switch (ctx.key) {
case "voice":
case "voice_id":
case "voiceid":
case `${providerConfigKey}_voice`:
case `${compactProviderKey}voice`:
if (!ctx.policy.allowVoice) return { handled: true };
return {
handled: true,
overrides: { voice: ctx.value }
};
case "model":
case "model_id":
case "modelid":
case `${providerConfigKey}_model`:
case `${compactProviderKey}model`:
if (!ctx.policy.allowModelId) return { handled: true };
return {
handled: true,
overrides: { model: ctx.value }
};
default: return { handled: false };
}
}
function buildExtraJsonBodyFields(config, fields) {
const body = {};
for (const field of fields ?? []) {
const value = config[field.configKey];
if (value != null) body[field.requestKey ?? field.configKey] = value;
}
return body;
}
/** Build a complete SpeechProviderPlugin for OpenAI-compatible speech endpoints. */
function createOpenAiCompatibleSpeechProvider(options) {
const providerConfigKey = options.configKey ?? options.id;
const normalizeModel = options.normalizeModel ?? ((value, fallback) => normalizeOptionalString(value) ?? fallback);
const readExtraConfig = options.readExtraConfig ?? (() => ({}));
function normalizeConfig(rawConfig) {
const raw = resolveProviderConfigRecord(rawConfig, providerConfigKey);
return readProviderConfig(raw, {
model: options.defaultModel,
voice: options.defaultVoice,
apiKey: normalizeResolvedSecretInputString({
value: raw?.apiKey,
path: `tts.providers.${providerConfigKey}.apiKey`
})
});
}
function readProviderConfig(config, normalized = normalizeConfig({})) {
return {
apiKey: normalizeOptionalString(config?.apiKey) ?? normalized.apiKey,
baseUrl: normalizeOptionalString(config?.baseUrl) == null ? normalized.baseUrl : normalizeBaseUrl({
value: config?.baseUrl,
fallback: options.defaultBaseUrl,
policy: options.baseUrlPolicy
}),
model: normalizeModel(normalizeOptionalString(config?.model ?? config?.modelId), normalized.model),
voice: resolveConfiguredSpeechVoice(config) ?? normalized.voice,
speed: asFiniteNumber(config?.speed) ?? normalized.speed,
responseFormat: normalizeResponseFormat({
providerLabel: options.label,
responseFormats: options.responseFormats,
value: config?.responseFormat
}) ?? normalized.responseFormat,
...readExtraConfig(config)
};
}
function resolveApiKey(params) {
return params.providerConfig.apiKey ?? normalizeResolvedSecretInputString({
value: readModelProviderConfig(params.cfg, providerConfigKey)?.apiKey,
path: `models.providers.${providerConfigKey}.apiKey`
}) ?? normalizeOptionalString(process.env[options.envKey]);
}
function resolveBaseUrl(params) {
return normalizeBaseUrl({
value: params.providerConfig.baseUrl ?? normalizeOptionalString(readModelProviderConfig(params.cfg, providerConfigKey)?.baseUrl),
fallback: options.defaultBaseUrl,
policy: options.baseUrlPolicy
});
}
return {
id: options.id,
label: options.label,
autoSelectOrder: options.autoSelectOrder,
defaultModel: options.defaultModel,
models: [...options.models],
voices: [...options.voices],
resolveConfig: ({ rawConfig }) => normalizeConfig(rawConfig),
parseDirectiveToken: (ctx) => parseDirectiveToken(ctx, providerConfigKey),
resolveTalkConfig: ({ baseTtsConfig, talkProviderConfig }) => {
const base = normalizeConfig(baseTtsConfig);
const responseFormat = normalizeResponseFormat({
providerLabel: options.label,
responseFormats: options.responseFormats,
value: talkProviderConfig.responseFormat
});
const next = { ...base };
if (talkProviderConfig.apiKey !== void 0) next.apiKey = normalizeResolvedSecretInputString({
value: talkProviderConfig.apiKey,
path: `talk.providers.${providerConfigKey}.apiKey`
});
const baseUrl = normalizeOptionalString(talkProviderConfig.baseUrl);
if (baseUrl !== void 0) next.baseUrl = normalizeBaseUrl({
value: baseUrl,
fallback: options.defaultBaseUrl,
policy: options.baseUrlPolicy
});
const modelId = normalizeOptionalString(talkProviderConfig.modelId);
if (modelId !== void 0) next.model = normalizeModel(modelId, options.defaultModel);
next.voice = resolveConfiguredSpeechVoice(talkProviderConfig) ?? base.voice;
const speed = asFiniteNumber(talkProviderConfig.speed);
if (speed !== void 0) next.speed = speed;
if (responseFormat !== void 0) next.responseFormat = responseFormat;
return next;
},
resolveTalkOverrides: ({ params }) => ({
...normalizeOptionalString(params.voiceId ?? params.voice) == null ? {} : { voice: normalizeOptionalString(params.voiceId ?? params.voice) },
...normalizeOptionalString(params.modelId ?? params.model) == null ? {} : { model: normalizeOptionalString(params.modelId ?? params.model) },
...asFiniteNumber(params.speed) == null ? {} : { speed: asFiniteNumber(params.speed) }
}),
listVoices: async () => options.voices.map((voice) => ({
id: voice,
name: voice
})),
isConfigured: ({ cfg, providerConfig }) => Boolean(resolveApiKey({
cfg,
providerConfig: readProviderConfig(providerConfig)
})),
synthesize: async (req) => {
const config = readProviderConfig(req.providerConfig);
const overrides = readSpeechOverrides(req.providerOverrides);
const apiKey = resolveApiKey({
cfg: req.cfg,
providerConfig: config
});
if (!apiKey) throw new Error(options.missingApiKeyError ?? `${options.label} API key missing`);
const baseUrl = resolveBaseUrl({
cfg: req.cfg,
providerConfig: config
});
const responseFormat = config.responseFormat ?? options.defaultResponseFormat;
const speed = overrides.speed ?? config.speed;
const { assertOkOrThrowHttpError, postJsonRequest, readProviderBinaryResponse, resolveProviderHttpRequestConfig } = await import("./plugin-sdk/provider-http.js");
const { allowPrivateNetwork, headers, dispatcherPolicy } = resolveProviderHttpRequestConfig({
baseUrl,
defaultBaseUrl: options.defaultBaseUrl,
allowPrivateNetwork: false,
defaultHeaders: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...options.extraHeaders
},
provider: options.id,
capability: "audio",
transport: "http"
});
const { response, release } = await postJsonRequest({
url: `${baseUrl}/audio/speech`,
headers,
body: {
model: normalizeModel(overrides.model ?? config.model, options.defaultModel),
input: req.text,
voice: overrides.voice ?? config.voice,
response_format: responseFormat,
...speed == null ? {} : { speed },
...buildExtraJsonBodyFields(config, options.extraJsonBodyFields)
},
timeoutMs: req.timeoutMs,
fetchFn: fetch,
allowPrivateNetwork,
dispatcherPolicy
});
try {
await assertOkOrThrowHttpError(response, options.apiErrorLabel ?? `${options.label} TTS API error`);
return {
audioBuffer: await readProviderBinaryResponse(response, options.apiErrorLabel ?? `${options.label} TTS API error`, "audio"),
outputFormat: responseFormat,
fileExtension: responseFormatToFileExtension(responseFormat),
voiceCompatible: options.voiceCompatibleResponseFormats.includes(responseFormat)
};
} finally {
await release();
}
}
};
}
//#endregion
export { createOpenAiCompatibleSpeechProvider as t };