From 81e233ca30f28f6ae5c9faf981f90996e373146d Mon Sep 17 00:00:00 2001 From: Jerop Kipruto Date: Mon, 13 Apr 2026 13:28:00 -0400 Subject: [PATCH 1/2] feat(core): implement silent fallback for Plan Mode model routing This change introduces a 'Unified Injection' approach to enforce silent fallback when the system is in Plan Mode. By intercepting the ModelPolicyChain resolution, all fallback actions are overridden to 'silent', ensuring that failures in the primary planning model (Pro) automatically trigger a transition to the next available model (Flash) without interrupting the user. - Exported SILENT_ACTIONS from policyCatalog.ts - Refactored resolvePolicyChain in policyHelpers.ts to apply silent overrides - Added unit tests to verify silent fallback in Plan Mode --- docs/cli/plan-mode.md | 4 + .../core/src/availability/policyCatalog.ts | 2 +- .../src/availability/policyHelpers.test.ts | 15 +++- .../core/src/availability/policyHelpers.ts | 81 ++++++++++--------- 4 files changed, 64 insertions(+), 38 deletions(-) diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index f5532a07ca8..7213d329f85 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -441,6 +441,10 @@ on the current phase of your task: switches to a high-speed **Flash** model. This provides a faster, more responsive experience during the implementation of the plan. +If the high-reasoning model is unavailable or you don't have access to it, +Gemini CLI automatically and silently falls back to a faster model to ensure +your workflow isn't interrupted. + This behavior is enabled by default to provide the best balance of quality and performance. You can disable this automatic switching in your settings: diff --git a/packages/core/src/availability/policyCatalog.ts b/packages/core/src/availability/policyCatalog.ts index 588d9a298d8..4ae8aeea7f1 100644 --- a/packages/core/src/availability/policyCatalog.ts +++ b/packages/core/src/availability/policyCatalog.ts @@ -41,7 +41,7 @@ const DEFAULT_ACTIONS: ModelPolicyActionMap = { unknown: 'prompt', }; -const SILENT_ACTIONS: ModelPolicyActionMap = { +export const SILENT_ACTIONS: ModelPolicyActionMap = { terminal: 'silent', transient: 'silent', not_found: 'silent', diff --git a/packages/core/src/availability/policyHelpers.test.ts b/packages/core/src/availability/policyHelpers.test.ts index 7035fa9ed98..42344f9bb9b 100644 --- a/packages/core/src/availability/policyHelpers.test.ts +++ b/packages/core/src/availability/policyHelpers.test.ts @@ -10,7 +10,7 @@ import { buildFallbackPolicyContext, applyModelSelection, } from './policyHelpers.js'; -import { createDefaultPolicy } from './policyCatalog.js'; +import { createDefaultPolicy, SILENT_ACTIONS } from './policyCatalog.js'; import type { Config } from '../config/config.js'; import { DEFAULT_GEMINI_FLASH_LITE_MODEL, @@ -21,6 +21,7 @@ import { import { AuthType } from '../core/contentGenerator.js'; import { ModelConfigService } from '../services/modelConfigService.js'; import { DEFAULT_MODEL_CONFIGS } from '../config/defaultModelConfigs.js'; +import { ApprovalMode } from '../policy/types.js'; const createMockConfig = (overrides: Partial = {}): Config => { const config = { @@ -164,6 +165,18 @@ describe('policyHelpers', () => { expect(chain[0]?.model).toBe(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL); expect(chain[1]?.model).toBe('gemini-3-flash-preview'); }); + + it('applies SILENT_ACTIONS when ApprovalMode is PLAN', () => { + const config = createMockConfig({ + getApprovalMode: () => ApprovalMode.PLAN, + getModel: () => DEFAULT_GEMINI_MODEL_AUTO, + }); + const chain = resolvePolicyChain(config); + + expect(chain).toHaveLength(2); + expect(chain[0]?.actions).toEqual(SILENT_ACTIONS); + expect(chain[1]?.actions).toEqual(SILENT_ACTIONS); + }); }); describe('resolvePolicyChain behavior is identical between dynamic and legacy implementations', () => { diff --git a/packages/core/src/availability/policyHelpers.ts b/packages/core/src/availability/policyHelpers.ts index 2581a07e289..57785081086 100644 --- a/packages/core/src/availability/policyHelpers.ts +++ b/packages/core/src/availability/policyHelpers.ts @@ -18,6 +18,7 @@ import { createSingleModelChain, getModelPolicyChain, getFlashLitePolicyChain, + SILENT_ACTIONS, } from './policyCatalog.js'; import { DEFAULT_GEMINI_FLASH_LITE_MODEL, @@ -29,6 +30,7 @@ import { } from '../config/models.js'; import type { ModelSelectionResult } from './modelAvailabilityService.js'; import type { ModelConfigKey } from '../services/modelConfigService.js'; +import { ApprovalMode } from '../policy/types.js'; /** * Resolves the active policy chain for the given config, ensuring the @@ -43,7 +45,7 @@ export function resolvePolicyChain( preferredModel ?? config.getActiveModel?.() ?? config.getModel(); const configuredModel = config.getModel(); - let chain; + let chain: ModelPolicyChain | undefined; const useGemini31 = config.getGemini31LaunchedSync?.() ?? false; const useGemini31FlashLite = config.getGemini31FlashLiteLaunchedSync?.() ?? false; @@ -103,45 +105,52 @@ export function resolvePolicyChain( // No matching modelChains found, default to single model chain chain = createSingleModelChain(modelFromConfig); } - return applyDynamicSlicing(chain, resolvedModel, wrapsAround); - } - - // --- LEGACY PATH --- + chain = applyDynamicSlicing(chain, resolvedModel, wrapsAround); + } else { + // --- LEGACY PATH --- - if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) { - chain = getFlashLitePolicyChain(); - } else if ( - isGemini3Model(resolvedModel, config) || - isAutoPreferred || - isAutoConfigured - ) { - if (hasAccessToPreview) { - const previewEnabled = - isGemini3Model(resolvedModel, config) || - preferredModel === PREVIEW_GEMINI_MODEL_AUTO || - configuredModel === PREVIEW_GEMINI_MODEL_AUTO; - chain = getModelPolicyChain({ - previewEnabled, - userTier: config.getUserTier(), - useGemini31, - useGemini31FlashLite, - useCustomToolModel, - }); + if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) { + chain = getFlashLitePolicyChain(); + } else if ( + isGemini3Model(resolvedModel, config) || + isAutoPreferred || + isAutoConfigured + ) { + if (hasAccessToPreview) { + const previewEnabled = + isGemini3Model(resolvedModel, config) || + preferredModel === PREVIEW_GEMINI_MODEL_AUTO || + configuredModel === PREVIEW_GEMINI_MODEL_AUTO; + chain = getModelPolicyChain({ + previewEnabled, + userTier: config.getUserTier(), + useGemini31, + useGemini31FlashLite, + useCustomToolModel, + }); + } else { + // User requested Gemini 3 but has no access. Proactively downgrade + // to the stable Gemini 2.5 chain. + chain = getModelPolicyChain({ + previewEnabled: false, + userTier: config.getUserTier(), + useGemini31, + useGemini31FlashLite, + useCustomToolModel, + }); + } } else { - // User requested Gemini 3 but has no access. Proactively downgrade - // to the stable Gemini 2.5 chain. - chain = getModelPolicyChain({ - previewEnabled: false, - userTier: config.getUserTier(), - useGemini31, - useGemini31FlashLite, - useCustomToolModel, - }); + chain = createSingleModelChain(modelFromConfig); } - } else { - chain = createSingleModelChain(modelFromConfig); + chain = applyDynamicSlicing(chain, resolvedModel, wrapsAround); } - return applyDynamicSlicing(chain, resolvedModel, wrapsAround); + + // Apply Unified Silent Injection for Plan Mode with defensive checks + if (config?.getApprovalMode?.() === ApprovalMode.PLAN) { + return chain.map((policy) => ({ ...policy, actions: SILENT_ACTIONS })); + } + + return chain; } /** From b3c0d1b5e55ed223668a448ab6936f4f92255ae4 Mon Sep 17 00:00:00 2001 From: Jerop Kipruto Date: Mon, 13 Apr 2026 14:48:28 -0400 Subject: [PATCH 2/2] fix(core): use fresh instance of SILENT_ACTIONS for each policy This addresses a review comment pointing out that assigning SILENT_ACTIONS directly could lead to shared state across policies. We now use the spread operator to provide each policy with its own instance of the actions map. --- packages/core/src/availability/policyHelpers.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/availability/policyHelpers.ts b/packages/core/src/availability/policyHelpers.ts index 57785081086..033443ad5cd 100644 --- a/packages/core/src/availability/policyHelpers.ts +++ b/packages/core/src/availability/policyHelpers.ts @@ -147,7 +147,10 @@ export function resolvePolicyChain( // Apply Unified Silent Injection for Plan Mode with defensive checks if (config?.getApprovalMode?.() === ApprovalMode.PLAN) { - return chain.map((policy) => ({ ...policy, actions: SILENT_ACTIONS })); + return chain.map((policy) => ({ + ...policy, + actions: { ...SILENT_ACTIONS }, + })); } return chain;