#!/usr/bin/env node
import { C as getEnvString, O as cliState_default, S as getEnvInt, a as logRequestResponse, b as getEnvBool, o as logger_default, u as sanitizeUrl } from "./logger-Bzi5o47S.js";
import { t as invariant } from "./invariant-DT20jrBd.js";
import { n as sleep } from "./time-CoOwN3kQ.js";
import { i as cloudConfig, n as CLOUD_API_HOST } from "./cloud-CZHXB86j.js";
import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
import path from "path";
import yaml from "js-yaml";
import * as fsPromises from "fs/promises";
import { getProxyForUrl } from "proxy-from-env";
import { promisify } from "util";
import { gzip } from "zlib";

//#region src/providers/constants.ts
const FILE_METADATA_KEY = "_promptfooFileMetadata";
/**
* Identifier for manual user ratings in componentResults.
* Used to distinguish human ratings from automated assertions.
*/
const HUMAN_ASSERTION_TYPE = "human";

//#endregion
//#region src/version.ts
/**
* Application version from package.json.
* Injected at build time, or read from npm environment in development.
*/
const VERSION = "0.120.22";
/**
* PostHog analytics key.
* Only populated during production builds via PROMPTFOO_POSTHOG_KEY env var.
* Empty string in development/test.
*/
const POSTHOG_KEY = "phc_E5n5uHnDo2eREJL1uqX1cIlbkoRby4yFWt3V94HqRRg";

//#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("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("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 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);
/**
* Extended timeout for long-running models (deep research, gpt-5-pro, etc.) in milliseconds.
* These models can take significantly longer to respond due to their complex reasoning.
*/
const LONG_RUNNING_MODEL_TIMEOUT_MS = 6e5;
/**
* 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");
}
/**
* Checks if the given value is an OpenAI tool choice format.
* Detects string values ('auto', 'none', 'required') and
* the object form ({ type: 'function', function: { name } }).
*/
function isOpenAIToolChoice(obj) {
	if (typeof obj === "string") return [
		"auto",
		"none",
		"required"
	].includes(obj);
	if (typeof obj === "object" && obj !== null) {
		const candidate = obj;
		if (candidate.type === "function" && typeof candidate.function === "object" && candidate.function !== null) return typeof candidate.function.name === "string";
	}
	return false;
}
/**
* Transforms an OpenAI tool choice to Anthropic format.
*/
function openaiToolChoiceToAnthropic(choice) {
	if (typeof choice === "string") switch (choice) {
		case "auto": return { type: "auto" };
		case "none": return { type: "auto" };
		case "required": return { type: "any" };
	}
	return {
		type: "tool",
		name: choice.function.name
	};
}
/**
* Transforms an OpenAI tool choice to Bedrock Converse format.
*/
function openaiToolChoiceToBedrock(choice) {
	if (typeof choice === "string") switch (choice) {
		case "auto": return { auto: {} };
		case "none": return;
		case "required": return { any: {} };
	}
	return { tool: { name: choice.function.name } };
}
/**
* Transforms an OpenAI tool choice to Google (Gemini) format.
*/
function openaiToolChoiceToGoogle(choice) {
	if (typeof choice === "string") switch (choice) {
		case "auto": return { functionCallingConfig: { mode: "AUTO" } };
		case "none": return { functionCallingConfig: { mode: "NONE" } };
		case "required": return { functionCallingConfig: { mode: "ANY" } };
	}
	return { functionCallingConfig: {
		mode: "ANY",
		allowedFunctionNames: [choice.function.name]
	} };
}
/**
* Transforms an OpenAI tool choice to the specified provider format.
* If the input is not in OpenAI format, it's returned as-is (native passthrough).
*/
function transformToolChoice(toolChoice, format) {
	if (!isOpenAIToolChoice(toolChoice)) return toolChoice;
	switch (format) {
		case "openai": return toolChoice;
		case "anthropic": return openaiToolChoiceToAnthropic(toolChoice);
		case "bedrock": return openaiToolChoiceToBedrock(toolChoice);
		case "google": return openaiToolChoiceToGoogle(toolChoice);
		default: return toolChoice;
	}
}
/**
* Checks if an array contains OpenAI-format tools.
* Returns true if the first tool has `type: 'function'` and `function.name`.
*/
function isOpenAIToolArray(tools) {
	if (!Array.isArray(tools) || tools.length === 0) return false;
	const first = tools[0];
	if (typeof first !== "object" || first === null) return false;
	const candidate = first;
	return candidate.type === "function" && typeof candidate.function === "object" && candidate.function !== null && typeof candidate.function.name === "string";
}
/**
* Transforms OpenAI-format tools to Anthropic format.
*/
function openaiToolsToAnthropic(tools) {
	return tools.map((tool) => ({
		name: tool.function.name,
		...tool.function.description ? { description: tool.function.description } : {},
		input_schema: tool.function.parameters || {
			type: "object",
			properties: {}
		}
	}));
}
/**
* Transforms OpenAI-format tools to Bedrock Converse format.
*/
function openaiToolsToBedrock(tools) {
	return tools.map((tool) => ({ toolSpec: {
		name: tool.function.name,
		...tool.function.description ? { description: tool.function.description } : {},
		inputSchema: { json: tool.function.parameters || {
			type: "object",
			properties: {}
		} }
	} }));
}
/**
* Sanitizes a schema for Google/Gemini compatibility.
* - Converts type strings to uppercase (string → STRING)
* - Removes unsupported properties (additionalProperties, $schema, default)
* - Recursively processes nested schemas
*/
function sanitizeSchemaForGoogle(schema) {
	const result = {};
	for (const [key, value] of Object.entries(schema)) {
		if ([
			"additionalProperties",
			"$schema",
			"default",
			"$id",
			"$ref"
		].includes(key)) continue;
		if (key === "type" && typeof value === "string") result[key] = value.toUpperCase();
		else if (key === "properties" && typeof value === "object" && value !== null) {
			const sanitizedProps = {};
			for (const [propKey, propValue] of Object.entries(value)) if (typeof propValue === "object" && propValue !== null) sanitizedProps[propKey] = sanitizeSchemaForGoogle(propValue);
			else sanitizedProps[propKey] = propValue;
			result[key] = sanitizedProps;
		} else if (key === "items" && typeof value === "object" && value !== null) result[key] = sanitizeSchemaForGoogle(value);
		else result[key] = value;
	}
	return result;
}
/**
* Transforms OpenAI-format tools to Google/Gemini format.
*/
function openaiToolsToGoogle(tools) {
	return [{ functionDeclarations: tools.map((tool) => ({
		name: tool.function.name,
		...tool.function.description ? { description: tool.function.description } : {},
		...tool.function.parameters ? { parameters: sanitizeSchemaForGoogle(tool.function.parameters) } : {}
	})) }];
}
/**
* Transforms tools from OpenAI format to the specified provider format.
* If the input is not in OpenAI format, it's returned as-is.
*/
function transformTools(tools, format) {
	if (!isOpenAIToolArray(tools)) return tools;
	switch (format) {
		case "openai": return tools;
		case "anthropic": return openaiToolsToAnthropic(tools);
		case "bedrock": return openaiToolsToBedrock(tools);
		case "google": return openaiToolsToGoogle(tools);
		default: return tools;
	}
}

