openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
245 lines (244 loc) • 10.1 kB
JavaScript
import { c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { s as asFiniteNumber } from "./number-coercion-CJQ8TR--.js";
import { u as normalizeResolvedSecretInputString } from "./types.secrets-_0JOMGE5.js";
import { d as readProviderBinaryResponse, n as asObject, r as assertOkOrThrowHttpError } from "./provider-http-errors-DqaqQLLZ.js";
import { c as postJsonRequest, p as resolveProviderHttpRequestConfig } from "./shared-IuCmx8oC.js";
import "./directives-BaSUaNZN.js";
import "./secret-input-DVCzFGxN.js";
import "./provider-http-BXCBCi4E.js";
//#region src/tts/openai-compatible-speech-provider.ts
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) {
return asObject(asObject(rawConfig.providers)?.[providerConfigKey]) ?? asObject(rawConfig[providerConfigKey]);
}
function readModelProviderConfig(cfg, providerConfigKey) {
return asObject(asObject(asObject(asObject(cfg)?.models)?.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 {
apiKey: normalizeResolvedSecretInputString({
value: raw?.apiKey,
path: `messages.tts.providers.${providerConfigKey}.apiKey`
}),
baseUrl: normalizeOptionalString(raw?.baseUrl) == null ? void 0 : normalizeBaseUrl({
value: raw?.baseUrl,
fallback: options.defaultBaseUrl,
policy: options.baseUrlPolicy
}),
model: normalizeModel(normalizeOptionalString(raw?.model ?? raw?.modelId), options.defaultModel),
voice: normalizeOptionalString(raw?.voice ?? raw?.voiceId) ?? options.defaultVoice,
speed: asFiniteNumber(raw?.speed),
responseFormat: normalizeResponseFormat({
providerLabel: options.label,
responseFormats: options.responseFormats,
value: raw?.responseFormat
}),
...readExtraConfig(raw)
};
}
function readProviderConfig(config) {
const 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: normalizeOptionalString(config.voice ?? config.voiceId) ?? 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);
const voiceId = normalizeOptionalString(talkProviderConfig.voiceId);
if (voiceId !== void 0) next.voice = voiceId;
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 { 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: Buffer.from(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 };