feat(core,cli): add support for Gemma 4 models (experimental) (#25604)

This commit is contained in:
Abhijit Balaji
2026-04-23 16:02:17 -07:00
committed by GitHub
parent 1f73ec70c5
commit d4c5333dcf
24 changed files with 364 additions and 9 deletions
+2 -1
View File
@@ -2,7 +2,8 @@
"experimental": {
"extensionReloading": true,
"modelSteering": true,
"autoMemory": true
"autoMemory": true,
"gemma": true
},
"general": {
"devtools": true
+1
View File
@@ -163,6 +163,7 @@ they appear in the UI.
| UI Label | Setting | Description | Default |
| ---------------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Gemma Models | `experimental.gemma` | Enable access to Gemma 4 models (experimental). | `false` |
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
+45
View File
@@ -563,6 +563,18 @@ their corresponding top-level category object in your `settings.json` file.
"model": "gemini-2.5-flash-lite"
}
},
"gemma-4-31b-it": {
"extends": "chat-base-3",
"modelConfig": {
"model": "gemma-4-31b-it"
}
},
"gemma-4-26b-a4b-it": {
"extends": "chat-base-3",
"modelConfig": {
"model": "gemma-4-26b-a4b-it"
}
},
"gemini-2.5-flash-base": {
"extends": "base",
"modelConfig": {
@@ -834,6 +846,28 @@ their corresponding top-level category object in your `settings.json` file.
"multimodalToolUse": false
}
},
"gemma-4-31b-it": {
"displayName": "gemma-4-31b-it",
"tier": "custom",
"family": "gemma-4",
"isPreview": false,
"isVisible": true,
"features": {
"thinking": true,
"multimodalToolUse": false
}
},
"gemma-4-26b-a4b-it": {
"displayName": "gemma-4-26b-a4b-it",
"tier": "custom",
"family": "gemma-4",
"isPreview": false,
"isVisible": true,
"features": {
"thinking": true,
"multimodalToolUse": false
}
},
"auto": {
"tier": "auto",
"isPreview": true,
@@ -904,6 +938,12 @@ their corresponding top-level category object in your `settings.json` file.
```json
{
"gemma-4-31b-it": {
"default": "gemma-4-31b-it"
},
"gemma-4-26b-a4b-it": {
"default": "gemma-4-26b-a4b-it"
},
"gemini-3.1-pro-preview": {
"default": "gemini-3.1-pro-preview",
"contexts": [
@@ -1646,6 +1686,11 @@ their corresponding top-level category object in your `settings.json` file.
#### `experimental`
- **`experimental.gemma`** (boolean):
- **Description:** Enable access to Gemma 4 models (experimental).
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.adk.agentSessionNoninteractiveEnabled`** (boolean):
- **Description:** Enable non-interactive agent sessions.
- **Default:** `false`
@@ -112,6 +112,7 @@ export function createMockConfig(
}),
isContextManagementEnabled: vi.fn().mockReturnValue(false),
getContextManagementConfig: vi.fn().mockReturnValue({ enabled: false }),
getExperimentalGemma: vi.fn().mockReturnValue(false),
...overrides,
} as unknown as Config;
+12
View File
@@ -3055,6 +3055,18 @@ describe('loadCliConfig gemmaModelRouter', () => {
expect(gemmaSettings.classifier?.model).toBe('custom-gemma');
});
it('should load experimental.gemma setting from merged settings', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
experimental: {
gemma: true,
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getExperimentalGemma()).toBe(true);
});
it('should handle partial gemmaModelRouter settings', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
+1
View File
@@ -1011,6 +1011,7 @@ export async function loadCliConfig(
experimentalJitContext,
experimentalMemoryV2: settings.experimental?.memoryV2,
experimentalAutoMemory: settings.experimental?.autoMemory,
experimentalGemma: settings.experimental?.gemma,
contextManagement,
modelSteering: settings.experimental?.modelSteering,
topicUpdateNarration:
@@ -2052,6 +2052,15 @@ const SETTINGS_SCHEMA = {
description: 'Setting to enable experimental features',
showInDialog: false,
properties: {
gemma: {
type: 'boolean',
label: 'Gemma Models',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable access to Gemma 4 models (experimental).',
showInDialog: true,
},
adk: {
type: 'object',
label: 'ADK',
+1
View File
@@ -306,6 +306,7 @@ describe('gemini.tsx main function cleanup', () => {
getMessageBus: () => ({ subscribe: vi.fn() }),
getEnableHooks: vi.fn(() => true),
getHookSystem: vi.fn(() => undefined),
getExperimentalGemma: vi.fn(() => false),
initialize: vi.fn(),
storage: { initialize: vi.fn().mockResolvedValue(undefined) },
getContentGeneratorConfig: vi.fn(),
@@ -89,6 +89,7 @@ describe('ShellProcessor', () => {
getPolicyEngine: vi.fn().mockReturnValue({
check: mockPolicyEngineCheck,
}),
getExperimentalGemma: vi.fn().mockReturnValue(false),
get config() {
return this as unknown as Config;
},
@@ -168,6 +168,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getAdminSkillsEnabled: vi.fn().mockReturnValue(false),
getDisabledSkills: vi.fn().mockReturnValue([]),
getExperimentalJitContext: vi.fn().mockReturnValue(false),
getExperimentalGemma: vi.fn().mockReturnValue(false),
getMemoryBoundaryMarkers: vi.fn().mockReturnValue(['.git']),
getTerminalBackground: vi.fn().mockReturnValue(undefined),
getEmbeddingModel: vi.fn().mockReturnValue('embedding-model'),
@@ -65,6 +65,7 @@ describe('<ModelDialog />', () => {
getGemini31FlashLiteLaunchedSync: () => boolean;
getProModelNoAccess: () => Promise<boolean>;
getProModelNoAccessSync: () => boolean;
getExperimentalGemma: () => boolean;
getLastRetrievedQuota: () =>
| {
buckets: Array<{
@@ -85,6 +86,7 @@ describe('<ModelDialog />', () => {
getGemini31FlashLiteLaunchedSync: mockGetGemini31FlashLiteLaunchedSync,
getProModelNoAccess: mockGetProModelNoAccess,
getProModelNoAccessSync: mockGetProModelNoAccessSync,
getExperimentalGemma: () => false,
getLastRetrievedQuota: () => ({ buckets: [] }),
getSessionId: () => 'test-session-id',
};
+23 -4
View File
@@ -19,6 +19,8 @@ import {
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
DEFAULT_GEMINI_MODEL_AUTO,
GEMMA_4_31B_IT_MODEL,
GEMMA_4_26B_A4B_IT_MODEL,
ModelSlashCommandEvent,
logModelSlashCommand,
getDisplayString,
@@ -222,7 +224,9 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
}
// --- LEGACY PATH ---
const list = [
const showGemmaModels = config?.getExperimentalGemma() ?? false;
const options = [
{
value: DEFAULT_GEMINI_MODEL,
title: getDisplayString(DEFAULT_GEMINI_MODEL),
@@ -240,6 +244,21 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
},
];
if (showGemmaModels) {
options.push(
{
value: GEMMA_4_31B_IT_MODEL,
title: getDisplayString(GEMMA_4_31B_IT_MODEL),
key: GEMMA_4_31B_IT_MODEL,
},
{
value: GEMMA_4_26B_A4B_IT_MODEL,
title: getDisplayString(GEMMA_4_26B_A4B_IT_MODEL),
key: GEMMA_4_26B_A4B_IT_MODEL,
},
);
}
if (shouldShowPreviewModels) {
const previewProModel = useGemini31
? PREVIEW_GEMINI_3_1_MODEL
@@ -270,15 +289,15 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
});
}
list.unshift(...previewOptions);
options.unshift(...previewOptions);
}
if (!hasAccessToProModel) {
// Filter out all Pro models for free tier
return list.filter((option) => !isProModel(option.value));
return options.filter((option) => !isProModel(option.value));
}
return list;
return options;
}, [
shouldShowPreviewModels,
useGemini31,
@@ -86,6 +86,7 @@ const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getProjectTempDir: () => '/tmp/test',
},
getSessionId: () => 'default-session-id',
getExperimentalGemma: () => false,
...overrides,
}) as Config;
@@ -69,6 +69,7 @@ describe('Session Cleanup (Refactored)', () => {
},
getSessionId: () => 'current123',
getDebugMode: () => false,
getExperimentalGemma: () => false,
initialize: async () => {},
...overrides,
} as unknown as Config;
+2
View File
@@ -12,6 +12,8 @@ export {
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
DEFAULT_GEMINI_EMBEDDING_MODEL,
GEMMA_4_31B_IT_MODEL,
GEMMA_4_26B_A4B_IT_MODEL,
} from './src/config/models.js';
export {
serializeTerminalToObject,
+41
View File
@@ -3645,6 +3645,47 @@ describe('Config JIT Initialization', () => {
expect(config.isAutoMemoryEnabled()).toBe(true);
});
it('should return true when experimentalGemma is true', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
experimentalGemma: true,
};
config = new Config(params);
expect(config.getExperimentalGemma()).toBe(true);
});
it('should return false when experimentalGemma is false', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
experimentalGemma: false,
};
config = new Config(params);
expect(config.getExperimentalGemma()).toBe(false);
});
it('should return false when experimentalGemma is not provided', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
};
config = new Config(params);
expect(config.getExperimentalGemma()).toBe(false);
});
it('should be independent of experimentalMemoryV2', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
+7
View File
@@ -711,6 +711,7 @@ export interface ConfigParameters {
autoDistillation?: boolean;
experimentalMemoryV2?: boolean;
experimentalAutoMemory?: boolean;
experimentalGemma?: boolean;
experimentalContextManagementConfig?: string;
experimentalAgentHistoryTruncation?: boolean;
experimentalAgentHistoryTruncationThreshold?: number;
@@ -956,6 +957,7 @@ export class Config implements McpContext, AgentLoopContext {
private readonly experimentalJitContext: boolean;
private readonly experimentalMemoryV2: boolean;
private readonly experimentalAutoMemory: boolean;
private readonly experimentalGemma: boolean;
private readonly experimentalContextManagementConfig?: string;
private readonly memoryBoundaryMarkers: readonly string[];
private readonly topicUpdateNarration: boolean;
@@ -1174,6 +1176,7 @@ export class Config implements McpContext, AgentLoopContext {
this.experimentalJitContext = params.experimentalJitContext ?? true;
this.experimentalMemoryV2 = params.experimentalMemoryV2 ?? true;
this.experimentalAutoMemory = params.experimentalAutoMemory ?? false;
this.experimentalGemma = params.experimentalGemma ?? false;
this.experimentalContextManagementConfig =
params.experimentalContextManagementConfig;
this.memoryBoundaryMarkers = params.memoryBoundaryMarkers ?? ['.git'];
@@ -2521,6 +2524,10 @@ export class Config implements McpContext, AgentLoopContext {
return this.experimentalAutoMemory;
}
getExperimentalGemma(): boolean {
return this.experimentalGemma;
}
getExperimentalContextManagementConfig(): string | undefined {
return this.experimentalContextManagementConfig;
}
@@ -89,6 +89,19 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
model: 'gemini-2.5-flash-lite',
},
},
'gemma-4-31b-it': {
extends: 'chat-base-3',
modelConfig: {
model: 'gemma-4-31b-it',
},
},
'gemma-4-26b-a4b-it': {
extends: 'chat-base-3',
modelConfig: {
model: 'gemma-4-26b-a4b-it',
},
},
// Bases for the internal model configs.
'gemini-2.5-flash-base': {
extends: 'base',
@@ -317,6 +330,23 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
isVisible: true,
features: { thinking: false, multimodalToolUse: false },
},
'gemma-4-31b-it': {
displayName: 'gemma-4-31b-it',
tier: 'custom',
family: 'gemma-4',
isPreview: false,
isVisible: true,
features: { thinking: true, multimodalToolUse: false },
},
'gemma-4-26b-a4b-it': {
displayName: 'gemma-4-26b-a4b-it',
tier: 'custom',
family: 'gemma-4',
isPreview: false,
isVisible: true,
features: { thinking: true, multimodalToolUse: false },
},
// Aliases
auto: {
tier: 'auto',
@@ -362,6 +392,13 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
},
modelIdResolutions: {
'gemma-4-31b-it': {
default: 'gemma-4-31b-it',
},
'gemma-4-26b-a4b-it': {
default: 'gemma-4-26b-a4b-it',
},
'gemini-3.1-pro-preview': {
default: 'gemini-3.1-pro-preview',
contexts: [
+17
View File
@@ -32,6 +32,8 @@ import {
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
isPreviewModel,
isProModel,
GEMMA_4_31B_IT_MODEL,
GEMMA_4_26B_A4B_IT_MODEL,
} from './models.js';
import type { Config } from './config.js';
import { ModelConfigService } from '../services/modelConfigService.js';
@@ -356,6 +358,10 @@ describe('getDisplayString', () => {
it('should return the model name as is for other models', () => {
expect(getDisplayString('custom-model')).toBe('custom-model');
expect(getDisplayString(GEMMA_4_31B_IT_MODEL)).toBe(GEMMA_4_31B_IT_MODEL);
expect(getDisplayString(GEMMA_4_26B_A4B_IT_MODEL)).toBe(
GEMMA_4_26B_A4B_IT_MODEL,
);
expect(getDisplayString(DEFAULT_GEMINI_FLASH_LITE_MODEL)).toBe(
DEFAULT_GEMINI_FLASH_LITE_MODEL,
);
@@ -573,6 +579,17 @@ describe('isActiveModel', () => {
expect(isActiveModel(DEFAULT_GEMINI_FLASH_MODEL)).toBe(true);
});
it('should return true for Gemma 4 models only when experimentalGemma is true', () => {
expect(isActiveModel(GEMMA_4_31B_IT_MODEL)).toBe(false);
expect(isActiveModel(GEMMA_4_26B_A4B_IT_MODEL)).toBe(false);
expect(isActiveModel(GEMMA_4_31B_IT_MODEL, false, false, false, true)).toBe(
true,
);
expect(
isActiveModel(GEMMA_4_26B_A4B_IT_MODEL, false, false, false, true),
).toBe(true);
});
it('should return false for Gemini 3.1 models when Gemini 3.1 is not launched', () => {
expect(isActiveModel(PREVIEW_GEMINI_3_1_MODEL)).toBe(false);
expect(isActiveModel(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL)).toBe(false);
+14
View File
@@ -61,6 +61,9 @@ export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro';
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';
export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-2.5-flash-lite';
export const GEMMA_4_31B_IT_MODEL = 'gemma-4-31b-it';
export const GEMMA_4_26B_A4B_IT_MODEL = 'gemma-4-26b-a4b-it';
export const VALID_GEMINI_MODELS = new Set([
PREVIEW_GEMINI_MODEL,
PREVIEW_GEMINI_3_1_MODEL,
@@ -70,6 +73,9 @@ export const VALID_GEMINI_MODELS = new Set([
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
GEMMA_4_31B_IT_MODEL,
GEMMA_4_26B_A4B_IT_MODEL,
]);
export const PREVIEW_GEMINI_MODEL_AUTO = 'auto-gemini-3';
@@ -257,6 +263,10 @@ export function getDisplayString(
return 'Auto (Gemini 3)';
case DEFAULT_GEMINI_MODEL_AUTO:
return 'Auto (Gemini 2.5)';
case GEMMA_4_31B_IT_MODEL:
return GEMMA_4_31B_IT_MODEL;
case GEMMA_4_26B_A4B_IT_MODEL:
return GEMMA_4_26B_A4B_IT_MODEL;
case GEMINI_MODEL_ALIAS_PRO:
return PREVIEW_GEMINI_MODEL;
case GEMINI_MODEL_ALIAS_FLASH:
@@ -438,10 +448,14 @@ export function isActiveModel(
useGemini3_1: boolean = false,
useGemini3_1FlashLite: boolean = false,
useCustomToolModel: boolean = false,
experimentalGemma: boolean = false,
): boolean {
if (!VALID_GEMINI_MODELS.has(model)) {
return false;
}
if (model === GEMMA_4_31B_IT_MODEL || model === GEMMA_4_26B_A4B_IT_MODEL) {
return experimentalGemma;
}
if (model === PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL) {
return useGemini3_1FlashLite;
}
+6
View File
@@ -10,17 +10,23 @@ import {
DEFAULT_GEMINI_MODEL,
PREVIEW_GEMINI_FLASH_MODEL,
PREVIEW_GEMINI_MODEL,
GEMMA_4_31B_IT_MODEL,
GEMMA_4_26B_A4B_IT_MODEL,
} from '../config/models.js';
type Model = string;
type TokenCount = number;
export const DEFAULT_TOKEN_LIMIT = 1_048_576;
export const GEMMA_4_TOKEN_LIMIT = 256_000;
export function tokenLimit(model: Model): TokenCount {
// Add other models as they become relevant or if specified by config
// Pulled from https://ai.google.dev/gemini-api/docs/models
switch (model) {
case GEMMA_4_31B_IT_MODEL:
case GEMMA_4_26B_A4B_IT_MODEL:
return GEMMA_4_TOKEN_LIMIT;
case PREVIEW_GEMINI_MODEL:
case PREVIEW_GEMINI_FLASH_MODEL:
case DEFAULT_GEMINI_MODEL:
@@ -97,6 +97,30 @@
"topK": 64
}
},
"gemma-4-31b-it": {
"model": "gemma-4-31b-it",
"generateContentConfig": {
"temperature": 1,
"topP": 0.95,
"thinkingConfig": {
"includeThoughts": true,
"thinkingLevel": "HIGH"
},
"topK": 64
}
},
"gemma-4-26b-a4b-it": {
"model": "gemma-4-26b-a4b-it",
"generateContentConfig": {
"temperature": 1,
"topP": 0.95,
"thinkingConfig": {
"includeThoughts": true,
"thinkingLevel": "HIGH"
},
"topK": 64
}
},
"gemini-2.5-flash-base": {
"model": "gemini-2.5-flash",
"generateContentConfig": {
@@ -97,6 +97,30 @@
"topK": 64
}
},
"gemma-4-31b-it": {
"model": "gemma-4-31b-it",
"generateContentConfig": {
"temperature": 1,
"topP": 0.95,
"thinkingConfig": {
"includeThoughts": true,
"thinkingLevel": "HIGH"
},
"topK": 64
}
},
"gemma-4-26b-a4b-it": {
"model": "gemma-4-26b-a4b-it",
"generateContentConfig": {
"temperature": 1,
"topP": 0.95,
"thinkingConfig": {
"includeThoughts": true,
"thinkingLevel": "HIGH"
},
"topK": 64
}
},
"gemini-2.5-flash-base": {
"model": "gemini-2.5-flash",
"generateContentConfig": {
File diff suppressed because one or more lines are too long