feat(core): Integrate remote experiments configuration (#12539)

This commit is contained in:
Shreya Keshive
2025-11-04 15:09:53 -08:00
committed by GitHub
parent 53c7646ed7
commit da3da19844
11 changed files with 173 additions and 43 deletions

View File

@@ -1550,7 +1550,7 @@ describe('loadCliConfig compressionThreshold', () => {
},
};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getCompressionThreshold()).toBe(0.5);
expect(await config.getCompressionThreshold()).toBe(0.5);
});
it('should have undefined compressionThreshold if not in settings', async () => {
@@ -1558,7 +1558,7 @@ describe('loadCliConfig compressionThreshold', () => {
const argv = await parseArguments({} as Settings);
const settings: Settings = {};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getCompressionThreshold()).toBeUndefined();
expect(await config.getCompressionThreshold()).toBeUndefined();
});
});

View File

@@ -47,3 +47,5 @@ export { logModelSlashCommand } from './src/telemetry/loggers.js';
export { KeychainTokenStorage } from './src/mcp/token-storage/keychain-token-storage.js';
export * from './src/utils/googleQuotaErrors.js';
export type { GoogleApiError } from './src/utils/googleErrors.js';
export { getCodeAssistServer } from './src/code_assist/codeAssist.js';
export { getExperiments } from './src/code_assist/experiments/experiments.js';

View File

