@@ -52,12 +52,15 @@ public async Task ConnectToModel(
5252 routing . Context . SetDialogs ( dialogs ) ;
5353 routing . Context . SetMessageId ( _conn . ConversationId , Guid . Empty . ToString ( ) ) ;
5454
55- var ( provider , model ) = GetLlmProviderModel ( agent ) ;
55+ var ( provider , model , isLive ) = GetLlmProviderModel ( agent ) ;
5656
57- _completer = _services . GetServices < IRealTimeCompletion > ( ) . First ( x => x . Provider == provider ) ;
57+ _completer = ResolveCompleter ( provider , isLive ) ;
5858 _completer . SetModelName ( model ) ;
5959 _completer . SetOptions ( options ) ;
6060
61+ // Lets a provider stream display-only events, such as partial transcripts.
62+ _conn . SendEventToUser = responseToUser ;
63+
6164 await _completer . Connect (
6265 conn : _conn ,
6366 onModelReady : async ( ) =>
@@ -189,27 +192,54 @@ public RealtimeHubConnection SetHubConnection(string conversationId)
189192 return _conn ;
190193 }
191194
192- private ( string , string ) GetLlmProviderModel ( Agent agent )
195+ /// <summary>
196+ /// Picks the voice model for this conversation, and which family it belongs to.
197+ ///
198+ /// The agent's live config wins when it is set, because a live session is a deliberate
199+ /// choice rather than a variation of a realtime one. Everything else resolves to realtime:
200+ /// RealtimeModelSettings describes a realtime provider, so a live provider is never reached
201+ /// by fallback, only by an agent asking for it.
202+ /// </summary>
203+ private ( string provider , string model , bool isLive ) GetLlmProviderModel ( Agent agent )
193204 {
194- var provider = agent ? . LlmConfig ? . Realtime ? . Provider ;
195- var model = agent ? . LlmConfig ? . Realtime ? . Model ;
196- var settingService = _services . GetRequiredService < ISettingService > ( ) ;
205+ var live = agent ? . LlmConfig ? . Live ;
206+ if ( live ? . IsValid == true )
207+ {
208+ return ( live . Provider ! , live . Model ! , true ) ;
209+ }
197210
198- if ( ! string . IsNullOrEmpty ( provider ) && ! string . IsNullOrEmpty ( model ) )
211+ var realtime = agent ? . LlmConfig ? . Realtime ;
212+ if ( realtime ? . IsValid == true )
199213 {
200- return ( provider , model ) ;
214+ return ( realtime . Provider ! , realtime . Model ! , false ) ;
201215 }
202216
203- provider = _settings . Provider ;
204- model = _settings . Model ;
217+ if ( ! string . IsNullOrEmpty ( _settings . Provider ) && ! string . IsNullOrEmpty ( _settings . Model ) )
218+ {
219+ return ( _settings . Provider , _settings . Model , false ) ;
220+ }
221+
222+ return ( "openai" , "gpt-realtime" , false ) ;
223+ }
224+
225+ /// <summary>
226+ /// Live providers register as <see cref="ILiveCompletion"/> only, so the two families are
227+ /// resolved from separate DI lists and a provider name is looked up in one of them, never
228+ /// both. A live completer still drives the hub as a realtime one - the interface derives
229+ /// from <see cref="IRealTimeCompletion"/> - so nothing downstream has to know the difference.
230+ /// </summary>
231+ private IRealTimeCompletion ResolveCompleter ( string provider , bool isLive )
232+ {
233+ var completer = isLive
234+ ? _services . GetServices < ILiveCompletion > ( ) . FirstOrDefault ( x => x . Provider == provider )
235+ : _services . GetServices < IRealTimeCompletion > ( ) . FirstOrDefault ( x => x . Provider == provider ) ;
205236
206- if ( ! string . IsNullOrEmpty ( provider ) && ! string . IsNullOrEmpty ( model ) )
237+ if ( completer == null )
207238 {
208- return ( provider , model ) ;
239+ var family = isLive ? "live" : "realtime" ;
240+ throw new InvalidOperationException ( $ "Can't resolve the { family } completion provider by \" { provider } \" .") ;
209241 }
210242
211- provider = "openai" ;
212- model = "gpt-realtime" ;
213- return ( provider , model ) ;
243+ return completer ;
214244 }
215245}
0 commit comments