diff --git a/docs/users/support/troubleshooting.md b/docs/users/support/troubleshooting.md index bcaa97df147..c4ffec4dabb 100644 --- a/docs/users/support/troubleshooting.md +++ b/docs/users/support/troubleshooting.md @@ -27,6 +27,10 @@ This guide provides solutions to common issues and debugging tips, including top - If you are behind a proxy, set it via `qwen --proxy ` (or the `proxy` setting in `settings.json`). - If your network uses a corporate TLS inspection CA, set `NODE_EXTRA_CA_CERTS` as described above. +- **Self-signed model endpoint: `[API Error: Connection error. (cause: fetch failed)]`** + - **Cause:** A self-signed or otherwise untrusted TLS certificate on the model server (common in dev / lab / homelab setups). Setting `NODE_TLS_REJECT_UNAUTHORIZED=0` alone does **not** fix this for `fetch`-based code paths because Node's bundled HTTP client (`undici`) ignores that env var by design. + - **Solution:** Pass `--insecure` on the command line, set `QWEN_TLS_INSECURE=1`, or set `NODE_TLS_REJECT_UNAUTHORIZED=0` (Qwen Code now also honors the latter for parity with Claude Code / Node's legacy http stack). All three configure undici with `rejectUnauthorized: false` for outbound model API and MCP traffic. Only use this on networks you trust — it disables certificate verification for the entire process. + - **Issue: Unable to display UI after authentication failure** - **Cause:** If authentication fails after selecting an authentication type, the `security.auth.selectedType` setting may be persisted in `settings.json`. On restart, the CLI may get stuck trying to authenticate with the failed auth type and fail to display the UI. - **Solution:** Clear the `security.auth.selectedType` configuration item in your `settings.json` file: diff --git a/packages/cli/src/commands/auth/handler.ts b/packages/cli/src/commands/auth/handler.ts index 25a7d44fabc..7d2399ee065 100644 --- a/packages/cli/src/commands/auth/handler.ts +++ b/packages/cli/src/commands/auth/handler.ts @@ -90,6 +90,7 @@ export async function handleQwenAuth( openaiBaseUrl: undefined, openaiLoggingDir: undefined, proxy: undefined, + insecure: undefined, includeDirectories: undefined, screenReader: undefined, inputFormat: undefined, diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 4c18efcc3d5..1e60640acc3 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -800,6 +800,101 @@ describe('loadCliConfig', () => { expect(config.getProxy()).toBe('http://localhost:7890'); }); }); + + describe('Insecure / TLS-skip configuration (#3535)', () => { + const insecureEnvVars = [ + 'QWEN_TLS_INSECURE', + 'NODE_TLS_REJECT_UNAUTHORIZED', + ]; + const original: { [key: string]: string | undefined } = {}; + + beforeEach(() => { + for (const key of insecureEnvVars) { + original[key] = process.env[key]; + delete process.env[key]; + } + }); + + afterEach(() => { + for (const key of insecureEnvVars) { + if (original[key] !== undefined) { + process.env[key] = original[key]; + } else { + delete process.env[key]; + } + } + }); + + it('defaults to false', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, argv); + expect(config.getInsecure()).toBe(false); + }); + + it('honors --insecure flag', async () => { + process.argv = ['node', 'script.js', '--insecure']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, argv); + expect(config.getInsecure()).toBe(true); + }); + + it('honors QWEN_TLS_INSECURE=1', async () => { + vi.stubEnv('QWEN_TLS_INSECURE', '1'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, argv); + expect(config.getInsecure()).toBe(true); + }); + + it('honors NODE_TLS_REJECT_UNAUTHORIZED=0 (matches Claude Code/Node convention)', async () => { + vi.stubEnv('NODE_TLS_REJECT_UNAUTHORIZED', '0'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, argv); + expect(config.getInsecure()).toBe(true); + }); + + it('ignores QWEN_TLS_INSECURE=0 / arbitrary values', async () => { + vi.stubEnv('QWEN_TLS_INSECURE', '0'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, argv); + expect(config.getInsecure()).toBe(false); + }); + + it('ignores NODE_TLS_REJECT_UNAUTHORIZED=1', async () => { + vi.stubEnv('NODE_TLS_REJECT_UNAUTHORIZED', '1'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, argv); + expect(config.getInsecure()).toBe(false); + }); + + it('--insecure overrides explicit env=0', async () => { + vi.stubEnv('QWEN_TLS_INSECURE', '0'); + process.argv = ['node', 'script.js', '--insecure']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, argv); + expect(config.getInsecure()).toBe(true); + }); + + it('--no-insecure forces verification on even with QWEN_TLS_INSECURE=1', async () => { + vi.stubEnv('QWEN_TLS_INSECURE', '1'); + process.argv = ['node', 'script.js', '--no-insecure']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, argv); + expect(config.getInsecure()).toBe(false); + }); + + it('--no-insecure overrides NODE_TLS_REJECT_UNAUTHORIZED=0', async () => { + vi.stubEnv('NODE_TLS_REJECT_UNAUTHORIZED', '0'); + process.argv = ['node', 'script.js', '--no-insecure']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, argv); + expect(config.getInsecure()).toBe(false); + }); + }); }); describe('loadCliConfig telemetry', () => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 304f878ac98..8e6fa86d3bb 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -136,6 +136,7 @@ export interface CliArgs { openaiBaseUrl: string | undefined; openaiLoggingDir: string | undefined; proxy: string | undefined; + insecure: boolean | undefined; includeDirectories: string[] | undefined; screenReader: boolean | undefined; inputFormat?: string | undefined; @@ -163,6 +164,35 @@ export interface CliArgs { inputFile?: string | undefined; } +/** + * Resolve the effective TLS-insecure setting from (in order): + * 1. ``--insecure`` / ``--no-insecure`` CLI flag (any explicit value wins), + * 2. ``QWEN_TLS_INSECURE`` env var (truthy: ``1``, ``true``, ``yes``, + * case-insensitive), + * 3. ``NODE_TLS_REJECT_UNAUTHORIZED=0`` for parity with Node's legacy + * ``http`` stack and Claude Code -- undici (used by ``fetch``) + * otherwise ignores this env var, leaving users surprised that the + * flag they set "for everything" silently does nothing here (#3535). + * + * ``cliFlag`` is tri-state: ``undefined`` means the user passed neither + * ``--insecure`` nor ``--no-insecure``, in which case we fall through to + * the env-var checks. An explicit ``false`` (``--no-insecure``) forces + * verification on regardless of env, since the CLI is documented to win. + */ +function resolveInsecureFlag(cliFlag: boolean | undefined): boolean { + if (cliFlag !== undefined) { + return cliFlag; + } + const qwenEnv = process.env['QWEN_TLS_INSECURE']?.trim().toLowerCase(); + if (qwenEnv === '1' || qwenEnv === 'true' || qwenEnv === 'yes') { + return true; + } + if (process.env['NODE_TLS_REJECT_UNAUTHORIZED']?.trim() === '0') { + return true; + } + return false; +} + function normalizeOutputFormat( format: string | OutputFormat | undefined, ): OutputFormat | undefined { @@ -272,6 +302,19 @@ export async function parseArguments(): Promise { 'proxy', 'Use the "proxy" setting in settings.json instead. This flag will be removed in a future version.', ) + .option('insecure', { + type: 'boolean', + description: + 'Skip TLS certificate verification for outbound HTTPS requests. ' + + 'Use for self-signed dev/lab endpoints. ' + + 'Pass --no-insecure to force verification on, overriding env vars. ' + + 'Equivalent env vars (lower precedence): ' + + 'QWEN_TLS_INSECURE=1 or NODE_TLS_REJECT_UNAUTHORIZED=0.', + // No default so yargs reports ``undefined`` when neither --insecure + // nor --no-insecure is passed. That preserves three distinct states + // (true / false / undefined), which lets the resolver below treat + // an explicit ``--no-insecure`` as the highest-precedence override. + }) .option('chat-recording', { type: 'boolean', description: @@ -1158,6 +1201,7 @@ export async function loadCliConfig( process.env['https_proxy'] || process.env['HTTP_PROXY'] || process.env['http_proxy'], + insecure: resolveInsecureFlag(argv.insecure), cwd, fileDiscoveryService: fileService, bugCommand: settings.advanced?.bugCommand, diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 6ecf2f0cc13..0a316788e5f 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -594,6 +594,7 @@ describe('gemini.tsx main function kitty protocol', () => { openaiBaseUrl: undefined, openaiLoggingDir: undefined, proxy: undefined, + insecure: undefined, includeDirectories: undefined, screenReader: undefined, inputFormat: undefined, diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index de4d151b1ae..ebaa65912bd 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -11,7 +11,7 @@ import * as path from 'node:path'; import process from 'node:process'; // External dependencies -import { ProxyAgent, setGlobalDispatcher } from 'undici'; +import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici'; // Types import type { @@ -372,6 +372,13 @@ export interface ConfigParameters { }; checkpointing?: boolean; proxy?: string; + /** + * Disable TLS certificate verification for outbound HTTPS requests + * (model APIs, MCP servers reached over HTTPS, and similar). Intended for + * self-signed dev/lab endpoints. See ``getInsecure`` for the resolution + * order applied at the CLI layer (#3535). + */ + insecure?: boolean; cwd: string; fileDiscoveryService?: FileDiscoveryService; includeDirectories?: string[]; @@ -613,6 +620,7 @@ export class Config { private chatRecordingService: ChatRecordingService | undefined = undefined; private readonly checkpointing: boolean; private readonly proxy: string | undefined; + private readonly insecure: boolean; private readonly cwd: string; private readonly explicitIncludeDirectories: string[]; private readonly bugCommand: BugCommandSettings | undefined; @@ -759,6 +767,7 @@ export class Config { }; this.checkpointing = params.checkpointing ?? false; this.proxy = params.proxy; + this.insecure = params.insecure ?? false; this.cwd = params.cwd ?? process.cwd(); this.fileDiscoveryService = params.fileDiscoveryService ?? null; this.bugCommand = params.bugCommand; @@ -850,8 +859,22 @@ export class Config { } const proxyUrl = this.getProxy(); + // The global dispatcher backs every ``fetch`` call that does not provide + // its own dispatcher (MCP transports, streaming endpoints, telemetry). + // Apply both proxy and insecure-TLS here so they take effect uniformly, + // not only for the SDK clients we control directly (#3535). + const connect = this.insecure ? { rejectUnauthorized: false } : undefined; if (proxyUrl) { - setGlobalDispatcher(new ProxyAgent(proxyUrl)); + setGlobalDispatcher( + new ProxyAgent({ + uri: proxyUrl, + ...(connect ? { connect } : {}), + }), + ); + } else if (this.insecure) { + setGlobalDispatcher( + new Agent({ connect: { rejectUnauthorized: false } }), + ); } this.geminiClient = new GeminiClient(this); this.chatRecordingService = this.chatRecordingEnabled @@ -2035,6 +2058,20 @@ export class Config { return normalizeProxyUrl(this.proxy); } + /** + * Whether outbound HTTPS connections should skip TLS certificate + * verification. Useful for self-signed dev/lab model endpoints (#3535). + * + * Resolution order is applied by the CLI layer: + * 1. ``--insecure`` flag + * 2. ``QWEN_TLS_INSECURE`` env var (truthy: ``1``, ``true``, ``yes``) + * 3. ``NODE_TLS_REJECT_UNAUTHORIZED=0`` for parity with Node's + * legacy http stack and Claude Code. + */ + getInsecure(): boolean { + return this.insecure; + } + getWorkingDir(): string { return this.cwd; } diff --git a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts index dbdb5501e3b..14be55884d0 100644 --- a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts +++ b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts @@ -97,6 +97,7 @@ describe('AnthropicContentGenerator', () => { mockConfig = { getCliVersion: vi.fn().mockReturnValue('1.2.3'), getProxy: vi.fn().mockReturnValue(undefined), + getInsecure: vi.fn().mockReturnValue(false), } as unknown as Config; }); diff --git a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts index 5fa4c32e13a..60c82dc5ebb 100644 --- a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts +++ b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts @@ -66,10 +66,10 @@ export class AnthropicContentGenerator implements ContentGenerator { const baseURL = contentGeneratorConfig.baseUrl; // Configure runtime options to ensure user-configured timeout works as expected // bodyTimeout is always disabled (0) to let Anthropic SDK timeout control the request - const runtimeOptions = buildRuntimeFetchOptions( - 'anthropic', - this.cliConfig.getProxy(), - ); + const runtimeOptions = buildRuntimeFetchOptions('anthropic', { + proxyUrl: this.cliConfig.getProxy(), + insecure: this.cliConfig.getInsecure(), + }); this.client = new Anthropic({ apiKey: contentGeneratorConfig.apiKey, diff --git a/packages/core/src/core/openaiContentGenerator/provider/dashscope.test.ts b/packages/core/src/core/openaiContentGenerator/provider/dashscope.test.ts index 6385c052b2f..1000b859531 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/dashscope.test.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/dashscope.test.ts @@ -69,6 +69,7 @@ describe('DashScopeOpenAICompatibleProvider', () => { enableCacheControl: true, }), getProxy: vi.fn().mockReturnValue(undefined), + getInsecure: vi.fn().mockReturnValue(false), } as unknown as Config; provider = new DashScopeOpenAICompatibleProvider( diff --git a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts index 3fc8ecc28b8..63e8b1bebf9 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts @@ -63,10 +63,10 @@ export class DashScopeOpenAICompatibleProvider extends DefaultOpenAICompatiblePr const defaultHeaders = this.buildHeaders(); // Configure fetch options to ensure user-configured timeout works as expected // bodyTimeout is always disabled (0) to let OpenAI SDK timeout control the request - const runtimeOptions = buildRuntimeFetchOptions( - 'openai', - this.cliConfig.getProxy(), - ); + const runtimeOptions = buildRuntimeFetchOptions('openai', { + proxyUrl: this.cliConfig.getProxy(), + insecure: this.cliConfig.getInsecure(), + }); return new OpenAI({ apiKey, baseURL: baseUrl, diff --git a/packages/core/src/core/openaiContentGenerator/provider/default.test.ts b/packages/core/src/core/openaiContentGenerator/provider/default.test.ts index 12c04c115cf..cf998f2fb6b 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/default.test.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/default.test.ts @@ -62,6 +62,7 @@ describe('DefaultOpenAICompatibleProvider', () => { mockCliConfig = { getCliVersion: vi.fn().mockReturnValue('1.0.0'), getProxy: vi.fn().mockReturnValue(undefined), + getInsecure: vi.fn().mockReturnValue(false), } as unknown as Config; provider = new DefaultOpenAICompatibleProvider( diff --git a/packages/core/src/core/openaiContentGenerator/provider/default.ts b/packages/core/src/core/openaiContentGenerator/provider/default.ts index 3066a372c31..e7dc918e64e 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/default.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/default.ts @@ -51,10 +51,10 @@ export class DefaultOpenAICompatibleProvider const defaultHeaders = this.buildHeaders(); // Configure fetch options to ensure user-configured timeout works as expected // bodyTimeout is always disabled (0) to let OpenAI SDK timeout control the request - const runtimeOptions = buildRuntimeFetchOptions( - 'openai', - this.cliConfig.getProxy(), - ); + const runtimeOptions = buildRuntimeFetchOptions('openai', { + proxyUrl: this.cliConfig.getProxy(), + insecure: this.cliConfig.getInsecure(), + }); return new OpenAI({ apiKey, baseURL: baseUrl, diff --git a/packages/core/src/utils/runtimeFetchOptions.test.ts b/packages/core/src/utils/runtimeFetchOptions.test.ts index fd4e7a0891b..307c3afcbc6 100644 --- a/packages/core/src/utils/runtimeFetchOptions.test.ts +++ b/packages/core/src/utils/runtimeFetchOptions.test.ts @@ -92,4 +92,81 @@ describe('buildRuntimeFetchOptions (node runtime)', () => { bodyTimeout: 0, }); }); + + describe('insecure flag (#3535)', () => { + it('omits connect option when insecure is unset', () => { + const result = buildRuntimeFetchOptions('openai'); + const dispatcher = ( + result as { + fetchOptions?: { dispatcher?: { options?: UndiciOptions } }; + } + ).fetchOptions?.dispatcher; + expect(dispatcher?.options).not.toHaveProperty('connect'); + }); + + it('forwards rejectUnauthorized: false to undici Agent', () => { + const result = buildRuntimeFetchOptions('openai', { insecure: true }); + const dispatcher = ( + result as { + fetchOptions?: { dispatcher?: { options?: UndiciOptions } }; + } + ).fetchOptions?.dispatcher; + expect(dispatcher?.options).toMatchObject({ + connect: { rejectUnauthorized: false }, + headersTimeout: 0, + bodyTimeout: 0, + }); + }); + + it('forwards rejectUnauthorized: false to undici ProxyAgent', () => { + const result = buildRuntimeFetchOptions('openai', { + proxyUrl: 'http://proxy.local', + insecure: true, + }); + const dispatcher = ( + result as { + fetchOptions?: { dispatcher?: { options?: UndiciOptions } }; + } + ).fetchOptions?.dispatcher; + expect(dispatcher?.options).toMatchObject({ + uri: 'http://proxy.local', + connect: { rejectUnauthorized: false }, + headersTimeout: 0, + bodyTimeout: 0, + }); + }); + + it('treats a bare proxy-URL string identically to legacy callers', () => { + const stringResult = buildRuntimeFetchOptions( + 'openai', + 'http://proxy.local', + ); + const objectResult = buildRuntimeFetchOptions('openai', { + proxyUrl: 'http://proxy.local', + }); + const stringDispatcher = ( + stringResult as { + fetchOptions?: { dispatcher?: { options?: UndiciOptions } }; + } + ).fetchOptions?.dispatcher?.options; + const objectDispatcher = ( + objectResult as { + fetchOptions?: { dispatcher?: { options?: UndiciOptions } }; + } + ).fetchOptions?.dispatcher?.options; + expect(stringDispatcher).toEqual(objectDispatcher); + }); + + it('also threads insecure into Anthropic builders', () => { + const result = buildRuntimeFetchOptions('anthropic', { insecure: true }); + const dispatcher = ( + result as { + fetchOptions?: { dispatcher?: { options?: UndiciOptions } }; + } + ).fetchOptions?.dispatcher; + expect(dispatcher?.options).toMatchObject({ + connect: { rejectUnauthorized: false }, + }); + }); + }); }); diff --git a/packages/core/src/utils/runtimeFetchOptions.ts b/packages/core/src/utils/runtimeFetchOptions.ts index 1e0ef48068b..74e73b7725f 100644 --- a/packages/core/src/utils/runtimeFetchOptions.ts +++ b/packages/core/src/utils/runtimeFetchOptions.ts @@ -52,19 +52,35 @@ export type AnthropicRuntimeFetchOptions = { */ export type SDKType = 'openai' | 'anthropic'; +/** + * Optional runtime configuration shared across SDK builders. + * + * - ``proxyUrl``: Outbound HTTP/HTTPS proxy. When set, an undici + * ``ProxyAgent`` is used as the dispatcher. + * - ``insecure``: Disable TLS certificate verification. Required for + * self-signed dev/lab endpoints because undici ignores the + * ``NODE_TLS_REJECT_UNAUTHORIZED`` env var by default. The Node global + * ``rejectUnauthorized`` setting only affects the legacy ``http`` + * module, not undici/``fetch``. + */ +export interface RuntimeFetchConfig { + proxyUrl?: string; + insecure?: boolean; +} + /** * Build runtime-specific fetch options for OpenAI SDK */ export function buildRuntimeFetchOptions( sdkType: 'openai', - proxyUrl?: string, + proxyUrlOrConfig?: string | RuntimeFetchConfig, ): OpenAIRuntimeFetchOptions; /** * Build runtime-specific fetch options for Anthropic SDK */ export function buildRuntimeFetchOptions( sdkType: 'anthropic', - proxyUrl?: string, + proxyUrlOrConfig?: string | RuntimeFetchConfig, ): AnthropicRuntimeFetchOptions; /** * Build runtime-specific fetch options based on the detected runtime and SDK type @@ -72,13 +88,16 @@ export function buildRuntimeFetchOptions( * across Node.js and Bun, ensuring user-configured timeout works as expected. * * @param sdkType - The SDK type ('openai' or 'anthropic') to determine return type + * @param proxyUrlOrConfig - Either a proxy URL string (legacy positional form) or a + * ``RuntimeFetchConfig`` object carrying ``proxyUrl`` and/or ``insecure``. * @returns Runtime-specific options compatible with the specified SDK */ export function buildRuntimeFetchOptions( sdkType: SDKType, - proxyUrl?: string, + proxyUrlOrConfig?: string | RuntimeFetchConfig, ): OpenAIRuntimeFetchOptions | AnthropicRuntimeFetchOptions { const runtime = detectRuntime(); + const { proxyUrl, insecure } = normalizeConfig(proxyUrlOrConfig); // Always disable undici timeouts (set to 0) to let SDK's timeout parameter // control the total request time. bodyTimeout monitors intervals between data @@ -121,30 +140,50 @@ export function buildRuntimeFetchOptions( // Node.js: Use undici dispatcher for both SDKs. // This enables proxy support and disables undici timeouts so SDK timeout // controls the total request time. - return buildFetchOptionsWithDispatcher(sdkType, proxyUrl); + return buildFetchOptionsWithDispatcher(sdkType, proxyUrl, insecure); } default: { // Unknown runtime: treat as Node.js-like environment. - return buildFetchOptionsWithDispatcher(sdkType, proxyUrl); + return buildFetchOptionsWithDispatcher(sdkType, proxyUrl, insecure); } } } +function normalizeConfig( + proxyUrlOrConfig: string | RuntimeFetchConfig | undefined, +): RuntimeFetchConfig { + if (proxyUrlOrConfig === undefined) { + return {}; + } + if (typeof proxyUrlOrConfig === 'string') { + return { proxyUrl: proxyUrlOrConfig }; + } + return proxyUrlOrConfig; +} + function buildFetchOptionsWithDispatcher( sdkType: SDKType, - proxyUrl?: string, + proxyUrl: string | undefined, + insecure: boolean | undefined, ): OpenAIRuntimeFetchOptions | AnthropicRuntimeFetchOptions { try { + // undici exposes TLS options via ``connect``. Setting ``rejectUnauthorized`` + // here is the only way to mirror what Node's legacy http stack does for + // ``NODE_TLS_REJECT_UNAUTHORIZED=0``; the env var alone has no effect on + // ``fetch``-based code paths because undici uses its own connector. + const connect = insecure ? { rejectUnauthorized: false } : undefined; const dispatcher = proxyUrl ? new ProxyAgent({ uri: proxyUrl, headersTimeout: 0, bodyTimeout: 0, + ...(connect ? { connect } : {}), }) : new Agent({ headersTimeout: 0, bodyTimeout: 0, + ...(connect ? { connect } : {}), }); return { fetchOptions: { dispatcher } }; } catch {