Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/cli/plan-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/availability/policyCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
15 changes: 14 additions & 1 deletion packages/core/src/availability/policyHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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> = {}): Config => {
const config = {
Expand Down Expand Up @@ -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', () => {
Expand Down
84 changes: 48 additions & 36 deletions packages/core/src/availability/policyHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
createSingleModelChain,
getModelPolicyChain,
getFlashLitePolicyChain,
SILENT_ACTIONS,
} from './policyCatalog.js';
import {
DEFAULT_GEMINI_FLASH_LITE_MODEL,
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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 ---
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;
Comment on lines +149 to +156

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The implementation of the Plan Mode check introduces a potential type safety issue and maintenance risk. The chain variable is declared as ModelPolicyChain | undefined (line 48), but the function's return type is ModelPolicyChain (line 43), which is not optional.

If the condition at line 149 is false and chain remains undefined, the function will return undefined, violating its type contract. Furthermore, the use of optional chaining at line 149 (config?.getApprovalMode?.()) suggests that config could be null or undefined; if it were, the function would skip the PLAN mode block and return chain, which might be undefined. While the current logic seems to ensure chain is assigned in both the dynamic and legacy paths, the loose typing bypasses compiler checks for exhaustive assignment and could lead to a runtime crash at line 150 (chain.map) if the logic is modified in the future.

Consider tightening the type of chain to ModelPolicyChain and ensuring it is assigned in all branches, or adding an explicit guard for chain before use. This ensures the code adheres to the interface contract and explicitly handles optional properties.

References
  1. When consuming an object, if a property is optional in its type definition (interface), callers must handle the undefined case (e.g., by providing a default with ??). Do not rely on the implementation details of the function that creates the object to always provide a value, as this can change. Code against the interface contract.

}

/**
Expand Down
Loading