diff --git a/README.md b/README.md index b1ed5d6d..aca495f4 100644 --- a/README.md +++ b/README.md @@ -429,6 +429,7 @@ Use this feature to monitor provider balances in `Provider Settings`. | Global Backoff Multiplier | `networkSettings.retry.backoffMultiplier` | Exponential backoff multiplier. Default: `2`; minimum: `1`. | | Global Jitter Factor | `networkSettings.retry.jitterFactor` | Jitter factor used to randomize delays. Default: `0.1`; range: `0`-`1`. | | Global Retryable Status Codes | `networkSettings.retry.statusCodes` | HTTP status codes that trigger retries. When set, this fully replaces the default rules: `408`, `409`, `429`, and all status codes `>=500`. | +| Ignore SSL Certificate Errors | `networkSettings.ignoreSSLErrors` | Whether to ignore TLS certificate validation errors for provider API requests. Independent of proxy settings; default: `false`. | | Global Proxy Settings | `networkSettings.proxy` | Global proxy settings for provider requests. See [Proxy Configuration](#proxy-configuration) for fields. | | Enable Code Completion | `completion.enabled` | Whether to enable this extension's code completion. Default: `true`; see [Completion Algorithm Parameters](#completion-algorithm-parameters). | | Completion Providers | `completion.providers` | Array of completion algorithm configurations. Default: `[]`; see [Completion Algorithm Parameters](#completion-algorithm-parameters). | @@ -458,6 +459,8 @@ Proxy settings can be configured globally through `unifyChatProvider.networkSett - `direct`: Connect directly and bypass VS Code/global proxy settings. - `custom`: Use `proxy.url`; optional fields are `authorization`, `strictSSL`, and `noProxy`. +Whether API requests ignore SSL/TLS certificate errors is controlled independently by `networkSettings.ignoreSSLErrors`. A provider-level `ignoreSSLErrors` value can override the global setting. + Supported custom proxy URL protocols are `http`, `https`, `socks`, `socks4`, `socks4a`, `socks5`, and `socks5h`. Proxy settings apply to provider HTTP requests, including chat requests, balance refreshes, and official model fetching. Example global proxy: @@ -525,6 +528,7 @@ The following fields correspond to `ProviderConfig` (field names used in import/ | Connection Timeout | `timeout.connection` | Must be a positive integer. Inherits the global value when unset; built-in default: `60000` (60 seconds). | | Response Interval Timeout | `timeout.response` | Must be a positive integer. Inherits the global value when unset; built-in default: `300000` (5 minutes). | | Retry | `retry` | Provider-level retry override for chat requests. Retryable HTTP status codes can be configured only through global `networkSettings.retry.statusCodes`. | +| Ignore SSL Certificate Errors | `ignoreSSLErrors` | Whether to ignore TLS certificate validation errors for this provider's API requests. Independent of proxy settings; default: `false`. | | Max Retries | `retry.maxRetries` | Must be a non-negative integer. Inherits the global value when unset; built-in default: `10`. | | Initial Delay | `retry.initialDelayMs` | Must be a non-negative integer in milliseconds. Inherits the global value when unset; built-in default: `1000`. | | Max Delay | `retry.maxDelayMs` | Must be a positive integer in milliseconds. Inherits the global value when unset; built-in default: `60000`. | diff --git a/README_zh-CN.md b/README_zh-CN.md index 9ab124ef..90b40ce0 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -429,6 +429,7 @@ Zed 编辑器使用自研的 Zeta 系列模型,这里推荐两种方式添加 | 全局退避倍数 | `networkSettings.retry.backoffMultiplier` | 指数退避倍数。默认:`2`,最小:`1`。 | | 全局抖动因子 | `networkSettings.retry.jitterFactor` | 用于随机化延迟的抖动因子。默认:`0.1`,范围:`0`-`1`。 | | 全局可重试状态码 | `networkSettings.retry.statusCodes` | 触发重试的 HTTP 状态码数组。设置后会完整覆盖默认规则;默认规则为 `408`、`409`、`429` 及所有 `>=500` 状态码。 | +| 忽略 SSL 证书错误 | `networkSettings.ignoreSSLErrors` | 是否忽略供应商 API 请求的 TLS 证书校验错误。与代理设置独立,默认:`false`;启用后会降低连接安全性。 | | 全局代理配置 | `networkSettings.proxy` | 供应商请求的全局代理设置。字段请查看 [代理配置](#代理配置)。 | | 启用代码补全 | `completion.enabled` | 是否启用本扩展的代码补全。默认:`true`;详细说明请查看 [补全算法参数](#补全算法参数)。 | | 补全供应商 | `completion.providers` | 补全算法配置数组。默认:`[]`;详细字段请查看 [补全算法参数](#补全算法参数)。 | @@ -458,6 +459,8 @@ Zed 编辑器使用自研的 Zeta 系列模型,这里推荐两种方式添加 - `direct`:直连,并绕过 VS Code 与全局代理设置。 - `custom`:使用 `proxy.url`;可选字段包括 `authorization`、`strictSSL` 和 `noProxy`。 +API 请求是否忽略 SSL/TLS 证书错误由 `networkSettings.ignoreSSLErrors` 控制,与是否使用代理无关。也可以在单个供应商的 `ignoreSSLErrors` 字段中覆盖该设置。 + 自定义代理 URL 支持 `http`、`https`、`socks`、`socks4`、`socks4a`、`socks5` 和 `socks5h` 协议。代理设置会影响供应商 HTTP 请求,包括聊天请求、余额刷新和官方模型拉取。 全局代理示例: @@ -525,6 +528,7 @@ Zed 编辑器使用自研的 Zeta 系列模型,这里推荐两种方式添加 | 建连超时 | `timeout.connection` | 必须为正整数。未设置时继承全局值;内置默认:`60000`(60 秒)。 | | 响应间隔超时 | `timeout.response` | 必须为正整数。未设置时继承全局值;内置默认:`300000`(5 分钟)。 | | 重试配置 | `retry` | 聊天请求的供应商级重试覆盖;可重试 HTTP 状态码只能通过全局 `networkSettings.retry.statusCodes` 配置。 | +| 忽略 SSL 证书错误 | `ignoreSSLErrors` | 是否忽略该供应商 API 请求的 TLS 证书校验错误。独立于代理设置,默认:`false`;启用后会降低连接安全性。 | | 最大重试次数 | `retry.maxRetries` | 必须为非负整数。未设置时继承全局值;内置默认:`10`。 | | 初始延迟 | `retry.initialDelayMs` | 必须为非负整数,单位毫秒。未设置时继承全局值;内置默认:`1000`。 | | 最大延迟 | `retry.maxDelayMs` | 必须为正整数,单位毫秒。未设置时继承全局值;内置默认:`60000`。 | diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 214ae651..60b5c703 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -1000,6 +1000,9 @@ "Please enter a valid HTTP(S) or SOCKS proxy URL": "Please enter a valid HTTP(S) or SOCKS proxy URL", "Proxy Authorization": "Proxy Authorization", "Proxy Strict SSL": "Proxy Strict SSL", + "Ignore SSL Certificate Errors": "Ignore SSL Certificate Errors", + "Disable TLS certificate validation for API requests": "Disable TLS certificate validation for API requests", + "Select whether to validate API certificates": "Select whether to validate API certificates", "Proxy Type": "Proxy Type", "Proxy URL": "Proxy URL", "Select proxy type": "Select proxy type", diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index 13050334..068c1504 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -1000,6 +1000,9 @@ "Please enter a valid HTTP(S) or SOCKS proxy URL": "请输入有效的 HTTP(S) 或 SOCKS 代理 URL", "Proxy Authorization": "代理认证", "Proxy Strict SSL": "代理 Strict SSL", + "Ignore SSL Certificate Errors": "忽略 SSL 证书错误", + "Disable TLS certificate validation for API requests": "禁用 API 请求的 TLS 证书校验", + "Select whether to validate API certificates": "选择是否校验 API 证书", "Proxy Type": "代理类型", "Proxy URL": "代理 URL", "Select proxy type": "选择代理类型", diff --git a/package.json b/package.json index 3be301cc..f8261b6e 100644 --- a/package.json +++ b/package.json @@ -322,6 +322,11 @@ } } }, + "ignoreSSLErrors": { + "type": "boolean", + "description": "%configuration.networkSettings.ignoreSSLErrors.description%", + "default": false + }, "proxy": { "type": "object", "description": "%configuration.networkSettings.proxy.description%", @@ -1050,6 +1055,11 @@ } } }, + "ignoreSSLErrors": { + "type": "boolean", + "description": "%configuration.endpoints.ignoreSSLErrors.description%", + "default": false + }, "proxy": { "type": "object", "description": "%configuration.endpoints.proxy.description%", diff --git a/package.nls.json b/package.nls.json index b13de277..bfee0037 100644 --- a/package.nls.json +++ b/package.nls.json @@ -55,6 +55,7 @@ "configuration.networkSettings.retry.backoffMultiplier.description": "Backoff multiplier for exponential retry delay (chat requests).", "configuration.networkSettings.retry.jitterFactor.description": "Jitter factor (0-1) to randomize retry delay (chat requests).", "configuration.networkSettings.retry.statusCodes.description": "HTTP status codes that should trigger retries for chat requests. When set, this overrides the default retryable status codes.", + "configuration.networkSettings.ignoreSSLErrors.description": "Ignore TLS certificate validation errors for provider API requests. This is independent of proxy settings and is insecure.", "configuration.networkSettings.proxy.description": "Global proxy settings for provider requests.", "configuration.proxy.type.description": "Proxy mode.", "configuration.proxy.type.enumDescriptions.0": "Use VS Code HTTP proxy settings.", @@ -148,6 +149,7 @@ "configuration.endpoints.retry.maxDelayMs.description": "Maximum retry delay cap in milliseconds (chat requests).", "configuration.endpoints.retry.backoffMultiplier.description": "Backoff multiplier for exponential retry delay (chat requests).", "configuration.endpoints.retry.jitterFactor.description": "Jitter factor (0-1) to randomize retry delay (chat requests).", + "configuration.endpoints.ignoreSSLErrors.description": "Ignore TLS certificate validation errors for this provider's API requests. This is independent of proxy settings and is insecure.", "configuration.endpoints.proxy.description": "Proxy settings for this provider. Overrides global proxy settings.", "configuration.endpoints.autoFetchOfficialModels.description": "Automatically fetch and sync official models from the provider API.", "configuration.endpoints.models.description": "List of available models.", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 45d45fcd..d868c52e 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -55,6 +55,7 @@ "configuration.networkSettings.retry.backoffMultiplier.description": "聊天请求的指数退避倍数。", "configuration.networkSettings.retry.jitterFactor.description": "聊天请求的抖动因子(0-1),用于随机化重试延迟。", "configuration.networkSettings.retry.statusCodes.description": "聊天请求中触发重试的 HTTP 状态码。设置后会覆盖默认可重试状态码。", + "configuration.networkSettings.ignoreSSLErrors.description": "忽略供应商 API 请求的 TLS 证书校验错误。此选项独立于代理设置,启用后会降低连接安全性。", "configuration.networkSettings.proxy.description": "供应商请求的全局代理设置。", "configuration.proxy.type.description": "代理模式。", "configuration.proxy.type.enumDescriptions.0": "使用 VS Code HTTP 代理设置。", @@ -148,6 +149,7 @@ "configuration.endpoints.retry.maxDelayMs.description": "聊天请求的最大重试延迟上限(毫秒)。", "configuration.endpoints.retry.backoffMultiplier.description": "聊天请求的指数退避倍数。", "configuration.endpoints.retry.jitterFactor.description": "聊天请求的抖动因子(0-1),用于随机化重试延迟。", + "configuration.endpoints.ignoreSSLErrors.description": "忽略该供应商 API 请求的 TLS 证书校验错误。此选项独立于代理设置,启用后会降低连接安全性。", "configuration.endpoints.proxy.description": "此供应商的代理设置。会覆盖全局代理设置。", "configuration.endpoints.autoFetchOfficialModels.description": "自动从供应商 API 拉取并同步官方模型。", "configuration.endpoints.models.description": "可用模型列表。", diff --git a/src/balance/balance-manager.ts b/src/balance/balance-manager.ts index 224fe286..6644a145 100644 --- a/src/balance/balance-manager.ts +++ b/src/balance/balance-manager.ts @@ -20,6 +20,7 @@ import { MainInstanceError, } from '../main-instance/errors'; import { t } from '../i18n'; +import { resolveChatNetwork } from '../utils'; const DEFAULT_PERIODIC_REFRESH_MS = 60_000; const DEFAULT_THROTTLE_WINDOW_MS = 10_000; @@ -650,6 +651,7 @@ export class BalanceManager implements vscode.Disposable { const providerWithResolvedProxy = { ...snapshot.provider, proxy: this.resolveProviderProxy(snapshot.provider), + ignoreSSLErrors: resolveChatNetwork(snapshot.provider).ignoreSSLErrors, }; const result = await balanceProvider.refresh({ provider: providerWithResolvedProxy, @@ -817,6 +819,8 @@ export class BalanceManager implements vscode.Disposable { useRawBaseUrl: provider.useRawBaseUrl === true, auth: provider.auth, balanceProvider: provider.balanceProvider, + ignoreSSLErrors: resolveChatNetwork(provider).ignoreSSLErrors, + proxy: this.resolveProviderProxy(provider), }); } diff --git a/src/balance/providers/aihubmix.ts b/src/balance/providers/aihubmix.ts index 5bb59c76..70c09180 100644 --- a/src/balance/providers/aihubmix.ts +++ b/src/balance/providers/aihubmix.ts @@ -225,6 +225,7 @@ export class AiHubMixBalanceProvider implements BalanceProvider { method: 'GET', headers, logger, + ignoreSSLErrors: input.provider.ignoreSSLErrors, proxy: input.provider.proxy, }); diff --git a/src/balance/providers/claude-relay-service.ts b/src/balance/providers/claude-relay-service.ts index e79cee49..55005a56 100644 --- a/src/balance/providers/claude-relay-service.ts +++ b/src/balance/providers/claude-relay-service.ts @@ -573,19 +573,41 @@ export class ClaudeRelayServiceBalanceProvider implements BalanceProvider { try { const proxy = input.provider.proxy; - const apiId = await this.fetchApiId(baseUrl, apiKey, logger, proxy); + const ignoreSSLErrors = input.provider.ignoreSSLErrors === true; + const apiId = await this.fetchApiId( + baseUrl, + apiKey, + logger, + proxy, + ignoreSSLErrors, + ); const [userStats, dailyStats, weeklyStats, monthlyStats] = await Promise.all([ - this.fetchUserStats(baseUrl, apiId, logger, proxy), - this.fetchUserModelStats(baseUrl, apiId, 'daily', logger, proxy), + this.fetchUserStats(baseUrl, apiId, logger, proxy, ignoreSSLErrors), + this.fetchUserModelStats( + baseUrl, + apiId, + 'daily', + logger, + proxy, + ignoreSSLErrors, + ), this.fetchUserModelStats( baseUrl, apiId, 'natural_weekly', logger, proxy, + ignoreSSLErrors, + ), + this.fetchUserModelStats( + baseUrl, + apiId, + 'monthly', + logger, + proxy, + ignoreSSLErrors, ), - this.fetchUserModelStats(baseUrl, apiId, 'monthly', logger, proxy), ]); const usage = this.aggregateUsage(userStats, { @@ -731,6 +753,7 @@ export class ClaudeRelayServiceBalanceProvider implements BalanceProvider { apiKey: string, logger: ReturnType, proxy: ProxyConfig | undefined, + ignoreSSLErrors: boolean, ): Promise { const endpoint = createEndpoint(baseUrl, 'get-key-id'); const response = await fetchWithRetry(endpoint, { @@ -741,6 +764,7 @@ export class ClaudeRelayServiceBalanceProvider implements BalanceProvider { }, body: JSON.stringify({ apiKey }), logger, + ignoreSSLErrors, proxy, }); @@ -793,6 +817,7 @@ export class ClaudeRelayServiceBalanceProvider implements BalanceProvider { apiId: string, logger: ReturnType, proxy: ProxyConfig | undefined, + ignoreSSLErrors: boolean, ): Promise { const endpoint = createEndpoint(baseUrl, 'user-stats'); const response = await fetchWithRetry(endpoint, { @@ -803,6 +828,7 @@ export class ClaudeRelayServiceBalanceProvider implements BalanceProvider { }, body: JSON.stringify({ apiId }), logger, + ignoreSSLErrors, proxy, }); @@ -884,6 +910,7 @@ export class ClaudeRelayServiceBalanceProvider implements BalanceProvider { period: CrsModelStatsPeriod, logger: ReturnType, proxy: ProxyConfig | undefined, + ignoreSSLErrors: boolean, ): Promise { const endpoint = createEndpoint(baseUrl, 'user-model-stats'); const response = await fetchWithRetry(endpoint, { @@ -894,6 +921,7 @@ export class ClaudeRelayServiceBalanceProvider implements BalanceProvider { }, body: JSON.stringify({ apiId, period }), logger, + ignoreSSLErrors, proxy, }); diff --git a/src/balance/providers/code-assist-quota.ts b/src/balance/providers/code-assist-quota.ts index 1bff2ecb..b2b523bd 100644 --- a/src/balance/providers/code-assist-quota.ts +++ b/src/balance/providers/code-assist-quota.ts @@ -331,6 +331,7 @@ export async function refreshCodeAssistQuota( }, body: JSON.stringify({ project: projectId }), logger, + ignoreSSLErrors: input.provider.ignoreSSLErrors, proxy: input.provider.proxy, }); diff --git a/src/balance/providers/codex.ts b/src/balance/providers/codex.ts index 3c3a04c7..e6ebcf0a 100644 --- a/src/balance/providers/codex.ts +++ b/src/balance/providers/codex.ts @@ -503,6 +503,7 @@ export class CodexBalanceProvider implements BalanceProvider { ...(accountId ? { 'ChatGPT-Account-Id': accountId } : {}), }, logger, + ignoreSSLErrors: input.provider.ignoreSSLErrors, proxy: input.provider.proxy, }); diff --git a/src/balance/providers/deepseek.ts b/src/balance/providers/deepseek.ts index f7e46b7c..1b2ac171 100644 --- a/src/balance/providers/deepseek.ts +++ b/src/balance/providers/deepseek.ts @@ -224,6 +224,7 @@ export class DeepSeekBalanceProvider implements BalanceProvider { Accept: 'application/json', }, logger, + ignoreSSLErrors: input.provider.ignoreSSLErrors, proxy: input.provider.proxy, }); diff --git a/src/balance/providers/kimi-code.ts b/src/balance/providers/kimi-code.ts index d01df49b..5b02a656 100644 --- a/src/balance/providers/kimi-code.ts +++ b/src/balance/providers/kimi-code.ts @@ -417,6 +417,7 @@ export class KimiCodeBalanceProvider implements BalanceProvider { Accept: 'application/json', }, logger, + ignoreSSLErrors: input.provider.ignoreSSLErrors, proxy: input.provider.proxy, }); diff --git a/src/balance/providers/litellm.ts b/src/balance/providers/litellm.ts index 6234bde1..664ca1e4 100644 --- a/src/balance/providers/litellm.ts +++ b/src/balance/providers/litellm.ts @@ -158,6 +158,7 @@ export class LiteLLMBalanceProvider implements BalanceProvider { Accept: 'application/json', }, logger, + ignoreSSLErrors: input.provider.ignoreSSLErrors, proxy: input.provider.proxy, }); diff --git a/src/balance/providers/minimax.ts b/src/balance/providers/minimax.ts index 484bd483..e20647f0 100644 --- a/src/balance/providers/minimax.ts +++ b/src/balance/providers/minimax.ts @@ -171,6 +171,7 @@ export class MiniMaxBalanceProvider implements BalanceProvider { Accept: 'application/json', }, logger, + ignoreSSLErrors: input.provider.ignoreSSLErrors, proxy: input.provider.proxy, }); diff --git a/src/balance/providers/moonshot-ai.ts b/src/balance/providers/moonshot-ai.ts index 342f27fc..b00e4d65 100644 --- a/src/balance/providers/moonshot-ai.ts +++ b/src/balance/providers/moonshot-ai.ts @@ -156,6 +156,7 @@ export class MoonshotAIBalanceProvider implements BalanceProvider { Accept: 'application/json', }, logger, + ignoreSSLErrors: input.provider.ignoreSSLErrors, proxy: input.provider.proxy, }); diff --git a/src/balance/providers/newapi.ts b/src/balance/providers/newapi.ts index 1cf5e88f..2894f6d8 100644 --- a/src/balance/providers/newapi.ts +++ b/src/balance/providers/newapi.ts @@ -449,6 +449,7 @@ export class NewAPIBalanceProvider implements BalanceProvider { apiKey, logger, input.provider.proxy, + input.provider.ignoreSSLErrors === true, ); const items: BalanceMetric[] = []; @@ -477,6 +478,7 @@ export class NewAPIBalanceProvider implements BalanceProvider { systemToken, logger, input.provider.proxy, + input.provider.ignoreSSLErrors === true, ); items.push(...userBalance.items); } catch (error) { @@ -518,6 +520,7 @@ export class NewAPIBalanceProvider implements BalanceProvider { apiKey: string, logger: ReturnType, proxy: ProxyConfig | undefined, + ignoreSSLErrors: boolean, ): Promise { const endpoint = new URL('/api/usage/token', `${baseUrl}/`).toString(); @@ -528,6 +531,7 @@ export class NewAPIBalanceProvider implements BalanceProvider { Accept: 'application/json', }, logger, + ignoreSSLErrors, proxy, }); @@ -617,6 +621,7 @@ export class NewAPIBalanceProvider implements BalanceProvider { systemToken: string, logger: ReturnType, proxy: ProxyConfig | undefined, + ignoreSSLErrors: boolean, ): Promise { const endpoint = new URL('/api/user/self', `${baseUrl}/`).toString(); @@ -628,6 +633,7 @@ export class NewAPIBalanceProvider implements BalanceProvider { Accept: 'application/json', }, logger, + ignoreSSLErrors, proxy, }); diff --git a/src/balance/providers/openrouter.ts b/src/balance/providers/openrouter.ts index f69c2879..34f0b935 100644 --- a/src/balance/providers/openrouter.ts +++ b/src/balance/providers/openrouter.ts @@ -172,6 +172,7 @@ export class OpenRouterBalanceProvider implements BalanceProvider { Accept: 'application/json', }, logger, + ignoreSSLErrors: input.provider.ignoreSSLErrors, proxy: input.provider.proxy, }); diff --git a/src/balance/providers/siliconflow.ts b/src/balance/providers/siliconflow.ts index 1a3066f9..d73de3ee 100644 --- a/src/balance/providers/siliconflow.ts +++ b/src/balance/providers/siliconflow.ts @@ -180,6 +180,7 @@ export class SiliconFlowBalanceProvider implements BalanceProvider { Accept: 'application/json', }, logger, + ignoreSSLErrors: input.provider.ignoreSSLErrors, proxy: input.provider.proxy, }); diff --git a/src/balance/providers/synthetic.ts b/src/balance/providers/synthetic.ts index 404bc321..9e19e1a3 100644 --- a/src/balance/providers/synthetic.ts +++ b/src/balance/providers/synthetic.ts @@ -137,6 +137,7 @@ export class SyntheticBalanceProvider implements BalanceProvider { 'Accept': 'application/json', }, logger, + ignoreSSLErrors: input.provider.ignoreSSLErrors, proxy: input.provider.proxy, }); diff --git a/src/client/anthropic/client.ts b/src/client/anthropic/client.ts index ad08eadc..f04e679f 100644 --- a/src/client/anthropic/client.ts +++ b/src/client/anthropic/client.ts @@ -145,7 +145,8 @@ export class AnthropicProvider implements ApiProvider { ): Anthropic { const chatNetwork = mode === 'chat' ? resolveChatNetwork(this.config) : undefined; - const proxy = chatNetwork?.proxy ?? resolveChatNetwork(this.config).proxy; + const network = chatNetwork ?? resolveChatNetwork(this.config); + const proxy = network.proxy; const effectiveTimeout = chatNetwork?.timeout ?? DEFAULT_NORMAL_TIMEOUT_CONFIG; @@ -174,6 +175,7 @@ export class AnthropicProvider implements ApiProvider { responseTimeoutMs: effectiveTimeout.response, logger, retryConfig: chatNetwork?.retry, + ignoreSSLErrors: network.ignoreSSLErrors, proxy, type: mode, abortSignal, diff --git a/src/client/github-copilot/client.ts b/src/client/github-copilot/client.ts index 9a66bbfb..cbf270b5 100644 --- a/src/client/github-copilot/client.ts +++ b/src/client/github-copilot/client.ts @@ -755,6 +755,7 @@ export class GitHubCopilotProvider implements ApiProvider { responseTimeoutMs: effectiveTimeout.response, logger, retryConfig: chatNetwork.retry, + ignoreSSLErrors: chatNetwork.ignoreSSLErrors, proxy: chatNetwork.proxy, type: 'normal', }); diff --git a/src/client/google/ai-studio-client.ts b/src/client/google/ai-studio-client.ts index 97128571..604ed488 100644 --- a/src/client/google/ai-studio-client.ts +++ b/src/client/google/ai-studio-client.ts @@ -1063,6 +1063,7 @@ export class GoogleAIStudioProvider implements ApiProvider { { connectionTimeoutMs: requestTimeoutMs, retryConfig: chatNetwork.retry, + ignoreSSLErrors: chatNetwork.ignoreSSLErrors, proxy: chatNetwork.proxy, }, ); @@ -1094,6 +1095,7 @@ export class GoogleAIStudioProvider implements ApiProvider { { connectionTimeoutMs: requestTimeoutMs, retryConfig: chatNetwork.retry, + ignoreSSLErrors: chatNetwork.ignoreSSLErrors, proxy: chatNetwork.proxy, }, ); @@ -1386,7 +1388,10 @@ export class GoogleAIStudioProvider implements ApiProvider { }, }); }, - { proxy: network.proxy }, + { + ignoreSSLErrors: network.ignoreSSLErrors, + proxy: network.proxy, + }, ); for await (const model of pager) { if (model.name) { @@ -1405,6 +1410,7 @@ type FetchLoggerContext = { logger: ProviderHttpLogger; connectionTimeoutMs?: number; retryConfig?: RetryConfig; + ignoreSSLErrors?: boolean; proxy?: ProviderConfig['proxy']; }; @@ -1491,6 +1497,7 @@ function ensureInstalled(): void { logger: ctx.logger, retryConfig: ctx.retryConfig, connectionTimeoutMs: ctx.connectionTimeoutMs, + ignoreSSLErrors: ctx.ignoreSSLErrors, proxy: ctx.proxy, }) : await baseFetch(input, init); @@ -1528,7 +1535,7 @@ async function withGoogleFetchLogger( fn: () => Promise, options?: Pick< FetchLoggerContext, - 'connectionTimeoutMs' | 'retryConfig' | 'proxy' + 'connectionTimeoutMs' | 'retryConfig' | 'ignoreSSLErrors' | 'proxy' >, ): Promise { ensureInstalled(); diff --git a/src/client/google/code-assist-client.ts b/src/client/google/code-assist-client.ts index bca9b894..6ef2c9cb 100644 --- a/src/client/google/code-assist-client.ts +++ b/src/client/google/code-assist-client.ts @@ -2042,6 +2042,7 @@ export abstract class GoogleCodeAssistProvider extends GoogleAIStudioProvider { responseTimeoutMs: chatNetwork.timeout.response, logger, retryConfig: { ...effectiveRetryConfig, maxRetries: 0 }, + ignoreSSLErrors: chatNetwork.ignoreSSLErrors, proxy: chatNetwork.proxy, type: 'chat', abortSignal: abortController.signal, diff --git a/src/client/ollama/client.ts b/src/client/ollama/client.ts index 2aab1f40..7a9dc594 100644 --- a/src/client/ollama/client.ts +++ b/src/client/ollama/client.ts @@ -145,7 +145,8 @@ export class OllamaProvider implements ApiProvider { ): Ollama { const chatNetwork = mode === 'chat' ? resolveChatNetwork(this.config) : undefined; - const proxy = chatNetwork?.proxy ?? resolveChatNetwork(this.config).proxy; + const network = chatNetwork ?? resolveChatNetwork(this.config); + const proxy = network.proxy; const effectiveTimeout = chatNetwork?.timeout ?? DEFAULT_NORMAL_TIMEOUT_CONFIG; @@ -156,6 +157,7 @@ export class OllamaProvider implements ApiProvider { responseTimeoutMs: effectiveTimeout.response, logger, retryConfig: chatNetwork?.retry, + ignoreSSLErrors: network.ignoreSSLErrors, proxy, type: mode, abortSignal, diff --git a/src/client/openai/chat-completion-client.ts b/src/client/openai/chat-completion-client.ts index 3e7225e4..795b7527 100644 --- a/src/client/openai/chat-completion-client.ts +++ b/src/client/openai/chat-completion-client.ts @@ -253,7 +253,8 @@ export class OpenAIChatCompletionProvider implements ApiProvider { ): OpenAI { const chatNetwork = mode === 'chat' ? resolveChatNetwork(this.config) : undefined; - const proxy = chatNetwork?.proxy ?? resolveChatNetwork(this.config).proxy; + const network = chatNetwork ?? resolveChatNetwork(this.config); + const proxy = network.proxy; const effectiveTimeout = chatNetwork?.timeout ?? DEFAULT_NORMAL_TIMEOUT_CONFIG; @@ -271,6 +272,7 @@ export class OpenAIChatCompletionProvider implements ApiProvider { responseTimeoutMs: effectiveTimeout.response, logger, retryConfig: chatNetwork?.retry, + ignoreSSLErrors: network.ignoreSSLErrors, proxy, type: mode, abortSignal, diff --git a/src/client/openai/codex-client.ts b/src/client/openai/codex-client.ts index 08b13475..9c47c241 100644 --- a/src/client/openai/codex-client.ts +++ b/src/client/openai/codex-client.ts @@ -398,7 +398,8 @@ export class OpenAICodexProvider extends OpenAIResponsesProvider { ): OpenAI { const chatNetwork = mode === 'chat' ? resolveChatNetwork(this.config) : undefined; - const proxy = chatNetwork?.proxy ?? resolveChatNetwork(this.config).proxy; + const network = chatNetwork ?? resolveChatNetwork(this.config); + const proxy = network.proxy; const effectiveTimeout = chatNetwork?.timeout ?? DEFAULT_NORMAL_TIMEOUT_CONFIG; @@ -411,6 +412,7 @@ export class OpenAICodexProvider extends OpenAIResponsesProvider { responseTimeoutMs: effectiveTimeout.response, logger, retryConfig: chatNetwork?.retry, + ignoreSSLErrors: network.ignoreSSLErrors, proxy, urlTransformer: this.config.auth?.method === 'openai-codex' diff --git a/src/client/openai/responses-client.ts b/src/client/openai/responses-client.ts index 73fcec65..7e3d2495 100644 --- a/src/client/openai/responses-client.ts +++ b/src/client/openai/responses-client.ts @@ -742,7 +742,8 @@ export class OpenAIResponsesProvider implements ApiProvider { ): OpenAI { const chatNetwork = mode === 'chat' ? resolveChatNetwork(this.config) : undefined; - const proxy = chatNetwork?.proxy ?? resolveChatNetwork(this.config).proxy; + const network = chatNetwork ?? resolveChatNetwork(this.config); + const proxy = network.proxy; const effectiveTimeout = chatNetwork?.timeout ?? DEFAULT_NORMAL_TIMEOUT_CONFIG; @@ -760,6 +761,7 @@ export class OpenAIResponsesProvider implements ApiProvider { responseTimeoutMs: effectiveTimeout.response, logger, retryConfig: chatNetwork?.retry, + ignoreSSLErrors: network.ignoreSSLErrors, proxy, type: mode, abortSignal, @@ -798,6 +800,7 @@ export class OpenAIResponsesProvider implements ApiProvider { transportClient, headers, multiAgentEnabled, + resolveChatNetwork(this.config).ignoreSSLErrors, ); } diff --git a/src/client/openai/responses-websocket-transport.ts b/src/client/openai/responses-websocket-transport.ts index aa156040..c2ff092c 100644 --- a/src/client/openai/responses-websocket-transport.ts +++ b/src/client/openai/responses-websocket-transport.ts @@ -110,9 +110,14 @@ export class OpenAIResponsesWebSocketTransport client: OpenAI, headers: Record | undefined, beta: boolean, + ignoreSSLErrors: boolean, ) { + const options = { + headers, + ...(ignoreSSLErrors ? { rejectUnauthorized: false } : {}), + }; if (beta) { - const ws = new BetaResponsesWS(client, { headers }); + const ws = new BetaResponsesWS(client, options); this.sendPayload = (payload) => { ws.sendRaw(JSON.stringify(payload)); }; @@ -133,7 +138,7 @@ export class OpenAIResponsesWebSocketTransport return; } - const ws = new ResponsesWS(client, { headers }); + const ws = new ResponsesWS(client, options); this.sendPayload = (payload) => { ws.sendRaw(JSON.stringify(payload)); }; diff --git a/src/client/utils.ts b/src/client/utils.ts index 7d8a756d..d1cdd035 100644 --- a/src/client/utils.ts +++ b/src/client/utils.ts @@ -1002,6 +1002,7 @@ export interface CreateCustomFetchOptions { urlTransformer?: (url: string) => string; retryConfig?: RetryConfig; proxy?: ProviderConfig['proxy']; + ignoreSSLErrors?: boolean; type: FetchMode; /** * Optional upstream abort signal (e.g. derived from VSCode CancellationToken). @@ -1039,6 +1040,7 @@ export function createCustomFetch( urlTransformer, retryConfig, proxy, + ignoreSSLErrors, type, abortSignal, } = options; @@ -1154,6 +1156,7 @@ export function createCustomFetch( ? DEFAULT_CHAT_RETRY_CONFIG : DEFAULT_NORMAL_RETRY_CONFIG), connectionTimeoutMs, + ignoreSSLErrors, proxy, }); diff --git a/src/client/zed/runtime.ts b/src/client/zed/runtime.ts index 42fca55f..48f14c37 100644 --- a/src/client/zed/runtime.ts +++ b/src/client/zed/runtime.ts @@ -73,6 +73,7 @@ export function createZedCloudClient(provider: ProviderConfig): ZedCloudClient { connectionTimeoutMs: network.timeout.connection, responseTimeoutMs: network.timeout.response, retryConfig: network.retry, + ignoreSSLErrors: network.ignoreSSLErrors, proxy: network.proxy, type: 'normal', }); diff --git a/src/completion/api/http.ts b/src/completion/api/http.ts index c39d1569..17410a93 100644 --- a/src/completion/api/http.ts +++ b/src/completion/api/http.ts @@ -67,6 +67,7 @@ export async function postCompletionJson( connectionTimeoutMs: network.timeout.connection, responseTimeoutMs: network.timeout.response, retryConfig: network.retry, + ignoreSSLErrors: network.ignoreSSLErrors, proxy: network.proxy, type: 'chat', abortSignal: abortController.signal, diff --git a/src/config-ops.ts b/src/config-ops.ts index cf5a31e9..8abb753f 100644 --- a/src/config-ops.ts +++ b/src/config-ops.ts @@ -67,6 +67,7 @@ export const PROVIDER_CONFIG_KEYS = [ 'extraBody', 'timeout', 'retry', + 'ignoreSSLErrors', 'proxy', 'autoFetchOfficialModels', 'contextCache', diff --git a/src/config-store.ts b/src/config-store.ts index 690933c9..becd3589 100644 --- a/src/config-store.ts +++ b/src/config-store.ts @@ -380,6 +380,10 @@ export class ConfigStore { provider.serviceTier = this.normalizeServiceTier(provider.serviceTier); provider.extraHeaders = this.normalizeStringRecord(provider.extraHeaders); provider.extraBody = this.normalizeObjectRecord(provider.extraBody); + provider.ignoreSSLErrors = + typeof provider.ignoreSSLErrors === 'boolean' + ? provider.ignoreSSLErrors + : undefined; provider.proxy = this.normalizeProxyConfig(provider.proxy); provider.contextCache = this.normalizeContextCacheConfig( provider.contextCache, diff --git a/src/main-instance/register-handlers.ts b/src/main-instance/register-handlers.ts index 609138e6..47113862 100644 --- a/src/main-instance/register-handlers.ts +++ b/src/main-instance/register-handlers.ts @@ -809,6 +809,10 @@ export function parseProviderConfig( const extraHeaders = parseStringRecord(record['extraHeaders']); const timeout = parseTimeoutConfig(record['timeout']); const retry = parseRetryConfig(record['retry']); + const ignoreSSLErrors = + typeof record['ignoreSSLErrors'] === 'boolean' + ? record['ignoreSSLErrors'] + : undefined; const contextCache = parseContextCacheConfig(record['contextCache']); const completion = parseCompletionConfig( record['completion'], @@ -833,6 +837,7 @@ export function parseProviderConfig( : {}), ...(timeout ? { timeout } : {}), ...(retry ? { retry } : {}), + ...(ignoreSSLErrors === undefined ? {} : { ignoreSSLErrors }), ...(typeof record['autoFetchOfficialModels'] === 'boolean' ? { autoFetchOfficialModels: record['autoFetchOfficialModels'] } : {}), @@ -1079,6 +1084,7 @@ export function parseOfficialModelsFetchState( method, 'state.lastConfigSignature.extraBodyHash', ), + ignoreSSLErrors: signature['ignoreSSLErrors'] === true, proxyHash: parseOptionalString(signature['proxyHash']) ?? '', }; } diff --git a/src/official-models-manager.ts b/src/official-models-manager.ts index 311d797c..a70287f4 100644 --- a/src/official-models-manager.ts +++ b/src/official-models-manager.ts @@ -72,6 +72,7 @@ export interface FetchConfigSignature { authHash: string; extraHeadersHash: string; extraBodyHash: string; + ignoreSSLErrors: boolean; proxyHash: string; } @@ -88,6 +89,7 @@ export interface OfficialModelsDraftInput { extraHeaders?: ProviderConfig['extraHeaders']; extraBody?: ProviderConfig['extraBody']; timeout?: ProviderConfig['timeout']; + ignoreSSLErrors?: ProviderConfig['ignoreSSLErrors']; proxy?: ProviderConfig['proxy']; } @@ -1207,6 +1209,7 @@ export class OfficialModelsManager { stableStringify(input.extraHeaders ?? {}), ), extraBodyHash: this.hashString(stableStringify(input.extraBody ?? {})), + ignoreSSLErrors: input.ignoreSSLErrors === true, proxyHash: this.hashString(stableStringify(input.proxy ?? {})), }; } @@ -1265,6 +1268,7 @@ export class OfficialModelsManager { extraHeaders: input.extraHeaders, extraBody: input.extraBody, timeout: input.timeout, + ignoreSSLErrors: input.ignoreSSLErrors, proxy: input.proxy, }; @@ -1532,6 +1536,7 @@ export class OfficialModelsManager { stableStringify(provider.extraHeaders ?? {}), ), extraBodyHash: this.hashString(stableStringify(provider.extraBody ?? {})), + ignoreSSLErrors: provider.ignoreSSLErrors === true, proxyHash: this.hashString(stableStringify(provider.proxy ?? {})), }; } @@ -1596,6 +1601,7 @@ export class OfficialModelsManager { authHash: '', extraHeadersHash: '', extraBodyHash: '', + ignoreSSLErrors: false, proxyHash: '', }, }; @@ -1674,6 +1680,7 @@ export class OfficialModelsManager { a.authHash === b.authHash && a.extraHeadersHash === b.extraHeadersHash && a.extraBodyHash === b.extraBodyHash && + a.ignoreSSLErrors === b.ignoreSSLErrors && a.proxyHash === b.proxyHash ); } diff --git a/src/types.ts b/src/types.ts index ba2a8396..1c5235e0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -112,6 +112,8 @@ export interface ProviderConfig { timeout?: TimeoutConfig; /** Retry configuration */ retry?: RetryConfig; + /** Whether to ignore TLS certificate validation errors for API requests. */ + ignoreSSLErrors?: boolean; /** Proxy configuration */ proxy?: ProxyConfig; /** Whether to auto-fetch official models from the provider API */ diff --git a/src/ui/router/types.ts b/src/ui/router/types.ts index 636b789d..61d82e77 100644 --- a/src/ui/router/types.ts +++ b/src/ui/router/types.ts @@ -121,6 +121,7 @@ export interface TimeoutFormRoute { kind: 'timeoutForm'; timeout: TimeoutConfig; retry: RetryConfig; + ignoreSSLErrors: boolean | undefined; proxy: ProxyConfig; draft: ProviderFormDraft; } diff --git a/src/ui/screens/model-list-screen.ts b/src/ui/screens/model-list-screen.ts index d0d9639f..fb1aa111 100644 --- a/src/ui/screens/model-list-screen.ts +++ b/src/ui/screens/model-list-screen.ts @@ -745,6 +745,7 @@ function buildOfficialModelsDraftInput( extraHeaders: draft.extraHeaders, extraBody: draft.extraBody, timeout: draft.timeout, + ignoreSSLErrors: draft.ignoreSSLErrors, proxy: draft.proxy, }; } diff --git a/src/ui/screens/provider-draft-form-screen.ts b/src/ui/screens/provider-draft-form-screen.ts index d33edae3..af559e7f 100644 --- a/src/ui/screens/provider-draft-form-screen.ts +++ b/src/ui/screens/provider-draft-form-screen.ts @@ -269,6 +269,7 @@ export async function runProviderDraftFormScreen( kind: 'timeoutForm', timeout: draft.timeout ?? {}, retry: draft.retry ?? {}, + ignoreSSLErrors: draft.ignoreSSLErrors, proxy: draft.proxy ?? {}, draft, }, diff --git a/src/ui/screens/provider-form-screen.ts b/src/ui/screens/provider-form-screen.ts index a16a198b..af9f5218 100644 --- a/src/ui/screens/provider-form-screen.ts +++ b/src/ui/screens/provider-form-screen.ts @@ -309,6 +309,7 @@ export async function runProviderFormScreen( kind: 'timeoutForm', timeout: draft.timeout ?? {}, retry: draft.retry ?? {}, + ignoreSSLErrors: draft.ignoreSSLErrors, proxy: draft.proxy ?? {}, draft, }, diff --git a/src/ui/screens/timeout-form-screen.ts b/src/ui/screens/timeout-form-screen.ts index 9e69ee12..e932f293 100644 --- a/src/ui/screens/timeout-form-screen.ts +++ b/src/ui/screens/timeout-form-screen.ts @@ -12,6 +12,7 @@ import { t } from '../../i18n'; type NetworkField = | { kind: 'timeout'; field: 'connection' | 'response' } + | { kind: 'ignoreSSLErrors' } | { kind: 'retry'; field: @@ -69,6 +70,18 @@ export async function runTimeoutFormScreen( edit: { kind: 'timeout', field: 'response' }, }, { label: '', kind: vscode.QuickPickItemKind.Separator }, + { + label: `$(shield) ${t('Ignore SSL Certificate Errors')}`, + description: + route.ignoreSSLErrors === undefined + ? t('default ({0})', t('Disabled')) + : route.ignoreSSLErrors + ? t('Enabled') + : t('Disabled'), + detail: t('Disable TLS certificate validation for API requests'), + edit: { kind: 'ignoreSSLErrors' }, + }, + { label: '', kind: vscode.QuickPickItemKind.Separator }, { label: `$(sync) ${t('Max Retries')}`, description: formatRetryValue( @@ -187,6 +200,7 @@ export async function runTimeoutFormScreen( if (!selection || selection.action === 'back') { route.draft.timeout = hasTimeoutValues(timeout) ? timeout : undefined; route.draft.retry = hasRetryValues(retry) ? retry : undefined; + route.draft.ignoreSSLErrors = route.ignoreSSLErrors; route.draft.proxy = hasProxyValues(proxy) ? proxy : undefined; return { kind: 'pop' }; } @@ -199,6 +213,7 @@ export async function runTimeoutFormScreen( route.retry.maxDelayMs = undefined; route.retry.backoffMultiplier = undefined; route.retry.jitterFactor = undefined; + route.ignoreSSLErrors = undefined; route.proxy.type = undefined; route.proxy.url = undefined; route.proxy.authorization = undefined; @@ -223,6 +238,22 @@ export async function runTimeoutFormScreen( await editTimeoutField(timeout, selection.edit.field, defaultValue); } else if (selection.edit.kind === 'retry') { await editRetryField(retry, selection.edit.field, globalDefaults.retry); + } else if (selection.edit.kind === 'ignoreSSLErrors') { + const picked = await pickQuickItem< + vscode.QuickPickItem & { value: boolean | undefined } + >({ + title: t('Ignore SSL Certificate Errors'), + placeholder: t('Select whether to validate API certificates'), + ignoreFocusOut: true, + items: [ + { label: t('Default'), value: undefined }, + { label: t('Disabled'), value: false }, + { label: t('Enabled'), value: true }, + ], + }); + if (picked) { + route.ignoreSSLErrors = picked.value; + } } else { await editProxyField(proxy, selection.edit.field); } diff --git a/src/utils.ts b/src/utils.ts index b9fce8c1..06388662 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -129,6 +129,7 @@ export interface ResolvedChatRetryConfig { export interface ResolvedChatNetworkConfig { timeout: ResolvedChatTimeoutConfig; retry: ResolvedChatRetryConfig; + ignoreSSLErrors: boolean; proxy?: ProxyConfig; } @@ -137,6 +138,7 @@ const MAX_SAFE_TIMEOUT_MS = 0x7fffffff; export interface ChatNetworkOverrides { timeout?: TimeoutConfig; retry?: RetryConfig; + ignoreSSLErrors?: boolean; proxy?: ProxyConfig; } @@ -256,6 +258,7 @@ function applyGlobalRetryOverrides( function readConfiguredChatNetworkOverrides(): { timeout?: unknown; retry?: unknown; + ignoreSSLErrors?: unknown; proxy?: unknown; } { const config = vscode.workspace.getConfiguration( @@ -266,9 +269,10 @@ function readConfiguredChatNetworkOverrides(): { const timeout = raw['timeout']; const retry = raw['retry']; + const ignoreSSLErrors = raw['ignoreSSLErrors']; const proxy = raw['proxy']; - return { timeout, retry, proxy }; + return { timeout, retry, ignoreSSLErrors, proxy }; } function readProxyType(value: unknown): ProxyType | undefined { @@ -374,14 +378,21 @@ export function resolveChatNetwork( backoffMultiplier: DEFAULT_CHAT_RETRY_CONFIG.backoffMultiplier, jitterFactor: DEFAULT_CHAT_RETRY_CONFIG.jitterFactor, }, + ignoreSSLErrors: false, }; const configured = readConfiguredChatNetworkOverrides(); applyTimeoutOverrides(resolved.timeout, configured.timeout); applyGlobalRetryOverrides(resolved.retry, configured.retry); + if (typeof configured.ignoreSSLErrors === 'boolean') { + resolved.ignoreSSLErrors = configured.ignoreSSLErrors; + } applyTimeoutOverrides(resolved.timeout, overrides?.timeout); applyRetryOverrides(resolved.retry, overrides?.retry); + if (typeof overrides?.ignoreSSLErrors === 'boolean') { + resolved.ignoreSSLErrors = overrides.ignoreSSLErrors; + } resolved.proxy = resolveChatProxyConfig(configured.proxy, overrides?.proxy); return resolved; @@ -440,6 +451,7 @@ export interface FetchWithRetryOptions extends RequestInit { logger?: ProviderHttpLogger; /** Connection timeout in milliseconds. If not specified, uses DEFAULT_NORMAL_TIMEOUT_CONFIG.connection */ connectionTimeoutMs?: number; + ignoreSSLErrors?: boolean; proxy?: ProxyConfig; } @@ -1109,9 +1121,10 @@ export async function fetchWithRetry( input: RequestInfo | URL, options: FetchWithRetryOptions = {}, ): Promise { - const { proxy, ...retryOptions } = options; + const { proxy, ignoreSSLErrors, ...retryOptions } = options; return fetchWithRetryUsingFetch( - (fetchInput, fetchInit) => fetchWithUndici(fetchInput, fetchInit, proxy), + (fetchInput, fetchInit) => + fetchWithUndici(fetchInput, fetchInit, proxy, ignoreSSLErrors), input, { ...retryOptions, proxy: { type: 'direct' } }, ); @@ -1669,6 +1682,7 @@ function readSocksProxy( function createAgentOptions( base: ResolvedUndiciDispatcherOptions, settings: ResolvedHttpProxySettings, + ignoreSSLErrors: boolean, ): Agent.Options { const options: Agent.Options = {}; @@ -1689,7 +1703,7 @@ function createAgentOptions( if (base.requestCA !== undefined) { connect.ca = base.requestCA; } - if (!settings.proxyStrictSSL) { + if (ignoreSSLErrors || !settings.proxyStrictSSL) { connect.rejectUnauthorized = false; } if (hasOwnProperties(connect)) { @@ -1868,13 +1882,14 @@ function createSocksProxyDispatcher( base: ResolvedUndiciDispatcherOptions, settings: ResolvedHttpProxySettings, protocol: SocksProxyProtocol, + ignoreSSLErrors: boolean, ): Dispatcher { const proxy = settings.proxy; if (proxy === undefined) { - return new Agent(createAgentOptions(base, settings)); + return new Agent(createAgentOptions(base, settings, ignoreSSLErrors)); } - const agentOptions = createAgentOptions(base, settings); + const agentOptions = createAgentOptions(base, settings, ignoreSSLErrors); const socksProxy = readSocksProxy( proxy, protocol, @@ -1891,6 +1906,7 @@ function createSocksProxyDispatcher( function createEnvProxyDispatcher( originalDispatcher: Dispatcher | undefined, settings: ResolvedHttpProxySettings, + ignoreSSLErrors: boolean, ): Dispatcher { const base = originalDispatcher === undefined @@ -1924,6 +1940,7 @@ function createEnvProxyDispatcher( proxyCA: base.proxyCA ? 'custom' : '', proxyProtocol: proxyProtocol ?? '', proxyStrictSSL: settings.proxyStrictSSL, + ignoreSSLErrors, requestCA: base.requestCA ? 'custom' : '', }); @@ -1943,7 +1960,12 @@ function createEnvProxyDispatcher( } if (isSocksProxy) { - const dispatcher = createSocksProxyDispatcher(base, settings, proxyProtocol); + const dispatcher = createSocksProxyDispatcher( + base, + settings, + proxyProtocol, + ignoreSSLErrors, + ); dispatcherCache.set(signature, dispatcher); return dispatcher; } @@ -1966,7 +1988,7 @@ function createEnvProxyDispatcher( if (base.requestCA !== undefined) { connect.ca = base.requestCA; } - if (!settings.proxyStrictSSL) { + if (ignoreSSLErrors || !settings.proxyStrictSSL) { connect.rejectUnauthorized = false; } if (hasOwnProperties(connect)) { @@ -1977,7 +1999,7 @@ function createEnvProxyDispatcher( if (base.requestCA !== undefined) { requestTls.ca = base.requestCA; } - if (!settings.proxyStrictSSL) { + if (ignoreSSLErrors || !settings.proxyStrictSSL) { requestTls.rejectUnauthorized = false; } if (hasOwnProperties(requestTls)) { @@ -2004,13 +2026,34 @@ function createEnvProxyDispatcher( function getUndiciInitWithProxySupport( init?: RequestInitWithDispatcher, proxyConfig?: ProxyConfig, + ignoreSSLErrors = false, ): RequestInitWithDispatcher | undefined { const settings = getConfiguredHttpProxySettings(proxyConfig); if (settings.proxySupport === 'off') { - return init; + if (!ignoreSSLErrors) { + return init; + } + const base = + init?.dispatcher === undefined + ? {} + : getDispatcherOptions(init.dispatcher); + return { + ...init, + dispatcher: new Agent(createAgentOptions(base, settings, true)), + }; } if (proxyConfig?.type === 'direct') { - return init; + if (!ignoreSSLErrors) { + return init; + } + const base = + init?.dispatcher === undefined + ? {} + : getDispatcherOptions(init.dispatcher); + return { + ...init, + dispatcher: new Agent(createAgentOptions(base, settings, true)), + }; } if (proxyConfig?.type === 'custom' && settings.proxy === undefined) { return init; @@ -2019,7 +2062,11 @@ function getUndiciInitWithProxySupport( // The dispatcher in this extension is used for timeout behavior, not as an // opt-out from VS Code proxy settings. Keep proxy support enabled unless the // user explicitly disables it with `http.proxySupport: off`. - const dispatcher = createEnvProxyDispatcher(init?.dispatcher, settings); + const dispatcher = createEnvProxyDispatcher( + init?.dispatcher, + settings, + ignoreSSLErrors, + ); if (dispatcher === init?.dispatcher) { return init; } @@ -2164,6 +2211,7 @@ function fetchWithUndici( input: RequestInfo | URL, init?: RequestInitWithDispatcher, proxyConfig?: ProxyConfig, + ignoreSSLErrors = false, ): Promise { if (typeof Request !== 'undefined' && input instanceof Request) { throw new TypeError('fetchWithRetry does not support Request input'); @@ -2179,7 +2227,9 @@ function fetchWithUndici( return undiciFetch( input, - toUndiciRequestInit(getUndiciInitWithProxySupport(init, proxyConfig)), + toUndiciRequestInit( + getUndiciInitWithProxySupport(init, proxyConfig, ignoreSSLErrors), + ), ).then(adaptUndiciResponse); } @@ -2188,8 +2238,14 @@ export async function fetchWithRetryUsingFetch( input: RequestInfo | URL, options: FetchWithRetryOptions = {}, ): Promise { - const { retryConfig, logger, connectionTimeoutMs, proxy, ...fetchOptions } = - options; + const { + retryConfig, + logger, + connectionTimeoutMs, + proxy, + ignoreSSLErrors, + ...fetchOptions + } = options; const maxRetries = retryConfig?.maxRetries ?? DEFAULT_NORMAL_RETRY_CONFIG.maxRetries; const initialDelayMs = @@ -2263,6 +2319,7 @@ export async function fetchWithRetryUsingFetch( signal: timeoutController.signal, }, proxy, + ignoreSSLErrors, ); const response = await fetcher(input, requestInit); diff --git a/test/unit/balance-manager-auth-snapshot.test.ts b/test/unit/balance-manager-auth-snapshot.test.ts index a133307c..c8d4f6b4 100644 --- a/test/unit/balance-manager-auth-snapshot.test.ts +++ b/test/unit/balance-manager-auth-snapshot.test.ts @@ -7,6 +7,10 @@ const state = vi.hoisted(() => ({ })); vi.mock('vscode', () => { + class ThemeIcon { + constructor(readonly id: string) {} + } + class EventEmitter { private readonly listeners = new Set<(value: T) => void>(); readonly event = (listener: (value: T) => void) => { @@ -25,7 +29,11 @@ vi.mock('vscode', () => { return { EventEmitter, + ThemeIcon, l10n: { t: (message: string) => message }, + workspace: { + getConfiguration: () => ({ get: () => undefined }), + }, window: { showErrorMessage: vi.fn() }, }; }); diff --git a/test/unit/official-models-manager.test.ts b/test/unit/official-models-manager.test.ts index 14c23a38..536bdcf6 100644 --- a/test/unit/official-models-manager.test.ts +++ b/test/unit/official-models-manager.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const providerClient = vi.hoisted(() => ({ getAvailableModels: vi.fn(), + configs: [] as ProviderConfig[], })); vi.mock('vscode', () => { @@ -48,9 +49,12 @@ vi.mock('../../src/main-instance', () => ({ })); vi.mock('../../src/client/utils', () => ({ - createProvider: () => ({ - getAvailableModels: providerClient.getAvailableModels, - }), + createProvider: (config: ProviderConfig) => { + providerClient.configs.push(config); + return { + getAvailableModels: providerClient.getAvailableModels, + }; + }, })); vi.mock('../../src/utils', () => ({ @@ -170,9 +174,25 @@ function secretStore(): SecretStore { beforeEach(() => { providerClient.getAvailableModels.mockReset(); + providerClient.configs.length = 0; }); describe('official model manager provider boundary', () => { + it('preserves ignoreSSLErrors when fetching models for a draft', async () => { + providerClient.getAvailableModels.mockResolvedValue([]); + const manager = new OfficialModelsManager(); + + await manager.getOfficialModelsForDraft('draft-ssl', { + type: 'openai-responses', + name: 'Self-signed API', + baseUrl: 'https://127.0.0.1:9943', + ignoreSSLErrors: true, + }); + + expect(providerClient.configs.at(-1)?.ignoreSSLErrors).toBe(true); + manager.dispose(); + }); + it('stores only the models returned through the common Provider API', async () => { providerClient.getAvailableModels.mockResolvedValue([ { id: 'zeta-cloud' },