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:
parent
c48fcaa8c3
commit
04518b52c0
|
@ -11,7 +11,10 @@
|
|||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "start"],
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"cwd": "${workspaceFolder}"
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"GEMINI_SANDBOX": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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.';
|
||||
};
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -359,6 +359,7 @@ describe('useGeminiStream', () => {
|
|||
props.handleSlashCommand,
|
||||
props.shellModeActive,
|
||||
() => 'vscode' as EditorType,
|
||||
() => {},
|
||||
),
|
||||
{
|
||||
initialProps: {
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {},
|
||||
) {}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}`,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue