basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2026 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7/**8 * API Preconnect - Warm API connections to reduce TCP+TLS handshake latency9 *10 * Principle: Fire a fire-and-forget HEAD request early in startup to warm11 * the TCP+TLS connection. Subsequent actual API calls reuse this connection,12 * saving 100-200ms.13 *14 * The preconnect uses the same shared undici dispatcher as the SDK clients,15 * ensuring the warmed TCP+TLS connection is reused by subsequent API calls.16 */17 18import {19 createDebugLogger,20 detectRuntime,21 getAllProviderBaseUrls,22 getOrCreateSharedDispatcher,23 redactProxyCredentials,24} from '@qwen-code/qwen-code-core';25import { fetch as undiciFetch } from 'undici';26 27const debugLogger = createDebugLogger('PRECONNECT');28 29let preconnectFired = false;30 31/**32 * Default API base URLs by AuthType.33 */34const DEFAULT_BASE_URLS: Record<string, string> = {35 openai: 'https://api.openai.com',36 'qwen-oauth': 'https://coding.dashscope.aliyuncs.com',37 anthropic: 'https://api.anthropic.com',38 dashscope: 'https://dashscope.aliyuncs.com',39};40 41/**42 * All known default base URLs, including all registered provider endpoints.43 * Used by isDefaultBaseUrl() to accept any supported default endpoint.44 */45const ALL_DEFAULT_URLS: string[] = [46 ...Object.values(DEFAULT_BASE_URLS),47 ...getAllProviderBaseUrls(),48];49 50/**51 * Check if preconnect should be skipped due to environment conditions52 */53function shouldSkipPreconnect(): boolean {54 // Skip for custom CA certificate (enterprise TLS inspection may interfere)55 if (process.env['NODE_EXTRA_CA_CERTS']) {56 debugLogger.debug('Skipping preconnect: custom CA certificate configured');57 return true;58 }59 60 return false;61}62 63/**64 * Check if running in sandbox mode65 * In sandbox mode, preconnect is ineffective because the process will restart66 */67function isInSandboxMode(): boolean {68 return process.env['SANDBOX'] !== undefined;69}70 71/**72 * Check if baseUrl is a default URL73 */74function isDefaultBaseUrl(baseUrl: string): boolean {75 const normalizedInput = baseUrl76 .toLowerCase()77 .replace(/^https?:\/\//, '')78 .replace(/\/+$/, '');79 return ALL_DEFAULT_URLS.some((defaultUrl) => {80 const normalizedDefault = defaultUrl81 .toLowerCase()82 .replace(/^https?:\/\//, '')83 .replace(/\/+$/, '');84 return (85 normalizedInput === normalizedDefault ||86 normalizedInput.startsWith(normalizedDefault + '/')87 );88 });89}90 91/**92 * Get the target URL for preconnect.93 * Uses the already-resolved base URL from the model config, falling back94 * to default URLs by authType.95 *96 * Only preconnects to known default URLs — custom URLs may not accept HEAD97 * requests or may require mTLS / private deployment configurations.98 */99function getPreconnectTargetUrl(100 authType: string | undefined,101 resolvedBaseUrl: string | undefined,102): string | undefined {103 // 1. Use the resolved base URL from model config (already incorporates104 // modelProviders > cli > env > settings priority chain)105 if (resolvedBaseUrl && /^https?:\/\//i.test(resolvedBaseUrl)) {106 if (isDefaultBaseUrl(resolvedBaseUrl)) {107 return resolvedBaseUrl;108 }109 debugLogger.debug(110 'Skipping preconnect: resolved baseUrl is not a default URL',111 );112 return undefined;113 }114 115 // 2. Fall back to default value by authType116 if (authType && DEFAULT_BASE_URLS[authType]) {117 return DEFAULT_BASE_URLS[authType];118 }119 120 return undefined;121}122 123/**124 * Execute API preconnect125 * Use HEAD request to establish TCP+TLS connection without sending actual request body.126 * Uses the shared undici dispatcher to ensure connection pool is shared with SDK clients.127 *128 * @param authType - Authentication type (openai, qwen-oauth, anthropic, etc.)129 * @param options - Configuration options130 */131export function preconnectApi(132 authType: string | undefined,133 options: {134 resolvedBaseUrl?: string;135 proxy?: string;136 } = {},137): void {138 if (preconnectFired) {139 return;140 }141 142 // Check if disabled143 if (process.env['QWEN_CODE_DISABLE_PRECONNECT'] === '1') {144 debugLogger.debug('Preconnect disabled by environment variable');145 preconnectFired = true;146 return;147 }148 149 // Check if in sandbox mode (process will restart, preconnect is ineffective)150 if (isInSandboxMode()) {151 debugLogger.debug('Skipping preconnect: sandbox mode detected');152 preconnectFired = true;153 return;154 }155 156 // Check environment skip conditions (custom CA)157 if (shouldSkipPreconnect()) {158 preconnectFired = true;159 return;160 }161 162 // Skip on non-Node runtimes (e.g. Bun) — they use independent connection163 // pools, so warming undici's pool provides no benefit.164 if (detectRuntime() !== 'node') {165 debugLogger.debug('Skipping preconnect: unsupported runtime');166 preconnectFired = true;167 return;168 }169 170 // Skip dispatcher creation when no proxy configured - SDK uses built-in fetch171 // with its own connection pool, so warming undici dispatcher provides no benefit.172 // This mirrors the logic in buildFetchOptionsWithDispatcher() which also skips173 // custom dispatcher creation when no proxy is set, ensuring consistent behavior.174 if (!options.proxy) {175 debugLogger.debug('Skipping preconnect dispatcher: no proxy configured');176 return;177 }178 const proxy = options.proxy;179 180 const targetUrl = getPreconnectTargetUrl(authType, options.resolvedBaseUrl);181 182 if (!targetUrl) {183 debugLogger.debug('No target URL for preconnect');184 return;185 }186 187 // Mark as fired before async operation — prevents duplicate fires.188 // If the request fails, we don't retry (fire-and-forget semantics).189 preconnectFired = true;190 debugLogger.debug(`Preconnecting to: ${targetUrl}`);191 192 try {193 // Use the same shared undici dispatcher that SDK clients will use,194 // so the warmed TCP+TLS connection is reused by subsequent API calls.195 const dispatcher = getOrCreateSharedDispatcher(proxy);196 197 // Fire HEAD request to warm connection (fire-and-forget).198 // Use undici's own fetch (not Node's built-in fetch) so the dispatcher199 // and fetch come from the same undici version — Node's bundled undici200 // may differ in major version from the bundled one (e.g. v8 vs v6),201 // causing handler-interface mismatches like `invalid onError method`.202 undiciFetch(targetUrl, {203 method: 'HEAD',204 signal: AbortSignal.timeout(5_000),205 headers: {206 'User-Agent': 'QwenCode-Preconnect/1.0',207 },208 dispatcher,209 })210 .then(() => {211 debugLogger.debug('Preconnect completed');212 })213 .catch((error) => {214 const redactedError = redactProxyCredentials(String(error));215 debugLogger.debug(`Preconnect failed (ignored): ${redactedError}`);216 });217 } catch (error) {218 // Preconnect failure doesn't affect main flow219 const redactedError = redactProxyCredentials(String(error));220 debugLogger.debug(`Preconnect failed (ignored): ${redactedError}`);221 }222}223 224/**225 * Reset preconnect state (for testing only)226 * @internal227 */228export function resetPreconnectState(): void {229 preconnectFired = false;230}231 