const require_rolldown_runtime = require("./rolldown-runtime-D_mwlA32.cjs");
const require_logger = require("./logger-DuG3amVa.cjs");
const require_invariant = require("./invariant-KxYLhw_b.cjs");
const require_time = require("./time-C8D2VmK1.cjs");
let path = require("path");
path = require_rolldown_runtime.__toESM(path, 1);
let js_yaml = require("js-yaml");
js_yaml = require_rolldown_runtime.__toESM(js_yaml, 1);
let node_async_hooks = require("node:async_hooks");
let fs = require("fs");
fs = require_rolldown_runtime.__toESM(fs, 1);
let node_fs_promises = require("node:fs/promises");
node_fs_promises = require_rolldown_runtime.__toESM(node_fs_promises, 1);
let proxy_from_env = require("proxy-from-env");
let undici = require("undici");
let util = require("util");
let zlib = require("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.121.15";
/**
* PostHog analytics key.
* Only populated during production builds via PROMPTFOO_POSTHOG_KEY env var.
* Empty string in development/test.
*/
const POSTHOG_KEY = "phc_E5n5uHnDo2eREJL1uqX1cIlbkoRby4yFWt3V94HqRRg";
function getShareApiBaseUrl() {
	return require_logger.getEnvString("PROMPTFOO_REMOTE_API_BASE_URL") || "https://api.promptfoo.app";
}
function getDefaultShareViewBaseUrl() {
	return require_logger.getEnvString("PROMPTFOO_SHARING_APP_BASE_URL", `https://promptfoo.app`);
}
function getShareViewBaseUrl() {
	return require_logger.getEnvString("PROMPTFOO_REMOTE_APP_BASE_URL") || getDefaultShareViewBaseUrl();
}
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/globalConfig/globalConfig.ts
/**
* Functions for manipulating the global configuration file, which lives at
* ~/.promptfoo/promptfoo.yaml by default.
*/
function writeGlobalConfig(config) {
	fs.writeFileSync(path.join(require_logger.getConfigDirectoryPath(true), "promptfoo.yaml"), js_yaml.default.dump(config));
}
function readGlobalConfig() {
	const configDir = require_logger.getConfigDirectoryPath();
	const configFilePath = path.join(configDir, "promptfoo.yaml");
	let globalConfig = { id: crypto.randomUUID() };
	if (fs.existsSync(configFilePath)) {
		globalConfig = js_yaml.default.load(fs.readFileSync(configFilePath, "utf-8")) || {};
		if (!globalConfig?.id) {
			globalConfig = {
				...globalConfig,
				id: crypto.randomUUID()
			};
			writeGlobalConfig(globalConfig);
		}
	} else {
		if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
		fs.writeFileSync(configFilePath, js_yaml.default.dump(globalConfig));
	}
	return globalConfig;
}
/**
* Merges the top-level keys into existing config.
* @param partialConfig New keys to merge into the existing config.
*/
function writeGlobalConfigPartial(partialConfig) {
	const updatedConfig = { ...readGlobalConfig() };
	Object.entries(partialConfig).forEach(([key, value]) => {
		if (value !== void 0 && value !== null) updatedConfig[key] = value;
		else delete updatedConfig[key];
	});
	writeGlobalConfig(updatedConfig);
}
//#endregion
//#region src/globalConfig/cloud.ts
const CLOUD_API_HOST = "https://api.promptfoo.app";
const API_HOST = require_logger.getEnvString("API_HOST", CLOUD_API_HOST);
const CLOUD_HOSTNAMES = new Set([
	new URL(CLOUD_API_HOST).hostname,
	new URL("https://www.promptfoo.app").hostname,
	new URL("https://promptfoo.app").hostname
]);
const SHARING_CUTOFF_DATE = /* @__PURE__ */ new Date("2026-03-09T00:00:00Z");
function isPromptfooCloudHost(url) {
	try {
		const hostname = new URL(url).hostname.toLowerCase().replace(/\.$/, "");
		return CLOUD_HOSTNAMES.has(hostname);
	} catch {
		return false;
	}
}
var CloudConfig = class {
	config;
	constructor() {
		const savedConfig = readGlobalConfig()?.cloud || {};
		this.config = {
			appUrl: savedConfig.appUrl || "https://www.promptfoo.app",
			apiHost: savedConfig.apiHost,
			apiKey: savedConfig.apiKey,
			sharing: savedConfig.sharing,
			currentOrganizationId: savedConfig.currentOrganizationId,
			currentTeamId: savedConfig.currentTeamId,
			teams: savedConfig.teams
		};
	}
	/**
	* Returns the API key from config file or PROMPTFOO_API_KEY environment variable.
	* Config file takes precedence over environment variable.
	*/
	resolveApiKey() {
		return this.config.apiKey || process.env.PROMPTFOO_API_KEY;
	}
	/**
	* Returns the API host from config file, PROMPTFOO_CLOUD_API_URL environment variable,
	* or defaults to the standard cloud API host.
	* Config file takes precedence over environment variable.
	*
	* Trailing slashes are stripped so callers that append a path (e.g.
	* `${getApiHost()}/api/v1/...`) never produce a double slash. On-prem hosts
	* entered via `promptfoo auth login --api-host https://host/` commonly include one.
	*/
	resolveApiHost() {
		return (this.config.apiHost || process.env.PROMPTFOO_CLOUD_API_URL || API_HOST).replace(/\/+$/, "");
	}
	isEnabled() {
		return !!this.resolveApiKey();
	}
	setApiHost(apiHost) {
		this.config.apiHost = apiHost.replace(/\/+$/, "");
		this.saveConfig();
	}
	setApiKey(apiKey) {
		this.config.apiKey = apiKey;
		this.saveConfig();
	}
	getApiKey() {
		return this.resolveApiKey();
	}
	getApiHost() {
		return this.resolveApiHost();
	}
	setAppUrl(appUrl) {
		this.config.appUrl = appUrl;
		this.saveConfig();
	}
	getAppUrl() {
		return this.config.appUrl;
	}
	getSharing() {
		return this.config.sharing;
	}
	/**
	* Sets the sharing preference. Note: this value is only updated at authentication time
	* (via `validateAndSetApiToken`) and may become stale if the user's license status
	* changes between re-authentications.
	*/
	setSharing(sharing) {
		this.config.sharing = sharing;
		this.saveConfig();
	}
	delete() {
		writeGlobalConfigPartial({ cloud: {} });
		this.reload();
	}
	saveConfig() {
		writeGlobalConfigPartial({ cloud: this.config });
		this.reload();
	}
	reload() {
		const savedConfig = readGlobalConfig()?.cloud || {};
		this.config = {
			appUrl: savedConfig.appUrl || "https://www.promptfoo.app",
			apiHost: savedConfig.apiHost,
			apiKey: savedConfig.apiKey,
			sharing: savedConfig.sharing,
			currentOrganizationId: savedConfig.currentOrganizationId,
			currentTeamId: savedConfig.currentTeamId,
			teams: savedConfig.teams
		};
	}
	saveValidatedApiToken(token, apiHost, user, app, hasActiveLicense) {
		this.setApiKey(token);
		this.setApiHost(apiHost);
		this.setAppUrl(app.url);
		if (!(isPromptfooCloudHost(apiHost) || isPromptfooCloudHost(app.url))) this.setSharing(true);
		else if (typeof hasActiveLicense === "boolean") {
			const createdAt = user?.createdAt ? new Date(user.createdAt) : null;
			const isGrandfathered = createdAt != null && createdAt < SHARING_CUTOFF_DATE;
			this.setSharing(hasActiveLicense || isGrandfathered);
		}
	}
	async validateApiToken(token, apiHost) {
		try {
			const { fetchWithProxy } = await Promise.resolve().then(() => fetch_exports);
			const response = await fetchWithProxy(`${apiHost}/api/v1/users/me`, { headers: { Authorization: `Bearer ${token}` } });
			if (!response.ok) {
				const errorMessage = await response.text();
				require_logger.logger.error(`[Cloud] Failed to validate API token: ${errorMessage}. HTTP Status: ${response.status} - ${response.statusText}.`);
				throw new Error("Failed to validate API token: " + response.statusText);
			}
			const { user, organization, app, hasActiveLicense } = await response.json();
			return {
				user,
				organization,
				app,
				...typeof hasActiveLicense === "boolean" ? { hasActiveLicense } : {}
			};
		} catch (err) {
			const error = err;
			const errorMessage = error instanceof Error ? error.message : String(error);
			require_logger.logger.error(`[Cloud] Failed to validate API token with host ${apiHost}: ${errorMessage}`);
			if (error.cause) require_logger.logger.error(`Cause: ${error.cause}`);
			throw error;
		}
	}
	async validateAndSetApiToken(token, apiHost) {
		const { user, organization, app, hasActiveLicense } = await this.validateApiToken(token, apiHost);
		this.saveValidatedApiToken(token, apiHost, user, app, hasActiveLicense);
		return {
			user,
			organization,
			app,
			hasActiveLicense: typeof hasActiveLicense === "boolean" ? hasActiveLicense : false
		};
	}
	getCurrentOrganizationId() {
		return this.config.currentOrganizationId;
	}
	setCurrentOrganization(organizationId) {
		this.config.currentOrganizationId = organizationId;
		this.saveConfig();
	}
	getCurrentTeamId(organizationId) {
		if (organizationId) return this.config.teams?.[organizationId]?.currentTeamId;
		return this.config.currentTeamId;
	}
	setCurrentTeamId(teamId, organizationId) {
		if (organizationId) {
			if (!this.config.teams) this.config.teams = {};
			if (!this.config.teams[organizationId]) this.config.teams[organizationId] = {};
			this.config.teams[organizationId].currentTeamId = teamId;
		} else this.config.currentTeamId = teamId;
		this.saveConfig();
	}
	clearCurrentTeamId(organizationId) {
		if (organizationId) {
			if (this.config.teams?.[organizationId]) delete this.config.teams[organizationId].currentTeamId;
		} else delete this.config.currentTeamId;
		this.saveConfig();
	}
	cacheTeams(teams, organizationId) {
		if (organizationId) {
			if (!this.config.teams) this.config.teams = {};
			if (!this.config.teams[organizationId]) this.config.teams[organizationId] = {};
			this.config.teams[organizationId].cache = teams.map((t) => ({
				id: t.id,
				name: t.name,
				slug: t.slug,
				lastFetched: (/* @__PURE__ */ new Date()).toISOString()
			}));
		}
		this.saveConfig();
	}
	getCachedTeams(organizationId) {
		if (organizationId) return this.config.teams?.[organizationId]?.cache;
	}
};
const cloudConfig = new CloudConfig();
//#endregion
//#region src/providers/shared.ts
/**
* The default timeout for API requests in milliseconds.
*/
function getRequestTimeoutMs() {
	return require_logger.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 longContextCost = model.cost.longContext && promptTokens > model.cost.longContext.threshold ? model.cost.longContext : void 0;
	const inputCost = config.inputCost ?? config.cost ?? longContextCost?.input ?? model.cost.input;
	const outputCost = config.outputCost ?? config.cost ?? longContextCost?.output ?? model.cost.output;
	return inputCost * promptTokens + outputCost * completionTokens;
}
/**
* 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 js_yaml.default.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 (require_logger.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: "none" };
		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/scheduler/headerParser.ts
const OPENAI_HEADERS = {
	remainingRequests: "x-ratelimit-remaining-requests",
	remainingTokens: "x-ratelimit-remaining-tokens",
	limitRequests: "x-ratelimit-limit-requests",
	limitTokens: "x-ratelimit-limit-tokens",
	resetRequests: "x-ratelimit-reset-requests",
	resetTokens: "x-ratelimit-reset-tokens"
};
const ANTHROPIC_HEADERS = {
	remainingRequests: "anthropic-ratelimit-requests-remaining",
	remainingTokens: "anthropic-ratelimit-tokens-remaining",
	limitRequests: "anthropic-ratelimit-requests-limit",
	limitTokens: "anthropic-ratelimit-tokens-limit",
	reset: "anthropic-ratelimit-requests-reset"
};
const STANDARD_HEADERS = {
	remaining: "ratelimit-remaining",
	limit: "ratelimit-limit",
	reset: "ratelimit-reset",
	remainingAlt: "x-ratelimit-remaining",
	limitAlt: "x-ratelimit-limit",
	resetAlt: "x-ratelimit-reset"
};
/**
* Parse rate limit headers from response.
*/
function parseRateLimitHeaders(headers) {
	const result = {};
	const h = lowercaseKeys(headers);
	result.remainingRequests = parseFirstMatch(h, [
		OPENAI_HEADERS.remainingRequests,
		ANTHROPIC_HEADERS.remainingRequests,
		STANDARD_HEADERS.remainingAlt,
		STANDARD_HEADERS.remaining
	]);
	result.remainingTokens = parseFirstMatch(h, [OPENAI_HEADERS.remainingTokens, ANTHROPIC_HEADERS.remainingTokens]);
	result.limitRequests = parseFirstMatch(h, [
		OPENAI_HEADERS.limitRequests,
		ANTHROPIC_HEADERS.limitRequests,
		STANDARD_HEADERS.limitAlt,
		STANDARD_HEADERS.limit
	]);
	result.limitTokens = parseFirstMatch(h, [OPENAI_HEADERS.limitTokens, ANTHROPIC_HEADERS.limitTokens]);
	for (const name of [
		OPENAI_HEADERS.resetRequests,
		OPENAI_HEADERS.resetTokens,
		ANTHROPIC_HEADERS.reset,
		STANDARD_HEADERS.resetAlt,
		STANDARD_HEADERS.reset
	]) if (h[name] !== void 0) {
		const parsed = parseResetTime(h[name]);
		if (parsed !== null) {
			result.resetAt = parsed;
			break;
		}
	}
	if (h["retry-after-ms"] !== void 0) {
		const ms = parseInt(h["retry-after-ms"], 10);
		if (!isNaN(ms) && ms >= 0) {
			result.retryAfterMs = ms;
			if (result.resetAt === void 0) result.resetAt = Date.now() + ms;
		}
	} else if (h["retry-after"] !== void 0) {
		const parsed = parseRetryAfter(h["retry-after"]);
		if (parsed !== null) {
			result.retryAfterMs = parsed;
			if (result.resetAt === void 0) result.resetAt = Date.now() + parsed;
		}
	}
	return result;
}
/**
* Parse Retry-After header value.
* Returns duration in milliseconds.
* Exported for integration use.
*/
function parseRetryAfter(value) {
	const seconds = parseInt(value, 10);
	if (!isNaN(seconds) && seconds >= 0 && String(seconds) === value.trim()) return seconds * 1e3;
	const httpDate = parseHttpDate(value);
	if (httpDate !== null) return Math.max(0, httpDate - Date.now());
	return null;
}
function parseFirstMatch(headers, names) {
	for (const name of names) {
		const value = headers[name];
		if (value !== void 0) {
			const num = parseInt(value, 10);
			if (!isNaN(num) && num >= 0) return num;
		}
	}
}
/**
* Parse reset time from various formats.
* Returns absolute Unix timestamp in milliseconds.
*/
function parseResetTime(value) {
	const durationMs = parseDuration(value);
	if (durationMs !== null) return Date.now() + durationMs;
	const num = parseFloat(value);
	if (!isNaN(num)) if (num < 1e9) return Date.now() + num * 1e3;
	else if (num < 1e10) return num * 1e3;
	else return num;
	const httpDate = parseHttpDate(value);
	if (httpDate !== null) return httpDate;
	return null;
}
/**
* Parse HTTP-date format (RFC 7231).
*/
function parseHttpDate(value) {
	const timestamp = Date.parse(value);
	if (!isNaN(timestamp)) {
		const now = Date.now();
		const oneYearMs = 365 * 24 * 60 * 60 * 1e3;
		if (timestamp > now - oneYearMs && timestamp < now + oneYearMs) return timestamp;
	}
	return null;
}
/**
* Parse duration strings like "1s", "100ms", "1m30s", "1h30s", "2h15m30s".
*
* Supported formats:
* - Xms (milliseconds)
* - Xs or X.Xs (seconds)
* - Xm or XmYs (minutes with optional seconds)
* - Xh or XhYm or XhYs or XhYmZs (hours with optional minutes/seconds)
*/
function parseDuration(value) {
	const match = value.match(/^(?:(\d+)h)?(?:(\d+)m(?!s))?(?:(\d+(?:\.\d+)?)(ms|s))?$/);
	if (!match) return null;
	const [, hours, minutes, secondsValue, secondsUnit] = match;
	if (!hours && !minutes && !secondsValue) return null;
	let ms = 0;
	if (hours) ms += parseInt(hours, 10) * 36e5;
	if (minutes) ms += parseInt(minutes, 10) * 6e4;
	if (secondsValue) {
		const num = parseFloat(secondsValue);
		ms += secondsUnit === "ms" ? num : num * 1e3;
	}
	return ms;
}
function lowercaseKeys(obj) {
	const result = {};
	for (const [key, value] of Object.entries(obj)) result[key.toLowerCase()] = value;
	return result;
}
//#endregion
//#region src/util/fetch/errors.ts
/**
* Body-level error codes that indicate a hard quota/billing failure rather than
* a transient per-window rate limit. Retrying these will not succeed and just
* amplifies load and cost; callers should fail fast.
*
* References:
* - OpenAI: https://platform.openai.com/docs/guides/error-codes/api-errors
* - Azure OpenAI: https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
* - Anthropic: https://docs.anthropic.com/en/api/errors
*
* Note: Azure OpenAI is known to return `insufficient_quota` for both billing
* exhaustion AND per-minute deployment quota saturation. The
* {@link HttpRateLimitError} constructor downgrades a quota code to
* `rate_limit` when a small `Retry-After` is also present — a billing server
* has no reason to hint at recovery time.
*/
const HARD_QUOTA_ERROR_CODES = new Set([
	"insufficient_quota",
	"billing_hard_limit_reached",
	"billing_not_active",
	"access_terminated",
	"quota_exceeded"
]);
/**
* Upper bound for the "server hinted a recovery time, so this isn't billing"
* heuristic. A server that says "retry in &lt;= 1 hour" is signalling a
* per-window throttle, not a billing exhaustion. Above 1h we treat a
* Retry-After as ambiguous and let the body code decide.
*/
const RATE_LIMIT_QUOTA_DOWNGRADE_THRESHOLD_MS = 3600 * 1e3;
/**
* Structured error representing a final HTTP rate limit (status 429 or an
* equivalent header signal) — i.e. retries are exhausted, or the response
* code indicates a hard quota that should not be retried.
*
* Distinguishes:
* - `kind: 'quota'`     — daily / billing / contractual exhaustion. Don't retry.
* - `kind: 'rate_limit'` — per-window throttling. Retry honoring `retryAfterMs`.
*
* The rendered `message` always contains the kind prefix (`"Rate limit
* exceeded"` or `"Quota exceeded"`) plus the literal status code. When the
* status is 429 (the typical construction path) the message also contains
* `"429"` and the default `"Too Many Requests"` statusText, which is what
* legacy substring classifiers downstream rely on.
*
* The `Http` prefix on the name scopes this to the network transport
* boundary, distinguishing it from rate-limit-shaped errors in other
* layers (e.g. an in-process scheduler queue exhausting its budget).
*/
var HttpRateLimitError = class extends Error {
	status;
	statusText;
	retryAfterMs;
	resetAt;
	code;
	kind;
	headers;
	body;
	constructor(init) {
		const status = init.status;
		const statusText = init.statusText ?? "Too Many Requests";
		const retryAfterMs = typeof init.retryAfterMs === "number" && init.retryAfterMs >= 0 ? init.retryAfterMs : void 0;
		let kind = isHardQuotaCode(init.code) ? "quota" : "rate_limit";
		if (kind === "quota" && retryAfterMs !== void 0 && retryAfterMs <= RATE_LIMIT_QUOTA_DOWNGRADE_THRESHOLD_MS) kind = "rate_limit";
		const prefix = kind === "quota" ? "Quota exceeded" : "Rate limit exceeded";
		const codeSuffix = init.code ? ` (code: ${init.code})` : "";
		super(`${prefix}: HTTP ${status} ${statusText}${codeSuffix}`);
		this.name = "HttpRateLimitError";
		this.status = status;
		this.statusText = statusText;
		this.retryAfterMs = retryAfterMs;
		this.resetAt = init.resetAt;
		this.code = init.code;
		this.kind = kind;
		this.headers = init.headers ? { ...init.headers } : void 0;
		this.body = init.body;
	}
};
function isHardQuotaCode(code) {
	return code !== void 0 && HARD_QUOTA_ERROR_CODES.has(code);
}
/**
* Render the per-window retry-after / reset detail for a structured
* rate-limit error. Returns empty string when:
* - No retry metadata is available, OR
* - The error is a hard quota (`kind === 'quota'`) — retries won't help,
*   so showing a "retry after Xs" hint would contradict the quota message.
*/
function formatRateLimitDetail(err) {
	if (err.kind === "quota") return "";
	const parts = [];
	if (typeof err.retryAfterMs === "number") parts.push(`retry after ${Math.round(err.retryAfterMs / 1e3)}s`);
	else if (typeof err.resetAt === "number") {
		const remainingMs = Math.max(err.resetAt - Date.now(), 0);
		if (remainingMs > 0) parts.push(`resets in ${Math.round(remainingMs / 1e3)}s`);
	}
	return parts.length > 0 ? ` [${parts.join(", ")}]` : "";
}
/**
* Render the user-facing error string for a structured rate-limit / quota
* failure. Single-prefix format (no provider-specific wrapper duplication):
*
*   "Rate limit exceeded: HTTP 429 Too Many Requests (code: rate_limit_exceeded) [retry after 7s]"
*   "Quota exceeded: HTTP 429 Too Many Requests (code: insufficient_quota). Retries will not help — check your billing or daily quota."
*
* `details` (optional) is appended verbatim before the kind-specific
* trailer. Use it to surface upstream-provided context (e.g. an SDK
* error's full message that names the deployment / token count) that the
* structured error itself doesn't carry.
*/
function formatRateLimitErrorMessage(err, details) {
	const codePart = err.code ? ` (code: ${err.code})` : "";
	const trailingDetails = details ? ` ${details}` : "";
	if (err.kind === "quota") return `Quota exceeded: HTTP ${err.status} ${err.statusText}${codePart}${trailingDetails}. Retries will not help — check your billing or daily quota.`;
	const detail = formatRateLimitDetail(err);
	return `Rate limit exceeded: HTTP ${err.status} ${err.statusText}${codePart}${trailingDetails}${detail}`;
}
function isHttpRateLimitError(err) {
	return err instanceof HttpRateLimitError;
}
/**
* Best-effort extraction of an error code from a parsed response body across
* provider variants (OpenAI / Azure / Anthropic / generic JSON-RPC style).
*/
function extractRateLimitErrorCode(body) {
	if (typeof body !== "object" || body === null) return;
	const root = body;
	if (typeof root.error === "object" && root.error !== null) {
		const err = root.error;
		if (typeof err.code === "string" && err.code.length > 0) return err.code;
		if (typeof err.type === "string" && err.type.length > 0) return err.type;
	}
	if (typeof root.code === "string" && root.code.length > 0) return root.code;
	if (typeof root.type === "string" && root.type.length > 0) return root.type;
}
/**
* Non-transient HTTP status codes that indicate the target is unavailable or misconfigured.
* These errors will not resolve on retry and should abort the scan immediately.
*
* - 401: Unauthorized - authentication required or invalid credentials
* - 403: Forbidden - valid credentials but access denied
* - 404: Not Found - target endpoint doesn't exist
* - 501: Not Implemented - server doesn't support the request method
*
* Excluded: 500 (often transient — server crashes, DB timeouts, deployment rollouts,
* or input-dependent bugs where one prompt triggers it but the next doesn't),
* 502/503/504 (typically transient gateway issues).
*/
const NON_TRANSIENT_HTTP_STATUSES = [
	401,
	403,
	404,
	501
];
function isNonTransientHttpStatus(status) {
	return NON_TRANSIENT_HTTP_STATUSES.includes(status);
}
function isTransientConnectionError(error) {
	if (!error) return false;
	const code = error.code;
	if (code === "ECONNRESET" || code === "EPIPE") return true;
	const message = (error.message ?? "").toLowerCase();
	if (message.includes("eproto") && (message.includes("wrong version number") || message.includes("self signed") || message.includes("unable to verify") || message.includes("unknown ca") || message.includes("cert"))) return false;
	return message.includes("bad record mac") || message.includes("eproto") || message.includes("econnreset") || message.includes("socket hang up");
}
//#endregion
//#region src/util/fetch/monkeyPatchFetch.ts
const gzipAsync = (0, util.promisify)(zlib.gzip);
function isConnectionError(error) {
	return error instanceof TypeError && error.message === "fetch failed" && error.cause?.stack?.includes("internalConnectMultiple");
}
/** Extracts the request URL as a string. A `Request`'s `toString()` is "[object Request]", so read `.url`. */
function getRequestUrlString(url) {
	return url instanceof Request ? url.url : url.toString();
}
function matchesNoLogUrl(url, noLogUrl) {
	try {
		const target = new URL(url);
		const excluded = new URL(noLogUrl);
		if (target.origin !== excluded.origin) return false;
		const excludedPath = excluded.pathname.replace(/\/+$/, "");
		return excludedPath === "" || target.pathname === excludedPath || target.pathname.startsWith(`${excludedPath}/`);
	} catch {
		return false;
	}
}
function getSafeUrlForConnectionLog(url) {
	return require_logger.sanitizeUrl(getRequestUrlString(url));
}
function getSafeProxyForConnectionLog() {
	const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
	return proxyUrl ? `or Proxy: ${require_logger.sanitizeUrl(proxyUrl)}` : "";
}
/**
* Returns true when `url` targets the configured Promptfoo Cloud origin: the public
* cloud host by default, or the on-prem host when one is configured via
* `cloudConfig.getApiHost()`. Matching is scheme+host+port exact (`URL.origin`), so
* look-alike hosts, HTTP downgrades, and different ports never match. Matching is
* origin-wide — every path on the configured cloud origin is treated as cloud, so the
* saved token may also reach sibling services hosted on that same origin. Fails closed
* (returns false) when either URL is unparseable, so a misconfigured host never leaks
* the token.
*/
function isPromptfooCloudApiHost(url) {
	try {
		const targetUrl = url instanceof Request ? url.url : url.toString();
		return new URL(targetUrl).origin === new URL(cloudConfig.getApiHost()).origin;
	} catch {
		return false;
	}
}
/**
* Resolves the `Authorization` header value for a request to the configured Promptfoo
* Cloud origin, or `undefined` when the request is not cloud-bound or no API key is
* saved. Centralizing this keeps the live request (`monkeyPatchFetch`) and the cache
* key (`getHeadersForCacheKey` in cache.ts) in lockstep.
*/
function getCloudBearerToken(url) {
	if (!isPromptfooCloudApiHost(url)) return;
	const token = cloudConfig.getApiKey();
	return token ? `Bearer ${token}` : void 0;
}
/**
* Resolves the caller-supplied headers for a request: the explicit `options.headers` when
* present, otherwise the headers carried by a `Request` URL.
*
* `monkeyPatchFetch`'s only in-repo caller (`fetchWithProxy`) always normalizes headers to
* a plain object, but the function is exported and typed to accept any `HeadersInit` /
* `Request`, so the helpers here preserve every shape rather than assuming a `Record`.
*/
function getEffectiveHeaders(url, headers) {
	return headers ?? (url instanceof Request ? url.headers : void 0);
}
/** Case-insensitive check for a caller-supplied `Authorization` header (any `HeadersInit` shape). */
function hasAuthorizationHeader(headers) {
	return new Headers(headers).has("authorization");
}
/**
* Returns `headers` with `name: value` set. Plain objects retain their shape, while a
* `Headers` instance or `[name, value][]` array is normalized through `Headers` so existing
* entries are never dropped (a naive object spread would discard them).
*/
function setHeader(headers, name, value) {
	if (headers instanceof Headers || Array.isArray(headers)) {
		const merged = new Headers(headers);
		merged.set(name, value);
		return merged;
	}
	return {
		...headers ?? {},
		[name]: value
	};
}
/**
* 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 urlString = getRequestUrlString(url);
	const callerHeaders = getEffectiveHeaders(url, options?.headers);
	const isSilent = new Headers(callerHeaders).get("x-promptfoo-silent") === "true";
	const logEnabled = !NO_LOG_URLS.some((logUrl) => matchesNoLogUrl(urlString, 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 = setHeader(getEffectiveHeaders(url, opts.headers), "Content-Encoding", "gzip");
	} catch (e) {
		require_logger.logger.warn(`Failed to compress request body: ${e}`);
	}
	const cloudAuth = getCloudBearerToken(url);
	const effectiveHeaders = getEffectiveHeaders(url, opts.headers);
	if (cloudAuth && !hasAuthorizationHeader(effectiveHeaders)) opts.headers = setHeader(effectiveHeaders, "Authorization", cloudAuth);
	try {
		const response = await fetch(url, opts);
		if (logEnabled) require_logger.logRequestResponse({
			url: urlString,
			requestBody: originalBody,
			requestMethod: opts.method || "GET",
			response
		});
		return response;
	} catch (e) {
		if (logEnabled) {
			require_logger.logRequestResponse({
				url: urlString,
				requestBody: opts.body,
				requestMethod: opts.method || "GET",
				response: null
			});
			if (isConnectionError(e)) {
				require_logger.logger.debug(`Connection error, please check your network connectivity to the host: ${getSafeUrlForConnectionLog(url)} ${getSafeProxyForConnectionLog()}`);
				throw e;
			}
			require_logger.logger.debug(`Error in fetch: ${JSON.stringify(e, Object.getOwnPropertyNames(e), 2)} ${e instanceof Error ? e.stack : ""}`);
		}
		throw e;
	}
}
//#endregion
//#region src/util/fetch/retryContext.ts
const fetchRetryContext = new node_async_hooks.AsyncLocalStorage();
/**
* Run `fn` with a fetch retry context so nested `fetchWithRetries` /
* `fetchWithProxy` calls inherit the provider's configured `maxRetries`.
*
* When `maxRetries` is `undefined`, the new scope deliberately shadows any
* outer provider context so providers without an override fall back to defaults.
*/
function withFetchRetryContext(maxRetries, fn) {
	return fetchRetryContext.run({ maxRetries }, fn);
}
/**
* Read the active context's `maxRetries`, or `undefined` when none is set.
*/
function getFetchRetryContextMaxRetries() {
	return fetchRetryContext.getStore()?.maxRetries;
}
//#endregion
//#region src/util/fetch/index.ts
var fetch_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
	computeRateLimitWaitMs: () => computeRateLimitWaitMs,
	fetchWithProxy: () => fetchWithProxy,
	fetchWithRetries: () => fetchWithRetries,
	fetchWithTimeout: () => fetchWithTimeout,
	getFetchWithProxyHeaders: () => getFetchWithProxyHeaders,
	handleRateLimit: () => handleRateLimit,
	isRateLimited: () => isRateLimited,
	isTransientError: () => isTransientError
});
const cachedAgents = /* @__PURE__ */ new Map();
const cachedProxyAgents = /* @__PURE__ */ new Map();
/**
* Get the connection pool size for HTTP agents.
* Priority: PROMPTFOO_FETCH_CONNECTIONS env var > CLI -j flag > DEFAULT_MAX_CONCURRENCY (4).
* Set PROMPTFOO_FETCH_CONNECTIONS to override independently of eval concurrency
* (e.g., server deployments that need more connections than the default 4).
*/
function getConnectionPoolSize() {
	const envConnections = require_logger.getEnvString("PROMPTFOO_FETCH_CONNECTIONS");
	if (envConnections != null) {
		const parsed = parseInt(envConnections, 10);
		if (!isNaN(parsed)) return parsed;
	}
	return require_logger.state.maxConcurrency || 4;
}
function getOrCreateAgent(tlsOptions) {
	const concurrency = getConnectionPoolSize();
	const existing = cachedAgents.get(concurrency);
	if (existing) return existing;
	const agent = new undici.Agent({
		headersTimeout: getRequestTimeoutMs(),
		keepAliveTimeout: 3e4,
		keepAliveMaxTimeout: 6e4,
		connections: concurrency,
		connect: tlsOptions
	}).compose(undici.interceptors.decompress({ skipErrorResponses: false }));
	cachedAgents.set(concurrency, agent);
	return agent;
}
function getProxyAgentCacheKey(proxyUrl, concurrency) {
	return `${proxyUrl}::${concurrency}`;
}
function getOrCreateProxyAgent(proxyUrl, tlsOptions) {
	const concurrency = getConnectionPoolSize();
	const cacheKey = getProxyAgentCacheKey(proxyUrl, concurrency);
	const existing = cachedProxyAgents.get(cacheKey);
	if (existing) return existing;
	const agent = new undici.ProxyAgent({
		uri: proxyUrl,
		proxyTls: tlsOptions,
		requestTls: tlsOptions,
		headersTimeout: getRequestTimeoutMs(),
		keepAliveTimeout: 3e4,
		keepAliveMaxTimeout: 6e4,
		connections: concurrency
	}).compose(undici.interceptors.decompress({ skipErrorResponses: false }));
	cachedProxyAgents.set(cacheKey, agent);
	return agent;
}
/**
* Resolve whether to disable transient-error retries. An explicit caller flag
* always wins (a caller may opt back in even when the provider set
* `maxRetries: 0`); otherwise we disable only when the retry context carries
* `maxRetries === 0`.
*/
function resolveTransientRetryDisabled(explicit) {
	if (explicit !== void 0) return explicit;
	return getFetchRetryContextMaxRetries() === 0;
}
function headersInitToRecord(headers) {
	if (!headers) return {};
	if (headers instanceof Headers) return Object.fromEntries(headers.entries());
	if (Array.isArray(headers)) return Object.fromEntries(new Headers(headers).entries());
	return { ...headers };
}
function getFetchWithProxyHeaders(url, options) {
	const requestHeaders = url instanceof Request && options.headers === void 0 ? headersInitToRecord(url.headers) : {};
	const optionHeaders = headersInitToRecord(options.headers);
	return {
		...requestHeaders,
		...optionHeaders,
		"x-promptfoo-version": VERSION
	};
}
function getFetchUrlString(url) {
	if (typeof url === "string") return url;
	if (url instanceof URL) return url.toString();
	if (url instanceof Request) return url.url;
}
async function fetchWithProxy(url, options = {}, abortSignal) {
	let finalUrl = url;
	let finalUrlString = getFetchUrlString(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: getFetchWithProxyHeaders(url, options),
		signal: combinedSignal
	};
	if (typeof url === "string") try {
		const parsedUrl = new URL(url);
		if (parsedUrl.username || parsedUrl.password) {
			if (finalOptions.headers && "Authorization" in finalOptions.headers) require_logger.logger.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) {
		require_logger.logger.debug(`URL parsing failed in fetchWithProxy: ${e}`);
	}
	const tlsOptions = { rejectUnauthorized: !require_logger.getEnvBool("PROMPTFOO_INSECURE_SSL", true) };
	const caCertPath = require_logger.getEnvString("PROMPTFOO_CA_CERT_PATH");
	if (caCertPath) try {
		const resolvedPath = path.default.resolve(require_logger.state.basePath || "", caCertPath);
		tlsOptions.ca = await node_fs_promises.readFile(resolvedPath, "utf8");
		require_logger.logger.debug(`Using custom CA certificate from ${resolvedPath}`);
	} catch (e) {
		require_logger.logger.warn(`Failed to read CA certificate from ${caCertPath}: ${e}`);
	}
	const proxyUrl = finalUrlString ? (0, proxy_from_env.getProxyForUrl)(finalUrlString) : "";
	if (!finalOptions.dispatcher) if (proxyUrl) {
		require_logger.logger.debug(`Using proxy: ${require_logger.sanitizeUrl(proxyUrl)}`);
		finalOptions.dispatcher = getOrCreateProxyAgent(proxyUrl, tlsOptions);
	} else finalOptions.dispatcher = getOrCreateAgent(tlsOptions);
	const disableTransientRetries = resolveTransientRetryDisabled(options.disableTransientRetries);
	const maxTransientRetries = disableTransientRetries ? 0 : 3;
	for (let attempt = 0; attempt <= maxTransientRetries; attempt++) {
		const response = await monkeyPatchFetch(finalUrl, finalOptions);
		if (!disableTransientRetries && isTransientError(response) && attempt < maxTransientRetries) {
			const backoffMs = Math.pow(2, attempt) * 1e3;
			require_logger.logger.debug(`Transient error (${response.status} ${response.statusText}), retry ${attempt + 1}/${maxTransientRetries} after ${backoffMs}ms`);
			await require_time.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) {
	require_invariant.invariant(response.headers, "Response headers are missing");
	require_invariant.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";
}
/**
* Compute how long to wait after a rate-limited response.
* Reads `Retry-After`, `X-RateLimit-Reset`, and OpenAI-style reset headers.
* Default: 60s.
*/
function computeRateLimitWaitMs(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) {
		const parsedHeaders = parseRateLimitHeaders(Object.fromEntries(response.headers.entries()));
		if (parsedHeaders.resetAt !== void 0) waitTime = Math.max(parsedHeaders.resetAt - Date.now(), 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 = parseRetryAfter(retryAfter) ?? waitTime;
	return waitTime;
}
/**
* Maximum jitter (in ms) added to rate-limit waits. Spreads concurrent
* waiters so they don't all retry in the same instant after Retry-After.
*/
const RATE_LIMIT_JITTER_MS = 1e3;
/**
* Handle rate limiting by waiting the appropriate amount of time, plus a
* uniform random jitter to avoid synchronized retry storms when many
* concurrent requests hit the same rate limit.
*/
async function handleRateLimit(response) {
	const waitTime = computeRateLimitWaitMs(response);
	const jitter = Math.floor(Math.random() * RATE_LIMIT_JITTER_MS);
	const totalWait = waitTime + jitter;
	require_logger.logger.debug(`Rate limited, waiting ${totalWait}ms (base ${waitTime}ms + ${jitter}ms jitter) before retry`);
	await require_time.sleep(totalWait);
}
/**
* Hard cap on bytes read from a 429 response body during classification.
* A hostile or buggy upstream could otherwise stream a multi-MB body on
* every 429, amplifying memory pressure under concurrent eval load. 64 KB
* is well above any well-formed provider error envelope (typical OpenAI
* / Anthropic JSON errors are <1 KB).
*/
const RATE_LIMIT_BODY_PEEK_BYTES = 64 * 1024;
/**
* Read the body of a rate-limited response without consuming the original
* stream. Returns the parsed body (JSON if parseable, else raw text) and
* any extracted error code (e.g. `insufficient_quota`).
*
* Why clone: `Response.text()` consumes the underlying stream. The original
* response may still be observed by upstream wrappers (logging middleware,
* monkey-patched fetch); cloning preserves their ability to read the body.
*
* Failures (clone, read, parse) degrade to `{ body: undefined, code:
* undefined }` and are logged at debug level. Losing the body code only
* widens classification from `quota` to `rate_limit`, which is the safer
* (retryable) side of the misclassification.
*/
async function peekRateLimitBody(response) {
	let cloned;
	try {
		cloned = response.clone();
	} catch (err) {
		require_logger.logger.debug(`[fetch] peekRateLimitBody: clone failed, skipping body code lookup: ${err}`);
		return {
			body: void 0,
			code: void 0
		};
	}
	let text;
	try {
		text = await readBoundedText(cloned, RATE_LIMIT_BODY_PEEK_BYTES);
	} catch (err) {
		require_logger.logger.debug(`[fetch] peekRateLimitBody: body read failed: ${err}`);
		return {
			body: void 0,
			code: void 0
		};
	}
	if (!text) return {
		body: void 0,
		code: void 0
	};
	try {
		const json = JSON.parse(text);
		return {
			body: json,
			code: extractRateLimitErrorCode(json)
		};
	} catch {
		require_logger.logger.debug("[fetch] peekRateLimitBody: response body was not JSON");
		return {
			body: text,
			code: void 0
		};
	}
}
/**
* Drain a Response's body into a string, but stop reading once `maxBytes`
* have been collected. Each streamed chunk is bounded to the remaining
* budget *before* it enters the in-memory buffer, so a single oversized
* chunk cannot exceed `maxBytes` of retained memory. Falls back to
* `.text()` when the body stream isn't available (some Response polyfills);
* in that path we consult `Content-Length` first to skip materializing
* very large bodies entirely.
*/
async function readBoundedText(response, maxBytes) {
	if (!response.body) {
		const contentLength = Number.parseInt(response.headers?.get?.("content-length") ?? "", 10);
		if (Number.isFinite(contentLength) && contentLength > maxBytes) return "";
		const text = await response.text();
		return text.length > maxBytes ? text.slice(0, maxBytes) : text;
	}
	const reader = response.body.getReader();
	const chunks = [];
	let total = 0;
	try {
		while (total < maxBytes) {
			const { done, value } = await reader.read();
			if (done) break;
			const room = maxBytes - total;
			const bounded = value.byteLength <= room ? value : value.slice(0, room);
			chunks.push(bounded);
			total += bounded.byteLength;
		}
	} finally {
		try {
			await reader.cancel();
		} catch {}
	}
	const merged = new Uint8Array(total);
	let offset = 0;
	for (const chunk of chunks) {
		merged.set(chunk, offset);
		offset += chunk.byteLength;
	}
	return new TextDecoder().decode(merged);
}
function buildHttpRateLimitError(response, body, code) {
	const headers = Object.fromEntries(response.headers.entries());
	const parsed = parseRateLimitHeaders(headers);
	return new HttpRateLimitError({
		status: response.status,
		statusText: response.statusText,
		retryAfterMs: parsed.retryAfterMs,
		resetAt: parsed.resetAt,
		code,
		headers,
		body
	});
}
/**
* 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");
		case 524: return statusText.includes("timeout");
		default: return false;
	}
}
/**
* Decide what to do with a rate-limited response inside `fetchWithRetries`.
*
* Throws on hard-quota fail-fast or retry exhaustion (with a structured
* {@link HttpRateLimitError} for status 429, or a plain `Error` for the soft
* `X-RateLimit-Remaining=0` 200 case). Otherwise sleeps via
* {@link handleRateLimit} and returns so the caller can `continue` the loop.
*/
/**
* Returns a string form of a {@link RequestInfo} suitable for log output.
* Strips basic-auth credentials and known sensitive query parameters (api_key,
* token, password, signature, …) via {@link sanitizeUrl} so providers that
* embed credentials in the URL (e.g. n8n webhooks, HTTP providers with
* `?api_key=…`) do not leak them into retry diagnostics.
*/
function urlForLog(url) {
	return require_logger.sanitizeUrl(typeof url === "string" ? url : url.url);
}
async function handleRateLimitedResponse(response, url, attempt, maxRetries) {
	const isHardRateLimit = response.status === 429;
	const { body, code } = isHardRateLimit ? await peekRateLimitBody(response) : {
		body: void 0,
		code: void 0
	};
	const safeUrl = urlForLog(url);
	if (isHardRateLimit && isHardQuotaCode(code)) {
		require_logger.logger.debug(`Quota exhausted on URL ${safeUrl}: HTTP ${response.status} (code: ${code}), failing fast.`);
		throw buildHttpRateLimitError(response, body, code);
	}
	if (attempt >= maxRetries) {
		if (isHardRateLimit) {
			require_logger.logger.debug(`Rate limited on URL ${safeUrl}: HTTP ${response.status} ${response.statusText}, attempt ${attempt + 1}/${maxRetries + 1}, no retries remain.`);
			throw buildHttpRateLimitError(response, body, code);
		}
		throw new Error(`Rate limited: ${response.status} ${response.statusText} after ${maxRetries + 1} attempts`);
	}
	require_logger.logger.debug(`Rate limited on URL ${safeUrl}: HTTP ${response.status} ${response.statusText}, attempt ${attempt + 1}/${maxRetries + 1}, waiting before retry...`);
	await handleRateLimit(response);
}
function formatFetchErrorMessage(error) {
	if (!(error instanceof Error)) return String(error);
	const typedError = error;
	let message = `${typedError.name}: ${typedError.message}`;
	if (typedError.cause) message += ` (Cause: ${typedError.cause})`;
	if (typedError.code) message += ` (Code: ${typedError.code})`;
	return message;
}
async function fetchWithRetries(url, options = {}, timeout, maxRetries) {
	const contextMaxRetries = getFetchRetryContextMaxRetries();
	maxRetries = Math.max(0, maxRetries ?? contextMaxRetries ?? 4);
	let lastErrorMessage;
	const backoff = require_logger.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 (require_logger.getEnvBool("PROMPTFOO_RETRY_5XX") && response.status >= 500 && response.status < 600) throw new Error(`Internal Server Error: ${response.status} ${response.statusText}`);
			if (response && isRateLimited(response)) {
				await handleRateLimitedResponse(response, url, i, maxRetries);
				continue;
			}
			return response;
		} catch (error) {
			if (error instanceof Error && error.name === "AbortError") throw error;
			if (error instanceof HttpRateLimitError) throw error;
			const errorMessage = formatFetchErrorMessage(error);
			require_logger.logger.debug(`Request to ${urlForLog(url)} failed (attempt #${i + 1}), retrying: ${errorMessage}`);
			if (i < maxRetries) await require_time.sleep(Math.pow(2, i) * (backoff + 1e3 * Math.random()));
			lastErrorMessage = errorMessage;
		}
	}
	throw new Error(`Request failed after ${maxRetries} retries: ${lastErrorMessage}`);
}
//#endregion
Object.defineProperty(exports, "CLOUD_PROVIDER_PREFIX", {
	enumerable: true,
	get: function() {
		return CLOUD_PROVIDER_PREFIX;
	}
});
Object.defineProperty(exports, "CONSENT_ENDPOINT", {
	enumerable: true,
	get: function() {
		return CONSENT_ENDPOINT;
	}
});
Object.defineProperty(exports, "CloudConfig", {
	enumerable: true,
	get: function() {
		return CloudConfig;
	}
});
Object.defineProperty(exports, "EVENTS_ENDPOINT", {
	enumerable: true,
	get: function() {
		return EVENTS_ENDPOINT;
	}
});
Object.defineProperty(exports, "FILE_METADATA_KEY", {
	enumerable: true,
	get: function() {
		return FILE_METADATA_KEY;
	}
});
Object.defineProperty(exports, "HUMAN_ASSERTION_TYPE", {
	enumerable: true,
	get: function() {
		return HUMAN_ASSERTION_TYPE;
	}
});
Object.defineProperty(exports, "HttpRateLimitError", {
	enumerable: true,
	get: function() {
		return HttpRateLimitError;
	}
});
Object.defineProperty(exports, "LONG_RUNNING_MODEL_TIMEOUT_MS", {
	enumerable: true,
	get: function() {
		return LONG_RUNNING_MODEL_TIMEOUT_MS;
	}
});
Object.defineProperty(exports, "NON_TRANSIENT_HTTP_STATUSES", {
	enumerable: true,
	get: function() {
		return NON_TRANSIENT_HTTP_STATUSES;
	}
});
Object.defineProperty(exports, "POSTHOG_KEY", {
	enumerable: true,
	get: function() {
		return POSTHOG_KEY;
	}
});
Object.defineProperty(exports, "R_ENDPOINT", {
	enumerable: true,
	get: function() {
		return R_ENDPOINT;
	}
});
Object.defineProperty(exports, "TERMINAL_MAX_WIDTH", {
	enumerable: true,
	get: function() {
		return TERMINAL_MAX_WIDTH;
	}
});
Object.defineProperty(exports, "VERSION", {
	enumerable: true,
	get: function() {
		return VERSION;
	}
});
Object.defineProperty(exports, "calculateCost", {
	enumerable: true,
	get: function() {
		return calculateCost;
	}
});
Object.defineProperty(exports, "cloudConfig", {
	enumerable: true,
	get: function() {
		return cloudConfig;
	}
});
Object.defineProperty(exports, "extractRateLimitErrorCode", {
	enumerable: true,
	get: function() {
		return extractRateLimitErrorCode;
	}
});
Object.defineProperty(exports, "fetchWithProxy", {
	enumerable: true,
	get: function() {
		return fetchWithProxy;
	}
});
Object.defineProperty(exports, "fetchWithRetries", {
	enumerable: true,
	get: function() {
		return fetchWithRetries;
	}
});
Object.defineProperty(exports, "fetchWithTimeout", {
	enumerable: true,
	get: function() {
		return fetchWithTimeout;
	}
});
Object.defineProperty(exports, "fetch_exports", {
	enumerable: true,
	get: function() {
		return fetch_exports;
	}
});
Object.defineProperty(exports, "formatRateLimitErrorMessage", {
	enumerable: true,
	get: function() {
		return formatRateLimitErrorMessage;
	}
});
Object.defineProperty(exports, "getCloudBearerToken", {
	enumerable: true,
	get: function() {
		return getCloudBearerToken;
	}
});
Object.defineProperty(exports, "getDefaultShareViewBaseUrl", {
	enumerable: true,
	get: function() {
		return getDefaultShareViewBaseUrl;
	}
});
Object.defineProperty(exports, "getFetchWithProxyHeaders", {
	enumerable: true,
	get: function() {
		return getFetchWithProxyHeaders;
	}
});
Object.defineProperty(exports, "getRequestTimeoutMs", {
	enumerable: true,
	get: function() {
		return getRequestTimeoutMs;
	}
});
Object.defineProperty(exports, "getShareApiBaseUrl", {
	enumerable: true,
	get: function() {
		return getShareApiBaseUrl;
	}
});
Object.defineProperty(exports, "getShareViewBaseUrl", {
	enumerable: true,
	get: function() {
		return getShareViewBaseUrl;
	}
});
Object.defineProperty(exports, "isHttpRateLimitError", {
	enumerable: true,
	get: function() {
		return isHttpRateLimitError;
	}
});
Object.defineProperty(exports, "isNonTransientHttpStatus", {
	enumerable: true,
	get: function() {
		return isNonTransientHttpStatus;
	}
});
Object.defineProperty(exports, "isOpenAIToolArray", {
	enumerable: true,
	get: function() {
		return isOpenAIToolArray;
	}
});
Object.defineProperty(exports, "isOpenAIToolChoice", {
	enumerable: true,
	get: function() {
		return isOpenAIToolChoice;
	}
});
Object.defineProperty(exports, "isPromptfooSampleTarget", {
	enumerable: true,
	get: function() {
		return isPromptfooSampleTarget;
	}
});
Object.defineProperty(exports, "isTransientConnectionError", {
	enumerable: true,
	get: function() {
		return isTransientConnectionError;
	}
});
Object.defineProperty(exports, "openaiToolChoiceToBedrock", {
	enumerable: true,
	get: function() {
		return openaiToolChoiceToBedrock;
	}
});
Object.defineProperty(exports, "openaiToolsToBedrock", {
	enumerable: true,
	get: function() {
		return openaiToolsToBedrock;
	}
});
Object.defineProperty(exports, "parseChatPrompt", {
	enumerable: true,
	get: function() {
		return parseChatPrompt;
	}
});
Object.defineProperty(exports, "parseRateLimitHeaders", {
	enumerable: true,
	get: function() {
		return parseRateLimitHeaders;
	}
});
Object.defineProperty(exports, "parseRetryAfter", {
	enumerable: true,
	get: function() {
		return parseRetryAfter;
	}
});
Object.defineProperty(exports, "readGlobalConfig", {
	enumerable: true,
	get: function() {
		return readGlobalConfig;
	}
});
Object.defineProperty(exports, "toTitleCase", {
	enumerable: true,
	get: function() {
		return toTitleCase;
	}
});
Object.defineProperty(exports, "transformToolChoice", {
	enumerable: true,
	get: function() {
		return transformToolChoice;
	}
});
Object.defineProperty(exports, "transformTools", {
	enumerable: true,
	get: function() {
		return transformTools;
	}
});
Object.defineProperty(exports, "withFetchRetryContext", {
	enumerable: true,
	get: function() {
		return withFetchRetryContext;
	}
});
Object.defineProperty(exports, "writeGlobalConfig", {
	enumerable: true,
	get: function() {
		return writeGlobalConfig;
	}
});
Object.defineProperty(exports, "writeGlobalConfigPartial", {
	enumerable: true,
	get: function() {
		return writeGlobalConfigPartial;
	}
});

//# sourceMappingURL=fetch-Hdu2Q-w3.cjs.map