const require_logger = require('./logger-CMp-NS-e.cjs');
const require_invariant = require('./invariant-CKcJAQ6M.cjs');
const require_esm = require('./esm-gWVPXn3r.cjs');
const require_pythonUtils = require('./pythonUtils-rOCm9w_5.cjs');
const require_fileExtensions = require('./fileExtensions-DkJYkWUy.cjs');
const require_types = require('./types-Cixl-wRO.cjs');
const require_fetch = require('./fetch-E3xbPP9w.cjs');
let fs = require("fs");
fs = require_logger.__toESM(fs);
let path = require("path");
path = require_logger.__toESM(path);
let js_yaml = require("js-yaml");
js_yaml = require_logger.__toESM(js_yaml);
let dotenv = require("dotenv");
dotenv = require_logger.__toESM(dotenv);
let os = require("os");
os = require_logger.__toESM(os);
let dedent = require("dedent");
dedent = require_logger.__toESM(dedent);
let fs_promises = require("fs/promises");
fs_promises = require_logger.__toESM(fs_promises);
let glob = require("glob");
let fast_deep_equal = require("fast-deep-equal");
fast_deep_equal = require_logger.__toESM(fast_deep_equal);
let csv_parse_sync = require("csv-parse/sync");
let nunjucks = require("nunjucks");
nunjucks = require_logger.__toESM(nunjucks);
let fast_xml_parser = require("fast-xml-parser");
let csv_stringify_sync = require("csv-stringify/sync");

//#region src/util/provider.ts
function canonicalizeProviderId(id) {
	if (id.startsWith("file://")) {
		const filePath = id.slice(7);
		return path.isAbsolute(filePath) ? id : `file://${path.resolve(filePath)}`;
	}
	for (const prefix of [
		"exec:",
		"python:",
		"golang:"
	]) if (id.startsWith(prefix)) {
		const filePath = id.slice(prefix.length);
		if (filePath.includes("/") || filePath.includes("\\")) return `${prefix}${path.resolve(filePath)}`;
		return id;
	}
	if ((id.endsWith(".js") || id.endsWith(".ts") || id.endsWith(".mjs")) && (id.includes("/") || id.includes("\\"))) return `file://${path.resolve(id)}`;
	return id;
}
function getProviderLabel(provider) {
	return provider?.label && typeof provider.label === "string" ? provider.label : void 0;
}
function providerToIdentifier(provider) {
	if (!provider) return;
	if (typeof provider === "string") return canonicalizeProviderId(provider);
	const label = getProviderLabel(provider);
	if (label) return label;
	if (require_types.isApiProvider(provider)) return canonicalizeProviderId(provider.id());
	if (require_types.isProviderOptions(provider)) {
		if (provider.id) return canonicalizeProviderId(provider.id);
		return;
	}
	if (typeof provider === "object" && "id" in provider && typeof provider.id === "string") return canonicalizeProviderId(provider.id);
}
/**
* Gets a descriptive identifier string for a provider, showing both label and ID when both exist.
* Useful for error messages to help users debug provider reference issues.
*/
function getProviderDescription(provider) {
	const label = provider.label;
	const id = provider.id();
	if (label && label !== id) return `${label} (${id})`;
	return id;
}
/**
* Checks if a provider reference matches a given provider.
* Supports exact matching and wildcard patterns.
*/
function doesProviderRefMatch(ref, provider) {
	const label = provider.label;
	const id = provider.id();
	const canonicalRef = canonicalizeProviderId(ref);
	const canonicalId = canonicalizeProviderId(id);
	if (label && label === ref) return true;
	if (id === ref || canonicalId === canonicalRef) return true;
	if (ref.endsWith("*")) {
		const prefix = ref.slice(0, -1);
		if (label?.startsWith(prefix) || id.startsWith(prefix) || canonicalId.startsWith(prefix)) return true;
	}
	if (label?.startsWith(`${ref}:`) || id.startsWith(`${ref}:`) || canonicalId.startsWith(`${ref}:`)) return true;
	return false;
}
/**
* Checks if a provider is allowed based on a list of allowed references.
*/
function isProviderAllowed(provider, allowedProviders) {
	if (!Array.isArray(allowedProviders)) return true;
	if (allowedProviders.length === 0) return false;
	return allowedProviders.some((ref) => doesProviderRefMatch(ref, provider));
}
/**
* Detects if a provider uses OpenAI models.
* This includes direct OpenAI providers and Azure OpenAI.
*/
function isOpenAiProvider(providerId) {
	const lowerProviderId = providerId.toLowerCase();
	if (lowerProviderId.startsWith("openai:")) return true;
	if (lowerProviderId.startsWith("azureopenai:")) return true;
	if (lowerProviderId.startsWith("azure:")) {
		if ([
			"gpt",
			"openai",
			"davinci",
			"curie",
			"babbage",
			"ada",
			"text-embedding",
			"whisper",
			"dall-e",
			"tts"
		].some((indicator) => lowerProviderId.includes(indicator))) return true;
	}
	return false;
}
/**
* Detects if a provider uses Anthropic/Claude models.
* This includes direct Anthropic providers, Bedrock with Claude, and Vertex with Claude.
*/
function isAnthropicProvider(providerId) {
	const lowerProviderId = providerId.toLowerCase();
	if (lowerProviderId.startsWith("anthropic:")) return true;
	if (lowerProviderId.startsWith("bedrock:")) {
		if (lowerProviderId.includes("claude") || lowerProviderId.includes("anthropic")) return true;
	}
	if (lowerProviderId.startsWith("vertex:")) {
		if (lowerProviderId.includes("claude")) return true;
	}
	return false;
}
/**
* Detects if a provider uses Google models.
* This includes direct Google/Vertex providers with Gemini and other Google models.
* Note: Vertex with Claude models is NOT counted as Google (it's Anthropic).
*/
function isGoogleProvider(providerId) {
	const lowerProviderId = providerId.toLowerCase();
	if (lowerProviderId.startsWith("google:")) return true;
	if (lowerProviderId.startsWith("vertex:")) {
		if (!lowerProviderId.includes("claude")) return true;
	}
	return false;
}

