#!/usr/bin/env node
import { A as getEnvBool, M as getEnvInt, N as getEnvString, R as cliState_default, d as logRequestResponse, f as logger_default, y as sanitizeUrl } from "./logger-BwEqwbvj.js";
import { t as invariant } from "./invariant-D-_Lf4ut.js";
import { n as sleep } from "./time-TE7fHEGt.js";
import { i as cloudConfig, n as CLOUD_API_HOST } from "./cloud-BVXcJxjl.js";
import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
import fs from "fs";
import path from "path";
import yaml from "js-yaml";
import { getProxyForUrl } from "proxy-from-env";
import { promisify } from "util";
import { gzip } from "zlib";

//#region src/version.ts
/**
* Application version from package.json.
* Injected at build time, or read from npm environment in development.
*/
const VERSION = "0.120.6";
/**
* PostHog analytics key.
* Only populated during production builds via PROMPTFOO_POSTHOG_KEY env var.
* Empty string in development/test.
*/
const POSTHOG_KEY = "phc_E5n5uHnDo2eREJL1uqX1cIlbkoRby4yFWt3V94HqRRg";
/**
* Node.js engine requirements from package.json.
* Used for version checking at startup.
*/
const ENGINES = { node: ">=20.0.0" };

//#endregion
//#region src/constants.ts
const DEFAULT_QUERY_LIMIT = 100;
const DEFAULT_MAX_CONCURRENCY = 4;
const DEFAULT_API_BASE_URL = "https://api.promptfoo.app";
function getShareApiBaseUrl() {
	return getEnvString("NEXT_PUBLIC_PROMPTFOO_REMOTE_API_BASE_URL") || getEnvString("NEXT_PUBLIC_PROMPTFOO_BASE_URL") || getEnvString("PROMPTFOO_REMOTE_API_BASE_URL") || DEFAULT_API_BASE_URL;
}
function getDefaultShareViewBaseUrl() {
	return getEnvString("PROMPTFOO_SHARING_APP_BASE_URL", `https://promptfoo.app`);
}
function getShareViewBaseUrl() {
	return getEnvString("NEXT_PUBLIC_PROMPTFOO_BASE_URL") || getEnvString("PROMPTFOO_REMOTE_APP_BASE_URL") || getDefaultShareViewBaseUrl();
}
function getDefaultPort() {
	return getEnvInt("API_PORT", 15500);
}
const TERMINAL_MAX_WIDTH = process?.stdout?.isTTY && process?.stdout?.columns && process?.stdout?.columns > 10 ? process?.stdout?.columns - 10 : 120;
const CLOUD_PROVIDER_PREFIX = "promptfoo://provider/";
const FILE_METADATA_KEY = "_promptfooFileMetadata";
const CONSENT_ENDPOINT = "https://api.promptfoo.dev/consent";
const EVENTS_ENDPOINT = "https://a.promptfoo.app";
const R_ENDPOINT = "https://r.promptfoo.app/";

