Simplify streaming code for code assist server (#4619)
This commit is contained in:
parent
f95674e646
commit
d7a57d85a3
|
@ -138,7 +138,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||||
getShowMemoryUsage: vi.fn(() => opts.showMemoryUsage ?? false),
|
getShowMemoryUsage: vi.fn(() => opts.showMemoryUsage ?? false),
|
||||||
getAccessibility: vi.fn(() => opts.accessibility ?? {}),
|
getAccessibility: vi.fn(() => opts.accessibility ?? {}),
|
||||||
getProjectRoot: vi.fn(() => opts.targetDir),
|
getProjectRoot: vi.fn(() => opts.targetDir),
|
||||||
getGeminiClient: vi.fn(() => ({})),
|
getGeminiClient: vi.fn(() => ({
|
||||||
|
getUserTier: vi.fn(),
|
||||||
|
})),
|
||||||
getCheckpointingEnabled: vi.fn(() => opts.checkpointing ?? true),
|
getCheckpointingEnabled: vi.fn(() => opts.checkpointing ?? true),
|
||||||
getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']),
|
getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']),
|
||||||
setFlashFallbackHandler: vi.fn(),
|
setFlashFallbackHandler: vi.fn(),
|
||||||
|
@ -639,6 +641,7 @@ describe('App UI', () => {
|
||||||
|
|
||||||
mockConfig.getGeminiClient.mockReturnValue({
|
mockConfig.getGeminiClient.mockReturnValue({
|
||||||
isInitialized: vi.fn(() => true),
|
isInitialized: vi.fn(() => true),
|
||||||
|
getUserTier: vi.fn(),
|
||||||
} as unknown as GeminiClient);
|
} as unknown as GeminiClient);
|
||||||
|
|
||||||
const { unmount, rerender } = render(
|
const { unmount, rerender } = render(
|
||||||
|
|
|
@ -206,26 +206,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||||
|
|
||||||
// Sync user tier from config when authentication changes
|
// Sync user tier from config when authentication changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const syncUserTier = async () => {
|
|
||||||
try {
|
|
||||||
const configUserTier = await config.getUserTier();
|
|
||||||
if (configUserTier !== userTier) {
|
|
||||||
setUserTier(configUserTier);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Silently fail - this is not critical functionality
|
|
||||||
// Only log in debug mode to avoid cluttering the console
|
|
||||||
if (config.getDebugMode()) {
|
|
||||||
console.debug('Failed to sync user tier:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only sync when not currently authenticating
|
// Only sync when not currently authenticating
|
||||||
if (!isAuthenticating) {
|
if (!isAuthenticating) {
|
||||||
syncUserTier();
|
setUserTier(config.getGeminiClient()?.getUserTier());
|
||||||
}
|
}
|
||||||
}, [config, userTier, isAuthenticating]);
|
}, [config, isAuthenticating]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isEditorDialogOpen,
|
isEditorDialogOpen,
|
||||||
|
|
|
@ -21,8 +21,14 @@ export async function createCodeAssistContentGenerator(
|
||||||
authType === AuthType.CLOUD_SHELL
|
authType === AuthType.CLOUD_SHELL
|
||||||
) {
|
) {
|
||||||
const authClient = await getOauthClient(authType, config);
|
const authClient = await getOauthClient(authType, config);
|
||||||
const projectId = await setupUser(authClient);
|
const userData = await setupUser(authClient);
|
||||||
return new CodeAssistServer(authClient, projectId, httpOptions, sessionId);
|
return new CodeAssistServer(
|
||||||
|
authClient,
|
||||||
|
userData.projectId,
|
||||||
|
httpOptions,
|
||||||
|
sessionId,
|
||||||
|
userData.userTier,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Unsupported authType: ${authType}`);
|
throw new Error(`Unsupported authType: ${authType}`);
|
||||||
|
|
|
@ -32,23 +32,6 @@ import {
|
||||||
toCountTokenRequest,
|
toCountTokenRequest,
|
||||||
toGenerateContentRequest,
|
toGenerateContentRequest,
|
||||||
} from './converter.js';
|
} from './converter.js';
|
||||||
import { Readable } from 'node:stream';
|
|
||||||
|
|
||||||
interface ErrorData {
|
|
||||||
error?: {
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GaxiosResponse {
|
|
||||||
status: number;
|
|
||||||
data: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StreamError extends Error {
|
|
||||||
status?: number;
|
|
||||||
response?: GaxiosResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** HTTP options to be used in each of the requests. */
|
/** HTTP options to be used in each of the requests. */
|
||||||
export interface HttpOptions {
|
export interface HttpOptions {
|
||||||
|
@ -60,13 +43,12 @@ export const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
|
||||||
export const CODE_ASSIST_API_VERSION = 'v1internal';
|
export const CODE_ASSIST_API_VERSION = 'v1internal';
|
||||||
|
|
||||||
export class CodeAssistServer implements ContentGenerator {
|
export class CodeAssistServer implements ContentGenerator {
|
||||||
private userTier: UserTierId | undefined = undefined;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly client: OAuth2Client,
|
readonly client: OAuth2Client,
|
||||||
readonly projectId?: string,
|
readonly projectId?: string,
|
||||||
readonly httpOptions: HttpOptions = {},
|
readonly httpOptions: HttpOptions = {},
|
||||||
readonly sessionId?: string,
|
readonly sessionId?: string,
|
||||||
|
readonly userTier?: UserTierId,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async generateContentStream(
|
async generateContentStream(
|
||||||
|
@ -196,45 +178,8 @@ export class CodeAssistServer implements ContentGenerator {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (async function* (): AsyncGenerator<T> {
|
return (async function* (): AsyncGenerator<T> {
|
||||||
// Convert ReadableStream to Node.js stream if needed
|
|
||||||
let nodeStream: NodeJS.ReadableStream;
|
|
||||||
|
|
||||||
if (res.data instanceof ReadableStream) {
|
|
||||||
// Convert Web ReadableStream to Node.js Readable stream
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
nodeStream = Readable.fromWeb(res.data as any);
|
|
||||||
} else if (
|
|
||||||
res.data &&
|
|
||||||
typeof (res.data as NodeJS.ReadableStream).on === 'function'
|
|
||||||
) {
|
|
||||||
// Already a Node.js stream
|
|
||||||
nodeStream = res.data as NodeJS.ReadableStream;
|
|
||||||
} else {
|
|
||||||
// If res.data is not a stream, it might be an error response
|
|
||||||
// Try to extract error information from the response
|
|
||||||
let errorMessage =
|
|
||||||
'Response data is not a readable stream. This may indicate a server error or quota issue.';
|
|
||||||
|
|
||||||
if (res.data && typeof res.data === 'object') {
|
|
||||||
// Check if this is an error response with error details
|
|
||||||
const errorData = res.data as ErrorData;
|
|
||||||
if (errorData.error?.message) {
|
|
||||||
errorMessage = errorData.error.message;
|
|
||||||
} else if (typeof errorData === 'string') {
|
|
||||||
errorMessage = errorData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an error that looks like a quota error if it contains quota information
|
|
||||||
const error: StreamError = new Error(errorMessage);
|
|
||||||
// Add status and response properties so it can be properly handled by retry logic
|
|
||||||
error.status = res.status;
|
|
||||||
error.response = res;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: nodeStream,
|
input: res.data as NodeJS.ReadableStream,
|
||||||
crlfDelay: Infinity, // Recognizes '\r\n' and '\n' as line breaks
|
crlfDelay: Infinity, // Recognizes '\r\n' and '\n' as line breaks
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -256,40 +201,6 @@ export class CodeAssistServer implements ContentGenerator {
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTier(): Promise<UserTierId | undefined> {
|
|
||||||
if (this.userTier === undefined) {
|
|
||||||
await this.detectUserTier();
|
|
||||||
}
|
|
||||||
return this.userTier;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async detectUserTier(): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Reset user tier when detection runs
|
|
||||||
this.userTier = undefined;
|
|
||||||
|
|
||||||
// Only attempt tier detection if we have a project ID
|
|
||||||
if (this.projectId) {
|
|
||||||
const loadRes = await this.loadCodeAssist({
|
|
||||||
cloudaicompanionProject: this.projectId,
|
|
||||||
metadata: {
|
|
||||||
ideType: 'IDE_UNSPECIFIED',
|
|
||||||
platform: 'PLATFORM_UNSPECIFIED',
|
|
||||||
pluginType: 'GEMINI',
|
|
||||||
duetProject: this.projectId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (loadRes.currentTier) {
|
|
||||||
this.userTier = loadRes.currentTier.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Silently fail - this is not critical functionality
|
|
||||||
// We'll default to FREE tier behavior if tier detection fails
|
|
||||||
console.debug('User tier detection failed:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getMethodUrl(method: string): string {
|
getMethodUrl(method: string): string {
|
||||||
const endpoint = process.env.CODE_ASSIST_ENDPOINT ?? CODE_ASSIST_ENDPOINT;
|
const endpoint = process.env.CODE_ASSIST_ENDPOINT ?? CODE_ASSIST_ENDPOINT;
|
||||||
return `${endpoint}/${CODE_ASSIST_API_VERSION}:${method}`;
|
return `${endpoint}/${CODE_ASSIST_API_VERSION}:${method}`;
|
||||||
|
|
|
@ -65,7 +65,10 @@ describe('setupUser', () => {
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
expect(projectId).toBe('server-project');
|
expect(projectId).toEqual({
|
||||||
|
projectId: 'server-project',
|
||||||
|
userTier: 'standard-tier',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw ProjectIdRequiredError when no project ID is available', async () => {
|
it('should throw ProjectIdRequiredError when no project ID is available', async () => {
|
||||||
|
|
|
@ -22,12 +22,17 @@ export class ProjectIdRequiredError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserData {
|
||||||
|
projectId: string;
|
||||||
|
userTier: UserTierId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param projectId the user's project id, if any
|
* @param projectId the user's project id, if any
|
||||||
* @returns the user's actual project id
|
* @returns the user's actual project id
|
||||||
*/
|
*/
|
||||||
export async function setupUser(client: OAuth2Client): Promise<string> {
|
export async function setupUser(client: OAuth2Client): Promise<UserData> {
|
||||||
let projectId = process.env.GOOGLE_CLOUD_PROJECT || undefined;
|
let projectId = process.env.GOOGLE_CLOUD_PROJECT || undefined;
|
||||||
const caServer = new CodeAssistServer(client, projectId);
|
const caServer = new CodeAssistServer(client, projectId);
|
||||||
|
|
||||||
|
@ -64,7 +69,10 @@ export async function setupUser(client: OAuth2Client): Promise<string> {
|
||||||
await new Promise((f) => setTimeout(f, 5000));
|
await new Promise((f) => setTimeout(f, 5000));
|
||||||
lroRes = await caServer.onboardUser(onboardReq);
|
lroRes = await caServer.onboardUser(onboardReq);
|
||||||
}
|
}
|
||||||
return lroRes.response?.cloudaicompanionProject?.id || '';
|
return {
|
||||||
|
projectId: lroRes.response?.cloudaicompanionProject?.id || '',
|
||||||
|
userTier: tier.id,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOnboardTier(res: LoadCodeAssistResponse): GeminiUserTier {
|
function getOnboardTier(res: LoadCodeAssistResponse): GeminiUserTier {
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
ContentGeneratorConfig,
|
ContentGeneratorConfig,
|
||||||
createContentGeneratorConfig,
|
createContentGeneratorConfig,
|
||||||
} from '../core/contentGenerator.js';
|
} from '../core/contentGenerator.js';
|
||||||
import { UserTierId } from '../code_assist/types.js';
|
|
||||||
import { ToolRegistry } from '../tools/tool-registry.js';
|
import { ToolRegistry } from '../tools/tool-registry.js';
|
||||||
import { LSTool } from '../tools/ls.js';
|
import { LSTool } from '../tools/ls.js';
|
||||||
import { ReadFileTool } from '../tools/read-file.js';
|
import { ReadFileTool } from '../tools/read-file.js';
|
||||||
|
@ -359,14 +358,6 @@ export class Config {
|
||||||
return this.quotaErrorOccurred;
|
return this.quotaErrorOccurred;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserTier(): Promise<UserTierId | undefined> {
|
|
||||||
if (!this.geminiClient) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const generator = this.geminiClient.getContentGenerator();
|
|
||||||
return await generator.getTier?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
getEmbeddingModel(): string {
|
getEmbeddingModel(): string {
|
||||||
return this.embeddingModel;
|
return this.embeddingModel;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
ChatCompressionInfo,
|
ChatCompressionInfo,
|
||||||
} from './turn.js';
|
} from './turn.js';
|
||||||
import { Config } from '../config/config.js';
|
import { Config } from '../config/config.js';
|
||||||
|
import { UserTierId } from '../code_assist/types.js';
|
||||||
import { getCoreSystemPrompt, getCompressionPrompt } from './prompts.js';
|
import { getCoreSystemPrompt, getCompressionPrompt } from './prompts.js';
|
||||||
import { ReadManyFilesTool } from '../tools/read-many-files.js';
|
import { ReadManyFilesTool } from '../tools/read-many-files.js';
|
||||||
import { getResponseText } from '../utils/generateContentResponseUtilities.js';
|
import { getResponseText } from '../utils/generateContentResponseUtilities.js';
|
||||||
|
@ -130,6 +131,10 @@ export class GeminiClient {
|
||||||
return this.contentGenerator;
|
return this.contentGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserTier(): UserTierId | undefined {
|
||||||
|
return this.contentGenerator?.userTier;
|
||||||
|
}
|
||||||
|
|
||||||
async addHistory(content: Content) {
|
async addHistory(content: Content) {
|
||||||
this.getChat().addHistory(content);
|
this.getChat().addHistory(content);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ export interface ContentGenerator {
|
||||||
|
|
||||||
embedContent(request: EmbedContentParameters): Promise<EmbedContentResponse>;
|
embedContent(request: EmbedContentParameters): Promise<EmbedContentResponse>;
|
||||||
|
|
||||||
getTier?(): Promise<UserTierId | undefined>;
|
userTier?: UserTierId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AuthType {
|
export enum AuthType {
|
||||||
|
|
|
@ -68,10 +68,6 @@ export function isProQuotaExceededError(error: unknown): boolean {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
if (gaxiosError.response && gaxiosError.response.data) {
|
if (gaxiosError.response && gaxiosError.response.data) {
|
||||||
console.log(
|
|
||||||
'[DEBUG] isProQuotaExceededError - checking response data:',
|
|
||||||
gaxiosError.response.data,
|
|
||||||
);
|
|
||||||
if (typeof gaxiosError.response.data === 'string') {
|
if (typeof gaxiosError.response.data === 'string') {
|
||||||
return checkMessage(gaxiosError.response.data);
|
return checkMessage(gaxiosError.response.data);
|
||||||
}
|
}
|
||||||
|
@ -87,11 +83,6 @@ export function isProQuotaExceededError(error: unknown): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[DEBUG] isProQuotaExceededError - no matching error format for:',
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue