From d2a6b303983dd4eaee9c930300e18bb55f6b188e Mon Sep 17 00:00:00 2001 From: Christian Gunderman Date: Tue, 9 Dec 2025 16:38:33 -0800 Subject: [PATCH] Send the model and CLI version with the user agent (#14865) --- packages/cli/src/config/config.ts | 4 +-- packages/cli/src/gemini.test.tsx | 9 ++---- packages/cli/src/gemini.tsx | 4 +-- .../cli/src/ui/commands/aboutCommand.test.ts | 10 ++---- packages/cli/src/ui/commands/aboutCommand.ts | 4 +-- .../cli/src/ui/commands/bugCommand.test.ts | 6 ++-- packages/cli/src/ui/commands/bugCommand.ts | 5 ++- .../experiments/client_metadata.test.ts | 20 +++++------- .../experiments/client_metadata.ts | 3 +- .../core/src/core/contentGenerator.test.ts | 32 +++++++++++++++++-- packages/core/src/core/contentGenerator.ts | 10 ++++-- packages/core/src/index.ts | 1 + .../{cli => core}/src/utils/version.test.ts | 12 +++---- packages/{cli => core}/src/utils/version.ts | 4 +-- 14 files changed, 74 insertions(+), 50 deletions(-) rename packages/{cli => core}/src/utils/version.test.ts (79%) rename packages/{cli => core}/src/utils/version.ts (76%) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 81fc01f001..c266e2ecc3 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -31,10 +31,10 @@ import { debugLogger, loadServerHierarchicalMemory, WEB_FETCH_TOOL_NAME, + getVersion, } from '@google/gemini-cli-core'; import type { Settings } from './settings.js'; -import { getCliVersion } from '../utils/version.js'; import { loadSandboxConfig } from './sandboxConfig.js'; import { resolvePath } from '../utils/resolvePath.js'; import { appEvents } from '../utils/events.js'; @@ -288,7 +288,7 @@ export async function parseArguments(settings: Settings): Promise { } yargsInstance - .version(await getCliVersion()) // This will enable the --version flag based on package.json + .version(await getVersion()) // This will enable the --version flag based on package.json .alias('v', 'version') .help() .alias('h', 'help') diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 5f0ddae622..67cac364f3 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -70,6 +70,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { disableMouseEvents: vi.fn(), enterAlternateScreen: vi.fn(), disableLineWrapping: vi.fn(), + getVersion: vi.fn(() => Promise.resolve('1.0.0')), }; }); @@ -1294,10 +1295,6 @@ describe('startInteractiveUI', () => { geminiMdFileCount: 0, }; - vi.mock('./utils/version.js', () => ({ - getCliVersion: vi.fn(() => Promise.resolve('1.0.0')), - })); - vi.mock('./ui/utils/kittyProtocolDetector.js', () => ({ detectAndEnableKittyProtocol: vi.fn(() => Promise.resolve(true)), isKittyProtocolSupported: vi.fn(() => true), @@ -1399,7 +1396,7 @@ describe('startInteractiveUI', () => { }); it('should perform all startup tasks in correct order', async () => { - const { getCliVersion } = await import('./utils/version.js'); + const { getVersion } = await import('@google/gemini-cli-core'); const { checkForUpdates } = await import('./ui/utils/updateCheck.js'); const { registerCleanup } = await import('./utils/cleanup.js'); @@ -1413,7 +1410,7 @@ describe('startInteractiveUI', () => { ); // Verify all startup tasks were called - expect(getCliVersion).toHaveBeenCalledTimes(1); + expect(getVersion).toHaveBeenCalledTimes(1); expect(registerCleanup).toHaveBeenCalledTimes(3); // Verify cleanup handler is registered with unmount function diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index a20fef8613..630f8d827c 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -33,7 +33,6 @@ import { runExitCleanup, registerTelemetryConfig, } from './utils/cleanup.js'; -import { getCliVersion } from './utils/version.js'; import { type Config, type ResumedSessionData, @@ -63,6 +62,7 @@ import { SessionEndReason, fireSessionStartHook, fireSessionEndHook, + getVersion, } from '@google/gemini-cli-core'; import { initializeApp, @@ -196,7 +196,7 @@ export async function startInteractiveUI( }); } - const version = await getCliVersion(); + const version = await getVersion(); setWindowTitle(basename(workspaceRoot), settings); const consolePatcher = new ConsolePatcher({ diff --git a/packages/cli/src/ui/commands/aboutCommand.test.ts b/packages/cli/src/ui/commands/aboutCommand.test.ts index be066783c8..b21dfa5233 100644 --- a/packages/cli/src/ui/commands/aboutCommand.test.ts +++ b/packages/cli/src/ui/commands/aboutCommand.test.ts @@ -8,9 +8,8 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { aboutCommand } from './aboutCommand.js'; import { type CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; -import * as versionUtils from '../../utils/version.js'; import { MessageType } from '../types.js'; -import { IdeClient } from '@google/gemini-cli-core'; +import { IdeClient, getVersion } from '@google/gemini-cli-core'; vi.mock('@google/gemini-cli-core', async (importOriginal) => { const actual = @@ -25,13 +24,10 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { UserAccountManager: vi.fn().mockImplementation(() => ({ getCachedGoogleAccount: vi.fn().mockReturnValue('test-email@example.com'), })), + getVersion: vi.fn(), }; }); -vi.mock('../../utils/version.js', () => ({ - getCliVersion: vi.fn(), -})); - describe('aboutCommand', () => { let mockContext: CommandContext; const originalPlatform = process.platform; @@ -59,7 +55,7 @@ describe('aboutCommand', () => { }, } as unknown as CommandContext); - vi.mocked(versionUtils.getCliVersion).mockResolvedValue('test-version'); + vi.mocked(getVersion).mockResolvedValue('test-version'); vi.spyOn(mockContext.services.config!, 'getModel').mockReturnValue( 'test-model', ); diff --git a/packages/cli/src/ui/commands/aboutCommand.ts b/packages/cli/src/ui/commands/aboutCommand.ts index 7870d2558a..9b4cc34d0c 100644 --- a/packages/cli/src/ui/commands/aboutCommand.ts +++ b/packages/cli/src/ui/commands/aboutCommand.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { getCliVersion } from '../../utils/version.js'; import type { CommandContext, SlashCommand } from './types.js'; import { CommandKind } from './types.js'; import process from 'node:process'; @@ -13,6 +12,7 @@ import { IdeClient, UserAccountManager, debugLogger, + getVersion, } from '@google/gemini-cli-core'; export const aboutCommand: SlashCommand = { @@ -31,7 +31,7 @@ export const aboutCommand: SlashCommand = { })`; } const modelVersion = context.services.config?.getModel() || 'Unknown'; - const cliVersion = await getCliVersion(); + const cliVersion = await getVersion(); const selectedAuthType = context.services.settings.merged.security?.auth?.selectedType || ''; const gcpProject = process.env['GOOGLE_CLOUD_PROJECT'] || ''; diff --git a/packages/cli/src/ui/commands/bugCommand.test.ts b/packages/cli/src/ui/commands/bugCommand.test.ts index a050620e9a..c071368c8f 100644 --- a/packages/cli/src/ui/commands/bugCommand.test.ts +++ b/packages/cli/src/ui/commands/bugCommand.test.ts @@ -8,13 +8,12 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import open from 'open'; import { bugCommand } from './bugCommand.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; -import { getCliVersion } from '../../utils/version.js'; +import { getVersion } from '@google/gemini-cli-core'; import { GIT_COMMIT_INFO } from '../../generated/git-commit.js'; import { formatMemoryUsage } from '../utils/formatters.js'; // Mock dependencies vi.mock('open'); -vi.mock('../../utils/version.js'); vi.mock('../utils/formatters.js'); vi.mock('@google/gemini-cli-core', async (importOriginal) => { const actual = @@ -27,6 +26,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { }), }, sessionId: 'test-session-id', + getVersion: vi.fn(), }; }); vi.mock('node:process', () => ({ @@ -41,7 +41,7 @@ vi.mock('node:process', () => ({ describe('bugCommand', () => { beforeEach(() => { - vi.mocked(getCliVersion).mockResolvedValue('0.1.0'); + vi.mocked(getVersion).mockResolvedValue('0.1.0'); vi.mocked(formatMemoryUsage).mockReturnValue('100 MB'); vi.stubEnv('SANDBOX', 'gemini-test'); }); diff --git a/packages/cli/src/ui/commands/bugCommand.ts b/packages/cli/src/ui/commands/bugCommand.ts index 7e69ec113f..106c1169d5 100644 --- a/packages/cli/src/ui/commands/bugCommand.ts +++ b/packages/cli/src/ui/commands/bugCommand.ts @@ -14,8 +14,7 @@ import { import { MessageType } from '../types.js'; import { GIT_COMMIT_INFO } from '../../generated/git-commit.js'; import { formatMemoryUsage } from '../utils/formatters.js'; -import { getCliVersion } from '../../utils/version.js'; -import { IdeClient, sessionId } from '@google/gemini-cli-core'; +import { IdeClient, sessionId, getVersion } from '@google/gemini-cli-core'; export const bugCommand: SlashCommand = { name: 'bug', @@ -36,7 +35,7 @@ export const bugCommand: SlashCommand = { })`; } const modelVersion = config?.getModel() || 'Unknown'; - const cliVersion = await getCliVersion(); + const cliVersion = await getVersion(); const memoryUsage = formatMemoryUsage(process.memoryUsage().rss); const ideClient = await getIdeClientName(context); diff --git a/packages/core/src/code_assist/experiments/client_metadata.test.ts b/packages/core/src/code_assist/experiments/client_metadata.test.ts index 63a5b71e89..311a88c377 100644 --- a/packages/core/src/code_assist/experiments/client_metadata.test.ts +++ b/packages/core/src/code_assist/experiments/client_metadata.test.ts @@ -6,6 +6,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { ReleaseChannel, getReleaseChannel } from '../../utils/channel.js'; +import { getVersion } from '../../utils/version.js'; // Mock dependencies before importing the module under test vi.mock('../../utils/channel.js', async () => { @@ -16,6 +17,10 @@ vi.mock('../../utils/channel.js', async () => { }; }); +vi.mock('../../utils/version.js', async () => ({ + getVersion: vi.fn(), +})); + describe('client_metadata', () => { const originalPlatform = process.platform; const originalArch = process.arch; @@ -29,6 +34,7 @@ describe('client_metadata', () => { await import('./client_metadata.js'); // Provide a default mock implementation for each test vi.mocked(getReleaseChannel).mockResolvedValue(ReleaseChannel.STABLE); + vi.mocked(getVersion).mockResolvedValue('0.0.0'); }); afterEach(() => { @@ -64,24 +70,14 @@ describe('client_metadata', () => { }); describe('getClientMetadata', () => { - it('should use CLI_VERSION for ideVersion if set', async () => { - process.env['CLI_VERSION'] = '1.2.3'; - Object.defineProperty(process, 'version', { value: 'v18.0.0' }); + it('should use version from getCliVersion for ideVersion', async () => { + vi.mocked(getVersion).mockResolvedValue('1.2.3'); const { getClientMetadata } = await import('./client_metadata.js'); const metadata = await getClientMetadata(); expect(metadata.ideVersion).toBe('1.2.3'); }); - it('should use process.version for ideVersion as a fallback', async () => { - delete process.env['CLI_VERSION']; - Object.defineProperty(process, 'version', { value: 'v20.0.0' }); - const { getClientMetadata } = await import('./client_metadata.js'); - - const metadata = await getClientMetadata(); - expect(metadata.ideVersion).toBe('v20.0.0'); - }); - it('should call getReleaseChannel to get the update channel', async () => { vi.mocked(getReleaseChannel).mockResolvedValue(ReleaseChannel.NIGHTLY); const { getClientMetadata } = await import('./client_metadata.js'); diff --git a/packages/core/src/code_assist/experiments/client_metadata.ts b/packages/core/src/code_assist/experiments/client_metadata.ts index 5d76cb742e..982aa384f7 100644 --- a/packages/core/src/code_assist/experiments/client_metadata.ts +++ b/packages/core/src/code_assist/experiments/client_metadata.ts @@ -8,6 +8,7 @@ import { getReleaseChannel } from '../../utils/channel.js'; import type { ClientMetadata, ClientMetadataPlatform } from '../types.js'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; +import { getVersion } from '../../utils/version.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -47,7 +48,7 @@ export async function getClientMetadata(): Promise { clientMetadataPromise = (async () => ({ ideName: 'IDE_UNSPECIFIED', pluginType: 'GEMINI', - ideVersion: process.env['CLI_VERSION'] || process.version, + ideVersion: await getVersion(), platform: getPlatform(), updateChannel: await getReleaseChannel(__dirname), }))(); diff --git a/packages/core/src/core/contentGenerator.test.ts b/packages/core/src/core/contentGenerator.test.ts index daf6e35878..f5f46d7d68 100644 --- a/packages/core/src/core/contentGenerator.test.ts +++ b/packages/core/src/core/contentGenerator.test.ts @@ -27,7 +27,13 @@ vi.mock('./apiKeyCredentialStorage.js', () => ({ vi.mock('./fakeContentGenerator.js'); -const mockConfig = {} as unknown as Config; +const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), + getUsageStatisticsEnabled: vi.fn().mockReturnValue(true), + isInFallbackMode: vi.fn().mockReturnValue(false), + getPreviewFeatures: vi.fn().mockReturnValue(false), +} as unknown as Config; describe('createContentGenerator', () => { beforeEach(() => { @@ -111,9 +117,16 @@ describe('createContentGenerator', () => { it('should create a GoogleGenAI content generator', async () => { const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => true, + isInFallbackMode: vi.fn().mockReturnValue(false), + getPreviewFeatures: vi.fn().mockReturnValue(false), } as unknown as Config; + // Set a fixed version for testing + vi.stubEnv('CLI_VERSION', '1.2.3'); + const mockGenerator = { models: {}, } as unknown as GoogleGenAI; @@ -130,7 +143,7 @@ describe('createContentGenerator', () => { vertexai: undefined, httpOptions: { headers: { - 'User-Agent': expect.any(String), + 'User-Agent': expect.stringContaining('GeminiCLI/1.2.3/gemini-pro'), 'x-gemini-api-privileged-user-id': expect.any(String), }, }, @@ -176,7 +189,11 @@ describe('createContentGenerator', () => { it('should include custom headers from GEMINI_CLI_CUSTOM_HEADERS for GoogleGenAI requests without inferring auth mechanism', async () => { const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => false, + isInFallbackMode: vi.fn().mockReturnValue(false), + getPreviewFeatures: vi.fn().mockReturnValue(false), } as unknown as Config; const mockGenerator = { @@ -220,7 +237,11 @@ describe('createContentGenerator', () => { it('should pass api key as Authorization Header when GEMINI_API_KEY_AUTH_MECHANISM is set to bearer', async () => { const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => false, + isInFallbackMode: vi.fn().mockReturnValue(false), + getPreviewFeatures: vi.fn().mockReturnValue(false), } as unknown as Config; const mockGenerator = { @@ -251,7 +272,11 @@ describe('createContentGenerator', () => { it('should not pass api key as Authorization Header when GEMINI_API_KEY_AUTH_MECHANISM is not set (default behavior)', async () => { const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => false, + isInFallbackMode: vi.fn().mockReturnValue(false), + getPreviewFeatures: vi.fn().mockReturnValue(false), } as unknown as Config; const mockGenerator = { @@ -291,7 +316,10 @@ describe('createContentGenerator', () => { it('should create a GoogleGenAI content generator with client install id logging disabled', async () => { const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), getUsageStatisticsEnabled: () => false, + isInFallbackMode: vi.fn().mockReturnValue(false), + getPreviewFeatures: vi.fn().mockReturnValue(false), } as unknown as Config; const mockGenerator = { models: {}, diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 8d6a8ad4ae..0861a79864 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -23,6 +23,7 @@ import { InstallationManager } from '../utils/installationManager.js'; import { FakeContentGenerator } from './fakeContentGenerator.js'; import { parseCustomHeaders } from '../utils/customHeaderUtils.js'; import { RecordingContentGenerator } from './recordingContentGenerator.js'; +import { getVersion, getEffectiveModel } from '../../index.js'; /** * Interface abstracting the core functionalities for generating content and counting tokens. @@ -115,10 +116,15 @@ export async function createContentGenerator( if (gcConfig.fakeResponses) { return FakeContentGenerator.fromFile(gcConfig.fakeResponses); } - const version = process.env['CLI_VERSION'] || process.version; + const version = await getVersion(); + const model = getEffectiveModel( + gcConfig.isInFallbackMode(), + gcConfig.getModel(), + gcConfig.getPreviewFeatures(), + ); const customHeadersEnv = process.env['GEMINI_CLI_CUSTOM_HEADERS'] || undefined; - const userAgent = `GeminiCLI/${version} (${process.platform}; ${process.arch})`; + const userAgent = `GeminiCLI/${version}/${model} (${process.platform}; ${process.arch})`; const customHeadersMap = parseCustomHeaders(customHeadersEnv); const apiKeyAuthMechanism = process.env['GEMINI_API_KEY_AUTH_MECHANISM'] || 'x-goog-api-key'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index bb9521860c..b268ea12df 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -78,6 +78,7 @@ export * from './utils/debugLogger.js'; export * from './utils/events.js'; export * from './utils/extensionLoader.js'; export * from './utils/package.js'; +export * from './utils/version.js'; export * from './utils/checkpointUtils.js'; // Export services diff --git a/packages/cli/src/utils/version.test.ts b/packages/core/src/utils/version.test.ts similarity index 79% rename from packages/cli/src/utils/version.test.ts rename to packages/core/src/utils/version.test.ts index 8e811efa39..775860b629 100644 --- a/packages/cli/src/utils/version.test.ts +++ b/packages/core/src/utils/version.test.ts @@ -5,10 +5,10 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { getCliVersion } from './version.js'; -import { getPackageJson } from '@google/gemini-cli-core'; +import { getVersion } from './version.js'; +import { getPackageJson } from './package.js'; -vi.mock('@google/gemini-cli-core', () => ({ +vi.mock('./package.js', () => ({ getPackageJson: vi.fn(), })); @@ -27,20 +27,20 @@ describe('version', () => { it('should return CLI_VERSION from env if set', async () => { process.env['CLI_VERSION'] = '2.0.0'; - const version = await getCliVersion(); + const version = await getVersion(); expect(version).toBe('2.0.0'); }); it('should return version from package.json if CLI_VERSION is not set', async () => { delete process.env['CLI_VERSION']; - const version = await getCliVersion(); + const version = await getVersion(); expect(version).toBe('1.0.0'); }); it('should return "unknown" if package.json is not found and CLI_VERSION is not set', async () => { delete process.env['CLI_VERSION']; vi.mocked(getPackageJson).mockResolvedValue(undefined); - const version = await getCliVersion(); + const version = await getVersion(); expect(version).toBe('unknown'); }); }); diff --git a/packages/cli/src/utils/version.ts b/packages/core/src/utils/version.ts similarity index 76% rename from packages/cli/src/utils/version.ts rename to packages/core/src/utils/version.ts index 82c55a1129..ecd7a8b503 100644 --- a/packages/cli/src/utils/version.ts +++ b/packages/core/src/utils/version.ts @@ -4,14 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { getPackageJson } from '@google/gemini-cli-core'; +import { getPackageJson } from './package.js'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -export async function getCliVersion(): Promise { +export async function getVersion(): Promise { const pkgJson = await getPackageJson(__dirname); return process.env['CLI_VERSION'] || pkgJson?.version || 'unknown'; }