//#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 = await fsPromises.readFile(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 }));
	const maxTransientRetries = options.disableTransientRetries ? 0 : 3;
	for (let attempt = 0; attempt <= maxTransientRetries; attempt++) {
		const response = await monkeyPatchFetch(finalUrl, finalOptions);
		if (!options.disableTransientRetries && isTransientError(response) && attempt < maxTransientRetries) {
			const backoffMs = Math.pow(2, attempt) * 1e3;
			logger_default.debug(`Transient error (${response.status} ${response.statusText}), retry ${attempt + 1}/${maxTransientRetries} after ${backoffMs}ms`);
			await sleep(backoffMs);
			continue;
		}
		return response;
	}
	throw new Error("Unexpected end of transient retry loop");
}
function fetchWithTimeout(url, options = {}, timeout) {
	return new Promise((resolve, 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(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);
}
/**
* Check if a response indicates a transient server error that should be retried.
* Matches specific status codes with their expected status text to avoid
* retrying permanent failures (e.g., some APIs return 502 for auth errors).
*/
function isTransientError(response) {
	if (!response?.statusText) return false;
	const statusText = response.statusText.toLowerCase();
	switch (response.status) {
		case 502: return statusText.includes("bad gateway");
		case 503: return statusText.includes("service unavailable");
		case 504: return statusText.includes("gateway timeout");
		default: return false;
	}
}
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,
				disableTransientRetries: true
			}, 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 { POSTHOG_KEY as A, EVENTS_ENDPOINT as C, getDefaultShareViewBaseUrl as D, getDefaultPort as E, FILE_METADATA_KEY as M, HUMAN_ASSERTION_TYPE as N, getShareApiBaseUrl as O, DEFAULT_QUERY_LIMIT as S, TERMINAL_MAX_WIDTH as T, transformToolChoice as _, isRateLimited as a, CONSENT_ENDPOINT as b, REQUEST_TIMEOUT_MS as c, isOpenAIToolChoice as d, isPromptfooSampleTarget as f, toTitleCase as g, parseChatPrompt as h, handleRateLimit as i, VERSION as j, getShareViewBaseUrl as k, calculateCost as l, openaiToolsToBedrock as m, fetchWithRetries as n, isTransientError as o, openaiToolChoiceToBedrock as p, fetchWithTimeout as r, LONG_RUNNING_MODEL_TIMEOUT_MS as s, fetchWithProxy as t, isOpenAIToolArray as u, transformTools as v, R_ENDPOINT as w, DEFAULT_MAX_CONCURRENCY as x, CLOUD_PROVIDER_PREFIX as y };
//# sourceMappingURL=fetch-CruohpfF.js.map