feat(auth): support gemini api key and vertex auth in non-interactive mode (#4631)

This commit is contained in:
Jerop Kipruto 2025-07-22 10:52:40 -04:00 committed by GitHub
parent 4d653c833a
commit e306b34a6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 209 additions and 27 deletions

View File

@ -107,3 +107,24 @@ GOOGLE_CLOUD_PROJECT="your-project-id"
GEMINI_API_KEY="your-gemini-api-key"
EOF
```
## Non-Interactive Mode / Headless Environments
When running the Gemini CLI in a non-interactive environment, you cannot use the interactive login flow.
Instead, you must configure authentication using environment variables.
The CLI will automatically detect if it is running in a non-interactive terminal and will use one of the
following authentication methods if available:
1. **Gemini API Key:**
- Set the `GEMINI_API_KEY` environment variable.
- The CLI will use this key to authenticate with the Gemini API.
2. **Vertex AI:**
- Set the `GOOGLE_GENAI_USE_VERTEXAI=true` environment variable.
- **Using an API Key:** Set the `GOOGLE_API_KEY` environment variable.
- **Using Application Default Credentials (ADC):**
- Run `gcloud auth application-default login` in your environment to configure ADC.
- Ensure the `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION` environment variables are set.
If none of these environment variables are set in a non-interactive session, the CLI will exit with an error.

View File

@ -17,7 +17,6 @@ import { start_sandbox } from './utils/sandbox.js';
import {
LoadedSettings,
loadSettings,
USER_SETTINGS_PATH,
SettingScope,
} from './config/settings.js';
import { themeManager } from './ui/themes/theme-manager.js';
@ -40,6 +39,7 @@ import {
} from '@google/gemini-cli-core';
import { validateAuthMethod } from './config/auth.js';
import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js';
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
function getNodeMemoryArgs(config: Config): string[] {
const totalMemoryMB = os.totalmem() / (1024 * 1024);
@ -320,33 +320,8 @@ async function loadNonInteractiveConfig(
await finalConfig.initialize();
}
return await validateNonInterActiveAuth(
return await validateNonInteractiveAuth(
settings.merged.selectedAuthType,
finalConfig,
);
}
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 ${USER_SETTINGS_PATH} 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

@ -0,0 +1,148 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
validateNonInteractiveAuth,
NonInteractiveConfig,
} from './validateNonInterActiveAuth.js';
import { AuthType } from '@google/gemini-cli-core';
describe('validateNonInterActiveAuth', () => {
let originalEnvGeminiApiKey: string | undefined;
let originalEnvVertexAi: string | undefined;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
let processExitSpy: ReturnType<typeof vi.spyOn>;
let refreshAuthMock: jest.MockedFunction<
(authType: AuthType) => Promise<unknown>
>;
beforeEach(() => {
originalEnvGeminiApiKey = process.env.GEMINI_API_KEY;
originalEnvVertexAi = process.env.GOOGLE_GENAI_USE_VERTEXAI;
delete process.env.GEMINI_API_KEY;
delete process.env.GOOGLE_GENAI_USE_VERTEXAI;
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
throw new Error(`process.exit(${code}) called`);
});
refreshAuthMock = vi.fn().mockResolvedValue('refreshed');
});
afterEach(() => {
if (originalEnvGeminiApiKey !== undefined) {
process.env.GEMINI_API_KEY = originalEnvGeminiApiKey;
} else {
delete process.env.GEMINI_API_KEY;
}
if (originalEnvVertexAi !== undefined) {
process.env.GOOGLE_GENAI_USE_VERTEXAI = originalEnvVertexAi;
} else {
delete process.env.GOOGLE_GENAI_USE_VERTEXAI;
}
vi.restoreAllMocks();
});
it('exits if no auth type is configured or env vars set', async () => {
const nonInteractiveConfig: NonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
try {
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Please set an Auth method'),
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});
it('uses USE_GEMINI if GEMINI_API_KEY is set', async () => {
process.env.GEMINI_API_KEY = 'fake-key';
const nonInteractiveConfig: NonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
});
it('uses USE_VERTEX_AI if GOOGLE_GENAI_USE_VERTEXAI is true (with GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION)', async () => {
process.env.GOOGLE_GENAI_USE_VERTEXAI = 'true';
process.env.GOOGLE_CLOUD_PROJECT = 'test-project';
process.env.GOOGLE_CLOUD_LOCATION = 'us-central1';
const nonInteractiveConfig: NonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
});
it('uses USE_VERTEX_AI if GOOGLE_GENAI_USE_VERTEXAI is true and GOOGLE_API_KEY is set', async () => {
process.env.GOOGLE_GENAI_USE_VERTEXAI = 'true';
process.env.GOOGLE_API_KEY = 'vertex-api-key';
const nonInteractiveConfig: NonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
});
it('uses USE_VERTEX_AI if both GEMINI_API_KEY and GOOGLE_GENAI_USE_VERTEXAI are set', async () => {
process.env.GEMINI_API_KEY = 'fake-key';
process.env.GOOGLE_GENAI_USE_VERTEXAI = 'true';
process.env.GOOGLE_CLOUD_PROJECT = 'test-project';
process.env.GOOGLE_CLOUD_LOCATION = 'us-central1';
const nonInteractiveConfig: NonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
});
it('uses USE_GEMINI if GOOGLE_GENAI_USE_VERTEXAI is false, GEMINI_API_KEY is set, and project/location are available', async () => {
process.env.GOOGLE_GENAI_USE_VERTEXAI = 'false';
process.env.GEMINI_API_KEY = 'fake-key';
process.env.GOOGLE_CLOUD_PROJECT = 'test-project';
process.env.GOOGLE_CLOUD_LOCATION = 'us-central1';
const nonInteractiveConfig: NonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
});
it('uses configuredAuthType if provided', async () => {
// Set required env var for USE_GEMINI
process.env.GEMINI_API_KEY = 'fake-key';
const nonInteractiveConfig: NonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
await validateNonInteractiveAuth(AuthType.USE_GEMINI, nonInteractiveConfig);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
});
it('exits if validateAuthMethod returns error', async () => {
// Mock validateAuthMethod to return error
const mod = await import('./config/auth.js');
vi.spyOn(mod, 'validateAuthMethod').mockReturnValue('Auth error!');
const nonInteractiveConfig: NonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
try {
await validateNonInteractiveAuth(
AuthType.USE_GEMINI,
nonInteractiveConfig,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(consoleErrorSpy).toHaveBeenCalledWith('Auth error!');
expect(processExitSpy).toHaveBeenCalledWith(1);
});
});

View File

@ -0,0 +1,38 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { AuthType, Config } from '@google/gemini-cli-core';
import { USER_SETTINGS_PATH } from './config/settings.js';
import { validateAuthMethod } from './config/auth.js';
export async function validateNonInteractiveAuth(
configuredAuthType: AuthType | undefined,
nonInteractiveConfig: Config,
) {
const effectiveAuthType =
configuredAuthType ||
(process.env.GOOGLE_GENAI_USE_VERTEXAI === 'true'
? AuthType.USE_VERTEX_AI
: process.env.GEMINI_API_KEY
? AuthType.USE_GEMINI
: undefined);
if (!effectiveAuthType) {
console.error(
`Please set an Auth method in your ${USER_SETTINGS_PATH} or specify either the GEMINI_API_KEY or GOOGLE_GENAI_USE_VERTEXAI environment variables before running`,
);
process.exit(1);
}
const err = validateAuthMethod(effectiveAuthType);
if (err != null) {
console.error(err);
process.exit(1);
}
await nonInteractiveConfig.refreshAuth(effectiveAuthType);
return nonInteractiveConfig;
}