diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 44570203c3..80a955cb19 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -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 diff --git a/packages/core/src/utils/runtimeFetchOptions.test.ts b/packages/core/src/utils/runtimeFetchOptions.test.ts index fd4e7a0891..f58d27821b 100644 --- a/packages/core/src/utils/runtimeFetchOptions.test.ts +++ b/packages/core/src/utils/runtimeFetchOptions.test.ts @@ -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; @@ -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: '*', + }); + }); + }); }); diff --git a/packages/core/src/utils/runtimeFetchOptions.ts b/packages/core/src/utils/runtimeFetchOptions.ts index 1e0ef48068..e243052b88 100644 --- a/packages/core/src/utils/runtimeFetchOptions.ts +++ b/packages/core/src/utils/runtimeFetchOptions.ts @@ -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,