Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0

This commit is contained in:
mingholy.lmh
2025-09-10 21:01:40 +08:00
583 changed files with 30160 additions and 10770 deletions

View File

@@ -4,11 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { AuthType, ContentGenerator } from '../core/contentGenerator.js';
import type { ContentGenerator } from '../core/contentGenerator.js';
import { AuthType } from '../core/contentGenerator.js';
import { getOauthClient } from './oauth2.js';
import { setupUser } from './setup.js';
import { CodeAssistServer, HttpOptions } from './server.js';
import { Config } from '../config/config.js';
import type { HttpOptions } from './server.js';
import { CodeAssistServer } from './server.js';
import type { Config } from '../config/config.js';
export async function createCodeAssistContentGenerator(
httpOptions: HttpOptions,

View File

@@ -5,18 +5,21 @@
*/
import { describe, it, expect } from 'vitest';
import type { CaGenerateContentResponse } from './converter.js';
import {
toGenerateContentRequest,
fromGenerateContentResponse,
CaGenerateContentResponse,
toContents,
} from './converter.js';
import {
import type {
ContentListUnion,
GenerateContentParameters,
} from '@google/genai';
import {
GenerateContentResponse,
FinishReason,
BlockedReason,
type Part,
} from '@google/genai';
describe('converter', () => {
@@ -349,5 +352,94 @@ describe('converter', () => {
{ role: 'user', parts: [{ text: 'string 2' }] },
]);
});
it('should convert thought parts to text parts for API compatibility', () => {
const contentWithThought: ContentListUnion = {
role: 'model',
parts: [
{ text: 'regular text' },
{ thought: 'thinking about the problem' } as Part & {
thought: string;
},
{ text: 'more text' },
],
};
expect(toContents(contentWithThought)).toEqual([
{
role: 'model',
parts: [
{ text: 'regular text' },
{ text: '[Thought: thinking about the problem]' },
{ text: 'more text' },
],
},
]);
});
it('should combine text and thought for text parts with thoughts', () => {
const contentWithTextAndThought: ContentListUnion = {
role: 'model',
parts: [
{
text: 'Here is my response',
thought: 'I need to be careful here',
} as Part & { thought: string },
],
};
expect(toContents(contentWithTextAndThought)).toEqual([
{
role: 'model',
parts: [
{
text: 'Here is my response\n[Thought: I need to be careful here]',
},
],
},
]);
});
it('should preserve non-thought properties while removing thought', () => {
const contentWithComplexPart: ContentListUnion = {
role: 'model',
parts: [
{
functionCall: { name: 'calculate', args: { x: 5, y: 10 } },
thought: 'Performing calculation',
} as Part & { thought: string },
],
};
expect(toContents(contentWithComplexPart)).toEqual([
{
role: 'model',
parts: [
{
functionCall: { name: 'calculate', args: { x: 5, y: 10 } },
},
],
},
]);
});
it('should convert invalid text content to valid text part with thought', () => {
const contentWithInvalidText: ContentListUnion = {
role: 'model',
parts: [
{
text: 123, // Invalid - should be string
thought: 'Processing number',
} as Part & { thought: string; text: number },
],
};
expect(toContents(contentWithInvalidText)).toEqual([
{
role: 'model',
parts: [
{
text: '123\n[Thought: Processing number]',
},
],
},
]);
});
});
});

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
import type {
Content,
ContentListUnion,
ContentUnion,
@@ -12,7 +12,6 @@ import {
GenerateContentParameters,
CountTokensParameters,
CountTokensResponse,
GenerateContentResponse,
GenerationConfigRoutingConfig,
MediaResolution,
Candidate,
@@ -27,6 +26,7 @@ import {
ToolListUnion,
ToolConfig,
} from '@google/genai';
import { GenerateContentResponse } from '@google/genai';
export interface CAGenerateContentRequest {
model: string;
@@ -189,13 +189,18 @@ function toContent(content: ContentUnion): Content {
};
}
if ('parts' in content) {
// it's a Content
return content;
// it's a Content - process parts to handle thought filtering
return {
...content,
parts: content.parts
? toParts(content.parts.filter((p) => p != null))
: [],
};
}
// it's a Part
return {
role: 'user',
parts: [content as Part],
parts: [toPart(content as Part)],
};
}
@@ -208,6 +213,41 @@ function toPart(part: PartUnion): Part {
// it's a string
return { text: part };
}
// Handle thought parts for CountToken API compatibility
// The CountToken API expects parts to have certain required "oneof" fields initialized,
// but thought parts don't conform to this schema and cause API failures
if ('thought' in part && part.thought) {
const thoughtText = `[Thought: ${part.thought}]`;
const newPart = { ...part };
delete (newPart as Record<string, unknown>)['thought'];
const hasApiContent =
'functionCall' in newPart ||
'functionResponse' in newPart ||
'inlineData' in newPart ||
'fileData' in newPart;
if (hasApiContent) {
// It's a functionCall or other non-text part. Just strip the thought.
return newPart;
}
// If no other valid API content, this must be a text part.
// Combine existing text (if any) with the thought, preserving other properties.
const text = (newPart as { text?: unknown }).text;
const existingText = text ? String(text) : '';
const combinedText = existingText
? `${existingText}\n${thoughtText}`
: thoughtText;
return {
...newPart,
text: combinedText,
};
}
return part;
}

