mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 11:34:44 -07:00
feat(core): differentiate User-Agent for a2a-server and ACP clients (#22059)
This commit is contained in:
@@ -502,6 +502,7 @@ export interface PolicyUpdateConfirmationRequest {
|
||||
|
||||
export interface ConfigParameters {
|
||||
sessionId: string;
|
||||
clientName?: string;
|
||||
clientVersion?: string;
|
||||
embeddingModel?: string;
|
||||
sandbox?: SandboxConfig;
|
||||
@@ -646,6 +647,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly acknowledgedAgentsService: AcknowledgedAgentsService;
|
||||
private skillManager!: SkillManager;
|
||||
private _sessionId: string;
|
||||
private readonly clientName: string | undefined;
|
||||
private clientVersion: string;
|
||||
private fileSystemService: FileSystemService;
|
||||
private trackerService?: TrackerService;
|
||||
@@ -843,6 +845,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
|
||||
constructor(params: ConfigParameters) {
|
||||
this._sessionId = params.sessionId;
|
||||
this.clientName = params.clientName;
|
||||
this.clientVersion = params.clientVersion ?? 'unknown';
|
||||
this.approvedPlanPath = undefined;
|
||||
this.embeddingModel =
|
||||
@@ -1408,6 +1411,10 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.promptId;
|
||||
}
|
||||
|
||||
getClientName(): string | undefined {
|
||||
return this.clientName;
|
||||
}
|
||||
|
||||
setSessionId(sessionId: string): void {
|
||||
this._sessionId = sessionId;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ const mockConfig = {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: vi.fn().mockReturnValue(true),
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
describe('createContentGenerator', () => {
|
||||
@@ -53,6 +54,7 @@ describe('createContentGenerator', () => {
|
||||
const fakeResponsesFile = 'fake/responses.yaml';
|
||||
const mockConfigWithFake = {
|
||||
fakeResponses: fakeResponsesFile,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
const generator = await createContentGenerator(
|
||||
{
|
||||
@@ -74,6 +76,7 @@ describe('createContentGenerator', () => {
|
||||
const mockConfigWithRecordResponses = {
|
||||
fakeResponses: fakeResponsesFile,
|
||||
recordResponses: recordResponsesFile,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
const generator = await createContentGenerator(
|
||||
{
|
||||
@@ -123,6 +126,7 @@ describe('createContentGenerator', () => {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
// Set a fixed version for testing
|
||||
@@ -144,7 +148,9 @@ describe('createContentGenerator', () => {
|
||||
vertexai: undefined,
|
||||
httpOptions: expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
'User-Agent': expect.stringContaining('GeminiCLI/1.2.3/gemini-pro'),
|
||||
'User-Agent': expect.stringMatching(
|
||||
/GeminiCLI\/1\.2\.3\/gemini-pro \(.*; .*; .*\)/,
|
||||
),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
@@ -153,6 +159,40 @@ describe('createContentGenerator', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should include clientName prefix in User-Agent when specified', async () => {
|
||||
const mockConfig = {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getClientName: vi.fn().mockReturnValue('a2a-server'),
|
||||
} as unknown as Config;
|
||||
|
||||
// Set a fixed version for testing
|
||||
vi.stubEnv('CLI_VERSION', '1.2.3');
|
||||
|
||||
const mockGenerator = {
|
||||
models: {},
|
||||
} as unknown as GoogleGenAI;
|
||||
vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never);
|
||||
await createContentGenerator(
|
||||
{ apiKey: 'test-api-key', authType: AuthType.USE_GEMINI },
|
||||
mockConfig,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(GoogleGenAI).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
httpOptions: expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
'User-Agent': expect.stringMatching(
|
||||
/GeminiCLI-a2a-server\/.*\/gemini-pro \(.*; .*; .*\)/,
|
||||
),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include custom headers from GEMINI_CLI_CUSTOM_HEADERS for Code Assist requests', async () => {
|
||||
const mockGenerator = {} as unknown as ContentGenerator;
|
||||
vi.mocked(createCodeAssistContentGenerator).mockResolvedValue(
|
||||
@@ -189,6 +229,7 @@ describe('createContentGenerator', () => {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
const mockGenerator = {
|
||||
@@ -235,6 +276,7 @@ describe('createContentGenerator', () => {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
const mockGenerator = {
|
||||
@@ -268,6 +310,7 @@ describe('createContentGenerator', () => {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
const mockGenerator = {
|
||||
@@ -309,6 +352,7 @@ describe('createContentGenerator', () => {
|
||||
const mockConfig = {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
const mockGenerator = {
|
||||
models: {},
|
||||
@@ -340,6 +384,7 @@ describe('createContentGenerator', () => {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
const mockGenerator = {
|
||||
@@ -373,6 +418,7 @@ describe('createContentGenerator', () => {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
const mockGenerator = {
|
||||
@@ -410,6 +456,7 @@ describe('createContentGenerator', () => {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
const mockGenerator = {
|
||||
@@ -448,6 +495,7 @@ describe('createContentGenerator', () => {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
const mockGenerator = {
|
||||
@@ -478,6 +526,7 @@ describe('createContentGenerator', () => {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
const mockGenerator = {
|
||||
@@ -511,6 +560,7 @@ describe('createContentGenerator', () => {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
const mockGenerator = {
|
||||
@@ -540,6 +590,7 @@ describe('createContentGenerator', () => {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
vi.stubEnv('GOOGLE_GEMINI_BASE_URL', 'http://evil-proxy.example.com');
|
||||
@@ -560,6 +611,7 @@ describe('createContentGenerator', () => {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
const mockGenerator = {
|
||||
@@ -596,6 +648,7 @@ describe('createContentGeneratorConfig', () => {
|
||||
setModel: vi.fn(),
|
||||
flashFallbackHandler: vi.fn(),
|
||||
getProxy: vi.fn(),
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { LoggingContentGenerator } from './loggingContentGenerator.js';
|
||||
import { InstallationManager } from '../utils/installationManager.js';
|
||||
import { FakeContentGenerator } from './fakeContentGenerator.js';
|
||||
import { parseCustomHeaders } from '../utils/customHeaderUtils.js';
|
||||
import { determineSurface } from '../utils/surface.js';
|
||||
import { RecordingContentGenerator } from './recordingContentGenerator.js';
|
||||
import { getVersion, resolveModel } from '../../index.js';
|
||||
import type { LlmRole } from '../telemetry/llmRole.js';
|
||||
@@ -173,7 +174,12 @@ export async function createContentGenerator(
|
||||
);
|
||||
const customHeadersEnv =
|
||||
process.env['GEMINI_CLI_CUSTOM_HEADERS'] || undefined;
|
||||
const userAgent = `GeminiCLI/${version}/${model} (${process.platform}; ${process.arch})`;
|
||||
const clientName = gcConfig.getClientName();
|
||||
const userAgentPrefix = clientName
|
||||
? `GeminiCLI-${clientName}`
|
||||
: 'GeminiCLI';
|
||||
const surface = determineSurface();
|
||||
const userAgent = `${userAgentPrefix}/${version}/${model} (${process.platform}; ${process.arch}; ${surface})`;
|
||||
const customHeadersMap = parseCustomHeaders(customHeadersEnv);
|
||||
const apiKeyAuthMechanism =
|
||||
process.env['GEMINI_API_KEY_AUTH_MECHANISM'] || 'x-goog-api-key';
|
||||
|
||||
@@ -140,6 +140,21 @@ describe('detectIde', () => {
|
||||
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.antigravity);
|
||||
});
|
||||
|
||||
it('should detect Zed via ZED_SESSION_ID', () => {
|
||||
vi.stubEnv('ZED_SESSION_ID', 'test-session-id');
|
||||
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.zed);
|
||||
});
|
||||
|
||||
it('should detect Zed via TERM_PROGRAM', () => {
|
||||
vi.stubEnv('TERM_PROGRAM', 'Zed');
|
||||
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.zed);
|
||||
});
|
||||
|
||||
it('should detect XCode via XCODE_VERSION_ACTUAL', () => {
|
||||
vi.stubEnv('XCODE_VERSION_ACTUAL', '1500');
|
||||
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.xcode);
|
||||
});
|
||||
|
||||
it('should detect JetBrains IDE via TERMINAL_EMULATOR', () => {
|
||||
vi.stubEnv('TERMINAL_EMULATOR', 'JetBrains-JediTerm');
|
||||
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.jetbrains);
|
||||
|
||||
@@ -27,6 +27,8 @@ export const IDE_DEFINITIONS = {
|
||||
rustrover: { name: 'rustrover', displayName: 'RustRover' },
|
||||
datagrip: { name: 'datagrip', displayName: 'DataGrip' },
|
||||
phpstorm: { name: 'phpstorm', displayName: 'PhpStorm' },
|
||||
zed: { name: 'zed', displayName: 'Zed' },
|
||||
xcode: { name: 'xcode', displayName: 'XCode' },
|
||||
} as const;
|
||||
|
||||
export interface IdeInfo {
|
||||
@@ -75,6 +77,12 @@ export function detectIdeFromEnv(): IdeInfo {
|
||||
if (process.env['TERM_PROGRAM'] === 'sublime') {
|
||||
return IDE_DEFINITIONS.sublimetext;
|
||||
}
|
||||
if (process.env['ZED_SESSION_ID'] || process.env['TERM_PROGRAM'] === 'Zed') {
|
||||
return IDE_DEFINITIONS.zed;
|
||||
}
|
||||
if (process.env['XCODE_VERSION_ACTUAL']) {
|
||||
return IDE_DEFINITIONS.xcode;
|
||||
}
|
||||
if (isJetBrains()) {
|
||||
return IDE_DEFINITIONS.jetbrains;
|
||||
}
|
||||
@@ -147,10 +155,13 @@ export function detectIde(
|
||||
};
|
||||
}
|
||||
|
||||
// Only VS Code, Sublime Text and JetBrains integrations are currently supported.
|
||||
// Only VS Code, Sublime Text, JetBrains, Zed, and XCode integrations are currently supported.
|
||||
if (
|
||||
process.env['TERM_PROGRAM'] !== 'vscode' &&
|
||||
process.env['TERM_PROGRAM'] !== 'sublime' &&
|
||||
process.env['TERM_PROGRAM'] !== 'Zed' &&
|
||||
!process.env['ZED_SESSION_ID'] &&
|
||||
!process.env['XCODE_VERSION_ACTUAL'] &&
|
||||
!isJetBrains()
|
||||
) {
|
||||
return undefined;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { detectIdeFromEnv } from '../ide/detect-ide.js';
|
||||
|
||||
/** Default surface value when no IDE/environment is detected. */
|
||||
export const SURFACE_NOT_SET = 'terminal';
|
||||
|
||||
/**
|
||||
* Determines the surface/distribution channel the CLI is running in.
|
||||
*
|
||||
* Priority:
|
||||
* 1. `GEMINI_CLI_SURFACE` env var (first-class override for enterprise customers)
|
||||
* 2. `SURFACE` env var (legacy override, kept for backward compatibility)
|
||||
* 3. Auto-detection via environment variables (Cloud Shell, GitHub Actions, IDE, etc.)
|
||||
*
|
||||
* @returns A human-readable surface identifier (e.g., "vscode", "cursor", "terminal").
|
||||
*/
|
||||
export function determineSurface(): string {
|
||||
// Priority 1 & 2: Explicit overrides from environment variables.
|
||||
const customSurface =
|
||||
process.env['GEMINI_CLI_SURFACE'] || process.env['SURFACE'];
|
||||
if (customSurface) {
|
||||
return customSurface;
|
||||
}
|
||||
|
||||
// Priority 3: Auto-detect IDE/environment.
|
||||
const ide = detectIdeFromEnv();
|
||||
|
||||
// `detectIdeFromEnv` falls back to 'vscode' for generic terminals.
|
||||
// If a specific IDE (e.g., Cloud Shell, Cursor, JetBrains) was detected,
|
||||
// its name will be something other than 'vscode', and we can use it directly.
|
||||
if (ide.name !== 'vscode') {
|
||||
return ide.name;
|
||||
}
|
||||
|
||||
// If the detected IDE is 'vscode', we only accept it if TERM_PROGRAM confirms it.
|
||||
// This prevents generic terminals from being misidentified as VSCode.
|
||||
if (process.env['TERM_PROGRAM'] === 'vscode') {
|
||||
return ide.name;
|
||||
}
|
||||
|
||||
// Priority 4: GitHub Actions (checked after IDE detection so that
|
||||
// specific environments like Cloud Shell take precedence).
|
||||
if (process.env['GITHUB_SHA']) {
|
||||
return 'GitHub';
|
||||
}
|
||||
|
||||
// Priority 5: Fallback for all other cases (e.g., a generic terminal).
|
||||
return SURFACE_NOT_SET;
|
||||
}
|
||||
Reference in New Issue
Block a user