mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix: validate openai speech speeds
This commit is contained in:
@@ -97,6 +97,64 @@ describe("buildOpenAISpeechProvider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("drops malformed speech speed values", () => {
|
||||
const provider = buildOpenAISpeechProvider();
|
||||
const resolved = provider.resolveConfig?.({
|
||||
cfg: {} as never,
|
||||
timeoutMs: 30_000,
|
||||
rawConfig: {
|
||||
providers: {
|
||||
openai: {
|
||||
speed: 4.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved?.speed).toBeUndefined();
|
||||
});
|
||||
|
||||
it("passes custom endpoint speech speeds through", () => {
|
||||
const provider = buildOpenAISpeechProvider();
|
||||
const resolved = provider.resolveConfig?.({
|
||||
cfg: {} as never,
|
||||
timeoutMs: 30_000,
|
||||
rawConfig: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://tts.example.com/v1",
|
||||
speed: 4.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved?.speed).toBe(4.5);
|
||||
});
|
||||
|
||||
it("uses talk base url overrides when validating speech speed", () => {
|
||||
const provider = buildOpenAISpeechProvider();
|
||||
|
||||
const resolvedConfig = provider.resolveTalkConfig?.({
|
||||
cfg: {} as never,
|
||||
timeoutMs: 30_000,
|
||||
baseTtsConfig: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "sk-base",
|
||||
},
|
||||
},
|
||||
},
|
||||
talkProviderConfig: {
|
||||
baseUrl: "https://tts.example.com/v1",
|
||||
speed: 4.5,
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolvedConfig?.baseUrl).toBe("https://tts.example.com/v1");
|
||||
expect(resolvedConfig?.speed).toBe(4.5);
|
||||
});
|
||||
|
||||
it("parses OpenAI directive tokens against the resolved base url", () => {
|
||||
const provider = buildOpenAISpeechProvider();
|
||||
|
||||
|
||||
@@ -105,24 +105,36 @@ function readExtraBody(value: unknown): Record<string, unknown> | undefined {
|
||||
return body;
|
||||
}
|
||||
|
||||
function normalizeOpenAISpeechSpeed(value: unknown, baseUrl?: string): number | undefined {
|
||||
const speed = asFiniteNumber(value);
|
||||
if (speed === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (baseUrl !== undefined && normalizeOpenAITtsBaseUrl(baseUrl) !== DEFAULT_OPENAI_BASE_URL) {
|
||||
return speed;
|
||||
}
|
||||
return speed >= 0.25 && speed <= 4 ? speed : undefined;
|
||||
}
|
||||
|
||||
function normalizeOpenAIProviderConfig(
|
||||
rawConfig: Record<string, unknown>,
|
||||
): OpenAITtsProviderConfig {
|
||||
const raw = resolveOpenAIProviderConfigRecord(rawConfig);
|
||||
const extraBody = readExtraBody(raw?.extraBody) ?? readExtraBody(raw?.extra_body);
|
||||
const baseUrl = normalizeOpenAITtsBaseUrl(
|
||||
trimToUndefined(raw?.baseUrl) ??
|
||||
trimToUndefined(process.env.OPENAI_TTS_BASE_URL) ??
|
||||
DEFAULT_OPENAI_BASE_URL,
|
||||
);
|
||||
return {
|
||||
apiKey: normalizeResolvedSecretInputString({
|
||||
value: raw?.apiKey,
|
||||
path: "messages.tts.providers.openai.apiKey",
|
||||
}),
|
||||
baseUrl: normalizeOpenAITtsBaseUrl(
|
||||
trimToUndefined(raw?.baseUrl) ??
|
||||
trimToUndefined(process.env.OPENAI_TTS_BASE_URL) ??
|
||||
DEFAULT_OPENAI_BASE_URL,
|
||||
),
|
||||
baseUrl,
|
||||
model: trimToUndefined(raw?.model) ?? "gpt-4o-mini-tts",
|
||||
voice: trimToUndefined(raw?.voice) ?? "coral",
|
||||
speed: asFiniteNumber(raw?.speed),
|
||||
speed: normalizeOpenAISpeechSpeed(raw?.speed, baseUrl),
|
||||
instructions: trimToUndefined(raw?.instructions),
|
||||
responseFormat: normalizeOpenAISpeechResponseFormat(raw?.responseFormat),
|
||||
extraBody,
|
||||
@@ -136,7 +148,11 @@ function readOpenAIProviderConfig(config: SpeechProviderConfig): OpenAITtsProvid
|
||||
baseUrl: trimToUndefined(config.baseUrl) ?? normalized.baseUrl,
|
||||
model: trimToUndefined(config.model) ?? normalized.model,
|
||||
voice: trimToUndefined(config.voice) ?? normalized.voice,
|
||||
speed: asFiniteNumber(config.speed) ?? normalized.speed,
|
||||
speed:
|
||||
normalizeOpenAISpeechSpeed(
|
||||
config.speed,
|
||||
trimToUndefined(config.baseUrl) ?? normalized.baseUrl,
|
||||
) ?? normalized.speed,
|
||||
instructions: trimToUndefined(config.instructions) ?? normalized.instructions,
|
||||
responseFormat:
|
||||
normalizeOpenAISpeechResponseFormat(config.responseFormat) ?? normalized.responseFormat,
|
||||
@@ -146,6 +162,7 @@ function readOpenAIProviderConfig(config: SpeechProviderConfig): OpenAITtsProvid
|
||||
|
||||
function readOpenAIOverrides(
|
||||
overrides: SpeechProviderOverrides | undefined,
|
||||
baseUrl: string,
|
||||
): OpenAITtsProviderOverrides {
|
||||
if (!overrides) {
|
||||
return {};
|
||||
@@ -153,7 +170,7 @@ function readOpenAIOverrides(
|
||||
return {
|
||||
model: trimToUndefined(overrides.model),
|
||||
voice: trimToUndefined(overrides.voice),
|
||||
speed: asFiniteNumber(overrides.speed),
|
||||
speed: normalizeOpenAISpeechSpeed(overrides.speed, baseUrl),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -232,6 +249,8 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin {
|
||||
resolveTalkConfig: ({ baseTtsConfig, talkProviderConfig }) => {
|
||||
const base = normalizeOpenAIProviderConfig(baseTtsConfig);
|
||||
const responseFormat = normalizeOpenAISpeechResponseFormat(talkProviderConfig.responseFormat);
|
||||
const baseUrl = trimToUndefined(talkProviderConfig.baseUrl) ?? base.baseUrl;
|
||||
const speed = normalizeOpenAISpeechSpeed(talkProviderConfig.speed, baseUrl);
|
||||
return {
|
||||
...base,
|
||||
...(talkProviderConfig.apiKey === undefined
|
||||
@@ -242,18 +261,14 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin {
|
||||
path: "talk.providers.openai.apiKey",
|
||||
}),
|
||||
}),
|
||||
...(trimToUndefined(talkProviderConfig.baseUrl) == null
|
||||
? {}
|
||||
: { baseUrl: trimToUndefined(talkProviderConfig.baseUrl) }),
|
||||
...(trimToUndefined(talkProviderConfig.baseUrl) == null ? {} : { baseUrl }),
|
||||
...(trimToUndefined(talkProviderConfig.modelId) == null
|
||||
? {}
|
||||
: { model: trimToUndefined(talkProviderConfig.modelId) }),
|
||||
...(trimToUndefined(talkProviderConfig.voiceId) == null
|
||||
? {}
|
||||
: { voice: trimToUndefined(talkProviderConfig.voiceId) }),
|
||||
...(asFiniteNumber(talkProviderConfig.speed) == null
|
||||
? {}
|
||||
: { speed: asFiniteNumber(talkProviderConfig.speed) }),
|
||||
...(speed == null ? {} : { speed }),
|
||||
...(trimToUndefined(talkProviderConfig.instructions) == null
|
||||
? {}
|
||||
: { instructions: trimToUndefined(talkProviderConfig.instructions) }),
|
||||
@@ -291,7 +306,7 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin {
|
||||
},
|
||||
synthesize: async (req) => {
|
||||
const config = readOpenAIProviderConfig(req.providerConfig);
|
||||
const overrides = readOpenAIOverrides(req.providerOverrides);
|
||||
const overrides = readOpenAIOverrides(req.providerOverrides, config.baseUrl);
|
||||
const apiKey = config.apiKey || process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("OpenAI API key missing");
|
||||
@@ -322,7 +337,7 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin {
|
||||
},
|
||||
synthesizeTelephony: async (req) => {
|
||||
const config = readOpenAIProviderConfig(req.providerConfig);
|
||||
const overrides = readOpenAIOverrides(req.providerOverrides);
|
||||
const overrides = readOpenAIOverrides(req.providerOverrides, config.baseUrl);
|
||||
const apiKey = config.apiKey || process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("OpenAI API key missing");
|
||||
|
||||
Reference in New Issue
Block a user