import { _ as getEnvBool, b as getEnvString, c as sanitizeUrl, g as getConfigDirectoryPath, i as logger_default, r as logRequestResponse, w as cliState_default, y as getEnvInt } from "./logger-q5I8CByj.js";
import { t as invariant } from "./invariant-1pAf2CD1.js";
import { n as sleep } from "./time-CRRzn6vA.js";
import * as fs$1 from "fs";
import * as path$1 from "path";
import path from "path";
import yaml from "js-yaml";
import { promisify } from "util";
import * as fsPromises from "fs/promises";
import { getProxyForUrl } from "proxy-from-env";
import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
import { gzip } from "zlib";

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

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

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

//#endregion
//#region src/providers/shared.ts
/**
* The default timeout for API requests in milliseconds.
*/
const REQUEST_TIMEOUT_MS = getEnvInt("REQUEST_TIMEOUT_MS", 3e5);
/**
* Extended timeout for long-running models (deep research, gpt-5-pro, etc.) in milliseconds.
* These models can take significantly longer to respond due to their complex reasoning.
*/
const LONG_RUNNING_MODEL_TIMEOUT_MS = 6e5;
/**
* Calculates the cost of an API call based on the model and token usage.
*
* @param {string} modelName The name of the model used.
* @param {ProviderConfig} config The provider configuration.
* @param {number | undefined} promptTokens The number of tokens in the prompt.
* @param {number | undefined} completionTokens The number of tokens in the completion.
* @param {ProviderModel[]} models An array of available models with their costs.
* @returns {number | undefined} The calculated cost, or undefined if it can't be calculated.
*/
function calculateCost(modelName, config, promptTokens, completionTokens, models) {
	if (!Number.isFinite(promptTokens) || !Number.isFinite(completionTokens) || typeof promptTokens === "undefined" || typeof completionTokens === "undefined") return;
	const model = models.find((m) => m.id === modelName);
	if (!model || !model.cost) return;
	const inputCost = config.cost ?? model.cost.input;
	const outputCost = config.cost ?? model.cost.output;
	return inputCost * promptTokens + outputCost * completionTokens || void 0;
}
/**
* Checks if a string looks like it's attempting to be JSON.
* This helps distinguish between actual JSON attempts and plain text that happens to start/end with brackets.
*/
function looksLikeJson(prompt) {
	const trimmed = prompt.trim();
	if (trimmed.startsWith("{") && trimmed.endsWith("}")) return true;
	if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
		const afterBracket = trimmed.slice(1).trimStart();
		if (afterBracket.startsWith("\"") || afterBracket.startsWith("{") || afterBracket.startsWith("[") || /^[\d-]/.test(afterBracket) || /^(true|false|null)/.test(afterBracket)) return true;
		if (afterBracket.length === 0 || /^\s+$/.test(afterBracket)) return true;
		return false;
	}
	return false;
}
/**
* Parses a chat prompt string into a structured format.
*
* @template T The expected return type of the parsed prompt.
* @param {string} prompt The input prompt string to parse.
* @param {T} defaultValue The default value to return if parsing fails.
* @returns {T} The parsed prompt or the default value.
* @throws {Error} If the prompt is invalid YAML or JSON (when required).
*/
function parseChatPrompt(prompt, defaultValue) {
	const trimmedPrompt = prompt.trim();
	if (trimmedPrompt.startsWith("- role:")) try {
		return yaml.load(prompt);
	} catch (err) {
		throw new Error(`Chat Completion prompt is not a valid YAML string: ${err}\n\n${prompt}`);
	}
	else try {
		return JSON.parse(prompt);
	} catch (err) {
		if (getEnvBool("PROMPTFOO_REQUIRE_JSON_PROMPTS") || looksLikeJson(trimmedPrompt)) throw new Error(`Chat Completion prompt is not a valid JSON string: ${err}\n\n${prompt}`);
		return defaultValue;
	}
}
/**
* Converts a string to title case.
*
* @param {string} str The input string to convert.
* @returns {string} The input string converted to title case.
*/
function toTitleCase(str) {
	return str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
}
function isPromptfooSampleTarget(provider) {
	const url = provider.config?.url;
	return url?.includes("promptfoo.app") || url?.includes("promptfoo.dev");
}
/**
* Checks if the given value is an OpenAI tool choice format.
* Detects string values ('auto', 'none', 'required') and
* the object form ({ type: 'function', function: { name } }).
*/
function isOpenAIToolChoice(obj) {
	if (typeof obj === "string") return [
		"auto",
		"none",
		"required"
	].includes(obj);
	if (typeof obj === "object" && obj !== null) {
		const candidate = obj;
		if (candidate.type === "function" && typeof candidate.function === "object" && candidate.function !== null) return typeof candidate.function.name === "string";
	}
	return false;
}
/**
* Transforms an OpenAI tool choice to Anthropic format.
*/
function openaiToolChoiceToAnthropic(choice) {
	if (typeof choice === "string") switch (choice) {
		case "auto": return { type: "auto" };
		case "none": return { type: "auto" };
		case "required": return { type: "any" };
	}
	return {
		type: "tool",
		name: choice.function.name
	};
}
/**
* Transforms an OpenAI tool choice to Bedrock Converse format.
*/
function openaiToolChoiceToBedrock(choice) {
	if (typeof choice === "string") switch (choice) {
		case "auto": return { auto: {} };
		case "none": return;
		case "required": return { any: {} };
	}
	return { tool: { name: choice.function.name } };
}
/**
* Transforms an OpenAI tool choice to Google (Gemini) format.
*/
function openaiToolChoiceToGoogle(choice) {
	if (typeof choice === "string") switch (choice) {
		case "auto": return { functionCallingConfig: { mode: "AUTO" } };
		case "none": return { functionCallingConfig: { mode: "NONE" } };
		case "required": return { functionCallingConfig: { mode: "ANY" } };
	}
	return { functionCallingConfig: {
		mode: "ANY",
		allowedFunctionNames: [choice.function.name]
	} };
}
/**
* Transforms an OpenAI tool choice to the specified provider format.
* If the input is not in OpenAI format, it's returned as-is (native passthrough).
*/
function transformToolChoice(toolChoice, format) {
	if (!isOpenAIToolChoice(toolChoice)) return toolChoice;
	switch (format) {
		case "openai": return toolChoice;
		case "anthropic": return openaiToolChoiceToAnthropic(toolChoice);
		case "bedrock": return openaiToolChoiceToBedrock(toolChoice);
		case "google": return openaiToolChoiceToGoogle(toolChoice);
		default: return toolChoice;
	}
}
/**
* Checks if an array contains OpenAI-format tools.
* Returns true if the first tool has `type: 'function'` and `function.name`.
*/
function isOpenAIToolArray(tools) {
	if (!Array.isArray(tools) || tools.length === 0) return false;
	const first = tools[0];
	if (typeof first !== "object" || first === null) return false;
	const candidate = first;
	return candidate.type === "function" && typeof candidate.function === "object" && candidate.function !== null && typeof candidate.function.name === "string";
}
/**
* Transforms OpenAI-format tools to Anthropic format.
*/
function openaiToolsToAnthropic(tools) {
	return tools.map((tool) => ({
		name: tool.function.name,
		...tool.function.description ? { description: tool.function.description } : {},
		input_schema: tool.function.parameters || {
			type: "object",
			properties: {}
		}
	}));
}
/**
* Transforms OpenAI-format tools to Bedrock Converse format.
*/
function openaiToolsToBedrock(tools) {
	return tools.map((tool) => ({ toolSpec: {
		name: tool.function.name,
		...tool.function.description ? { description: tool.function.description } : {},
		inputSchema: { json: tool.function.parameters || {
			type: "object",
			properties: {}
		} }
	} }));
}
/**
* Sanitizes a schema for Google/Gemini compatibility.
* - Converts type strings to uppercase (string → STRING)
* - Removes unsupported properties (additionalProperties, $schema, default)
* - Recursively processes nested schemas
*/
function sanitizeSchemaForGoogle(schema) {
	const result = {};
	for (const [key, value] of Object.entries(schema)) {
		if ([
			"additionalProperties",
			"$schema",
			"default",
			"$id",
			"$ref"
		].includes(key)) continue;
		if (key === "type" && typeof value === "string") result[key] = value.toUpperCase();
		else if (key === "properties" && typeof value === "object" && value !== null) {
			const sanitizedProps = {};
			for (const [propKey, propValue] of Object.entries(value)) if (typeof propValue === "object" && propValue !== null) sanitizedProps[propKey] = sanitizeSchemaForGoogle(propValue);
			else sanitizedProps[propKey] = propValue;
			result[key] = sanitizedProps;
		} else if (key === "items" && typeof value === "object" && value !== null) result[key] = sanitizeSchemaForGoogle(value);
		else result[key] = value;
	}
	return result;
}
/**
* Transforms OpenAI-format tools to Google/Gemini format.
*/
function openaiToolsToGoogle(tools) {
	return [{ functionDeclarations: tools.map((tool) => ({
		name: tool.function.name,
		...tool.function.description ? { description: tool.function.description } : {},
		...tool.function.parameters ? { parameters: sanitizeSchemaForGoogle(tool.function.parameters) } : {}
	})) }];
}
/**
* Transforms tools from OpenAI format to the specified provider format.
* If the input is not in OpenAI format, it's returned as-is.
*/
function transformTools(tools, format) {
	if (!isOpenAIToolArray(tools)) return tools;
	switch (format) {
		case "openai": return tools;
		case "anthropic": return openaiToolsToAnthropic(tools);
		case "bedrock": return openaiToolsToBedrock(tools);
		case "google": return openaiToolsToGoogle(tools);
		default: return tools;
	}
}

