Make compression threshold editable in the UI. (#12317)

This commit is contained in:
Tommaso Sciortino
2025-10-30 16:03:58 -07:00
committed by GitHub
parent 643f2c0958
commit 3332703fca
10 changed files with 41 additions and 109 deletions
-15
View File
@@ -473,21 +473,6 @@ a few things you can try in order of recommendation:
"loadMemoryFromIncludeDirectories": true "loadMemoryFromIncludeDirectories": true
``` ```
- **`chatCompression`** (object):
- **Description:** Controls the settings for chat history compression, both
automatic and when manually invoked through the /compress command.
- **Properties:**
- **`contextPercentageThreshold`** (number): A value between 0 and 1 that
specifies the token threshold for compression as a percentage of the
model's total token limit. For example, a value of `0.6` will trigger
compression when the chat history exceeds 60% of the token limit.
- **Example:**
```json
"chatCompression": {
"contextPercentageThreshold": 0.6
}
```
- **`showLineNumbers`** (boolean): - **`showLineNumbers`** (boolean):
- **Description:** Controls whether line numbers are displayed in code blocks - **Description:** Controls whether line numbers are displayed in code blocks
in the CLI output. in the CLI output.
+3 -3
View File
@@ -245,13 +245,13 @@ their corresponding top-level category object in your `settings.json` file.
example `{"run_shell_command": {"tokenBudget": 2000}}` example `{"run_shell_command": {"tokenBudget": 2000}}`
- **Default:** `undefined` - **Default:** `undefined`
- **`model.chatCompression.contextPercentageThreshold`** (number): - **`model.compressionThreshold`** (number):
- **Description:** Sets the threshold for chat history compression as a - **Description:** Sets the threshold for chat history compression as a
percentage of the model's total token limit. This is a value between 0 and 1 fraction of the model's total token limit. This is a value between 0 and 1
that applies to both automatic compression and the manual `/compress` that applies to both automatic compression and the manual `/compress`
command. For example, a value of `0.6` will trigger compression when the command. For example, a value of `0.6` will trigger compression when the
chat history exceeds 60% of the token limit. chat history exceeds 60% of the token limit.
- **Default:** `0.7` - **Default:** `0.2`
- **`model.skipNextSpeakerCheck`** (boolean): - **`model.skipNextSpeakerCheck`** (boolean):
- **Description:** Skip the next speaker check. - **Description:** Skip the next speaker check.
+6 -10
View File
@@ -1545,7 +1545,7 @@ describe('loadCliConfig with includeDirectories', () => {
}); });
}); });
describe('loadCliConfig chatCompression', () => { describe('loadCliConfig compressionThreshold', () => {
beforeEach(() => { beforeEach(() => {
vi.resetAllMocks(); vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user'); vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
@@ -1558,28 +1558,24 @@ describe('loadCliConfig chatCompression', () => {
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
it('should pass chatCompression settings to the core config', async () => { it('should pass settings to the core config', async () => {
process.argv = ['node', 'script.js']; process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings); const argv = await parseArguments({} as Settings);
const settings: Settings = { const settings: Settings = {
model: { model: {
chatCompression: { compressionThreshold: 0.5,
contextPercentageThreshold: 0.5,
},
}, },
}; };
const config = await loadCliConfig(settings, 'test-session', argv); const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getChatCompression()).toEqual({ expect(config.getCompressionThreshold()).toBe(0.5);
contextPercentageThreshold: 0.5,
});
}); });
it('should have undefined chatCompression if not in settings', async () => { it('should have undefined compressionThreshold if not in settings', async () => {
process.argv = ['node', 'script.js']; process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings); const argv = await parseArguments({} as Settings);
const settings: Settings = {}; const settings: Settings = {};
const config = await loadCliConfig(settings, 'test-session', argv); const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getChatCompression()).toBeUndefined(); expect(config.getCompressionThreshold()).toBeUndefined();
}); });
}); });
+1 -1
View File
@@ -685,7 +685,7 @@ export async function loadCliConfig(
noBrowser: !!process.env['NO_BROWSER'], noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.model?.summarizeToolOutput, summarizeToolOutput: settings.model?.summarizeToolOutput,
ideMode, ideMode,
chatCompression: settings.model?.chatCompression, compressionThreshold: settings.model?.compressionThreshold,
folderTrust, folderTrust,
interactive, interactive,
trustedFolder, trustedFolder,
+15 -59
View File
@@ -1104,15 +1104,15 @@ describe('Settings Loading and Merging', () => {
}); });
}); });
it('should merge chatCompression settings, with workspace taking precedence', () => { it('should merge compressionThreshold settings, with workspace taking precedence', () => {
(mockFsExistsSync as Mock).mockReturnValue(true); (mockFsExistsSync as Mock).mockReturnValue(true);
const userSettingsContent = { const userSettingsContent = {
general: {}, general: {},
model: { chatCompression: { contextPercentageThreshold: 0.5 } }, model: { compressionThreshold: 0.5 },
}; };
const workspaceSettingsContent = { const workspaceSettingsContent = {
general: {}, general: {},
model: { chatCompression: { contextPercentageThreshold: 0.8 } }, model: { compressionThreshold: 0.8 },
}; };
(fs.readFileSync as Mock).mockImplementation( (fs.readFileSync as Mock).mockImplementation(
@@ -1127,15 +1127,11 @@ describe('Settings Loading and Merging', () => {
const settings = loadSettings(MOCK_WORKSPACE_DIR); const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.user.settings.model?.chatCompression).toEqual({ expect(settings.user.settings.model?.compressionThreshold).toEqual(0.5);
contextPercentageThreshold: 0.5, expect(settings.workspace.settings.model?.compressionThreshold).toEqual(
}); 0.8,
expect(settings.workspace.settings.model?.chatCompression).toEqual({ );
contextPercentageThreshold: 0.8, expect(settings.merged.model?.compressionThreshold).toEqual(0.8);
});
expect(settings.merged.model?.chatCompression).toEqual({
contextPercentageThreshold: 0.8,
});
}); });
it('should merge output format settings, with workspace taking precedence', () => { it('should merge output format settings, with workspace taking precedence', () => {
@@ -1162,13 +1158,13 @@ describe('Settings Loading and Merging', () => {
expect(settings.merged.output?.format).toBe('json'); expect(settings.merged.output?.format).toBe('json');
}); });
it('should handle chatCompression when only in user settings', () => { it('should handle compressionThreshold when only in user settings', () => {
(mockFsExistsSync as Mock).mockImplementation( (mockFsExistsSync as Mock).mockImplementation(
(p: fs.PathLike) => p === USER_SETTINGS_PATH, (p: fs.PathLike) => p === USER_SETTINGS_PATH,
); );
const userSettingsContent = { const userSettingsContent = {
general: {}, general: {},
model: { chatCompression: { contextPercentageThreshold: 0.5 } }, model: { compressionThreshold: 0.5 },
}; };
(fs.readFileSync as Mock).mockImplementation( (fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => { (p: fs.PathOrFileDescriptor) => {
@@ -1179,9 +1175,7 @@ describe('Settings Loading and Merging', () => {
); );
const settings = loadSettings(MOCK_WORKSPACE_DIR); const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.model?.chatCompression).toEqual({ expect(settings.merged.model?.compressionThreshold).toEqual(0.5);
contextPercentageThreshold: 0.5,
});
}); });
it('should have model as undefined if not in any settings file', () => { it('should have model as undefined if not in any settings file', () => {
@@ -1191,39 +1185,15 @@ describe('Settings Loading and Merging', () => {
expect(settings.merged.model).toBeUndefined(); expect(settings.merged.model).toBeUndefined();
}); });
it('should ignore chatCompression if contextPercentageThreshold is invalid', () => { it('should use user compressionThreshold if workspace does not define it', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
(mockFsExistsSync as Mock).mockImplementation(
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
);
const userSettingsContent = {
general: {},
model: { chatCompression: { contextPercentageThreshold: 1.5 } },
};
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === USER_SETTINGS_PATH)
return JSON.stringify(userSettingsContent);
return '{}';
},
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.model?.chatCompression).toEqual({
contextPercentageThreshold: 1.5,
});
warnSpy.mockRestore();
});
it('should deep merge chatCompression settings', () => {
(mockFsExistsSync as Mock).mockReturnValue(true); (mockFsExistsSync as Mock).mockReturnValue(true);
const userSettingsContent = { const userSettingsContent = {
general: {}, general: {},
model: { chatCompression: { contextPercentageThreshold: 0.5 } }, model: { compressionThreshold: 0.5 },
}; };
const workspaceSettingsContent = { const workspaceSettingsContent = {
general: {}, general: {},
model: { chatCompression: {} }, model: {},
}; };
(fs.readFileSync as Mock).mockImplementation( (fs.readFileSync as Mock).mockImplementation(
@@ -1238,9 +1208,7 @@ describe('Settings Loading and Merging', () => {
const settings = loadSettings(MOCK_WORKSPACE_DIR); const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.model?.chatCompression).toEqual({ expect(settings.merged.model?.compressionThreshold).toEqual(0.5);
contextPercentageThreshold: 0.5,
});
}); });
it('should merge includeDirectories from all scopes', () => { it('should merge includeDirectories from all scopes', () => {
@@ -2025,9 +1993,6 @@ describe('Settings Loading and Merging', () => {
}, },
model: { model: {
name: 'gemini-pro', name: 'gemini-pro',
chatCompression: {
contextPercentageThreshold: 0.5,
},
}, },
mcpServers: { mcpServers: {
'server-1': { 'server-1': {
@@ -2046,9 +2011,6 @@ describe('Settings Loading and Merging', () => {
myTheme: {}, myTheme: {},
}, },
model: 'gemini-pro', model: 'gemini-pro',
chatCompression: {
contextPercentageThreshold: 0.5,
},
mcpServers: { mcpServers: {
'server-1': { 'server-1': {
command: 'node server.js', command: 'node server.js',
@@ -2088,9 +2050,6 @@ describe('Settings Loading and Merging', () => {
}, },
model: { model: {
name: 'gemini-pro', name: 'gemini-pro',
chatCompression: {
contextPercentageThreshold: 0.8,
},
}, },
context: { context: {
fileName: 'CONTEXT.md', fileName: 'CONTEXT.md',
@@ -2130,9 +2089,6 @@ describe('Settings Loading and Merging', () => {
theme: 'dark', theme: 'dark',
usageStatisticsEnabled: false, usageStatisticsEnabled: false,
model: 'gemini-pro', model: 'gemini-pro',
chatCompression: {
contextPercentageThreshold: 0.8,
},
contextFileName: 'CONTEXT.md', contextFileName: 'CONTEXT.md',
includeDirectories: ['/src'], includeDirectories: ['/src'],
sandbox: true, sandbox: true,
+1 -1
View File
@@ -64,7 +64,7 @@ const MIGRATION_MAP: Record<string, string> = {
autoAccept: 'tools.autoAccept', autoAccept: 'tools.autoAccept',
autoConfigureMaxOldSpaceSize: 'advanced.autoConfigureMemory', autoConfigureMaxOldSpaceSize: 'advanced.autoConfigureMemory',
bugCommand: 'advanced.bugCommand', bugCommand: 'advanced.bugCommand',
chatCompression: 'model.chatCompression', chatCompression: 'model.compressionThreshold',
checkpointing: 'general.checkpointing', checkpointing: 'general.checkpointing',
coreTools: 'tools.core', coreTools: 'tools.core',
contextFileName: 'context.fileName', contextFileName: 'context.fileName',
+7 -7
View File
@@ -14,7 +14,6 @@ import type {
BugCommandSettings, BugCommandSettings,
TelemetrySettings, TelemetrySettings,
AuthType, AuthType,
ChatCompressionSettings,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { import {
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
@@ -578,14 +577,15 @@ const SETTINGS_SCHEMA = {
description: 'Settings for summarizing tool output.', description: 'Settings for summarizing tool output.',
showInDialog: false, showInDialog: false,
}, },
chatCompression: { compressionThreshold: {
type: 'object', type: 'number',
label: 'Chat Compression', label: 'Compression Threshold',
category: 'Model', category: 'Model',
requiresRestart: false, requiresRestart: false,
default: undefined as ChatCompressionSettings | undefined, default: 0.2 as number,
description: 'Chat compression settings.', description:
showInDialog: false, 'The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3).',
showInDialog: true,
}, },
skipNextSpeakerCheck: { skipNextSpeakerCheck: {
type: 'boolean', type: 'boolean',
+5 -9
View File
@@ -92,10 +92,6 @@ export interface BugCommandSettings {
urlTemplate: string; urlTemplate: string;
} }
export interface ChatCompressionSettings {
contextPercentageThreshold?: number;
}
export interface SummarizeToolOutputSettings { export interface SummarizeToolOutputSettings {
tokenBudget?: number; tokenBudget?: number;
} }
@@ -262,7 +258,7 @@ export interface ConfigParameters {
folderTrust?: boolean; folderTrust?: boolean;
ideMode?: boolean; ideMode?: boolean;
loadMemoryFromIncludeDirectories?: boolean; loadMemoryFromIncludeDirectories?: boolean;
chatCompression?: ChatCompressionSettings; compressionThreshold?: number;
interactive?: boolean; interactive?: boolean;
trustedFolder?: boolean; trustedFolder?: boolean;
useRipgrep?: boolean; useRipgrep?: boolean;
@@ -359,7 +355,7 @@ export class Config {
| undefined; | undefined;
private readonly experimentalZedIntegration: boolean = false; private readonly experimentalZedIntegration: boolean = false;
private readonly loadMemoryFromIncludeDirectories: boolean = false; private readonly loadMemoryFromIncludeDirectories: boolean = false;
private readonly chatCompression: ChatCompressionSettings | undefined; private readonly compressionThreshold: number | undefined;
private readonly interactive: boolean; private readonly interactive: boolean;
private readonly ptyInfo: string; private readonly ptyInfo: string;
private readonly trustedFolder: boolean | undefined; private readonly trustedFolder: boolean | undefined;
@@ -462,7 +458,7 @@ export class Config {
this.ideMode = params.ideMode ?? false; this.ideMode = params.ideMode ?? false;
this.loadMemoryFromIncludeDirectories = this.loadMemoryFromIncludeDirectories =
params.loadMemoryFromIncludeDirectories ?? false; params.loadMemoryFromIncludeDirectories ?? false;
this.chatCompression = params.chatCompression; this.compressionThreshold = params.compressionThreshold;
this.interactive = params.interactive ?? false; this.interactive = params.interactive ?? false;
this.ptyInfo = params.ptyInfo ?? 'child_process'; this.ptyInfo = params.ptyInfo ?? 'child_process';
this.trustedFolder = params.trustedFolder; this.trustedFolder = params.trustedFolder;
@@ -1003,8 +999,8 @@ export class Config {
this.fileSystemService = fileSystemService; this.fileSystemService = fileSystemService;
} }
getChatCompression(): ChatCompressionSettings | undefined { getCompressionThreshold(): number | undefined {
return this.chatCompression; return this.compressionThreshold;
} }
isInteractiveShellEnabled(): boolean { isInteractiveShellEnabled(): boolean {
@@ -70,7 +70,7 @@ describe('findCompressSplitPoint', () => {
expect(findCompressSplitPoint(history, 0.8)).toBe(4); expect(findCompressSplitPoint(history, 0.8)).toBe(4);
}); });
it('should return earlier splitpoint if no valid ones are after threshhold', () => { it('should return earlier splitpoint if no valid ones are after threshold', () => {
const history: Content[] = [ const history: Content[] = [
{ role: 'user', parts: [{ text: 'This is the first message.' }] }, { role: 'user', parts: [{ text: 'This is the first message.' }] },
{ role: 'model', parts: [{ text: 'This is the second message.' }] }, { role: 'model', parts: [{ text: 'This is the second message.' }] },
@@ -115,7 +115,7 @@ describe('ChatCompressionService', () => {
getLastPromptTokenCount: vi.fn().mockReturnValue(500), getLastPromptTokenCount: vi.fn().mockReturnValue(500),
} as unknown as GeminiChat; } as unknown as GeminiChat;
mockConfig = { mockConfig = {
getChatCompression: vi.fn(), getCompressionThreshold: vi.fn(),
getContentGenerator: vi.fn(), getContentGenerator: vi.fn(),
} as unknown as Config; } as unknown as Config;
@@ -106,8 +106,7 @@ export class ChatCompressionService {
// Don't compress if not forced and we are under the limit. // Don't compress if not forced and we are under the limit.
if (!force) { if (!force) {
const threshold = const threshold =
config.getChatCompression()?.contextPercentageThreshold ?? config.getCompressionThreshold() ?? DEFAULT_COMPRESSION_TOKEN_THRESHOLD;
DEFAULT_COMPRESSION_TOKEN_THRESHOLD;
if (originalTokenCount < threshold * tokenLimit(model)) { if (originalTokenCount < threshold * tokenLimit(model)) {
return { return {
newHistory: null, newHistory: null,