From aab1435cb3d1d665de69c8d56cb5a279c0134390 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 8 Jun 2026 14:03:15 +0700 Subject: [PATCH] Fix OpenAI speech streaming after SDK upgrade --- package-lock.json | 2 +- packages/server/package.json | 2 +- .../speech/providers/openai/tts.test.ts | 62 +++++++++++++++++++ .../src/server/speech/providers/openai/tts.ts | 7 ++- 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 packages/server/src/server/speech/providers/openai/tts.test.ts diff --git a/package-lock.json b/package-lock.json index cc9d08748..c8af37911 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38150,7 +38150,7 @@ "version": "0.1.91-beta.2", "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", - "@anthropic-ai/claude-agent-sdk": "0.3.168", + "@anthropic-ai/claude-agent-sdk": "^0.3.168", "@anthropic-ai/sdk": "^0.102.0", "@getpaseo/client": "0.1.91-beta.2", "@getpaseo/highlight": "0.1.91-beta.2", diff --git a/packages/server/package.json b/packages/server/package.json index 14836c098..724976116 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -58,7 +58,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", - "@anthropic-ai/claude-agent-sdk": "0.3.168", + "@anthropic-ai/claude-agent-sdk": "^0.3.168", "@anthropic-ai/sdk": "^0.102.0", "@getpaseo/client": "0.1.91-beta.2", "@getpaseo/highlight": "0.1.91-beta.2", diff --git a/packages/server/src/server/speech/providers/openai/tts.test.ts b/packages/server/src/server/speech/providers/openai/tts.test.ts new file mode 100644 index 000000000..7f9a335b0 --- /dev/null +++ b/packages/server/src/server/speech/providers/openai/tts.test.ts @@ -0,0 +1,62 @@ +import type pino from "pino"; +import { ReadableStream } from "node:stream/web"; +import { beforeEach, describe, expect, test, vi } from "vitest"; + +const openAiMocks = vi.hoisted(() => ({ + createSpeech: vi.fn(), +})); + +vi.mock("openai", () => ({ + OpenAI: vi.fn().mockImplementation( + class { + audio = { + speech: { + create: openAiMocks.createSpeech, + }, + }; + }, + ), +})); + +import { OpenAITTS } from "./tts.js"; + +function createLogger(): pino.Logger { + const logger = { + child: vi.fn(), + debug: vi.fn(), + error: vi.fn(), + info: vi.fn(), + }; + + logger.child.mockReturnValue(logger); + + return logger as unknown as pino.Logger; +} + +describe("OpenAITTS", () => { + beforeEach(() => { + openAiMocks.createSpeech.mockReset(); + }); + + test("returns a Node stream from the OpenAI web response body", async () => { + openAiMocks.createSpeech.mockResolvedValue({ + body: new ReadableStream({ + start(controller) { + controller.enqueue(Buffer.from("audio")); + controller.close(); + }, + }), + }); + + const tts = new OpenAITTS({ apiKey: "sk-test" }, createLogger()); + const result = await tts.synthesizeSpeech("hello"); + + const chunks: Buffer[] = []; + for await (const chunk of result.stream) { + chunks.push(Buffer.from(chunk)); + } + + expect(Buffer.concat(chunks).toString()).toBe("audio"); + expect(result.format).toBe("pcm"); + }); +}); diff --git a/packages/server/src/server/speech/providers/openai/tts.ts b/packages/server/src/server/speech/providers/openai/tts.ts index f7cc7dd39..9813b4760 100644 --- a/packages/server/src/server/speech/providers/openai/tts.ts +++ b/packages/server/src/server/speech/providers/openai/tts.ts @@ -1,6 +1,7 @@ import type pino from "pino"; import { OpenAI } from "openai"; import { Readable } from "node:stream"; +import type { ReadableStream as NodeReadableStream } from "node:stream/web"; import type { SpeechStreamResult, TextToSpeechProvider } from "../../speech-provider.js"; export type { SpeechStreamResult }; @@ -65,7 +66,11 @@ export class OpenAITTS implements TextToSpeechProvider { | "pcm", }); - const audioStream = response.body as unknown as Readable; + if (!response.body) { + throw new Error("OpenAI speech response did not include an audio stream"); + } + + const audioStream = Readable.fromWeb(response.body as NodeReadableStream); const duration = Date.now() - startTime; this.logger.debug({ duration }, "Speech synthesis stream ready");