import { t as __exportAll } from "./chunk-DEq-mXcV.js";
import { D as getEnvInt, O as getEnvString, T as getEnvBool, d as looksLikeSecret, o as logger, u as isSecretField, w as getConfigDirectoryPath } from "./logger-CtGm8dC_.js";
import { a as getFetchWithProxyHeaders, m as isTransientConnectionError, n as fetchWithRetries, s as getCloudBearerToken, y as getRequestTimeoutMs } from "./fetch-AchwhzAC.js";
import { n as sleep } from "./time-b8bV6BmZ.js";
import { n as sha256 } from "./createHash-CwDVU5xr.js";
import path from "path";
import { AsyncLocalStorage } from "node:async_hooks";
import fs from "fs";
import crypto from "node:crypto";
import { createCache } from "cache-manager";
import { Keyv } from "keyv";
import { KeyvFile } from "keyv-file";
//#region src/cache.ts
var cache_exports = /* @__PURE__ */ __exportAll({
	clearCache: () => clearCache,
	disableCache: () => disableCache,
	enableCache: () => enableCache,
	fetchWithCache: () => fetchWithCache,
	getCache: () => getCache,
	getScopedCacheKey: () => getScopedCacheKey,
	isCacheEnabled: () => isCacheEnabled,
	withCacheEnabled: () => withCacheEnabled,
	withCacheNamespace: () => withCacheNamespace
});
let cacheInstance;
const namespacedCacheInstances = /* @__PURE__ */ new Map();
const cacheNamespaceStorage = new AsyncLocalStorage();
const cacheEnabledStorage = new AsyncLocalStorage();
let enabled = getEnvBool("PROMPTFOO_CACHE_ENABLED", true);
const cacheType = getEnvString("PROMPTFOO_CACHE_TYPE") || (getEnvString("NODE_ENV") === "test" ? "memory" : "disk");
/** Default cache TTL: 14 days in seconds */
const DEFAULT_CACHE_TTL_SECONDS = 3600 * 24 * 14;
/**
* Get the cache TTL in milliseconds.
* Reads from PROMPTFOO_CACHE_TTL environment variable (in seconds) or uses default.
*/
function getCacheTtlMs() {
	return getEnvInt("PROMPTFOO_CACHE_TTL", DEFAULT_CACHE_TTL_SECONDS) * 1e3;
}
/**
* Get the cache instance with optional namespace isolation.
*
* @returns The current cache instance (namespace-aware if inside withCacheNamespace)
*
* @example
* ```typescript
* import { cache } from 'promptfoo';
*
* const cacheInstance = cache.getCache();
* const value = await cacheInstance.get('my-key');
* ```
*/
function getCache() {
	const namespace = cacheNamespaceStorage.getStore()?.namespace;
	if (namespace) return getNamespacedCache(namespace);
	return getCacheInstance();
}
function getCacheInstance() {
	if (!cacheInstance) {
		let cachePath = "";
		const stores = [];
		if (cacheType === "disk" && enabled) {
			cachePath = getEnvString("PROMPTFOO_CACHE_PATH") || path.join(getConfigDirectoryPath(), "cache");
			if (!fs.existsSync(cachePath)) {
				logger.info(`Creating cache folder at ${cachePath}.`);
				fs.mkdirSync(cachePath, { recursive: true });
			}
			const newCacheFile = path.join(cachePath, "cache.json");
			try {
				const keyv = new Keyv({
					store: new KeyvFile({ filename: newCacheFile }),
					ttl: getCacheTtlMs()
				});
				stores.push(keyv);
			} catch (err) {
				logger.warn(`[Cache] Failed to initialize disk cache: ${err.message}. Using memory cache instead.`);
			}
		}
		cacheInstance = createCache({
			stores,
			ttl: getCacheTtlMs(),
			refreshThreshold: 0
		});
	}
	return cacheInstance;
}
function getNamespacedCache(namespace) {
	const cachedNamespaceInstance = namespacedCacheInstances.get(namespace);
	if (cachedNamespaceInstance) return cachedNamespaceInstance;
	const cache = getCacheInstance();
	const namespacedCache = {
		...cache,
		get: (key) => cache.get(getScopedCacheKey(key, namespace)),
		set: (key, value, ttl) => cache.set(getScopedCacheKey(key, namespace), value, ttl),
		del: (key) => cache.del(getScopedCacheKey(key, namespace)),
		mget: (keys) => cache.mget(keys.map((key) => getScopedCacheKey(key, namespace))),
		mset: async (list) => {
			const scopedList = list.map(({ key, value, ttl }) => ({
				key: getScopedCacheKey(key, namespace),
				value,
				ttl
			}));
			return (await cache.mset(scopedList) ?? scopedList).map(({ key, value, ttl }) => ({
				key: getUnscopedCacheKey(key, namespace),
				value,
				ttl
			}));
		},
		mdel: (keys) => cache.mdel(keys.map((key) => getScopedCacheKey(key, namespace))),
		ttl: (key) => cache.ttl(getScopedCacheKey(key, namespace)),
		clear: () => clearNamespacedCache(cache, namespace),
		wrap: (...args) => cache.wrap(getScopedCacheKey(args[0], namespace), ...args.slice(1))
	};
	namespacedCacheInstances.set(namespace, namespacedCache);
	return namespacedCache;
}
function getCurrentCacheNamespace() {
	return cacheNamespaceStorage.getStore()?.namespace;
}
function currentNamespaceIncludesRepeatIndex(repeatIndex) {
	const namespaceParts = getCurrentCacheNamespace()?.split(":") ?? [];
	return namespaceParts.some((part, index) => part === "repeat" && namespaceParts[index + 1] === String(repeatIndex));
}
function shouldApplyRepeatCacheSuffix(repeatIndex) {
	return repeatIndex != null && repeatIndex > 0 && !currentNamespaceIncludesRepeatIndex(repeatIndex);
}
function getScopedCacheKey(cacheKey, namespace = getCurrentCacheNamespace()) {
	return namespace ? `${namespace}:${cacheKey}` : cacheKey;
}
function getUnscopedCacheKey(cacheKey, namespace) {
	const namespacePrefix = `${namespace}:`;
	return cacheKey.startsWith(namespacePrefix) ? cacheKey.slice(namespacePrefix.length) : cacheKey;
}
async function clearNamespacedCache(cache, namespace) {
	const namespacePrefix = `${namespace}:`;
	for (const store of cache.stores) {
		if (!store.iterator) throw new Error(`[Cache] Cannot clear namespace ${namespace} because a cache store does not support key iteration.`);
		const keysToDelete = [];
		for await (const [key] of store.iterator(void 0)) if (typeof key === "string" && key.startsWith(namespacePrefix)) keysToDelete.push(key);
		if (keysToDelete.length === 0) continue;
		try {
			if (store.deleteMany) await store.deleteMany(keysToDelete);
			else await Promise.all(keysToDelete.map((key) => store.delete(key)));
		} catch (err) {
			throw new Error(`[Cache] Failed to clear ${keysToDelete.length} keys for namespace "${namespace}": ${err.message}`);
		}
	}
	return true;
}
/**
* Run a function with isolated cache namespace.
*
* All cache operations within the function will be scoped to the namespace,
* preventing cache collisions between different test runs or environments.
*
* @param namespace Namespace prefix for cache keys (undefined = no namespace)
* @param fn Async function to run with the namespace
*
* @returns Result of the function
*
* @example
* ```typescript
* import { cache, evaluate } from 'promptfoo';
*
* // Run v1 and v2 evals with separate caches
* const v1Results = await cache.withCacheNamespace('v1', async () => {
*   return evaluate(testSuiteV1);
* });
*
* const v2Results = await cache.withCacheNamespace('v2', async () => {
*   return evaluate(testSuiteV2);
* });
* ```
*/
function withCacheNamespace(namespace, fn) {
	if (!namespace) return fn();
	const parentNamespace = getCurrentCacheNamespace();
	if (parentNamespace === namespace) return fn();
	const scopedNamespace = parentNamespace ? `${parentNamespace}:${namespace}` : namespace;
	return cacheNamespaceStorage.run({ namespace: scopedNamespace }, fn);
}
function withCacheEnabled(enabledOverride, fn) {
	if (enabledOverride === void 0) return fn();
	return cacheEnabledStorage.run({ enabled: enabledOverride }, fn);
}
function getEffectiveCacheEnabled() {
	return cacheEnabledStorage.getStore()?.enabled ?? enabled;
}
const inflightFetchResponses = /* @__PURE__ */ new Map();
const IGNORED_FETCH_CACHE_OPTION_KEYS = new Set(["method", "signal"]);
const FETCH_CACHE_SECRET_HMAC_CONTEXT = "promptfoo:fetch-cache-secret-key";
const FETCH_CACHE_SECRET_HMAC_SALT = "promptfoo:fetch-cache-secret-hmac-salt:v1";
const abortSignalIds = /* @__PURE__ */ new WeakMap();
let nextAbortSignalId = 0;
function fingerprintFetchCacheSecret(value) {
	return { __promptfooSecretFingerprint: crypto.createHmac("sha256", FETCH_CACHE_SECRET_HMAC_SALT).update(FETCH_CACHE_SECRET_HMAC_CONTEXT).update("\0").update(value).digest("hex") };
}
function isSensitiveFetchCacheString(value, fieldName) {
	return fieldName && isSecretField(fieldName) || looksLikeSecret(value);
}
function getStringForFetchCacheKey(value, fieldName) {
	if (isSensitiveFetchCacheString(value, fieldName)) return fingerprintFetchCacheSecret(value);
	return value;
}
function hasSensitiveJsonValue(value, fieldName) {
	if (typeof value === "string") return isSensitiveFetchCacheString(value, fieldName);
	if (Array.isArray(value)) return value.some((item) => hasSensitiveJsonValue(item, fieldName));
	if (value && typeof value === "object") return Object.entries(value).some(([key, nestedValue]) => hasSensitiveJsonValue(nestedValue, key));
	return false;
}
function getJsonValueForFetchCacheKey(value, fieldName) {
	if (typeof value === "string") return getStringForFetchCacheKey(value, fieldName);
	if (Array.isArray(value)) return value.map((item) => getJsonValueForFetchCacheKey(item, fieldName));
	if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [key, getJsonValueForFetchCacheKey(nestedValue, key)]));
	return value;
}
function getBodyStringForFetchCacheKey(value) {
	try {
		const parsedValue = JSON.parse(value);
		return hasSensitiveJsonValue(parsedValue) ? {
			encoding: "json",
			value: getJsonValueForFetchCacheKey(parsedValue)
		} : value;
	} catch {
		return getStringForFetchCacheKey(value);
	}
}
function hasSensitiveSearchParam(searchParams) {
	return Array.from(searchParams.entries()).some(([name, value]) => isSensitiveFetchCacheString(value, name));
}
function getSearchParamsForFetchCacheKey(searchParams) {
	if (!hasSensitiveSearchParam(searchParams)) return searchParams.toString();
	return Array.from(searchParams.entries()).map(([name, value]) => [name, getStringForFetchCacheKey(value, name)]);
}
function getUrlForFetchCacheKey(url) {
	const urlString = url instanceof Request ? url.url : String(url);
	try {
		const parsedUrl = new URL(urlString);
		if (!hasSensitiveSearchParam(parsedUrl.searchParams)) return urlString;
		parsedUrl.search = "";
		return {
			href: parsedUrl.toString(),
			searchParams: getSearchParamsForFetchCacheKey(new URL(urlString).searchParams)
		};
	} catch {
		return getStringForFetchCacheKey(urlString);
	}
}
function getHeadersForCacheKey(url, options) {
	const headers = new Headers(getFetchWithProxyHeaders(url, options));
	const cloudAuth = getCloudBearerToken(url);
	if (cloudAuth && !headers.has("Authorization")) headers.set("Authorization", cloudAuth);
	return Array.from(headers.entries()).sort(([nameA, valueA], [nameB, valueB]) => {
		const nameComparison = nameA.localeCompare(nameB);
		return nameComparison === 0 ? valueA.localeCompare(valueB) : nameComparison;
	}).map(([name, value]) => [name, getStringForFetchCacheKey(value, name)]);
}
function hashFetchCacheKey(identity) {
	return sha256(JSON.stringify(identity));
}
function hashBytesForCacheKey(bytes) {
	const buffer = ArrayBuffer.isView(bytes) ? Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength) : Buffer.from(bytes);
	return {
		byteLength: buffer.byteLength,
		hmacSha256: crypto.createHmac("sha256", FETCH_CACHE_SECRET_HMAC_SALT).update(`${FETCH_CACHE_SECRET_HMAC_CONTEXT}:bytes`).update("\0").update(buffer).digest("hex")
	};
}
function getBodyForFetchCacheKey(body) {
	if (body == null) return {
		cacheable: true,
		identity: void 0
	};
	if (typeof body === "string") return {
		cacheable: true,
		identity: {
			type: "string",
			value: getBodyStringForFetchCacheKey(body)
		}
	};
	if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) return {
		cacheable: true,
		identity: {
			type: "url-search-params",
			value: getSearchParamsForFetchCacheKey(body)
		}
	};
	if (body instanceof ArrayBuffer) return {
		cacheable: true,
		identity: {
			type: "array-buffer",
			...hashBytesForCacheKey(body)
		}
	};
	if (ArrayBuffer.isView(body)) return {
		cacheable: true,
		identity: {
			type: body.constructor.name,
			...hashBytesForCacheKey(body)
		}
	};
	return {
		cacheable: false,
		identity: void 0
	};
}
function getOptionsForFetchCacheKey(options, bodyIdentity) {
	const identity = {};
	for (const [key, value] of Object.entries(options).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))) {
		if (key === "headers" || IGNORED_FETCH_CACHE_OPTION_KEYS.has(key)) continue;
		if (key === "body") {
			identity.body = bodyIdentity;
			continue;
		}
		if (value == null || [
			"boolean",
			"number",
			"string"
		].includes(typeof value)) {
			identity[key] = value;
			continue;
		}
		return {
			cacheable: false,
			identity: void 0
		};
	}
	if (!Object.prototype.hasOwnProperty.call(options, "body") && bodyIdentity !== void 0) identity.body = bodyIdentity;
	return {
		cacheable: true,
		identity
	};
}
function getFetchCacheKey(url, options, method, format, repeatIndex) {
	const bodyForCacheKey = getBodyForFetchCacheKey(options.body ?? (url instanceof Request ? url.body : void 0));
	if (!bodyForCacheKey.cacheable) return null;
	const optionsForCacheKey = getOptionsForFetchCacheKey(options, bodyForCacheKey.identity);
	if (!optionsForCacheKey.cacheable) return null;
	const repeatSuffix = shouldApplyRepeatCacheSuffix(repeatIndex) ? `:repeat${repeatIndex}` : "";
	return getScopedCacheKey(`fetch:v3:${hashFetchCacheKey({
		format,
		headers: getHeadersForCacheKey(url, options),
		method,
		options: optionsForCacheKey.identity,
		url: getUrlForFetchCacheKey(url)
	})}${repeatSuffix}`);
}
function getAbortSignalId(signal) {
	let signalId = abortSignalIds.get(signal);
	if (signalId === void 0) {
		signalId = ++nextAbortSignalId;
		abortSignalIds.set(signal, signalId);
	}
	return signalId;
}
function getInflightFetchCacheKey(cacheKey, url, options) {
	const signal = options.signal ?? (url instanceof Request ? url.signal : void 0);
	return signal ? `${cacheKey}:signal:${getAbortSignalId(signal)}` : cacheKey;
}
function serializeFetchResponse(data, status, statusText, headers, latencyMs) {
	return JSON.stringify({
		data,
		status,
		statusText,
		headers,
		latencyMs
	});
}
function deserializeFetchResponse(response, cached, cache, cacheKey) {
	const parsedResponse = JSON.parse(response);
	return {
		cached,
		data: parsedResponse.data,
		status: parsedResponse.status,
		statusText: parsedResponse.statusText,
		headers: parsedResponse.headers,
		latencyMs: parsedResponse.latencyMs,
		deleteFromCache: async () => {
			await cache.del(cacheKey);
			logger.debug(`Evicted from cache: ${cacheKey}`);
		}
	};
}
async function fetchAndReadBody(url, options, timeout, maxRetries, isIdempotent) {
	const maxBodyRetries = isIdempotent ? 2 : 0;
	for (let bodyAttempt = 0; bodyAttempt <= maxBodyRetries; bodyAttempt++) {
		const fetchStart = Date.now();
		const resp = await fetchWithRetries(url, options, timeout, maxRetries);
		const fetchLatencyMs = Date.now() - fetchStart;
		try {
			return {
				respText: await resp.text(),
				resp,
				fetchLatencyMs
			};
		} catch (err) {
			if (isTransientConnectionError(err) && bodyAttempt < maxBodyRetries) {
				const backoffMs = Math.pow(2, bodyAttempt) * 1e3;
				logger.debug("[Cache] Body stream failed with transient error, retrying", {
					attempt: bodyAttempt + 1,
					maxRetries: maxBodyRetries,
					backoffMs,
					error: err?.message?.slice(0, 200)
				});
				await sleep(backoffMs);
				continue;
			}
			throw err;
		}
	}
	throw new Error("Exhausted body retries without returning or throwing");
}
async function prepareFetchResponse(url, options, timeout, maxRetries, isIdempotent, format) {
	const result = await fetchAndReadBody(url, options, timeout, maxRetries, isIdempotent);
	const response = result.resp;
	const responseText = result.respText;
	const fetchLatencyMs = result.fetchLatencyMs;
	const headers = Object.fromEntries(response.headers.entries());
	try {
		const parsedData = format === "json" ? JSON.parse(responseText) : responseText;
		const serializedResponse = serializeFetchResponse(parsedData, response.status, response.statusText, headers, fetchLatencyMs);
		if (!response.ok) return {
			response: responseText === "" ? serializeFetchResponse(`Empty Response: ${response.status}: ${response.statusText}`, response.status, response.statusText, headers, fetchLatencyMs) : serializedResponse,
			cacheable: false
		};
		if (format === "json" && parsedData?.error) {
			logger.debug(`Not caching ${url} because it contains an 'error' key: ${parsedData.error}`);
			return {
				response: serializedResponse,
				cacheable: false
			};
		}
		logger.debug(`Storing ${url} response in cache with latencyMs=${fetchLatencyMs}: ${serializedResponse}`);
		return {
			response: serializedResponse,
			cacheable: true
		};
	} catch (err) {
		throw new Error(`Error parsing response from ${url}: ${err.message}. HTTP ${response.status} ${response.statusText}. Received text: ${responseText}`);
	}
}
/**
* Fetch a URL with automatic caching.
*
* Caches HTTP responses with configurable TTL. Useful for fetching external
* data files, embeddings, or API responses that don't change frequently.
*
* @param url URL to fetch
* @param options Fetch options (method, headers, body, etc.)
* @param timeout Request timeout in milliseconds (default: standard timeout)
* @param format Response format: 'json' or 'text' (default: 'json')
* @param bustOrOptions Bypass cache or provide cache options for this request
* @param maxRetries Maximum number of retries on transient errors
*
* @returns FetchWithCacheResult with data, cache status, and HTTP metadata
*
* @example
* ```typescript
* import { cache } from 'promptfoo';
*
* // Fetch with 1-hour TTL
* const result = await cache.fetchWithCache(
*   'https://api.example.com/data',
*   { method: 'GET' },
*   undefined,
*   'json'
* );
*
* console.log(result.cached); // true if from cache
* console.log(result.data); // the fetched data
* console.log(result.status); // HTTP status code
* ```
*
* @see withCacheNamespace for cache isolation
* @see enableCache / disableCache for cache control
*/
async function fetchWithCache(url, options = {}, timeout = getRequestTimeoutMs(), format = "json", bustOrOptions = false, maxRetries) {
	const { bust = false, repeatIndex } = typeof bustOrOptions === "boolean" ? { bust: bustOrOptions } : bustOrOptions ?? {};
	const method = (options.method ?? (url instanceof Request ? url.method : "GET")).toUpperCase();
	const isIdempotent = [
		"GET",
		"HEAD",
		"OPTIONS",
		"PUT",
		"DELETE"
	].includes(method);
	const cacheEnabled = getEffectiveCacheEnabled();
	const cacheKey = cacheEnabled && !bust ? getFetchCacheKey(url, options, method, format, repeatIndex) : null;
	if (!cacheEnabled || bust || cacheKey == null) {
		const { respText, resp, fetchLatencyMs } = await fetchAndReadBody(url, options, timeout, maxRetries, isIdempotent);
		try {
			return {
				cached: false,
				data: format === "json" ? JSON.parse(respText) : respText,
				status: resp.status,
				statusText: resp.statusText,
				headers: Object.fromEntries(resp.headers.entries()),
				latencyMs: fetchLatencyMs,
				deleteFromCache: async () => {}
			};
		} catch (err) {
			throw new Error(`Error parsing response from ${url}: ${err.message}. HTTP ${resp.status} ${resp.statusText}. Received text: ${respText}`);
		}
	}
	const cache = getCacheInstance();
	const cachedResponse = await cache.get(cacheKey);
	if (cachedResponse != null) {
		logger.debug(`Returning cached response for ${url}: ${cachedResponse}`);
		return deserializeFetchResponse(cachedResponse, true, cache, cacheKey);
	}
	const inflightCacheKey = getInflightFetchCacheKey(cacheKey, url, options);
	let inflightResponse = inflightFetchResponses.get(inflightCacheKey);
	if (!inflightResponse) {
		inflightResponse = (async () => {
			const preparedResponse = await prepareFetchResponse(url, options, timeout, maxRetries, isIdempotent, format);
			if (preparedResponse.cacheable) await cache.set(cacheKey, preparedResponse.response);
			return preparedResponse.response;
		})().finally(() => {
			inflightFetchResponses.delete(inflightCacheKey);
		});
		inflightFetchResponses.set(inflightCacheKey, inflightResponse);
	}
	return deserializeFetchResponse(await inflightResponse, false, cache, cacheKey);
}
/**
* Enable caching for all provider calls (default behavior).
*
* @example
* ```typescript
* import { cache } from 'promptfoo';
* cache.enableCache();
* ```
*/
function enableCache() {
	enabled = true;
}
/**
* Disable caching. Provider calls will hit the API every time.
*
* Useful during development or testing when you want fresh results.
*
* @example
* ```typescript
* import { cache, evaluate } from 'promptfoo';
*
* cache.disableCache();
* const results = await evaluate(testSuite);  // Always fresh
* cache.enableCache();
* ```
*/
function disableCache() {
	enabled = false;
}
/**
* Clear all cached results.
*
* Removes all cached provider responses. The cache will refetch on next access.
*
* @example
* ```typescript
* import { cache, evaluate } from 'promptfoo';
*
* await cache.clearCache();
* const results = await evaluate(testSuite);  // Refetches all
* ```
*/
async function clearCache() {
	inflightFetchResponses.clear();
	namespacedCacheInstances.clear();
	return getCacheInstance().clear();
}
/**
* Check if caching is currently enabled.
*
* @returns true if cache is enabled, false otherwise
*
* @example
* ```typescript
* import { cache } from 'promptfoo';
*
* if (cache.isCacheEnabled()) {
*   console.log('Cache is active');
* }
* ```
*/
function isCacheEnabled() {
	return getEffectiveCacheEnabled();
}
//#endregion
export { getScopedCacheKey as a, withCacheNamespace as c, getCache as i, disableCache as n, isCacheEnabled as o, fetchWithCache as r, withCacheEnabled as s, cache_exports as t };

//# sourceMappingURL=cache-wQj0h878.js.map