//#endregion
//#region src/globalConfig/globalConfig.ts
/**
* Functions for manipulating the global configuration file, which lives at
* ~/.promptfoo/promptfoo.yaml by default.
*/
function writeGlobalConfig(config) {
	fs$1.writeFileSync(path$1.join(getConfigDirectoryPath(true), "promptfoo.yaml"), yaml.dump(config));
}
function readGlobalConfig() {
	const configDir = getConfigDirectoryPath();
	const configFilePath = path$1.join(configDir, "promptfoo.yaml");
	let globalConfig = { id: crypto.randomUUID() };
	if (fs$1.existsSync(configFilePath)) {
		globalConfig = yaml.load(fs$1.readFileSync(configFilePath, "utf-8")) || {};
		if (!globalConfig?.id) {
			globalConfig = {
				...globalConfig,
				id: crypto.randomUUID()
			};
			writeGlobalConfig(globalConfig);
		}
	} else {
		if (!fs$1.existsSync(configDir)) fs$1.mkdirSync(configDir, { recursive: true });
		fs$1.writeFileSync(configFilePath, yaml.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 = getEnvString("API_HOST", CLOUD_API_HOST);
var CloudConfig = class {
	config;
	constructor() {
		const savedConfig = readGlobalConfig()?.cloud || {};
		this.config = {
			appUrl: savedConfig.appUrl || "https://www.promptfoo.app",
			apiHost: savedConfig.apiHost,
			apiKey: savedConfig.apiKey,
			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.
	*/
	resolveApiHost() {
		return this.config.apiHost || process.env.PROMPTFOO_CLOUD_API_URL || API_HOST;
	}
	isEnabled() {
		return !!this.resolveApiKey();
	}
	setApiHost(apiHost) {
		this.config.apiHost = apiHost;
		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;
	}
	delete() {
		writeGlobalConfigPartial({ cloud: {} });
	}
	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,
			currentOrganizationId: savedConfig.currentOrganizationId,
			currentTeamId: savedConfig.currentTeamId,
			teams: savedConfig.teams
		};
	}
	async validateAndSetApiToken(token, apiHost) {
		try {
			const { fetchWithProxy } = await import("./fetch-YrlVkoyU.js");
			const response = await fetchWithProxy(`${apiHost}/api/v1/users/me`, { headers: { Authorization: `Bearer ${token}` } });
			if (!response.ok) {
				const errorMessage = await response.text();
				logger_default.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 } = await response.json();
			this.setApiKey(token);
			this.setApiHost(apiHost);
			this.setAppUrl(app.url);
			return {
				user,
				organization,
				app
			};
		} catch (err) {
			const error = err;
			const errorMessage = error instanceof Error ? error.message : String(error);
			logger_default.error(`[Cloud] Failed to validate API token with host ${apiHost}: ${errorMessage}`);
			if (error.cause) logger_default.error(`Cause: ${error.cause}`);
			throw error;
		}
	}
	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/util/fetch/monkeyPatchFetch.ts
const gzipAsync = promisify(gzip);
function isConnectionError(error) {
	return error instanceof TypeError && error.message === "fetch failed" && error.cause?.stack?.includes("internalConnectMultiple");
}
/**
* Enhanced fetch wrapper that adds logging, authentication, error handling, and optional compression
*/
async function monkeyPatchFetch(url, options) {
	const NO_LOG_URLS = [
		R_ENDPOINT,
		CONSENT_ENDPOINT,
		EVENTS_ENDPOINT
	];
	const isSilent = (options?.headers || {})["x-promptfoo-silent"] === "true";
	const logEnabled = !NO_LOG_URLS.some((logUrl) => url.toString().startsWith(logUrl)) && !isSilent;
	const opts = { ...options };
	const originalBody = opts.body;
	if (options?.compress && opts.body && typeof opts.body === "string") try {
		opts.body = await gzipAsync(opts.body);
		opts.headers = {
			...opts.headers || {},
			"Content-Encoding": "gzip"
		};
	} catch (e) {
		logger_default.warn(`Failed to compress request body: ${e}`);
	}
	if (typeof url === "string" && url.startsWith(CLOUD_API_HOST) || url instanceof URL && url.host === CLOUD_API_HOST.replace(/^https?:\/\//, "")) {
		const token = cloudConfig.getApiKey();
		opts.headers = {
			...opts.headers || {},
			...token ? { Authorization: `Bearer ${token}` } : {}
		};
	}
	try {
		const response = await fetch(url, opts);
		if (logEnabled) logRequestResponse({
			url: url.toString(),
			requestBody: originalBody,
			requestMethod: opts.method || "GET",
			response
		});
		return response;
	} catch (e) {
		if (logEnabled) {
			logRequestResponse({
				url: url.toString(),
				requestBody: opts.body,
				requestMethod: opts.method || "GET",
				response: null
			});
			if (isConnectionError(e)) {
				logger_default.debug(`Connection error, please check your network connectivity to the host: ${url} ${process.env.HTTP_PROXY || process.env.HTTPS_PROXY ? `or Proxy: ${process.env.HTTP_PROXY || process.env.HTTPS_PROXY}` : ""}`);
				throw e;
			}
			logger_default.debug(`Error in fetch: ${JSON.stringify(e, Object.getOwnPropertyNames(e), 2)} ${e instanceof Error ? e.stack : ""}`);
		}
		throw e;
	}
}

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

//#endregion
export { TERMINAL_MAX_WIDTH as A, transformTools as C, DEFAULT_QUERY_LIMIT as D, DEFAULT_MAX_CONCURRENCY as E, VERSION as F, FILE_METADATA_KEY as I, HUMAN_ASSERTION_TYPE as L, getShareApiBaseUrl as M, getShareViewBaseUrl as N, EVENTS_ENDPOINT as O, POSTHOG_KEY as P, transformToolChoice as S, CONSENT_ENDPOINT as T, isPromptfooSampleTarget as _, isRateLimited as a, parseChatPrompt as b, cloudConfig as c, writeGlobalConfigPartial as d, LONG_RUNNING_MODEL_TIMEOUT_MS as f, isOpenAIToolChoice as g, isOpenAIToolArray as h, handleRateLimit as i, getDefaultShareViewBaseUrl as j, R_ENDPOINT as k, readGlobalConfig as l, calculateCost as m, fetchWithRetries as n, isTransientError as o, REQUEST_TIMEOUT_MS as p, fetchWithTimeout as r, CloudConfig as s, fetchWithProxy as t, writeGlobalConfig as u, openaiToolChoiceToBedrock as v, CLOUD_PROVIDER_PREFIX as w, toTitleCase as x, openaiToolsToBedrock as y };
//# sourceMappingURL=fetch-Cn7G4hKv.js.map