mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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]',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user