@@ -14,6 +14,7 @@ import type {
1414 Tool ,
1515 PartListUnion ,
1616 GenerateContentConfig ,
17+ GenerateContentParameters ,
1718} from '@google/genai' ;
1819import { ThinkingLevel } from '@google/genai' ;
1920import { toParts } from '../code_assist/converter.js' ;
@@ -47,6 +48,11 @@ import { isFunctionResponse } from '../utils/messageInspectors.js';
4748import { partListUnionToString } from './geminiRequest.js' ;
4849import type { ModelConfigKey } from '../services/modelConfigService.js' ;
4950import { estimateTokenCountSync } from '../utils/tokenCalculation.js' ;
51+ import {
52+ fireAfterModelHook ,
53+ fireBeforeModelHook ,
54+ fireBeforeToolSelectionHook ,
55+ } from './geminiChatHookTriggers.js' ;
5056
5157export enum StreamEventType {
5258 /** A regular content chunk from the API. */
@@ -287,17 +293,17 @@ export class GeminiChat {
287293 this . history . push ( userContent ) ;
288294 const requestContents = this . getHistory ( true ) ;
289295
290- // eslint-disable-next-line @typescript-eslint/no-this-alias
291- const self = this ;
292- return ( async function * ( ) {
296+ const streamWithRetries = async function * (
297+ this : GeminiChat ,
298+ ) : AsyncGenerator < StreamEvent , void , void > {
293299 try {
294300 let lastError : unknown = new Error ( 'Request failed after all retries.' ) ;
295301
296302 let maxAttempts = INVALID_CONTENT_RETRY_OPTIONS . maxAttempts ;
297303 // If we are in Preview Model Fallback Mode, we want to fail fast (1 attempt)
298304 // when probing the Preview Model.
299305 if (
300- self . config . isPreviewModelFallbackMode ( ) &&
306+ this . config . isPreviewModelFallbackMode ( ) &&
301307 model === PREVIEW_GEMINI_MODEL
302308 ) {
303309 maxAttempts = 1 ;
@@ -314,7 +320,7 @@ export class GeminiChat {
314320 generateContentConfig . temperature = 1 ;
315321 }
316322
317- const stream = await self . makeApiCallAndProcessStream (
323+ const stream = await this . makeApiCallAndProcessStream (
318324 model ,
319325 generateContentConfig ,
320326 requestContents ,
@@ -335,7 +341,7 @@ export class GeminiChat {
335341 // Check if we have more attempts left.
336342 if ( attempt < maxAttempts - 1 ) {
337343 logContentRetry (
338- self . config ,
344+ this . config ,
339345 new ContentRetryEvent (
340346 attempt ,
341347 ( error as InvalidStreamError ) . type ,
@@ -363,7 +369,7 @@ export class GeminiChat {
363369 isGemini2Model ( model )
364370 ) {
365371 logContentRetryFailure (
366- self . config ,
372+ this . config ,
367373 new ContentRetryFailureEvent (
368374 maxAttempts ,
369375 ( lastError as InvalidStreamError ) . type ,
@@ -377,15 +383,17 @@ export class GeminiChat {
377383 // We only do this if we didn't bypass Preview Model (i.e. we actually used it).
378384 if (
379385 model === PREVIEW_GEMINI_MODEL &&
380- ! self . config . isPreviewModelBypassMode ( )
386+ ! this . config . isPreviewModelBypassMode ( )
381387 ) {
382- self . config . setPreviewModelFallbackMode ( false ) ;
388+ this . config . setPreviewModelFallbackMode ( false ) ;
383389 }
384390 }
385391 } finally {
386392 streamDoneResolver ! ( ) ;
387393 }
388- } ) ( ) ;
394+ } ;
395+
396+ return streamWithRetries . call ( this ) ;
389397 }
390398
391399 private async makeApiCallAndProcessStream (
@@ -397,7 +405,13 @@ export class GeminiChat {
397405 let effectiveModel = model ;
398406 const contentsForPreviewModel =
399407 this . ensureActiveLoopHasThoughtSignatures ( requestContents ) ;
400- const apiCall = ( ) => {
408+
409+ // Track final request parameters for AfterModel hooks
410+ let lastModelToUse = model ;
411+ let lastConfig : GenerateContentConfig = generateContentConfig ;
412+ let lastContentsToUse : Content [ ] = requestContents ;
413+
414+ const apiCall = async ( ) => {
401415 let modelToUse = getEffectiveModel (
402416 this . config . isInFallbackMode ( ) ,
403417 model ,
@@ -439,14 +453,79 @@ export class GeminiChat {
439453 } ;
440454 delete config . thinkingConfig ?. thinkingLevel ;
441455 }
456+ let contentsToUse =
457+ modelToUse === PREVIEW_GEMINI_MODEL
458+ ? contentsForPreviewModel
459+ : requestContents ;
460+
461+ // Fire BeforeModel and BeforeToolSelection hooks if enabled
462+ const hooksEnabled = this . config . getEnableHooks ( ) ;
463+ const messageBus = this . config . getMessageBus ( ) ;
464+ if ( hooksEnabled && messageBus ) {
465+ // Fire BeforeModel hook
466+ const beforeModelResult = await fireBeforeModelHook ( messageBus , {
467+ model : modelToUse ,
468+ config,
469+ contents : contentsToUse ,
470+ } ) ;
471+
472+ // Check if hook blocked the model call
473+ if ( beforeModelResult . blocked ) {
474+ // Return a synthetic response generator
475+ const syntheticResponse = beforeModelResult . syntheticResponse ;
476+ if ( syntheticResponse ) {
477+ return ( async function * ( ) {
478+ yield syntheticResponse ;
479+ } ) ( ) ;
480+ }
481+ // If blocked without synthetic response, return empty generator
482+ return ( async function * ( ) {
483+ // Empty generator - no response
484+ } ) ( ) ;
485+ }
486+
487+ // Apply modifications from BeforeModel hook
488+ if ( beforeModelResult . modifiedConfig ) {
489+ Object . assign ( config , beforeModelResult . modifiedConfig ) ;
490+ }
491+ if (
492+ beforeModelResult . modifiedContents &&
493+ Array . isArray ( beforeModelResult . modifiedContents )
494+ ) {
495+ contentsToUse = beforeModelResult . modifiedContents as Content [ ] ;
496+ }
497+
498+ // Fire BeforeToolSelection hook
499+ const toolSelectionResult = await fireBeforeToolSelectionHook (
500+ messageBus ,
501+ {
502+ model : modelToUse ,
503+ config,
504+ contents : contentsToUse ,
505+ } ,
506+ ) ;
507+
508+ // Apply tool configuration modifications
509+ if ( toolSelectionResult . toolConfig ) {
510+ config . toolConfig = toolSelectionResult . toolConfig ;
511+ }
512+ if (
513+ toolSelectionResult . tools &&
514+ Array . isArray ( toolSelectionResult . tools )
515+ ) {
516+ config . tools = toolSelectionResult . tools as Tool [ ] ;
517+ }
518+ }
519+
520+ // Track final request parameters for AfterModel hooks
521+ lastModelToUse = modelToUse ;
522+ lastConfig = config ;
523+ lastContentsToUse = contentsToUse ;
442524
443525 return this . config . getContentGenerator ( ) . generateContentStream (
444526 {
445527 model : modelToUse ,
446- contents :
447- modelToUse === PREVIEW_GEMINI_MODEL
448- ? contentsForPreviewModel
449- : requestContents ,
528+ contents : contentsToUse ,
450529 config,
451530 } ,
452531 prompt_id ,
@@ -470,7 +549,18 @@ export class GeminiChat {
470549 : undefined ,
471550 } ) ;
472551
473- return this . processStreamResponse ( effectiveModel , streamResponse ) ;
552+ // Store the original request for AfterModel hooks
553+ const originalRequest : GenerateContentParameters = {
554+ model : lastModelToUse ,
555+ config : lastConfig ,
556+ contents : lastContentsToUse ,
557+ } ;
558+
559+ return this . processStreamResponse (
560+ effectiveModel ,
561+ streamResponse ,
562+ originalRequest ,
563+ ) ;
474564 }
475565
476566 /**
@@ -624,6 +714,7 @@ export class GeminiChat {
624714 private async * processStreamResponse (
625715 model : string ,
626716 streamResponse : AsyncGenerator < GenerateContentResponse > ,
717+ originalRequest : GenerateContentParameters ,
627718 ) : AsyncGenerator < GenerateContentResponse > {
628719 const modelResponseParts : Part [ ] = [ ] ;
629720
@@ -663,7 +754,19 @@ export class GeminiChat {
663754 }
664755 }
665756
666- yield chunk ; // Yield every chunk to the UI immediately.
757+ // Fire AfterModel hook through MessageBus (only if hooks are enabled)
758+ const hooksEnabled = this . config . getEnableHooks ( ) ;
759+ const messageBus = this . config . getMessageBus ( ) ;
760+ if ( hooksEnabled && messageBus && originalRequest && chunk ) {
761+ const hookResult = await fireAfterModelHook (
762+ messageBus ,
763+ originalRequest ,
764+ chunk ,
765+ ) ;
766+ yield hookResult . response ;
767+ } else {
768+ yield chunk ; // Yield every chunk to the UI immediately.
769+ }
667770 }
668771
669772 // String thoughts and consolidate text parts.
0 commit comments