mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(openai): treat blank env API key as unconfigured in speech provider (#108212)
* fix(openai): treat blank env API key as unconfigured in speech provider process.env.OPENAI_API_KEY set to whitespace-only (e.g. ' ') causes isConfigured to return true, but downstream synthesis fails with an unusable bearer token. Add ?.trim() to treat whitespace-only env keys as missing, matching the existing normalization in the image-generation provider (image-generation-provider.ts:347). Fixes #108186 * test(openai): cover speech env key normalization Co-authored-by: luyifan <al3060388206@gmail.com> * fix(openai): normalize realtime transcription env key * chore(ci): prune stale max-lines baseline --------- Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: luyifan <al3060388206@gmail.com> Co-authored-by: Peter Steinberger <peter@steipete.me>
This commit is contained in:
co-authored by
Peter Steinberger
luyifan
Peter Steinberger
parent
0f7fbfa003
commit
4108bbca96
@@ -216,6 +216,13 @@ describe("buildOpenAIRealtimeTranscriptionProvider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not treat a whitespace-only environment API key as configured", () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", " ");
|
||||
const provider = buildOpenAIRealtimeTranscriptionProvider();
|
||||
|
||||
expect(provider.isConfigured({ cfg: {} as never, providerConfig: {} })).toBe(false);
|
||||
});
|
||||
|
||||
it("mints an API-key client secret for realtime transcription sockets", async () => {
|
||||
const provider = buildOpenAIRealtimeTranscriptionProvider();
|
||||
const release = vi.fn();
|
||||
|
||||
@@ -268,7 +268,7 @@ export function buildOpenAIRealtimeTranscriptionProvider(): RealtimeTranscriptio
|
||||
isConfigured: ({ cfg, providerConfig }) =>
|
||||
Boolean(
|
||||
normalizeProviderConfig(providerConfig).apiKey ||
|
||||
process.env.OPENAI_API_KEY ||
|
||||
process.env.OPENAI_API_KEY?.trim() ||
|
||||
isProviderAuthProfileConfigured({ provider: "openai", cfg, profileTypes: ["api_key"] }),
|
||||
),
|
||||
createSession: (req) => {
|
||||
|
||||
@@ -58,6 +58,7 @@ describe("buildOpenAISpeechProvider", () => {
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -376,6 +377,68 @@ describe("buildOpenAISpeechProvider", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("treats a blank environment API key as missing across speech entry points", async () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", " ");
|
||||
const provider = buildOpenAISpeechProvider();
|
||||
const fetchMock = vi.fn(async () => new Response(new Uint8Array([1, 2, 3]), { status: 200 }));
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||
const providerConfig = {
|
||||
model: "gpt-4o-mini-tts",
|
||||
voice: "alloy",
|
||||
};
|
||||
|
||||
expect(provider.isConfigured({ providerConfig, timeoutMs: 30_000 })).toBe(false);
|
||||
await expect(
|
||||
provider.synthesize({
|
||||
text: "hello",
|
||||
cfg: {} as never,
|
||||
providerConfig,
|
||||
target: "audio-file",
|
||||
timeoutMs: 1_000,
|
||||
}),
|
||||
).rejects.toThrow("OpenAI API key missing");
|
||||
await expect(
|
||||
provider.synthesizeTelephony?.({
|
||||
text: "hello",
|
||||
cfg: {} as never,
|
||||
providerConfig,
|
||||
timeoutMs: 1_000,
|
||||
}),
|
||||
).rejects.toThrow("OpenAI API key missing");
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("trims a valid environment API key for normal and telephony synthesis", async () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", " sk-env ");
|
||||
const provider = buildOpenAISpeechProvider();
|
||||
const authorizationHeaders: Array<string | null> = [];
|
||||
globalThis.fetch = vi.fn(async (_url: string, init?: RequestInit) => {
|
||||
authorizationHeaders.push(new Headers(init?.headers).get("authorization"));
|
||||
return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
|
||||
}) as unknown as typeof fetch;
|
||||
const providerConfig = {
|
||||
model: "gpt-4o-mini-tts",
|
||||
voice: "alloy",
|
||||
};
|
||||
|
||||
expect(provider.isConfigured({ providerConfig, timeoutMs: 30_000 })).toBe(true);
|
||||
await provider.synthesize({
|
||||
text: "hello",
|
||||
cfg: {} as never,
|
||||
providerConfig,
|
||||
target: "audio-file",
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
await provider.synthesizeTelephony?.({
|
||||
text: "hello",
|
||||
cfg: {} as never,
|
||||
providerConfig,
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
|
||||
expect(authorizationHeaders).toEqual(["Bearer sk-env", "Bearer sk-env"]);
|
||||
});
|
||||
|
||||
it("preserves talk responseFormat overrides", () => {
|
||||
const provider = buildOpenAISpeechProvider();
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ type OpenAITtsProviderOverrides = {
|
||||
speed?: number;
|
||||
};
|
||||
|
||||
function resolveOpenAISpeechApiKey(config: OpenAITtsProviderConfig): string | undefined {
|
||||
return trimToUndefined(config.apiKey) ?? trimToUndefined(process.env.OPENAI_API_KEY);
|
||||
}
|
||||
|
||||
function normalizeOpenAISpeechResponseFormat(
|
||||
value: unknown,
|
||||
): OpenAiSpeechResponseFormat | undefined {
|
||||
@@ -322,7 +326,7 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin {
|
||||
}),
|
||||
listVoices: async () => OPENAI_TTS_VOICES.map((voice) => ({ id: voice, name: voice })),
|
||||
isConfigured: ({ providerConfig }) =>
|
||||
Boolean(readOpenAIProviderConfig(providerConfig).apiKey || process.env.OPENAI_API_KEY),
|
||||
Boolean(resolveOpenAISpeechApiKey(readOpenAIProviderConfig(providerConfig))),
|
||||
prepareSynthesis: (ctx) => {
|
||||
const config = readOpenAIProviderConfig(ctx.providerConfig);
|
||||
if (config.instructions) {
|
||||
@@ -343,7 +347,7 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin {
|
||||
synthesize: async (req) => {
|
||||
const config = readOpenAIProviderConfig(req.providerConfig);
|
||||
const overrides = readOpenAIOverrides(req.providerOverrides, config.baseUrl);
|
||||
const apiKey = config.apiKey || process.env.OPENAI_API_KEY;
|
||||
const apiKey = resolveOpenAISpeechApiKey(config);
|
||||
if (!apiKey) {
|
||||
throw new Error("OpenAI API key missing");
|
||||
}
|
||||
@@ -378,7 +382,7 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin {
|
||||
synthesizeTelephony: async (req) => {
|
||||
const config = readOpenAIProviderConfig(req.providerConfig);
|
||||
const overrides = readOpenAIOverrides(req.providerOverrides, config.baseUrl);
|
||||
const apiKey = config.apiKey || process.env.OPENAI_API_KEY;
|
||||
const apiKey = resolveOpenAISpeechApiKey(config);
|
||||
if (!apiKey) {
|
||||
throw new Error("OpenAI API key missing");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user