//#endregion
//#region src/util/comparison.ts
/**
* Explicit runtime variable names that don't follow the underscore convention.
* These are added during evaluation but aren't part of the original test definition.
*
* - sessionId: Added by multi-turn strategy providers (GOAT, Crescendo)
*
* Note: Variables starting with underscore (e.g., _conversation) are automatically
* treated as runtime variables and filtered out.
*/
const EXPLICIT_RUNTIME_VAR_KEYS = ["sessionId"];
/**
* Checks if a variable key is a runtime-only variable that should be filtered
* when comparing test cases.
*
* Runtime variables are identified by:
* 1. Starting with underscore (_) - convention for internal/runtime vars
* 2. Being in the explicit runtime var list (for legacy vars like sessionId)
*/
function isRuntimeVar(key) {
	return key.startsWith("_") || EXPLICIT_RUNTIME_VAR_KEYS.includes(key);
}
/**
* Filters out runtime-only variables that are added during evaluation
* but aren't part of the original test definition.
*
* This is used when comparing test cases to determine if a result
* corresponds to a particular test, regardless of runtime state.
*
* Runtime variables are identified by:
* - Starting with underscore (e.g., _conversation, _metadata)
* - Being in the explicit list (e.g., sessionId for backward compatibility)
*/
function filterRuntimeVars(vars) {
	if (!vars) return vars;
	const filtered = {};
	for (const [key, value] of Object.entries(vars)) if (!isRuntimeVar(key)) filtered[key] = value;
	return filtered;
}
function varsMatch(vars1, vars2) {
	return (0, fast_deep_equal.default)(vars1, vars2);
}
/**
* Generate a unique key for a test case for deduplication purposes.
* Excludes runtime variables and includes strategyId to distinguish tests
* with the same prompt but different strategies.
*
* @param testCase - The test case to generate a key for
* @returns A JSON string that uniquely identifies the test case
*/
function getTestCaseDeduplicationKey(testCase) {
	const filteredVars = filterRuntimeVars(testCase.vars);
	const strategyId = testCase.metadata?.strategyId || "none";
	return JSON.stringify({
		vars: filteredVars,
		strategyId
	});
}
/**
* Deduplicates an array of test cases based on their vars and strategyId.
* Tests with the same vars but different strategies are considered different.
* Runtime variables (like _conversation, sessionId) are filtered out before comparison.
*
* @param tests - Array of test cases to deduplicate
* @returns Deduplicated array of test cases
*/
function deduplicateTestCases(tests) {
	const seen = /* @__PURE__ */ new Set();
	return tests.filter((test) => {
		const key = getTestCaseDeduplicationKey(test);
		if (seen.has(key)) return false;
		seen.add(key);
		return true;
	});
}
function resultIsForTestCase(result, testCase) {
	const testProviderId = testCase.provider ? providerToIdentifier(testCase.provider) : void 0;
	const resultProviderId = providerToIdentifier(result.provider);
	const providersMatch = !testProviderId || !resultProviderId || testProviderId === resultProviderId;
	const resultVars = filterRuntimeVars(result.vars);
	const testVars = filterRuntimeVars(testCase.vars);
	const doVarsMatch = varsMatch(testVars, resultVars);
	const isMatch = doVarsMatch && providersMatch;
	if (!isMatch) {
		const varKeys = testVars ? Object.keys(testVars).join(", ") : "none";
		require_logger.logger_default.debug(`[resultIsForTestCase] No match: vars=${doVarsMatch}, providers=${providersMatch}`, {
			testProvider: testProviderId || "none",
			resultProvider: resultProviderId || "none",
			testVarKeys: varKeys
		});
	}
	return isMatch;
}

//#endregion
//#region src/util/env.ts
/**
* Load environment variables from .env file(s).
* @param envPath - Single path, array of paths, or undefined for default .env loading.
*                  When paths are explicitly specified, all files must exist or an error is thrown.
*                  When multiple files are provided, later files override values from earlier files.
*/
function setupEnv(envPath) {
	if (envPath) {
		const paths = (Array.isArray(envPath) ? envPath : [envPath]).flatMap((p) => p.includes(",") ? p.split(",").map((s) => s.trim()) : p.trim()).filter((p) => p.length > 0);
		if (paths.length === 0) {
			dotenv.default.config({ quiet: true });
			return;
		}
		for (const p of paths) if (!fs.existsSync(p)) throw new Error(`Environment file not found: ${p}`);
		if (paths.length === 1) require_logger.logger_default.info(`Loading environment variables from ${paths[0]}`);
		else require_logger.logger_default.info(`Loading environment variables from: ${paths.join(", ")}`);
		const pathArg = paths.length === 1 ? paths[0] : paths;
		dotenv.default.config({
			path: pathArg,
			override: true,
			quiet: true
		});
	} else dotenv.default.config({ quiet: true });
}

//#endregion
//#region src/util/functions/loadFunction.ts
const functionCache = {};
/**
* Loads a function from a JavaScript or Python file
* @param options Options for loading the function
* @returns The loaded function
*/
async function loadFunction({ filePath, functionName, defaultFunctionName = "func", basePath = require_logger.cliState_default.basePath, useCache = true }) {
	const cacheKey = `${filePath}${functionName ? `:${functionName}` : ""}`;
	if (useCache && functionCache[cacheKey]) return functionCache[cacheKey];
	const resolvedPath = basePath ? path.default.resolve(basePath, filePath) : filePath;
	if (!require_fileExtensions.isJavascriptFile(resolvedPath) && !resolvedPath.endsWith(".py")) throw new Error(`File must be a JavaScript (${require_fileExtensions.JAVASCRIPT_EXTENSIONS.join(", ")}) or Python (.py) file`);
	try {
		let func;
		if (require_fileExtensions.isJavascriptFile(resolvedPath)) {
			const module = await require_esm.importModule(resolvedPath, functionName);
			let moduleFunc;
			if (functionName) moduleFunc = module;
			else moduleFunc = typeof module === "function" ? module : module?.default?.default || module?.default || module?.[defaultFunctionName] || module;
			if (typeof moduleFunc !== "function") throw new Error(functionName ? `JavaScript file must export a "${functionName}" function` : `JavaScript file must export a function (as default export or named export "${defaultFunctionName}")`);
			func = moduleFunc;
		} else {
			const result = (...args) => require_pythonUtils.runPython(resolvedPath, functionName || defaultFunctionName, args);
			func = result;
		}
		if (useCache) functionCache[cacheKey] = func;
		return func;
	} catch (err) {
		require_logger.logger_default.error(`Failed to load function: ${err.message}`);
		throw err;
	}
}
/**
* Extracts the file path and function name from a file:// URL
* @param fileUrl The file:// URL (e.g., "file://path/to/file.js:functionName")
* @returns The file path and optional function name
*/
function parseFileUrl(fileUrl) {
	if (!fileUrl.startsWith("file://")) throw new Error("URL must start with file://");
	const urlWithoutProtocol = fileUrl.slice(7);
	const lastColonIndex = urlWithoutProtocol.lastIndexOf(":");
	if (lastColonIndex > 1) return {
		filePath: urlWithoutProtocol.slice(0, lastColonIndex),
		functionName: urlWithoutProtocol.slice(lastColonIndex + 1)
	};
	return { filePath: urlWithoutProtocol };
}

