mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: add explicit license selection and status visibility (#6751)
This commit is contained in:
@@ -18,10 +18,11 @@ export async function createCodeAssistContentGenerator(
|
||||
): Promise<ContentGenerator> {
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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=<your-project-id>\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<UserData> {
|
||||
const projectId = process.env['GOOGLE_CLOUD_PROJECT'] || undefined;
|
||||
export async function setupUser(
|
||||
client: OAuth2Client,
|
||||
authType: AuthType,
|
||||
): Promise<UserData> {
|
||||
// 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<UserData> {
|
||||
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<UserData> {
|
||||
|
||||
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<UserData> {
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user