@@ -864,6 +864,176 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
864864 ) ;
865865 } ) ;
866866
867+ it ( "preserves autoRespondToAudio from raw provider config" , ( ) => {
868+ const provider = buildOpenAIRealtimeVoiceProvider ( ) ;
869+ const resolved = provider . resolveConfig ?.( {
870+ cfg : { } as never ,
871+ rawConfig : {
872+ providers : {
873+ openai : {
874+ autoRespondToAudio : false ,
875+ } ,
876+ } ,
877+ } ,
878+ } ) ;
879+
880+ expect ( resolved ) . toEqual ( { autoRespondToAudio : false } ) ;
881+ } ) ;
882+
883+ it ( "suppresses server-VAD auto-response during the initial greeting and restores it on response.done" , async ( ) => {
884+ const provider = buildOpenAIRealtimeVoiceProvider ( ) ;
885+ const bridge = provider . createBridge ( {
886+ providerConfig : { apiKey : "sk-test" } , // pragma: allowlist secret
887+ autoRespondToAudio : false ,
888+ restoreAutoRespondToAudioAfterInitialGreeting : true ,
889+ instructions : "Greet the caller." ,
890+ onAudio : vi . fn ( ) ,
891+ onClearAudio : vi . fn ( ) ,
892+ } ) ;
893+ const connecting = bridge . connect ( ) ;
894+ const socket = FakeWebSocket . instances [ 0 ] ;
895+ if ( ! socket ) {
896+ throw new Error ( "expected bridge to create a websocket" ) ;
897+ }
898+
899+ socket . readyState = FakeWebSocket . OPEN ;
900+ socket . emit ( "open" ) ;
901+ socket . emit ( "message" , Buffer . from ( JSON . stringify ( { type : "session.updated" } ) ) ) ;
902+ await connecting ;
903+
904+ expectRecordFields (
905+ requireNestedRecord ( requireSession ( socket , 0 ) , [ "audio" , "input" , "turn_detection" ] ) ,
906+ "initial turn detection" ,
907+ {
908+ create_response : false ,
909+ interrupt_response : false ,
910+ } ,
911+ ) ;
912+
913+ bridge . triggerGreeting ?.( "Say hi for issue 85846." ) ;
914+
915+ const sentTypesAfterGreeting = parseSent ( socket ) . map ( ( event ) => event . type ) ;
916+ expect ( sentTypesAfterGreeting ) . toEqual ( [
917+ "session.update" ,
918+ "conversation.item.create" ,
919+ "response.create" ,
920+ ] ) ;
921+
922+ socket . emit (
923+ "message" ,
924+ Buffer . from ( JSON . stringify ( { type : "response.created" , response : { id : "resp_1" } } ) ) ,
925+ ) ;
926+ socket . emit (
927+ "message" ,
928+ Buffer . from ( JSON . stringify ( { type : "response.done" , response : { id : "resp_1" } } ) ) ,
929+ ) ;
930+
931+ const finalTypes = parseSent ( socket ) . map ( ( event ) => event . type ) ;
932+ expect ( finalTypes ) . toEqual ( [
933+ "session.update" ,
934+ "conversation.item.create" ,
935+ "response.create" ,
936+ "session.update" ,
937+ ] ) ;
938+ expectRecordFields (
939+ requireNestedRecord ( requireSession ( socket , 3 ) , [ "audio" , "input" , "turn_detection" ] ) ,
940+ "restored turn detection" ,
941+ {
942+ create_response : true ,
943+ interrupt_response : true ,
944+ } ,
945+ ) ;
946+
947+ socket . emit (
948+ "message" ,
949+ Buffer . from ( JSON . stringify ( { type : "response.created" , response : { id : "resp_2" } } ) ) ,
950+ ) ;
951+ socket . emit (
952+ "message" ,
953+ Buffer . from ( JSON . stringify ( { type : "response.done" , response : { id : "resp_2" } } ) ) ,
954+ ) ;
955+
956+ expect ( parseSent ( socket ) . filter ( ( event ) => event . type === "session.update" ) ) . toHaveLength ( 2 ) ;
957+ } ) ;
958+
959+ it ( "restores server-VAD auto-response after an initial greeting is cancelled" , async ( ) => {
960+ const provider = buildOpenAIRealtimeVoiceProvider ( ) ;
961+ const bridge = provider . createBridge ( {
962+ providerConfig : { apiKey : "sk-test" } , // pragma: allowlist secret
963+ autoRespondToAudio : false ,
964+ restoreAutoRespondToAudioAfterInitialGreeting : true ,
965+ onAudio : vi . fn ( ) ,
966+ onClearAudio : vi . fn ( ) ,
967+ } ) ;
968+ const connecting = bridge . connect ( ) ;
969+ const socket = FakeWebSocket . instances [ 0 ] ;
970+ if ( ! socket ) {
971+ throw new Error ( "expected bridge to create a websocket" ) ;
972+ }
973+
974+ socket . readyState = FakeWebSocket . OPEN ;
975+ socket . emit ( "open" ) ;
976+ socket . emit ( "message" , Buffer . from ( JSON . stringify ( { type : "session.updated" } ) ) ) ;
977+ await connecting ;
978+
979+ bridge . triggerGreeting ?.( "Say hi." ) ;
980+ socket . emit (
981+ "message" ,
982+ Buffer . from ( JSON . stringify ( { type : "response.created" , response : { id : "resp_1" } } ) ) ,
983+ ) ;
984+ socket . emit (
985+ "message" ,
986+ Buffer . from ( JSON . stringify ( { type : "response.cancelled" , response : { id : "resp_1" } } ) ) ,
987+ ) ;
988+
989+ const finalTypes = parseSent ( socket ) . map ( ( event ) => event . type ) ;
990+ expect ( finalTypes . filter ( ( type ) => type === "session.update" ) ) . toHaveLength ( 2 ) ;
991+ expectRecordFields (
992+ requireNestedRecord ( requireSession ( socket , finalTypes . lastIndexOf ( "session.update" ) ) , [
993+ "audio" ,
994+ "input" ,
995+ "turn_detection" ,
996+ ] ) ,
997+ "restored turn detection" ,
998+ {
999+ create_response : true ,
1000+ interrupt_response : true ,
1001+ } ,
1002+ ) ;
1003+ } ) ;
1004+
1005+ it ( "does not send a restore session.update when restoreAutoRespondToAudioAfterInitialGreeting is not set" , async ( ) => {
1006+ const provider = buildOpenAIRealtimeVoiceProvider ( ) ;
1007+ const bridge = provider . createBridge ( {
1008+ providerConfig : { apiKey : "sk-test" } , // pragma: allowlist secret
1009+ autoRespondToAudio : false ,
1010+ onAudio : vi . fn ( ) ,
1011+ onClearAudio : vi . fn ( ) ,
1012+ } ) ;
1013+ const connecting = bridge . connect ( ) ;
1014+ const socket = FakeWebSocket . instances [ 0 ] ;
1015+ if ( ! socket ) {
1016+ throw new Error ( "expected bridge to create a websocket" ) ;
1017+ }
1018+
1019+ socket . readyState = FakeWebSocket . OPEN ;
1020+ socket . emit ( "open" ) ;
1021+ socket . emit ( "message" , Buffer . from ( JSON . stringify ( { type : "session.updated" } ) ) ) ;
1022+ await connecting ;
1023+
1024+ bridge . triggerGreeting ?.( "Say hi." ) ;
1025+ socket . emit (
1026+ "message" ,
1027+ Buffer . from ( JSON . stringify ( { type : "response.created" , response : { id : "resp_1" } } ) ) ,
1028+ ) ;
1029+ socket . emit (
1030+ "message" ,
1031+ Buffer . from ( JSON . stringify ( { type : "response.done" , response : { id : "resp_1" } } ) ) ,
1032+ ) ;
1033+
1034+ expect ( parseSent ( socket ) . filter ( ( event ) => event . type === "session.update" ) ) . toHaveLength ( 1 ) ;
1035+ } ) ;
1036+
8671037 it ( "can disable realtime response interruption while keeping audio responses enabled" , async ( ) => {
8681038 const provider = buildOpenAIRealtimeVoiceProvider ( ) ;
8691039 const bridge = provider . createBridge ( {
0 commit comments