Skip to content

Commit 9302c80

Browse files
fix(openai): route mp3 tts as voice messages (#100715)
Co-authored-by: Hemantsudarshan <[email protected]>
1 parent a118b32 commit 9302c80

3 files changed

Lines changed: 32 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
2323
### Fixes
2424

2525
- **OpenAI Realtime Codex auth:** reuse external Codex OAuth profiles for Realtime voice sessions when no explicit OpenAI API key is configured.
26+
- **OpenAI-compatible TTS voice notes:** route configured MP3 speech output through native voice-message delivery when the channel supports it, while keeping WAV output on the audio-file path. (#83227, #80317) Thanks @HemantSudarshan.
2627
- **Talk transcription providers:** cold-load explicitly configured Voice Call streaming providers, including runtime aliases, when another provider registry is already active, keeping catalog and session selection aligned. (#97170, #97738) Thanks @solavrc.
2728
- **Android Canvas navigation:** block device-local loopback and unspecified main-frame web targets across direct, user, JavaScript, and redirect navigation while preserving remote, LAN, emulator-host, and bundled canvases. (#99874) Thanks @ly85206559.
2829
- **Android network recovery:** reconnect Gateway sessions immediately when Android regains a validated network instead of waiting for the current reconnect backoff. (#100347) Thanks @ly85206559.

extensions/openai/speech-provider.test.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -535,28 +535,33 @@ describe("buildOpenAISpeechProvider", () => {
535535
expect(fetchMock).toHaveBeenCalledTimes(1);
536536
});
537537

538-
it("honors explicit responseFormat overrides and clears voice-note compatibility when not opus", async () => {
539-
const provider = buildOpenAISpeechProvider();
540-
mockSpeechFetchExpectingFormat("wav");
538+
it.each([
539+
{ responseFormat: "wav" as const, voiceCompatible: false },
540+
{ responseFormat: "mp3" as const, voiceCompatible: true },
541+
])(
542+
"marks configured $responseFormat voice-note compatibility as $voiceCompatible",
543+
async ({ responseFormat, voiceCompatible }) => {
544+
const provider = buildOpenAISpeechProvider();
545+
mockSpeechFetchExpectingFormat(responseFormat);
541546

542-
const result = await provider.synthesize({
543-
text: "hello",
544-
cfg: {} as never,
545-
providerConfig: {
546-
apiKey: "sk-test",
547-
baseUrl: "https://proxy.example.com/openai/v1",
548-
model: "canopylabs/orpheus-v1-english",
549-
voice: "daniel",
550-
responseFormat: "wav",
551-
},
552-
target: "voice-note",
553-
timeoutMs: 1_000,
554-
});
547+
const result = await provider.synthesize({
548+
text: "hello",
549+
cfg: {} as never,
550+
providerConfig: {
551+
apiKey: "sk-test",
552+
model: "gpt-4o-mini-tts",
553+
voice: "alloy",
554+
responseFormat,
555+
},
556+
target: "voice-note",
557+
timeoutMs: 1_000,
558+
});
555559

556-
expect(result.outputFormat).toBe("wav");
557-
expect(result.fileExtension).toBe(".wav");
558-
expect(result.voiceCompatible).toBe(false);
559-
});
560+
expect(result.outputFormat).toBe(responseFormat);
561+
expect(result.fileExtension).toBe(`.${responseFormat}`);
562+
expect(result.voiceCompatible).toBe(voiceCompatible);
563+
},
564+
);
560565

561566
it("passes extra_body config through to OpenAI-compatible speech requests", async () => {
562567
const provider = buildOpenAISpeechProvider();

extensions/openai/speech-provider.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Openai provider module implements model/runtime integration.
2+
import { isVoiceMessageCompatibleAudio } from "openclaw/plugin-sdk/media-runtime";
23
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
34
import type {
45
SpeechDirectiveTokenParseContext,
@@ -364,11 +365,14 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin {
364365
timeoutMs: req.timeoutMs,
365366
maxBytes: resolveGeneratedAudioMaxBytes(req),
366367
});
368+
const fileExtension = responseFormatToFileExtension(responseFormat);
367369
return {
368370
audioBuffer,
369371
outputFormat: responseFormat,
370-
fileExtension: responseFormatToFileExtension(responseFormat),
371-
voiceCompatible: req.target === "voice-note" && responseFormat === "opus",
372+
fileExtension,
373+
voiceCompatible:
374+
req.target === "voice-note" &&
375+
isVoiceMessageCompatibleAudio({ fileName: `speech${fileExtension}` }),
372376
};
373377
},
374378
synthesizeTelephony: async (req) => {

0 commit comments

Comments
 (0)