Improve auth env var validation logic and messaging to detect settings that confuse GenAI SDK (#1381)

Co-authored-by: Scott Densmore <scottdensmore@mac.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Marat Boshernitsan 2025-07-08 09:37:10 -07:00 committed by GitHub
parent 5c759d48c7
commit f1647d9e19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 175 additions and 30 deletions

View File

@ -47,18 +47,16 @@ The Gemini CLI requires you to authenticate with Google's AI services. On initia
gcloud auth application-default login
```
For more information, see [Set up Application Default Credentials for Google Cloud](https://cloud.google.com/docs/authentication/provide-credentials-adc).
- Set the `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, and `GOOGLE_GENAI_USE_VERTEXAI` environment variables. In the following methods, replace `YOUR_PROJECT_ID` and `YOUR_PROJECT_LOCATION` with the relevant values for your project:
- Set the `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION` environment variables. In the following methods, replace `YOUR_PROJECT_ID` and `YOUR_PROJECT_LOCATION` with the relevant values for your project:
- You can temporarily set these environment variables in your current shell session using the following commands:
```bash
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION" # e.g., us-central1
export GOOGLE_GENAI_USE_VERTEXAI=true
```
- For repeated use, you can add the environment variables to your [.env file](#persisting-environment-variables-with-env-files) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following commands add the environment variables to a `~/.bashrc` file:
```bash
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
echo 'export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"' >> ~/.bashrc
echo 'export GOOGLE_GENAI_USE_VERTEXAI=true' >> ~/.bashrc
source ~/.bashrc
```
- If using express mode:
@ -66,12 +64,10 @@ The Gemini CLI requires you to authenticate with Google's AI services. On initia
- You can temporarily set these environment variables in your current shell session using the following commands:
```bash
export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
export GOOGLE_GENAI_USE_VERTEXAI=true
```
- For repeated use, you can add the environment variables to your [.env file](#persisting-environment-variables-with-env-files) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following commands add the environment variables to a `~/.bashrc` file:
```bash
echo 'export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"' >> ~/.bashrc
echo 'export GOOGLE_GENAI_USE_VERTEXAI=true' >> ~/.bashrc
source ~/.bashrc
```
4. **Cloud Shell:**

View File

@ -242,12 +242,12 @@ The CLI automatically loads environment variables from an `.env` file. The loadi
- **`GOOGLE_API_KEY`**:
- Your Google Cloud API key.
- Required for using Vertex AI in express mode.
- Ensure you have the necessary permissions and set the `GOOGLE_GENAI_USE_VERTEXAI=true` environment variable.
- Ensure you have the necessary permissions.
- Example: `export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"`.
- **`GOOGLE_CLOUD_PROJECT`**:
- Your Google Cloud Project ID.
- Required for using Code Assist or Vertex AI.
- If using Vertex AI, ensure you have the necessary permissions and set the `GOOGLE_GENAI_USE_VERTEXAI=true` environment variable.
- If using Vertex AI, ensure you have the necessary permissions in this project.
- **Cloud Shell Note:** When running in a Cloud Shell environment, this variable defaults to a special project allocated for Cloud Shell users. If you have `GOOGLE_CLOUD_PROJECT` set in your global environment in Cloud Shell, it will be overridden by this default. To use a different project in Cloud Shell, you must define `GOOGLE_CLOUD_PROJECT` in a `.env` file.
- Example: `export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
- **`GOOGLE_APPLICATION_CREDENTIALS`** (string):
@ -258,8 +258,7 @@ The CLI automatically loads environment variables from an `.env` file. The loadi
- Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
- **`GOOGLE_CLOUD_LOCATION`**:
- Your Google Cloud Project Location (e.g., us-central1).
- Required for using Vertex AI in non-express mode.
- If using Vertex AI, ensure you have the necessary permissions and set the `GOOGLE_GENAI_USE_VERTEXAI=true` environment variable.
- Required for using Vertex AI in non express mode.
- Example: `export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"`.
- **`GEMINI_SANDBOX`**:
- Alternative to the `sandbox` setting in `settings.json`.

View File

@ -40,7 +40,7 @@ describe('validateAuthMethod', () => {
it('should return an error message if GEMINI_API_KEY is not set', () => {
expect(validateAuthMethod(AuthType.USE_GEMINI)).toBe(
'GEMINI_API_KEY environment variable not found. Add that to your .env and try again, no reload needed!',
'GEMINI_API_KEY environment variable not found. Add that to your environment and try again (no reload needed if using .env)!',
);
});
});
@ -59,10 +59,10 @@ describe('validateAuthMethod', () => {
it('should return an error message if no required environment variables are set', () => {
expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBe(
'Must specify GOOGLE_GENAI_USE_VERTEXAI=true and either:\n' +
'When using Vertex AI, you must specify either:\n' +
'• GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION environment variables.\n' +
'• GOOGLE_API_KEY environment variable (if using express mode).\n' +
'Update your .env and try again, no reload needed!',
'Update your environment and try again (no reload needed if using .env)!',
);
});
});

View File

@ -18,7 +18,7 @@ export const validateAuthMethod = (authMethod: string): string | 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 'GEMINI_API_KEY environment variable not found. Add that to your environment and try again (no reload needed if using .env)!';
}
return null;
}
@ -29,10 +29,10 @@ export const validateAuthMethod = (authMethod: string): string | null => {
const hasGoogleApiKey = !!process.env.GOOGLE_API_KEY;
if (!hasVertexProjectLocationConfig && !hasGoogleApiKey) {
return (
'Must specify GOOGLE_GENAI_USE_VERTEXAI=true and either:\n' +
'When using Vertex AI, you must specify either:\n' +
'• GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION environment variables.\n' +
'• GOOGLE_API_KEY environment variable (if using express mode).\n' +
'Update your .env and try again, no reload needed!'
'Update your environment and try again (no reload needed if using .env)!'
);
}
return null;

View File

@ -0,0 +1,82 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { setupUser, ProjectIdRequiredError } from './setup.js';
import { CodeAssistServer } from '../code_assist/server.js';
import { OAuth2Client } from 'google-auth-library';
import { GeminiUserTier, UserTierId } from './types.js';
vi.mock('../code_assist/server.js');
const mockPaidTier: GeminiUserTier = {
id: UserTierId.STANDARD,
name: 'paid',
description: 'Paid tier',
};
describe('setupUser', () => {
let mockLoad: ReturnType<typeof vi.fn>;
let mockOnboardUser: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.resetAllMocks();
mockLoad = vi.fn();
mockOnboardUser = vi.fn().mockResolvedValue({
done: true,
response: {
cloudaicompanionProject: {
id: 'server-project',
},
},
});
vi.mocked(CodeAssistServer).mockImplementation(
() =>
({
loadCodeAssist: mockLoad,
onboardUser: mockOnboardUser,
}) as unknown as CodeAssistServer,
);
});
it('should use GOOGLE_CLOUD_PROJECT when set', async () => {
process.env.GOOGLE_CLOUD_PROJECT = 'test-project';
mockLoad.mockResolvedValue({
currentTier: mockPaidTier,
});
await setupUser({} as OAuth2Client);
expect(CodeAssistServer).toHaveBeenCalledWith(
expect.any(Object),
'test-project',
);
});
it('should treat empty GOOGLE_CLOUD_PROJECT as undefined and use project from server', async () => {
process.env.GOOGLE_CLOUD_PROJECT = '';
mockLoad.mockResolvedValue({
cloudaicompanionProject: 'server-project',
currentTier: mockPaidTier,
});
const projectId = await setupUser({} as OAuth2Client);
expect(CodeAssistServer).toHaveBeenCalledWith(
expect.any(Object),
undefined,
);
expect(projectId).toBe('server-project');
});
it('should throw ProjectIdRequiredError when no project ID is available', async () => {
delete process.env.GOOGLE_CLOUD_PROJECT;
// And the server itself requires a project ID internally
vi.mocked(CodeAssistServer).mockImplementation(() => {
throw new ProjectIdRequiredError();
});
await expect(setupUser({} as OAuth2Client)).rejects.toThrow(
ProjectIdRequiredError,
);
});
});

