Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,13 @@ export class Config {
}

if (this.getProxy()) {
setGlobalDispatcher(new ProxyAgent(this.getProxy() as string));
const noProxy = process.env['NO_PROXY'] || process.env['no_proxy'];
setGlobalDispatcher(
new ProxyAgent({
uri: this.getProxy() as string,
...(noProxy ? { noProxy } : {}),
}),
);
}
this.geminiClient = new GeminiClient(this);
this.chatRecordingService = this.chatRecordingEnabled
Expand Down
122 changes: 121 additions & 1 deletion packages/core/src/utils/runtimeFetchOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { buildRuntimeFetchOptions } from './runtimeFetchOptions.js';

type UndiciOptions = Record<string, unknown>;
Expand Down Expand Up @@ -92,4 +92,124 @@ describe('buildRuntimeFetchOptions (node runtime)', () => {
bodyTimeout: 0,
});
});

describe('NO_PROXY handling', () => {
const originalEnv: { [key: string]: string | undefined } = {};

beforeEach(() => {
// Save original environment variables
originalEnv['NO_PROXY'] = process.env['NO_PROXY'];
originalEnv['no_proxy'] = process.env['no_proxy'];
// Clear environment variables before each test
delete process.env['NO_PROXY'];
delete process.env['no_proxy'];
});

afterEach(() => {
// Restore original environment variables
if (originalEnv['NO_PROXY'] !== undefined) {
process.env['NO_PROXY'] = originalEnv['NO_PROXY'];
} else {
delete process.env['NO_PROXY'];
}
if (originalEnv['no_proxy'] !== undefined) {
process.env['no_proxy'] = originalEnv['no_proxy'];
} else {
delete process.env['no_proxy'];
}
});

it('should pass noProxy option when NO_PROXY environment variable is set', () => {
process.env['NO_PROXY'] = 'localhost,127.0.0.1,internal.local';

const result = buildRuntimeFetchOptions('openai', 'http://proxy.local');

expect(result).toBeDefined();
const dispatcher = (
result as {
fetchOptions?: { dispatcher?: { options?: UndiciOptions } };
}
).fetchOptions?.dispatcher;
expect(dispatcher?.options).toMatchObject({
uri: 'http://proxy.local',
headersTimeout: 0,
bodyTimeout: 0,
noProxy: 'localhost,127.0.0.1,internal.local',
});
});

it('should pass noProxy option when no_proxy (lowercase) environment variable is set', () => {
process.env['no_proxy'] = 'api.local,*.internal';

const result = buildRuntimeFetchOptions('openai', 'http://proxy.local');

expect(result).toBeDefined();
const dispatcher = (
result as {
fetchOptions?: { dispatcher?: { options?: UndiciOptions } };
}
).fetchOptions?.dispatcher;
expect(dispatcher?.options).toMatchObject({
uri: 'http://proxy.local',
headersTimeout: 0,
bodyTimeout: 0,
noProxy: 'api.local,*.internal',
});
});

it('should use NO_PROXY or no_proxy when either is set', () => {
// On Windows, environment variables are case-insensitive, so we can only
// reliably test that one of them is used when both are set
process.env['NO_PROXY'] = 'priority.local';

const result = buildRuntimeFetchOptions('openai', 'http://proxy.local');

expect(result).toBeDefined();
const dispatcher = (
result as {
fetchOptions?: { dispatcher?: { options?: UndiciOptions } };
}
).fetchOptions?.dispatcher;
expect(dispatcher?.options).toMatchObject({
noProxy: 'priority.local',
});
});

it('should not pass noProxy option when neither NO_PROXY nor no_proxy is set', () => {
const result = buildRuntimeFetchOptions('openai', 'http://proxy.local');

expect(result).toBeDefined();
const dispatcher = (
result as {
fetchOptions?: { dispatcher?: { options?: UndiciOptions } };
}
).fetchOptions?.dispatcher;
expect(dispatcher?.options).toMatchObject({
uri: 'http://proxy.local',
headersTimeout: 0,
bodyTimeout: 0,
});
expect(dispatcher?.options).not.toHaveProperty('noProxy');
});

it('should handle NO_PROXY with wildcard (*)', () => {
process.env['NO_PROXY'] = '*';

const result = buildRuntimeFetchOptions(
'anthropic',
'http://proxy.local',
);

expect(result).toBeDefined();
const dispatcher = (
result as {
fetchOptions?: { dispatcher?: { options?: UndiciOptions } };
}
).fetchOptions?.dispatcher;
expect(dispatcher?.options).toMatchObject({
uri: 'http://proxy.local',
noProxy: '*',
});
});
});
});
2 changes: 2 additions & 0 deletions packages/core/src/utils/runtimeFetchOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,13 @@ function buildFetchOptionsWithDispatcher(
proxyUrl?: string,
): OpenAIRuntimeFetchOptions | AnthropicRuntimeFetchOptions {
try {
const noProxy = process.env['NO_PROXY'] || process.env['no_proxy'];
const dispatcher = proxyUrl
? new ProxyAgent({
uri: proxyUrl,
headersTimeout: 0,
bodyTimeout: 0,
...(noProxy ? { noProxy } : {}),
})
: new Agent({
headersTimeout: 0,
Expand Down