@use-lattice/litmus 0.121.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/LICENSE +19 -0
  2. package/dist/src/accounts-Bt1oJb1Z.cjs +219 -0
  3. package/dist/src/accounts-DjOU8Rm3.js +178 -0
  4. package/dist/src/agentic-utils-D03IiXQc.js +153 -0
  5. package/dist/src/agentic-utils-Dh7xaMQM.cjs +180 -0
  6. package/dist/src/agents-C6BIMlZa.js +231 -0
  7. package/dist/src/agents-DvIpNX1L.cjs +666 -0
  8. package/dist/src/agents-ZP0RP9vV.cjs +231 -0
  9. package/dist/src/agents-maJXdjbR.js +665 -0
  10. package/dist/src/aimlapi-BTbQjG2E.cjs +30 -0
  11. package/dist/src/aimlapi-CwMxqfXP.js +30 -0
  12. package/dist/src/audio-BBUdvsde.cjs +97 -0
  13. package/dist/src/audio-D5DPZ7I-.js +97 -0
  14. package/dist/src/base-BEysXrkq.cjs +222 -0
  15. package/dist/src/base-C451JQfq.js +193 -0
  16. package/dist/src/blobs-BY8MDmpo.js +230 -0
  17. package/dist/src/blobs-BgcNn97m.cjs +256 -0
  18. package/dist/src/cache-BBE_lsTA.cjs +4 -0
  19. package/dist/src/cache-BkrqU5Ba.js +237 -0
  20. package/dist/src/cache-DsCxFlsZ.cjs +297 -0
  21. package/dist/src/chat-CPJWDP6a.cjs +289 -0
  22. package/dist/src/chat-CXX3xzkk.cjs +811 -0
  23. package/dist/src/chat-CcDgZFJ4.js +787 -0
  24. package/dist/src/chat-Dz5ZeGO2.js +289 -0
  25. package/dist/src/chatkit-Dw0mKkML.cjs +1158 -0
  26. package/dist/src/chatkit-swAIVuea.js +1157 -0
  27. package/dist/src/chunk-DEq-mXcV.js +15 -0
  28. package/dist/src/claude-agent-sdk-BXZJtOg6.js +379 -0
  29. package/dist/src/claude-agent-sdk-CkfyjDoG.cjs +383 -0
  30. package/dist/src/cloudflare-ai-BzpJcqUH.js +161 -0
  31. package/dist/src/cloudflare-ai-Cmy_R1y2.cjs +161 -0
  32. package/dist/src/cloudflare-gateway-B9tVQKok.cjs +272 -0
  33. package/dist/src/cloudflare-gateway-DrD3ew3H.js +272 -0
  34. package/dist/src/codex-sdk-Dezj9Nwm.js +1056 -0
  35. package/dist/src/codex-sdk-Dl9D4k5B.cjs +1060 -0
  36. package/dist/src/cometapi-C-9YvCHC.js +54 -0
  37. package/dist/src/cometapi-DHgDKoO2.cjs +54 -0
  38. package/dist/src/completion-B8Ctyxpr.js +120 -0
  39. package/dist/src/completion-Cxrt08sj.cjs +131 -0
  40. package/dist/src/createHash-BwgE13yv.cjs +27 -0
  41. package/dist/src/createHash-DmPQkvBh.js +15 -0
  42. package/dist/src/docker-BiqcTwLv.js +80 -0
  43. package/dist/src/docker-C7tEJnP-.cjs +80 -0
  44. package/dist/src/esm-C62Zofr1.cjs +409 -0
  45. package/dist/src/esm-DMVc93eh.js +379 -0
  46. package/dist/src/evalResult-C3NJPQOo.cjs +301 -0
  47. package/dist/src/evalResult-C7JJAPBb.js +295 -0
  48. package/dist/src/evalResult-DoVTZZWI.cjs +2 -0
  49. package/dist/src/extractor-DnMD3fwt.cjs +391 -0
  50. package/dist/src/extractor-DtlL28vL.js +374 -0
  51. package/dist/src/fetch-BTxakTSg.cjs +1133 -0
  52. package/dist/src/fetch-DQckpUFz.js +928 -0
  53. package/dist/src/fileExtensions-DnqA1y9x.js +85 -0
  54. package/dist/src/fileExtensions-bYh77CN8.cjs +114 -0
  55. package/dist/src/genaiTracer-CyZrmaK0.cjs +268 -0
  56. package/dist/src/genaiTracer-D3fD9dNV.js +256 -0
  57. package/dist/src/graders-BNscxFrU.js +13644 -0
  58. package/dist/src/graders-D2oE9Msq.js +2 -0
  59. package/dist/src/graders-c0Ez_w-9.cjs +2 -0
  60. package/dist/src/graders-d0F2M3e9.cjs +14056 -0
  61. package/dist/src/image-0ZhE0VlR.cjs +280 -0
  62. package/dist/src/image-CWE1pdNv.js +257 -0
  63. package/dist/src/image-D9ZK6hwL.js +163 -0
  64. package/dist/src/image-DKZgZITg.cjs +163 -0
  65. package/dist/src/index.cjs +11366 -0
  66. package/dist/src/index.d.cts +19640 -0
  67. package/dist/src/index.d.ts +19641 -0
  68. package/dist/src/index.js +11306 -0
  69. package/dist/src/invariant-Ddh24eXh.js +25 -0
  70. package/dist/src/invariant-kfQ8Bu82.cjs +30 -0
  71. package/dist/src/knowledgeBase-BgPyGFUd.cjs +122 -0
  72. package/dist/src/knowledgeBase-DyHilYaP.js +122 -0
  73. package/dist/src/litellm-CyMeneHS.js +135 -0
  74. package/dist/src/litellm-DWDF73yF.cjs +135 -0
  75. package/dist/src/logger-C40ZGil9.js +717 -0
  76. package/dist/src/logger-DyfK9PBt.cjs +917 -0
  77. package/dist/src/luma-ray-BAU9X_ep.cjs +315 -0
  78. package/dist/src/luma-ray-nwVseBbv.js +313 -0
  79. package/dist/src/messages-B5ADWTTv.js +245 -0
  80. package/dist/src/messages-BCnZfqrS.cjs +257 -0
  81. package/dist/src/meteor-DLZZ3osF.cjs +134 -0
  82. package/dist/src/meteor-DUiCJRC-.js +134 -0
  83. package/dist/src/modelslab-00cveB8L.cjs +163 -0
  84. package/dist/src/modelslab-D9sCU_L7.js +163 -0
  85. package/dist/src/nova-reel-CTapvqYH.js +276 -0
  86. package/dist/src/nova-reel-DlWuuroF.cjs +278 -0
  87. package/dist/src/nova-sonic-5UPWfeMv.cjs +363 -0
  88. package/dist/src/nova-sonic-BhSwQNym.js +363 -0
  89. package/dist/src/openai-BWrJK9d8.cjs +52 -0
  90. package/dist/src/openai-DumO8WQn.js +47 -0
  91. package/dist/src/openclaw-B8brrjC_.cjs +577 -0
  92. package/dist/src/openclaw-Bkayww9q.js +571 -0
  93. package/dist/src/opencode-sdk-7xjoDNiM.cjs +562 -0
  94. package/dist/src/opencode-sdk-SGwAPxht.js +558 -0
  95. package/dist/src/otlpReceiver-CoAHfAN9.cjs +15 -0
  96. package/dist/src/otlpReceiver-oO3EQwI9.js +14 -0
  97. package/dist/src/providerRegistry-4yjhaEM8.js +45 -0
  98. package/dist/src/providerRegistry-DhV4rJIc.cjs +50 -0
  99. package/dist/src/providers-B5RJVG-7.cjs +33609 -0
  100. package/dist/src/providers-BdmZCLzV.js +33262 -0
  101. package/dist/src/providers-CxtRxn8e.js +2 -0
  102. package/dist/src/providers-DnQLNbx1.cjs +3 -0
  103. package/dist/src/pythonUtils-BD0druiM.cjs +275 -0
  104. package/dist/src/pythonUtils-IBhn5YGR.js +249 -0
  105. package/dist/src/quiverai-BDOwZBsM.cjs +213 -0
  106. package/dist/src/quiverai-D3JTF5lD.js +213 -0
  107. package/dist/src/responses-B2LCDCXZ.js +667 -0
  108. package/dist/src/responses-BvNm4Xv9.cjs +685 -0
  109. package/dist/src/rubyUtils-B0NwnfpY.cjs +245 -0
  110. package/dist/src/rubyUtils-BroxzZ7c.cjs +2 -0
  111. package/dist/src/rubyUtils-hqVw5UvJ.js +222 -0
  112. package/dist/src/sagemaker-Cno2V-Sx.js +689 -0
  113. package/dist/src/sagemaker-fV_KUgs5.cjs +691 -0
  114. package/dist/src/server-BOuAXb06.cjs +238 -0
  115. package/dist/src/server-CtI-EWzm.cjs +2 -0
  116. package/dist/src/server-Cy3DZymt.js +189 -0
  117. package/dist/src/slack-CP8xBePa.js +135 -0
  118. package/dist/src/slack-DSQ1yXVb.cjs +135 -0
  119. package/dist/src/store-BwDDaBjb.cjs +246 -0
  120. package/dist/src/store-DcbLC593.cjs +2 -0
  121. package/dist/src/store-IGpqMIkv.js +240 -0
  122. package/dist/src/tables-3Q2cL7So.cjs +373 -0
  123. package/dist/src/tables-Bi2fjr4W.js +288 -0
  124. package/dist/src/telemetry-Bg2WqF79.js +161 -0
  125. package/dist/src/telemetry-D0x6u5kX.cjs +166 -0
  126. package/dist/src/telemetry-DXNimrI0.cjs +2 -0
  127. package/dist/src/text-B_UCRPp2.js +22 -0
  128. package/dist/src/text-CW1cyrwj.cjs +33 -0
  129. package/dist/src/tokenUsageUtils-NYT-WKS6.js +138 -0
  130. package/dist/src/tokenUsageUtils-bVa1ga6f.cjs +173 -0
  131. package/dist/src/transcription-Cl_W16Pr.js +122 -0
  132. package/dist/src/transcription-yt1EecY8.cjs +124 -0
  133. package/dist/src/transform-BCtGrl_W.cjs +228 -0
  134. package/dist/src/transform-Bv6gG2MJ.cjs +1688 -0
  135. package/dist/src/transform-CY1wbpRy.js +1507 -0
  136. package/dist/src/transform-DU8rUL9P.cjs +2 -0
  137. package/dist/src/transform-yWaShiKr.js +216 -0
  138. package/dist/src/transformersAvailability-BGkzavwb.js +35 -0
  139. package/dist/src/transformersAvailability-DKoRtQLy.cjs +35 -0
  140. package/dist/src/types-5aqHpBwE.cjs +3769 -0
  141. package/dist/src/types-Bn6D9c4U.js +3300 -0
  142. package/dist/src/util-BkKlTkI2.js +293 -0
  143. package/dist/src/util-CTh0bfOm.cjs +1119 -0
  144. package/dist/src/util-D17oBwo7.cjs +328 -0
  145. package/dist/src/util-DsS_-v4p.js +613 -0
  146. package/dist/src/util-DuntT1Ga.js +951 -0
  147. package/dist/src/util-aWjdCYMI.cjs +667 -0
  148. package/dist/src/utils-CisQwpjA.js +94 -0
  149. package/dist/src/utils-yWamDvmz.cjs +123 -0
  150. package/dist/tsconfig.tsbuildinfo +1 -0
  151. package/drizzle/0000_lush_hellion.sql +36 -0
  152. package/drizzle/0001_wide_calypso.sql +3 -0
  153. package/drizzle/0002_tidy_juggernaut.sql +1 -0
  154. package/drizzle/0003_lively_naoko.sql +8 -0
  155. package/drizzle/0004_minor_peter_quill.sql +19 -0
  156. package/drizzle/0005_silky_millenium_guard.sql +2 -0
  157. package/drizzle/0006_harsh_caretaker.sql +42 -0
  158. package/drizzle/0007_cloudy_wong.sql +1 -0
  159. package/drizzle/0008_broad_boomer.sql +2 -0
  160. package/drizzle/0009_strong_marten_broadcloak.sql +19 -0
  161. package/drizzle/0010_needy_bishop.sql +11 -0
  162. package/drizzle/0011_moaning_millenium_guard.sql +1 -0
  163. package/drizzle/0012_late_marten_broadcloak.sql +2 -0
  164. package/drizzle/0013_previous_dormammu.sql +9 -0
  165. package/drizzle/0014_lazy_captain_universe.sql +2 -0
  166. package/drizzle/0015_zippy_wallop.sql +29 -0
  167. package/drizzle/0016_jazzy_zemo.sql +2 -0
  168. package/drizzle/0017_reflective_praxagora.sql +4 -0
  169. package/drizzle/0018_fat_vanisher.sql +22 -0
  170. package/drizzle/0019_new_clint_barton.sql +8 -0
  171. package/drizzle/0020_skinny_maverick.sql +1 -0
  172. package/drizzle/0021_mysterious_madelyne_pryor.sql +13 -0
  173. package/drizzle/0022_sleepy_ultimo.sql +25 -0
  174. package/drizzle/0023_wooden_mandrill.sql +2 -0
  175. package/drizzle/AGENTS.md +68 -0
  176. package/drizzle/CLAUDE.md +1 -0
  177. package/drizzle/meta/0000_snapshot.json +221 -0
  178. package/drizzle/meta/0001_snapshot.json +214 -0
  179. package/drizzle/meta/0002_snapshot.json +221 -0
  180. package/drizzle/meta/0005_snapshot.json +369 -0
  181. package/drizzle/meta/0006_snapshot.json +638 -0
  182. package/drizzle/meta/0007_snapshot.json +640 -0
  183. package/drizzle/meta/0008_snapshot.json +649 -0
  184. package/drizzle/meta/0009_snapshot.json +554 -0
  185. package/drizzle/meta/0010_snapshot.json +619 -0
  186. package/drizzle/meta/0011_snapshot.json +627 -0
  187. package/drizzle/meta/0012_snapshot.json +639 -0
  188. package/drizzle/meta/0013_snapshot.json +717 -0
  189. package/drizzle/meta/0014_snapshot.json +717 -0
  190. package/drizzle/meta/0015_snapshot.json +897 -0
  191. package/drizzle/meta/0016_snapshot.json +1031 -0
  192. package/drizzle/meta/0018_snapshot.json +1210 -0
  193. package/drizzle/meta/0019_snapshot.json +1165 -0
  194. package/drizzle/meta/0020_snapshot.json +1232 -0
  195. package/drizzle/meta/0021_snapshot.json +1311 -0
  196. package/drizzle/meta/0022_snapshot.json +1481 -0
  197. package/drizzle/meta/0023_snapshot.json +1496 -0
  198. package/drizzle/meta/_journal.json +174 -0
  199. package/package.json +240 -0