View File

@@ -4,25 +4,33 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
import { Compute, OAuth2Client } from 'google-auth-library';
import crypto from 'node:crypto';
import * as fs from 'node:fs';
import http from 'node:http';
import * as os from 'node:os';
import * as path from 'node:path';
import readline from 'node:readline';
import open from 'open';
import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from 'vitest';
import type { Config } from '../config/config.js';
import { AuthType } from '../core/contentGenerator.js';
import { QWEN_DIR } from '../utils/paths.js';
import { UserAccountManager } from '../utils/userAccountManager.js';
import {
getOauthClient,
resetOauthClientForTesting,
clearCachedCredentialFile,
clearOauthClientCache,
getOauthClient,
resetOauthClientForTesting,
} from './oauth2.js';
import { getCachedGoogleAccount } from '../utils/user_account.js';
import { OAuth2Client, Compute } from 'google-auth-library';
import * as fs from 'fs';
import * as path from 'path';
import http from 'http';
import open from 'open';
import crypto from 'crypto';
import * as os from 'os';
import { AuthType } from '../core/contentGenerator.js';
import { Config } from '../config/config.js';
import readline from 'node:readline';
import { QWEN_DIR } from '../utils/paths.js';
vi.mock('os', async (importOriginal) => {
const os = await importOriginal<typeof import('os')>();
@@ -181,7 +189,10 @@ describe('oauth2', () => {
});
// Verify the getCachedGoogleAccount function works
expect(getCachedGoogleAccount()).toBe('test-google-account@gmail.com');
const userAccountManager = new UserAccountManager();
expect(userAccountManager.getCachedGoogleAccount()).toBe(
'test-google-account@gmail.com',
);
});
it('should perform login with user code', async () => {
@@ -516,6 +527,7 @@ describe('oauth2', () => {
expect(mockSetCredentials).toHaveBeenCalledWith(cachedCreds);
});
});
describe('clearCachedCredentialFile', () => {
it('should clear cached credentials and Google account', async () => {
const cachedCreds = { refresh_token: 'test-token' };
@@ -533,14 +545,17 @@ describe('oauth2', () => {
googleAccountPath,
JSON.stringify(accountData),
);
const userAccountManager = new UserAccountManager();
expect(fs.existsSync(credsPath)).toBe(true);
expect(fs.existsSync(googleAccountPath)).toBe(true);
expect(getCachedGoogleAccount()).toBe('test@example.com');
expect(userAccountManager.getCachedGoogleAccount()).toBe(
'test@example.com',
);
await clearCachedCredentialFile();
expect(fs.existsSync(credsPath)).toBe(false);
expect(getCachedGoogleAccount()).toBeNull();
expect(userAccountManager.getCachedGoogleAccount()).toBeNull();
const updatedAccountData = JSON.parse(
fs.readFileSync(googleAccountPath, 'utf-8'),
);

View File

@@ -4,30 +4,27 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Credentials } from 'google-auth-library';
import {
OAuth2Client,
Credentials,
Compute,
CodeChallengeMethod,
Compute,
OAuth2Client,
} from 'google-auth-library';
import * as http from 'http';
import url from 'url';
import crypto from 'crypto';
import * as net from 'net';
import open from 'open';
import path from 'node:path';
import crypto from 'node:crypto';
import { promises as fs } from 'node:fs';
import * as os from 'os';
import { Config } from '../config/config.js';
import { getErrorMessage } from '../utils/errors.js';
import {
cacheGoogleAccount,
getCachedGoogleAccount,
clearCachedGoogleAccount,
} from '../utils/user_account.js';
import { AuthType } from '../core/contentGenerator.js';
import * as http from 'node:http';
import * as net from 'node:net';
import path from 'node:path';
import readline from 'node:readline';
import { QWEN_DIR } from '../utils/paths.js';
import url from 'node:url';
import open from 'open';
import type { Config } from '../config/config.js';
import { Storage } from '../config/storage.js';
import { AuthType } from '../core/contentGenerator.js';
import { FatalAuthenticationError, getErrorMessage } from '../utils/errors.js';
import { UserAccountManager } from '../utils/userAccountManager.js';
const userAccountManager = new UserAccountManager();
// OAuth Client ID used to initiate OAuth2Client class.
const OAUTH_CLIENT_ID =
@@ -54,8 +51,6 @@ const SIGN_IN_SUCCESS_URL =
const SIGN_IN_FAILURE_URL =
'https://developers.google.com/gemini-code-assist/auth_failure_gemini';
const CREDENTIAL_FILENAME = 'oauth_creds.json';
/**
* An Authentication URL for updating the credentials of a Oauth2Client
* as well as a promise that will resolve when the credentials have
@@ -99,7 +94,7 @@ async function initOauthClient(
if (await loadCachedCredentials(client)) {
// Found valid cached credentials.
// Check if we need to retrieve Google Account ID or Email
if (!getCachedGoogleAccount()) {
if (!userAccountManager.getCachedGoogleAccount()) {
try {
await fetchAndCacheUserInfo(client);
} catch {
@@ -147,7 +142,9 @@ async function initOauthClient(
}
}
if (!success) {
process.exit(1);
throw new FatalAuthenticationError(
'Failed to authenticate with user code.',
);
}
} else {
const webLogin = await authWithWeb(client);
@@ -171,7 +168,7 @@ async function initOauthClient(
console.error(
'Failed to open browser automatically. Please try running again with NO_BROWSER=true set.',
);
process.exit(1);
throw new FatalAuthenticationError('Failed to open browser.');
});
} catch (err) {
console.error(
@@ -179,7 +176,7 @@ async function initOauthClient(
err,
'\nPlease try running again with NO_BROWSER=true set.',
);
process.exit(1);
throw new FatalAuthenticationError('Failed to open browser.');
}
console.log('Waiting for authentication...');
@@ -352,7 +349,7 @@ export function getAvailablePort(): Promise<number> {
async function loadCachedCredentials(client: OAuth2Client): Promise<boolean> {
const pathsToTry = [
getCachedCredentialPath(),
Storage.getOAuthCredsPath(),
process.env['GOOGLE_APPLICATION_CREDENTIALS'],
].filter((p): p is string => !!p);
@@ -380,26 +377,22 @@ async function loadCachedCredentials(client: OAuth2Client): Promise<boolean> {
}
async function cacheCredentials(credentials: Credentials) {
const filePath = getCachedCredentialPath();
const filePath = Storage.getOAuthCredsPath();
await fs.mkdir(path.dirname(filePath), { recursive: true });
const credString = JSON.stringify(credentials, null, 2);
await fs.writeFile(filePath, credString, { mode: 0o600 });
}
function getCachedCredentialPath(): string {
return path.join(os.homedir(), QWEN_DIR, CREDENTIAL_FILENAME);
}
export function clearOauthClientCache() {
oauthClientPromises.clear();
}
export async function clearCachedCredentialFile() {
try {
await fs.rm(getCachedCredentialPath(), { force: true });
await fs.rm(Storage.getOAuthCredsPath(), { force: true });
// Clear the Google Account ID cache when credentials are cleared
await clearCachedGoogleAccount();
await userAccountManager.clearCachedGoogleAccount();
// Clear the in-memory OAuth client cache to force re-authentication
clearOauthClientCache();
} catch (e) {
@@ -433,9 +426,7 @@ async function fetchAndCacheUserInfo(client: OAuth2Client): Promise<void> {
}
const userInfo = await response.json();
if (userInfo.email) {
await cacheGoogleAccount(userInfo.email);
}
await userAccountManager.cacheGoogleAccount(userInfo.email);
} catch (error) {
console.error('Error retrieving user info:', error);
}

View File

@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { OAuth2Client } from 'google-auth-library';
import {
import type { OAuth2Client } from 'google-auth-library';
import type {
CodeAssistGlobalUserSettingResponse,
LoadCodeAssistRequest,
LoadCodeAssistResponse,
@@ -13,7 +13,7 @@ import {
OnboardUserRequest,
SetCodeAssistGlobalUserSettingRequest,
} from './types.js';
import {
import type {
CountTokensParameters,
CountTokensResponse,
EmbedContentParameters,
@@ -21,12 +21,14 @@ import {
GenerateContentParameters,
GenerateContentResponse,
} from '@google/genai';
import * as readline from 'readline';
import { ContentGenerator } from '../core/contentGenerator.js';
import { UserTierId } from './types.js';
import {
import * as readline from 'node:readline';
import type { ContentGenerator } from '../core/contentGenerator.js';
import type { UserTierId } from './types.js';
import type {
CaCountTokenResponse,
CaGenerateContentResponse,
} from './converter.js';
import {
fromCountTokenResponse,
fromGenerateContentResponse,
toCountTokenRequest,

View File

@@ -7,8 +7,9 @@
import { describe, it, expect, vi, beforeEach, afterEach } 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';
import type { OAuth2Client } from 'google-auth-library';
import type { GeminiUserTier } from './types.js';
import { UserTierId } from './types.js';
vi.mock('../code_assist/server.js');

View File

@@ -4,15 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
import type {
ClientMetadata,
GeminiUserTier,
LoadCodeAssistResponse,
OnboardUserRequest,
UserTierId,
} from './types.js';
import { UserTierId } from './types.js';
import { CodeAssistServer } from './server.js';
import { OAuth2Client } from 'google-auth-library';
import type { OAuth2Client } from 'google-auth-library';
export class ProjectIdRequiredError extends Error {
constructor() {