feat(cli): add explicit proxy option in cli (#2526)
Co-authored-by: Dcatfly <dcatfly@gmail.com>
This commit is contained in:
parent
4ca471bac6
commit
606a7702de
|
@ -370,6 +370,9 @@ Arguments passed directly when running the CLI can override other configurations
|
||||||
- Example: `gemini -e my-extension -e my-other-extension`
|
- Example: `gemini -e my-extension -e my-other-extension`
|
||||||
- **`--list-extensions`** (**`-l`**):
|
- **`--list-extensions`** (**`-l`**):
|
||||||
- Lists all available extensions and exits.
|
- Lists all available extensions and exits.
|
||||||
|
- **`--proxy`**:
|
||||||
|
- Sets the proxy for the CLI.
|
||||||
|
- Example: `--proxy http://localhost:7890`.
|
||||||
- **`--version`**:
|
- **`--version`**:
|
||||||
- Displays the version of the CLI.
|
- Displays the version of the CLI.
|
||||||
|
|
||||||
|
|
|
@ -11940,6 +11940,7 @@
|
||||||
"glob": "^10.4.5",
|
"glob": "^10.4.5",
|
||||||
"google-auth-library": "^9.11.0",
|
"google-auth-library": "^9.11.0",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.0",
|
||||||
"micromatch": "^4.0.8",
|
"micromatch": "^4.0.8",
|
||||||
"open": "^10.1.2",
|
"open": "^10.1.2",
|
||||||
|
|
|
@ -187,6 +187,73 @@ describe('loadCliConfig', () => {
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getShowMemoryUsage()).toBe(true);
|
expect(config.getShowMemoryUsage()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`should leave proxy to empty by default`, async () => {
|
||||||
|
process.argv = ['node', 'script.js'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const settings: Settings = {};
|
||||||
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
|
expect(config.getProxy()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const proxy_url = 'http://localhost:7890';
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
env_name: 'https_proxy',
|
||||||
|
proxy_url,
|
||||||
|
},
|
||||||
|
expected: proxy_url,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
env_name: 'http_proxy',
|
||||||
|
proxy_url,
|
||||||
|
},
|
||||||
|
expected: proxy_url,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
env_name: 'HTTPS_PROXY',
|
||||||
|
proxy_url,
|
||||||
|
},
|
||||||
|
expected: proxy_url,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
env_name: 'HTTP_PROXY',
|
||||||
|
proxy_url,
|
||||||
|
},
|
||||||
|
expected: proxy_url,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
testCases.forEach(({ input, expected }) => {
|
||||||
|
it(`should set proxy to ${expected} according to environment variable [${input.env_name}]`, async () => {
|
||||||
|
process.env[input.env_name] = input.proxy_url;
|
||||||
|
process.argv = ['node', 'script.js'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const settings: Settings = {};
|
||||||
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
|
expect(config.getProxy()).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set proxy when --proxy flag is present', async () => {
|
||||||
|
process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const settings: Settings = {};
|
||||||
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
|
expect(config.getProxy()).toBe('http://localhost:7890');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prioritize CLI flag over environment variable for proxy (CLI http://localhost:7890, environment variable http://localhost:7891)', async () => {
|
||||||
|
process.env['http_proxy'] = 'http://localhost:7891';
|
||||||
|
process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const settings: Settings = {};
|
||||||
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
|
expect(config.getProxy()).toBe('http://localhost:7890');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loadCliConfig telemetry', () => {
|
describe('loadCliConfig telemetry', () => {
|
||||||
|
|
|
@ -57,6 +57,7 @@ export interface CliArgs {
|
||||||
extensions: string[] | undefined;
|
extensions: string[] | undefined;
|
||||||
listExtensions: boolean | undefined;
|
listExtensions: boolean | undefined;
|
||||||
ideMode: boolean | undefined;
|
ideMode: boolean | undefined;
|
||||||
|
proxy: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseArguments(): Promise<CliArgs> {
|
export async function parseArguments(): Promise<CliArgs> {
|
||||||
|
@ -182,7 +183,11 @@ export async function parseArguments(): Promise<CliArgs> {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Run in IDE mode?',
|
description: 'Run in IDE mode?',
|
||||||
})
|
})
|
||||||
|
.option('proxy', {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Proxy for gemini client, like schema://user:password@host:port',
|
||||||
|
})
|
||||||
.version(await getCliVersion()) // This will enable the --version flag based on package.json
|
.version(await getCliVersion()) // This will enable the --version flag based on package.json
|
||||||
.alias('v', 'version')
|
.alias('v', 'version')
|
||||||
.help()
|
.help()
|
||||||
|
@ -380,6 +385,7 @@ export async function loadCliConfig(
|
||||||
},
|
},
|
||||||
checkpointing: argv.checkpointing || settings.checkpointing?.enabled,
|
checkpointing: argv.checkpointing || settings.checkpointing?.enabled,
|
||||||
proxy:
|
proxy:
|
||||||
|
argv.proxy ||
|
||||||
process.env.HTTPS_PROXY ||
|
process.env.HTTPS_PROXY ||
|
||||||
process.env.https_proxy ||
|
process.env.https_proxy ||
|
||||||
process.env.HTTP_PROXY ||
|
process.env.HTTP_PROXY ||
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
"glob": "^10.4.5",
|
"glob": "^10.4.5",
|
||||||
"google-auth-library": "^9.11.0",
|
"google-auth-library": "^9.11.0",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.0",
|
||||||
"micromatch": "^4.0.8",
|
"micromatch": "^4.0.8",
|
||||||
"open": "^10.1.2",
|
"open": "^10.1.2",
|
||||||
|
|
|
@ -34,6 +34,7 @@ vi.mock('node:readline');
|
||||||
|
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
getNoBrowser: () => false,
|
getNoBrowser: () => false,
|
||||||
|
getProxy: () => 'http://test.proxy.com:8080',
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
// Mock fetch globally
|
// Mock fetch globally
|
||||||
|
@ -175,6 +176,7 @@ describe('oauth2', () => {
|
||||||
it('should perform login with user code', async () => {
|
it('should perform login with user code', async () => {
|
||||||
const mockConfigWithNoBrowser = {
|
const mockConfigWithNoBrowser = {
|
||||||
getNoBrowser: () => true,
|
getNoBrowser: () => true,
|
||||||
|
getProxy: () => 'http://test.proxy.com:8080',
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
const mockCodeVerifier = {
|
const mockCodeVerifier = {
|
||||||
|
|
|
@ -73,6 +73,9 @@ export async function getOauthClient(
|
||||||
const client = new OAuth2Client({
|
const client = new OAuth2Client({
|
||||||
clientId: OAUTH_CLIENT_ID,
|
clientId: OAUTH_CLIENT_ID,
|
||||||
clientSecret: OAUTH_CLIENT_SECRET,
|
clientSecret: OAUTH_CLIENT_SECRET,
|
||||||
|
transporterOptions: {
|
||||||
|
proxy: config.getProxy(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('tokens', async (tokens: Credentials) => {
|
client.on('tokens', async (tokens: Credentials) => {
|
||||||
|
|
|
@ -68,6 +68,7 @@ describe('createContentGeneratorConfig', () => {
|
||||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||||
setModel: vi.fn(),
|
setModel: vi.fn(),
|
||||||
flashFallbackHandler: vi.fn(),
|
flashFallbackHandler: vi.fn(),
|
||||||
|
getProxy: vi.fn(),
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -50,6 +50,7 @@ export type ContentGeneratorConfig = {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
vertexai?: boolean;
|
vertexai?: boolean;
|
||||||
authType?: AuthType | undefined;
|
authType?: AuthType | undefined;
|
||||||
|
proxy?: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createContentGeneratorConfig(
|
export function createContentGeneratorConfig(
|
||||||
|
@ -67,6 +68,7 @@ export function createContentGeneratorConfig(
|
||||||
const contentGeneratorConfig: ContentGeneratorConfig = {
|
const contentGeneratorConfig: ContentGeneratorConfig = {
|
||||||
model: effectiveModel,
|
model: effectiveModel,
|
||||||
authType,
|
authType,
|
||||||
|
proxy: config?.getProxy(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now
|
// If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now
|
||||||
|
@ -83,11 +85,8 @@ export function createContentGeneratorConfig(
|
||||||
getEffectiveModel(
|
getEffectiveModel(
|
||||||
contentGeneratorConfig.apiKey,
|
contentGeneratorConfig.apiKey,
|
||||||
contentGeneratorConfig.model,
|
contentGeneratorConfig.model,
|
||||||
).then((newModel) => {
|
contentGeneratorConfig.proxy,
|
||||||
if (newModel !== contentGeneratorConfig.model) {
|
);
|
||||||
config.flashFallbackHandler?.(contentGeneratorConfig.model, newModel);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return contentGeneratorConfig;
|
return contentGeneratorConfig;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { setGlobalDispatcher, ProxyAgent } from 'undici';
|
||||||
import {
|
import {
|
||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
DEFAULT_GEMINI_FLASH_MODEL,
|
DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
|
@ -20,6 +21,7 @@ import {
|
||||||
export async function getEffectiveModel(
|
export async function getEffectiveModel(
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
currentConfiguredModel: string,
|
currentConfiguredModel: string,
|
||||||
|
proxy?: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (currentConfiguredModel !== DEFAULT_GEMINI_MODEL) {
|
if (currentConfiguredModel !== DEFAULT_GEMINI_MODEL) {
|
||||||
// Only check if the user is trying to use the specific pro model we want to fallback from.
|
// Only check if the user is trying to use the specific pro model we want to fallback from.
|
||||||
|
@ -43,6 +45,9 @@ export async function getEffectiveModel(
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 2000); // 500ms timeout for the request
|
const timeoutId = setTimeout(() => controller.abort(), 2000); // 500ms timeout for the request
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (proxy) {
|
||||||
|
setGlobalDispatcher(new ProxyAgent(proxy));
|
||||||
|
}
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
StartSessionEvent,
|
StartSessionEvent,
|
||||||
EndSessionEvent,
|
EndSessionEvent,
|
||||||
|
@ -132,12 +134,18 @@ export class ClearcutLogger {
|
||||||
headers: { 'Content-Length': Buffer.byteLength(body) },
|
headers: { 'Content-Length': Buffer.byteLength(body) },
|
||||||
};
|
};
|
||||||
const bufs: Buffer[] = [];
|
const bufs: Buffer[] = [];
|
||||||
const req = https.request(options, (res) => {
|
const req = https.request(
|
||||||
res.on('data', (buf) => bufs.push(buf));
|
{
|
||||||
res.on('end', () => {
|
...options,
|
||||||
resolve(Buffer.concat(bufs));
|
agent: this.getProxyAgent(),
|
||||||
});
|
},
|
||||||
});
|
(res) => {
|
||||||
|
res.on('data', (buf) => bufs.push(buf));
|
||||||
|
res.on('end', () => {
|
||||||
|
resolve(Buffer.concat(bufs));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
req.on('error', (e) => {
|
req.on('error', (e) => {
|
||||||
if (this.config?.getDebugMode()) {
|
if (this.config?.getDebugMode()) {
|
||||||
console.log('Clearcut POST request error: ', e);
|
console.log('Clearcut POST request error: ', e);
|
||||||
|
@ -499,6 +507,18 @@ export class ClearcutLogger {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProxyAgent() {
|
||||||
|
const proxyUrl = this.config?.getProxy();
|
||||||
|
if (!proxyUrl) return undefined;
|
||||||
|
// undici which is widely used in the repo can only support http & https proxy protocol,
|
||||||
|
// https://github.com/nodejs/undici/issues/2224
|
||||||
|
if (proxyUrl.startsWith('http')) {
|
||||||
|
return new HttpsProxyAgent(proxyUrl);
|
||||||
|
} else {
|
||||||
|
throw new Error('Unsupported proxy type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
shutdown() {
|
shutdown() {
|
||||||
const event = new EndSessionEvent(this.config);
|
const event = new EndSessionEvent(this.config);
|
||||||
this.logEndSessionEvent(event);
|
this.logEndSessionEvent(event);
|
||||||
|
|
|
@ -13,6 +13,7 @@ describe('WebFetchTool', () => {
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
getApprovalMode: vi.fn(),
|
getApprovalMode: vi.fn(),
|
||||||
setApprovalMode: vi.fn(),
|
setApprovalMode: vi.fn(),
|
||||||
|
getProxy: vi.fn(),
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
describe('shouldConfirmExecute', () => {
|
describe('shouldConfirmExecute', () => {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { Config, ApprovalMode } from '../config/config.js';
|
||||||
import { getResponseText } from '../utils/generateContentResponseUtilities.js';
|
import { getResponseText } from '../utils/generateContentResponseUtilities.js';
|
||||||
import { fetchWithTimeout, isPrivateIp } from '../utils/fetch.js';
|
import { fetchWithTimeout, isPrivateIp } from '../utils/fetch.js';
|
||||||
import { convert } from 'html-to-text';
|
import { convert } from 'html-to-text';
|
||||||
|
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||||
|
|
||||||
const URL_FETCH_TIMEOUT_MS = 10000;
|
const URL_FETCH_TIMEOUT_MS = 10000;
|
||||||
const MAX_CONTENT_LENGTH = 100000;
|
const MAX_CONTENT_LENGTH = 100000;
|
||||||
|
@ -81,6 +82,10 @@ export class WebFetchTool extends BaseTool<WebFetchToolParams, ToolResult> {
|
||||||
type: Type.OBJECT,
|
type: Type.OBJECT,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const proxy = config.getProxy();
|
||||||
|
if (proxy) {
|
||||||
|
setGlobalDispatcher(new ProxyAgent(proxy as string));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async executeFallback(
|
private async executeFallback(
|
||||||
|
|
Loading…
Reference in New Issue