@@ -303,6 +303,19 @@ export class RealtimeModel extends llm.RealtimeModel {
303303 if ( options . realtimeInputConfig ?. automaticActivityDetection ?. disabled ) {
304304 serverTurnDetection = false ;
305305 }
306+ // Environment variable fallbacks
307+ const apiKey = options . apiKey || process . env . GOOGLE_API_KEY ;
308+ const project = options . project || process . env . GOOGLE_CLOUD_PROJECT ;
309+ const location = options . location || process . env . GOOGLE_CLOUD_LOCATION || 'us-central1' ;
310+ const vertexai = options . vertexai ?? false ;
311+
312+ // Model selection based on API type
313+ const defaultModel = vertexai
314+ ? 'gemini-live-2.5-flash-native-audio'
315+ : 'gemini-2.5-flash-native-audio-preview-12-2025' ;
316+
317+ const model = options . model || defaultModel ;
318+ const mutableSession = ! model . includes ( '3.1' ) ;
306319
307320 super ( {
308321 messageTruncation : false ,
@@ -311,25 +324,14 @@ export class RealtimeModel extends llm.RealtimeModel {
311324 autoToolReplyGeneration : true ,
312325 audioOutput : options . modalities ?. includes ( Modality . AUDIO ) ?? true ,
313326 manualFunctionCalls : false ,
314- midSessionChatCtxUpdate : false ,
315- midSessionInstructionsUpdate : true ,
327+ midSessionChatCtxUpdate : mutableSession ,
328+ midSessionInstructionsUpdate : mutableSession ,
316329 midSessionToolsUpdate : false ,
317330 perResponseToolChoice : false ,
318331 } ) ;
319332
320- // Environment variable fallbacks
321- const apiKey = options . apiKey || process . env . GOOGLE_API_KEY ;
322- const project = options . project || process . env . GOOGLE_CLOUD_PROJECT ;
323- const location = options . location || process . env . GOOGLE_CLOUD_LOCATION || 'us-central1' ;
324- const vertexai = options . vertexai ?? false ;
325-
326- // Model selection based on API type
327- const defaultModel = vertexai
328- ? 'gemini-live-2.5-flash-native-audio'
329- : 'gemini-2.5-flash-native-audio-preview-12-2025' ;
330-
331333 this . _options = {
332- model : options . model || defaultModel ,
334+ model,
333335 apiKey,
334336 voice : options . voice || 'Puck' ,
335337 language : options . language ? normalizeLanguage ( options . language ) : undefined ,
@@ -547,10 +549,41 @@ export class RealtimeSession extends llm.RealtimeSession {
547549 }
548550
549551 async updateInstructions ( instructions : string ) : Promise < void > {
550- if ( this . options . instructions === undefined || this . options . instructions !== instructions ) {
551- this . options . instructions = instructions ;
552- this . markRestartNeeded ( ) ;
552+ if ( this . options . instructions !== undefined && this . options . instructions === instructions ) {
553+ return ;
554+ }
555+
556+ this . options . instructions = instructions ;
557+
558+ const unlock = await this . sessionLock . lock ( ) ;
559+ try {
560+ if ( ! this . activeSession ) {
561+ this . markRestartNeeded ( ) ;
562+ return ;
563+ }
564+ } finally {
565+ unlock ( ) ;
566+ }
567+
568+ if ( ! this . realtimeModel . capabilities . midSessionInstructionsUpdate ) {
569+ return ;
553570 }
571+
572+ this . #logger. debug ( 'Updating instructions mid-session' ) ;
573+ this . sendClientEvent ( {
574+ type : 'content' ,
575+ value : {
576+ turns : [
577+ {
578+ parts : [ { text : instructions } ] ,
579+ // Vertex AI ignores role=None or role="system" and only works with role="model".
580+ // Gemini Live API (non-Vertex) errors on role="system"; role=None works as system role.
581+ role : this . options . vertexai ? 'model' : undefined ,
582+ } ,
583+ ] ,
584+ turnComplete : false ,
585+ } ,
586+ } ) ;
554587 }
555588
556589 async updateChatCtx ( chatCtx : llm . ChatContext ) : Promise < void > {
@@ -609,20 +642,21 @@ export class RealtimeSession extends llm.RealtimeSession {
609642 }
610643 }
611644
612- this . sendClientEvent ( {
613- type : 'content' ,
614- value : {
615- turns : turns as types . Content [ ] ,
616- turnComplete : false ,
617- } ,
618- } ) ;
619- }
620-
621- if ( toolResults ) {
622- this . sendClientEvent ( {
623- type : 'tool_response' ,
624- value : toolResults ,
625- } ) ;
645+ if ( toolResults ) {
646+ this . sendClientEvent ( {
647+ type : 'tool_response' ,
648+ value : toolResults ,
649+ } ) ;
650+ }
651+ if ( this . realtimeModel . capabilities . midSessionChatCtxUpdate ) {
652+ this . sendClientEvent ( {
653+ type : 'content' ,
654+ value : {
655+ turns : turns as types . Content [ ] ,
656+ turnComplete : false ,
657+ } ,
658+ } ) ;
659+ }
626660 }
627661 }
628662
@@ -686,13 +720,11 @@ export class RealtimeSession extends llm.RealtimeSession {
686720 }
687721
688722 async generateReply ( instructions ?: string ) : Promise < llm . GenerationCreatedEvent > {
689- if ( this . options . model === 'gemini-3.1-flash-live-preview' ) {
723+ if ( ! this . realtimeModel . capabilities . midSessionChatCtxUpdate ) {
690724 this . #logger. warn (
691- 'generateReply is not compatible with gemini-3.1-flash-live-preview. Use a Gemini 2.5 live model for voice-agent flows that require programmatic reply generation.' ,
692- ) ;
693- throw new Error (
694- "generateReply is not compatible with 'gemini-3.1-flash-live-preview'; use a Gemini 2.5 live model for voice-agent flows that require programmatic reply generation." ,
725+ `generateReply is not compatible with '${ this . options . model } ' and will be ignored.` ,
695726 ) ;
727+ throw new Error ( `generateReply is not compatible with '${ this . options . model } '` ) ;
696728 }
697729
698730 if ( this . pendingGenerationFut && ! this . pendingGenerationFut . done ) {
0 commit comments