Skip to content

Commit 050c303

Browse files
authored
feat(core): implement silent fallback for Plan Mode model routing (#25317)
1 parent a172b32 commit 050c303

4 files changed

Lines changed: 67 additions & 38 deletions

File tree

docs/cli/plan-mode.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,10 @@ on the current phase of your task:
445445
switches to a high-speed **Flash** model. This provides a faster, more
446446
responsive experience during the implementation of the plan.
447447

448+
If the high-reasoning model is unavailable or you don't have access to it,
449+
Gemini CLI automatically and silently falls back to a faster model to ensure
450+
your workflow isn't interrupted.
451+
448452
This behavior is enabled by default to provide the best balance of quality and
449453
performance. You can disable this automatic switching in your settings:
450454

packages/core/src/availability/policyCatalog.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const DEFAULT_ACTIONS: ModelPolicyActionMap = {
4141
unknown: 'prompt',
4242
};
4343

44-
const SILENT_ACTIONS: ModelPolicyActionMap = {
44+
export const SILENT_ACTIONS: ModelPolicyActionMap = {
4545
terminal: 'silent',
4646
transient: 'silent',
4747
not_found: 'silent',

packages/core/src/availability/policyHelpers.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
buildFallbackPolicyContext,
1111
applyModelSelection,
1212
} from './policyHelpers.js';
13-
import { createDefaultPolicy } from './policyCatalog.js';
13+
import { createDefaultPolicy, SILENT_ACTIONS } from './policyCatalog.js';
1414
import type { Config } from '../config/config.js';
1515
import {
1616
DEFAULT_GEMINI_FLASH_LITE_MODEL,
@@ -21,6 +21,7 @@ import {
2121
import { AuthType } from '../core/contentGenerator.js';
2222
import { ModelConfigService } from '../services/modelConfigService.js';
2323
import { DEFAULT_MODEL_CONFIGS } from '../config/defaultModelConfigs.js';
24+
import { ApprovalMode } from '../policy/types.js';
2425

2526
const createMockConfig = (overrides: Partial<Config> = {}): Config => {
2627
const config = {
@@ -164,6 +165,18 @@ describe('policyHelpers', () => {
164165
expect(chain[0]?.model).toBe(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL);
165166
expect(chain[1]?.model).toBe('gemini-3-flash-preview');
166167
});
168+
169+
it('applies SILENT_ACTIONS when ApprovalMode is PLAN', () => {
170+
const config = createMockConfig({
171+
getApprovalMode: () => ApprovalMode.PLAN,
172+
getModel: () => DEFAULT_GEMINI_MODEL_AUTO,
173+
});
174+
const chain = resolvePolicyChain(config);
175+
176+
expect(chain).toHaveLength(2);
177+
expect(chain[0]?.actions).toEqual(SILENT_ACTIONS);
178+
expect(chain[1]?.actions).toEqual(SILENT_ACTIONS);
179+
});
167180
});
168181

169182
describe('resolvePolicyChain behavior is identical between dynamic and legacy implementations', () => {

packages/core/src/availability/policyHelpers.ts

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
createSingleModelChain,
1919
getModelPolicyChain,
2020
getFlashLitePolicyChain,
21+
SILENT_ACTIONS,
2122
} from './policyCatalog.js';
2223
import {
2324
DEFAULT_GEMINI_FLASH_LITE_MODEL,
@@ -29,6 +30,7 @@ import {
2930
} from '../config/models.js';
3031
import type { ModelSelectionResult } from './modelAvailabilityService.js';
3132
import type { ModelConfigKey } from '../services/modelConfigService.js';
33+
import { ApprovalMode } from '../policy/types.js';
3234

3335
/**
3436
* Resolves the active policy chain for the given config, ensuring the
@@ -43,7 +45,7 @@ export function resolvePolicyChain(
4345
preferredModel ?? config.getActiveModel?.() ?? config.getModel();
4446
const configuredModel = config.getModel();
4547

46-
let chain;
48+
let chain: ModelPolicyChain | undefined;
4749
const useGemini31 = config.getGemini31LaunchedSync?.() ?? false;
4850
const useGemini31FlashLite =
4951
config.getGemini31FlashLiteLaunchedSync?.() ?? false;
@@ -103,45 +105,55 @@ export function resolvePolicyChain(
103105
// No matching modelChains found, default to single model chain
104106
chain = createSingleModelChain(modelFromConfig);
105107
}
106-
return applyDynamicSlicing(chain, resolvedModel, wrapsAround);
107-
}
108-
109-
// --- LEGACY PATH ---
108+
chain = applyDynamicSlicing(chain, resolvedModel, wrapsAround);
109+
} else {
110+
// --- LEGACY PATH ---
110111

111-
if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) {
112-
chain = getFlashLitePolicyChain();
113-
} else if (
114-
isGemini3Model(resolvedModel, config) ||
115-
isAutoPreferred ||
116-
isAutoConfigured
117-
) {
118-
if (hasAccessToPreview) {
119-
const previewEnabled =
120-
isGemini3Model(resolvedModel, config) ||
121-
preferredModel === PREVIEW_GEMINI_MODEL_AUTO ||
122-
configuredModel === PREVIEW_GEMINI_MODEL_AUTO;
123-
chain = getModelPolicyChain({
124-
previewEnabled,
125-
userTier: config.getUserTier(),
126-
useGemini31,
127-
useGemini31FlashLite,
128-
useCustomToolModel,
129-
});
112+
if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) {
113+
chain = getFlashLitePolicyChain();
114+
} else if (
115+
isGemini3Model(resolvedModel, config) ||
116+
isAutoPreferred ||
117+
isAutoConfigured
118+
) {
119+
if (hasAccessToPreview) {
120+
const previewEnabled =
121+
isGemini3Model(resolvedModel, config) ||
122+
preferredModel === PREVIEW_GEMINI_MODEL_AUTO ||
123+
configuredModel === PREVIEW_GEMINI_MODEL_AUTO;
124+
chain = getModelPolicyChain({
125+
previewEnabled,
126+
userTier: config.getUserTier(),
127+
useGemini31,
128+
useGemini31FlashLite,
129+
useCustomToolModel,
130+
});
131+
} else {
132+
// User requested Gemini 3 but has no access. Proactively downgrade
133+
// to the stable Gemini 2.5 chain.
134+
chain = getModelPolicyChain({
135+
previewEnabled: false,
136+
userTier: config.getUserTier(),
137+
useGemini31,
138+
useGemini31FlashLite,
139+
useCustomToolModel,
140+
});
141+
}
130142
} else {
131-
// User requested Gemini 3 but has no access. Proactively downgrade
132-
// to the stable Gemini 2.5 chain.
133-
chain = getModelPolicyChain({
134-
previewEnabled: false,
135-
userTier: config.getUserTier(),
136-
useGemini31,
137-
useGemini31FlashLite,
138-
useCustomToolModel,
139-
});
143+
chain = createSingleModelChain(modelFromConfig);
140144
}
141-
} else {
142-
chain = createSingleModelChain(modelFromConfig);
145+
chain = applyDynamicSlicing(chain, resolvedModel, wrapsAround);
143146
}
144-
return applyDynamicSlicing(chain, resolvedModel, wrapsAround);
147+
148+
// Apply Unified Silent Injection for Plan Mode with defensive checks
149+
if (config?.getApprovalMode?.() === ApprovalMode.PLAN) {
150+
return chain.map((policy) => ({
151+
...policy,
152+
actions: { ...SILENT_ACTIONS },
153+
}));
154+
}
155+
156+
return chain;
145157
}
146158

147159
/**

0 commit comments

Comments
 (0)