//#endregion
//#region src/util/templates.ts
/**
* Get a Nunjucks engine instance with optional filters and configuration.
* @param filters - Optional map of custom Nunjucks filters.
* @param throwOnUndefined - Whether to throw an error on undefined variables.
* @param isGrader - Whether this engine is being used in a grader context.
* Nunjucks is always enabled in grader mode.
* @returns A configured Nunjucks environment.
*/
function getNunjucksEngine(filters, throwOnUndefined = false, isGrader = false) {
	if (!isGrader && require_logger.getEnvBool("PROMPTFOO_DISABLE_TEMPLATING")) return { renderString: (template) => template };
	const env = nunjucks.default.configure({
		autoescape: false,
		throwOnUndefined
	});
	const envGlobals = {
		...require_logger.getEnvBool("PROMPTFOO_DISABLE_TEMPLATE_ENV_VARS", require_logger.getEnvBool("PROMPTFOO_SELF_HOSTED", false)) ? {} : process.env,
		...require_logger.cliState_default.config?.env
	};
	env.addGlobal("env", envGlobals);
	env.addFilter("load", function(str) {
		return JSON.parse(str);
	});
	if (filters) for (const [name, filter] of Object.entries(filters)) env.addFilter(name, filter);
	return env;
}
/**
* Parse Nunjucks template to extract variables.
* @param template - The Nunjucks template string.
* @returns An array of variables used in the template.
*/
function extractVariablesFromTemplate(template) {
	const variableSet = /* @__PURE__ */ new Set();
	const regex = /\{\{[\s]*([^{}\s|]+)[\s]*(?:\|[^}]+)?\}\}|\{%[\s]*(?:if|for)[\s]+([^{}\s]+)[\s]*.*?%\}/g;
	template = template.replace(/\{#[\s\S]*?#\}/g, "");
	let match;
	while ((match = regex.exec(template)) !== null) {
		const variable = match[1] || match[2];
		if (variable) variableSet.add(variable);
	}
	const forLoopRegex = /\{%[\s]*for[\s]+(\w+)[\s]+in[\s]+(\w+)[\s]*%\}/g;
	while ((match = forLoopRegex.exec(template)) !== null) {
		variableSet.delete(match[1]);
		variableSet.add(match[2]);
	}
	return Array.from(variableSet);
}
/**
* Extract variables from multiple Nunjucks templates.
* @param templates - An array of Nunjucks template strings.
* @returns An array of variables used in the templates.
*/
function extractVariablesFromTemplates(templates) {
	const variableSet = /* @__PURE__ */ new Set();
	for (const template of templates) extractVariablesFromTemplate(template).forEach((variable) => variableSet.add(variable));
	return Array.from(variableSet);
}

//#endregion
//#region src/util/render.ts
/**
* Renders ONLY environment variable templates in an object, leaving all other templates untouched.
* This allows env vars to be resolved at provider load time while preserving runtime var templates.
*
* Supports full Nunjucks syntax for env vars including filters and expressions:
* - {{ env.VAR_NAME }}
* - {{ env['VAR-NAME'] }}
* - {{ env["VAR-NAME"] }}
* - {{ env.VAR | default('fallback') }}
* - {{ env.VAR | upper }}
*
* Preserves non-env templates for runtime rendering:
* - {{ vars.x }} - preserved as literal
* - {{ prompt }} - preserved as literal
*
* Implementation: Uses regex to find env templates, delegates to Nunjucks for rendering.
* This ensures full Nunjucks feature support while preserving non-env templates.
*
* @param obj - The object to process
* @param envOverrides - Optional env vars to merge with (or replace) the base env
* @param replaceBase - If true, envOverrides replaces the base env entirely instead of merging
* @returns The object with only env templates rendered
*/
function renderEnvOnlyInObject(obj, envOverrides, replaceBase) {
	if (require_logger.getEnvBool("PROMPTFOO_DISABLE_TEMPLATING")) return obj;
	if (typeof obj === "string") {
		if (obj.length > 5e4) {
			require_logger.logger_default.warn(`String too long (${obj.length} chars) for template matching. Skipping env var rendering.`);
			return obj;
		}
		const nunjucks = getNunjucksEngine();
		const baseEnvGlobals = nunjucks.getGlobal("env");
		const envGlobals = replaceBase ? envOverrides ?? {} : envOverrides ? {
			...baseEnvGlobals,
			...envOverrides
		} : baseEnvGlobals;
		return obj.replace(/\{\{(?:[^}]|\}(?!\}))*\}\}/g, (match) => {
			if (!match.match(/\benv\.|env\[/)) return match;
			const varMatch = match.match(/env\.(\w+)|env\[['"]([^'"]+)['"]\]/);
			const varName = varMatch?.[1] || varMatch?.[2];
			if (match.includes("|") || varName && varName in envGlobals && envGlobals[varName] !== void 0) try {
				return nunjucks.renderString(match, { env: envGlobals });
			} catch (error) {
				require_logger.logger_default.debug(`Failed to render env template "${match}": ${error instanceof Error ? error.message : String(error)}`);
				return match;
			}
			return match;
		});
	}
	if (Array.isArray(obj)) return obj.map((item) => renderEnvOnlyInObject(item, envOverrides, replaceBase));
	if (typeof obj === "object" && obj !== null) {
		const result = {};
		for (const key in obj) result[key] = renderEnvOnlyInObject(obj[key], envOverrides, replaceBase);
		return result;
	}
	return obj;
}
function renderVarsInObject(obj, vars) {
	if (!vars || require_logger.getEnvBool("PROMPTFOO_DISABLE_TEMPLATING")) return obj;
	if (typeof obj === "string") return getNunjucksEngine().renderString(obj, vars);
	if (Array.isArray(obj)) return obj.map((item) => renderVarsInObject(item, vars));
	if (typeof obj === "object" && obj !== null) {
		const result = {};
		for (const key in obj) result[key] = renderVarsInObject(obj[key], vars);
		return result;
	} else if (typeof obj === "function") return renderVarsInObject(obj({ vars }));
	return obj;
}

