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 <noreply@anthropic.com>

* fix(gradium): centralize speech API key resolution

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
lsr911
2026-07-16 21:29:47 +08:00
committed by GitHub
parent 990d673083
commit d2e9fa22bc
2 changed files with 41 additions and 3 deletions

View File

@@ -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();
});
});

View File

@@ -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");
}