@@ -0,0 +1,928 @@
1
+ import { t as __exportAll } from "./chunk-DEq-mXcV.js";
2
+ import { T as state, _ as getConfigDirectoryPath, b as getEnvInt, l as sanitizeUrl, n as logRequestResponse, r as logger, v as getEnvBool, x as getEnvString } from "./logger-C40ZGil9.js";
3
+ import { t as invariant } from "./invariant-Ddh24eXh.js";
4
+ import * as fs$1 from "fs";
5
+ import * as path$1 from "path";
6
+ import path from "path";
7
+ import yaml from "js-yaml";
8
+ import { promisify } from "util";
9
+ import * as fsPromises from "fs/promises";
10
+ import { getProxyForUrl } from "proxy-from-env";
11
+ import { Agent, ProxyAgent } from "undici";
12
+ import { gzip } from "zlib";
13
+ //#region src/providers/constants.ts
14
+ const FILE_METADATA_KEY = "_promptfooFileMetadata";
15
+ /**
16
+ * Identifier for manual user ratings in componentResults.
17
+ * Used to distinguish human ratings from automated assertions.
18
+ */
19
+ const HUMAN_ASSERTION_TYPE = "human";
20
+ //#endregion
21
+ //#region src/version.ts
22
+ /**
23
+ * Application version from package.json.
24
+ * Injected at build time, or read from npm environment in development.
25
+ */
26
+ const VERSION = "0.121.3";
27
+ function getShareApiBaseUrl() {
28
+ return getEnvString("PROMPTFOO_REMOTE_API_BASE_URL") || "https://api.promptfoo.app";
29
+ }
30
+ const TERMINAL_MAX_WIDTH = process?.stdout?.isTTY && process?.stdout?.columns && process?.stdout?.columns > 10 ? process?.stdout?.columns - 10 : 120;
31
+ const CLOUD_PROVIDER_PREFIX = "promptfoo://provider/";
32
+ const CONSENT_ENDPOINT = "https://api.promptfoo.dev/consent";
33
+ const EVENTS_ENDPOINT = "https://a.promptfoo.app";
34
+ const R_ENDPOINT = "https://r.promptfoo.app/";
35
+ //#endregion
36
+ //#region src/providers/shared.ts
37
+ /**
38
+ * The default timeout for API requests in milliseconds.
39
+ */
40
+ const REQUEST_TIMEOUT_MS = getEnvInt("REQUEST_TIMEOUT_MS", 3e5);
41
+ /**
42
+ * Extended timeout for long-running models (deep research, gpt-5-pro, etc.) in milliseconds.
43
+ * These models can take significantly longer to respond due to their complex reasoning.
44
+ */
45
+ const LONG_RUNNING_MODEL_TIMEOUT_MS = 6e5;
46
+ /**
47
+ * Calculates the cost of an API call based on the model and token usage.
48
+ *
49
+ * @param {string} modelName The name of the model used.
50
+ * @param {ProviderConfig} config The provider configuration.
51
+ * @param {number | undefined} promptTokens The number of tokens in the prompt.
52
+ * @param {number | undefined} completionTokens The number of tokens in the completion.
53
+ * @param {ProviderModel[]} models An array of available models with their costs.
54
+ * @returns {number | undefined} The calculated cost, or undefined if it can't be calculated.
55
+ */
56
+ function calculateCost(modelName, config, promptTokens, completionTokens, models) {
57
+ if (!Number.isFinite(promptTokens) || !Number.isFinite(completionTokens) || typeof promptTokens === "undefined" || typeof completionTokens === "undefined") return;
58
+ const model = models.find((m) => m.id === modelName);
59
+ if (!model || !model.cost) return;
60
+ const inputCost = config.cost ?? model.cost.input;
61
+ const outputCost = config.cost ?? model.cost.output;
62
+ return inputCost * promptTokens + outputCost * completionTokens;
63
+ }
64
+ /**
65
+ * Checks if a string looks like it's attempting to be JSON.
66
+ * This helps distinguish between actual JSON attempts and plain text that happens to start/end with brackets.
67
+ */
68
+ function looksLikeJson(prompt) {
69
+ const trimmed = prompt.trim();
70
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) return true;
71
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
72
+ const afterBracket = trimmed.slice(1).trimStart();
73
+ if (afterBracket.startsWith("\"") || afterBracket.startsWith("{") || afterBracket.startsWith("[") || /^[\d-]/.test(afterBracket) || /^(true|false|null)/.test(afterBracket)) return true;
74
+ if (afterBracket.length === 0 || /^\s+$/.test(afterBracket)) return true;
75
+ return false;
76
+ }
77
+ return false;
78
+ }
79
+ /**
80
+ * Parses a chat prompt string into a structured format.
81
+ *
82
+ * @template T The expected return type of the parsed prompt.
83
+ * @param {string} prompt The input prompt string to parse.
84
+ * @param {T} defaultValue The default value to return if parsing fails.
85
+ * @returns {T} The parsed prompt or the default value.
86
+ * @throws {Error} If the prompt is invalid YAML or JSON (when required).
87
+ */
88
+ function parseChatPrompt(prompt, defaultValue) {
89
+ const trimmedPrompt = prompt.trim();
90
+ if (trimmedPrompt.startsWith("- role:")) try {
91
+ return yaml.load(prompt);
92
+ } catch (err) {
93
+ throw new Error(`Chat Completion prompt is not a valid YAML string: ${err}\n\n${prompt}`);
94
+ }
95
+ else try {
96
+ return JSON.parse(prompt);
97
+ } catch (err) {
98
+ if (getEnvBool("PROMPTFOO_REQUIRE_JSON_PROMPTS") || looksLikeJson(trimmedPrompt)) throw new Error(`Chat Completion prompt is not a valid JSON string: ${err}\n\n${prompt}`);
99
+ return defaultValue;
100
+ }
101
+ }
102
+ /**
103
+ * Converts a string to title case.
104
+ *
105
+ * @param {string} str The input string to convert.
106
+ * @returns {string} The input string converted to title case.
107
+ */
108
+ function toTitleCase(str) {
109
+ return str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
110
+ }
111
+ function isPromptfooSampleTarget(provider) {
112
+ const url = provider.config?.url;
113
+ return url?.includes("promptfoo.app") || url?.includes("promptfoo.dev");
114
+ }
115
+ /**
116
+ * Checks if the given value is an OpenAI tool choice format.
117
+ * Detects string values ('auto', 'none', 'required') and
118
+ * the object form ({ type: 'function', function: { name } }).
119
+ */
120
+ function isOpenAIToolChoice(obj) {
121
+ if (typeof obj === "string") return [
122
+ "auto",
123
+ "none",
124
+ "required"
125
+ ].includes(obj);
126
+ if (typeof obj === "object" && obj !== null) {
127
+ const candidate = obj;
128
+ if (candidate.type === "function" && typeof candidate.function === "object" && candidate.function !== null) return typeof candidate.function.name === "string";
129
+ }
130
+ return false;
131
+ }
132
+ /**
133
+ * Transforms an OpenAI tool choice to Anthropic format.
134
+ */
135
+ function openaiToolChoiceToAnthropic(choice) {
136
+ if (typeof choice === "string") switch (choice) {
137
+ case "auto": return { type: "auto" };
138
+ case "none": return { type: "auto" };
139
+ case "required": return { type: "any" };
140
+ }
141
+ return {
142
+ type: "tool",
143
+ name: choice.function.name
144
+ };
145
+ }
146
+ /**
147
+ * Transforms an OpenAI tool choice to Bedrock Converse format.
148
+ */
149
+ function openaiToolChoiceToBedrock(choice) {
150
+ if (typeof choice === "string") switch (choice) {
151
+ case "auto": return { auto: {} };
152
+ case "none": return;
153
+ case "required": return { any: {} };
154
+ }
155
+ return { tool: { name: choice.function.name } };
156
+ }
157
+ /**
158
+ * Transforms an OpenAI tool choice to Google (Gemini) format.
159
+ */
160
+ function openaiToolChoiceToGoogle(choice) {
161
+ if (typeof choice === "string") switch (choice) {
162
+ case "auto": return { functionCallingConfig: { mode: "AUTO" } };
163
+ case "none": return { functionCallingConfig: { mode: "NONE" } };
164
+ case "required": return { functionCallingConfig: { mode: "ANY" } };
165
+ }
166
+ return { functionCallingConfig: {
167
+ mode: "ANY",
168
+ allowedFunctionNames: [choice.function.name]
169
+ } };
170
+ }
171
+ /**
172
+ * Transforms an OpenAI tool choice to the specified provider format.
173
+ * If the input is not in OpenAI format, it's returned as-is (native passthrough).
174
+ */
175
+ function transformToolChoice(toolChoice, format) {
176
+ if (!isOpenAIToolChoice(toolChoice)) return toolChoice;
177
+ switch (format) {
178
+ case "openai": return toolChoice;
179
+ case "anthropic": return openaiToolChoiceToAnthropic(toolChoice);
180
+ case "bedrock": return openaiToolChoiceToBedrock(toolChoice);
181
+ case "google": return openaiToolChoiceToGoogle(toolChoice);
182
+ default: return toolChoice;
183
+ }
184
+ }
185
+ /**
186
+ * Checks if an array contains OpenAI-format tools.
187
+ * Returns true if the first tool has `type: 'function'` and `function.name`.
188
+ */
189
+ function isOpenAIToolArray(tools) {
190
+ if (!Array.isArray(tools) || tools.length === 0) return false;
191
+ const first = tools[0];
192
+ if (typeof first !== "object" || first === null) return false;
193
+ const candidate = first;
194
+ return candidate.type === "function" && typeof candidate.function === "object" && candidate.function !== null && typeof candidate.function.name === "string";
195
+ }
196
+ /**
197
+ * Transforms OpenAI-format tools to Anthropic format.
198
+ */
199
+ function openaiToolsToAnthropic(tools) {
200
+ return tools.map((tool) => ({
201
+ name: tool.function.name,
202
+ ...tool.function.description ? { description: tool.function.description } : {},
203
+ input_schema: tool.function.parameters || {
204
+ type: "object",
205
+ properties: {}
206
+ }
207
+ }));
208
+ }
209
+ /**
210
+ * Transforms OpenAI-format tools to Bedrock Converse format.
211
+ */
212
+ function openaiToolsToBedrock(tools) {
213
+ return tools.map((tool) => ({ toolSpec: {
214
+ name: tool.function.name,
215
+ ...tool.function.description ? { description: tool.function.description } : {},
216
+ inputSchema: { json: tool.function.parameters || {
217
+ type: "object",
218
+ properties: {}
219
+ } }
220
+ } }));
221
+ }
222
+ /**
223
+ * Sanitizes a schema for Google/Gemini compatibility.
224
+ * - Converts type strings to uppercase (string → STRING)
225
+ * - Removes unsupported properties (additionalProperties, $schema, default)
226
+ * - Recursively processes nested schemas
227
+ */
228
+ function sanitizeSchemaForGoogle(schema) {
229
+ const result = {};
230
+ for (const [key, value] of Object.entries(schema)) {
231
+ if ([
232
+ "additionalProperties",
233
+ "$schema",
234
+ "default",
235
+ "$id",
236
+ "$ref"
237
+ ].includes(key)) continue;
238
+ if (key === "type" && typeof value === "string") result[key] = value.toUpperCase();
239
+ else if (key === "properties" && typeof value === "object" && value !== null) {
240
+ const sanitizedProps = {};
241
+ for (const [propKey, propValue] of Object.entries(value)) if (typeof propValue === "object" && propValue !== null) sanitizedProps[propKey] = sanitizeSchemaForGoogle(propValue);
242
+ else sanitizedProps[propKey] = propValue;
243
+ result[key] = sanitizedProps;
244
+ } else if (key === "items" && typeof value === "object" && value !== null) result[key] = sanitizeSchemaForGoogle(value);
245
+ else result[key] = value;
246
+ }
247
+ return result;
248
+ }
249
+ /**
250
+ * Transforms OpenAI-format tools to Google/Gemini format.
251
+ */
252
+ function openaiToolsToGoogle(tools) {
253
+ return [{ functionDeclarations: tools.map((tool) => ({
254
+ name: tool.function.name,
255
+ ...tool.function.description ? { description: tool.function.description } : {},
256
+ ...tool.function.parameters ? { parameters: sanitizeSchemaForGoogle(tool.function.parameters) } : {}
257
+ })) }];
258
+ }
259
+ /**
260
+ * Transforms tools from OpenAI format to the specified provider format.
261
+ * If the input is not in OpenAI format, it's returned as-is.
262
+ */
263
+ function transformTools(tools, format) {
264
+ if (!isOpenAIToolArray(tools)) return tools;
265
+ switch (format) {
266
+ case "openai": return tools;
267
+ case "anthropic": return openaiToolsToAnthropic(tools);
268
+ case "bedrock": return openaiToolsToBedrock(tools);
269
+ case "google": return openaiToolsToGoogle(tools);
270
+ default: return tools;
271
+ }
272
+ }
273
+ //#endregion
274
+ //#region src/scheduler/headerParser.ts
275
+ const OPENAI_HEADERS = {
276
+ remainingRequests: "x-ratelimit-remaining-requests",
277
+ remainingTokens: "x-ratelimit-remaining-tokens",
278
+ limitRequests: "x-ratelimit-limit-requests",
279
+ limitTokens: "x-ratelimit-limit-tokens",
280
+ resetRequests: "x-ratelimit-reset-requests",
281
+ resetTokens: "x-ratelimit-reset-tokens"
282
+ };
283
+ const ANTHROPIC_HEADERS = {
284
+ remainingRequests: "anthropic-ratelimit-requests-remaining",
285
+ remainingTokens: "anthropic-ratelimit-tokens-remaining",
286
+ limitRequests: "anthropic-ratelimit-requests-limit",
287
+ limitTokens: "anthropic-ratelimit-tokens-limit",
288
+ reset: "anthropic-ratelimit-requests-reset"
289
+ };
290
+ const STANDARD_HEADERS = {
291
+ remaining: "ratelimit-remaining",
292
+ limit: "ratelimit-limit",
293
+ reset: "ratelimit-reset",
294
+ remainingAlt: "x-ratelimit-remaining",
295
+ limitAlt: "x-ratelimit-limit",
296
+ resetAlt: "x-ratelimit-reset"
297
+ };
298
+ /**
299
+ * Parse rate limit headers from response.
300
+ */
301
+ function parseRateLimitHeaders(headers) {
302
+ const result = {};
303
+ const h = lowercaseKeys(headers);
304
+ result.remainingRequests = parseFirstMatch(h, [
305
+ OPENAI_HEADERS.remainingRequests,
306
+ ANTHROPIC_HEADERS.remainingRequests,
307
+ STANDARD_HEADERS.remainingAlt,
308
+ STANDARD_HEADERS.remaining
309
+ ]);
310
+ result.remainingTokens = parseFirstMatch(h, [OPENAI_HEADERS.remainingTokens, ANTHROPIC_HEADERS.remainingTokens]);
311
+ result.limitRequests = parseFirstMatch(h, [
312
+ OPENAI_HEADERS.limitRequests,
313
+ ANTHROPIC_HEADERS.limitRequests,
314
+ STANDARD_HEADERS.limitAlt,
315
+ STANDARD_HEADERS.limit
316
+ ]);
317
+ result.limitTokens = parseFirstMatch(h, [OPENAI_HEADERS.limitTokens, ANTHROPIC_HEADERS.limitTokens]);
318
+ for (const name of [
319
+ OPENAI_HEADERS.resetRequests,
320
+ OPENAI_HEADERS.resetTokens,
321
+ ANTHROPIC_HEADERS.reset,
322
+ STANDARD_HEADERS.resetAlt,
323
+ STANDARD_HEADERS.reset
324
+ ]) if (h[name] !== void 0) {
325
+ const parsed = parseResetTime(h[name]);
326
+ if (parsed !== null) {
327
+ result.resetAt = parsed;
328
+ break;
329
+ }
330
+ }
331
+ if (h["retry-after-ms"] !== void 0) {
332
+ const ms = parseInt(h["retry-after-ms"], 10);
333
+ if (!isNaN(ms) && ms >= 0) {
334
+ result.retryAfterMs = ms;
335
+ if (result.resetAt === void 0) result.resetAt = Date.now() + ms;
336
+ }
337
+ } else if (h["retry-after"] !== void 0) {
338
+ const parsed = parseRetryAfter(h["retry-after"]);
339
+ if (parsed !== null) {
340
+ result.retryAfterMs = parsed;
341
+ if (result.resetAt === void 0) result.resetAt = Date.now() + parsed;
342
+ }
343
+ }
344
+ return result;
345
+ }
346
+ /**
347
+ * Parse Retry-After header value.
348
+ * Returns duration in milliseconds.
349
+ * Exported for integration use.
350
+ */
351
+ function parseRetryAfter(value) {
352
+ const seconds = parseInt(value, 10);
353
+ if (!isNaN(seconds) && seconds >= 0 && String(seconds) === value.trim()) return seconds * 1e3;
354
+ const httpDate = parseHttpDate(value);
355
+ if (httpDate !== null) return Math.max(0, httpDate - Date.now());
356
+ return null;
357
+ }
358
+ function parseFirstMatch(headers, names) {
359
+ for (const name of names) {
360
+ const value = headers[name];
361
+ if (value !== void 0) {
362
+ const num = parseInt(value, 10);
363
+ if (!isNaN(num) && num >= 0) return num;
364
+ }
365
+ }
366
+ }
367
+ /**
368
+ * Parse reset time from various formats.
369
+ * Returns absolute Unix timestamp in milliseconds.
370
+ */
371
+ function parseResetTime(value) {
372
+ const durationMs = parseDuration(value);
373
+ if (durationMs !== null) return Date.now() + durationMs;
374
+ const num = parseFloat(value);
375
+ if (!isNaN(num)) if (num < 1e9) return Date.now() + num * 1e3;
376
+ else if (num < 1e10) return num * 1e3;
377
+ else return num;
378
+ const httpDate = parseHttpDate(value);
379
+ if (httpDate !== null) return httpDate;
380
+ return null;
381
+ }
382
+ /**
383
+ * Parse HTTP-date format (RFC 7231).
384
+ */
385
+ function parseHttpDate(value) {
386
+ const timestamp = Date.parse(value);
387
+ if (!isNaN(timestamp)) {
388
+ const now = Date.now();
389
+ const oneYearMs = 365 * 24 * 60 * 60 * 1e3;
390
+ if (timestamp > now - oneYearMs && timestamp < now + oneYearMs) return timestamp;
391
+ }
392
+ return null;
393
+ }
394
+ /**
395
+ * Parse duration strings like "1s", "100ms", "1m30s", "1h30s", "2h15m30s".
396
+ *
397
+ * Supported formats:
398
+ * - Xms (milliseconds)
399
+ * - Xs or X.Xs (seconds)
400
+ * - Xm or XmYs (minutes with optional seconds)
401
+ * - Xh or XhYm or XhYs or XhYmZs (hours with optional minutes/seconds)
402
+ */
403
+ function parseDuration(value) {
404
+ const match = value.match(/^(?:(\d+)h)?(?:(\d+)m(?!s))?(?:(\d+(?:\.\d+)?)(ms|s))?$/);
405
+ if (!match) return null;
406
+ const [, hours, minutes, secondsValue, secondsUnit] = match;
407
+ if (!hours && !minutes && !secondsValue) return null;
408
+ let ms = 0;
409
+ if (hours) ms += parseInt(hours, 10) * 36e5;
410
+ if (minutes) ms += parseInt(minutes, 10) * 6e4;
411
+ if (secondsValue) {
412
+ const num = parseFloat(secondsValue);
413
+ ms += secondsUnit === "ms" ? num : num * 1e3;
414
+ }
415
+ return ms;
416
+ }
417
+ function lowercaseKeys(obj) {
418
+ const result = {};
419
+ for (const [key, value] of Object.entries(obj)) result[key.toLowerCase()] = value;
420
+ return result;
421
+ }
422
+ //#endregion
423
+ //#region src/util/time.ts
424
+ function getCurrentTimestamp() {
425
+ return Math.floor((/* @__PURE__ */ new Date()).getTime() / 1e3);
426
+ }
427
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
428
+ //#endregion
429
+ //#region src/globalConfig/globalConfig.ts
430
+ /**
431
+ * Functions for manipulating the global configuration file, which lives at
432
+ * ~/.promptfoo/promptfoo.yaml by default.
433
+ */
434
+ function writeGlobalConfig(config) {
435
+ fs$1.writeFileSync(path$1.join(getConfigDirectoryPath(true), "promptfoo.yaml"), yaml.dump(config));
436
+ }
437
+ function readGlobalConfig() {
438
+ const configDir = getConfigDirectoryPath();
439
+ const configFilePath = path$1.join(configDir, "promptfoo.yaml");
440
+ let globalConfig = { id: crypto.randomUUID() };
441
+ if (fs$1.existsSync(configFilePath)) {
442
+ globalConfig = yaml.load(fs$1.readFileSync(configFilePath, "utf-8")) || {};
443
+ if (!globalConfig?.id) {
444
+ globalConfig = {
445
+ ...globalConfig,
446
+ id: crypto.randomUUID()
447
+ };
448
+ writeGlobalConfig(globalConfig);
449
+ }
450
+ } else {
451
+ if (!fs$1.existsSync(configDir)) fs$1.mkdirSync(configDir, { recursive: true });
452
+ fs$1.writeFileSync(configFilePath, yaml.dump(globalConfig));
453
+ }
454
+ return globalConfig;
455
+ }
456
+ /**
457
+ * Merges the top-level keys into existing config.
458
+ * @param partialConfig New keys to merge into the existing config.
459
+ */
460
+ function writeGlobalConfigPartial(partialConfig) {
461
+ const updatedConfig = { ...readGlobalConfig() };
462
+ Object.entries(partialConfig).forEach(([key, value]) => {
463
+ if (value !== void 0 && value !== null) updatedConfig[key] = value;
464
+ else delete updatedConfig[key];
465
+ });
466
+ writeGlobalConfig(updatedConfig);
467
+ }
468
+ const API_HOST = getEnvString("API_HOST", "https://api.promptfoo.app");
469
+ const SHARING_CUTOFF_DATE = /* @__PURE__ */ new Date("2026-03-09T00:00:00Z");
470
+ var CloudConfig = class {
471
+ config;
472
+ constructor() {
473
+ const savedConfig = readGlobalConfig()?.cloud || {};
474
+ this.config = {
475
+ appUrl: savedConfig.appUrl || "https://www.promptfoo.app",
476
+ apiHost: savedConfig.apiHost,
477
+ apiKey: savedConfig.apiKey,
478
+ sharing: savedConfig.sharing,
479
+ currentOrganizationId: savedConfig.currentOrganizationId,
480
+ currentTeamId: savedConfig.currentTeamId,
481
+ teams: savedConfig.teams
482
+ };
483
+ }
484
+ /**
485
+ * Returns the API key from config file or PROMPTFOO_API_KEY environment variable.
486
+ * Config file takes precedence over environment variable.
487
+ */
488
+ resolveApiKey() {
489
+ return this.config.apiKey || process.env.PROMPTFOO_API_KEY;
490
+ }
491
+ /**
492
+ * Returns the API host from config file, PROMPTFOO_CLOUD_API_URL environment variable,
493
+ * or defaults to the standard cloud API host.
494
+ * Config file takes precedence over environment variable.
495
+ */
496
+ resolveApiHost() {
497
+ return this.config.apiHost || process.env.PROMPTFOO_CLOUD_API_URL || API_HOST;
498
+ }
499
+ isEnabled() {
500
+ return !!this.resolveApiKey();
501
+ }
502
+ setApiHost(apiHost) {
503
+ this.config.apiHost = apiHost;
504
+ this.saveConfig();
505
+ }
506
+ setApiKey(apiKey) {
507
+ this.config.apiKey = apiKey;
508
+ this.saveConfig();
509
+ }
510
+ getApiKey() {
511
+ return this.resolveApiKey();
512
+ }
513
+ getApiHost() {
514
+ return this.resolveApiHost();
515
+ }
516
+ setAppUrl(appUrl) {
517
+ this.config.appUrl = appUrl;
518
+ this.saveConfig();
519
+ }
520
+ getAppUrl() {
521
+ return this.config.appUrl;
522
+ }
523
+ getSharing() {
524
+ return this.config.sharing;
525
+ }
526
+ /**
527
+ * Sets the sharing preference. Note: this value is only updated at authentication time
528
+ * (via `validateAndSetApiToken`) and may become stale if the user's license status
529
+ * changes between re-authentications.
530
+ */
531
+ setSharing(sharing) {
532
+ this.config.sharing = sharing;
533
+ this.saveConfig();
534
+ }
535
+ delete() {
536
+ writeGlobalConfigPartial({ cloud: {} });
537
+ this.reload();
538
+ }
539
+ saveConfig() {
540
+ writeGlobalConfigPartial({ cloud: this.config });
541
+ this.reload();
542
+ }
543
+ reload() {
544
+ const savedConfig = readGlobalConfig()?.cloud || {};
545
+ this.config = {
546
+ appUrl: savedConfig.appUrl || "https://www.promptfoo.app",
547
+ apiHost: savedConfig.apiHost,
548
+ apiKey: savedConfig.apiKey,
549
+ sharing: savedConfig.sharing,
550
+ currentOrganizationId: savedConfig.currentOrganizationId,
551
+ currentTeamId: savedConfig.currentTeamId,
552
+ teams: savedConfig.teams
553
+ };
554
+ }
555
+ saveValidatedApiToken(token, apiHost, user, app, hasActiveLicense) {
556
+ this.setApiKey(token);
557
+ this.setApiHost(apiHost);
558
+ this.setAppUrl(app.url);
559
+ if (typeof hasActiveLicense === "boolean") {
560
+ const createdAt = user?.createdAt ? new Date(user.createdAt) : null;
561
+ const isGrandfathered = createdAt != null && createdAt < SHARING_CUTOFF_DATE;
562
+ this.setSharing(hasActiveLicense || isGrandfathered);
563
+ }
564
+ }
565
+ async validateApiToken(token, apiHost) {
566
+ try {
567
+ const { fetchWithProxy } = await Promise.resolve().then(() => fetch_exports);
568
+ const response = await fetchWithProxy(`${apiHost}/api/v1/users/me`, { headers: { Authorization: `Bearer ${token}` } });
569
+ if (!response.ok) {
570
+ const errorMessage = await response.text();
571
+ logger.error(`[Cloud] Failed to validate API token: ${errorMessage}. HTTP Status: ${response.status} - ${response.statusText}.`);
572
+ throw new Error("Failed to validate API token: " + response.statusText);
573
+ }
574
+ const { user, organization, app, hasActiveLicense } = await response.json();
575
+ return {
576
+ user,
577
+ organization,
578
+ app,
579
+ ...typeof hasActiveLicense === "boolean" ? { hasActiveLicense } : {}
580
+ };
581
+ } catch (err) {
582
+ const error = err;
583
+ const errorMessage = error instanceof Error ? error.message : String(error);
584
+ logger.error(`[Cloud] Failed to validate API token with host ${apiHost}: ${errorMessage}`);
585
+ if (error.cause) logger.error(`Cause: ${error.cause}`);
586
+ throw error;
587
+ }
588
+ }
589
+ async validateAndSetApiToken(token, apiHost) {
590
+ const { user, organization, app, hasActiveLicense } = await this.validateApiToken(token, apiHost);
591
+ this.saveValidatedApiToken(token, apiHost, user, app, hasActiveLicense);
592
+ return {
593
+ user,
594
+ organization,
595
+ app,
596
+ hasActiveLicense: typeof hasActiveLicense === "boolean" ? hasActiveLicense : false
597
+ };
598
+ }
599
+ getCurrentOrganizationId() {
600
+ return this.config.currentOrganizationId;
601
+ }
602
+ setCurrentOrganization(organizationId) {
603
+ this.config.currentOrganizationId = organizationId;
604
+ this.saveConfig();
605
+ }
606
+ getCurrentTeamId(organizationId) {
607
+ if (organizationId) return this.config.teams?.[organizationId]?.currentTeamId;
608
+ return this.config.currentTeamId;
609
+ }
610
+ setCurrentTeamId(teamId, organizationId) {
611
+ if (organizationId) {
612
+ if (!this.config.teams) this.config.teams = {};
613
+ if (!this.config.teams[organizationId]) this.config.teams[organizationId] = {};
614
+ this.config.teams[organizationId].currentTeamId = teamId;
615
+ } else this.config.currentTeamId = teamId;
616
+ this.saveConfig();
617
+ }
618
+ clearCurrentTeamId(organizationId) {
619
+ if (organizationId) {
620
+ if (this.config.teams?.[organizationId]) delete this.config.teams[organizationId].currentTeamId;
621
+ } else delete this.config.currentTeamId;
622
+ this.saveConfig();
623
+ }
624
+ cacheTeams(teams, organizationId) {
625
+ if (organizationId) {
626
+ if (!this.config.teams) this.config.teams = {};
627
+ if (!this.config.teams[organizationId]) this.config.teams[organizationId] = {};
628
+ this.config.teams[organizationId].cache = teams.map((t) => ({
629
+ id: t.id,
630
+ name: t.name,
631
+ slug: t.slug,
632
+ lastFetched: (/* @__PURE__ */ new Date()).toISOString()
633
+ }));
634
+ }
635
+ this.saveConfig();
636
+ }
637
+ getCachedTeams(organizationId) {
638
+ if (organizationId) return this.config.teams?.[organizationId]?.cache;
639
+ }
640
+ };
641
+ const cloudConfig = new CloudConfig();
642
+ //#endregion
643
+ //#region src/util/fetch/monkeyPatchFetch.ts
644
+ const gzipAsync = promisify(gzip);
645
+ function isConnectionError(error) {
646
+ return error instanceof TypeError && error.message === "fetch failed" && error.cause?.stack?.includes("internalConnectMultiple");
647
+ }
648
+ /**
649
+ * Enhanced fetch wrapper that adds logging, authentication, error handling, and optional compression
650
+ */
651
+ async function monkeyPatchFetch(url, options) {
652
+ const NO_LOG_URLS = [
653
+ R_ENDPOINT,
654
+ CONSENT_ENDPOINT,
655
+ EVENTS_ENDPOINT
656
+ ];
657
+ const isSilent = (options?.headers || {})["x-promptfoo-silent"] === "true";
658
+ const logEnabled = !NO_LOG_URLS.some((logUrl) => url.toString().startsWith(logUrl)) && !isSilent;
659
+ const opts = { ...options };
660
+ const originalBody = opts.body;
661
+ if (options?.compress && opts.body && typeof opts.body === "string") try {
662
+ opts.body = await gzipAsync(opts.body);
663
+ opts.headers = {
664
+ ...opts.headers || {},
665
+ "Content-Encoding": "gzip"
666
+ };
667
+ } catch (e) {
668
+ logger.warn(`Failed to compress request body: ${e}`);
669
+ }
670
+ if (typeof url === "string" && url.startsWith("https://api.promptfoo.app") || url instanceof URL && url.host === "https://api.promptfoo.app".replace(/^https?:\/\//, "")) {
671
+ const token = cloudConfig.getApiKey();
672
+ opts.headers = {
673
+ ...opts.headers || {},
674
+ ...token ? { Authorization: `Bearer ${token}` } : {}
675
+ };
676
+ }
677
+ try {
678
+ const response = await fetch(url, opts);
679
+ if (logEnabled) logRequestResponse({
680
+ url: url.toString(),
681
+ requestBody: originalBody,
682
+ requestMethod: opts.method || "GET",
683
+ response
684
+ });
685
+ return response;
686
+ } catch (e) {
687
+ if (logEnabled) {
688
+ logRequestResponse({
689
+ url: url.toString(),
690
+ requestBody: opts.body,
691
+ requestMethod: opts.method || "GET",
692
+ response: null
693
+ });
694
+ if (isConnectionError(e)) {
695
+ logger.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}` : ""}`);
696
+ throw e;
697
+ }
698
+ logger.debug(`Error in fetch: ${JSON.stringify(e, Object.getOwnPropertyNames(e), 2)} ${e instanceof Error ? e.stack : ""}`);
699
+ }
700
+ throw e;
701
+ }
702
+ }
703
+ //#endregion
704
+ //#region src/util/fetch/index.ts
705
+ var fetch_exports = /* @__PURE__ */ __exportAll({
706
+ fetchWithProxy: () => fetchWithProxy,
707
+ fetchWithRetries: () => fetchWithRetries,
708
+ fetchWithTimeout: () => fetchWithTimeout,
709
+ handleRateLimit: () => handleRateLimit,
710
+ isRateLimited: () => isRateLimited,
711
+ isTransientError: () => isTransientError
712
+ });
713
+ let cachedAgent = null;
714
+ let cachedAgentConcurrency;
715
+ let cachedProxyAgents = /* @__PURE__ */ new Map();
716
+ /**
717
+ * Get the connection pool size for HTTP agents.
718
+ * Priority: PROMPTFOO_FETCH_CONNECTIONS env var > CLI -j flag > DEFAULT_MAX_CONCURRENCY (4).
719
+ * Set PROMPTFOO_FETCH_CONNECTIONS to override independently of eval concurrency
720
+ * (e.g., server deployments that need more connections than the default 4).
721
+ */
722
+ function getConnectionPoolSize() {
723
+ const envConnections = getEnvString("PROMPTFOO_FETCH_CONNECTIONS");
724
+ if (envConnections != null) {
725
+ const parsed = parseInt(envConnections, 10);
726
+ if (!isNaN(parsed)) return parsed;
727
+ }
728
+ return state.maxConcurrency || 4;
729
+ }
730
+ function getOrCreateAgent(tlsOptions) {
731
+ const concurrency = getConnectionPoolSize();
732
+ if (cachedAgent && cachedAgentConcurrency !== concurrency) {
733
+ if (typeof cachedAgent.close === "function") cachedAgent.close();
734
+ cachedAgent = null;
735
+ }
736
+ if (!cachedAgent) {
737
+ cachedAgent = new Agent({
738
+ headersTimeout: REQUEST_TIMEOUT_MS,
739
+ keepAliveTimeout: 3e4,
740
+ keepAliveMaxTimeout: 6e4,
741
+ connections: concurrency,
742
+ connect: tlsOptions
743
+ });
744
+ cachedAgentConcurrency = concurrency;
745
+ }
746
+ return cachedAgent;
747
+ }
748
+ function getOrCreateProxyAgent(proxyUrl, tlsOptions) {
749
+ if (!cachedProxyAgents.has(proxyUrl)) {
750
+ const agent = new ProxyAgent({
751
+ uri: proxyUrl,
752
+ proxyTls: tlsOptions,
753
+ requestTls: tlsOptions,
754
+ headersTimeout: REQUEST_TIMEOUT_MS,
755
+ keepAliveTimeout: 3e4,
756
+ keepAliveMaxTimeout: 6e4,
757
+ connections: getConnectionPoolSize()
758
+ });
759
+ cachedProxyAgents.set(proxyUrl, agent);
760
+ }
761
+ return cachedProxyAgents.get(proxyUrl);
762
+ }
763
+ async function fetchWithProxy(url, options = {}, abortSignal) {
764
+ let finalUrl = url;
765
+ let finalUrlString;
766
+ if (typeof url === "string") finalUrlString = url;
767
+ else if (url instanceof URL) finalUrlString = url.toString();
768
+ else if (url instanceof Request) finalUrlString = url.url;
769
+ if (!finalUrlString) throw new Error("Invalid URL");
770
+ const combinedSignal = abortSignal ? options.signal ? AbortSignal.any([options.signal, abortSignal]) : abortSignal : options.signal;
771
+ const finalOptions = {
772
+ ...options,
773
+ headers: {
774
+ ...options.headers,
775
+ "x-promptfoo-version": VERSION
776
+ },
777
+ signal: combinedSignal
778
+ };
779
+ if (typeof url === "string") try {
780
+ const parsedUrl = new URL(url);
781
+ if (parsedUrl.username || parsedUrl.password) {
782
+ if (finalOptions.headers && "Authorization" in finalOptions.headers) logger.warn("Both URL credentials and Authorization header present - URL credentials will be ignored");
783
+ else {
784
+ const username = parsedUrl.username || "";
785
+ const password = parsedUrl.password || "";
786
+ const credentials = Buffer.from(`${username}:${password}`).toString("base64");
787
+ finalOptions.headers = {
788
+ ...finalOptions.headers,
789
+ Authorization: `Basic ${credentials}`
790
+ };
791
+ }
792
+ parsedUrl.username = "";
793
+ parsedUrl.password = "";
794
+ finalUrl = parsedUrl.toString();
795
+ finalUrlString = finalUrl.toString();
796
+ }
797
+ } catch (e) {
798
+ logger.debug(`URL parsing failed in fetchWithProxy: ${e}`);
799
+ }
800
+ const tlsOptions = { rejectUnauthorized: !getEnvBool("PROMPTFOO_INSECURE_SSL", true) };
801
+ const caCertPath = getEnvString("PROMPTFOO_CA_CERT_PATH");
802
+ if (caCertPath) try {
803
+ const resolvedPath = path.resolve(state.basePath || "", caCertPath);
804
+ tlsOptions.ca = await fsPromises.readFile(resolvedPath, "utf8");
805
+ logger.debug(`Using custom CA certificate from ${resolvedPath}`);
806
+ } catch (e) {
807
+ logger.warn(`Failed to read CA certificate from ${caCertPath}: ${e}`);
808
+ }
809
+ const proxyUrl = finalUrlString ? getProxyForUrl(finalUrlString) : "";
810
+ if (!finalOptions.dispatcher) if (proxyUrl) {
811
+ logger.debug(`Using proxy: ${sanitizeUrl(proxyUrl)}`);
812
+ finalOptions.dispatcher = getOrCreateProxyAgent(proxyUrl, tlsOptions);
813
+ } else finalOptions.dispatcher = getOrCreateAgent(tlsOptions);
814
+ const maxTransientRetries = options.disableTransientRetries ? 0 : 3;
815
+ for (let attempt = 0; attempt <= maxTransientRetries; attempt++) {
816
+ const response = await monkeyPatchFetch(finalUrl, finalOptions);
817
+ if (!options.disableTransientRetries && isTransientError(response) && attempt < maxTransientRetries) {
818
+ const backoffMs = Math.pow(2, attempt) * 1e3;
819
+ logger.debug(`Transient error (${response.status} ${response.statusText}), retry ${attempt + 1}/${maxTransientRetries} after ${backoffMs}ms`);
820
+ await sleep(backoffMs);
821
+ continue;
822
+ }
823
+ return response;
824
+ }
825
+ throw new Error("Unexpected end of transient retry loop");
826
+ }
827
+ function fetchWithTimeout(url, options = {}, timeout) {
828
+ return new Promise((resolve, reject) => {
829
+ const timeoutController = new AbortController();
830
+ const signal = options.signal ? AbortSignal.any([options.signal, timeoutController.signal]) : timeoutController.signal;
831
+ const timeoutId = setTimeout(() => {
832
+ timeoutController.abort();
833
+ reject(/* @__PURE__ */ new Error(`Request timed out after ${timeout} ms`));
834
+ }, timeout);
835
+ fetchWithProxy(url, {
836
+ ...options,
837
+ signal
838
+ }).then((response) => {
839
+ clearTimeout(timeoutId);
840
+ resolve(response);
841
+ }).catch((error) => {
842
+ clearTimeout(timeoutId);
843
+ reject(error);
844
+ });
845
+ });
846
+ }
847
+ /**
848
+ * Check if a response indicates rate limiting
849
+ */
850
+ function isRateLimited(response) {
851
+ invariant(response.headers, "Response headers are missing");
852
+ invariant(response.status, "Response status is missing");
853
+ 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";
854
+ }
855
+ /**
856
+ * Handle rate limiting by waiting the appropriate amount of time
857
+ */
858
+ async function handleRateLimit(response) {
859
+ const rateLimitReset = response.headers.get("X-RateLimit-Reset");
860
+ const retryAfter = response.headers.get("Retry-After");
861
+ const openaiReset = response.headers.get("x-ratelimit-reset-requests") || response.headers.get("x-ratelimit-reset-tokens");
862
+ let waitTime = 6e4;
863
+ if (openaiReset) {
864
+ const parsedHeaders = parseRateLimitHeaders(Object.fromEntries(response.headers.entries()));
865
+ if (parsedHeaders.resetAt !== void 0) waitTime = Math.max(parsedHeaders.resetAt - Date.now(), 0);
866
+ } else if (rateLimitReset) {
867
+ const resetTime = /* @__PURE__ */ new Date(Number.parseInt(rateLimitReset) * 1e3);
868
+ const now = /* @__PURE__ */ new Date();
869
+ waitTime = Math.max(resetTime.getTime() - now.getTime() + 1e3, 0);
870
+ } else if (retryAfter) waitTime = parseRetryAfter(retryAfter) ?? waitTime;
871
+ logger.debug(`Rate limited, waiting ${waitTime}ms before retry`);
872
+ await sleep(waitTime);
873
+ }
874
+ /**
875
+ * Check if a response indicates a transient server error that should be retried.
876
+ * Matches specific status codes with their expected status text to avoid
877
+ * retrying permanent failures (e.g., some APIs return 502 for auth errors).
878
+ */
879
+ function isTransientError(response) {
880
+ if (!response?.statusText) return false;
881
+ const statusText = response.statusText.toLowerCase();
882
+ switch (response.status) {
883
+ case 502: return statusText.includes("bad gateway");
884
+ case 503: return statusText.includes("service unavailable");
885
+ case 504: return statusText.includes("gateway timeout");
886
+ case 524: return statusText.includes("timeout");
887
+ default: return false;
888
+ }
889
+ }
890
+ async function fetchWithRetries(url, options = {}, timeout, maxRetries) {
891
+ maxRetries = Math.max(0, maxRetries ?? 4);
892
+ let lastErrorMessage;
893
+ const backoff = getEnvInt("PROMPTFOO_REQUEST_BACKOFF_MS", 5e3);
894
+ for (let i = 0; i <= maxRetries; i++) {
895
+ let response;
896
+ try {
897
+ response = await fetchWithTimeout(url, {
898
+ ...options,
899
+ disableTransientRetries: true
900
+ }, timeout);
901
+ if (getEnvBool("PROMPTFOO_RETRY_5XX") && response.status >= 500 && response.status < 600) throw new Error(`Internal Server Error: ${response.status} ${response.statusText}`);
902
+ if (response && isRateLimited(response)) {
903
+ logger.debug(`Rate limited on URL ${url}: ${response.status} ${response.statusText}, attempt ${i + 1}/${maxRetries + 1}, waiting before retry...`);
904
+ lastErrorMessage = `Rate limited: ${response.status} ${response.statusText}`;
905
+ await handleRateLimit(response);
906
+ continue;
907
+ }
908
+ return response;
909
+ } catch (error) {
910
+ if (error instanceof Error && error.name === "AbortError") throw error;
911
+ let errorMessage;
912
+ if (error instanceof Error) {
913
+ const typedError = error;
914
+ errorMessage = `${typedError.name}: ${typedError.message}`;
915
+ if (typedError.cause) errorMessage += ` (Cause: ${typedError.cause})`;
916
+ if (typedError.code) errorMessage += ` (Code: ${typedError.code})`;
917
+ } else errorMessage = String(error);
918
+ logger.debug(`Request to ${url} failed (attempt #${i + 1}), retrying: ${errorMessage}`);
919
+ if (i < maxRetries) await sleep(Math.pow(2, i) * (backoff + 1e3 * Math.random()));
920
+ lastErrorMessage = errorMessage;
921
+ }
922
+ }
923
+ throw new Error(`Request failed after ${maxRetries} retries: ${lastErrorMessage}`);
924
+ }
925
+ //#endregion
926
+ export { TERMINAL_MAX_WIDTH as A, toTitleCase as C, CONSENT_ENDPOINT as D, CLOUD_PROVIDER_PREFIX as E, VERSION as M, FILE_METADATA_KEY as N, EVENTS_ENDPOINT as O, HUMAN_ASSERTION_TYPE as P, parseChatPrompt as S, transformTools as T, isOpenAIToolArray as _, CloudConfig as a, openaiToolChoiceToBedrock as b, writeGlobalConfig as c, sleep as d, parseRateLimitHeaders as f, calculateCost as g, REQUEST_TIMEOUT_MS as h, fetch_exports as i, getShareApiBaseUrl as j, R_ENDPOINT as k, writeGlobalConfigPartial as l, LONG_RUNNING_MODEL_TIMEOUT_MS as m, fetchWithRetries as n, cloudConfig as o, parseRetryAfter as p, fetchWithTimeout as r, readGlobalConfig as s, fetchWithProxy as t, getCurrentTimestamp as u, isOpenAIToolChoice as v, transformToolChoice as w, openaiToolsToBedrock as x, isPromptfooSampleTarget as y };
927
+
928
+ //# sourceMappingURL=fetch-DQckpUFz.js.map