feat: launch Gemini 3 Flash in Gemini CLI ️ (#15196)

Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
Co-authored-by: joshualitt <joshualitt@google.com>
Co-authored-by: Sehoon Shon <sshon@google.com>
Co-authored-by: Adam Weidman <65992621+adamfweidman@users.noreply.github.com>
Co-authored-by: Adib234 <30782825+Adib234@users.noreply.github.com>
Co-authored-by: Jenna Inouye <jinouye@google.com>
This commit is contained in:
Tommaso Sciortino
2025-12-17 09:43:21 -08:00
committed by GitHub
parent 18698d6929
commit bf90b59935
65 changed files with 1898 additions and 2060 deletions

View File

@@ -19,7 +19,7 @@ describe('policyCatalog', () => {
it('returns preview chain when preview enabled', () => {
const chain = getModelPolicyChain({ previewEnabled: true });
expect(chain[0]?.model).toBe(PREVIEW_GEMINI_MODEL);
expect(chain).toHaveLength(3);
expect(chain).toHaveLength(2);
});
it('returns default chain when preview disabled', () => {
@@ -31,7 +31,7 @@ describe('policyCatalog', () => {
it('marks preview transients as sticky retries', () => {
const [previewPolicy] = getModelPolicyChain({ previewEnabled: true });
expect(previewPolicy.model).toBe(PREVIEW_GEMINI_MODEL);
expect(previewPolicy.stateTransitions.transient).toBe('sticky_retry');
expect(previewPolicy.stateTransitions.transient).toBe('terminal');
});
it('applies default actions and state transitions for unspecified kinds', () => {

View File

@@ -13,6 +13,7 @@ import type {
import {
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_MODEL,
PREVIEW_GEMINI_FLASH_MODEL,
PREVIEW_GEMINI_MODEL,
} from '../config/models.js';
import type { UserTierId } from '../code_assist/types.js';
@@ -48,13 +49,8 @@ const DEFAULT_CHAIN: ModelPolicyChain = [
];
const PREVIEW_CHAIN: ModelPolicyChain = [
definePolicy({
model: PREVIEW_GEMINI_MODEL,
stateTransitions: { transient: 'sticky_retry' },
actions: { transient: 'silent' },
}),
definePolicy({ model: DEFAULT_GEMINI_MODEL }),
definePolicy({ model: DEFAULT_GEMINI_FLASH_MODEL, isLastResort: true }),
definePolicy({ model: PREVIEW_GEMINI_MODEL }),
definePolicy({ model: PREVIEW_GEMINI_FLASH_MODEL, isLastResort: true }),
];
/**
@@ -70,6 +66,10 @@ export function getModelPolicyChain(
return cloneChain(DEFAULT_CHAIN);
}
export function createSingleModelChain(model: string): ModelPolicyChain {
return [definePolicy({ model, isLastResort: true })];
}
/**
* Provides a default policy scaffold for models not present in the catalog.
*/

View File

@@ -12,6 +12,7 @@ import {
} from './policyHelpers.js';
import { createDefaultPolicy } from './policyCatalog.js';
import type { Config } from '../config/config.js';
import { DEFAULT_GEMINI_MODEL_AUTO } from '../config/models.js';
const createMockConfig = (overrides: Partial<Config> = {}): Config =>
({
@@ -24,7 +25,7 @@ const createMockConfig = (overrides: Partial<Config> = {}): Config =>
describe('policyHelpers', () => {
describe('resolvePolicyChain', () => {
it('inserts the active model when missing from the catalog', () => {
it('returns a single-model chain for a custom model', () => {
const config = createMockConfig({
getModel: () => 'custom-model',
});
@@ -43,7 +44,7 @@ describe('policyHelpers', () => {
it('returns the default chain when active model is "auto"', () => {
const config = createMockConfig({
getModel: () => 'auto',
getModel: () => DEFAULT_GEMINI_MODEL_AUTO,
});
const chain = resolvePolicyChain(config);
@@ -52,6 +53,25 @@ describe('policyHelpers', () => {
expect(chain[0]?.model).toBe('gemini-2.5-pro');
expect(chain[1]?.model).toBe('gemini-2.5-flash');
});
it('starts chain from preferredModel when model is "auto"', () => {
const config = createMockConfig({
getModel: () => DEFAULT_GEMINI_MODEL_AUTO,
});
const chain = resolvePolicyChain(config, 'gemini-2.5-flash');
expect(chain).toHaveLength(1);
expect(chain[0]?.model).toBe('gemini-2.5-flash');
});
it('wraps around the chain when wrapsAround is true', () => {
const config = createMockConfig({
getModel: () => DEFAULT_GEMINI_MODEL_AUTO,
});
const chain = resolvePolicyChain(config, 'gemini-2.5-flash', true);
expect(chain).toHaveLength(2);
expect(chain[0]?.model).toBe('gemini-2.5-flash');
expect(chain[1]?.model).toBe('gemini-2.5-pro');
});
});
describe('buildFallbackPolicyContext', () => {
@@ -63,6 +83,17 @@ describe('policyHelpers', () => {
];
const context = buildFallbackPolicyContext(chain, 'b');
expect(context.failedPolicy?.model).toBe('b');
expect(context.candidates.map((p) => p.model)).toEqual(['c']);
});
it('wraps around when building fallback context if wrapsAround is true', () => {
const chain = [
createDefaultPolicy('a'),
createDefaultPolicy('b'),
createDefaultPolicy('c'),
];
const context = buildFallbackPolicyContext(chain, 'b', true);
expect(context.failedPolicy?.model).toBe('b');
expect(context.candidates.map((p) => p.model)).toEqual(['c', 'a']);
});

View File

@@ -13,8 +13,17 @@ import type {
ModelPolicyChain,
RetryAvailabilityContext,
} from './modelPolicy.js';
import { createDefaultPolicy, getModelPolicyChain } from './policyCatalog.js';
import { DEFAULT_GEMINI_MODEL, getEffectiveModel } from '../config/models.js';
import {
createDefaultPolicy,
createSingleModelChain,
getModelPolicyChain,
} from './policyCatalog.js';
import {
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_MODEL_AUTO,
PREVIEW_GEMINI_MODEL_AUTO,
resolveModel,
} from '../config/models.js';
import type { ModelSelectionResult } from './modelAvailabilityService.js';
/**
@@ -24,27 +33,34 @@ import type { ModelSelectionResult } from './modelAvailabilityService.js';
export function resolvePolicyChain(
config: Config,
preferredModel?: string,
wrapsAround: boolean = false,
): ModelPolicyChain {
const chain = getModelPolicyChain({
previewEnabled: !!config.getPreviewFeatures(),
userTier: config.getUserTier(),
});
// TODO: This will be replaced when we get rid of Fallback Modes.
// Switch to getActiveModel()
const activeModel =
preferredModel ??
getEffectiveModel(
config.isInFallbackMode(),
config.getModel(),
config.getPreviewFeatures(),
);
// Availability uses the active/requested model directly. Legacy fallback logic
// (getEffectiveModel) only applies when availability is disabled.
const modelFromConfig =
preferredModel ?? config.getActiveModel?.() ?? config.getModel();
if (activeModel === 'auto') {
return [...chain];
let chain;
if (
config.getModel() === PREVIEW_GEMINI_MODEL_AUTO ||
config.getModel() === DEFAULT_GEMINI_MODEL_AUTO
) {
chain = getModelPolicyChain({
previewEnabled: config.getModel() === PREVIEW_GEMINI_MODEL_AUTO,
userTier: config.getUserTier(),
});
} else {
chain = createSingleModelChain(modelFromConfig);
}
if (chain.some((policy) => policy.model === activeModel)) {
return [...chain];
const activeModel = resolveModel(modelFromConfig);
const activeIndex = chain.findIndex((policy) => policy.model === activeModel);
if (activeIndex !== -1) {
return wrapsAround
? [...chain.slice(activeIndex), ...chain.slice(0, activeIndex)]
: [...chain.slice(activeIndex)];
}
// If the user specified a model not in the default chain, we assume they want
@@ -55,10 +71,14 @@ export function resolvePolicyChain(
/**
* Produces the failed policy (if it exists in the chain) and the list of
* fallback candidates that follow it.
* @param chain - The ordered list of available model policies.
* @param failedModel - The identifier of the model that failed.
* @param wrapsAround - If true, treats the chain as a circular buffer.
*/
export function buildFallbackPolicyContext(
chain: ModelPolicyChain,
failedModel: string,
wrapsAround: boolean = false,
): {
failedPolicy?: ModelPolicy;
candidates: ModelPolicy[];
@@ -69,9 +89,12 @@ export function buildFallbackPolicyContext(
}
// Return [candidates_after, candidates_before] to prioritize downgrades
// (continuing the chain) before wrapping around to upgrades.
const candidates = wrapsAround
? [...chain.slice(index + 1), ...chain.slice(0, index)]
: [...chain.slice(index + 1)];
return {
failedPolicy: chain[index],
candidates: [...chain.slice(index + 1), ...chain.slice(0, index)],
candidates,
};
}