Auth First Run (#1207)

Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
This commit is contained in:
matt korwel 2025-06-19 16:52:22 -07:00 committed by GitHub
parent c48fcaa8c3
commit 04518b52c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 636 additions and 349 deletions

5
.vscode/launch.json vendored
View File

@ -11,7 +11,10 @@
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start"],
"skipFiles": ["<node_internals>/**"],
"cwd": "${workspaceFolder}"
"cwd": "${workspaceFolder}",
"env": {
"GEMINI_SANDBOX": "false"
}
},
{
"type": "node",

View File

@ -25,7 +25,7 @@
"lint": "eslint . --ext .ts,.tsx && eslint integration-tests",
"typecheck": "npm run typecheck --workspaces --if-present",
"format": "prettier --write .",
"preflight": "npm ci && npm run format && npm run lint:fix && npm run build && npm run typecheck && npm run test:ci",
"preflight": "npm run clean && npm ci && npm run format && npm run lint:fix && npm run build && npm run typecheck && npm run test:ci",
"auth:npm": "npx google-artifactregistry-auth",
"auth:docker": "gcloud auth configure-docker us-west1-docker.pkg.dev",
"auth": "npm run auth:npm && npm run auth:docker",

View File

@ -13,7 +13,7 @@ import { main } from './src/gemini.js';
main().catch((error) => {
console.error('An unexpected critical error occurred:');
if (error instanceof Error) {
console.error(error.message);
console.error(error.stack);
} else {
console.error(String(error));
}

View File

@ -0,0 +1,44 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { AuthType } from '@gemini-cli/core';
import { loadEnvironment } from './config.js';
export const validateAuthMethod = (authMethod: string): string | null => {
loadEnvironment();
if (authMethod === AuthType.LOGIN_WITH_GOOGLE_PERSONAL) {
return null;
}
if (authMethod === AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE) {
if (!process.env.GOOGLE_CLOUD_PROJECT) {
return 'GOOGLE_CLOUD_PROJECT environment variable not found. Add that to your .env and try again, no reload needed!';
}
return null;
}
if (authMethod === AuthType.USE_GEMINI) {
if (!process.env.GEMINI_API_KEY) {
return 'GEMINI_API_KEY environment variable not found. Add that to your .env and try again, no reload needed!';
}
return null;
}
if (authMethod === AuthType.USE_VERTEX_AI) {
if (!process.env.GOOGLE_API_KEY) {
return 'GOOGLE_API_KEY environment variable not found. Add that to your .env and try again, no reload needed!';
}
if (!process.env.GOOGLE_CLOUD_PROJECT) {
return 'GOOGLE_CLOUD_PROJECT environment variable not found. Add that to your .env and try again, no reload needed!';
}
if (!process.env.GOOGLE_CLOUD_LOCATION) {
return 'GOOGLE_CLOUD_LOCATION environment variable not found. Add that to your .env and try again, no reload needed!';
}
return null;
}
return 'Invalid auth method selected.';
};

View File

@ -247,48 +247,6 @@ describe('loadCliConfig telemetry', () => {
});
});
describe('API Key Handling', () => {
const originalEnv = { ...process.env };
const originalArgv = process.argv;
beforeEach(() => {
vi.resetAllMocks();
process.argv = ['node', 'script.js'];
});
afterEach(() => {
process.env = originalEnv;
process.argv = originalArgv;
});
it('should use GEMINI_API_KEY from env', async () => {
process.env.GEMINI_API_KEY = 'gemini-key';
delete process.env.GOOGLE_API_KEY;
const settings: Settings = {};
const result = await loadCliConfig(settings, [], 'test-session');
expect(result.getContentGeneratorConfig().apiKey).toBe('gemini-key');
});
it('should use GOOGLE_API_KEY and warn when both GOOGLE_API_KEY and GEMINI_API_KEY are set', async () => {
const consoleWarnSpy = vi
.spyOn(console, 'warn')
.mockImplementation(() => {});
process.env.GEMINI_API_KEY = 'gemini-key';
process.env.GOOGLE_API_KEY = 'google-key';
const settings: Settings = {};
const result = await loadCliConfig(settings, [], 'test-session');
expect(consoleWarnSpy).toHaveBeenCalledWith(
'[WARN]',
'Both GEMINI_API_KEY and GOOGLE_API_KEY are set. Using GOOGLE_API_KEY.',
);
expect(result.getContentGeneratorConfig().apiKey).toBe('google-key');
});
});
describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
beforeEach(() => {
vi.resetAllMocks();

View File

@ -13,7 +13,6 @@ import {
setGeminiMdFilename as setServerGeminiMdFilename,
getCurrentGeminiMdFilename,
ApprovalMode,
ContentGeneratorConfig,
GEMINI_CONFIG_DIR as GEMINI_DIR,
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_EMBEDDING_MODEL,
@ -21,7 +20,7 @@ import {
TelemetryTarget,
} from '@gemini-cli/core';
import { Settings } from './settings.js';
import { getEffectiveModel } from '../utils/modelCheck.js';
import { Extension } from './extension.js';
import { getCliVersion } from '../utils/version.js';
import * as dotenv from 'dotenv';
@ -194,15 +193,12 @@ export async function loadCliConfig(
extensionContextFilePaths,
);
const contentGeneratorConfig = await createContentGeneratorConfig(argv);
const mcpServers = mergeMcpServers(settings, extensions);
const sandboxConfig = await loadSandboxConfig(settings, argv);
return new Config({
sessionId,
contentGeneratorConfig,
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
sandbox: sandboxConfig,
targetDir: process.cwd(),
@ -242,6 +238,7 @@ export async function loadCliConfig(
cwd: process.cwd(),
fileDiscoveryService: fileService,
bugCommand: settings.bugCommand,
model: argv.model!,
});
}
@ -262,59 +259,6 @@ function mergeMcpServers(settings: Settings, extensions: Extension[]) {
}
return mcpServers;
}
async function createContentGeneratorConfig(
argv: CliArgs,
): Promise<ContentGeneratorConfig> {
const geminiApiKey = process.env.GEMINI_API_KEY;
const googleApiKey = process.env.GOOGLE_API_KEY;
const googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT;
const googleCloudLocation = process.env.GOOGLE_CLOUD_LOCATION;
const hasCodeAssist = process.env.GEMINI_CODE_ASSIST === 'true';
const hasGeminiApiKey = !!geminiApiKey;
const hasGoogleApiKey = !!googleApiKey;
const hasVertexProjectLocationConfig =
!!googleCloudProject && !!googleCloudLocation;
if (hasGeminiApiKey && hasGoogleApiKey) {
logger.warn(
'Both GEMINI_API_KEY and GOOGLE_API_KEY are set. Using GOOGLE_API_KEY.',
);
}
if (
!hasCodeAssist &&
!hasGeminiApiKey &&
!hasGoogleApiKey &&
!hasVertexProjectLocationConfig
) {
logger.error(
'No valid API authentication configuration found. Please set ONE of the following combinations in your environment variables or .env file:\n' +
'1. GEMINI_CODE_ASSIST=true (for Code Assist access).\n' +
'2. GEMINI_API_KEY (for Gemini API access).\n' +
'3. GOOGLE_API_KEY (for Gemini API or Vertex AI Express Mode access).\n' +
'4. GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION (for Vertex AI access).\n\n' +
'For Gemini API keys, visit: https://ai.google.dev/gemini-api/docs/api-key\n' +
'For Vertex AI authentication, visit: https://cloud.google.com/vertex-ai/docs/authentication\n' +
'The GOOGLE_GENAI_USE_VERTEXAI environment variable can also be set to true/false to influence service selection when ambiguity exists.',
);
process.exit(1);
}
const config: ContentGeneratorConfig = {
model: argv.model || DEFAULT_GEMINI_MODEL,
apiKey: googleApiKey || geminiApiKey || '',
vertexai: hasGeminiApiKey ? false : undefined,
codeAssist: hasCodeAssist,
};
if (config.apiKey) {
config.model = await getEffectiveModel(config.apiKey, config.model);
}
return config;
}
function findEnvFile(startDir: string): string | null {
let currentDir = path.resolve(startDir);
while (true) {

View File

@ -12,6 +12,7 @@ import {
getErrorMessage,
BugCommandSettings,
TelemetrySettings,
AuthType,
} from '@gemini-cli/core';
import stripJsonComments from 'strip-json-comments';
import { DefaultLight } from '../ui/themes/default-light.js';
@ -32,6 +33,7 @@ export interface AccessibilitySettings {
export interface Settings {
theme?: string;
selectedAuthType?: AuthType;
sandbox?: boolean | string;
coreTools?: string[];
excludeTools?: string[];

View File

@ -25,7 +25,9 @@ import {
WriteFileTool,
sessionId,
logUserPrompt,
AuthType,
} from '@gemini-cli/core';
import { validateAuthMethod } from './config/auth.js';
export async function main() {
const workspaceRoot = process.cwd();
@ -47,10 +49,6 @@ export async function main() {
const extensions = loadExtensions(workspaceRoot);
const config = await loadCliConfig(settings.merged, extensions, sessionId);
// When using Code Assist this triggers the Oauth login.
// Do this now, before sandboxing, so web redirect works.
await config.getGeminiClient().initialize();
// Initialize centralized FileDiscoveryService
config.getFileService();
if (config.getCheckpointEnabled()) {
@ -73,6 +71,15 @@ export async function main() {
if (!process.env.SANDBOX) {
const sandboxConfig = config.getSandbox();
if (sandboxConfig) {
if (settings.merged.selectedAuthType) {
// Validate authentication here because the sandbox will interfere with the Oauth2 web redirect.
const err = validateAuthMethod(settings.merged.selectedAuthType);
if (err) {
console.error(err);
process.exit(1);
}
await config.refreshAuth(settings.merged.selectedAuthType);
}
await start_sandbox(sandboxConfig);
process.exit(0);
}
@ -152,28 +159,58 @@ async function loadNonInteractiveConfig(
extensions: Extension[],
settings: LoadedSettings,
) {
if (config.getApprovalMode() === ApprovalMode.YOLO) {
// Since everything is being allowed we can use normal yolo behavior.
return config;
let finalConfig = config;
if (config.getApprovalMode() !== ApprovalMode.YOLO) {
// Everything is not allowed, ensure that only read-only tools are configured.
const existingExcludeTools = settings.merged.excludeTools || [];
const interactiveTools = [
ShellTool.Name,
EditTool.Name,
WriteFileTool.Name,
];
const newExcludeTools = [
...new Set([...existingExcludeTools, ...interactiveTools]),
];
const nonInteractiveSettings = {
...settings.merged,
excludeTools: newExcludeTools,
};
finalConfig = await loadCliConfig(
nonInteractiveSettings,
extensions,
config.getSessionId(),
);
}
// Everything is not allowed, ensure that only read-only tools are configured.
const existingExcludeTools = settings.merged.excludeTools || [];
const interactiveTools = [ShellTool.Name, EditTool.Name, WriteFileTool.Name];
const newExcludeTools = [
...new Set([...existingExcludeTools, ...interactiveTools]),
];
const nonInteractiveSettings = {
...settings.merged,
excludeTools: newExcludeTools,
};
const newConfig = await loadCliConfig(
nonInteractiveSettings,
extensions,
config.getSessionId(),
return await validateNonInterActiveAuth(
settings.merged.selectedAuthType,
finalConfig,
);
await newConfig.getGeminiClient().initialize();
return newConfig;
}
async function validateNonInterActiveAuth(
selectedAuthType: AuthType | undefined,
nonInteractiveConfig: Config,
) {
// making a special case for the cli. many headless environments might not have a settings.json set
// so if GEMINI_API_KEY is set, we'll use that. However since the oauth things are interactive anyway, we'll
// still expect that exists
if (!selectedAuthType && !process.env.GEMINI_API_KEY) {
console.error(
'Please set an Auth method in your .gemini/settings.json OR specify GEMINI_API_KEY env variable file before running',
);
process.exit(1);
}
selectedAuthType = selectedAuthType || AuthType.USE_GEMINI;
const err = validateAuthMethod(selectedAuthType);
if (err != null) {
console.error(err);
process.exit(1);
}
await nonInteractiveConfig.refreshAuth(selectedAuthType);
return nonInteractiveConfig;
}

View File

@ -145,6 +145,15 @@ vi.mock('./hooks/useGeminiStream', () => ({
})),
}));
vi.mock('./hooks/useAuthCommand', () => ({
useAuthCommand: vi.fn(() => ({
isAuthDialogOpen: false,
openAuthDialog: vi.fn(),
handleAuthSelect: vi.fn(),
handleAuthHighlight: vi.fn(),
})),
}));
vi.mock('./hooks/useLogger', () => ({
useLogger: vi.fn(() => ({
getPreviousUserMessages: vi.fn().mockResolvedValue([]),
@ -176,7 +185,9 @@ describe('App UI', () => {
};
const workspaceSettingsFile: SettingsFile = {
path: '/workspace/.gemini/settings.json',
settings,
settings: {
...settings,
},
};
return new LoadedSettings(userSettingsFile, workspaceSettingsFile, []);
};
@ -184,10 +195,6 @@ describe('App UI', () => {
beforeEach(() => {
const ServerConfigMocked = vi.mocked(ServerConfig, true);
mockConfig = new ServerConfigMocked({
contentGeneratorConfig: {
apiKey: 'test-key',
model: 'test-model',
},
embeddingModel: 'test-embedding-model',
sandbox: undefined,
targetDir: '/test/dir',
@ -197,7 +204,7 @@ describe('App UI', () => {
showMemoryUsage: false,
sessionId: 'test-session-id',
cwd: '/tmp',
// Provide other required fields for ConfigParameters if necessary
model: 'model',
}) as unknown as MockServerConfig;
// Ensure the getShowMemoryUsage mock function is specifically set up if not covered by constructor mock

View File

@ -20,6 +20,7 @@ import { useTerminalSize } from './hooks/useTerminalSize.js';
import { useGeminiStream } from './hooks/useGeminiStream.js';
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
import { useThemeCommand } from './hooks/useThemeCommand.js';
import { useAuthCommand } from './hooks/useAuthCommand.js';
import { useEditorSettings } from './hooks/useEditorSettings.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
@ -31,6 +32,7 @@ import { ShellModeIndicator } from './components/ShellModeIndicator.js';
import { InputPrompt } from './components/InputPrompt.js';
import { Footer } from './components/Footer.js';
import { ThemeDialog } from './components/ThemeDialog.js';
import { AuthDialog } from './components/AuthDialog.js';
import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
import { Colors } from './colors.js';
import { Help } from './components/Help.js';
@ -51,6 +53,7 @@ import {
isEditorAvailable,
EditorType,
} from '@gemini-cli/core';
import { validateAuthMethod } from '../config/auth.js';
import { useLogger } from './hooks/useLogger.js';
import { StreamingContext } from './contexts/StreamingContext.js';
import {
@ -101,6 +104,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
const [debugMessage, setDebugMessage] = useState<string>('');
const [showHelp, setShowHelp] = useState<boolean>(false);
const [themeError, setThemeError] = useState<string | null>(null);
const [authError, setAuthError] = useState<string | null>(null);
const [editorError, setEditorError] = useState<string | null>(null);
const [footerHeight, setFooterHeight] = useState<number>(0);
const [corgiMode, setCorgiMode] = useState(false);
@ -129,6 +133,23 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
handleThemeHighlight,
} = useThemeCommand(settings, setThemeError, addItem);
const {
isAuthDialogOpen,
openAuthDialog,
handleAuthSelect,
handleAuthHighlight,
} = useAuthCommand(settings, setAuthError, config);
useEffect(() => {
if (settings.merged.selectedAuthType) {
const error = validateAuthMethod(settings.merged.selectedAuthType);
if (error) {
setAuthError(error);
openAuthDialog();
}
}
}, [settings.merged.selectedAuthType, openAuthDialog, setAuthError]);
const {
isEditorDialogOpen,
openEditorDialog,
@ -197,6 +218,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
setShowHelp,
setDebugMessage,
openThemeDialog,
openAuthDialog,
openEditorDialog,
performMemoryRefresh,
toggleCorgiMode,
@ -306,6 +328,11 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
return editorType as EditorType;
}, [settings, openEditorDialog]);
const onAuthError = useCallback(() => {
setAuthError('reauth required');
openAuthDialog();
}, [openAuthDialog, setAuthError]);
const {
streamingState,
submitQuery,
@ -322,6 +349,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
handleSlashCommand,
shellModeActive,
getPreferredEditor,
onAuthError,
);
pendingHistoryItems.push(...pendingGeminiHistoryItems);
const { elapsedTime, currentLoadingPhrase } =
@ -557,6 +585,20 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
terminalWidth={mainAreaWidth}
/>
</Box>
) : isAuthDialogOpen ? (
<Box flexDirection="column">
{authError && (
<Box marginBottom={1}>
<Text color={Colors.AccentRed}>{authError}</Text>
</Box>
)}
<AuthDialog
onSelect={handleAuthSelect}
onHighlight={handleAuthHighlight}
settings={settings}
initialErrorMessage={authError}
/>
</Box>
) : isEditorDialogOpen ? (
<Box flexDirection="column">
{editorError && (

View File

@ -0,0 +1,41 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { AuthDialog } from './AuthDialog.js';
import { LoadedSettings } from '../../config/settings.js';
import { AuthType } from '@gemini-cli/core';
describe('AuthDialog', () => {
it('should show an error if the initial auth type is invalid', () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
selectedAuthType: AuthType.USE_GEMINI,
},
path: '',
},
{
settings: {},
path: '',
},
[],
);
const { lastFrame } = render(
<AuthDialog
onSelect={() => {}}
onHighlight={() => {}}
settings={settings}
initialErrorMessage="GEMINI_API_KEY environment variable not found"
/>,
);
expect(lastFrame()).toContain(
'GEMINI_API_KEY environment variable not found',
);
});
});

View File

@ -0,0 +1,94 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState } from 'react';
import { Box, Text, useInput } from 'ink';
import { Colors } from '../colors.js';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
import { LoadedSettings, SettingScope } from '../../config/settings.js';
import { AuthType } from '@gemini-cli/core';
import { validateAuthMethod } from '../../config/auth.js';
interface AuthDialogProps {
onSelect: (authMethod: string | undefined, scope: SettingScope) => void;
onHighlight: (authMethod: string | undefined) => void;
settings: LoadedSettings;
initialErrorMessage?: string | null;
}
export function AuthDialog({
onSelect,
onHighlight,
settings,
initialErrorMessage,
}: AuthDialogProps): React.JSX.Element {
const [errorMessage, setErrorMessage] = useState<string | null>(
initialErrorMessage || null,
);
const authItems = [
{
label: 'Login with Google Personal Account',
value: AuthType.LOGIN_WITH_GOOGLE_PERSONAL,
},
{ label: 'Gemini API Key', value: AuthType.USE_GEMINI },
{
label: 'Login with GCP Project and Google Work Account',
value: AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE,
},
{ label: 'Vertex AI', value: AuthType.USE_VERTEX_AI },
];
let initialAuthIndex = authItems.findIndex(
(item) => item.value === settings.merged.selectedAuthType,
);
if (initialAuthIndex === -1) {
initialAuthIndex = 0;
}
const handleAuthSelect = (authMethod: string) => {
const error = validateAuthMethod(authMethod);
if (error) {
setErrorMessage(error);
} else {
setErrorMessage(null);
onSelect(authMethod, SettingScope.User);
}
};
useInput((_input, key) => {
if (key.escape) {
onSelect(undefined, SettingScope.User);
}
});
return (
<Box
borderStyle="round"
borderColor={Colors.Gray}
flexDirection="column"
padding={1}
width="100%"
>
<Text bold>Select Auth Method</Text>
<RadioButtonSelect
items={authItems}
initialIndex={initialAuthIndex}
onSelect={handleAuthSelect}
onHighlight={onHighlight}
isFocused={true}
/>
{errorMessage && (
<Box marginTop={1}>
<Text color={Colors.AccentRed}>{errorMessage}</Text>
</Box>
)}
<Box marginTop={1}>
<Text color={Colors.Gray}>(Use Enter to select)</Text>
</Box>
</Box>
);
}

View File

@ -103,6 +103,7 @@ describe('useSlashCommandProcessor', () => {
let mockSetShowHelp: ReturnType<typeof vi.fn>;
let mockOnDebugMessage: ReturnType<typeof vi.fn>;
let mockOpenThemeDialog: ReturnType<typeof vi.fn>;
let mockOpenAuthDialog: ReturnType<typeof vi.fn>;
let mockOpenEditorDialog: ReturnType<typeof vi.fn>;
let mockPerformMemoryRefresh: ReturnType<typeof vi.fn>;
let mockSetQuittingMessages: ReturnType<typeof vi.fn>;
@ -120,6 +121,7 @@ describe('useSlashCommandProcessor', () => {
mockSetShowHelp = vi.fn();
mockOnDebugMessage = vi.fn();
mockOpenThemeDialog = vi.fn();
mockOpenAuthDialog = vi.fn();
mockOpenEditorDialog = vi.fn();
mockPerformMemoryRefresh = vi.fn().mockResolvedValue(undefined);
mockSetQuittingMessages = vi.fn();
@ -171,6 +173,7 @@ describe('useSlashCommandProcessor', () => {
mockSetShowHelp,
mockOnDebugMessage,
mockOpenThemeDialog,
mockOpenAuthDialog,
mockOpenEditorDialog,
mockPerformMemoryRefresh,
mockCorgiMode,

View File

@ -68,6 +68,7 @@ export const useSlashCommandProcessor = (
setShowHelp: React.Dispatch<React.SetStateAction<boolean>>,
onDebugMessage: (message: string) => void,
openThemeDialog: () => void,
openAuthDialog: () => void,
openEditorDialog: () => void,
performMemoryRefresh: () => Promise<void>,
toggleCorgiMode: () => void,
@ -197,6 +198,13 @@ export const useSlashCommandProcessor = (
openThemeDialog();
},
},
{
name: 'auth',
description: 'change the auth method',
action: (_mainCommand, _subCommand, _args) => {
openAuthDialog();
},
},
{
name: 'editor',
description: 'set external editor preference',
@ -907,6 +915,7 @@ Add any other context about the problem here.
setShowHelp,
refreshStatic,
openThemeDialog,
openAuthDialog,
openEditorDialog,
clearItems,
performMemoryRefresh,

View File

@ -0,0 +1,57 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useCallback, useEffect } from 'react';
import { LoadedSettings, SettingScope } from '../../config/settings.js';
import { AuthType, Config, clearCachedCredentialFile } from '@gemini-cli/core';
async function performAuthFlow(authMethod: AuthType, config: Config) {
await config.refreshAuth(authMethod);
console.log(`Authenticated via "${authMethod}".`);
}
export const useAuthCommand = (
settings: LoadedSettings,
setAuthError: (error: string | null) => void,
config: Config,
) => {
const [isAuthDialogOpen, setIsAuthDialogOpen] = useState(
settings.merged.selectedAuthType === undefined,
);
useEffect(() => {
if (!isAuthDialogOpen) {
performAuthFlow(settings.merged.selectedAuthType as AuthType, config);
}
}, [isAuthDialogOpen, settings, config]);
const openAuthDialog = useCallback(() => {
setIsAuthDialogOpen(true);
}, []);
const handleAuthSelect = useCallback(
async (authMethod: string | undefined, scope: SettingScope) => {
if (authMethod) {
await clearCachedCredentialFile();
settings.setValue(scope, 'selectedAuthType', authMethod);
}
setIsAuthDialogOpen(false);
setAuthError(null);
},
[settings, setAuthError],
);
const handleAuthHighlight = useCallback((_authMethod: string | undefined) => {
// For now, we don't do anything on highlight.
}, []);
return {
isAuthDialogOpen,
openAuthDialog,
handleAuthSelect,
handleAuthHighlight,
};
};

View File

@ -359,6 +359,7 @@ describe('useGeminiStream', () => {
props.handleSlashCommand,
props.shellModeActive,
() => 'vscode' as EditorType,
() => {},
),
{
initialProps: {

View File

@ -22,6 +22,7 @@ import {
GitService,
EditorType,
ThoughtSummary,
isAuthError,
} from '@gemini-cli/core';
import { type Part, type PartListUnion } from '@google/genai';
import {
@ -87,6 +88,7 @@ export const useGeminiStream = (
>,
shellModeActive: boolean,
getPreferredEditor: () => EditorType | undefined,
onAuthError: () => void,
) => {
const [initError, setInitError] = useState<string | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
@ -496,7 +498,9 @@ export const useGeminiStream = (
setPendingHistoryItem(null);
}
} catch (error: unknown) {
if (!isNodeError(error) || error.name !== 'AbortError') {
if (isAuthError(error)) {
onAuthError();
} else if (!isNodeError(error) || error.name !== 'AbortError') {
addItem(
{
type: MessageType.ERROR,
@ -522,6 +526,7 @@ export const useGeminiStream = (
setInitError,
geminiClient,
startNewTurn,
onAuthError,
],
);

View File

@ -1,154 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { getEffectiveModel } from './modelCheck.js';
import {
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
} from '@gemini-cli/core';
// Mock global fetch
global.fetch = vi.fn();
// Mock AbortController
const mockAbort = vi.fn();
global.AbortController = vi.fn(() => ({
signal: { aborted: false }, // Start with not aborted
abort: mockAbort,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
})) as any;
describe('getEffectiveModel', () => {
const apiKey = 'test-api-key';
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
// Reset signal for each test if AbortController mock is more complex
global.AbortController = vi.fn(() => ({
signal: { aborted: false },
abort: mockAbort,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
})) as any;
});
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});
describe('when currentConfiguredModel is not DEFAULT_GEMINI_MODEL', () => {
it('should return the currentConfiguredModel without fetching', async () => {
const customModel = 'custom-model-name';
const result = await getEffectiveModel(apiKey, customModel);
expect(result).toEqual(customModel);
expect(fetch).not.toHaveBeenCalled();
});
});
describe('when currentConfiguredModel is DEFAULT_GEMINI_MODEL', () => {
it('should switch to DEFAULT_GEMINI_FLASH_MODEL if fetch returns 429', async () => {
(fetch as vi.Mock).mockResolvedValueOnce({
ok: false,
status: 429,
});
const result = await getEffectiveModel(apiKey, DEFAULT_GEMINI_MODEL);
expect(result).toEqual(DEFAULT_GEMINI_FLASH_MODEL);
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith(
`https://generativelanguage.googleapis.com/v1beta/models/${DEFAULT_GEMINI_MODEL}:generateContent?key=${apiKey}`,
expect.any(Object),
);
});
it('should return DEFAULT_GEMINI_MODEL if fetch returns 200', async () => {
(fetch as vi.Mock).mockResolvedValueOnce({
ok: true,
status: 200,
});
const result = await getEffectiveModel(apiKey, DEFAULT_GEMINI_MODEL);
expect(result).toEqual(DEFAULT_GEMINI_MODEL);
expect(fetch).toHaveBeenCalledTimes(1);
});
it('should return DEFAULT_GEMINI_MODEL if fetch returns a non-429 error status (e.g., 500)', async () => {
(fetch as vi.Mock).mockResolvedValueOnce({
ok: false,
status: 500,
});
const result = await getEffectiveModel(apiKey, DEFAULT_GEMINI_MODEL);
expect(result).toEqual(DEFAULT_GEMINI_MODEL);
expect(fetch).toHaveBeenCalledTimes(1);
});
it('should return DEFAULT_GEMINI_MODEL if fetch throws a network error', async () => {
(fetch as vi.Mock).mockRejectedValueOnce(new Error('Network error'));
const result = await getEffectiveModel(apiKey, DEFAULT_GEMINI_MODEL);
expect(result).toEqual(DEFAULT_GEMINI_MODEL);
expect(fetch).toHaveBeenCalledTimes(1);
});
it('should return DEFAULT_GEMINI_MODEL if fetch times out', async () => {
// Simulate AbortController's signal changing and fetch throwing AbortError
const abortControllerInstance = {
signal: { aborted: false }, // mutable signal
abort: vi.fn(() => {
abortControllerInstance.signal.aborted = true; // Use abortControllerInstance
}),
};
(global.AbortController as vi.Mock).mockImplementationOnce(
() => abortControllerInstance,
);
(fetch as vi.Mock).mockImplementationOnce(
async ({ signal }: { signal: AbortSignal }) => {
// Simulate the timeout advancing and abort being called
vi.advanceTimersByTime(2000);
if (signal.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
// Should not reach here in a timeout scenario
return { ok: true, status: 200 };
},
);
const resultPromise = getEffectiveModel(apiKey, DEFAULT_GEMINI_MODEL);
// Ensure timers are advanced to trigger the timeout within getEffectiveModel
await vi.advanceTimersToNextTimerAsync(); // Or advanceTimersByTime(2000) if more precise control is needed
const result = await resultPromise;
expect(mockAbort).toHaveBeenCalledTimes(0); // setTimeout calls controller.abort(), not our direct mockAbort
expect(abortControllerInstance.abort).toHaveBeenCalledTimes(1);
expect(result).toEqual(DEFAULT_GEMINI_MODEL);
expect(fetch).toHaveBeenCalledTimes(1);
});
it('should correctly pass API key and model in the fetch request', async () => {
(fetch as vi.Mock).mockResolvedValueOnce({ ok: true, status: 200 });
const specificApiKey = 'specific-key-for-this-test';
await getEffectiveModel(specificApiKey, DEFAULT_GEMINI_MODEL);
expect(fetch).toHaveBeenCalledWith(
`https://generativelanguage.googleapis.com/v1beta/models/${DEFAULT_GEMINI_MODEL}:generateContent?key=${specificApiKey}`,
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: 'test' }] }],
generationConfig: {
maxOutputTokens: 1,
temperature: 0,
topK: 1,
thinkingConfig: { thinkingBudget: 0, includeThoughts: false },
},
}),
}),
);
});
});
});

View File

@ -4,15 +4,23 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { ContentGenerator } from '../core/contentGenerator.js';
import { AuthType, ContentGenerator } from '../core/contentGenerator.js';
import { getOauthClient } from './oauth2.js';
import { setupUser } from './setup.js';
import { CodeAssistServer, HttpOptions } from './server.js';
export async function createCodeAssistContentGenerator(
httpOptions: HttpOptions,
authType: AuthType,
): Promise<ContentGenerator> {
const authClient = await getOauthClient();
const projectId = await setupUser(authClient);
return new CodeAssistServer(authClient, projectId, httpOptions);
if (
authType === AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE ||
authType === AuthType.LOGIN_WITH_GOOGLE_PERSONAL
) {
const authClient = await getOauthClient();
const projectId = await setupUser(authClient);
return new CodeAssistServer(authClient, projectId, httpOptions);
}
throw new Error(`Unsupported authType: ${authType}`);
}

View File

@ -0,0 +1,13 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { GaxiosError } from 'gaxios';
export function isAuthError(error: unknown): boolean {
return (
error instanceof GaxiosError && error.response?.data?.error?.code === 401
);
}

View File

@ -192,3 +192,11 @@ async function cacheCredentials(credentials: Credentials) {
function getCachedCredentialPath(): string {
return path.join(os.homedir(), GEMINI_DIR, CREDENTIAL_FILENAME);
}
export async function clearCachedCredentialFile() {
try {
await fs.rm(getCachedCredentialPath());
} catch (_) {
/* empty */
}
}

View File

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { OAuth2Client } from 'google-auth-library';
import { AuthClient } from 'google-auth-library';
import {
LoadCodeAssistResponse,
LoadCodeAssistRequest,
@ -45,7 +45,7 @@ export const CODE_ASSIST_API_VERSION = 'v1internal';
export class CodeAssistServer implements ContentGenerator {
constructor(
readonly auth: OAuth2Client,
readonly auth: AuthClient,
readonly projectId?: string,
readonly httpOptions: HttpOptions = {},
) {}

View File

@ -42,6 +42,21 @@ vi.mock('../tools/memoryTool', () => ({
GEMINI_CONFIG_DIR: '.gemini',
}));
vi.mock('../core/contentGenerator.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../core/contentGenerator.js')>();
return {
...actual,
createContentGeneratorConfig: vi.fn(),
};
});
vi.mock('../core/client.js', () => ({
GeminiClient: vi.fn().mockImplementation(() => ({
// Mock any methods on GeminiClient that might be used.
})),
}));
vi.mock('../telemetry/index.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../telemetry/index.js')>();
return {
@ -51,7 +66,6 @@ vi.mock('../telemetry/index.js', async (importOriginal) => {
});
describe('Server Config (config.ts)', () => {
const API_KEY = 'server-api-key';
const MODEL = 'gemini-pro';
const SANDBOX: SandboxConfig = {
command: 'docker',
@ -67,10 +81,6 @@ describe('Server Config (config.ts)', () => {
const SESSION_ID = 'test-session-id';
const baseParams: ConfigParameters = {
cwd: '/tmp',
contentGeneratorConfig: {
apiKey: API_KEY,
model: MODEL,
},
embeddingModel: EMBEDDING_MODEL,
sandbox: SANDBOX,
targetDir: TARGET_DIR,
@ -80,6 +90,7 @@ describe('Server Config (config.ts)', () => {
userMemory: USER_MEMORY,
telemetry: TELEMETRY_SETTINGS,
sessionId: SESSION_ID,
model: MODEL,
};
beforeEach(() => {
@ -87,6 +98,32 @@ describe('Server Config (config.ts)', () => {
vi.clearAllMocks();
});
// i can't get vi mocking to import in core. only in cli. can't fix it now.
// describe('refreshAuth', () => {
// it('should refresh auth and update config', async () => {
// const config = new Config(baseParams);
// const newModel = 'gemini-ultra';
// const authType = AuthType.USE_GEMINI;
// const mockContentConfig = {
// model: newModel,
// apiKey: 'test-key',
// };
// (createContentGeneratorConfig as vi.Mock).mockResolvedValue(
// mockContentConfig,
// );
// await config.refreshAuth(authType);
// expect(createContentGeneratorConfig).toHaveBeenCalledWith(
// newModel,
// authType,
// );
// expect(config.getContentGeneratorConfig()).toEqual(mockContentConfig);
// expect(GeminiClient).toHaveBeenCalledWith(config);
// });
// });
it('Config constructor should store userMemory correctly', () => {
const config = new Config(baseParams);

View File

@ -6,7 +6,11 @@
import * as path from 'node:path';
import process from 'node:process';
import { ContentGeneratorConfig } from '../core/contentGenerator.js';
import {
AuthType,
ContentGeneratorConfig,
createContentGeneratorConfig,
} from '../core/contentGenerator.js';
import { ToolRegistry } from '../tools/tool-registry.js';
import { LSTool } from '../tools/ls.js';
import { ReadFileTool } from '../tools/read-file.js';
@ -80,7 +84,6 @@ export interface SandboxConfig {
export interface ConfigParameters {
sessionId: string;
contentGeneratorConfig: ContentGeneratorConfig;
embeddingModel?: string;
sandbox?: SandboxConfig;
targetDir: string;
@ -106,12 +109,13 @@ export interface ConfigParameters {
cwd: string;
fileDiscoveryService?: FileDiscoveryService;
bugCommand?: BugCommandSettings;
model: string;
}
export class Config {
private toolRegistry: Promise<ToolRegistry>;
private readonly sessionId: string;
private readonly contentGeneratorConfig: ContentGeneratorConfig;
private contentGeneratorConfig!: ContentGeneratorConfig;
private readonly embeddingModel: string;
private readonly sandbox: SandboxConfig | undefined;
private readonly targetDir: string;
@ -130,7 +134,7 @@ export class Config {
private readonly showMemoryUsage: boolean;
private readonly accessibility: AccessibilitySettings;
private readonly telemetrySettings: TelemetrySettings;
private readonly geminiClient: GeminiClient;
private geminiClient!: GeminiClient;
private readonly fileFilteringRespectGitIgnore: boolean;
private fileDiscoveryService: FileDiscoveryService | null = null;
private gitService: GitService | undefined = undefined;
@ -138,10 +142,10 @@ export class Config {
private readonly proxy: string | undefined;
private readonly cwd: string;
private readonly bugCommand: BugCommandSettings | undefined;
private readonly model: string;
constructor(params: ConfigParameters) {
this.sessionId = params.sessionId;
this.contentGeneratorConfig = params.contentGeneratorConfig;
this.embeddingModel =
params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL;
this.sandbox = params.sandbox;
@ -174,12 +178,12 @@ export class Config {
this.cwd = params.cwd ?? process.cwd();
this.fileDiscoveryService = params.fileDiscoveryService ?? null;
this.bugCommand = params.bugCommand;
this.model = params.model;
if (params.contextFileName) {
setGeminiMdFilename(params.contextFileName);
}
this.geminiClient = new GeminiClient(this);
this.toolRegistry = createToolRegistry(this);
if (this.telemetrySettings.enabled) {
@ -187,6 +191,19 @@ export class Config {
}
}
async refreshAuth(authMethod: AuthType) {
const contentConfig = await createContentGeneratorConfig(
this.getModel(),
authMethod,
);
const gc = new GeminiClient(this);
await gc.initialize(contentConfig);
this.contentGeneratorConfig = contentConfig;
this.geminiClient = gc;
}
getSessionId(): string {
return this.sessionId;
}
@ -196,7 +213,7 @@ export class Config {
}
getModel(): string {
return this.contentGeneratorConfig.model;
return this.contentGeneratorConfig?.model || this.model;
}
getEmbeddingModel(): string {

View File

@ -22,6 +22,8 @@ When requested to perform tasks like fixing bugs, adding features, refactoring,
1. **Understand:** Think about the user's request and the relevant codebase context. Use 'search_file_content' and 'glob' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use 'read_file' and 'read_many_files' to understand context and validate any assumptions you may have.
2. **Plan:** Build a coherent and grounded (based off of the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should try to use a self verification loop by writing unit tests if relevant to the task. Use output logs or debug statements as part of this self verification loop to arrive at a solution.
3. **Implement:** Use the available tools (e.g., 'replace', 'write_file' 'run_shell_command' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands.
5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, you can ask the user if they'd like you to run them and if so how to.
## New Applications
@ -200,6 +202,8 @@ When requested to perform tasks like fixing bugs, adding features, refactoring,
1. **Understand:** Think about the user's request and the relevant codebase context. Use 'search_file_content' and 'glob' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use 'read_file' and 'read_many_files' to understand context and validate any assumptions you may have.
2. **Plan:** Build a coherent and grounded (based off of the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should try to use a self verification loop by writing unit tests if relevant to the task. Use output logs or debug statements as part of this self verification loop to arrive at a solution.
3. **Implement:** Use the available tools (e.g., 'replace', 'write_file' 'run_shell_command' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands.
5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, you can ask the user if they'd like you to run them and if so how to.
## New Applications
@ -388,6 +392,8 @@ When requested to perform tasks like fixing bugs, adding features, refactoring,
1. **Understand:** Think about the user's request and the relevant codebase context. Use 'search_file_content' and 'glob' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use 'read_file' and 'read_many_files' to understand context and validate any assumptions you may have.
2. **Plan:** Build a coherent and grounded (based off of the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should try to use a self verification loop by writing unit tests if relevant to the task. Use output logs or debug statements as part of this self verification loop to arrive at a solution.
3. **Implement:** Use the available tools (e.g., 'replace', 'write_file' 'run_shell_command' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands.
5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, you can ask the user if they'd like you to run them and if so how to.
## New Applications
@ -561,6 +567,8 @@ When requested to perform tasks like fixing bugs, adding features, refactoring,
1. **Understand:** Think about the user's request and the relevant codebase context. Use 'search_file_content' and 'glob' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use 'read_file' and 'read_many_files' to understand context and validate any assumptions you may have.
2. **Plan:** Build a coherent and grounded (based off of the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should try to use a self verification loop by writing unit tests if relevant to the task. Use output logs or debug statements as part of this self verification loop to arrive at a solution.
3. **Implement:** Use the available tools (e.g., 'replace', 'write_file' 'run_shell_command' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands.
5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, you can ask the user if they'd like you to run them and if so how to.
## New Applications
@ -734,6 +742,8 @@ When requested to perform tasks like fixing bugs, adding features, refactoring,
1. **Understand:** Think about the user's request and the relevant codebase context. Use 'search_file_content' and 'glob' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use 'read_file' and 'read_many_files' to understand context and validate any assumptions you may have.
2. **Plan:** Build a coherent and grounded (based off of the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should try to use a self verification loop by writing unit tests if relevant to the task. Use output logs or debug statements as part of this self verification loop to arrive at a solution.
3. **Implement:** Use the available tools (e.g., 'replace', 'write_file' 'run_shell_command' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands.
5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, you can ask the user if they'd like you to run them and if so how to.
## New Applications
@ -907,6 +917,8 @@ When requested to perform tasks like fixing bugs, adding features, refactoring,
1. **Understand:** Think about the user's request and the relevant codebase context. Use 'search_file_content' and 'glob' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use 'read_file' and 'read_many_files' to understand context and validate any assumptions you may have.
2. **Plan:** Build a coherent and grounded (based off of the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should try to use a self verification loop by writing unit tests if relevant to the task. Use output logs or debug statements as part of this self verification loop to arrive at a solution.
3. **Implement:** Use the available tools (e.g., 'replace', 'write_file' 'run_shell_command' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands.
5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, you can ask the user if they'd like you to run them and if so how to.
## New Applications
@ -1080,6 +1092,8 @@ When requested to perform tasks like fixing bugs, adding features, refactoring,
1. **Understand:** Think about the user's request and the relevant codebase context. Use 'search_file_content' and 'glob' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use 'read_file' and 'read_many_files' to understand context and validate any assumptions you may have.
2. **Plan:** Build a coherent and grounded (based off of the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should try to use a self verification loop by writing unit tests if relevant to the task. Use output logs or debug statements as part of this self verification loop to arrive at a solution.
3. **Implement:** Use the available tools (e.g., 'replace', 'write_file' 'run_shell_command' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands.
5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, you can ask the user if they'd like you to run them and if so how to.
## New Applications
@ -1253,6 +1267,8 @@ When requested to perform tasks like fixing bugs, adding features, refactoring,
1. **Understand:** Think about the user's request and the relevant codebase context. Use 'search_file_content' and 'glob' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use 'read_file' and 'read_many_files' to understand context and validate any assumptions you may have.
2. **Plan:** Build a coherent and grounded (based off of the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should try to use a self verification loop by writing unit tests if relevant to the task. Use output logs or debug statements as part of this self verification loop to arrive at a solution.
3. **Implement:** Use the available tools (e.g., 'replace', 'write_file' 'run_shell_command' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands.
5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, you can ask the user if they'd like you to run them and if so how to.
## New Applications
@ -1426,6 +1442,8 @@ When requested to perform tasks like fixing bugs, adding features, refactoring,
1. **Understand:** Think about the user's request and the relevant codebase context. Use 'search_file_content' and 'glob' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use 'read_file' and 'read_many_files' to understand context and validate any assumptions you may have.
2. **Plan:** Build a coherent and grounded (based off of the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should try to use a self verification loop by writing unit tests if relevant to the task. Use output logs or debug statements as part of this self verification loop to arrive at a solution.
3. **Implement:** Use the available tools (e.g., 'replace', 'write_file' 'run_shell_command' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands.
5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, you can ask the user if they'd like you to run them and if so how to.
## New Applications

View File

@ -13,7 +13,7 @@ import {
GoogleGenAI,
} from '@google/genai';
import { GeminiClient } from './client.js';
import { ContentGenerator } from './contentGenerator.js';
import { AuthType, ContentGenerator } from './contentGenerator.js';
import { GeminiChat } from './geminiChat.js';
import { Config } from '../config/config.js';
import { Turn } from './turn.js';
@ -102,13 +102,17 @@ describe('Gemini Client (client.ts)', () => {
};
const fileService = new FileDiscoveryService('/test/dir');
const MockedConfig = vi.mocked(Config, true);
const contentGeneratorConfig = {
model: 'test-model',
apiKey: 'test-key',
vertexai: false,
authType: AuthType.USE_GEMINI,
};
MockedConfig.mockImplementation(() => {
const mock = {
getContentGeneratorConfig: vi.fn().mockReturnValue({
model: 'test-model',
apiKey: 'test-key',
vertexai: false,
}),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue(contentGeneratorConfig),
getToolRegistry: vi.fn().mockResolvedValue(mockToolRegistry),
getModel: vi.fn().mockReturnValue('test-model'),
getEmbeddingModel: vi.fn().mockReturnValue('test-embedding-model'),
@ -131,7 +135,7 @@ describe('Gemini Client (client.ts)', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockConfig = new Config({} as any);
client = new GeminiClient(mockConfig);
await client.initialize();
await client.initialize(contentGeneratorConfig);
});
afterEach(() => {

View File

@ -33,6 +33,7 @@ import { getErrorMessage } from '../utils/errors.js';
import { tokenLimit } from './tokenLimits.js';
import {
ContentGenerator,
ContentGeneratorConfig,
createContentGenerator,
} from './contentGenerator.js';
import { ProxyAgent, setGlobalDispatcher } from 'undici';
@ -63,12 +64,18 @@ export class GeminiClient {
this.embeddingModel = config.getEmbeddingModel();
}
async initialize() {
async initialize(contentGeneratorConfig: ContentGeneratorConfig) {
this.contentGenerator = await createContentGenerator(
this.config.getContentGeneratorConfig(),
contentGeneratorConfig,
);
this.chat = await this.startChat();
}
private getContentGenerator(): ContentGenerator {
if (!this.contentGenerator) {
throw new Error('Content generator not initialized');
}
return this.contentGenerator;
}
async addHistory(content: Content) {
this.getChat().addHistory(content);
@ -81,13 +88,6 @@ export class GeminiClient {
return this.chat;
}
private getContentGenerator(): ContentGenerator {
if (!this.contentGenerator) {
throw new Error('Content generator not initialized');
}
return this.contentGenerator;
}
async getHistory(): Promise<Content[]> {
return this.getChat().getHistory();
}

View File

@ -5,7 +5,7 @@
*/
import { describe, it, expect, vi } from 'vitest';
import { createContentGenerator } from './contentGenerator.js';
import { createContentGenerator, AuthType } from './contentGenerator.js';
import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js';
import { GoogleGenAI } from '@google/genai';
@ -20,7 +20,7 @@ describe('contentGenerator', () => {
);
const generator = await createContentGenerator({
model: 'test-model',
codeAssist: true,
authType: AuthType.LOGIN_WITH_GOOGLE_PERSONAL,
});
expect(createCodeAssistContentGenerator).toHaveBeenCalled();
expect(generator).toBe(mockGenerator);
@ -34,6 +34,7 @@ describe('contentGenerator', () => {
const generator = await createContentGenerator({
model: 'test-model',
apiKey: 'test-api-key',
authType: AuthType.USE_GEMINI,
});
expect(GoogleGenAI).toHaveBeenCalledWith({
apiKey: 'test-api-key',

View File

@ -14,6 +14,8 @@ import {
GoogleGenAI,
} from '@google/genai';
import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js';
import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
import { getEffectiveModel } from './modelCheck.js';
/**
* Interface abstracting the core functionalities for generating content and counting tokens.
@ -32,13 +34,77 @@ export interface ContentGenerator {
embedContent(request: EmbedContentParameters): Promise<EmbedContentResponse>;
}
export enum AuthType {
LOGIN_WITH_GOOGLE_PERSONAL = 'oauth-personal',
LOGIN_WITH_GOOGLE_ENTERPRISE = 'oauth-enterprise',
USE_GEMINI = 'gemini-api-key',
USE_VERTEX_AI = 'vertex-ai',
}
export type ContentGeneratorConfig = {
model: string;
apiKey?: string;
vertexai?: boolean;
codeAssist?: boolean;
authType?: AuthType | undefined;
};
export async function createContentGeneratorConfig(
model: string | undefined,
authType: AuthType | undefined,
): Promise<ContentGeneratorConfig> {
const geminiApiKey = process.env.GEMINI_API_KEY;
const googleApiKey = process.env.GOOGLE_API_KEY;
const googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT;
const googleCloudLocation = process.env.GOOGLE_CLOUD_LOCATION;
const contentGeneratorConfig: ContentGeneratorConfig = {
model: model || DEFAULT_GEMINI_MODEL,
authType,
};
// if we are using google auth nothing else to validate for now
if (authType === AuthType.LOGIN_WITH_GOOGLE_PERSONAL) {
return contentGeneratorConfig;
}
// if its enterprise make sure we have a cloud project
if (
authType === AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE &&
!!googleCloudProject
) {
return contentGeneratorConfig;
}
//
if (authType === AuthType.USE_GEMINI && geminiApiKey) {
contentGeneratorConfig.apiKey = geminiApiKey;
contentGeneratorConfig.model = await getEffectiveModel(
contentGeneratorConfig.apiKey,
contentGeneratorConfig.model,
);
return contentGeneratorConfig;
}
if (
authType === AuthType.USE_VERTEX_AI &&
!!googleApiKey &&
googleCloudProject &&
googleCloudLocation
) {
contentGeneratorConfig.apiKey = googleApiKey;
contentGeneratorConfig.vertexai = true;
contentGeneratorConfig.model = await getEffectiveModel(
contentGeneratorConfig.apiKey,
contentGeneratorConfig.model,
);
return contentGeneratorConfig;
}
return contentGeneratorConfig;
}
export async function createContentGenerator(
config: ContentGeneratorConfig,
): Promise<ContentGenerator> {
@ -48,13 +114,27 @@ export async function createContentGenerator(
'User-Agent': `GeminiCLI/${version}/(${process.platform}; ${process.arch})`,
},
};
if (config.codeAssist) {
return await createCodeAssistContentGenerator(httpOptions);
if (
config.authType === AuthType.LOGIN_WITH_GOOGLE_PERSONAL ||
config.authType === AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE
) {
return createCodeAssistContentGenerator(httpOptions, config.authType);
}
const googleGenAI = new GoogleGenAI({
apiKey: config.apiKey === '' ? undefined : config.apiKey,
vertexai: config.vertexai,
httpOptions,
});
return googleGenAI.models;
if (
config.authType === AuthType.USE_GEMINI ||
config.authType === AuthType.USE_VERTEX_AI
) {
const googleGenAI = new GoogleGenAI({
apiKey: config.apiKey === '' ? undefined : config.apiKey,
vertexai: config.vertexai,
httpOptions,
});
return googleGenAI.models;
}
throw new Error(
`Error creating contentGenerator: Unsupported authType: ${config.authType}`,
);
}

View File

@ -58,6 +58,8 @@ When requested to perform tasks like fixing bugs, adding features, refactoring,
1. **Understand:** Think about the user's request and the relevant codebase context. Use '${GrepTool.Name}' and '${GlobTool.Name}' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use '${ReadFileTool.Name}' and '${ReadManyFilesTool.Name}' to understand context and validate any assumptions you may have.
2. **Plan:** Build a coherent and grounded (based off of the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should try to use a self verification loop by writing unit tests if relevant to the task. Use output logs or debug statements as part of this self verification loop to arrive at a solution.
3. **Implement:** Use the available tools (e.g., '${EditTool.Name}', '${WriteFileTool.Name}' '${ShellTool.Name}' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands.
5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, you can ask the user if they'd like you to run them and if so how to.
## New Applications

View File

@ -20,6 +20,7 @@ import { getResponseText } from '../utils/generateContentResponseUtilities.js';
import { reportError } from '../utils/errorReporting.js';
import { getErrorMessage } from '../utils/errors.js';
import { GeminiChat } from './geminiChat.js';
import { isAuthError } from '../code_assist/errors.js';
// Define a structure for tools passed to the server
export interface ServerTool {
@ -222,6 +223,9 @@ export class Turn {
};
}
} catch (error) {
if (isAuthError(error)) {
throw error;
}
if (signal.aborted) {
yield { type: GeminiEventType.UserCancelled };
// Regular cancellation error, fail gracefully.

View File

@ -20,6 +20,8 @@ export * from './core/coreToolScheduler.js';
export * from './core/nonInteractiveToolExecutor.js';
export * from './code_assist/codeAssist.js';
export * from './code_assist/oauth2.js';
export * from './code_assist/errors.js';
// Export utilities
export * from './utils/paths.js';

View File

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { ToolConfirmationOutcome } from '../index.js';
import { AuthType, ToolConfirmationOutcome } from '../index.js';
import { logs } from '@opentelemetry/api-logs';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import { Config } from '../config/config.js';
@ -57,8 +57,7 @@ describe('loggers', () => {
getContentGeneratorConfig: () => ({
model: 'test-model',
apiKey: 'test-api-key',
vertexai: true,
codeAssist: false,
authType: AuthType.USE_VERTEX_AI,
}),
getTelemetryLogPromptsEnabled: () => true,
getFileFilteringRespectGitIgnore: () => true,
@ -86,7 +85,6 @@ describe('loggers', () => {
approval_mode: 'default',
api_key_enabled: true,
vertex_ai_enabled: true,
code_assist_enabled: false,
log_user_prompts_enabled: true,
file_filtering_respect_git_ignore: true,
debug_mode: true,

View File

@ -35,6 +35,7 @@ import {
GenerateContentResponse,
GenerateContentResponseUsageMetadata,
} from '@google/genai';
import { AuthType } from '../core/contentGenerator.js';
const shouldLogUserPrompts = (config: Config): boolean =>
config.getTelemetryLogPromptsEnabled() ?? false;
@ -72,6 +73,14 @@ export function logCliConfiguration(config: Config): void {
if (!isTelemetrySdkInitialized()) return;
const generatorConfig = config.getContentGeneratorConfig();
let useGemini = false;
let useVertex = false;
if (generatorConfig && generatorConfig.authType) {
useGemini = generatorConfig.authType === AuthType.USE_GEMINI;
useVertex = generatorConfig.authType === AuthType.USE_VERTEX_AI;
}
const mcpServers = config.getMcpServers();
const attributes: LogAttributes = {
...getCommonAttributes(config),
@ -82,9 +91,8 @@ export function logCliConfiguration(config: Config): void {
sandbox_enabled: !!config.getSandbox(),
core_tools_enabled: (config.getCoreTools() ?? []).join(','),
approval_mode: config.getApprovalMode(),
api_key_enabled: !!generatorConfig.apiKey,
vertex_ai_enabled: !!generatorConfig.vertexai,
code_assist_enabled: !!generatorConfig.codeAssist,
api_key_enabled: useGemini || useVertex,
vertex_ai_enabled: useVertex,
log_user_prompts_enabled: config.getTelemetryLogPromptsEnabled(),
file_filtering_respect_git_ignore:
config.getFileFilteringRespectGitIgnore(),

View File

@ -27,9 +27,7 @@ describe('telemetry', () => {
mockConfig = new Config({
sessionId: 'test-session-id',
contentGeneratorConfig: {
model: 'test-model',
},
model: 'test-model',
targetDir: '/test/dir',
debugMode: false,
cwd: '/test/dir',

View File

@ -125,11 +125,7 @@ class MockTool extends BaseTool<{ param: string }, ToolResult> {
const baseConfigParams: ConfigParameters = {
cwd: '/tmp',
contentGeneratorConfig: {
model: 'test-model',
apiKey: 'test-api-key',
vertexai: false,
},
model: 'test-model',
embeddingModel: 'test-embedding-model',
sandbox: undefined,
targetDir: '/test/dir',