Skip to content

Commit c45bc66

Browse files
committed
fix: preserve realtime output audio configuration
1 parent ead88d4 commit c45bc66

4 files changed

Lines changed: 47 additions & 12 deletions

File tree

src/__tests__/multimedia-record.test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ describe("multimedia record: transcription response detection", () => {
285285
it("persists a typed terminal stream when the client closes before upstream end", async () => {
286286
const fixturePath = makeTmpDir();
287287
const terminal =
288-
'data: {"type":"transcript.text.done","text":"Typed terminal","languages":[{"code":"en"}]}\n\n';
288+
'data: {"type":"transcript.text.done","text":"Typed terminal","languages":[{"code":"en"}],"usage":{"input_tokens":4,"output_tokens":5}}\n\n';
289289
const { server: upstream, url } = await createUpstream((_req, res) => {
290290
res.writeHead(200, { "Content-Type": "text/event-stream" });
291291
res.write(terminal);
@@ -307,11 +307,30 @@ describe("multimedia record: transcription response detection", () => {
307307
await new Promise((resolve) => setTimeout(resolve, 250));
308308

309309
expect(fixtures).toHaveLength(1);
310+
expect(fixtures[0].response).toEqual({
311+
transcription: {
312+
text: "Typed terminal",
313+
languages: [{ code: "en" }],
314+
usage: { input_tokens: 4, output_tokens: 5 },
315+
},
316+
});
317+
const savedFixture = JSON.parse(
318+
fs.readFileSync(
319+
path.join(
320+
fixturePath,
321+
fs.readdirSync(fixturePath).find((file) => file.endsWith(".json"))!,
322+
),
323+
"utf8",
324+
),
325+
);
326+
expect(savedFixture.fixtures[0].response).toEqual(fixtures[0].response);
310327
await closeServer(upstream);
311328

312329
const replay = await requestStreamingTranscription(recorder.url);
313330
expect(replay.status).toBe(200);
314-
expect(await replay.text()).toContain('"text":"Typed terminal"');
331+
expect(await replay.text()).toContain(
332+
'"text":"Typed terminal","languages":[{"code":"en"}],"usage":{"input_tokens":4,"output_tokens":5}',
333+
);
315334
} finally {
316335
await closeServer(recorder.server);
317336
if (upstream.listening) await closeServer(upstream);

src/__tests__/ws-api-conformance.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,14 +690,23 @@ describe("GA Realtime conformance", () => {
690690
ws.send(
691691
JSON.stringify({
692692
type: "session.update",
693-
session: { audio: { input: { format: { type: "audio/pcm", rate: 24000 } } } },
693+
session: {
694+
audio: {
695+
input: { format: { type: "audio/pcm", rate: 24000 } },
696+
output: { voice: "alloy", format: { type: "audio/pcm", rate: 24000 } },
697+
},
698+
},
694699
}),
695700
);
696701
const raw = await ws.waitForMessages(2);
697702
ws.close();
698703
const frame = JSON.parse(raw[1]) as any;
699704
expect(frame.type).toBe("session.updated");
700705
expect(frame.session.audio.input.format).toEqual({ type: "audio/pcm", rate: 24000 });
706+
expect(frame.session.audio.output).toEqual({
707+
voice: "alloy",
708+
format: { type: "audio/pcm", rate: 24000 },
709+
});
701710
});
702711
});
703712

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ export type RealtimePhase = "final_answer" | "commentary";
473473
export interface GASessionAudioConfig {
474474
voice: string | null;
475475
input_audio_format: { type: string; rate?: number; [key: string]: unknown } | null;
476-
output_audio_format: string | null;
476+
output_audio_format: { type: string; rate?: number; [key: string]: unknown } | null;
477477
input_audio_noise_reduction: { type: string } | null;
478478
input_audio_transcription: { model: string } | null;
479479
}

src/ws-realtime.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ interface SessionConfig {
6161
tools: unknown[];
6262
voice: string | null;
6363
input_audio_format: Record<string, unknown> | null;
64-
output_audio_format: string | null;
64+
output_audio_format: Record<string, unknown> | null;
6565
input_audio_noise_reduction: { type: string } | null;
6666
input_audio_transcription: { model: string; language?: string; prompt?: string } | null;
6767
turn_detection: unknown | null;
@@ -93,7 +93,7 @@ function isLiveTranscriptionModel(model: string): boolean {
9393
);
9494
}
9595

96-
function normalizeInputAudioFormat(value: unknown): Record<string, unknown> | null {
96+
function normalizeAudioFormat(value: unknown): Record<string, unknown> | null {
9797
if (value === null) return null;
9898
if (typeof value === "string") return { type: value };
9999
if (
@@ -137,7 +137,7 @@ function serializeSession(session: SessionConfig, sessionId?: string): Record<st
137137
turn_detection: session.turn_detection,
138138
},
139139
output: {
140-
format: session.output_audio_format ? { type: session.output_audio_format } : null,
140+
format: session.output_audio_format,
141141
voice: session.voice,
142142
},
143143
},
@@ -550,9 +550,9 @@ async function processMessage(
550550
const audio = (s as Record<string, unknown>).audio as Record<string, unknown>;
551551
if (audio.voice !== undefined) session.voice = audio.voice as string | null;
552552
if (audio.input_audio_format !== undefined)
553-
session.input_audio_format = normalizeInputAudioFormat(audio.input_audio_format);
553+
session.input_audio_format = normalizeAudioFormat(audio.input_audio_format);
554554
if (audio.output_audio_format !== undefined)
555-
session.output_audio_format = audio.output_audio_format as string | null;
555+
session.output_audio_format = normalizeAudioFormat(audio.output_audio_format);
556556
if (audio.input_audio_noise_reduction !== undefined)
557557
session.input_audio_noise_reduction = audio.input_audio_noise_reduction as {
558558
type: string;
@@ -577,14 +577,21 @@ async function processMessage(
577577
session.input_audio_noise_reduction = input.noise_reduction as { type: string } | null;
578578
if (input.turn_detection !== undefined) session.turn_detection = input.turn_detection;
579579
if (input.format !== undefined)
580-
session.input_audio_format = normalizeInputAudioFormat(input.format);
580+
session.input_audio_format = normalizeAudioFormat(input.format);
581+
}
582+
if (audio.output && typeof audio.output === "object") {
583+
const output = audio.output as Record<string, unknown>;
584+
if (output.voice !== undefined) session.voice = output.voice as string | null;
585+
if (output.format !== undefined)
586+
session.output_audio_format = normalizeAudioFormat(output.format);
581587
}
582588
}
583589
// Beta flat fields (backward compat)
584590
if (s.voice !== undefined) session.voice = s.voice;
585591
if (s.input_audio_format !== undefined)
586-
session.input_audio_format = normalizeInputAudioFormat(s.input_audio_format);
587-
if (s.output_audio_format !== undefined) session.output_audio_format = s.output_audio_format;
592+
session.input_audio_format = normalizeAudioFormat(s.input_audio_format);
593+
if (s.output_audio_format !== undefined)
594+
session.output_audio_format = normalizeAudioFormat(s.output_audio_format);
588595
if (s.input_audio_noise_reduction !== undefined)
589596
session.input_audio_noise_reduction = s.input_audio_noise_reduction;
590597
if (s.input_audio_transcription !== undefined)

0 commit comments

Comments
 (0)