From d2e9fa22bca1818869cf85a90ceb01ee881f84c3 Mon Sep 17 00:00:00 2001 From: lsr911 Date: Thu, 16 Jul 2026 21:29:47 +0800 Subject: [PATCH] fix(gradium): treat blank env API key as unconfigured in synthesis paths (#108800) * fix(gradium): treat blank env API key as unconfigured in synthesis paths The isGradiumProviderConfigured helper already uses trimToUndefined on GRADIUM_API_KEY, but synthesize and synthesizeTelephony still used the bare env var. This made whitespace-only credentials pass the missing-key guard and send unusable bearer tokens. - Wrap process.env.GRADIUM_API_KEY with trimToUndefined in synthesize - Wrap process.env.GRADIUM_API_KEY with trimToUndefined in synthesizeTelephony - Add test for whitespace-only env key rejection in synthesize This completes the blank-credential guard for the Gradium provider, matching the pattern from OpenAI (#108212), Xiaomi (#108558), Deepgram (#108565), and Inworld (#108783). Co-Authored-By: Claude * fix(gradium): centralize speech API key resolution --------- Co-authored-by: Claude Co-authored-by: Peter Steinberger --- extensions/gradium/speech-provider.test.ts | 33 ++++++++++++++++++++++ extensions/gradium/speech-provider.ts | 11 ++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/extensions/gradium/speech-provider.test.ts b/extensions/gradium/speech-provider.test.ts index 768132c1353b..8cf9cd38f165 100644 --- a/extensions/gradium/speech-provider.test.ts +++ b/extensions/gradium/speech-provider.test.ts @@ -18,6 +18,7 @@ describe("gradium speech provider", () => { afterEach(() => { vi.unstubAllGlobals(); + vi.unstubAllEnvs(); vi.restoreAllMocks(); }); @@ -213,4 +214,36 @@ describe("gradium speech provider", () => { } } }); + + it("rejects a blank environment key before normal or telephony requests", async () => { + vi.stubEnv("GRADIUM_API_KEY", " "); + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + + expect(provider.isConfigured({ providerConfig: {}, timeoutMs: 5_000 })).toBe(false); + await expect( + provider.synthesize({ + text: "test", + cfg: {} as never, + providerConfig: {}, + target: "audio-file", + timeoutMs: 5_000, + }), + ).rejects.toThrow("Gradium API key missing"); + + const synthesizeTelephony = provider.synthesizeTelephony; + if (!synthesizeTelephony) { + throw new Error("Expected Gradium provider synthesizeTelephony"); + } + await expect( + synthesizeTelephony({ + text: "test", + cfg: {} as never, + providerConfig: {}, + timeoutMs: 5_000, + }), + ).rejects.toThrow("Gradium API key missing"); + + expect(fetchMock).not.toHaveBeenCalled(); + }); }); diff --git a/extensions/gradium/speech-provider.ts b/extensions/gradium/speech-provider.ts index 3aec7344440a..241883a192b4 100644 --- a/extensions/gradium/speech-provider.ts +++ b/extensions/gradium/speech-provider.ts @@ -6,6 +6,7 @@ import type { SpeechProviderPlugin, } from "openclaw/plugin-sdk/speech"; import { asObject, trimToUndefined } from "openclaw/plugin-sdk/speech"; +import { resolveSpeechProviderApiKey } from "openclaw/plugin-sdk/speech-core"; import { DEFAULT_GRADIUM_VOICE_ID, GRADIUM_VOICES, normalizeGradiumBaseUrl } from "./shared.js"; import { gradiumTTS } from "./tts.js"; @@ -39,8 +40,12 @@ function readGradiumProviderConfig(config: SpeechProviderConfig): GradiumProvide }; } +function resolveGradiumApiKey(configApiKey: unknown): string | undefined { + return resolveSpeechProviderApiKey(trimToUndefined(configApiKey), process.env.GRADIUM_API_KEY); +} + function isGradiumProviderConfigured(config: SpeechProviderConfig): boolean { - const apiKey = trimToUndefined(config.apiKey) ?? trimToUndefined(process.env.GRADIUM_API_KEY); + const apiKey = resolveGradiumApiKey(config.apiKey); if (!apiKey) { return false; } @@ -99,7 +104,7 @@ export function buildGradiumSpeechProvider(): SpeechProviderPlugin { synthesize: async (req) => { const config = readGradiumProviderConfig(req.providerConfig); const overrides = req.providerOverrides ?? {}; - const apiKey = config.apiKey || process.env.GRADIUM_API_KEY; + const apiKey = resolveGradiumApiKey(config.apiKey); if (!apiKey) { throw new Error("Gradium API key missing"); } @@ -124,7 +129,7 @@ export function buildGradiumSpeechProvider(): SpeechProviderPlugin { synthesizeTelephony: async (req) => { const config = readGradiumProviderConfig(req.providerConfig); const overrides = req.providerOverrides ?? {}; - const apiKey = config.apiKey || process.env.GRADIUM_API_KEY; + const apiKey = resolveGradiumApiKey(config.apiKey); if (!apiKey) { throw new Error("Gradium API key missing"); }