//#endregion
//#region src/providers/shared.ts
/**
* The default timeout for API requests in milliseconds.
*/
const REQUEST_TIMEOUT_MS = getEnvInt("REQUEST_TIMEOUT_MS", 3e5);
/**
* Calculates the cost of an API call based on the model and token usage.
*
* @param {string} modelName The name of the model used.
* @param {ProviderConfig} config The provider configuration.
* @param {number | undefined} promptTokens The number of tokens in the prompt.
* @param {number | undefined} completionTokens The number of tokens in the completion.
* @param {ProviderModel[]} models An array of available models with their costs.
* @returns {number | undefined} The calculated cost, or undefined if it can't be calculated.
*/
function calculateCost(modelName, config, promptTokens, completionTokens, models) {
	if (!Number.isFinite(promptTokens) || !Number.isFinite(completionTokens) || typeof promptTokens === "undefined" || typeof completionTokens === "undefined") return;
	const model = models.find((m) => m.id === modelName);
	if (!model || !model.cost) return;
	const inputCost = config.cost ?? model.cost.input;
	const outputCost = config.cost ?? model.cost.output;
	return inputCost * promptTokens + outputCost * completionTokens || void 0;
}
/**
* Checks if a string looks like it's attempting to be JSON.
* This helps distinguish between actual JSON attempts and plain text that happens to start/end with brackets.
*/
function looksLikeJson(prompt) {
	const trimmed = prompt.trim();
	if (trimmed.startsWith("{") && trimmed.endsWith("}")) return true;
	if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
		const afterBracket = trimmed.slice(1).trimStart();
		if (afterBracket.startsWith("\"") || afterBracket.startsWith("{") || afterBracket.startsWith("[") || /^[\d-]/.test(afterBracket) || /^(true|false|null)/.test(afterBracket)) return true;
		if (afterBracket.length === 0 || /^\s+$/.test(afterBracket)) return true;
		return false;
	}
	return false;
}
/**
* Parses a chat prompt string into a structured format.
*
* @template T The expected return type of the parsed prompt.
* @param {string} prompt The input prompt string to parse.
* @param {T} defaultValue The default value to return if parsing fails.
* @returns {T} The parsed prompt or the default value.
* @throws {Error} If the prompt is invalid YAML or JSON (when required).
*/
function parseChatPrompt(prompt, defaultValue) {
	const trimmedPrompt = prompt.trim();
	if (trimmedPrompt.startsWith("- role:")) try {
		return yaml.load(prompt);
	} catch (err) {
		throw new Error(`Chat Completion prompt is not a valid YAML string: ${err}\n\n${prompt}`);
	}
	else try {
		return JSON.parse(prompt);
	} catch (err) {
		if (getEnvBool("PROMPTFOO_REQUIRE_JSON_PROMPTS") || looksLikeJson(trimmedPrompt)) throw new Error(`Chat Completion prompt is not a valid JSON string: ${err}\n\n${prompt}`);
		return defaultValue;
	}
}
/**
* Converts a string to title case.
*
* @param {string} str The input string to convert.
* @returns {string} The input string converted to title case.
*/
function toTitleCase(str) {
	return str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
}
function isPromptfooSampleTarget(provider) {
	const url = provider.config?.url;
	return url?.includes("promptfoo.app") || url?.includes("promptfoo.dev");
}