//#endregion
//#region src/util/file.ts
/**
* Simple Nunjucks engine specifically for file paths
* This function is separate from the main getNunjucksEngine to avoid circular dependencies
*/
function getNunjucksEngineForFilePath() {
	const env = nunjucks.default.configure({ autoescape: false });
	env.addGlobal("env", {
		...process.env,
		...require_logger.cliState_default.config?.env
	});
	return env;
}
/**
* Loads content from an external file if the input is a file path, otherwise
* returns the input as-is. Supports Nunjucks templating for file paths.
*
* @param filePath - The input to process. Can be a file path string starting with "file://",
* an array of file paths, or any other type of data.
* @param context - Optional context to control file loading behavior. 'assertion' context
* preserves Python/JS file references instead of loading their content.
* @returns The loaded content if the input was a file path, otherwise the original input.
* For JSON and YAML files, the content is parsed into an object.
* For other file types, the raw file content is returned as a string.
*
* @throws {Error} If the specified file does not exist.
*/
function maybeLoadFromExternalFile(filePath, context) {
	if (Array.isArray(filePath)) return filePath.map((path) => {
		return maybeLoadFromExternalFile(path, context);
	});
	if (typeof filePath !== "string") return filePath;
	if (!filePath.startsWith("file://")) return filePath;
	const renderedFilePath = getNunjucksEngineForFilePath().renderString(filePath, {});
	const { filePath: cleanPath, functionName } = parseFileUrl(renderedFilePath);
	if (context === "assertion" && (cleanPath.endsWith(".py") || require_fileExtensions.isJavascriptFile(cleanPath))) {
		require_logger.logger_default.debug(`Preserving Python/JS file reference in assertion context: ${renderedFilePath}`);
		return renderedFilePath;
	}
	if (context === "vars") {
		require_logger.logger_default.debug(`Preserving file reference in vars context: ${renderedFilePath}`);
		return renderedFilePath;
	}
	if (functionName && (cleanPath.endsWith(".py") || require_fileExtensions.isJavascriptFile(cleanPath))) return renderedFilePath;
	const pathToUse = functionName && !(cleanPath.endsWith(".py") || require_fileExtensions.isJavascriptFile(cleanPath)) ? renderedFilePath.slice(7) : cleanPath;
	const resolvedPath = path.resolve(require_logger.cliState_default.basePath || "", pathToUse);
	if ((0, glob.hasMagic)(pathToUse)) {
		const matchedFiles = (0, glob.globSync)(resolvedPath, { windowsPathsNoEscape: true });
		if (matchedFiles.length === 0) throw new Error(`No files found matching pattern: ${resolvedPath}`);
		const allContents = [];
		for (const matchedFile of matchedFiles) {
			let contents;
			try {
				contents = fs.readFileSync(matchedFile, "utf8");
			} catch (error) {
				if (error.code === "ENOENT") {
					require_logger.logger_default.debug(`File disappeared during glob expansion: ${matchedFile}`);
					continue;
				}
				throw error;
			}
			if (matchedFile.endsWith(".json")) {
				const parsed = JSON.parse(contents);
				if (Array.isArray(parsed)) allContents.push(...parsed);
				else allContents.push(parsed);
			} else if (matchedFile.endsWith(".yaml") || matchedFile.endsWith(".yml")) {
				const parsed = js_yaml.default.load(contents);
				if (parsed === null || parsed === void 0) continue;
				if (Array.isArray(parsed)) allContents.push(...parsed);
				else allContents.push(parsed);
			} else if (matchedFile.endsWith(".csv")) {
				const records = (0, csv_parse_sync.parse)(contents, { columns: true });
				if (records.length > 0 && Object.keys(records[0]).length === 1) allContents.push(...records.map((record) => Object.values(record)[0]));
				else allContents.push(...records);
			} else allContents.push(contents);
		}
		return allContents;
	}
	const finalPath = resolvedPath;
	let contents;
	try {
		contents = fs.readFileSync(finalPath, "utf8");
	} catch (error) {
		if (error.code === "ENOENT") throw new Error(`File does not exist: ${finalPath}`);
		throw new Error(`Failed to read file ${finalPath}: ${error}`);
	}
	if (finalPath.endsWith(".json")) try {
		return JSON.parse(contents);
	} catch (error) {
		throw new Error(`Failed to parse JSON file ${finalPath}: ${error}`);
	}
	if (finalPath.endsWith(".yaml") || finalPath.endsWith(".yml")) try {
		return js_yaml.default.load(contents);
	} catch (error) {
		throw new Error(`Failed to parse YAML file ${finalPath}: ${error}`);
	}
	if (finalPath.endsWith(".csv")) {
		const records = (0, csv_parse_sync.parse)(contents, { columns: true });
		if (records.length > 0 && Object.keys(records[0]).length === 1) return records.map((record) => Object.values(record)[0]);
		return records;
	}
	return contents;
}
/**
* Resolves a relative file path with respect to a base path, handling cloud configuration appropriately.
* When using a cloud configuration, the current working directory is always used instead of the context's base path.
*
* @param filePath - The relative or absolute file path to resolve.
* @param isCloudConfig - Whether this is a cloud configuration.
* @returns The resolved absolute file path.
*/
function getResolvedRelativePath(filePath, isCloudConfig) {
	if (path.isAbsolute(filePath) || !isCloudConfig) return filePath;
	return path.join(process.cwd(), filePath);
}
/**
* Recursively loads external file references from a configuration object.
*
* @param config - The configuration object to process
* @param context - Optional context to control file loading behavior
* @returns The configuration with external file references resolved
*/
function maybeLoadConfigFromExternalFile(config, context) {
	if (Array.isArray(config)) return config.map((item) => maybeLoadConfigFromExternalFile(item, context));
	if (config && typeof config === "object" && config !== null) {
		const result = {};
		for (const key of Object.keys(config)) {
			const childContext = key === "value" && typeof config === "object" && config && "type" in config && typeof config.type === "string" && (config.type === "python" || config.type === "javascript") ? "assertion" : key === "vars" ? "vars" : context;
			result[key] = maybeLoadConfigFromExternalFile(config[key], childContext);
		}
		return result;
	}
	return maybeLoadFromExternalFile(config, context);
}
/**
* Parses a file path or glob pattern to extract function names and file extensions.
* Function names can be specified in the filename like this:
* prompt.py:myFunction or prompts.js:myFunction.
* @param basePath - The base path for file resolution.
* @param promptPath - The path or glob pattern.
* @returns Parsed details including function name, file extension, and directory status.
*/
function parsePathOrGlob(basePath, promptPath) {
	if (promptPath.startsWith("file://")) promptPath = promptPath.slice(7);
	const filePath = path.resolve(basePath, promptPath);
	let filename = path.relative(basePath, filePath);
	let functionName;
	if (filename.includes(":")) {
		const lastColonIndex = filename.lastIndexOf(":");
		if (lastColonIndex > 1) {
			const pathWithoutFunction = filename.slice(0, lastColonIndex);
			if (require_fileExtensions.isJavascriptFile(pathWithoutFunction) || pathWithoutFunction.endsWith(".py") || pathWithoutFunction.endsWith(".go") || pathWithoutFunction.endsWith(".rb")) {
				functionName = filename.slice(lastColonIndex + 1);
				filename = pathWithoutFunction;
			}
		}
	}
	let stats;
	try {
		stats = fs.statSync(path.join(basePath, filename));
	} catch (err) {
		if (require_logger.getEnvBool("PROMPTFOO_STRICT_FILES")) throw err;
	}
	const normalizedFilePath = filePath.replace(/\\/g, "/");
	const isPathPattern = stats?.isDirectory() || (0, glob.hasMagic)(promptPath) || (0, glob.hasMagic)(normalizedFilePath);
	const safeFilename = path.relative(basePath, require_esm.safeResolve(basePath, filename));
	return {
		extension: isPathPattern ? void 0 : path.parse(safeFilename).ext,
		filePath: path.join(basePath, safeFilename),
		functionName,
		isPathPattern
	};
}
function readOutput(outputPath) {
	const ext = path.parse(outputPath).ext.slice(1);
	switch (ext) {
		case "json": return JSON.parse(fs.readFileSync(outputPath, "utf-8"));
		default: throw new Error(`Unsupported output file format: ${ext} currently only supports json`);
	}
}
/**
* Load custom Nunjucks filters from external files.
* Note: If a glob pattern matches multiple files, only the last file's export is used.
* Each filter name should typically resolve to a single file.
*/
async function readFilters(filters, basePath = "") {
	const ret = {};
	for (const [name, filterPath] of Object.entries(filters)) {
		const filePaths = (0, glob.globSync)(path.join(basePath, filterPath), { windowsPathsNoEscape: true });
		for (const filePath of filePaths) ret[name] = await require_esm.importModule(path.resolve(filePath));
	}
	return ret;
}
/**
* Loads configuration from an external file with variable rendering.
* This is a convenience wrapper that combines renderVarsInObject and maybeLoadFromExternalFile.
*
* Use this for simple config fields that:
* - Need variable rendering ({{ vars.x }}, {{ env.X }})
* - May reference external files (file://path.json)
* - Don't have nested file references that need loading
*
* For fields with nested file references (like response_format.schema),
* use maybeLoadResponseFormatFromExternalFile instead.
*
* @param config - The configuration to process
* @param vars - Variables for template rendering
* @returns The processed configuration with variables rendered and files loaded
*/
function maybeLoadFromExternalFileWithVars(config, vars) {
	return maybeLoadFromExternalFile(renderVarsInObject(config, vars));
}
/**
* Loads response_format configuration from an external file with variable rendering.
*
* This function handles the special case where response_format may contain:
* 1. A top-level file reference (file://format.json)
* 2. A nested schema reference for json_schema type (schema: file://schema.json)
*
* Both levels need variable rendering and file loading.
*
* @param responseFormat - The response_format configuration
* @param vars - Variables for template rendering
* @returns The processed response_format with all files loaded
*/
function maybeLoadResponseFormatFromExternalFile(responseFormat, vars) {
	if (responseFormat === void 0 || responseFormat === null) return responseFormat;
	const loaded = maybeLoadFromExternalFile(renderVarsInObject(responseFormat, vars));
	if (!loaded || typeof loaded !== "object") return loaded;
	if (loaded.type === "json_schema") {
		const nestedSchema = loaded.schema || loaded.json_schema?.schema;
		if (nestedSchema) {
			const loadedSchema = maybeLoadFromExternalFile(renderVarsInObject(nestedSchema, vars));
			if (loaded.schema !== void 0) return {
				...loaded,
				schema: loadedSchema
			};
			else if (loaded.json_schema?.schema !== void 0) return {
				...loaded,
				json_schema: {
					...loaded.json_schema,
					schema: loadedSchema
				}
			};
		}
	}
	return loaded;
}
/**
* Renders variables in a tools object and loads from external file if applicable.
* This function combines renderVarsInObject and maybeLoadFromExternalFile into a single step
* specifically for handling tools configurations.
*
* Supports loading from JSON, YAML, Python, and JavaScript files.
*
* @param tools - The tools configuration object or array to process.
* @param vars - Variables to use for rendering.
* @returns The processed tools configuration with variables rendered and content loaded from files if needed.
* @throws {Error} If the loaded tools are in an invalid format
*/
async function maybeLoadToolsFromExternalFile(tools, vars) {
	const rendered = renderVarsInObject(tools, vars);
	if (typeof rendered === "string" && rendered.startsWith("file://")) {
		const { filePath, functionName } = parseFileUrl(rendered);
		if (functionName && (filePath.endsWith(".py") || require_fileExtensions.isJavascriptFile(filePath))) {
			const fileType = filePath.endsWith(".py") ? "Python" : "JavaScript";
			require_logger.logger_default.debug(`[maybeLoadToolsFromExternalFile] Loading tools from ${fileType} file: ${filePath}:${functionName}`);
			try {
				let toolDefinitions;
				if (filePath.endsWith(".py")) {
					const absPath = require_esm.safeResolve(require_logger.cliState_default.basePath || process.cwd(), filePath);
					require_logger.logger_default.debug(`[maybeLoadToolsFromExternalFile] Resolved Python path: ${absPath}`);
					toolDefinitions = await require_pythonUtils.runPython(absPath, functionName, []);
				} else {
					const absPath = require_esm.safeResolve(require_logger.cliState_default.basePath || process.cwd(), filePath);
					require_logger.logger_default.debug(`[maybeLoadToolsFromExternalFile] Resolved JavaScript path: ${absPath}`);
					const module = await require_esm.importModule(absPath);
					const fn = module[functionName] || module.default?.[functionName];
					if (typeof fn !== "function") {
						const availableExports = Object.keys(module).filter((k) => k !== "default");
						const basePath = require_logger.cliState_default.basePath || process.cwd();
						throw new Error(`Function "${functionName}" not found in ${filePath}. Available exports: ${availableExports.length > 0 ? availableExports.join(", ") : "(none)"}\nResolved from: ${basePath}`);
					}
					toolDefinitions = await Promise.resolve(fn());
				}
				if (!toolDefinitions || typeof toolDefinitions === "string" || typeof toolDefinitions === "number" || typeof toolDefinitions === "boolean") throw new Error(`Function "${functionName}" must return an array or object of tool definitions, but returned: ${toolDefinitions === null ? "null" : typeof toolDefinitions}`);
				require_logger.logger_default.debug(`[maybeLoadToolsFromExternalFile] Successfully loaded ${Array.isArray(toolDefinitions) ? toolDefinitions.length : "object"} tools`);
				return toolDefinitions;
			} catch (err) {
				const errorMessage = err instanceof Error ? err.message : String(err);
				const basePath = require_logger.cliState_default.basePath || process.cwd();
				throw new Error(`Failed to load tools from ${rendered}:\n${errorMessage}\n\nMake sure the function "${functionName}" exists and returns a valid tool definition array.\nResolved from: ${basePath}`);
			}
		}
		if (filePath.endsWith(".py") || require_fileExtensions.isJavascriptFile(filePath)) {
			const ext = filePath.endsWith(".py") ? "Python" : "JavaScript";
			const basePath = require_logger.cliState_default.basePath || process.cwd();
			throw new Error(`Cannot load tools from ${rendered}\n${ext} files require a function name. Use this format:\n  tools: file://${filePath}:get_tools\n\nYour ${ext} file should export a function that returns tool definitions:\n` + (filePath.endsWith(".py") ? `  def get_tools():\n      return [{"type": "function", "function": {...}}]` : `  module.exports.get_tools = () => [{ type: "function", function: {...} }];`) + `\n\nResolved from: ${basePath}`);
		}
	}
	if (Array.isArray(rendered)) {
		const results = await Promise.all(rendered.map((item) => maybeLoadToolsFromExternalFile(item, vars)));
		if (results.every((r) => Array.isArray(r))) return results.flat();
		return results;
	}
	if (typeof rendered !== "string") return rendered;
	const loaded = maybeLoadFromExternalFile(rendered);
	if (loaded !== void 0 && loaded !== null && typeof loaded === "string") {
		if (loaded.startsWith("file://")) throw new Error(`Failed to load tools from ${loaded}\nEnsure the file exists and contains valid JSON or YAML tool definitions.`);
		if (loaded.includes("def ") || loaded.includes("import ")) throw new Error("Invalid tools configuration: file appears to contain Python code.\nPython files require a function name. Use this format:\n  tools: file://tools.py:get_tools");
		throw new Error("Invalid tools configuration: expected an array or object, but got a string.\nIf using file://, ensure the file contains valid JSON or YAML tool definitions.");
	}
	return loaded;
}

