
























@@ -91,6 +91,8 @@ type SentRealtimeEvent = {
9191audio?: {
9292input?: {
9393format?: Record<string, unknown>;
94+noise_reduction?: Record<string, unknown>;
95+transcription?: Record<string, unknown>;
9496turn_detection?: {
9597create_response?: boolean;
9698interrupt_response?: boolean;
@@ -100,6 +102,7 @@ type SentRealtimeEvent = {
100102format?: Record<string, unknown>;
101103};
102104};
105+item?: unknown;
103106};
104107};
105108@@ -124,6 +127,7 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
124127});
125128126129afterEach(() => {
130+vi.useRealTimers();
127131vi.unstubAllEnvs();
128132});
129133@@ -247,12 +251,13 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
247251};
248252expect(body.session?.model).toBe("gpt-realtime-2");
249253expect(body.session?.audio?.input).toEqual({
254+noise_reduction: { type: "near_field" },
250255turn_detection: {
251256type: "server_vad",
252257create_response: true,
253258interrupt_response: true,
254259},
255-transcription: { model: "whisper-1" },
260+transcription: { model: "gpt-4o-mini-transcribe" },
256261});
257262expect(session).toMatchObject({
258263provider: "openai",
@@ -422,7 +427,8 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
422427audio: {
423428input: {
424429format: { type: "audio/pcmu" },
425-transcription: { model: "whisper-1" },
430+noise_reduction: { type: "near_field" },
431+transcription: { model: "gpt-4o-mini-transcribe" },
426432},
427433output: {
428434format: { type: "audio/pcmu" },
@@ -806,4 +812,161 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
806812expect(onEvent).toHaveBeenCalledWith({ direction: "client", type: "conversation.item.create" });
807813expect(onEvent).toHaveBeenCalledWith({ direction: "client", type: "response.create" });
808814});
815+816+it("defers manual response.create while a realtime response is active", async () => {
817+const provider = buildOpenAIRealtimeVoiceProvider();
818+const bridge = provider.createBridge({
819+providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
820+onAudio: vi.fn(),
821+onClearAudio: vi.fn(),
822+});
823+const connecting = bridge.connect();
824+const socket = FakeWebSocket.instances[0];
825+if (!socket) {
826+throw new Error("expected bridge to create a websocket");
827+}
828+829+socket.readyState = FakeWebSocket.OPEN;
830+socket.emit("open");
831+socket.emit("message", Buffer.from(JSON.stringify({ type: "session.updated" })));
832+await connecting;
833+socket.emit(
834+"message",
835+Buffer.from(JSON.stringify({ type: "response.created", response: { id: "resp_1" } })),
836+);
837+838+bridge.submitToolResult("call_1", { text: "done" });
839+840+expect(parseSent(socket).slice(-1)).toEqual([
841+{
842+type: "conversation.item.create",
843+item: {
844+type: "function_call_output",
845+call_id: "call_1",
846+output: JSON.stringify({ text: "done" }),
847+},
848+},
849+]);
850+851+socket.emit("message", Buffer.from(JSON.stringify({ type: "response.done" })));
852+853+expect(parseSent(socket).slice(-1)).toEqual([{ type: "response.create" }]);
854+});
855+856+it("drains deferred response.create after response.cancelled", async () => {
857+const provider = buildOpenAIRealtimeVoiceProvider();
858+const bridge = provider.createBridge({
859+providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
860+onAudio: vi.fn(),
861+onClearAudio: vi.fn(),
862+});
863+const connecting = bridge.connect();
864+const socket = FakeWebSocket.instances[0];
865+if (!socket) {
866+throw new Error("expected bridge to create a websocket");
867+}
868+869+socket.readyState = FakeWebSocket.OPEN;
870+socket.emit("open");
871+socket.emit("message", Buffer.from(JSON.stringify({ type: "session.updated" })));
872+await connecting;
873+socket.emit(
874+"message",
875+Buffer.from(JSON.stringify({ type: "response.created", response: { id: "resp_1" } })),
876+);
877+878+bridge.submitToolResult("call_1", { text: "done" });
879+socket.emit("message", Buffer.from(JSON.stringify({ type: "response.cancelled" })));
880+881+expect(parseSent(socket).slice(-1)).toEqual([{ type: "response.create" }]);
882+});
883+884+it("resets deferred response guards after websocket reconnect", async () => {
885+vi.useFakeTimers();
886+const provider = buildOpenAIRealtimeVoiceProvider();
887+const bridge = provider.createBridge({
888+providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
889+onAudio: vi.fn(),
890+onClearAudio: vi.fn(),
891+});
892+const connecting = bridge.connect();
893+const socket = FakeWebSocket.instances[0];
894+if (!socket) {
895+throw new Error("expected bridge to create a websocket");
896+}
897+898+socket.readyState = FakeWebSocket.OPEN;
899+socket.emit("open");
900+socket.emit("message", Buffer.from(JSON.stringify({ type: "session.updated" })));
901+await connecting;
902+socket.emit(
903+"message",
904+Buffer.from(JSON.stringify({ type: "response.created", response: { id: "resp_1" } })),
905+);
906+bridge.submitToolResult("call_1", { text: "done" });
907+908+expect(parseSent(socket).slice(-1)[0]?.type).toBe("conversation.item.create");
909+910+socket.emit("close", 1006, Buffer.from("transient drop"));
911+await vi.advanceTimersByTimeAsync(1000);
912+const reconnectedSocket = FakeWebSocket.instances[1];
913+if (!reconnectedSocket) {
914+throw new Error("expected bridge to reconnect");
915+}
916+917+reconnectedSocket.readyState = FakeWebSocket.OPEN;
918+reconnectedSocket.emit("open");
919+reconnectedSocket.emit("message", Buffer.from(JSON.stringify({ type: "session.updated" })));
920+bridge.sendUserMessage?.("Say hello after reconnect.");
921+922+expect(parseSent(reconnectedSocket).slice(-2)).toEqual([
923+{
924+type: "conversation.item.create",
925+item: {
926+type: "message",
927+role: "user",
928+content: [{ type: "input_text", text: "Say hello after reconnect." }],
929+},
930+},
931+{ type: "response.create" },
932+]);
933+});
934+935+it("turns active-response errors into a deferred response.create retry", async () => {
936+const provider = buildOpenAIRealtimeVoiceProvider();
937+const onError = vi.fn();
938+const bridge = provider.createBridge({
939+providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
940+onAudio: vi.fn(),
941+onClearAudio: vi.fn(),
942+ onError,
943+});
944+const connecting = bridge.connect();
945+const socket = FakeWebSocket.instances[0];
946+if (!socket) {
947+throw new Error("expected bridge to create a websocket");
948+}
949+950+socket.readyState = FakeWebSocket.OPEN;
951+socket.emit("open");
952+socket.emit("message", Buffer.from(JSON.stringify({ type: "session.updated" })));
953+await connecting;
954+955+bridge.submitToolResult("call_1", { text: "done" });
956+socket.emit(
957+"message",
958+Buffer.from(
959+JSON.stringify({
960+type: "error",
961+error: {
962+message: "Conversation already has an active response in progress: resp_1",
963+},
964+}),
965+),
966+);
967+socket.emit("message", Buffer.from(JSON.stringify({ type: "response.done" })));
968+969+expect(onError).not.toHaveBeenCalled();
970+expect(parseSent(socket).slice(-1)).toEqual([{ type: "response.create" }]);
971+});
809972});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。