//#endregion
//#region src/util/fetch/monkeyPatchFetch.ts
const gzipAsync = promisify(gzip);
function isConnectionError(error) {
	return error instanceof TypeError && error.message === "fetch failed" && error.cause?.stack?.includes("internalConnectMultiple");
}
/**
* Enhanced fetch wrapper that adds logging, authentication, error handling, and optional compression
*/
async function monkeyPatchFetch(url, options) {
	const NO_LOG_URLS = [
		R_ENDPOINT,
		CONSENT_ENDPOINT,
		EVENTS_ENDPOINT
	];
	const isSilent = (options?.headers || {})["x-promptfoo-silent"] === "true";
	const logEnabled = !NO_LOG_URLS.some((logUrl) => url.toString().startsWith(logUrl)) && !isSilent;
	const opts = { ...options };
	const originalBody = opts.body;
	if (options?.compress && opts.body && typeof opts.body === "string") try {
		opts.body = await gzipAsync(opts.body);
		opts.headers = {
			...opts.headers || {},
			"Content-Encoding": "gzip"
		};
	} catch (e) {
		logger_default.warn(`Failed to compress request body: ${e}`);
	}
	if (typeof url === "string" && url.startsWith(CLOUD_API_HOST) || url instanceof URL && url.host === CLOUD_API_HOST.replace(/^https?:\/\//, "")) {
		const token = cloudConfig.getApiKey();
		opts.headers = {
			...opts.headers || {},
			...token ? { Authorization: `Bearer ${token}` } : {}
		};
	}
	try {
		const response = await fetch(url, opts);
		if (logEnabled) logRequestResponse({
			url: url.toString(),
			requestBody: originalBody,
			requestMethod: opts.method || "GET",
			response
		});
		return response;
	} catch (e) {
		if (logEnabled) {
			logRequestResponse({
				url: url.toString(),
				requestBody: opts.body,
				requestMethod: opts.method || "GET",
				response: null
			});
			if (isConnectionError(e)) {
				logger_default.debug(`Connection error, please check your network connectivity to the host: ${url} ${process.env.HTTP_PROXY || process.env.HTTPS_PROXY ? `or Proxy: ${process.env.HTTP_PROXY || process.env.HTTPS_PROXY}` : ""}`);
				throw e;
			}
			logger_default.debug(`Error in fetch: ${JSON.stringify(e, Object.getOwnPropertyNames(e), 2)} ${e instanceof Error ? e.stack : ""}`);
		}
		throw e;
	}
}

//#endregion
//#region src/util/fetch/index.ts
async function fetchWithProxy(url, options = {}, abortSignal) {
	let finalUrl = url;
	let finalUrlString;
	if (typeof url === "string") finalUrlString = url;
	else if (url instanceof URL) finalUrlString = url.toString();
	else if (url instanceof Request) finalUrlString = url.url;
	if (!finalUrlString) throw new Error("Invalid URL");
	const combinedSignal = abortSignal ? options.signal ? AbortSignal.any([options.signal, abortSignal]) : abortSignal : options.signal;
	const finalOptions = {
		...options,
		headers: {
			...options.headers,
			"x-promptfoo-version": VERSION
		},
		signal: combinedSignal
	};
	if (typeof url === "string") try {
		const parsedUrl = new URL(url);
		if (parsedUrl.username || parsedUrl.password) {
			if (finalOptions.headers && "Authorization" in finalOptions.headers) logger_default.warn("Both URL credentials and Authorization header present - URL credentials will be ignored");
			else {
				const username = parsedUrl.username || "";
				const password = parsedUrl.password || "";
				const credentials = Buffer.from(`${username}:${password}`).toString("base64");
				finalOptions.headers = {
					...finalOptions.headers,
					Authorization: `Basic ${credentials}`
				};
			}
			parsedUrl.username = "";
			parsedUrl.password = "";
			finalUrl = parsedUrl.toString();
			finalUrlString = finalUrl.toString();
		}
	} catch (e) {
		logger_default.debug(`URL parsing failed in fetchWithProxy: ${e}`);
	}
	const tlsOptions = { rejectUnauthorized: !getEnvBool("PROMPTFOO_INSECURE_SSL", true) };
	const caCertPath = getEnvString("PROMPTFOO_CA_CERT_PATH");
	if (caCertPath) try {
		const resolvedPath = path.resolve(cliState_default.basePath || "", caCertPath);
		tlsOptions.ca = fs.readFileSync(resolvedPath, "utf8");
		logger_default.debug(`Using custom CA certificate from ${resolvedPath}`);
	} catch (e) {
		logger_default.warn(`Failed to read CA certificate from ${caCertPath}: ${e}`);
	}
	const proxyUrl = finalUrlString ? getProxyForUrl(finalUrlString) : "";
	if (proxyUrl) {
		logger_default.debug(`Using proxy: ${sanitizeUrl(proxyUrl)}`);
		setGlobalDispatcher(new ProxyAgent({
			uri: proxyUrl,
			proxyTls: tlsOptions,
			requestTls: tlsOptions,
			headersTimeout: REQUEST_TIMEOUT_MS
		}));
	} else setGlobalDispatcher(new Agent({ headersTimeout: REQUEST_TIMEOUT_MS }));
	return await monkeyPatchFetch(finalUrl, finalOptions);
}
function fetchWithTimeout(url, options = {}, timeout) {
	return new Promise((resolve$1, reject) => {
		const timeoutController = new AbortController();
		const signal = options.signal ? AbortSignal.any([options.signal, timeoutController.signal]) : timeoutController.signal;
		const timeoutId = setTimeout(() => {
			timeoutController.abort();
			reject(/* @__PURE__ */ new Error(`Request timed out after ${timeout} ms`));
		}, timeout);
		fetchWithProxy(url, {
			...options,
			signal
		}).then((response) => {
			clearTimeout(timeoutId);
			resolve$1(response);
		}).catch((error) => {
			clearTimeout(timeoutId);
			reject(error);
		});
	});
}
/**
* Check if a response indicates rate limiting
*/
function isRateLimited(response) {
	invariant(response.headers, "Response headers are missing");
	invariant(response.status, "Response status is missing");
	return response.headers.get("X-RateLimit-Remaining") === "0" || response.status === 429 || response.headers.get("x-ratelimit-remaining-requests") === "0" || response.headers.get("x-ratelimit-remaining-tokens") === "0";
}
/**
* Handle rate limiting by waiting the appropriate amount of time
*/
async function handleRateLimit(response) {
	const rateLimitReset = response.headers.get("X-RateLimit-Reset");
	const retryAfter = response.headers.get("Retry-After");
	const openaiReset = response.headers.get("x-ratelimit-reset-requests") || response.headers.get("x-ratelimit-reset-tokens");
	let waitTime = 6e4;
	if (openaiReset) waitTime = Math.max(Number.parseInt(openaiReset) * 1e3, 0);
	else if (rateLimitReset) {
		const resetTime = /* @__PURE__ */ new Date(Number.parseInt(rateLimitReset) * 1e3);
		const now = /* @__PURE__ */ new Date();
		waitTime = Math.max(resetTime.getTime() - now.getTime() + 1e3, 0);
	} else if (retryAfter) waitTime = Number.parseInt(retryAfter) * 1e3;
	logger_default.debug(`Rate limited, waiting ${waitTime}ms before retry`);
	await sleep(waitTime);
}
async function fetchWithRetries(url, options = {}, timeout, maxRetries) {
	maxRetries = Math.max(0, maxRetries ?? 4);
	let lastErrorMessage;
	const backoff = getEnvInt("PROMPTFOO_REQUEST_BACKOFF_MS", 5e3);
	for (let i = 0; i <= maxRetries; i++) {
		let response;
		try {
			response = await fetchWithTimeout(url, options, timeout);
			if (getEnvBool("PROMPTFOO_RETRY_5XX") && response.status >= 500 && response.status < 600) throw new Error(`Internal Server Error: ${response.status} ${response.statusText}`);
			if (response && isRateLimited(response)) {
				logger_default.debug(`Rate limited on URL ${url}: ${response.status} ${response.statusText}, attempt ${i + 1}/${maxRetries + 1}, waiting before retry...`);
				lastErrorMessage = `Rate limited: ${response.status} ${response.statusText}`;
				await handleRateLimit(response);
				continue;
			}
			return response;
		} catch (error) {
			if (error instanceof Error && error.name === "AbortError") throw error;
			let errorMessage;
			if (error instanceof Error) {
				const typedError = error;
				errorMessage = `${typedError.name}: ${typedError.message}`;
				if (typedError.cause) errorMessage += ` (Cause: ${typedError.cause})`;
				if (typedError.code) errorMessage += ` (Code: ${typedError.code})`;
			} else errorMessage = String(error);
			logger_default.debug(`Request to ${url} failed (attempt #${i + 1}), retrying: ${errorMessage}`);
			if (i < maxRetries) await sleep(Math.pow(2, i) * (backoff + 1e3 * Math.random()));
			lastErrorMessage = errorMessage;
		}
	}
	throw new Error(`Request failed after ${maxRetries} retries: ${lastErrorMessage}`);
}

//#endregion
export { ENGINES as C, getShareViewBaseUrl as S, VERSION as T, R_ENDPOINT as _, isRateLimited as a, getDefaultShareViewBaseUrl as b, isPromptfooSampleTarget as c, CLOUD_PROVIDER_PREFIX as d, CONSENT_ENDPOINT as f, FILE_METADATA_KEY as g, EVENTS_ENDPOINT as h, handleRateLimit as i, parseChatPrompt as l, DEFAULT_QUERY_LIMIT as m, fetchWithRetries as n, REQUEST_TIMEOUT_MS as o, DEFAULT_MAX_CONCURRENCY as p, fetchWithTimeout as r, calculateCost as s, fetchWithProxy as t, toTitleCase as u, TERMINAL_MAX_WIDTH as v, POSTHOG_KEY as w, getShareApiBaseUrl as x, getDefaultPort as y };
//# sourceMappingURL=fetch-DHKTlqnO.js.map