//#endregion
//#region src/googleSheets.ts
async function checkGoogleSheetAccess(url) {
	try {
		const response = await require_fetch.fetchWithProxy(url);
		if (response.ok) return {
			public: true,
			status: response.status
		};
		else return {
			public: false,
			status: response.status
		};
	} catch (error) {
		require_logger.logger_default.error(`Error checking sheet access: ${error}`);
		return { public: false };
	}
}
async function fetchCsvFromGoogleSheetUnauthenticated(url) {
	const { parse: parseCsv } = await import("csv-parse/sync");
	const gid = new URL(url).searchParams.get("gid");
	const response = await require_fetch.fetchWithProxy(`${url.replace(/\/edit.*$/, "/export")}?format=csv${gid ? `&gid=${gid}` : ""}`);
	if (response.status !== 200) throw new Error(`Failed to fetch CSV from Google Sheets URL: ${url}`);
	return parseCsv(await response.text(), { columns: true });
}
async function fetchCsvFromGoogleSheetAuthenticated(url) {
	const { sheets: googleSheets, auth: googleAuth } = await import("@googleapis/sheets");
	const auth = new googleAuth.GoogleAuth({ scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"] });
	const sheets = googleSheets("v4");
	const match = url.match(/\/d\/([^/]+)/);
	if (!match) throw new Error(`Invalid Google Sheets URL: ${url}`);
	const spreadsheetId = match[1];
	let range;
	const gid = Number(new URL(url).searchParams.get("gid"));
	if (gid) {
		const sheet = (await sheets.spreadsheets.get({
			spreadsheetId,
			auth
		})).data.sheets?.find((sheet) => sheet.properties?.sheetId === gid);
		if (!sheet || !sheet.properties?.title) throw new Error(`Sheet not found for gid: ${gid}`);
		range = sheet.properties.title;
	} else {
		const firstSheet = (await sheets.spreadsheets.get({
			spreadsheetId,
			auth
		})).data.sheets?.[0];
		if (!firstSheet || !firstSheet.properties?.title) throw new Error(`No sheets found in spreadsheet`);
		range = firstSheet.properties.title;
	}
	const rows = (await sheets.spreadsheets.values.get({
		spreadsheetId,
		range,
		auth
	})).data.values;
	if (!rows?.length) throw new Error(`No data found in Google Sheets URL: ${url}`);
	const headers = rows[0];
	return rows.slice(1).map((row) => {
		const csvRow = {};
		headers.forEach((header, index) => {
			csvRow[header] = row[index] ?? "";
		});
		return csvRow;
	});
}
async function fetchCsvFromGoogleSheet(url) {
	const { public: isPublic } = await checkGoogleSheetAccess(url);
	require_logger.logger_default.debug(`Google Sheets URL: ${url}, isPublic: ${isPublic}`);
	if (isPublic) return fetchCsvFromGoogleSheetUnauthenticated(url);
	return fetchCsvFromGoogleSheetAuthenticated(url);
}
async function writeCsvToGoogleSheet(rows, url) {
	const { sheets: googleSheets, auth: googleAuth } = await import("@googleapis/sheets");
	const auth = new googleAuth.GoogleAuth({ scopes: ["https://www.googleapis.com/auth/spreadsheets"] });
	const sheets = googleSheets("v4");
	const match = url.match(/\/d\/([^/]+)/);
	if (!match) throw new Error(`Invalid Google Sheets URL: ${url}`);
	const spreadsheetId = match[1];
	const headers = Object.keys(rows[0]);
	const values = [headers, ...rows.map((row) => headers.map((header) => row[header]))];
	const getColumnLetter = (col) => {
		let letter = "";
		while (col > 0) {
			col--;
			letter = String.fromCharCode(65 + col % 26) + letter;
			col = Math.floor(col / 26);
		}
		return letter;
	};
	const numRows = values.length;
	const numCols = headers.length;
	const endColumn = getColumnLetter(numCols);
	let range;
	const gid = Number(new URL(url).searchParams.get("gid"));
	if (gid) {
		const sheet = (await sheets.spreadsheets.get({
			spreadsheetId,
			auth
		})).data.sheets?.find((sheet) => sheet.properties?.sheetId === gid);
		if (!sheet || !sheet.properties?.title) throw new Error(`Sheet not found for gid: ${gid}`);
		range = `${sheet.properties.title}!A1:${endColumn}${numRows}`;
	} else {
		const newSheetTitle = `Sheet${Date.now()}`;
		await sheets.spreadsheets.batchUpdate({
			spreadsheetId,
			auth,
			requestBody: { requests: [{ addSheet: { properties: { title: newSheetTitle } } }] }
		});
		range = `${newSheetTitle}!A1:${endColumn}${numRows}`;
	}
	require_logger.logger_default.debug(`Writing CSV to Google Sheets URL: ${url} with ${values.length} rows`);
	await sheets.spreadsheets.values.update({
		spreadsheetId,
		range,
		valueInputOption: "USER_ENTERED",
		auth,
		requestBody: { values }
	});
}

//#endregion
//#region src/server/utils/evalTableUtils.ts
/**
*
*
*
* Keep this in it's current order, as it is used to map the columns in the CSV, so it needs to be static.
*
*
* The keys are the names of the columns in the metadata object, and the values are the names of the columns in the CSV.
*
* This is imported by enterprise so it doesn't need to be copied.
*
*/
const REDTEAM_METADATA_KEYS_TO_CSV_COLUMN_NAMES = {
	messages: "Messages",
	redteamHistory: "RedteamHistory",
	redteamTreeHistory: "RedteamTreeHistory",
	pluginId: "pluginId",
	strategyId: "strategyId",
	sessionId: "sessionId",
	sessionIds: "sessionIds"
};
const REDTEAM_METADATA_COLUMNS = Object.values(REDTEAM_METADATA_KEYS_TO_CSV_COLUMN_NAMES);
/**
* Get the status string for an output
*/
function getOutputStatus(output) {
	if (output.pass) return "PASS";
	return output.failureReason === require_types.ResultFailureReason.ASSERT ? "FAIL" : "ERROR";
}
/**
* Format named scores for CSV output.
* Returns empty string if no named scores, otherwise JSON string.
*/
function formatNamedScores(namedScores) {
	if (!namedScores || Object.keys(namedScores).length === 0) return "";
	const rounded = {};
	for (const [key, value] of Object.entries(namedScores)) if (typeof value === "number" && !Number.isNaN(value)) rounded[key] = Number(value.toFixed(2));
	if (Object.keys(rounded).length === 0) return "";
	return JSON.stringify(rounded);
}
/**
* Build CSV headers for an evaluation table.
*
* @param vars - Variable names from the table head
* @param prompts - Prompt definitions from the table head
* @param options - Export options
* @returns Array of header strings
*/
function buildCsvHeaders(vars, prompts, options = {}) {
	const headers = [
		...options.hasDescriptions ? ["Description"] : [],
		...vars,
		...prompts.flatMap((prompt) => {
			const provider = prompt.provider || "";
			return [
				provider ? `[${provider}] ${prompt.label}` : prompt.label,
				"Status",
				"Score",
				"Named Scores",
				"Grader Reason",
				"Comment"
			];
		})
	];
	if (options.isRedteam) headers.push(...REDTEAM_METADATA_COLUMNS);
	return headers;
}
/**
* Convert a single table row to CSV row values.
*
* @param row - The table row to convert
* @param options - Export options
* @returns Array of values for the CSV row
*/
function tableRowToCsvValues(row, options = {}) {
	const rowValues = [
		...options.hasDescriptions ? [row.test.description || ""] : [],
		...row.vars,
		...row.outputs.flatMap((output) => {
			if (!output) return [
				"",
				"",
				"",
				"",
				"",
				""
			];
			const status = getOutputStatus(output);
			const score = output.score?.toFixed(2) ?? "";
			const namedScores = formatNamedScores(output.namedScores);
			return [
				output.text || "",
				status,
				score,
				namedScores,
				output.gradingResult?.reason || "",
				output.gradingResult?.comment || ""
			];
		})
	];
	if (options.isRedteam) {
		const redteamKeys = Object.keys(REDTEAM_METADATA_KEYS_TO_CSV_COLUMN_NAMES);
		const firstOutputMetadata = row.outputs[0]?.metadata;
		for (const key of redteamKeys) {
			let value = firstOutputMetadata?.[key];
			if (key === "strategyId" && (value === null || value === void 0)) value = "basic";
			if (value === null || value === void 0) rowValues.push("");
			else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") rowValues.push(value.toString());
			else rowValues.push(JSON.stringify(value));
		}
	}
	return rowValues;
}
/**
* Stream CSV data from an evaluation in batches.
*
* This is more memory-efficient for large evaluations as it processes
* results in batches rather than loading everything into memory.
*
* Used by the CLI export (`promptfoo eval -o output.csv`) to maintain
* consistent CSV format with WebUI exports while handling large datasets.
*
* @param eval_ - The evaluation to export
* @param options - Streaming options including the write callback
*/
async function streamEvalCsv(eval_, options) {
	const { isRedteam = false, write } = options;
	const varNames = eval_.vars;
	const prompts = eval_.prompts;
	const numPrompts = prompts.length;
	let headersWritten = false;
	let hasDescriptions = false;
	let firstBatchBuffer = null;
	for await (const batchResults of eval_.fetchResultsBatched()) {
		const rowsByTestIdx = /* @__PURE__ */ new Map();
		for (const result of batchResults) {
			if (!rowsByTestIdx.has(result.testIdx)) rowsByTestIdx.set(result.testIdx, {
				testIdx: result.testIdx,
				vars: varNames.map((varName) => {
					const value = result.testCase?.vars?.[varName];
					return value !== void 0 ? String(value) : "";
				}),
				outputs: new Array(numPrompts).fill(null),
				test: { description: result.testCase?.description }
			});
			const row = rowsByTestIdx.get(result.testIdx);
			row.outputs[result.promptIdx] = {
				text: result.response?.output ?? "",
				pass: result.success,
				score: result.score,
				namedScores: result.namedScores,
				failureReason: result.failureReason,
				gradingResult: result.gradingResult,
				metadata: result.metadata
			};
		}
		const rows = Array.from(rowsByTestIdx.values());
		if (!headersWritten) {
			hasDescriptions = rows.some((r) => r.test.description);
			await write((0, csv_stringify_sync.stringify)([buildCsvHeaders(varNames, prompts, {
				hasDescriptions,
				isRedteam
			})]));
			headersWritten = true;
			if (!hasDescriptions) {
				firstBatchBuffer = rows;
				continue;
			}
		}
		if (firstBatchBuffer !== null) {
			if (rows.some((r) => r.test.description) && !hasDescriptions) {}
			const bufferedCsvRows = firstBatchBuffer.map((row) => tableRowToCsvValues(row, {
				hasDescriptions,
				isRedteam
			}));
			if (bufferedCsvRows.length > 0) await write((0, csv_stringify_sync.stringify)(bufferedCsvRows));
			firstBatchBuffer = null;
		}
		const csvRows = rows.map((row) => tableRowToCsvValues(row, {
			hasDescriptions,
			isRedteam
		}));
		if (csvRows.length > 0) await write((0, csv_stringify_sync.stringify)(csvRows));
	}
	if (firstBatchBuffer !== null) {
		const bufferedCsvRows = firstBatchBuffer.map((row) => tableRowToCsvValues(row, {
			hasDescriptions,
			isRedteam
		}));
		if (bufferedCsvRows.length > 0) await write((0, csv_stringify_sync.stringify)(bufferedCsvRows));
	}
	if (!headersWritten) await write((0, csv_stringify_sync.stringify)([buildCsvHeaders(varNames, prompts, {
		hasDescriptions: false,
		isRedteam
	})]));
}

//#endregion
//#region src/util/output.ts
const outputToSimpleString = (output) => {
	const passFailText = output.pass ? "[PASS]" : output.failureReason === require_types.ResultFailureReason.ASSERT ? "[FAIL]" : "[ERROR]";
	const namedScoresText = Object.entries(output.namedScores).map(([name, value]) => `${name}: ${value?.toFixed(2)}`).join(", ");
	const scoreText = namedScoresText.length > 0 ? `(${output.score?.toFixed(2)}, ${namedScoresText})` : `(${output.score?.toFixed(2)})`;
	const gradingResultText = output.gradingResult ? `${output.pass ? "Pass" : "Fail"} Reason: ${output.gradingResult.reason}` : "";
	return dedent.default`
      ${passFailText} ${scoreText}

      ${output.text}

      ${gradingResultText}
    `.trim();
};
function createOutputMetadata(evalRecord) {
	let evaluationCreatedAt;
	if (evalRecord.createdAt) try {
		const date = new Date(evalRecord.createdAt);
		evaluationCreatedAt = Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
	} catch {
		evaluationCreatedAt = void 0;
	}
	return {
		promptfooVersion: require_fetch.VERSION,
		nodeVersion: process.version,
		platform: os.platform(),
		arch: os.arch(),
		exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
		evaluationCreatedAt,
		author: evalRecord.author
	};
}
/**
* JSON writer with improved error handling for large datasets.
* Provides helpful error messages when memory limits are exceeded.
*/
async function writeJsonOutputSafely(outputPath, evalRecord, shareableUrl) {
	const metadata = createOutputMetadata(evalRecord);
	try {
		const summary = await evalRecord.toEvaluateSummary();
		const outputData = {
			evalId: evalRecord.id,
			results: summary,
			config: evalRecord.config,
			shareableUrl,
			metadata
		};
		const jsonString = JSON.stringify(outputData, null, 2);
		await fs_promises.writeFile(outputPath, jsonString);
	} catch (error) {
		const msg = error?.message ?? "";
		const isStringLen = error instanceof RangeError && msg.includes("Invalid string length");
		const isHeapOOM = /heap out of memory|Array buffer allocation failed|ERR_STRING_TOO_LONG/i.test(msg);
		if (isStringLen || isHeapOOM) {
			const resultCount = await evalRecord.getResultsCount();
			require_logger.logger_default.error(`Dataset too large for JSON export (${resultCount} results).`);
			throw new Error(`Dataset too large for JSON export. The evaluation has ${resultCount} results which exceeds memory limits. Consider using JSONL format instead: --output output.jsonl`);
		} else throw error;
	}
}
async function writeOutput(outputPath, evalRecord, shareableUrl) {
	if (outputPath.match(/^https:\/\/docs\.google\.com\/spreadsheets\//)) {
		const table = await evalRecord.getTable();
		require_invariant.invariant(table, "Table is required");
		const rows = table.body.map((row) => {
			const csvRow = {};
			table.head.vars.forEach((varName, index) => {
				csvRow[varName] = row.vars[index];
			});
			table.head.prompts.forEach((prompt, index) => {
				csvRow[`[${prompt.provider}] ${prompt.label}`] = outputToSimpleString(row.outputs[index]);
			});
			return csvRow;
		});
		require_logger.logger_default.info(`Writing ${rows.length} rows to Google Sheets...`);
		await writeCsvToGoogleSheet(rows, outputPath);
		return;
	}
	const { data: outputExtension } = require_types.OutputFileExtension.safeParse(path.extname(outputPath).slice(1).toLowerCase());
	require_invariant.invariant(outputExtension, `Unsupported output file format ${outputExtension}. Please use one of: ${require_types.OutputFileExtension.options.join(", ")}.`);
	const outputDir = path.dirname(outputPath);
	await fs_promises.mkdir(outputDir, { recursive: true });
	const metadata = createOutputMetadata(evalRecord);
	if (outputExtension === "csv") {
		const fileHandle = await fs_promises.open(outputPath, "w");
		try {
			await streamEvalCsv(evalRecord, {
				isRedteam: Boolean(evalRecord.config.redteam),
				write: async (data) => {
					await fileHandle.write(data);
				}
			});
		} finally {
			await fileHandle.close();
		}
	} else if (outputExtension === "json") await writeJsonOutputSafely(outputPath, evalRecord, shareableUrl);
	else if (outputExtension === "yaml" || outputExtension === "yml" || outputExtension === "txt") {
		const summary = await evalRecord.toEvaluateSummary();
		await fs_promises.writeFile(outputPath, js_yaml.default.dump({
			evalId: evalRecord.id,
			results: summary,
			config: evalRecord.config,
			shareableUrl,
			metadata
		}));
	} else if (outputExtension === "html") {
		const table = await evalRecord.getTable();
		require_invariant.invariant(table, "Table is required");
		const summary = await evalRecord.toEvaluateSummary();
		const template = await fs_promises.readFile(path.join(require_esm.getDirectory(), "tableOutput.html"), "utf-8");
		const htmlTable = [[...table.head.vars, ...table.head.prompts.map((prompt) => `[${prompt.provider}] ${prompt.label}`)], ...table.body.map((row) => [...row.vars, ...row.outputs.map(outputToSimpleString)])];
		const htmlOutput = getNunjucksEngine().renderString(template, {
			config: evalRecord.config,
			table: htmlTable,
			results: summary
		});
		await fs_promises.writeFile(outputPath, htmlOutput);
	} else if (outputExtension === "jsonl") {
		await fs_promises.writeFile(outputPath, "");
		for await (const batchResults of evalRecord.fetchResultsBatched()) {
			const text = batchResults.map((result) => JSON.stringify(result)).join(os.EOL) + os.EOL;
			await fs_promises.appendFile(outputPath, text);
		}
	} else if (outputExtension === "xml") {
		const summary = await evalRecord.toEvaluateSummary();
		const sanitizeForXml = (obj) => {
			if (obj === null || obj === void 0) return "";
			if (typeof obj === "boolean" || typeof obj === "number") return String(obj);
			if (typeof obj === "string") return obj;
			if (Array.isArray(obj)) return obj.map(sanitizeForXml);
			if (typeof obj === "object") {
				const sanitized = {};
				for (const [key, value] of Object.entries(obj)) sanitized[key] = sanitizeForXml(value);
				return sanitized;
			}
			return String(obj);
		};
		const xmlData = new fast_xml_parser.XMLBuilder({
			ignoreAttributes: false,
			format: true,
			indentBy: "  "
		}).build({ promptfoo: {
			evalId: evalRecord.id,
			results: sanitizeForXml(summary),
			config: sanitizeForXml(evalRecord.config),
			shareableUrl: shareableUrl || ""
		} });
		await fs_promises.writeFile(outputPath, xmlData);
	}
}
async function writeMultipleOutputs(outputPaths, evalRecord, shareableUrl) {
	await Promise.all(outputPaths.map((outputPath) => writeOutput(outputPath, evalRecord, shareableUrl)));
}

//#endregion
//#region src/util/runtime.ts
function printBorder() {
	const border = "=".repeat(require_fetch.TERMINAL_MAX_WIDTH);
	require_logger.logger_default.info(border);
}

//#endregion
Object.defineProperty(exports, 'deduplicateTestCases', {
  enumerable: true,
  get: function () {
    return deduplicateTestCases;
  }
});
Object.defineProperty(exports, 'doesProviderRefMatch', {
  enumerable: true,
  get: function () {
    return doesProviderRefMatch;
  }
});
Object.defineProperty(exports, 'extractVariablesFromTemplate', {
  enumerable: true,
  get: function () {
    return extractVariablesFromTemplate;
  }
});
Object.defineProperty(exports, 'extractVariablesFromTemplates', {
  enumerable: true,
  get: function () {
    return extractVariablesFromTemplates;
  }
});
Object.defineProperty(exports, 'fetchCsvFromGoogleSheet', {
  enumerable: true,
  get: function () {
    return fetchCsvFromGoogleSheet;
  }
});
Object.defineProperty(exports, 'filterRuntimeVars', {
  enumerable: true,
  get: function () {
    return filterRuntimeVars;
  }
});
Object.defineProperty(exports, 'getNunjucksEngine', {
  enumerable: true,
  get: function () {
    return getNunjucksEngine;
  }
});
Object.defineProperty(exports, 'getNunjucksEngineForFilePath', {
  enumerable: true,
  get: function () {
    return getNunjucksEngineForFilePath;
  }
});
Object.defineProperty(exports, 'getProviderDescription', {
  enumerable: true,
  get: function () {
    return getProviderDescription;
  }
});
Object.defineProperty(exports, 'getResolvedRelativePath', {
  enumerable: true,
  get: function () {
    return getResolvedRelativePath;
  }
});
Object.defineProperty(exports, 'getTestCaseDeduplicationKey', {
  enumerable: true,
  get: function () {
    return getTestCaseDeduplicationKey;
  }
});
Object.defineProperty(exports, 'isAnthropicProvider', {
  enumerable: true,
  get: function () {
    return isAnthropicProvider;
  }
});
Object.defineProperty(exports, 'isGoogleProvider', {
  enumerable: true,
  get: function () {
    return isGoogleProvider;
  }
});
Object.defineProperty(exports, 'isOpenAiProvider', {
  enumerable: true,
  get: function () {
    return isOpenAiProvider;
  }
});
Object.defineProperty(exports, 'isProviderAllowed', {
  enumerable: true,
  get: function () {
    return isProviderAllowed;
  }
});
Object.defineProperty(exports, 'loadFunction', {
  enumerable: true,
  get: function () {
    return loadFunction;
  }
});
Object.defineProperty(exports, 'maybeLoadConfigFromExternalFile', {
  enumerable: true,
  get: function () {
    return maybeLoadConfigFromExternalFile;
  }
});
Object.defineProperty(exports, 'maybeLoadFromExternalFile', {
  enumerable: true,
  get: function () {
    return maybeLoadFromExternalFile;
  }
});
Object.defineProperty(exports, 'maybeLoadFromExternalFileWithVars', {
  enumerable: true,
  get: function () {
    return maybeLoadFromExternalFileWithVars;
  }
});
Object.defineProperty(exports, 'maybeLoadResponseFormatFromExternalFile', {
  enumerable: true,
  get: function () {
    return maybeLoadResponseFormatFromExternalFile;
  }
});
Object.defineProperty(exports, 'maybeLoadToolsFromExternalFile', {
  enumerable: true,
  get: function () {
    return maybeLoadToolsFromExternalFile;
  }
});
Object.defineProperty(exports, 'parseFileUrl', {
  enumerable: true,
  get: function () {
    return parseFileUrl;
  }
});
Object.defineProperty(exports, 'parsePathOrGlob', {
  enumerable: true,
  get: function () {
    return parsePathOrGlob;
  }
});
Object.defineProperty(exports, 'printBorder', {
  enumerable: true,
  get: function () {
    return printBorder;
  }
});
Object.defineProperty(exports, 'readFilters', {
  enumerable: true,
  get: function () {
    return readFilters;
  }
});
Object.defineProperty(exports, 'readOutput', {
  enumerable: true,
  get: function () {
    return readOutput;
  }
});
Object.defineProperty(exports, 'renderEnvOnlyInObject', {
  enumerable: true,
  get: function () {
    return renderEnvOnlyInObject;
  }
});
Object.defineProperty(exports, 'renderVarsInObject', {
  enumerable: true,
  get: function () {
    return renderVarsInObject;
  }
});
Object.defineProperty(exports, 'resultIsForTestCase', {
  enumerable: true,
  get: function () {
    return resultIsForTestCase;
  }
});
Object.defineProperty(exports, 'setupEnv', {
  enumerable: true,
  get: function () {
    return setupEnv;
  }
});
Object.defineProperty(exports, 'writeMultipleOutputs', {
  enumerable: true,
  get: function () {
    return writeMultipleOutputs;
  }
});
Object.defineProperty(exports, 'writeOutput', {
  enumerable: true,
  get: function () {
    return writeOutput;
  }
});
//# sourceMappingURL=util-CxWghxGa.cjs.map