From 050c30330e2bb71558f51b2a4482d8970a7a84cb Mon Sep 17 00:00:00 2001 From: Jerop Kipruto Date: Mon, 13 Apr 2026 15:59:24 -0400 Subject: [PATCH] feat(core): implement silent fallback for Plan Mode model routing (#25317) --- docs/cli/plan-mode.md | 4 + .../core/src/availability/policyCatalog.ts | 2 +- .../src/availability/policyHelpers.test.ts | 15 +++- .../core/src/availability/policyHelpers.ts | 88 +++++++++++-------- 4 files changed, 69 insertions(+), 40 deletions(-) diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index 4d9c45ce17..00677943ad 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -445,6 +445,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 588d9a298d..4ae8aeea7f 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 7035fa9ed9..42344f9bb9 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 2581a07e28..033443ad5c 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,55 @@ export function resolvePolicyChain( // No matching modelChains found, default to single model chain chain = createSingleModelChain(modelFromConfig); } - return applyDynamicSlicing(chain, resolvedModel, wrapsAround); - } - - // --- 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, - }); - } 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 = applyDynamicSlicing(chain, resolvedModel, wrapsAround); } else { - chain = createSingleModelChain(modelFromConfig); + // --- 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, + }); + } 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 { + 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; } /**