View File

@ -28,7 +28,7 @@ export class ProjectIdRequiredError extends Error {
* @returns the user's actual project id
*/
export async function setupUser(client: OAuth2Client): Promise<string> {
let projectId = process.env.GOOGLE_CLOUD_PROJECT;
let projectId = process.env.GOOGLE_CLOUD_PROJECT || undefined;
const caServer = new CodeAssistServer(client, projectId);
const clientMetadata: ClientMetadata = {

View File

@ -4,15 +4,19 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { createContentGenerator, AuthType } from './contentGenerator.js';
import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest';
import {
createContentGenerator,
AuthType,
createContentGeneratorConfig,
} from './contentGenerator.js';
import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js';
import { GoogleGenAI } from '@google/genai';
vi.mock('../code_assist/codeAssist.js');
vi.mock('@google/genai');
describe('contentGenerator', () => {
describe('createContentGenerator', () => {
it('should create a CodeAssistContentGenerator', async () => {
const mockGenerator = {} as unknown;
vi.mocked(createCodeAssistContentGenerator).mockResolvedValue(
@ -48,3 +52,72 @@ describe('contentGenerator', () => {
expect(generator).toBe((mockGenerator as GoogleGenAI).models);
});
});
describe('createContentGeneratorConfig', () => {
const originalEnv = process.env;
beforeEach(() => {
// Reset modules to re-evaluate imports and environment variables
vi.resetModules();
// Restore process.env before each test
process.env = { ...originalEnv };
});
afterAll(() => {
// Restore original process.env after all tests
process.env = originalEnv;
});
it('should configure for Gemini using GEMINI_API_KEY when set', async () => {
process.env.GEMINI_API_KEY = 'env-gemini-key';
const config = await createContentGeneratorConfig(
undefined,
AuthType.USE_GEMINI,
);
expect(config.apiKey).toBe('env-gemini-key');
expect(config.vertexai).toBe(false);
});
it('should not configure for Gemini if GEMINI_API_KEY is empty', async () => {
process.env.GEMINI_API_KEY = '';
const config = await createContentGeneratorConfig(
undefined,
AuthType.USE_GEMINI,
);
expect(config.apiKey).toBeUndefined();
expect(config.vertexai).toBeUndefined();
});
it('should configure for Vertex AI using GOOGLE_API_KEY when set', async () => {
process.env.GOOGLE_API_KEY = 'env-google-key';
const config = await createContentGeneratorConfig(
undefined,
AuthType.USE_VERTEX_AI,
);
expect(config.apiKey).toBe('env-google-key');
expect(config.vertexai).toBe(true);
});
it('should configure for Vertex AI using GCP project and location when set', async () => {
process.env.GOOGLE_CLOUD_PROJECT = 'env-gcp-project';
process.env.GOOGLE_CLOUD_LOCATION = 'env-gcp-location';
const config = await createContentGeneratorConfig(
undefined,
AuthType.USE_VERTEX_AI,
);
expect(config.vertexai).toBe(true);
expect(config.apiKey).toBeUndefined();
});
it('should not configure for Vertex AI if required env vars are empty', async () => {
process.env.GOOGLE_API_KEY = '';
process.env.GOOGLE_CLOUD_PROJECT = '';
process.env.GOOGLE_CLOUD_LOCATION = '';
const config = await createContentGeneratorConfig(
undefined,
AuthType.USE_VERTEX_AI,
);
expect(config.apiKey).toBeUndefined();
expect(config.vertexai).toBeUndefined();
});
});

View File

@ -52,10 +52,10 @@ 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 geminiApiKey = process.env.GEMINI_API_KEY || undefined;
const googleApiKey = process.env.GOOGLE_API_KEY || undefined;
const googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT || undefined;
const googleCloudLocation = process.env.GOOGLE_CLOUD_LOCATION || undefined;
// Use runtime model from config if available, otherwise fallback to parameter or default
const effectiveModel = model || DEFAULT_GEMINI_MODEL;
@ -75,6 +75,7 @@ export async function createContentGeneratorConfig(
if (authType === AuthType.USE_GEMINI && geminiApiKey) {
contentGeneratorConfig.apiKey = geminiApiKey;
contentGeneratorConfig.vertexai = false;
contentGeneratorConfig.model = await getEffectiveModel(
contentGeneratorConfig.apiKey,
contentGeneratorConfig.model,
@ -85,16 +86,10 @@ export async function createContentGeneratorConfig(
if (
authType === AuthType.USE_VERTEX_AI &&
!!googleApiKey &&
googleCloudProject &&
googleCloudLocation
(googleApiKey || (googleCloudProject && googleCloudLocation))
) {
contentGeneratorConfig.apiKey = googleApiKey;
contentGeneratorConfig.vertexai = true;
contentGeneratorConfig.model = await getEffectiveModel(
contentGeneratorConfig.apiKey,
contentGeneratorConfig.model,
);
return contentGeneratorConfig;
}