fix(xai): reject malformed streamed TTS base64 (#111201)

This commit is contained in:
Wynne668
2026-07-19 20:40:53 -04:00
committed by GitHub
parent 8a858c9c65
commit 0074270056
2 changed files with 29 additions and 1 deletions
+22
View File
@@ -281,6 +281,28 @@ describe("xai tts", () => {
await result.release();
});
it("errors and releases the stream for malformed audio base64", async () => {
const resultPromise = xaiTTSStream({
text: "hello",
apiKey: "dummy",
baseUrl: XAI_BASE_URL,
voiceId: "eve",
timeoutMs: 5_000,
});
const ws = FakeWebSocket.instances.at(0);
ws?.emit("open");
const result = await resultPromise;
const reader = result.audioStream.getReader();
ws?.emit("message", JSON.stringify({ type: "audio.delta", delta: "not-base64!" }));
await expect(reader.read()).rejects.toThrow(
"xAI TTS stream returned malformed base64 audio data",
);
expect(ws?.readyState).toBe(FakeWebSocket.CLOSED);
await result.release();
});
it("splits text above xAI's 15,000-character limit into ordered delta frames", async () => {
const text = `${"a".repeat(15_000)}${"b".repeat(15_000)}c`;
const resultPromise = xaiTTSStream({
+7 -1
View File
@@ -1,4 +1,5 @@
// Xai plugin module implements tts behavior.
import { canonicalizeBase64 } from "openclaw/plugin-sdk/media-runtime";
import {
assertOkOrThrowProviderError,
postJsonRequest,
@@ -375,7 +376,12 @@ export async function xaiTTSStream(params: {
if (!encoded) {
return;
}
const chunk = Buffer.from(encoded, "base64");
const canonicalAudio = canonicalizeBase64(encoded);
if (!canonicalAudio) {
failStream(new Error("xAI TTS stream returned malformed base64 audio data"));
return;
}
const chunk = Buffer.from(canonicalAudio, "base64");
totalBytes += chunk.length;
if (totalBytes > maxBytes) {
errorStream?.(new Error(`xAI TTS audio stream exceeds ${maxBytes} bytes`));