mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(vydra): ignore blank environment API key (#108757)
* fix(vydra): ignore blank environment API key * refactor(tts): centralize speech API key resolution * chore(plugin-sdk): refresh speech API baseline * chore(plugin-sdk): pin speech resolver surface --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
2713341b4e458d22f23a993423ee60ba20734b0c90f6792134941c5830b863f5 plugin-sdk-api-baseline.json
|
||||
041a52f9ff06f3cdebccf6704960de02142abec68266daff37b0399795c212d1 plugin-sdk-api-baseline.jsonl
|
||||
4a7f00ae294da967f487090775e6d9fde7b1aac80b123618d7ba01186484bcdb plugin-sdk-api-baseline.json
|
||||
6a966808a3e34c6b750e008b901e7170503f3c0788fdb086166ed31866b588e9 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -7,6 +7,7 @@ describe("vydra speech provider", () => {
|
||||
installPinnedHostnameTestHooks();
|
||||
|
||||
const provider = buildVydraSpeechProvider();
|
||||
const originalVydraApiKey = process.env.VYDRA_API_KEY;
|
||||
|
||||
const oversizedJsonResponse = () =>
|
||||
new Response(Buffer.alloc(16 * 1024 * 1024 + 1, 0x20), {
|
||||
@@ -15,6 +16,11 @@ describe("vydra speech provider", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalVydraApiKey === undefined) {
|
||||
delete process.env.VYDRA_API_KEY;
|
||||
} else {
|
||||
process.env.VYDRA_API_KEY = originalVydraApiKey;
|
||||
}
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
@@ -77,6 +83,30 @@ describe("vydra speech provider", () => {
|
||||
expect(result.audioBuffer).toEqual(Buffer.from("mp3-data"));
|
||||
});
|
||||
|
||||
it("does not treat a blank environment API key as configured", () => {
|
||||
process.env.VYDRA_API_KEY = " ";
|
||||
|
||||
expect(provider.isConfigured?.({ providerConfig: {}, timeoutMs: 30_000 })).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects blank environment API keys before making requests", async () => {
|
||||
process.env.VYDRA_API_KEY = "\t \n";
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
await expect(
|
||||
provider.synthesize({
|
||||
text: "OpenClaw test",
|
||||
cfg: {} as never,
|
||||
providerConfig: {},
|
||||
target: "audio-file",
|
||||
timeoutMs: 30_000,
|
||||
}),
|
||||
).rejects.toThrow("Vydra API key missing");
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects generated audio downloads that exceed the configured media cap", async () => {
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
SpeechProviderOverrides,
|
||||
SpeechProviderPlugin,
|
||||
} from "openclaw/plugin-sdk/speech-core";
|
||||
import { asObject } from "openclaw/plugin-sdk/speech-core";
|
||||
import { asObject, resolveSpeechProviderApiKey } from "openclaw/plugin-sdk/speech-core";
|
||||
import {
|
||||
DEFAULT_VYDRA_BASE_URL,
|
||||
DEFAULT_VYDRA_SPEECH_MODEL,
|
||||
@@ -91,11 +91,16 @@ export function buildVydraSpeechProvider(): SpeechProviderPlugin {
|
||||
resolveConfig: ({ rawConfig }) => normalizeVydraSpeechConfig(rawConfig),
|
||||
listVoices: async () => VYDRA_SPEECH_VOICES.map((voice) => Object.assign({}, voice)),
|
||||
isConfigured: ({ providerConfig }) =>
|
||||
Boolean(readVydraSpeechConfig(providerConfig).apiKey || process.env.VYDRA_API_KEY),
|
||||
Boolean(
|
||||
resolveSpeechProviderApiKey(
|
||||
readVydraSpeechConfig(providerConfig).apiKey,
|
||||
process.env.VYDRA_API_KEY,
|
||||
),
|
||||
),
|
||||
synthesize: async (req) => {
|
||||
const config = readVydraSpeechConfig(req.providerConfig);
|
||||
const overrides = readVydraOverrides(req.providerOverrides);
|
||||
const apiKey = config.apiKey || process.env.VYDRA_API_KEY;
|
||||
const apiKey = resolveSpeechProviderApiKey(config.apiKey, process.env.VYDRA_API_KEY);
|
||||
if (!apiKey) {
|
||||
throw new Error("Vydra API key missing");
|
||||
}
|
||||
|
||||
@@ -237,7 +237,8 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
|
||||
// Used-union narrowing: 31 wildcard barrels drop to explicit used exports;
|
||||
// proxy stream API and codex marker/scaffold pins retained.
|
||||
// +2: generic channel retry runner and Retry-After parser.
|
||||
7951,
|
||||
// +1: shared speech-provider API key resolver.
|
||||
7952,
|
||||
env,
|
||||
),
|
||||
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
|
||||
@@ -255,7 +256,8 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
|
||||
// +2: widget HTML document detection and size assertion.
|
||||
// Used-union narrowing of the 31 wildcard barrels.
|
||||
// +2: generic channel retry runner and Retry-After parser.
|
||||
4439,
|
||||
// +1: shared speech-provider API key resolver.
|
||||
4440,
|
||||
env,
|
||||
),
|
||||
publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv(
|
||||
|
||||
@@ -32,6 +32,7 @@ export {
|
||||
normalizeLanguageCode,
|
||||
normalizeSeed,
|
||||
requireInRange,
|
||||
resolveSpeechProviderApiKey,
|
||||
} from "../tts/tts-core.js";
|
||||
export { parseTtsDirectives } from "../tts/directives.js";
|
||||
export { parseSpeechDirectiveNumberOverride } from "../tts/directive-number.js";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import type { AssistantMessage, Model, Usage } from "../llm/types.js";
|
||||
import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js";
|
||||
import type { SpeechModelOverridePolicy } from "./provider-types.js";
|
||||
import { summarizeText } from "./tts-core.js";
|
||||
import { resolveSpeechProviderApiKey, summarizeText } from "./tts-core.js";
|
||||
import type { ResolvedTtsConfig } from "./tts-types.js";
|
||||
|
||||
const modelOverridePolicy: SpeechModelOverridePolicy = {
|
||||
@@ -33,6 +33,13 @@ const usage: Usage = {
|
||||
};
|
||||
|
||||
describe("TTS core", () => {
|
||||
it("resolves the first non-blank speech provider API key", () => {
|
||||
expect(resolveSpeechProviderApiKey(undefined, " \t", " provider-key ", "fallback")).toBe(
|
||||
"provider-key",
|
||||
);
|
||||
expect(resolveSpeechProviderApiKey(undefined, "\n")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clamps oversized summarization timeout timers", async () => {
|
||||
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
|
||||
try {
|
||||
|
||||
@@ -18,6 +18,7 @@ export {
|
||||
normalizeLanguageCode,
|
||||
normalizeSeed,
|
||||
requireInRange,
|
||||
resolveSpeechProviderApiKey,
|
||||
scheduleCleanup,
|
||||
} from "./tts-provider-helpers.js";
|
||||
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
// TTS provider helpers manage provider temp files and output cleanup.
|
||||
import { rmSync } from "node:fs";
|
||||
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
const TEMP_FILE_CLEANUP_DELAY_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
/** Resolve the first non-blank API key in provider-defined precedence order. */
|
||||
export function resolveSpeechProviderApiKey(
|
||||
...candidates: Array<string | undefined>
|
||||
): string | undefined {
|
||||
for (const candidate of candidates) {
|
||||
const apiKey = normalizeOptionalString(candidate);
|
||||
if (apiKey) {
|
||||
return apiKey;
|
||||
}
|
||||
}
|
||||
return 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}`);
|
||||
|
||||
Reference in New Issue
Block a user