diff --git a/packages/cli/src/config/auth.ts b/packages/cli/src/config/auth.ts index 4676bb2f..e9fb4b59 100644 --- a/packages/cli/src/config/auth.ts +++ b/packages/cli/src/config/auth.ts @@ -16,6 +16,18 @@ export const validateAuthMethod = (authMethod: string): string | null => { return null; } + if (authMethod === AuthType.LOGIN_WITH_GOOGLE_GCA) { + if (!process.env['GOOGLE_CLOUD_PROJECT']) { + return ( + '[Error] GOOGLE_CLOUD_PROJECT is not set.\n' + + 'Please set it using:\n' + + ' export GOOGLE_CLOUD_PROJECT=\n' + + 'and try again.' + ); + } + 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 environment and try again (no reload needed if using .env)!'; diff --git a/packages/cli/src/ui/commands/aboutCommand.test.ts b/packages/cli/src/ui/commands/aboutCommand.test.ts index 9e8be38e..81aaad04 100644 --- a/packages/cli/src/ui/commands/aboutCommand.test.ts +++ b/packages/cli/src/ui/commands/aboutCommand.test.ts @@ -6,12 +6,12 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { aboutCommand } from './aboutCommand.js'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import * as versionUtils from '../../utils/version.js'; import { MessageType } from '../types.js'; -import { IdeClient } from '../../../../core/src/ide/ide-client.js'; +import type { IdeClient } from '@google/gemini-cli-core'; vi.mock('../../utils/version.js', () => ({ getCliVersion: vi.fn(), @@ -29,10 +29,11 @@ describe('aboutCommand', () => { getModel: vi.fn(), getIdeClient: vi.fn(), getIdeMode: vi.fn().mockReturnValue(true), + getGeminiClient: vi.fn(), }, settings: { merged: { - selectedAuthType: 'test-auth', + selectedAuthType: 'oauth-gca', }, }, }, @@ -52,6 +53,11 @@ describe('aboutCommand', () => { vi.spyOn(mockContext.services.config!, 'getIdeClient').mockReturnValue({ getDetectedIdeDisplayName: vi.fn().mockReturnValue('test-ide'), } as Partial as IdeClient); + vi.spyOn(mockContext.services.config!, 'getGeminiClient').mockReturnValue({ + getUserTier: vi.fn().mockReturnValue(undefined), + } as unknown as ReturnType< + NonNullable['getGeminiClient'] + >); }); afterEach(() => { @@ -83,9 +89,10 @@ describe('aboutCommand', () => { osVersion: 'test-os', sandboxEnv: 'no sandbox', modelVersion: 'test-model', - selectedAuthType: 'test-auth', + selectedAuthType: 'oauth-gca', gcpProject: 'test-gcp-project', ideClient: 'test-ide', + userTier: undefined, }, expect.any(Number), ); @@ -125,11 +132,14 @@ describe('aboutCommand', () => { }); it('should not show ide client when it is not detected', async () => { + // Change to oauth type that doesn't use GCP project + mockContext.services.settings.merged.selectedAuthType = 'oauth'; + vi.spyOn(mockContext.services.config!, 'getIdeClient').mockReturnValue({ getDetectedIdeDisplayName: vi.fn().mockReturnValue(undefined), } as Partial as IdeClient); - process.env.SANDBOX = ''; + process.env['SANDBOX'] = ''; if (!aboutCommand.action) { throw new Error('The about command must have an action.'); } @@ -143,8 +153,8 @@ describe('aboutCommand', () => { osVersion: 'test-os', sandboxEnv: 'no sandbox', modelVersion: 'test-model', - selectedAuthType: 'test-auth', - gcpProject: 'test-gcp-project', + selectedAuthType: 'oauth', + gcpProject: '', ideClient: '', }), expect.any(Number), diff --git a/packages/cli/src/ui/commands/aboutCommand.ts b/packages/cli/src/ui/commands/aboutCommand.ts index 44bf00dd..92d250d5 100644 --- a/packages/cli/src/ui/commands/aboutCommand.ts +++ b/packages/cli/src/ui/commands/aboutCommand.ts @@ -27,11 +27,18 @@ export const aboutCommand: SlashCommand = { const cliVersion = await getCliVersion(); const selectedAuthType = context.services.settings.merged.selectedAuthType || ''; - const gcpProject = process.env['GOOGLE_CLOUD_PROJECT'] || ''; + // Only show GCP Project for auth types that actually use it + const gcpProject = + selectedAuthType === 'oauth-gca' || + selectedAuthType === 'vertex-ai' || + selectedAuthType === 'cloud-shell' + ? process.env['GOOGLE_CLOUD_PROJECT'] || '' + : ''; const ideClient = (context.services.config?.getIdeMode() && context.services.config?.getIdeClient()?.getDetectedIdeDisplayName()) || ''; + const userTier = context.services.config?.getGeminiClient()?.getUserTier(); const aboutItem: Omit = { type: MessageType.ABOUT, @@ -42,6 +49,7 @@ export const aboutCommand: SlashCommand = { selectedAuthType, gcpProject, ideClient, + userTier, }; context.ui.addItem(aboutItem, Date.now()); diff --git a/packages/cli/src/ui/components/AboutBox.tsx b/packages/cli/src/ui/components/AboutBox.tsx index a0954576..9970ed4d 100644 --- a/packages/cli/src/ui/components/AboutBox.tsx +++ b/packages/cli/src/ui/components/AboutBox.tsx @@ -8,6 +8,8 @@ import React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { GIT_COMMIT_INFO } from '../../generated/git-commit.js'; +import { UserTierId } from '@google/gemini-cli-core'; +import { getLicenseDisplay } from '../../utils/license.js'; interface AboutBoxProps { cliVersion: string; @@ -17,6 +19,7 @@ interface AboutBoxProps { selectedAuthType: string; gcpProject: string; ideClient: string; + userTier?: UserTierId; } export const AboutBox: React.FC = ({ @@ -27,6 +30,7 @@ export const AboutBox: React.FC = ({ selectedAuthType, gcpProject, ideClient, + userTier, }) => ( = ({ + + + + License + + + + {getLicenseDisplay(selectedAuthType, userTier)} + + {gcpProject && ( diff --git a/packages/cli/src/ui/components/AuthDialog.tsx b/packages/cli/src/ui/components/AuthDialog.tsx index fb3d116b..483308dc 100644 --- a/packages/cli/src/ui/components/AuthDialog.tsx +++ b/packages/cli/src/ui/components/AuthDialog.tsx @@ -62,9 +62,14 @@ export function AuthDialog({ }); const items = [ { - label: 'Login with Google', + label: 'Login with Google - Free Tier', value: AuthType.LOGIN_WITH_GOOGLE, }, + { + label: + 'Login with Google - Gemini Code Assist (Requires GOOGLE_CLOUD_PROJECT)', + value: AuthType.LOGIN_WITH_GOOGLE_GCA, + }, ...(process.env['CLOUD_SHELL'] === 'true' ? [ { diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx index 89dd0149..5ccba8b7 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx @@ -74,6 +74,7 @@ export const HistoryItemDisplay: React.FC = ({ selectedAuthType={item.selectedAuthType} gcpProject={item.gcpProject} ideClient={item.ideClient} + userTier={item.userTier} /> )} {item.type === 'help' && commands && } diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index f798283a..bc4406c4 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -7,6 +7,7 @@ import { ToolCallConfirmationDetails, ToolResultDisplay, + UserTierId, } from '@google/gemini-cli-core'; // Only defining the state enum needed by the UI @@ -96,6 +97,7 @@ export type HistoryItemAbout = HistoryItemBase & { selectedAuthType: string; gcpProject: string; ideClient: string; + userTier?: UserTierId; }; export type HistoryItemHelp = HistoryItemBase & { diff --git a/packages/cli/src/utils/license.test.ts b/packages/cli/src/utils/license.test.ts new file mode 100644 index 00000000..1a9fc02b --- /dev/null +++ b/packages/cli/src/utils/license.test.ts @@ -0,0 +1,119 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { getLicenseDisplay } from './license.js'; +import { AuthType, UserTierId } from '@google/gemini-cli-core'; + +describe('getLicenseDisplay', () => { + describe('Free Tier (Login with Google)', () => { + it('should return Free Tier for LOGIN_WITH_GOOGLE', () => { + expect(getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE)).toBe( + 'Free Tier (Login with Google)', + ); + }); + + it('should ignore userTier for LOGIN_WITH_GOOGLE', () => { + expect( + getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE, UserTierId.STANDARD), + ).toBe('Free Tier (Login with Google)'); + expect( + getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE, UserTierId.LEGACY), + ).toBe('Free Tier (Login with Google)'); + }); + }); + + describe('Gemini Code Assist (Google Workspace)', () => { + it('should return GCA Standard for LOGIN_WITH_GOOGLE_GCA with STANDARD tier', () => { + expect( + getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE_GCA, UserTierId.STANDARD), + ).toBe('Gemini Code Assist Standard (Google Workspace)'); + }); + + it('should return GCA Enterprise for LOGIN_WITH_GOOGLE_GCA with LEGACY tier', () => { + expect( + getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE_GCA, UserTierId.LEGACY), + ).toBe('Gemini Code Assist Enterprise (Google Workspace)'); + }); + + it('should return generic GCA for LOGIN_WITH_GOOGLE_GCA without tier', () => { + expect(getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE_GCA)).toBe( + 'Gemini Code Assist (Google Workspace)', + ); + }); + + it('should return generic GCA for LOGIN_WITH_GOOGLE_GCA with unknown tier', () => { + expect( + getLicenseDisplay( + AuthType.LOGIN_WITH_GOOGLE_GCA, + 'unknown-tier' as UserTierId, + ), + ).toBe('Gemini Code Assist (Google Workspace)'); + }); + + it('should return generic GCA for LOGIN_WITH_GOOGLE_GCA with FREE tier', () => { + expect( + getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE_GCA, UserTierId.FREE), + ).toBe('Gemini Code Assist (Google Workspace)'); + }); + }); + + describe('Gemini API Key', () => { + it('should return Gemini API Key for USE_GEMINI', () => { + expect(getLicenseDisplay(AuthType.USE_GEMINI)).toBe('Gemini API Key'); + }); + + it('should ignore userTier for USE_GEMINI', () => { + expect(getLicenseDisplay(AuthType.USE_GEMINI, UserTierId.STANDARD)).toBe( + 'Gemini API Key', + ); + }); + }); + + describe('Vertex AI', () => { + it('should return Vertex AI for USE_VERTEX_AI', () => { + expect(getLicenseDisplay(AuthType.USE_VERTEX_AI)).toBe('Vertex AI'); + }); + + it('should ignore userTier for USE_VERTEX_AI', () => { + expect(getLicenseDisplay(AuthType.USE_VERTEX_AI, UserTierId.LEGACY)).toBe( + 'Vertex AI', + ); + }); + }); + + describe('Cloud Shell', () => { + it('should return Cloud Shell for CLOUD_SHELL', () => { + expect(getLicenseDisplay(AuthType.CLOUD_SHELL)).toBe('Cloud Shell'); + }); + + it('should ignore userTier for CLOUD_SHELL', () => { + expect(getLicenseDisplay(AuthType.CLOUD_SHELL, UserTierId.STANDARD)).toBe( + 'Cloud Shell', + ); + }); + }); + + describe('Unknown auth types', () => { + it('should return the auth type as-is for unknown values', () => { + expect(getLicenseDisplay('custom-auth-type')).toBe('custom-auth-type'); + expect(getLicenseDisplay('oauth')).toBe('oauth'); + expect(getLicenseDisplay('unknown-auth')).toBe('unknown-auth'); + }); + + it('should handle undefined gracefully', () => { + expect(getLicenseDisplay(undefined as unknown as string)).toBe(undefined); + }); + + it('should handle null gracefully', () => { + expect(getLicenseDisplay(null as unknown as string)).toBe(null); + }); + + it('should handle empty string', () => { + expect(getLicenseDisplay('')).toBe(''); + }); + }); +}); diff --git a/packages/cli/src/utils/license.ts b/packages/cli/src/utils/license.ts new file mode 100644 index 00000000..a6ae3b8e --- /dev/null +++ b/packages/cli/src/utils/license.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AuthType, UserTierId } from '@google/gemini-cli-core'; + +/** + * Get human-readable license display text based on auth type and user tier. + * @param selectedAuthType - The authentication type selected by the user + * @param userTier - Optional user tier information from the server + * @returns Human-readable license information + */ +export function getLicenseDisplay( + selectedAuthType: string, + userTier?: UserTierId, +): string { + switch (selectedAuthType) { + case AuthType.LOGIN_WITH_GOOGLE: + return 'Free Tier (Login with Google)'; + + case AuthType.LOGIN_WITH_GOOGLE_GCA: + if (userTier === UserTierId.STANDARD) { + return 'Gemini Code Assist Standard (Google Workspace)'; + } else if (userTier === UserTierId.LEGACY) { + return 'Gemini Code Assist Enterprise (Google Workspace)'; + } + return 'Gemini Code Assist (Google Workspace)'; + + case AuthType.USE_GEMINI: + return 'Gemini API Key'; + + case AuthType.USE_VERTEX_AI: + return 'Vertex AI'; + + case AuthType.CLOUD_SHELL: + return 'Cloud Shell'; + + default: + return selectedAuthType; + } +} diff --git a/packages/core/src/code_assist/codeAssist.ts b/packages/core/src/code_assist/codeAssist.ts index f9c6a7a3..b5838209 100644 --- a/packages/core/src/code_assist/codeAssist.ts +++ b/packages/core/src/code_assist/codeAssist.ts @@ -18,10 +18,11 @@ export async function createCodeAssistContentGenerator( ): Promise { if ( authType === AuthType.LOGIN_WITH_GOOGLE || + authType === AuthType.LOGIN_WITH_GOOGLE_GCA || authType === AuthType.CLOUD_SHELL ) { const authClient = await getOauthClient(authType, config); - const userData = await setupUser(authClient); + const userData = await setupUser(authClient, authType); return new CodeAssistServer( authClient, userData.projectId, diff --git a/packages/core/src/code_assist/setup.test.ts b/packages/core/src/code_assist/setup.test.ts index 61a7431a..a069686c 100644 --- a/packages/core/src/code_assist/setup.test.ts +++ b/packages/core/src/code_assist/setup.test.ts @@ -5,10 +5,15 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { setupUser, ProjectIdRequiredError } from './setup.js'; +import { + setupUser, + ProjectIdRequiredError, + ProjectAccessError, +} from './setup.js'; import { CodeAssistServer } from '../code_assist/server.js'; import { OAuth2Client } from 'google-auth-library'; import { GeminiUserTier, UserTierId } from './types.js'; +import { AuthType } from '../core/contentGenerator.js'; vi.mock('../code_assist/server.js'); @@ -58,8 +63,9 @@ describe('setupUser for existing user', () => { vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'test-project'); mockLoad.mockResolvedValue({ currentTier: mockPaidTier, + cloudaicompanionProject: 'test-project', }); - await setupUser({} as OAuth2Client); + await setupUser({} as OAuth2Client, AuthType.LOGIN_WITH_GOOGLE_GCA); expect(CodeAssistServer).toHaveBeenCalledWith( {}, 'test-project', @@ -75,7 +81,10 @@ describe('setupUser for existing user', () => { cloudaicompanionProject: 'server-project', currentTier: mockPaidTier, }); - const projectId = await setupUser({} as OAuth2Client); + const projectId = await setupUser( + {} as OAuth2Client, + AuthType.LOGIN_WITH_GOOGLE_GCA, + ); expect(CodeAssistServer).toHaveBeenCalledWith( {}, 'test-project', @@ -96,9 +105,9 @@ describe('setupUser for existing user', () => { throw new ProjectIdRequiredError(); }); - await expect(setupUser({} as OAuth2Client)).rejects.toThrow( - ProjectIdRequiredError, - ); + await expect( + setupUser({} as OAuth2Client, AuthType.LOGIN_WITH_GOOGLE_GCA), + ).rejects.toThrow(ProjectIdRequiredError); }); }); @@ -135,7 +144,10 @@ describe('setupUser for new user', () => { mockLoad.mockResolvedValue({ allowedTiers: [mockPaidTier], }); - const userData = await setupUser({} as OAuth2Client); + const userData = await setupUser( + {} as OAuth2Client, + AuthType.LOGIN_WITH_GOOGLE_GCA, + ); expect(CodeAssistServer).toHaveBeenCalledWith( {}, 'test-project', @@ -165,7 +177,10 @@ describe('setupUser for new user', () => { mockLoad.mockResolvedValue({ allowedTiers: [mockFreeTier], }); - const userData = await setupUser({} as OAuth2Client); + const userData = await setupUser( + {} as OAuth2Client, + AuthType.LOGIN_WITH_GOOGLE, + ); expect(CodeAssistServer).toHaveBeenCalledWith( {}, undefined, @@ -189,7 +204,7 @@ describe('setupUser for new user', () => { }); }); - it('should use GOOGLE_CLOUD_PROJECT when onboard response has no project ID', async () => { + it('should throw ProjectAccessError when LOGIN_WITH_GOOGLE_GCA onboard response has no project ID', async () => { vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'test-project'); mockLoad.mockResolvedValue({ allowedTiers: [mockPaidTier], @@ -200,11 +215,9 @@ describe('setupUser for new user', () => { cloudaicompanionProject: undefined, }, }); - const userData = await setupUser({} as OAuth2Client); - expect(userData).toEqual({ - projectId: 'test-project', - userTier: 'standard-tier', - }); + await expect( + setupUser({} as OAuth2Client, AuthType.LOGIN_WITH_GOOGLE_GCA), + ).rejects.toThrow(ProjectAccessError); }); it('should throw ProjectIdRequiredError when no project ID is available', async () => { @@ -216,8 +229,8 @@ describe('setupUser for new user', () => { done: true, response: {}, }); - await expect(setupUser({} as OAuth2Client)).rejects.toThrow( - ProjectIdRequiredError, - ); + await expect( + setupUser({} as OAuth2Client, AuthType.LOGIN_WITH_GOOGLE_GCA), + ).rejects.toThrow(ProjectIdRequiredError); }); }); diff --git a/packages/core/src/code_assist/setup.ts b/packages/core/src/code_assist/setup.ts index d563301d..32415274 100644 --- a/packages/core/src/code_assist/setup.ts +++ b/packages/core/src/code_assist/setup.ts @@ -13,6 +13,7 @@ import { } from './types.js'; import { CodeAssistServer } from './server.js'; import { OAuth2Client } from 'google-auth-library'; +import { AuthType } from '../core/contentGenerator.js'; export class ProjectIdRequiredError extends Error { constructor() { @@ -22,6 +23,41 @@ export class ProjectIdRequiredError extends Error { } } +export class ProjectAccessError extends Error { + constructor(projectId: string, details?: string) { + super( + `Failed to access GCP project "${projectId}" for Gemini Code Assist.\n` + + `${details || ''}\n` + + `Please verify:\n` + + `1. The project ID is correct\n` + + `2. You have the necessary permissions for this project\n` + + `3. The Gemini for Cloud API is enabled for this project\n` + + `\n` + + `To use a different project:\n` + + ` export GOOGLE_CLOUD_PROJECT=\n` + + `\n` + + `To use Free Tier instead, run /auth and select "Login with Google - Free Tier"`, + ); + } +} + +export class LicenseMismatchError extends Error { + constructor(expected: string, actual: string) { + super( + `License type mismatch detected.\n` + + `You selected: ${expected}\n` + + `But the server returned: ${actual}\n` + + `\n` + + `This may indicate:\n` + + `1. The project doesn't have a valid GCA license\n` + + `2. You don't have access to the specified project\n` + + `3. The project configuration is incorrect\n` + + `\n` + + `Please verify your project settings or contact your administrator.`, + ); + } +} + export interface UserData { projectId: string; userTier: UserTierId; @@ -29,11 +65,20 @@ export interface UserData { /** * - * @param projectId the user's project id, if any - * @returns the user's actual project id + * @param client OAuth2 client + * @param authType the authentication type being used + * @returns the user's actual project id and tier */ -export async function setupUser(client: OAuth2Client): Promise { - const projectId = process.env['GOOGLE_CLOUD_PROJECT'] || undefined; +export async function setupUser( + client: OAuth2Client, + authType: AuthType, +): Promise { + // Only use GOOGLE_CLOUD_PROJECT for GCA login or Cloud Shell + const projectId = + authType === AuthType.LOGIN_WITH_GOOGLE_GCA || + authType === AuthType.CLOUD_SHELL + ? process.env['GOOGLE_CLOUD_PROJECT'] || undefined + : undefined; const caServer = new CodeAssistServer(client, projectId, {}, '', undefined); const coreClientMetadata: ClientMetadata = { ideType: 'IDE_UNSPECIFIED', @@ -41,22 +86,56 @@ export async function setupUser(client: OAuth2Client): Promise { pluginType: 'GEMINI', }; - const loadRes = await caServer.loadCodeAssist({ - cloudaicompanionProject: projectId, - metadata: { - ...coreClientMetadata, - duetProject: projectId, - }, - }); + let loadRes: LoadCodeAssistResponse; + try { + loadRes = await caServer.loadCodeAssist({ + cloudaicompanionProject: projectId, + metadata: { + ...coreClientMetadata, + duetProject: projectId, + }, + }); + } catch (error) { + // If GCA login failed with a project, throw a clear error + if (authType === AuthType.LOGIN_WITH_GOOGLE_GCA && projectId) { + throw new ProjectAccessError( + projectId, + error instanceof Error ? error.message : 'Authentication failed', + ); + } + throw error; + } if (loadRes.currentTier) { + // Check for license mismatch - GCA selected but Free Tier returned + if ( + authType === AuthType.LOGIN_WITH_GOOGLE_GCA && + loadRes.currentTier.id === UserTierId.FREE + ) { + throw new LicenseMismatchError('Gemini Code Assist (GCA)', 'Free Tier'); + } + if (!loadRes.cloudaicompanionProject) { if (projectId) { + // GCA with project but no cloudaicompanionProject means project access issue + if (authType === AuthType.LOGIN_WITH_GOOGLE_GCA) { + throw new ProjectAccessError( + projectId, + 'The project exists but is not configured for Gemini Code Assist', + ); + } return { projectId, userTier: loadRes.currentTier.id, }; } + // For Free Tier login, don't require project ID + if (authType === AuthType.LOGIN_WITH_GOOGLE) { + return { + projectId: '', + userTier: loadRes.currentTier.id, + }; + } throw new ProjectIdRequiredError(); } return { @@ -67,8 +146,16 @@ export async function setupUser(client: OAuth2Client): Promise { const tier = getOnboardTier(loadRes); + // Check for license mismatch during onboarding + if ( + authType === AuthType.LOGIN_WITH_GOOGLE_GCA && + tier.id === UserTierId.FREE + ) { + throw new LicenseMismatchError('Gemini Code Assist (GCA)', 'Free Tier'); + } + let onboardReq: OnboardUserRequest; - if (tier.id === UserTierId.FREE) { + if (tier.id === UserTierId.FREE || authType === AuthType.LOGIN_WITH_GOOGLE) { // The free tier uses a managed google cloud project. Setting a project in the `onboardUser` request causes a `Precondition Failed` error. onboardReq = { tierId: tier.id, @@ -95,18 +182,42 @@ export async function setupUser(client: OAuth2Client): Promise { if (!lroRes.response?.cloudaicompanionProject?.id) { if (projectId) { + // GCA with project but onboarding didn't return a project + if (authType === AuthType.LOGIN_WITH_GOOGLE_GCA) { + throw new ProjectAccessError( + projectId, + 'Failed to onboard to Gemini Code Assist with this project', + ); + } return { projectId, userTier: tier.id, }; } + // For Free Tier login, don't require project ID + if (authType === AuthType.LOGIN_WITH_GOOGLE) { + return { + projectId: '', + userTier: tier.id, + }; + } throw new ProjectIdRequiredError(); } - return { + // Final validation: ensure GCA users don't get Free Tier + const finalUserData = { projectId: lroRes.response.cloudaicompanionProject.id, userTier: tier.id, }; + + if ( + authType === AuthType.LOGIN_WITH_GOOGLE_GCA && + finalUserData.userTier === UserTierId.FREE + ) { + throw new LicenseMismatchError('Gemini Code Assist (GCA)', 'Free Tier'); + } + + return finalUserData; } function getOnboardTier(res: LoadCodeAssistResponse): GeminiUserTier { diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 387ae951..cfbff84e 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -44,6 +44,7 @@ export interface ContentGenerator { export enum AuthType { LOGIN_WITH_GOOGLE = 'oauth-personal', + LOGIN_WITH_GOOGLE_GCA = 'oauth-gca', USE_GEMINI = 'gemini-api-key', USE_VERTEX_AI = 'vertex-ai', CLOUD_SHELL = 'cloud-shell', @@ -78,6 +79,7 @@ export function createContentGeneratorConfig( // If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now if ( authType === AuthType.LOGIN_WITH_GOOGLE || + authType === AuthType.LOGIN_WITH_GOOGLE_GCA || authType === AuthType.CLOUD_SHELL ) { return contentGeneratorConfig; @@ -116,6 +118,7 @@ export async function createContentGenerator( if ( config.authType === AuthType.LOGIN_WITH_GOOGLE || + config.authType === AuthType.LOGIN_WITH_GOOGLE_GCA || config.authType === AuthType.CLOUD_SHELL ) { const httpOptions = { headers: baseHeaders };