@@ -5,7 +5,7 @@
*/
import { getReleaseChannel } from '../../utils/channel.js';
import type { ClientMetadata, Platform } from './types.js';
import type { ClientMetadata, ClientMetadataPlatform } from '../types.js';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
@@ -15,7 +15,7 @@ const __dirname = path.dirname(__filename);
// Cache all client metadata.
let clientMetadataPromise: Promise<ClientMetadata> | undefined;
function getPlatform(): Platform {
function getPlatform(): ClientMetadataPlatform {
const platform = process.platform;
const arch = process.arch;
@@ -45,10 +45,10 @@ function getPlatform(): Platform {
export async function getClientMetadata(): Promise<ClientMetadata> {
if (!clientMetadataPromise) {
clientMetadataPromise = (async () => ({
ide_type: 'GEMINI_CLI',
ide_version: process.env['CLI_VERSION'] || process.version,
ideName: 'GEMINI_CLI',
ideVersion: process.env['CLI_VERSION'] || process.version,
platform: getPlatform(),
update_channel: await getReleaseChannel(__dirname),
updateChannel: await getReleaseChannel(__dirname),
}))();
}
return await clientMetadataPromise;

View File

@@ -44,6 +44,6 @@ function parseExperiments(response: ListExperimentsResponse): Experiments {
}
return {
flags,
experimentIds: response.experiment_ids ?? [],
experimentIds: response.experimentIds ?? [],
};
}

View File

@@ -4,26 +4,28 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { ClientMetadata } from '../types.js';
export interface ListExperimentsRequest {
project: string;
metadata?: ClientMetadata;
}
export interface ListExperimentsResponse {
experiment_ids?: number[];
experimentIds?: number[];
flags?: Flag[];
filtered_flags?: FilteredFlag[];
debug_string?: string;
filteredFlags?: FilteredFlag[];
debugString?: string;
}
export interface Flag {
name?: string;
bool_value?: boolean;
float_value?: number;
int_value?: string; // int64
string_value?: string;
int32_list_value?: Int32List;
string_list_value?: StringList;
boolValue?: boolean;
floatValue?: number;
intValue?: string; // int64
stringValue?: string;
int32ListValue?: Int32List;
stringListValue?: StringList;
}
export interface Int32List {
@@ -38,21 +40,3 @@ export interface FilteredFlag {
name?: string;
reason?: string;
}
export interface ClientMetadata {
ide_type?: IdeType;
ide_version?: string;
platform?: Platform;
update_channel?: 'nightly' | 'preview' | 'stable';
duet_project?: string;
}
export type IdeType = 'GEMINI_CLI';
export type Platform =
| 'PLATFORM_UNSPECIFIED'
| 'DARWIN_AMD64'
| 'DARWIN_ARM64'
| 'LINUX_AMD64'
| 'LINUX_ARM64'
| 'WINDOWS_AMD64';

View File

@@ -268,13 +268,13 @@ describe('CodeAssistServer', () => {
vi.spyOn(server, 'requestPost').mockResolvedValue(mockResponse);
const metadata = {
ide_version: 'v0.1.0',
ideVersion: 'v0.1.0',
};
const response = await server.listExperiments(metadata);
expect(server.requestPost).toHaveBeenCalledWith('listExperiments', {
project: 'test-project',
metadata: { ide_version: 'v0.1.0', duet_project: 'test-project' },
metadata: { ideVersion: 'v0.1.0', duetProject: 'test-project' },
});
expect(response).toEqual(mockResponse);
});

View File

@@ -13,11 +13,11 @@ import type {
LongRunningOperationResponse,
OnboardUserRequest,
SetCodeAssistGlobalUserSettingRequest,
ClientMetadata,
} from './types.js';
import type {
ListExperimentsRequest,
ListExperimentsResponse,
ClientMetadata,
} from './experiments/types.js';
import type {
CountTokensParameters,
@@ -163,7 +163,7 @@ export class CodeAssistServer implements ContentGenerator {
const projectId = this.projectId;
const req: ListExperimentsRequest = {
project: projectId,
metadata: { ...metadata, duet_project: projectId },
metadata: { ...metadata, duetProject: projectId },
};
return await this.requestPost<ListExperimentsResponse>(
'listExperiments',

View File

@@ -21,7 +21,8 @@ export type ClientMetadataIdeType =
| 'INTELLIJ'
| 'VSCODE_CLOUD_WORKSTATION'
| 'INTELLIJ_CLOUD_WORKSTATION'
| 'CLOUD_SHELL';
| 'CLOUD_SHELL'
| 'GEMINI_CLI';
export type ClientMetadataPlatform =
| 'PLATFORM_UNSPECIFIED'
| 'DARWIN_AMD64'

View File

@@ -230,6 +230,49 @@ describe('Server Config (config.ts)', () => {
'Config was already initialized',
);
});
describe('getCompressionThreshold', () => {
it('should return the local compression threshold if it is set', async () => {
const config = new Config({
...baseParams,
compressionThreshold: 0.5,
});
expect(await config.getCompressionThreshold()).toBe(0.5);
});
it('should return the remote experiment threshold if it is a positive number', async () => {
const config = new Config({
...baseParams,
experiments: {
flags: {
GeminiCLIContextCompression__threshold_fraction: {
floatValue: 0.8,
},
},
},
} as unknown as ConfigParameters);
expect(await config.getCompressionThreshold()).toBe(0.8);
});
it('should return undefined if the remote experiment threshold is 0', async () => {
const config = new Config({
...baseParams,
experiments: {
flags: {
GeminiCLIContextCompression__threshold_fraction: {
floatValue: 0.0,
},
},
},
} as unknown as ConfigParameters);
expect(await config.getCompressionThreshold()).toBeUndefined();
});
it('should return undefined if there are no experiments', async () => {
const config = new Config(baseParams);
expect(await config.getCompressionThreshold()).toBeUndefined();
});
});
});
describe('refreshAuth', () => {
@@ -1312,3 +1355,48 @@ describe('Config getHooks', () => {
expect(Object.keys(retrievedHooks!)).toHaveLength(11); // All hook event types
});
});
describe('Config getExperiments', () => {
const baseParams: ConfigParameters = {
cwd: '/tmp',
targetDir: '/path/to/target',
debugMode: false,
sessionId: 'test-session-id',
model: 'gemini-pro',
usageStatisticsEnabled: false,
};
it('should return undefined when no experiments are provided', () => {
const config = new Config(baseParams);
expect(config.getExperiments()).toBeUndefined();
});
it('should return empty object when empty experiments are provided', () => {
const configWithEmptyExps = new Config({
...baseParams,
experiments: { flags: {}, experimentIds: [] },
});
expect(configWithEmptyExps.getExperiments()).toEqual({
flags: {},
experimentIds: [],
});
});
it('should return the experiments configuration when provided', () => {
const mockExps = {
flags: {
testFlag: { boolValue: true },
},
experimentIds: [],
};
const config = new Config({
...baseParams,
experiments: mockExps,
});
const retrievedExps = config.getExperiments();
expect(retrievedExps).toEqual(mockExps);
expect(retrievedExps).toBe(mockExps); // Should return the same reference
});
});

View File

@@ -75,9 +75,13 @@ import { MessageBus } from '../confirmation-bus/message-bus.js';
import { PolicyEngine } from '../policy/policy-engine.js';
import type { PolicyEngineConfig } from '../policy/types.js';
import type { UserTierId } from '../code_assist/types.js';
import { getCodeAssistServer } from '../code_assist/codeAssist.js';
import type { Experiments } from '../code_assist/experiments/experiments.js';
import { AgentRegistry } from '../agents/registry.js';
import { setGlobalProxy } from '../utils/fetch.js';
import { SubagentToolWrapper } from '../agents/subagent-tool-wrapper.js';
import { getExperiments } from '../code_assist/experiments/experiments.js';
import { debugLogger } from '../utils/debugLogger.js';
import { ApprovalMode } from '../policy/types.js';
@@ -288,6 +292,7 @@ export interface ConfigParameters {
ptyInfo?: string;
disableYoloMode?: boolean;
enableHooks?: boolean;
experiments?: Experiments;
hooks?: {
[K in HookEventName]?: HookDefinition[];
};
@@ -396,6 +401,8 @@ export class Config {
private readonly hooks:
| { [K in HookEventName]?: HookDefinition[] }
| undefined;
private experiments: Experiments | undefined;
private experimentsPromise: Promise<void> | undefined;
constructor(params: ConfigParameters) {
this.sessionId = params.sessionId;
@@ -529,6 +536,7 @@ export class Config {
this.retryFetchErrors = params.retryFetchErrors ?? false;
this.disableYoloMode = params.disableYoloMode ?? false;
this.hooks = params.hooks;
this.experiments = params.experiments;
if (params.contextFileName) {
setGeminiMdFilename(params.contextFileName);
@@ -625,6 +633,20 @@ export class Config {
// Initialize BaseLlmClient now that the ContentGenerator is available
this.baseLlmClient = new BaseLlmClient(this.contentGenerator, this);
const codeAssistServer = getCodeAssistServer(this);
if (codeAssistServer) {
this.experimentsPromise = getExperiments(codeAssistServer)
.then((experiments) => {
this.setExperiments(experiments);
})
.catch((e) => {
debugLogger.error('Failed to fetch experiments', e);
});
} else {
this.experiments = undefined;
this.experimentsPromise = undefined;
}
// Reset the session flag since we're explicitly changing auth and using default model
this.inFallbackMode = false;
}
@@ -1064,8 +1086,26 @@ export class Config {
this.fileSystemService = fileSystemService;
}
getCompressionThreshold(): number | undefined {
return this.compressionThreshold;
async getCompressionThreshold(): Promise<number | undefined> {
if (this.compressionThreshold) {
return this.compressionThreshold;
}
if (this.experimentsPromise) {
try {
await this.experimentsPromise;
} catch (e) {
debugLogger.debug('Failed to fetch experiments', e);
}
}
const remoteThreshold =
this.experiments?.flags['GeminiCLIContextCompression__threshold_fraction']
?.floatValue;
if (remoteThreshold === 0) {
return undefined;
}
return remoteThreshold;
}
isInteractiveShellEnabled(): boolean {
@@ -1318,6 +1358,20 @@ export class Config {
getHooks(): { [K in HookEventName]?: HookDefinition[] } | undefined {
return this.hooks;
}
/**
* Get experiments configuration
*/
getExperiments(): Experiments | undefined {
return this.experiments;
}
/**
* Set experiments configuration
*/
setExperiments(experiments: Experiments): void {
this.experiments = experiments;
}
}
// Export model constants for use in CLI
export { DEFAULT_GEMINI_FLASH_MODEL };

View File

@@ -106,7 +106,8 @@ export class ChatCompressionService {
// Don't compress if not forced and we are under the limit.
if (!force) {
const threshold =
config.getCompressionThreshold() ?? DEFAULT_COMPRESSION_TOKEN_THRESHOLD;
(await config.getCompressionThreshold()) ??
DEFAULT_COMPRESSION_TOKEN_THRESHOLD;
if (originalTokenCount < threshold * tokenLimit(model)) {
return {
newHistory: null,