Merge "Login with Google Workspace" auth option into "Login with Google" (#1574)
Co-authored-by: Scott Densmore <scottdensmore@mac.com>
This commit is contained in:
parent
00b24c917e
commit
79c647d486
|
@ -4,9 +4,21 @@ The Gemini CLI requires you to authenticate with Google's AI services. On initia
|
||||||
|
|
||||||
1. **Login with Google (Gemini Code Assist):**
|
1. **Login with Google (Gemini Code Assist):**
|
||||||
|
|
||||||
- Use this option to log in with the standard, personal account you use for services like Gmail, Google Photos, and Google Drive for personal use (e.g. your-name@gmail.com).
|
- Use this option to log in with your google account.
|
||||||
- During initial startup, Gemini CLI will direct you to a webpage for authentication. Once authenticated, your credentials will be cached locally so the web login can be skipped on subsequent runs.
|
- During initial startup, Gemini CLI will direct you to a webpage for authentication. Once authenticated, your credentials will be cached locally so the web login can be skipped on subsequent runs.
|
||||||
- Note that the web login must be done in a browser that can communicate with the machine Gemini CLI is being run from. (Specifically, the browser will be redirected to a localhost url that Gemini CLI will be listening on).
|
- Note that the web login must be done in a browser that can communicate with the machine Gemini CLI is being run from. (Specifically, the browser will be redirected to a localhost url that Gemini CLI will be listening on).
|
||||||
|
- Users may have to specify a GOOGLE_CLOUD_PROJECT if:
|
||||||
|
1. You have a Google Workspace account. Google Workspace is a paid service for businesses and organizations that provides a suite of productivity tools, including a custom email domain (e.g. your-name@your-company.com), enhanced security features, and administrative controls. These accounts are often managed by an employer or school.
|
||||||
|
2. You are a licensed Code Assist user. This can happen if you have previously purchased a Code Assist license or have acquired one through Google Developer Program.
|
||||||
|
- If you fall into one of these categories, you must first configure a Google Cloud Project Id to use, [enable the Gemini for Cloud API](https://cloud.google.com/gemini/docs/discover/set-up-gemini#enable-api) and [configure access permissions](https://cloud.google.com/gemini/docs/discover/set-up-gemini#grant-iam). You can temporarily set the environment variable in your current shell session using the following command:
|
||||||
|
```bash
|
||||||
|
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||||
|
```
|
||||||
|
- For repeated use, you can add the environment variable to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
|
||||||
|
```bash
|
||||||
|
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
|
||||||
|
source ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
2. **<a id="gemini-api-key"></a>Gemini API key:**
|
2. **<a id="gemini-api-key"></a>Gemini API key:**
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,6 @@ export const validateAuthMethod = (authMethod: string): string | null => {
|
||||||
return null;
|
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 (authMethod === AuthType.USE_GEMINI) {
|
||||||
if (!process.env.GEMINI_API_KEY) {
|
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 .env and try again, no reload needed!';
|
||||||
|
|
|
@ -29,32 +29,15 @@ export function AuthDialog({
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(
|
const [errorMessage, setErrorMessage] = useState<string | null>(
|
||||||
initialErrorMessage || null,
|
initialErrorMessage || null,
|
||||||
);
|
);
|
||||||
const allAuthItems = [
|
const items = [
|
||||||
{
|
{
|
||||||
label: 'Login with Google',
|
label: 'Login with Google',
|
||||||
value: AuthType.LOGIN_WITH_GOOGLE_PERSONAL,
|
value: AuthType.LOGIN_WITH_GOOGLE_PERSONAL,
|
||||||
},
|
},
|
||||||
{ label: 'Gemini API Key', value: AuthType.USE_GEMINI },
|
{ label: 'Gemini API Key', value: AuthType.USE_GEMINI },
|
||||||
{
|
|
||||||
label: 'Login with Google (for Workspace or licensed Code Assist users)',
|
|
||||||
value: AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE,
|
|
||||||
},
|
|
||||||
{ label: 'Vertex AI', value: AuthType.USE_VERTEX_AI },
|
{ label: 'Vertex AI', value: AuthType.USE_VERTEX_AI },
|
||||||
];
|
];
|
||||||
|
|
||||||
const isSelectedAuthInMore = allAuthItems
|
|
||||||
.slice(2)
|
|
||||||
.some((item) => item.value === settings.merged.selectedAuthType);
|
|
||||||
|
|
||||||
const [showAll, setShowAll] = useState(isSelectedAuthInMore);
|
|
||||||
|
|
||||||
const initialAuthItems = [
|
|
||||||
...allAuthItems.slice(0, 2),
|
|
||||||
{ label: 'More...', value: 'more' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const items = showAll ? allAuthItems : initialAuthItems;
|
|
||||||
|
|
||||||
let initialAuthIndex = items.findIndex(
|
let initialAuthIndex = items.findIndex(
|
||||||
(item) => item.value === settings.merged.selectedAuthType,
|
(item) => item.value === settings.merged.selectedAuthType,
|
||||||
);
|
);
|
||||||
|
@ -64,10 +47,6 @@ export function AuthDialog({
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAuthSelect = (authMethod: string) => {
|
const handleAuthSelect = (authMethod: string) => {
|
||||||
if (authMethod === 'more') {
|
|
||||||
setShowAll(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const error = validateAuthMethod(authMethod);
|
const error = validateAuthMethod(authMethod);
|
||||||
if (error) {
|
if (error) {
|
||||||
setErrorMessage(error);
|
setErrorMessage(error);
|
||||||
|
|
|
@ -46,12 +46,16 @@ export const useAuthCommand = (
|
||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorMessage =
|
let errorMessage = `Failed to login.\nMessage: ${getErrorMessage(e)}`;
|
||||||
|
if (
|
||||||
settings.merged.selectedAuthType ===
|
settings.merged.selectedAuthType ===
|
||||||
AuthType.LOGIN_WITH_GOOGLE_PERSONAL
|
AuthType.LOGIN_WITH_GOOGLE_PERSONAL &&
|
||||||
? `Failed to login. Ensure the Google account you are using is not a Workspace account and that you are not a licensed Code Assist user (see https://goo.gle/gemini-cli-auth-docs#workspace-gca).
|
!process.env.GOOGLE_CLOUD_PROJECT
|
||||||
Message: ${getErrorMessage(e)}`
|
) {
|
||||||
: `Failed to login. Message: ${getErrorMessage(e)}`;
|
errorMessage =
|
||||||
|
'Failed to login. Workspace accounts and licensed Code Assist users must configure' +
|
||||||
|
` GOOGLE_CLOUD_PROJECT (see https://goo.gle/gemini-cli-auth-docs#workspace-gca).\nMessage: ${getErrorMessage(e)}`;
|
||||||
|
}
|
||||||
setAuthError(errorMessage);
|
setAuthError(errorMessage);
|
||||||
openAuthDialog();
|
openAuthDialog();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -1043,7 +1043,7 @@ describe('useGeminiStream', () => {
|
||||||
it('should call parseAndFormatApiError with the correct authType on stream initialization failure', async () => {
|
it('should call parseAndFormatApiError with the correct authType on stream initialization failure', async () => {
|
||||||
// 1. Setup
|
// 1. Setup
|
||||||
const mockError = new Error('Rate limit exceeded');
|
const mockError = new Error('Rate limit exceeded');
|
||||||
const mockAuthType = AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE;
|
const mockAuthType = AuthType.LOGIN_WITH_GOOGLE_PERSONAL;
|
||||||
mockParseAndFormatApiError.mockClear();
|
mockParseAndFormatApiError.mockClear();
|
||||||
mockSendMessageStream.mockReturnValue(
|
mockSendMessageStream.mockReturnValue(
|
||||||
(async function* () {
|
(async function* () {
|
||||||
|
|
|
@ -29,12 +29,12 @@ describe('parseAndFormatApiError', () => {
|
||||||
expect(result).toContain('Your request has been rate limited');
|
expect(result).toContain('Your request has been rate limited');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format a 429 API error with the enterprise message', () => {
|
it('should format a 429 API error with the personal message', () => {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}';
|
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}';
|
||||||
const result = parseAndFormatApiError(
|
const result = parseAndFormatApiError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE,
|
AuthType.LOGIN_WITH_GOOGLE_PERSONAL,
|
||||||
);
|
);
|
||||||
expect(result).toContain('[API Error: Rate limit exceeded');
|
expect(result).toContain('[API Error: Rate limit exceeded');
|
||||||
expect(result).toContain(enterpriseMessage);
|
expect(result).toContain(enterpriseMessage);
|
||||||
|
|
|
@ -46,7 +46,6 @@ function isStructuredError(error: unknown): error is StructuredError {
|
||||||
function getRateLimitMessage(authType?: AuthType): string {
|
function getRateLimitMessage(authType?: AuthType): string {
|
||||||
switch (authType) {
|
switch (authType) {
|
||||||
case AuthType.LOGIN_WITH_GOOGLE_PERSONAL:
|
case AuthType.LOGIN_WITH_GOOGLE_PERSONAL:
|
||||||
case AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE:
|
|
||||||
return RATE_LIMIT_ERROR_MESSAGE_GOOGLE;
|
return RATE_LIMIT_ERROR_MESSAGE_GOOGLE;
|
||||||
case AuthType.USE_GEMINI:
|
case AuthType.USE_GEMINI:
|
||||||
return RATE_LIMIT_ERROR_MESSAGE_USE_GEMINI;
|
return RATE_LIMIT_ERROR_MESSAGE_USE_GEMINI;
|
||||||
|
|
|
@ -13,10 +13,7 @@ export async function createCodeAssistContentGenerator(
|
||||||
httpOptions: HttpOptions,
|
httpOptions: HttpOptions,
|
||||||
authType: AuthType,
|
authType: AuthType,
|
||||||
): Promise<ContentGenerator> {
|
): Promise<ContentGenerator> {
|
||||||
if (
|
if (authType === AuthType.LOGIN_WITH_GOOGLE_PERSONAL) {
|
||||||
authType === AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE ||
|
|
||||||
authType === AuthType.LOGIN_WITH_GOOGLE_PERSONAL
|
|
||||||
) {
|
|
||||||
const authClient = await getOauthClient();
|
const authClient = await getOauthClient();
|
||||||
const projectId = await setupUser(authClient);
|
const projectId = await setupUser(authClient);
|
||||||
return new CodeAssistServer(authClient, projectId, httpOptions);
|
return new CodeAssistServer(authClient, projectId, httpOptions);
|
||||||
|
|
|
@ -505,10 +505,7 @@ export class GeminiClient {
|
||||||
*/
|
*/
|
||||||
private async handleFlashFallback(authType?: string): Promise<string | null> {
|
private async handleFlashFallback(authType?: string): Promise<string | null> {
|
||||||
// Only handle fallback for OAuth users
|
// Only handle fallback for OAuth users
|
||||||
if (
|
if (authType !== AuthType.LOGIN_WITH_GOOGLE_PERSONAL) {
|
||||||
authType !== AuthType.LOGIN_WITH_GOOGLE_PERSONAL &&
|
|
||||||
authType !== AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,6 @@ export interface ContentGenerator {
|
||||||
|
|
||||||
export enum AuthType {
|
export enum AuthType {
|
||||||
LOGIN_WITH_GOOGLE_PERSONAL = 'oauth-personal',
|
LOGIN_WITH_GOOGLE_PERSONAL = 'oauth-personal',
|
||||||
LOGIN_WITH_GOOGLE_ENTERPRISE = 'oauth-enterprise',
|
|
||||||
USE_GEMINI = 'gemini-api-key',
|
USE_GEMINI = 'gemini-api-key',
|
||||||
USE_VERTEX_AI = 'vertex-ai',
|
USE_VERTEX_AI = 'vertex-ai',
|
||||||
}
|
}
|
||||||
|
@ -71,14 +70,6 @@ export async function createContentGeneratorConfig(
|
||||||
return contentGeneratorConfig;
|
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) {
|
if (authType === AuthType.USE_GEMINI && geminiApiKey) {
|
||||||
contentGeneratorConfig.apiKey = geminiApiKey;
|
contentGeneratorConfig.apiKey = geminiApiKey;
|
||||||
|
@ -118,10 +109,7 @@ export async function createContentGenerator(
|
||||||
'User-Agent': `GeminiCLI/${version} (${process.platform}; ${process.arch})`,
|
'User-Agent': `GeminiCLI/${version} (${process.platform}; ${process.arch})`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (
|
if (config.authType === AuthType.LOGIN_WITH_GOOGLE_PERSONAL) {
|
||||||
config.authType === AuthType.LOGIN_WITH_GOOGLE_PERSONAL ||
|
|
||||||
config.authType === AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE
|
|
||||||
) {
|
|
||||||
return createCodeAssistContentGenerator(httpOptions, config.authType);
|
return createCodeAssistContentGenerator(httpOptions, config.authType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -188,10 +188,7 @@ export class GeminiChat {
|
||||||
*/
|
*/
|
||||||
private async handleFlashFallback(authType?: string): Promise<string | null> {
|
private async handleFlashFallback(authType?: string): Promise<string | null> {
|
||||||
// Only handle fallback for OAuth users
|
// Only handle fallback for OAuth users
|
||||||
if (
|
if (authType !== AuthType.LOGIN_WITH_GOOGLE_PERSONAL) {
|
||||||
authType !== AuthType.LOGIN_WITH_GOOGLE_PERSONAL &&
|
|
||||||
authType !== AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -275,35 +275,6 @@ describe('retryWithBackoff', () => {
|
||||||
expect(mockFn).toHaveBeenCalledTimes(4); // 3 initial attempts + 1 after fallback
|
expect(mockFn).toHaveBeenCalledTimes(4); // 3 initial attempts + 1 after fallback
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should trigger fallback for OAuth enterprise users after persistent 429 errors', async () => {
|
|
||||||
const fallbackCallback = vi.fn().mockResolvedValue('gemini-2.5-flash');
|
|
||||||
|
|
||||||
let fallbackOccurred = false;
|
|
||||||
const mockFn = vi.fn().mockImplementation(async () => {
|
|
||||||
if (!fallbackOccurred) {
|
|
||||||
const error: HttpError = new Error('Rate limit exceeded');
|
|
||||||
error.status = 429;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return 'success';
|
|
||||||
});
|
|
||||||
|
|
||||||
const promise = retryWithBackoff(mockFn, {
|
|
||||||
maxAttempts: 3,
|
|
||||||
initialDelayMs: 100,
|
|
||||||
onPersistent429: async (authType?: string) => {
|
|
||||||
fallbackOccurred = true;
|
|
||||||
return await fallbackCallback(authType);
|
|
||||||
},
|
|
||||||
authType: 'oauth-enterprise',
|
|
||||||
});
|
|
||||||
|
|
||||||
await vi.runAllTimersAsync();
|
|
||||||
|
|
||||||
await expect(promise).resolves.toBe('success');
|
|
||||||
expect(fallbackCallback).toHaveBeenCalledWith('oauth-enterprise');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should NOT trigger fallback for API key users', async () => {
|
it('should NOT trigger fallback for API key users', async () => {
|
||||||
const fallbackCallback = vi.fn();
|
const fallbackCallback = vi.fn();
|
||||||
|
|
||||||
|
|
|
@ -99,8 +99,7 @@ export async function retryWithBackoff<T>(
|
||||||
if (
|
if (
|
||||||
consecutive429Count >= 2 &&
|
consecutive429Count >= 2 &&
|
||||||
onPersistent429 &&
|
onPersistent429 &&
|
||||||
(authType === AuthType.LOGIN_WITH_GOOGLE_PERSONAL ||
|
authType === AuthType.LOGIN_WITH_GOOGLE_PERSONAL
|
||||||
authType === AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE)
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const fallbackModel = await onPersistent429(authType);
|
const fallbackModel = await onPersistent429(authType);
|
||||||
|
|
Loading…
Reference in New Issue