promptfoo 0.120.27 → 0.121.2
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.
- package/README.md +1 -1
- package/dist/src/{ListApp-8WOe2nT6.js → ListApp-Du7YVwj5.js} +2 -4
- package/dist/src/accounts-B0pgC1oV.js +206 -0
- package/dist/src/{accounts-DVINui-2.js → accounts-Bm2D8Db9.js} +39 -34
- package/dist/src/{accounts-CPDRAMND.js → accounts-CiBLOnA7.js} +38 -33
- package/dist/src/{accounts-Fl2J3_Fu.cjs → accounts-gtkH-5KX.cjs} +77 -78
- package/dist/src/{agentic-utils-D922n6mm.js → agentic-utils-DS1g3GLF.js} +9 -10
- package/dist/src/{agents-BcsN_BgB.js → agents-9qiOy0ho.js} +16 -12
- package/dist/src/{agents-BXLmVsxR.js → agents-CBr9A01V.js} +37 -37
- package/dist/src/{agents-pMfppv9Z.js → agents-CmvBq8LV.js} +16 -18
- package/dist/src/{agents-hqgSV-3o.js → agents-D__IdAlg.js} +39 -40
- package/dist/src/{agents-BO2n8Z0d.cjs → agents-DbRtpYxR.cjs} +37 -40
- package/dist/src/{agents-BdUTAwi-.js → agents-DgF2zDag.js} +37 -42
- package/dist/src/{agents-DgJf2-ez.cjs → agents-Di9DKPzn.cjs} +16 -17
- package/dist/src/{agents-DNvSH78i.js → agents-cLXA8a_8.js} +17 -19
- package/dist/src/{aimlapi-DtgPI0nE.js → aimlapi-B4rcnZgv.js} +15 -17
- package/dist/src/{aimlapi-BE_Tg9Fl.cjs → aimlapi-BvlNH0gr.cjs} +15 -16
- package/dist/src/{aimlapi-DOib86oE.js → aimlapi-CnkC2HqE.js} +16 -18
- package/dist/src/{aimlapi-DTPACCB1.js → aimlapi-DHJU_kcV.js} +15 -4
- package/dist/src/app/assets/index-4LKxG2CG.js +439 -0
- package/dist/src/app/assets/{index-NCn4eVBv.css → index-C3zcsZFQ.css} +1 -1
- package/dist/src/app/assets/vendor-charts-BnDWwBlI.js +36 -0
- package/dist/src/app/index.html +3 -3
- package/dist/src/app/tsconfig.app.tsbuildinfo +1 -1
- package/dist/src/{audio-BnRUGAm_.js → audio-Bkv46et0.js} +6 -5
- package/dist/src/{audio-Cwo68yZS.cjs → audio-CGMyULza.cjs} +6 -7
- package/dist/src/{audio-MSRki4JU.js → audio-ClI_AFre.js} +6 -8
- package/dist/src/{audio-BRYU0BFo.js → audio-Dz3z7s3J.js} +7 -9
- package/dist/src/{base-pGVmXNl4.cjs → base-CGrhspbK.cjs} +36 -38
- package/dist/src/{base-h961VXYk.js → base-CpjcHe4e.js} +11 -13
- package/dist/src/base-DLKtKMFh.js +193 -0
- package/dist/src/{base-XB2tDJrB.js → base-Dy1V8--Z.js} +11 -13
- package/dist/src/blobs-BDbfYdrJ.js +236 -0
- package/dist/src/{blobs-CR5C4Ihh.js → blobs-CBO20krR.js} +9 -12
- package/dist/src/{blobs-BM_e6hCa.js → blobs-CMHN0Qcz.js} +9 -12
- package/dist/src/{blobs-B-KQAFhX.cjs → blobs-D23XLin-.cjs} +34 -37
- package/dist/src/{cache-jsiwsAJv.js → cache-BVeDlD87.js} +132 -117
- package/dist/src/{cache-CIpsoBZR.js → cache-C4Nxf52C.js} +132 -118
- package/dist/src/cache-CeUpFm3M.cjs +5 -0
- package/dist/src/{cache-BTVYfbka.cjs → cache-Dh5WtQps.cjs} +182 -168
- package/dist/src/cache-i1P6crbO.js +756 -0
- package/dist/src/cache-n-RCJ-hL.js +6 -0
- package/dist/src/{chat-BcPjZXIp.js → chat-BiKyneZl.js} +45 -46
- package/dist/src/{chat-D31K7C4u.cjs → chat-C1Qst7jL.cjs} +20 -21
- package/dist/src/{chat-B84t99NW.js → chat-C2jrdPMx.js} +20 -9
- package/dist/src/{chat-BE44YOc6.cjs → chat-CgF-J-Jj.cjs} +65 -66
- package/dist/src/{chat-DwWifjxi.js → chat-CzkrVDfz.js} +20 -22
- package/dist/src/chat-DJIw17u0.js +766 -0
- package/dist/src/{chat-CcUCysjU.js → chat-DqxYYtWA.js} +45 -46
- package/dist/src/{chat-DZM2GUHO.js → chat-qmatte1u.js} +21 -23
- package/dist/src/{chatkit-D67HS_0b.js → chatkit-65VXf5SR.js} +58 -58
- package/dist/src/{chatkit-DAB_qfzI.js → chatkit-Be-Q-a9F.js} +58 -60
- package/dist/src/{chatkit-Biqb_wsD.js → chatkit-BxFvW8KY.js} +58 -60
- package/dist/src/{chatkit-PGG4ZYIn.cjs → chatkit-DKyPi1Gs.cjs} +58 -60
- package/dist/src/chunk-DEq-mXcV.js +15 -0
- package/dist/src/chunk-DRamLcfz.js +16 -0
- package/dist/src/{claude-agent-sdk-SVM6AdBu.js → claude-agent-sdk-Apiy0iaz.js} +31 -31
- package/dist/src/{claude-agent-sdk-C-IOTPfo.js → claude-agent-sdk-D2bJee9S.js} +31 -29
- package/dist/src/{claude-agent-sdk-C9SiaQub.cjs → claude-agent-sdk-D9Z5Pr9X.cjs} +31 -28
- package/dist/src/{claude-agent-sdk-CiluSyW1.js → claude-agent-sdk-DfCoW0E6.js} +33 -20
- package/dist/src/cloud-BBh91EUK.js +4 -0
- package/dist/src/{cloud-CZ-q9Ier.js → cloud-C0dlstV_.js} +7 -9
- package/dist/src/{cloudflare-ai-BahKHyhh.js → cloudflare-ai-8TDxHR0x.js} +16 -18
- package/dist/src/{cloudflare-ai-v_qZD6_q.js → cloudflare-ai-BxAGvfju.js} +17 -19
- package/dist/src/{cloudflare-ai-Dfahv5SY.cjs → cloudflare-ai-CknbZ5LJ.cjs} +16 -17
- package/dist/src/{cloudflare-ai-Dxyt50Nl.js → cloudflare-ai-g7PB6VHR.js} +16 -4
- package/dist/src/{cloudflare-gateway-Bi_FpOFy.js → cloudflare-gateway-B9HWA5wf.js} +23 -23
- package/dist/src/{cloudflare-gateway-BPWoZIzJ.cjs → cloudflare-gateway-BSnDmHYo.cjs} +21 -22
- package/dist/src/{cloudflare-gateway-C0guUNwk.js → cloudflare-gateway-CKDb4dJ8.js} +26 -14
- package/dist/src/{cloudflare-gateway-btS7h1OZ.js → cloudflare-gateway-CP9QEWYS.js} +21 -25
- package/dist/src/{codex-sdk-DSxAnbfT.js → codex-sdk-C6UMlxwV.js} +28 -29
- package/dist/src/{codex-sdk-IYVi9fuM.js → codex-sdk-DUwKWezN.js} +28 -27
- package/dist/src/{codex-sdk-DulY0ZRq.js → codex-sdk-GGAw0qbD.js} +28 -29
- package/dist/src/{codex-sdk-DFKMtAyf.cjs → codex-sdk-fAO0c3yA.cjs} +28 -29
- package/dist/src/{cometapi-DzrR3SR_.js → cometapi-BL9yvj_f.js} +16 -4
- package/dist/src/{cometapi-DIO64tf4.cjs → cometapi-C4xSqeID.cjs} +21 -22
- package/dist/src/{cometapi-C9EEpJzT.js → cometapi-CUQq3H_a.js} +21 -24
- package/dist/src/{cometapi-DkNBMk0G.js → cometapi-DFNiKmSz.js} +17 -19
- package/dist/src/{completion-CG29bfKX.js → completion-5MzrpJxT.js} +11 -13
- package/dist/src/{completion-CCRT4kX1.cjs → completion-CM6oK8PS.cjs} +21 -23
- package/dist/src/{completion-Bgf1VJoq.js → completion-DZ083F31.js} +11 -13
- package/dist/src/completion-qRoZAYRB.js +120 -0
- package/dist/src/{createHash-Dw_iLu31.js → createHash-CTQmL3G2.js} +2 -3
- package/dist/src/{createHash-CYQy4YeL.cjs → createHash-CfZSc0b4.cjs} +13 -14
- package/dist/src/{createHash-CJcfskIZ.js → createHash-Da8fMwqB.js} +2 -3
- package/dist/src/createHash-DmPQkvBh.js +15 -0
- package/dist/src/{docker-D-ayp2FW.js → docker-Bb5dcxr8.js} +18 -20
- package/dist/src/{docker-B81N0t4e.js → docker-BvfL2BrW.js} +19 -21
- package/dist/src/{docker-DNcLR4Ig.cjs → docker-DcF2pRrj.cjs} +18 -19
- package/dist/src/{docker-egERKxCF.js → docker-ExVyLp0S.js} +18 -7
- package/dist/src/entrypoint.js +2 -3
- package/dist/src/{errors-DnGCbnx8.js → errors-P6ll7XSJ.js} +2 -2
- package/dist/src/{esm-B9dPm_BF.js → esm-C03C-mv3.js} +17 -20
- package/dist/src/{esm-D2pZ87fL.js → esm-CaIwzWR5.js} +18 -21
- package/dist/src/esm-Cd1AjG1D.js +379 -0
- package/dist/src/{esm-Ct-Joyue.cjs → esm-CnNt7sI4.cjs} +47 -49
- package/dist/src/eval-B3r2CVXr.js +15 -0
- package/dist/src/{eval-C-Nr6wX_.js → eval-Dg2nG4v2.js} +47 -54
- package/dist/src/evalResult-5xwYnECe.js +12 -0
- package/dist/src/evalResult-71lY93Kj.cjs +10 -0
- package/dist/src/{evalResult-DXMWJ3sx.js → evalResult-BBRNtX4I.js} +10 -11
- package/dist/src/{evalResult-4BzI2tmj.js → evalResult-BDMqrapS.js} +16 -12
- package/dist/src/evalResult-Dx5P5cIv.js +10 -0
- package/dist/src/{evalResult-CX8wQecI.cjs → evalResult-fuaI8HkH.cjs} +20 -21
- package/dist/src/{evaluator-8aGyV12L.js → evaluator-BhoWwp5b.js} +211 -235
- package/dist/src/evaluator-Jx6bRZV6.js +36 -0
- package/dist/src/{extractor-V5x_m1i0.js → extractor-C0EVHewb.js} +22 -24
- package/dist/src/extractor-D25qpmGX.js +374 -0
- package/dist/src/{extractor-CD5yKL-G.js → extractor-DReVID0K.js} +22 -24
- package/dist/src/{extractor-C031XmTA.cjs → extractor-pYLLi3wS.cjs} +37 -39
- package/dist/src/{fetch-BmbD-v1L.cjs → fetch-BPkYtG8K.cjs} +244 -277
- package/dist/src/fetch-BxNb_Lp3.js +5 -0
- package/dist/src/{fetch-D3OHf-lV.js → fetch-Cwxnd8zz.js} +36 -44
- package/dist/src/{fetch-CXZI9RRr.js → fetch-Dxpd4_sr.js} +23 -35
- package/dist/src/fetch-HaqdX7U1.js +780 -0
- package/dist/src/{fileExtensions-ePDqouxn.js → fileExtensions-DnqA1y9x.js} +2 -2
- package/dist/src/{fileExtensions-BpuMmaFL.js → fileExtensions-Ds-foDzt.js} +2 -2
- package/dist/src/fileExtensions-LcDYkU4v.js +85 -0
- package/dist/src/{fileExtensions-DkJYkWUy.cjs → fileExtensions-bYh77CN8.cjs} +27 -28
- package/dist/src/{formatDuration-CdevI3An.js → formatDuration-DgBVMN65.js} +2 -2
- package/dist/src/{genaiTracer-Ce19n68P.js → genaiTracer-70Z8BIuV.js} +2 -3
- package/dist/src/{genaiTracer-CqNnnXrE.js → genaiTracer-C1rxGO8Q.js} +2 -3
- package/dist/src/genaiTracer-D3fD9dNV.js +256 -0
- package/dist/src/{genaiTracer-Dres3qrN.cjs → genaiTracer-DN4dQywX.cjs} +13 -14
- package/dist/src/{graders--1y2u9HO.js → graders-BTeBGqjJ.js} +349 -397
- package/dist/src/graders-B_pgMLS2.js +34 -0
- package/dist/src/{graders-DTeBrzWp.js → graders-Bj_Odv7c.js} +349 -397
- package/dist/src/graders-DErokPDO.cjs +32 -0
- package/dist/src/graders-DP7KFFo-.js +13466 -0
- package/dist/src/graders-DR_uNe54.js +32 -0
- package/dist/src/{graders-DohM2dir.cjs → graders-DU49_J8Y.cjs} +684 -732
- package/dist/src/graders-w3176Wz-.js +32 -0
- package/dist/src/{image-B0U4Hqll.js → image-B02ogr_b.js} +7 -9
- package/dist/src/{image-DmE-niFE.js → image-B0h9VEMc.js} +6 -5
- package/dist/src/{image-CuKHuccK.cjs → image-BLmROtN3.cjs} +29 -30
- package/dist/src/{image-DNEIf_aI.js → image-Bb4vWQLM.js} +6 -8
- package/dist/src/{image-DpKl2F15.cjs → image-C1madmKh.cjs} +6 -7
- package/dist/src/{image-C3wHC9_h.js → image-CHfWvljl.js} +9 -10
- package/dist/src/{image-O1u4bCFg.js → image-DS-o-0ph.js} +9 -10
- package/dist/src/image-Dpxa1Jt6.js +257 -0
- package/dist/src/index.cjs +615 -695
- package/dist/src/index.d.cts +271 -7
- package/dist/src/index.d.ts +271 -3
- package/dist/src/index.js +580 -664
- package/dist/src/{interactiveCheck-Bxj1Swex.js → interactiveCheck-BgLZUIt3.js} +7 -8
- package/dist/src/{invariant-DT20jrBd.js → invariant-BtWWVVhl.js} +2 -2
- package/dist/src/{invariant-1pAf2CD1.js → invariant-Ddh24eXh.js} +2 -2
- package/dist/src/{invariant-CKcJAQ6M.cjs → invariant-kfQ8Bu82.cjs} +7 -8
- package/dist/src/invariant-vgHWClmd.js +25 -0
- package/dist/src/{knowledgeBase-CEzQobWX.js → knowledgeBase-B3OoKIej.js} +14 -9
- package/dist/src/{knowledgeBase-Be_zyW4L.js → knowledgeBase-CYTLHOt1.js} +16 -16
- package/dist/src/{knowledgeBase-BZ41IFwq.js → knowledgeBase-D33Ty2l6.js} +14 -18
- package/dist/src/{knowledgeBase-D-5BMXlr.cjs → knowledgeBase-DOO_BM9b.cjs} +14 -15
- package/dist/src/{litellm-DnbRJ2if.js → litellm-AaeZcZQF.js} +18 -19
- package/dist/src/{litellm-hUSNM_M2.cjs → litellm-I_hbp_dc.cjs} +17 -17
- package/dist/src/{litellm-CRDqPhNI.js → litellm-NbjknEh6.js} +17 -18
- package/dist/src/{litellm-9vR8zpfU.js → litellm-TrljxD9G.js} +17 -5
- package/dist/src/{logger-CG1uZPbQ.js → logger-CT3IKMKA.js} +10 -29
- package/dist/src/{logger-B7sBeGa0.cjs → logger-Cp1GPUjj.cjs} +152 -180
- package/dist/src/logger-DLcq4dWf.js +713 -0
- package/dist/src/{logger-LSBxlt7a.js → logger-KkObSCzq.js} +13 -31
- package/dist/src/{luma-ray-4blv9iZ2.js → luma-ray-BS2_tY8L.js} +22 -21
- package/dist/src/{luma-ray-drvgdpP9.js → luma-ray-DDsjcgZZ.js} +20 -13
- package/dist/src/{luma-ray-Hm3d6VJE.cjs → luma-ray-Due0n7di.cjs} +20 -21
- package/dist/src/{luma-ray-B2__8lYH.js → luma-ray-f6I2fft-.js} +20 -23
- package/dist/src/main.js +1170 -1321
- package/dist/src/{messages-Uee41Mj5.js → messages-BS17jdMx.js} +22 -24
- package/dist/src/{messages-XhiwCbi4.cjs → messages-Bs1kC7P4.cjs} +32 -34
- package/dist/src/{messages-CGPPidQr.js → messages-D0lx5qK7.js} +22 -24
- package/dist/src/messages-ZJk778GH.js +240 -0
- package/dist/src/{meteor-BYykdXrV.js → meteor-44VjEACX.js} +3 -4
- package/dist/src/{meteor-CsopaHrH.js → meteor-D-SotUw9.js} +3 -4
- package/dist/src/{meteor-e-E-2vVl.cjs → meteor-DLZZ3osF.cjs} +3 -4
- package/dist/src/{meteor-C8lGP6P4.js → meteor-DUiCJRC-.js} +3 -4
- package/dist/src/{modelslab-yKz-ZNB4.js → modelslab-Bmni6skY.js} +17 -10
- package/dist/src/{modelslab-E9gO-bYd.js → modelslab-Bx9IrZfS.js} +18 -20
- package/dist/src/{modelslab-lUVW0cmB.cjs → modelslab-CoUX6Jc_.cjs} +17 -18
- package/dist/src/{modelslab-ClBkr8_9.js → modelslab-DRb74SP4.js} +17 -19
- package/dist/src/{nova-reel-Dk8jNpId.js → nova-reel-BfPq-0Yk.js} +20 -13
- package/dist/src/{nova-reel-D8CuO6QH.cjs → nova-reel-C_QM18Xn.cjs} +20 -21
- package/dist/src/{nova-reel-u2eF2Cxm.js → nova-reel-D_W1tjMH.js} +22 -21
- package/dist/src/{nova-reel-P9bwvtYX.js → nova-reel-bgjxilYW.js} +20 -23
- package/dist/src/{nova-sonic-CK2rAiKi.js → nova-sonic-CFb5GYhg.js} +30 -26
- package/dist/src/{nova-sonic-BaqWlkds.js → nova-sonic-DIGQNR07.js} +30 -31
- package/dist/src/{nova-sonic-yZapPLv7.js → nova-sonic-De1HW5fD.js} +31 -32
- package/dist/src/{nova-sonic-Ds1C-dpm.cjs → nova-sonic-zfcljeRp.cjs} +30 -31
- package/dist/src/{openai-DUFopMrH.cjs → openai-Cuif0GEt.cjs} +8 -9
- package/dist/src/{openai-PblZ3jUE.js → openai-DElQ-fPX.js} +3 -4
- package/dist/src/{openai-CcN1B8Sb.js → openai-DhbB7eWK.js} +3 -4
- package/dist/src/openai-j-sE2O7r.js +44 -0
- package/dist/src/{openclaw-B6qqDr_u.cjs → openclaw-CSugPYAr.cjs} +188 -130
- package/dist/src/{openclaw-A-3_loM7.js → openclaw-DiSz3I5L.js} +180 -109
- package/dist/src/{openclaw-a3lylB-V.js → openclaw-DuvJKEW5.js} +178 -124
- package/dist/src/{openclaw-COn6QzDi.js → openclaw-tiVYRtr-.js} +178 -122
- package/dist/src/opencode-sdk-0j6rTWNb.js +562 -0
- package/dist/src/opencode-sdk-B3CWY9h_.js +560 -0
- package/dist/src/opencode-sdk-BL764Jdi.cjs +564 -0
- package/dist/src/opencode-sdk-C2y6UkP2.js +560 -0
- package/dist/src/{otlpReceiver-oyf5wLGC.js → otlpReceiver-C99PPb48.js} +53 -51
- package/dist/src/{otlpReceiver-lXsYVbpj.cjs → otlpReceiver-CGq6LspY.cjs} +53 -55
- package/dist/src/{otlpReceiver-94URx7UW.js → otlpReceiver-CdNBdbsk.js} +53 -55
- package/dist/src/{otlpReceiver-BmmTiMjA.js → otlpReceiver-D89fR-rC.js} +53 -55
- package/dist/src/{providerRegistry-Cq_JK_CJ.js → providerRegistry-B0RUOLI_.js} +7 -8
- package/dist/src/{providerRegistry-DSSHjMKf.js → providerRegistry-CD8MEar9.js} +7 -8
- package/dist/src/{providerRegistry-CvHEVJad.cjs → providerRegistry-Civky8Ar.cjs} +12 -13
- package/dist/src/providerRegistry-DM8rZYol.js +45 -0
- package/dist/src/providers-B7V0njNs.js +32 -0
- package/dist/src/providers-BEwbhv0X.js +30 -0
- package/dist/src/{providers-Iil64vk9.js → providers-BlqUifFg.js} +1543 -1676
- package/dist/src/providers-CH3C7zf7.js +30 -0
- package/dist/src/{providers-DHbjzW2e.cjs → providers-CgKOSgTR.cjs} +1896 -2029
- package/dist/src/providers-D8lF1sqW.js +33246 -0
- package/dist/src/{providers-BnFpbY_s.js → providers-Dk_6ocUX.js} +1536 -1669
- package/dist/src/providers-zyB6k_38.cjs +31 -0
- package/dist/src/{pythonUtils-CcT5LH1M.js → pythonUtils-C3py6GC1.js} +18 -19
- package/dist/src/{pythonUtils-DBbuI3QJ.cjs → pythonUtils-CTU3Y3lw.cjs} +42 -43
- package/dist/src/{pythonUtils-hZ8LeQLv.js → pythonUtils-D5nxkQ0P.js} +18 -19
- package/dist/src/pythonUtils-D6fwaDSg.js +249 -0
- package/dist/src/{quiverai-BuI0tE39.js → quiverai-BbOUOn2L.js} +8 -7
- package/dist/src/{quiverai-DCGSZt4U.js → quiverai-CIaELU_m.js} +8 -10
- package/dist/src/{quiverai-DiMVJQDz.cjs → quiverai-PdShCPox.cjs} +8 -9
- package/dist/src/{quiverai-fQNkExW4.js → quiverai-uH-dcTIr.js} +9 -11
- package/dist/src/{render-Dj1smHEb.js → render-Drod8m7K.js} +4 -5
- package/dist/src/responses-CB2jwoAr.js +660 -0
- package/dist/src/{responses-ghR3IOfy.cjs → responses-D8SBTL64.cjs} +39 -42
- package/dist/src/{responses-DOAFFENS.js → responses-DIR9Ud3j.js} +24 -27
- package/dist/src/{responses-CxzoQoBe.js → responses-WNGNYe3K.js} +24 -27
- package/dist/src/rubyUtils-BUHu6PhO.js +5 -0
- package/dist/src/{rubyUtils-CwbGmgYN.js → rubyUtils-BUVePouc.js} +27 -20
- package/dist/src/rubyUtils-BcuGX77l.js +222 -0
- package/dist/src/{rubyUtils-DudlFZed.js → rubyUtils-Boc4HZzX.js} +18 -19
- package/dist/src/rubyUtils-CP42kMvq.cjs +4 -0
- package/dist/src/{rubyUtils-C8MhKGHb.cjs → rubyUtils-DhCAlxZr.cjs} +48 -50
- package/dist/src/{sagemaker-gmskuyre.js → sagemaker-CNBxx5CJ.js} +75 -70
- package/dist/src/{sagemaker-CcxhlOAR.js → sagemaker-CemTFp2h.js} +75 -79
- package/dist/src/{sagemaker-77zbJ2Q2.cjs → sagemaker-Cl28mZU2.cjs} +75 -76
- package/dist/src/{sagemaker-DuM71dVU.js → sagemaker-YSyBXQQh.js} +77 -77
- package/dist/src/{scanner-DJYiSXQj.js → scanner-BsBlNXNn.js} +100 -121
- package/dist/src/server/index.js +5520 -67427
- package/dist/src/{server-B5v33lvE.cjs → server-C_7Ax-hA.cjs} +57 -67
- package/dist/src/{server-BJ4m4f1D.js → server-CqzrVGpF.js} +26 -29
- package/dist/src/server-CuxBbeSY.js +229 -0
- package/dist/src/server-DA4Cyrrq.js +7 -0
- package/dist/src/server-Dulb-4-K.cjs +5 -0
- package/dist/src/{server-RV_i_YX5.js → server-VWgWb00X.js} +19 -24
- package/dist/src/{signal-BW33JuId.js → signal-4U3mfRvL.js} +9 -11
- package/dist/src/{slack-DEURelTy.cjs → slack-BmVAVGaK.cjs} +7 -8
- package/dist/src/{slack-BQYeW9L3.js → slack-DCUPTzS2.js} +8 -8
- package/dist/src/{slack-BB6yuZzp.js → slack-DOdy_kyv.js} +7 -8
- package/dist/src/{slack-2pRrhhgJ.js → slack-DXMKtA-f.js} +7 -9
- package/dist/src/store-CXGFv4aR.js +228 -0
- package/dist/src/store-CXS-Q_91.js +6 -0
- package/dist/src/{store-D7CgQzAR.cjs → store-DLlFCC4h.cjs} +44 -45
- package/dist/src/{store-DJNsD1iC.js → store-DXilxTl-.js} +40 -36
- package/dist/src/{store-s3SftUwF.js → store-Dim__MDd.js} +34 -35
- package/dist/src/store-eYkaKMwq.cjs +5 -0
- package/dist/src/{tables-DfTsNN7X.js → tables-6YKwjN9-.js} +19 -21
- package/dist/src/tables-DLJPUdUE.js +288 -0
- package/dist/src/{tables-BKTmd6u7.cjs → tables-DPi7wKeM.cjs} +89 -91
- package/dist/src/{tables-DMegD0Xf.js → tables-gftXzE9I.js} +21 -23
- package/dist/src/telemetry-BpMfhthR.cjs +5 -0
- package/dist/src/{telemetry--WAdAfVi.js → telemetry-CMrFgtPB.js} +11 -13
- package/dist/src/telemetry-Cps3mIU-.js +171 -0
- package/dist/src/{telemetry-DQgVBCAb.cjs → telemetry-DaX14Chu.cjs} +21 -24
- package/dist/src/{telemetry-BedSm-bZ.js → telemetry-Dthj_BbD.js} +17 -14
- package/dist/src/telemetry-Dw38hanS.js +7 -0
- package/dist/src/{text-oiSbwSOI.js → text-B_UCRPp2.js} +2 -2
- package/dist/src/{text-oKzCBnK6.cjs → text-CW1cyrwj.cjs} +12 -13
- package/dist/src/{text-B_IrO4GZ.js → text-Db-Wt2u2.js} +2 -2
- package/dist/src/text-TIv0QYnd.js +22 -0
- package/dist/src/{tokenUsageUtils-FZd5O_4A.js → tokenUsageUtils-BDGe-iyI.js} +2 -2
- package/dist/src/{tokenUsageUtils-DmZSD2eU.js → tokenUsageUtils-DflFMjS0.js} +2 -2
- package/dist/src/tokenUsageUtils-NYT-WKS6.js +138 -0
- package/dist/src/{tokenUsageUtils-CXhxVj72.cjs → tokenUsageUtils-bVa1ga6f.cjs} +32 -33
- package/dist/src/{transcription-mYS9vd5v.js → transcription-BNYURcXg.js} +14 -7
- package/dist/src/{transcription-X2-B4vkX.js → transcription-B_OdaHp7.js} +14 -16
- package/dist/src/{transcription-BO1AHegO.cjs → transcription-NLVG9MT1.cjs} +14 -15
- package/dist/src/{transcription-lzBLiTFJ.js → transcription-s6A-bNrZ.js} +15 -17
- package/dist/src/{transform-B1Hi5lWS.cjs → transform-CzK1Q0zl.cjs} +24 -26
- package/dist/src/{transform-DeGlxb0D.js → transform-D5HsjduX.js} +39 -47
- package/dist/src/{transform-CYDILYDe.js → transform-DECvGmzp.js} +15 -13
- package/dist/src/transform-DTGDnAzW.js +6 -0
- package/dist/src/{transform-BEgStbHK.js → transform-DilY9wbS.js} +10 -12
- package/dist/src/{transform-D5PjiWiZ.cjs → transform-DuHvhZpj.cjs} +179 -187
- package/dist/src/transform-aa6tmVpZ.js +216 -0
- package/dist/src/transform-m3qNw4KP.cjs +5 -0
- package/dist/src/transform-uAytVuyX.js +1506 -0
- package/dist/src/{transform-Dfl89yi4.js → transform-vNucnNr0.js} +39 -47
- package/dist/src/{transformersAvailability-SZnTS3pJ.js → transformersAvailability-CEVM2GNQ.js} +2 -2
- package/dist/src/{transformersAvailability-D-glmEy7.cjs → transformersAvailability-CwayUSlh.cjs} +2 -3
- package/dist/src/{transformersAvailability-CjeFXhuJ.js → transformersAvailability-D6c6ROpT.js} +2 -2
- package/dist/src/{types-DWNf48sT.cjs → types-C_7nyzr1.cjs} +538 -574
- package/dist/src/{types-CXQduE9o.js → types-Cbd8uOMq.js} +68 -100
- package/dist/src/types-CzW2QFyi.js +3288 -0
- package/dist/src/{types-C5hEkb-x.js → types-DmyIJ-sR.js} +63 -99
- package/dist/src/{util-CoQjmE3u.js → util-B3xGByQh.js} +4 -5
- package/dist/src/{util-aLhtl3fe.cjs → util-B9vlHIIh.cjs} +208 -223
- package/dist/src/{util-Du96oyYS.js → util-BHGHw5G1.js} +4 -5
- package/dist/src/{util-DQ984syk.js → util-BRYkYPTd.js} +36 -51
- package/dist/src/{util-D9eLdGfa.js → util-BV4XUC0n.js} +5 -6
- package/dist/src/util-Bv6uGDfH.js +293 -0
- package/dist/src/{util-1wWM599Z.cjs → util-BzMcevZc.cjs} +50 -51
- package/dist/src/{util-_h4pVqrz.js → util-C1CeHl-P.js} +36 -51
- package/dist/src/{util-Bm_-UMD_.js → util-CMy69ZgQ.js} +5 -6
- package/dist/src/{util-CyUdMzV0.cjs → util-DGNOS1db.cjs} +34 -35
- package/dist/src/util-Dnmk2mBQ.js +599 -0
- package/dist/src/util-ZzmqNPlg.js +1426 -0
- package/dist/src/{utils-BjLy-Q72.cjs → utils-Cz9qXqII.cjs} +29 -32
- package/dist/src/{utils-CFMn2yHW.js → utils-XiOAgly5.js} +4 -7
- package/dist/src/utils-dLokC-eR.js +94 -0
- package/dist/src/{utils-DvWMzuMx.js → utils-f2-Moju7.js} +4 -7
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +38 -38
- package/dist/src/app/assets/index-B2D0bCSI.js +0 -439
- package/dist/src/app/assets/vendor-charts-CCl15Imd.js +0 -36
- package/dist/src/cache-ChPcurj7.js +0 -6
- package/dist/src/cache-VVu_W-yg.js +0 -8
- package/dist/src/cache-YLNCFEM2.cjs +0 -6
- package/dist/src/chunk-DHDDz29n.js +0 -22
- package/dist/src/chunk-FhC4c-0y.js +0 -21
- package/dist/src/cloud-BndfXy4H.js +0 -5
- package/dist/src/eval-BhHvMY82.js +0 -17
- package/dist/src/evalResult-Dq2gFNQY.js +0 -12
- package/dist/src/evalResult-nmcP5VKH.cjs +0 -12
- package/dist/src/evalResult-trqZjVYh.js +0 -14
- package/dist/src/evaluator-CnfPstzT.js +0 -39
- package/dist/src/fetch-IDPDue6F.cjs +0 -4
- package/dist/src/fetch-hKJ-It8q.js +0 -6
- package/dist/src/fetch-ouKnrWK-.js +0 -4
- package/dist/src/graders-CQn7WUsd.cjs +0 -34
- package/dist/src/graders-DC6QAbpW.js +0 -35
- package/dist/src/graders-DUWz3Y7j.js +0 -37
- package/dist/src/opencode-sdk-4bL9n-Gk.js +0 -382
- package/dist/src/opencode-sdk-BfC2zWcR.js +0 -376
- package/dist/src/opencode-sdk-DMJyuwMg.js +0 -380
- package/dist/src/opencode-sdk-Da-9adza.cjs +0 -383
- package/dist/src/providers-CsXB2Ix-.js +0 -35
- package/dist/src/providers-DO8ltjLC.js +0 -33
- package/dist/src/providers-Dtq-xnXd.cjs +0 -33
- package/dist/src/rubyUtils-BUbcND2f.js +0 -6
- package/dist/src/rubyUtils-Cr55X_KE.js +0 -5
- package/dist/src/rubyUtils-DlIiqoYo.cjs +0 -5
- package/dist/src/server-C2eQH4Gu.js +0 -6
- package/dist/src/server-CXWycu7H.cjs +0 -6
- package/dist/src/server-Q6OGlxxT.js +0 -8
- package/dist/src/store-B3EDO9Q3.js +0 -7
- package/dist/src/store-Dl9F8aw5.js +0 -6
- package/dist/src/store-SnrGrlt9.cjs +0 -6
- package/dist/src/telemetry-BGhiPZtl.js +0 -8
- package/dist/src/telemetry-CFfiYan6.cjs +0 -6
- package/dist/src/telemetry-DHzEduxX.js +0 -6
- package/dist/src/transform-C1x1ZlMQ.cjs +0 -6
- package/dist/src/transform-DYHjFmQu.js +0 -8
- package/dist/src/transform-rmwJT5JQ.js +0 -7
- package/dist/src/transformersAvailability-eJooj0gX.js +0 -35
|
@@ -0,0 +1,1426 @@
|
|
|
1
|
+
import { _ as getEnvBool, i as logger, s as sanitizeObject, w as state } from "./logger-DLcq4dWf.js";
|
|
2
|
+
import { O as TERMINAL_MAX_WIDTH, P as VERSION, t as fetchWithProxy } from "./fetch-HaqdX7U1.js";
|
|
3
|
+
import { t as invariant } from "./invariant-vgHWClmd.js";
|
|
4
|
+
import { o as safeResolve, r as importModule, t as getDirectory } from "./esm-CaIwzWR5.js";
|
|
5
|
+
import { m as isProviderOptions, o as OutputFileExtension, p as isApiProvider, s as ResultFailureReason } from "./types-CzW2QFyi.js";
|
|
6
|
+
import { i as isJavascriptFile, t as JAVASCRIPT_EXTENSIONS } from "./fileExtensions-LcDYkU4v.js";
|
|
7
|
+
import { r as runPython } from "./pythonUtils-D6fwaDSg.js";
|
|
8
|
+
import dotenv from "dotenv";
|
|
9
|
+
import * as fs$1 from "fs";
|
|
10
|
+
import * as path$1 from "path";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import * as os$1 from "os";
|
|
13
|
+
import yaml from "js-yaml";
|
|
14
|
+
import * as fsPromises from "fs/promises";
|
|
15
|
+
import dedent from "dedent";
|
|
16
|
+
import { parse as parse$1 } from "csv-parse/sync";
|
|
17
|
+
import { globSync, hasMagic } from "glob";
|
|
18
|
+
import nunjucks from "nunjucks";
|
|
19
|
+
import deepEqual from "fast-deep-equal";
|
|
20
|
+
import { XMLBuilder } from "fast-xml-parser";
|
|
21
|
+
import { stringify } from "csv-stringify/sync";
|
|
22
|
+
//#region src/util/functions/loadFunction.ts
|
|
23
|
+
const functionCache = {};
|
|
24
|
+
/**
|
|
25
|
+
* Loads a function from a JavaScript or Python file
|
|
26
|
+
* @param options Options for loading the function
|
|
27
|
+
* @returns The loaded function
|
|
28
|
+
*/
|
|
29
|
+
async function loadFunction({ filePath, functionName, defaultFunctionName = "func", basePath = state.basePath, useCache = true }) {
|
|
30
|
+
const cacheKey = `${filePath}${functionName ? `:${functionName}` : ""}`;
|
|
31
|
+
if (useCache && functionCache[cacheKey]) return functionCache[cacheKey];
|
|
32
|
+
const resolvedPath = basePath ? path.resolve(basePath, filePath) : filePath;
|
|
33
|
+
if (!isJavascriptFile(resolvedPath) && !resolvedPath.endsWith(".py")) throw new Error(`File must be a JavaScript (${JAVASCRIPT_EXTENSIONS.join(", ")}) or Python (.py) file`);
|
|
34
|
+
try {
|
|
35
|
+
let func;
|
|
36
|
+
if (isJavascriptFile(resolvedPath)) {
|
|
37
|
+
const module = await importModule(resolvedPath, functionName);
|
|
38
|
+
let moduleFunc;
|
|
39
|
+
if (functionName) moduleFunc = module;
|
|
40
|
+
else moduleFunc = typeof module === "function" ? module : module?.default?.default || module?.default || module?.[defaultFunctionName] || module;
|
|
41
|
+
if (typeof moduleFunc !== "function") throw new Error(functionName ? `JavaScript file must export a "${functionName}" function` : `JavaScript file must export a function (as default export or named export "${defaultFunctionName}")`);
|
|
42
|
+
func = moduleFunc;
|
|
43
|
+
} else {
|
|
44
|
+
const result = (...args) => runPython(resolvedPath, functionName || defaultFunctionName, args);
|
|
45
|
+
func = result;
|
|
46
|
+
}
|
|
47
|
+
if (useCache) functionCache[cacheKey] = func;
|
|
48
|
+
return func;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
logger.error(`Failed to load function: ${err.message}`);
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extracts the file path and function name from a file:// URL
|
|
56
|
+
* @param fileUrl The file:// URL (e.g., "file://path/to/file.js:functionName")
|
|
57
|
+
* @returns The file path and optional function name
|
|
58
|
+
*/
|
|
59
|
+
function parseFileUrl(fileUrl) {
|
|
60
|
+
if (!fileUrl.startsWith("file://")) throw new Error("URL must start with file://");
|
|
61
|
+
const urlWithoutProtocol = fileUrl.slice(7);
|
|
62
|
+
const lastColonIndex = urlWithoutProtocol.lastIndexOf(":");
|
|
63
|
+
if (lastColonIndex > 1) return {
|
|
64
|
+
filePath: urlWithoutProtocol.slice(0, lastColonIndex),
|
|
65
|
+
functionName: urlWithoutProtocol.slice(lastColonIndex + 1)
|
|
66
|
+
};
|
|
67
|
+
return { filePath: urlWithoutProtocol };
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/util/templates.ts
|
|
71
|
+
/**
|
|
72
|
+
* Get a Nunjucks engine instance with optional filters and configuration.
|
|
73
|
+
* @param filters - Optional map of custom Nunjucks filters.
|
|
74
|
+
* @param throwOnUndefined - Whether to throw an error on undefined variables.
|
|
75
|
+
* @param isGrader - Whether this engine is being used in a grader context.
|
|
76
|
+
* Nunjucks is always enabled in grader mode.
|
|
77
|
+
* @returns A configured Nunjucks environment.
|
|
78
|
+
*/
|
|
79
|
+
function getNunjucksEngine(filters, throwOnUndefined = false, isGrader = false) {
|
|
80
|
+
if (!isGrader && getEnvBool("PROMPTFOO_DISABLE_TEMPLATING")) return { renderString: (template) => template };
|
|
81
|
+
const env = nunjucks.configure({
|
|
82
|
+
autoescape: false,
|
|
83
|
+
throwOnUndefined
|
|
84
|
+
});
|
|
85
|
+
const envGlobals = {
|
|
86
|
+
...getEnvBool("PROMPTFOO_DISABLE_TEMPLATE_ENV_VARS", getEnvBool("PROMPTFOO_SELF_HOSTED", false)) ? {} : process.env,
|
|
87
|
+
...state.config?.env
|
|
88
|
+
};
|
|
89
|
+
env.addGlobal("env", envGlobals);
|
|
90
|
+
env.addFilter("load", function(str) {
|
|
91
|
+
return JSON.parse(str);
|
|
92
|
+
});
|
|
93
|
+
if (filters) for (const [name, filter] of Object.entries(filters)) env.addFilter(name, filter);
|
|
94
|
+
return env;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Parse Nunjucks template to extract variables.
|
|
98
|
+
* @param template - The Nunjucks template string.
|
|
99
|
+
* @returns An array of variables used in the template.
|
|
100
|
+
*/
|
|
101
|
+
function extractVariablesFromTemplate(template) {
|
|
102
|
+
const variableSet = /* @__PURE__ */ new Set();
|
|
103
|
+
const regex = /\{\{[\s]*([^{}\s|]+)[\s]*(?:\|[^}]+)?\}\}|\{%[\s]*(?:if|for)[\s]+([^{}\s]+)[\s]*.*?%\}/g;
|
|
104
|
+
template = template.replace(/\{#[\s\S]*?#\}/g, "");
|
|
105
|
+
let match;
|
|
106
|
+
while ((match = regex.exec(template)) !== null) {
|
|
107
|
+
const variable = match[1] || match[2];
|
|
108
|
+
if (variable) variableSet.add(variable);
|
|
109
|
+
}
|
|
110
|
+
const forLoopRegex = /\{%[\s]*for[\s]+(\w+)[\s]+in[\s]+(\w+)[\s]*%\}/g;
|
|
111
|
+
while ((match = forLoopRegex.exec(template)) !== null) {
|
|
112
|
+
variableSet.delete(match[1]);
|
|
113
|
+
variableSet.add(match[2]);
|
|
114
|
+
}
|
|
115
|
+
return Array.from(variableSet);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Extract variables from multiple Nunjucks templates.
|
|
119
|
+
* @param templates - An array of Nunjucks template strings.
|
|
120
|
+
* @returns An array of variables used in the templates.
|
|
121
|
+
*/
|
|
122
|
+
function extractVariablesFromTemplates(templates) {
|
|
123
|
+
const variableSet = /* @__PURE__ */ new Set();
|
|
124
|
+
for (const template of templates) extractVariablesFromTemplate(template).forEach((variable) => variableSet.add(variable));
|
|
125
|
+
return Array.from(variableSet);
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/util/render.ts
|
|
129
|
+
/**
|
|
130
|
+
* Renders ONLY environment variable templates in an object, leaving all other templates untouched.
|
|
131
|
+
* This allows env vars to be resolved at provider load time while preserving runtime var templates.
|
|
132
|
+
*
|
|
133
|
+
* Supports full Nunjucks syntax for env vars including filters and expressions:
|
|
134
|
+
* - {{ env.VAR_NAME }}
|
|
135
|
+
* - {{ env['VAR-NAME'] }}
|
|
136
|
+
* - {{ env["VAR-NAME"] }}
|
|
137
|
+
* - {{ env.VAR | default('fallback') }}
|
|
138
|
+
* - {{ env.VAR | upper }}
|
|
139
|
+
*
|
|
140
|
+
* Preserves non-env templates for runtime rendering:
|
|
141
|
+
* - {{ vars.x }} - preserved as literal
|
|
142
|
+
* - {{ prompt }} - preserved as literal
|
|
143
|
+
*
|
|
144
|
+
* Implementation: Uses regex to find env templates, delegates to Nunjucks for rendering.
|
|
145
|
+
* This ensures full Nunjucks feature support while preserving non-env templates.
|
|
146
|
+
*
|
|
147
|
+
* @param obj - The object to process
|
|
148
|
+
* @param envOverrides - Optional env vars to merge with (or replace) the base env
|
|
149
|
+
* @param replaceBase - If true, envOverrides replaces the base env entirely instead of merging
|
|
150
|
+
* @returns The object with only env templates rendered
|
|
151
|
+
*/
|
|
152
|
+
function renderEnvOnlyInObject(obj, envOverrides, replaceBase) {
|
|
153
|
+
if (getEnvBool("PROMPTFOO_DISABLE_TEMPLATING")) return obj;
|
|
154
|
+
if (typeof obj === "string") {
|
|
155
|
+
const nunjucks = getNunjucksEngine();
|
|
156
|
+
const baseEnvGlobals = nunjucks.getGlobal("env");
|
|
157
|
+
const envGlobals = replaceBase ? envOverrides ?? {} : envOverrides ? {
|
|
158
|
+
...baseEnvGlobals,
|
|
159
|
+
...envOverrides
|
|
160
|
+
} : baseEnvGlobals;
|
|
161
|
+
return obj.replace(/\{\{(?:[^}]|\}(?!\}))*\}\}/g, (match) => {
|
|
162
|
+
if (!match.match(/\benv\.|env\[/)) return match;
|
|
163
|
+
const varMatch = match.match(/env\.(\w+)|env\[['"]([^'"]+)['"]\]/);
|
|
164
|
+
const varName = varMatch?.[1] || varMatch?.[2];
|
|
165
|
+
if (match.includes("|") || varName && varName in envGlobals && envGlobals[varName] !== void 0) try {
|
|
166
|
+
return nunjucks.renderString(match, { env: envGlobals });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.debug(`Failed to render env template "${match}": ${error instanceof Error ? error.message : String(error)}`);
|
|
169
|
+
return match;
|
|
170
|
+
}
|
|
171
|
+
return match;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(obj)) return obj.map((item) => renderEnvOnlyInObject(item, envOverrides, replaceBase));
|
|
175
|
+
if (typeof obj === "object" && obj !== null) {
|
|
176
|
+
const result = {};
|
|
177
|
+
for (const key in obj) result[key] = renderEnvOnlyInObject(obj[key], envOverrides, replaceBase);
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
return obj;
|
|
181
|
+
}
|
|
182
|
+
function renderVarsInObject(obj, vars) {
|
|
183
|
+
if (!vars || getEnvBool("PROMPTFOO_DISABLE_TEMPLATING")) return obj;
|
|
184
|
+
if (typeof obj === "string") return getNunjucksEngine().renderString(obj, vars);
|
|
185
|
+
if (Array.isArray(obj)) return obj.map((item) => renderVarsInObject(item, vars));
|
|
186
|
+
if (typeof obj === "object" && obj !== null) {
|
|
187
|
+
const result = {};
|
|
188
|
+
for (const key in obj) result[key] = renderVarsInObject(obj[key], vars);
|
|
189
|
+
return result;
|
|
190
|
+
} else if (typeof obj === "function") return renderVarsInObject(obj({ vars }));
|
|
191
|
+
return obj;
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/util/file.ts
|
|
195
|
+
/**
|
|
196
|
+
* Simple Nunjucks engine specifically for file paths
|
|
197
|
+
* This function is separate from the main getNunjucksEngine to avoid circular dependencies
|
|
198
|
+
*/
|
|
199
|
+
function getNunjucksEngineForFilePath() {
|
|
200
|
+
const env = nunjucks.configure({ autoescape: false });
|
|
201
|
+
env.addGlobal("env", {
|
|
202
|
+
...process.env,
|
|
203
|
+
...state.config?.env
|
|
204
|
+
});
|
|
205
|
+
return env;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Loads content from an external file if the input is a file path, otherwise
|
|
209
|
+
* returns the input as-is. Supports Nunjucks templating for file paths.
|
|
210
|
+
*
|
|
211
|
+
* @param filePath - The input to process. Can be a file path string starting with "file://",
|
|
212
|
+
* an array of file paths, or any other type of data.
|
|
213
|
+
* @param context - Optional context to control file loading behavior. 'assertion' context
|
|
214
|
+
* preserves Python/JS file references instead of loading their content.
|
|
215
|
+
* @returns The loaded content if the input was a file path, otherwise the original input.
|
|
216
|
+
* For JSON and YAML files, the content is parsed into an object.
|
|
217
|
+
* For other file types, the raw file content is returned as a string.
|
|
218
|
+
*
|
|
219
|
+
* @throws {Error} If the specified file does not exist.
|
|
220
|
+
*/
|
|
221
|
+
function maybeLoadFromExternalFile(filePath, context) {
|
|
222
|
+
if (Array.isArray(filePath)) return filePath.map((path) => {
|
|
223
|
+
return maybeLoadFromExternalFile(path, context);
|
|
224
|
+
});
|
|
225
|
+
if (typeof filePath !== "string") return filePath;
|
|
226
|
+
if (!filePath.startsWith("file://")) return filePath;
|
|
227
|
+
const renderedFilePath = getNunjucksEngineForFilePath().renderString(filePath, {});
|
|
228
|
+
const { filePath: cleanPath, functionName } = parseFileUrl(renderedFilePath);
|
|
229
|
+
if (context === "assertion" && (cleanPath.endsWith(".py") || isJavascriptFile(cleanPath))) {
|
|
230
|
+
logger.debug(`Preserving Python/JS file reference in assertion context: ${renderedFilePath}`);
|
|
231
|
+
return renderedFilePath;
|
|
232
|
+
}
|
|
233
|
+
if (context === "vars") {
|
|
234
|
+
logger.debug(`Preserving file reference in vars context: ${renderedFilePath}`);
|
|
235
|
+
return renderedFilePath;
|
|
236
|
+
}
|
|
237
|
+
if (functionName && (cleanPath.endsWith(".py") || isJavascriptFile(cleanPath))) return renderedFilePath;
|
|
238
|
+
const pathToUse = functionName && !(cleanPath.endsWith(".py") || isJavascriptFile(cleanPath)) ? renderedFilePath.slice(7) : cleanPath;
|
|
239
|
+
const resolvedPath = path$1.resolve(state.basePath || "", pathToUse);
|
|
240
|
+
if (hasMagic(pathToUse)) {
|
|
241
|
+
const matchedFiles = globSync(resolvedPath, { windowsPathsNoEscape: true });
|
|
242
|
+
if (matchedFiles.length === 0) throw new Error(`No files found matching pattern: ${resolvedPath}`);
|
|
243
|
+
const allContents = [];
|
|
244
|
+
for (const matchedFile of matchedFiles) {
|
|
245
|
+
let contents;
|
|
246
|
+
try {
|
|
247
|
+
contents = fs$1.readFileSync(matchedFile, "utf8");
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error.code === "ENOENT") {
|
|
250
|
+
logger.debug(`File disappeared during glob expansion: ${matchedFile}`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
if (matchedFile.endsWith(".json")) {
|
|
256
|
+
const parsed = JSON.parse(contents);
|
|
257
|
+
if (Array.isArray(parsed)) allContents.push(...parsed);
|
|
258
|
+
else allContents.push(parsed);
|
|
259
|
+
} else if (matchedFile.endsWith(".yaml") || matchedFile.endsWith(".yml")) {
|
|
260
|
+
const parsed = yaml.load(contents);
|
|
261
|
+
if (parsed === null || parsed === void 0) continue;
|
|
262
|
+
if (Array.isArray(parsed)) allContents.push(...parsed);
|
|
263
|
+
else allContents.push(parsed);
|
|
264
|
+
} else if (matchedFile.endsWith(".csv")) {
|
|
265
|
+
const records = parse$1(contents, { columns: true });
|
|
266
|
+
if (records.length > 0 && Object.keys(records[0]).length === 1) allContents.push(...records.map((record) => Object.values(record)[0]));
|
|
267
|
+
else allContents.push(...records);
|
|
268
|
+
} else allContents.push(contents);
|
|
269
|
+
}
|
|
270
|
+
return allContents;
|
|
271
|
+
}
|
|
272
|
+
const finalPath = resolvedPath;
|
|
273
|
+
let contents;
|
|
274
|
+
try {
|
|
275
|
+
contents = fs$1.readFileSync(finalPath, "utf8");
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error.code === "ENOENT") throw new Error(`File does not exist: ${finalPath}`);
|
|
278
|
+
throw new Error(`Failed to read file ${finalPath}: ${error}`);
|
|
279
|
+
}
|
|
280
|
+
if (finalPath.endsWith(".json")) try {
|
|
281
|
+
return JSON.parse(contents);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
throw new Error(`Failed to parse JSON file ${finalPath}: ${error}`);
|
|
284
|
+
}
|
|
285
|
+
if (finalPath.endsWith(".yaml") || finalPath.endsWith(".yml")) try {
|
|
286
|
+
return yaml.load(contents);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
throw new Error(`Failed to parse YAML file ${finalPath}: ${error}`);
|
|
289
|
+
}
|
|
290
|
+
if (finalPath.endsWith(".csv")) {
|
|
291
|
+
const records = parse$1(contents, { columns: true });
|
|
292
|
+
if (records.length > 0 && Object.keys(records[0]).length === 1) return records.map((record) => Object.values(record)[0]);
|
|
293
|
+
return records;
|
|
294
|
+
}
|
|
295
|
+
return contents;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Resolves a relative file path with respect to a base path, handling cloud configuration appropriately.
|
|
299
|
+
* When using a cloud configuration, the current working directory is always used instead of the context's base path.
|
|
300
|
+
*
|
|
301
|
+
* @param filePath - The relative or absolute file path to resolve.
|
|
302
|
+
* @param isCloudConfig - Whether this is a cloud configuration.
|
|
303
|
+
* @returns The resolved absolute file path.
|
|
304
|
+
*/
|
|
305
|
+
function getResolvedRelativePath(filePath, isCloudConfig) {
|
|
306
|
+
if (path$1.isAbsolute(filePath) || !isCloudConfig) return filePath;
|
|
307
|
+
return path$1.join(process.cwd(), filePath);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Recursively loads external file references from a configuration object.
|
|
311
|
+
*
|
|
312
|
+
* @param config - The configuration object to process
|
|
313
|
+
* @param context - Optional context to control file loading behavior
|
|
314
|
+
* @returns The configuration with external file references resolved
|
|
315
|
+
*/
|
|
316
|
+
function maybeLoadConfigFromExternalFile(config, context) {
|
|
317
|
+
if (Array.isArray(config)) return config.map((item) => maybeLoadConfigFromExternalFile(item, context));
|
|
318
|
+
if (config && typeof config === "object" && config !== null) {
|
|
319
|
+
const result = {};
|
|
320
|
+
for (const key of Object.keys(config)) {
|
|
321
|
+
const childContext = key === "value" && typeof config === "object" && config && "type" in config && typeof config.type === "string" && (config.type === "python" || config.type === "javascript") ? "assertion" : key === "vars" ? "vars" : context;
|
|
322
|
+
result[key] = maybeLoadConfigFromExternalFile(config[key], childContext);
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
return maybeLoadFromExternalFile(config, context);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Parses a file path or glob pattern to extract function names and file extensions.
|
|
330
|
+
* Function names can be specified in the filename like this:
|
|
331
|
+
* prompt.py:myFunction or prompts.js:myFunction.
|
|
332
|
+
* @param basePath - The base path for file resolution.
|
|
333
|
+
* @param promptPath - The path or glob pattern.
|
|
334
|
+
* @returns Parsed details including function name, file extension, and directory status.
|
|
335
|
+
*/
|
|
336
|
+
function parsePathOrGlob(basePath, promptPath) {
|
|
337
|
+
if (promptPath.startsWith("file://")) promptPath = promptPath.slice(7);
|
|
338
|
+
const filePath = path$1.resolve(basePath, promptPath);
|
|
339
|
+
let filename = path$1.relative(basePath, filePath);
|
|
340
|
+
let functionName;
|
|
341
|
+
if (filename.includes(":")) {
|
|
342
|
+
const lastColonIndex = filename.lastIndexOf(":");
|
|
343
|
+
if (lastColonIndex > 1) {
|
|
344
|
+
const pathWithoutFunction = filename.slice(0, lastColonIndex);
|
|
345
|
+
if (isJavascriptFile(pathWithoutFunction) || pathWithoutFunction.endsWith(".py") || pathWithoutFunction.endsWith(".go") || pathWithoutFunction.endsWith(".rb")) {
|
|
346
|
+
functionName = filename.slice(lastColonIndex + 1);
|
|
347
|
+
filename = pathWithoutFunction;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
let stats;
|
|
352
|
+
try {
|
|
353
|
+
stats = fs$1.statSync(path$1.join(basePath, filename));
|
|
354
|
+
} catch (err) {
|
|
355
|
+
if (getEnvBool("PROMPTFOO_STRICT_FILES")) throw err;
|
|
356
|
+
}
|
|
357
|
+
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
358
|
+
const isPathPattern = stats?.isDirectory() || hasMagic(promptPath) || hasMagic(normalizedFilePath);
|
|
359
|
+
const safeFilename = path$1.relative(basePath, safeResolve(basePath, filename));
|
|
360
|
+
return {
|
|
361
|
+
extension: isPathPattern ? void 0 : path$1.parse(safeFilename).ext,
|
|
362
|
+
filePath: path$1.join(basePath, safeFilename),
|
|
363
|
+
functionName,
|
|
364
|
+
isPathPattern
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function readOutput(outputPath) {
|
|
368
|
+
const ext = path$1.parse(outputPath).ext.slice(1);
|
|
369
|
+
switch (ext) {
|
|
370
|
+
case "json": return JSON.parse(fs$1.readFileSync(outputPath, "utf-8"));
|
|
371
|
+
default: throw new Error(`Unsupported output file format: ${ext} currently only supports json`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Load custom Nunjucks filters from external files.
|
|
376
|
+
* Note: If a glob pattern matches multiple files, only the last file's export is used.
|
|
377
|
+
* Each filter name should typically resolve to a single file.
|
|
378
|
+
*/
|
|
379
|
+
async function readFilters(filters, basePath = "") {
|
|
380
|
+
const ret = {};
|
|
381
|
+
for (const [name, filterPath] of Object.entries(filters)) {
|
|
382
|
+
const filePaths = globSync(path$1.join(basePath, filterPath), { windowsPathsNoEscape: true });
|
|
383
|
+
for (const filePath of filePaths) ret[name] = await importModule(path$1.resolve(filePath));
|
|
384
|
+
}
|
|
385
|
+
return ret;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Loads configuration from an external file with variable rendering.
|
|
389
|
+
* This is a convenience wrapper that combines renderVarsInObject and maybeLoadFromExternalFile.
|
|
390
|
+
*
|
|
391
|
+
* Use this for simple config fields that:
|
|
392
|
+
* - Need variable rendering ({{ vars.x }}, {{ env.X }})
|
|
393
|
+
* - May reference external files (file://path.json)
|
|
394
|
+
* - Don't have nested file references that need loading
|
|
395
|
+
*
|
|
396
|
+
* For fields with nested file references (like response_format.schema),
|
|
397
|
+
* use maybeLoadResponseFormatFromExternalFile instead.
|
|
398
|
+
*
|
|
399
|
+
* @param config - The configuration to process
|
|
400
|
+
* @param vars - Variables for template rendering
|
|
401
|
+
* @returns The processed configuration with variables rendered and files loaded
|
|
402
|
+
*/
|
|
403
|
+
function maybeLoadFromExternalFileWithVars(config, vars) {
|
|
404
|
+
return maybeLoadFromExternalFile(renderVarsInObject(config, vars));
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Loads response_format configuration from an external file with variable rendering.
|
|
408
|
+
*
|
|
409
|
+
* This function handles the special case where response_format may contain:
|
|
410
|
+
* 1. A top-level file reference (file://format.json)
|
|
411
|
+
* 2. A nested schema reference for json_schema type (schema: file://schema.json)
|
|
412
|
+
*
|
|
413
|
+
* Both levels need variable rendering and file loading.
|
|
414
|
+
*
|
|
415
|
+
* @param responseFormat - The response_format configuration
|
|
416
|
+
* @param vars - Variables for template rendering
|
|
417
|
+
* @returns The processed response_format with all files loaded
|
|
418
|
+
*/
|
|
419
|
+
function maybeLoadResponseFormatFromExternalFile(responseFormat, vars) {
|
|
420
|
+
if (responseFormat === void 0 || responseFormat === null) return responseFormat;
|
|
421
|
+
const loaded = maybeLoadFromExternalFile(renderVarsInObject(responseFormat, vars));
|
|
422
|
+
if (!loaded || typeof loaded !== "object") return loaded;
|
|
423
|
+
if (loaded.type === "json_schema") {
|
|
424
|
+
const nestedSchema = loaded.schema || loaded.json_schema?.schema;
|
|
425
|
+
if (nestedSchema) {
|
|
426
|
+
const loadedSchema = maybeLoadFromExternalFile(renderVarsInObject(nestedSchema, vars));
|
|
427
|
+
if (loaded.schema !== void 0) return {
|
|
428
|
+
...loaded,
|
|
429
|
+
schema: loadedSchema
|
|
430
|
+
};
|
|
431
|
+
else if (loaded.json_schema?.schema !== void 0) return {
|
|
432
|
+
...loaded,
|
|
433
|
+
json_schema: {
|
|
434
|
+
...loaded.json_schema,
|
|
435
|
+
schema: loadedSchema
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return loaded;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Renders variables in a tools object and loads from external file if applicable.
|
|
444
|
+
* This function combines renderVarsInObject and maybeLoadFromExternalFile into a single step
|
|
445
|
+
* specifically for handling tools configurations.
|
|
446
|
+
*
|
|
447
|
+
* Supports loading from JSON, YAML, Python, and JavaScript files.
|
|
448
|
+
*
|
|
449
|
+
* @param tools - The tools configuration object or array to process.
|
|
450
|
+
* @param vars - Variables to use for rendering.
|
|
451
|
+
* @returns The processed tools configuration with variables rendered and content loaded from files if needed.
|
|
452
|
+
* @throws {Error} If the loaded tools are in an invalid format
|
|
453
|
+
*/
|
|
454
|
+
async function maybeLoadToolsFromExternalFile(tools, vars) {
|
|
455
|
+
const rendered = renderVarsInObject(tools, vars);
|
|
456
|
+
if (typeof rendered === "string" && rendered.startsWith("file://")) {
|
|
457
|
+
const { filePath, functionName } = parseFileUrl(rendered);
|
|
458
|
+
if (functionName && (filePath.endsWith(".py") || isJavascriptFile(filePath))) {
|
|
459
|
+
const fileType = filePath.endsWith(".py") ? "Python" : "JavaScript";
|
|
460
|
+
logger.debug(`[maybeLoadToolsFromExternalFile] Loading tools from ${fileType} file: ${filePath}:${functionName}`);
|
|
461
|
+
try {
|
|
462
|
+
let toolDefinitions;
|
|
463
|
+
if (filePath.endsWith(".py")) {
|
|
464
|
+
const absPath = safeResolve(state.basePath || process.cwd(), filePath);
|
|
465
|
+
logger.debug(`[maybeLoadToolsFromExternalFile] Resolved Python path: ${absPath}`);
|
|
466
|
+
toolDefinitions = await runPython(absPath, functionName, []);
|
|
467
|
+
} else {
|
|
468
|
+
const absPath = safeResolve(state.basePath || process.cwd(), filePath);
|
|
469
|
+
logger.debug(`[maybeLoadToolsFromExternalFile] Resolved JavaScript path: ${absPath}`);
|
|
470
|
+
const module = await importModule(absPath);
|
|
471
|
+
const fn = module[functionName] || module.default?.[functionName];
|
|
472
|
+
if (typeof fn !== "function") {
|
|
473
|
+
const availableExports = Object.keys(module).filter((k) => k !== "default");
|
|
474
|
+
const basePath = state.basePath || process.cwd();
|
|
475
|
+
throw new Error(`Function "${functionName}" not found in ${filePath}. Available exports: ${availableExports.length > 0 ? availableExports.join(", ") : "(none)"}\nResolved from: ${basePath}`);
|
|
476
|
+
}
|
|
477
|
+
toolDefinitions = await Promise.resolve(fn());
|
|
478
|
+
}
|
|
479
|
+
if (!toolDefinitions || typeof toolDefinitions === "string" || typeof toolDefinitions === "number" || typeof toolDefinitions === "boolean") throw new Error(`Function "${functionName}" must return an array or object of tool definitions, but returned: ${toolDefinitions === null ? "null" : typeof toolDefinitions}`);
|
|
480
|
+
logger.debug(`[maybeLoadToolsFromExternalFile] Successfully loaded ${Array.isArray(toolDefinitions) ? toolDefinitions.length : "object"} tools`);
|
|
481
|
+
return toolDefinitions;
|
|
482
|
+
} catch (err) {
|
|
483
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
484
|
+
const basePath = state.basePath || process.cwd();
|
|
485
|
+
throw new Error(`Failed to load tools from ${rendered}:\n${errorMessage}\n\nMake sure the function "${functionName}" exists and returns a valid tool definition array.\nResolved from: ${basePath}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (filePath.endsWith(".py") || isJavascriptFile(filePath)) {
|
|
489
|
+
const ext = filePath.endsWith(".py") ? "Python" : "JavaScript";
|
|
490
|
+
const basePath = state.basePath || process.cwd();
|
|
491
|
+
throw new Error(`Cannot load tools from ${rendered}\n${ext} files require a function name. Use this format:\n tools: file://${filePath}:get_tools\n\nYour ${ext} file should export a function that returns tool definitions:\n` + (filePath.endsWith(".py") ? ` def get_tools():\n return [{"type": "function", "function": {...}}]` : ` module.exports.get_tools = () => [{ type: "function", function: {...} }];`) + `\n\nResolved from: ${basePath}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (Array.isArray(rendered)) {
|
|
495
|
+
const results = await Promise.all(rendered.map((item) => maybeLoadToolsFromExternalFile(item, vars)));
|
|
496
|
+
if (results.every((r) => Array.isArray(r))) return results.flat();
|
|
497
|
+
return results;
|
|
498
|
+
}
|
|
499
|
+
if (typeof rendered !== "string") return rendered;
|
|
500
|
+
const loaded = maybeLoadFromExternalFile(rendered);
|
|
501
|
+
if (loaded !== void 0 && loaded !== null && typeof loaded === "string") {
|
|
502
|
+
if (loaded.startsWith("file://")) throw new Error(`Failed to load tools from ${loaded}\nEnsure the file exists and contains valid JSON or YAML tool definitions.`);
|
|
503
|
+
if (loaded.includes("def ") || loaded.includes("import ")) throw new Error("Invalid tools configuration: file appears to contain Python code.\nPython files require a function name. Use this format:\n tools: file://tools.py:get_tools");
|
|
504
|
+
throw new Error("Invalid tools configuration: expected an array or object, but got a string.\nIf using file://, ensure the file contains valid JSON or YAML tool definitions.");
|
|
505
|
+
}
|
|
506
|
+
return loaded;
|
|
507
|
+
}
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/util/provider.ts
|
|
510
|
+
function canonicalizeProviderId(id) {
|
|
511
|
+
if (id.startsWith("file://")) {
|
|
512
|
+
const filePath = id.slice(7);
|
|
513
|
+
return path$1.isAbsolute(filePath) ? id : `file://${path$1.resolve(filePath)}`;
|
|
514
|
+
}
|
|
515
|
+
for (const prefix of [
|
|
516
|
+
"exec:",
|
|
517
|
+
"python:",
|
|
518
|
+
"golang:"
|
|
519
|
+
]) if (id.startsWith(prefix)) {
|
|
520
|
+
const filePath = id.slice(prefix.length);
|
|
521
|
+
if (filePath.includes("/") || filePath.includes("\\")) return `${prefix}${path$1.resolve(filePath)}`;
|
|
522
|
+
return id;
|
|
523
|
+
}
|
|
524
|
+
if ((id.endsWith(".js") || id.endsWith(".ts") || id.endsWith(".mjs")) && (id.includes("/") || id.includes("\\"))) return `file://${path$1.resolve(id)}`;
|
|
525
|
+
return id;
|
|
526
|
+
}
|
|
527
|
+
function getProviderLabel(provider) {
|
|
528
|
+
return provider?.label && typeof provider.label === "string" ? provider.label : void 0;
|
|
529
|
+
}
|
|
530
|
+
function providerToIdentifier(provider) {
|
|
531
|
+
if (!provider) return;
|
|
532
|
+
if (typeof provider === "string") return canonicalizeProviderId(provider);
|
|
533
|
+
const label = getProviderLabel(provider);
|
|
534
|
+
if (label) return label;
|
|
535
|
+
if (isApiProvider(provider)) return canonicalizeProviderId(provider.id());
|
|
536
|
+
if (isProviderOptions(provider)) {
|
|
537
|
+
if (provider.id) return canonicalizeProviderId(provider.id);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (typeof provider === "object" && "id" in provider && typeof provider.id === "string") return canonicalizeProviderId(provider.id);
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Gets a descriptive identifier string for a provider, showing both label and ID when both exist.
|
|
544
|
+
* Useful for error messages to help users debug provider reference issues.
|
|
545
|
+
*/
|
|
546
|
+
function getProviderDescription(provider) {
|
|
547
|
+
const label = provider.label;
|
|
548
|
+
const id = provider.id();
|
|
549
|
+
if (label && label !== id) return `${label} (${id})`;
|
|
550
|
+
return id;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Checks if a provider reference matches a given provider.
|
|
554
|
+
* Supports exact matching and wildcard patterns.
|
|
555
|
+
*/
|
|
556
|
+
function doesProviderRefMatch(ref, provider) {
|
|
557
|
+
const label = provider.label;
|
|
558
|
+
const id = provider.id();
|
|
559
|
+
const canonicalRef = canonicalizeProviderId(ref);
|
|
560
|
+
const canonicalId = canonicalizeProviderId(id);
|
|
561
|
+
if (label && label === ref) return true;
|
|
562
|
+
if (id === ref || canonicalId === canonicalRef) return true;
|
|
563
|
+
if (ref.endsWith("*")) {
|
|
564
|
+
const prefix = ref.slice(0, -1);
|
|
565
|
+
if (label?.startsWith(prefix) || id.startsWith(prefix) || canonicalId.startsWith(prefix)) return true;
|
|
566
|
+
}
|
|
567
|
+
if (label?.startsWith(`${ref}:`) || id.startsWith(`${ref}:`) || canonicalId.startsWith(`${ref}:`)) return true;
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Checks if a provider is allowed based on a list of allowed references.
|
|
572
|
+
*/
|
|
573
|
+
function isProviderAllowed(provider, allowedProviders) {
|
|
574
|
+
if (!Array.isArray(allowedProviders)) return true;
|
|
575
|
+
if (allowedProviders.length === 0) return false;
|
|
576
|
+
return allowedProviders.some((ref) => doesProviderRefMatch(ref, provider));
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Detects if a provider uses OpenAI models.
|
|
580
|
+
* This includes direct OpenAI providers and Azure OpenAI.
|
|
581
|
+
*/
|
|
582
|
+
function isOpenAiProvider(providerId) {
|
|
583
|
+
const lowerProviderId = providerId.toLowerCase();
|
|
584
|
+
if (lowerProviderId.startsWith("openai:")) return true;
|
|
585
|
+
if (lowerProviderId.startsWith("azureopenai:")) return true;
|
|
586
|
+
if (lowerProviderId.startsWith("azure:")) {
|
|
587
|
+
if ([
|
|
588
|
+
"gpt",
|
|
589
|
+
"openai",
|
|
590
|
+
"davinci",
|
|
591
|
+
"curie",
|
|
592
|
+
"babbage",
|
|
593
|
+
"ada",
|
|
594
|
+
"text-embedding",
|
|
595
|
+
"whisper",
|
|
596
|
+
"dall-e",
|
|
597
|
+
"tts"
|
|
598
|
+
].some((indicator) => lowerProviderId.includes(indicator))) return true;
|
|
599
|
+
}
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Detects if a provider uses Anthropic/Claude models.
|
|
604
|
+
* This includes direct Anthropic providers, Bedrock with Claude, and Vertex with Claude.
|
|
605
|
+
*/
|
|
606
|
+
function isAnthropicProvider(providerId) {
|
|
607
|
+
const lowerProviderId = providerId.toLowerCase();
|
|
608
|
+
if (lowerProviderId.startsWith("anthropic:")) return true;
|
|
609
|
+
if (lowerProviderId.startsWith("bedrock:")) {
|
|
610
|
+
if (lowerProviderId.includes("claude") || lowerProviderId.includes("anthropic")) return true;
|
|
611
|
+
}
|
|
612
|
+
if (lowerProviderId.startsWith("vertex:")) {
|
|
613
|
+
if (lowerProviderId.includes("claude")) return true;
|
|
614
|
+
}
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
const KNOWN_ENV_VARS = {
|
|
618
|
+
openai: "OPENAI_API_KEY",
|
|
619
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
620
|
+
google: "GOOGLE_API_KEY",
|
|
621
|
+
mistral: "MISTRAL_API_KEY",
|
|
622
|
+
cohere: "COHERE_API_KEY",
|
|
623
|
+
replicate: "REPLICATE_API_TOKEN",
|
|
624
|
+
voyage: "VOYAGE_API_KEY",
|
|
625
|
+
ai21: "AI21_API_KEY",
|
|
626
|
+
xai: "XAI_API_KEY",
|
|
627
|
+
groq: "GROQ_API_KEY",
|
|
628
|
+
deepseek: "DEEPSEEK_API_KEY",
|
|
629
|
+
perplexity: "PERPLEXITY_API_KEY",
|
|
630
|
+
hyperbolic: "HYPERBOLIC_API_KEY",
|
|
631
|
+
cerebras: "CEREBRAS_API_KEY",
|
|
632
|
+
togetherai: "TOGETHER_API_KEY",
|
|
633
|
+
fal: "FAL_KEY",
|
|
634
|
+
huggingface: "HF_TOKEN",
|
|
635
|
+
"cloudflare-ai": "CLOUDFLARE_API_KEY"
|
|
636
|
+
};
|
|
637
|
+
function getDefaultEnvVar(providerId) {
|
|
638
|
+
const prefix = providerId.split(":")[0];
|
|
639
|
+
return KNOWN_ENV_VARS[prefix] || `${prefix.toUpperCase()}_API_KEY`;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Pre-checks providers for missing API keys before evaluation starts.
|
|
643
|
+
* Assumes getApiKey() is side-effect free (no network calls or token refresh).
|
|
644
|
+
*/
|
|
645
|
+
function checkProviderApiKeys(providers) {
|
|
646
|
+
const missingApiKeys = /* @__PURE__ */ new Map();
|
|
647
|
+
for (const provider of providers) {
|
|
648
|
+
const p = provider;
|
|
649
|
+
if (typeof p.getApiKey !== "function") continue;
|
|
650
|
+
if (provider.id().startsWith("azure:")) continue;
|
|
651
|
+
const requiresKey = typeof p.requiresApiKey === "function" ? p.requiresApiKey() : p.config?.apiKeyRequired !== false;
|
|
652
|
+
let apiKey;
|
|
653
|
+
try {
|
|
654
|
+
apiKey = p.getApiKey();
|
|
655
|
+
} catch {
|
|
656
|
+
apiKey = void 0;
|
|
657
|
+
}
|
|
658
|
+
if (requiresKey && !apiKey) {
|
|
659
|
+
const envVar = p.config?.apiKeyEnvar || getDefaultEnvVar(provider.id());
|
|
660
|
+
if (!missingApiKeys.has(envVar)) missingApiKeys.set(envVar, []);
|
|
661
|
+
missingApiKeys.get(envVar).push(provider.id());
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return missingApiKeys;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Detects if a provider uses Google models.
|
|
668
|
+
* This includes direct Google/Vertex providers with Gemini and other Google models.
|
|
669
|
+
* Note: Vertex with Claude models is NOT counted as Google (it's Anthropic).
|
|
670
|
+
*/
|
|
671
|
+
function isGoogleProvider(providerId) {
|
|
672
|
+
const lowerProviderId = providerId.toLowerCase();
|
|
673
|
+
if (lowerProviderId.startsWith("google:")) return true;
|
|
674
|
+
if (lowerProviderId.startsWith("vertex:")) {
|
|
675
|
+
if (!lowerProviderId.includes("claude")) return true;
|
|
676
|
+
}
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region src/util/comparison.ts
|
|
681
|
+
/**
|
|
682
|
+
* Explicit runtime variable names that don't follow the underscore convention.
|
|
683
|
+
* These are added during evaluation but aren't part of the original test definition.
|
|
684
|
+
*
|
|
685
|
+
* - sessionId: Added by multi-turn strategy providers (GOAT, Crescendo)
|
|
686
|
+
*
|
|
687
|
+
* Note: Variables starting with underscore (e.g., _conversation) are automatically
|
|
688
|
+
* treated as runtime variables and filtered out.
|
|
689
|
+
*/
|
|
690
|
+
const EXPLICIT_RUNTIME_VAR_KEYS = ["sessionId"];
|
|
691
|
+
/**
|
|
692
|
+
* Checks if a variable key is a runtime-only variable that should be filtered
|
|
693
|
+
* when comparing test cases.
|
|
694
|
+
*
|
|
695
|
+
* Runtime variables are identified by:
|
|
696
|
+
* 1. Starting with underscore (_) - convention for internal/runtime vars
|
|
697
|
+
* 2. Being in the explicit runtime var list (for legacy vars like sessionId)
|
|
698
|
+
*/
|
|
699
|
+
function isRuntimeVar(key) {
|
|
700
|
+
return key.startsWith("_") || EXPLICIT_RUNTIME_VAR_KEYS.includes(key);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Filters out runtime-only variables that are added during evaluation
|
|
704
|
+
* but aren't part of the original test definition.
|
|
705
|
+
*
|
|
706
|
+
* This is used when comparing test cases to determine if a result
|
|
707
|
+
* corresponds to a particular test, regardless of runtime state.
|
|
708
|
+
*
|
|
709
|
+
* Runtime variables are identified by:
|
|
710
|
+
* - Starting with underscore (e.g., _conversation, _metadata)
|
|
711
|
+
* - Being in the explicit list (e.g., sessionId for backward compatibility)
|
|
712
|
+
*/
|
|
713
|
+
function filterRuntimeVars(vars) {
|
|
714
|
+
if (!vars || typeof vars !== "object" || Array.isArray(vars)) return vars;
|
|
715
|
+
const filtered = {};
|
|
716
|
+
for (const [key, value] of Object.entries(vars)) if (!isRuntimeVar(key)) filtered[key] = value;
|
|
717
|
+
return filtered;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Extracts only runtime variables from a vars object.
|
|
721
|
+
* This is the inverse of filterRuntimeVars.
|
|
722
|
+
*
|
|
723
|
+
* Used to restore runtime state when re-running filtered tests.
|
|
724
|
+
*/
|
|
725
|
+
function extractRuntimeVars(vars) {
|
|
726
|
+
if (!vars || typeof vars !== "object" || Array.isArray(vars)) return;
|
|
727
|
+
const extracted = {};
|
|
728
|
+
for (const [key, value] of Object.entries(vars)) if (isRuntimeVar(key)) extracted[key] = value;
|
|
729
|
+
return Object.keys(extracted).length > 0 ? extracted : void 0;
|
|
730
|
+
}
|
|
731
|
+
function varsMatch(vars1, vars2) {
|
|
732
|
+
return deepEqual(vars1, vars2);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Generate a unique key for a test case for deduplication purposes.
|
|
736
|
+
* Excludes runtime variables and includes strategyId to distinguish tests
|
|
737
|
+
* with the same prompt but different strategies.
|
|
738
|
+
*
|
|
739
|
+
* @param testCase - The test case to generate a key for
|
|
740
|
+
* @returns A JSON string that uniquely identifies the test case
|
|
741
|
+
*/
|
|
742
|
+
function getTestCaseDeduplicationKey(testCase) {
|
|
743
|
+
const filteredVars = filterRuntimeVars(testCase.vars);
|
|
744
|
+
const strategyId = testCase.metadata?.strategyId || "none";
|
|
745
|
+
return JSON.stringify({
|
|
746
|
+
vars: filteredVars,
|
|
747
|
+
strategyId
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Deduplicates an array of test cases based on their vars and strategyId.
|
|
752
|
+
* Tests with the same vars but different strategies are considered different.
|
|
753
|
+
* Runtime variables (like _conversation, sessionId) are filtered out before comparison.
|
|
754
|
+
*
|
|
755
|
+
* @param tests - Array of test cases to deduplicate
|
|
756
|
+
* @returns Deduplicated array of test cases
|
|
757
|
+
*/
|
|
758
|
+
function deduplicateTestCases(tests) {
|
|
759
|
+
const seen = /* @__PURE__ */ new Set();
|
|
760
|
+
return tests.filter((test) => {
|
|
761
|
+
const key = getTestCaseDeduplicationKey(test);
|
|
762
|
+
if (seen.has(key)) return false;
|
|
763
|
+
seen.add(key);
|
|
764
|
+
return true;
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
function resultIsForTestCase(result, testCase) {
|
|
768
|
+
const testProviderId = testCase.provider ? providerToIdentifier(testCase.provider) : void 0;
|
|
769
|
+
const resultProviderId = providerToIdentifier(result.provider);
|
|
770
|
+
const providersMatch = !testProviderId || !resultProviderId || testProviderId === resultProviderId;
|
|
771
|
+
const resultVars = filterRuntimeVars(result.vars);
|
|
772
|
+
const testVars = filterRuntimeVars(testCase.vars);
|
|
773
|
+
const doVarsMatch = varsMatch(testVars, resultVars);
|
|
774
|
+
const isMatch = doVarsMatch && providersMatch;
|
|
775
|
+
if (!isMatch) {
|
|
776
|
+
const varKeys = testVars ? Object.keys(testVars).join(", ") : "none";
|
|
777
|
+
logger.debug(`[resultIsForTestCase] No match: vars=${doVarsMatch}, providers=${providersMatch}`, {
|
|
778
|
+
testProvider: testProviderId || "none",
|
|
779
|
+
resultProvider: resultProviderId || "none",
|
|
780
|
+
testVarKeys: varKeys
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
return isMatch;
|
|
784
|
+
}
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/util/env.ts
|
|
787
|
+
/**
|
|
788
|
+
* Load environment variables from .env file(s).
|
|
789
|
+
* @param envPath - Single path, array of paths, or undefined for default .env loading.
|
|
790
|
+
* When paths are explicitly specified, all files must exist or an error is thrown.
|
|
791
|
+
* When multiple files are provided, later files override values from earlier files.
|
|
792
|
+
*/
|
|
793
|
+
function setupEnv(envPath) {
|
|
794
|
+
if (envPath) {
|
|
795
|
+
const paths = (Array.isArray(envPath) ? envPath : [envPath]).flatMap((p) => p.includes(",") ? p.split(",").map((s) => s.trim()) : p.trim()).filter((p) => p.length > 0);
|
|
796
|
+
if (paths.length === 0) {
|
|
797
|
+
dotenv.config({ quiet: true });
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
for (const p of paths) if (!fs$1.existsSync(p)) throw new Error(`Environment file not found: ${p}`);
|
|
801
|
+
if (paths.length === 1) logger.info(`Loading environment variables from ${paths[0]}`);
|
|
802
|
+
else logger.info(`Loading environment variables from: ${paths.join(", ")}`);
|
|
803
|
+
const pathArg = paths.length === 1 ? paths[0] : paths;
|
|
804
|
+
dotenv.config({
|
|
805
|
+
path: pathArg,
|
|
806
|
+
override: true,
|
|
807
|
+
quiet: true
|
|
808
|
+
});
|
|
809
|
+
} else dotenv.config({ quiet: true });
|
|
810
|
+
}
|
|
811
|
+
//#endregion
|
|
812
|
+
//#region src/googleSheets.ts
|
|
813
|
+
async function checkGoogleSheetAccess(url) {
|
|
814
|
+
try {
|
|
815
|
+
const response = await fetchWithProxy(url);
|
|
816
|
+
if (response.ok) return {
|
|
817
|
+
public: true,
|
|
818
|
+
status: response.status
|
|
819
|
+
};
|
|
820
|
+
else return {
|
|
821
|
+
public: false,
|
|
822
|
+
status: response.status
|
|
823
|
+
};
|
|
824
|
+
} catch (error) {
|
|
825
|
+
logger.error(`Error checking sheet access: ${error}`);
|
|
826
|
+
return { public: false };
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
async function fetchCsvFromGoogleSheetUnauthenticated(url) {
|
|
830
|
+
const { parse: parseCsv } = await import("csv-parse/sync");
|
|
831
|
+
const gid = new URL(url).searchParams.get("gid");
|
|
832
|
+
const response = await fetchWithProxy(`${url.replace(/\/edit.*$/, "/export")}?format=csv${gid ? `&gid=${gid}` : ""}`);
|
|
833
|
+
if (response.status !== 200) throw new Error(`Failed to fetch CSV from Google Sheets URL: ${url}`);
|
|
834
|
+
return parseCsv(await response.text(), { columns: true });
|
|
835
|
+
}
|
|
836
|
+
async function fetchCsvFromGoogleSheetAuthenticated(url) {
|
|
837
|
+
const { sheets: googleSheets, auth: googleAuth } = await import("@googleapis/sheets");
|
|
838
|
+
const auth = new googleAuth.GoogleAuth({ scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"] });
|
|
839
|
+
const sheets = googleSheets("v4");
|
|
840
|
+
const match = url.match(/\/d\/([^/]+)/);
|
|
841
|
+
if (!match) throw new Error(`Invalid Google Sheets URL: ${url}`);
|
|
842
|
+
const spreadsheetId = match[1];
|
|
843
|
+
let range;
|
|
844
|
+
const gid = Number(new URL(url).searchParams.get("gid"));
|
|
845
|
+
if (gid) {
|
|
846
|
+
const sheet = (await sheets.spreadsheets.get({
|
|
847
|
+
spreadsheetId,
|
|
848
|
+
auth
|
|
849
|
+
})).data.sheets?.find((sheet) => sheet.properties?.sheetId === gid);
|
|
850
|
+
if (!sheet || !sheet.properties?.title) throw new Error(`Sheet not found for gid: ${gid}`);
|
|
851
|
+
range = sheet.properties.title;
|
|
852
|
+
} else {
|
|
853
|
+
const firstSheet = (await sheets.spreadsheets.get({
|
|
854
|
+
spreadsheetId,
|
|
855
|
+
auth
|
|
856
|
+
})).data.sheets?.[0];
|
|
857
|
+
if (!firstSheet || !firstSheet.properties?.title) throw new Error(`No sheets found in spreadsheet`);
|
|
858
|
+
range = firstSheet.properties.title;
|
|
859
|
+
}
|
|
860
|
+
const rows = (await sheets.spreadsheets.values.get({
|
|
861
|
+
spreadsheetId,
|
|
862
|
+
range,
|
|
863
|
+
auth
|
|
864
|
+
})).data.values;
|
|
865
|
+
if (!rows?.length) throw new Error(`No data found in Google Sheets URL: ${url}`);
|
|
866
|
+
const headers = rows[0];
|
|
867
|
+
return rows.slice(1).map((row) => {
|
|
868
|
+
const csvRow = {};
|
|
869
|
+
headers.forEach((header, index) => {
|
|
870
|
+
csvRow[header] = row[index] ?? "";
|
|
871
|
+
});
|
|
872
|
+
return csvRow;
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
async function fetchCsvFromGoogleSheet(url) {
|
|
876
|
+
const { public: isPublic } = await checkGoogleSheetAccess(url);
|
|
877
|
+
logger.debug(`Google Sheets URL: ${url}, isPublic: ${isPublic}`);
|
|
878
|
+
if (isPublic) return fetchCsvFromGoogleSheetUnauthenticated(url);
|
|
879
|
+
return fetchCsvFromGoogleSheetAuthenticated(url);
|
|
880
|
+
}
|
|
881
|
+
async function writeCsvToGoogleSheet(rows, url) {
|
|
882
|
+
const { sheets: googleSheets, auth: googleAuth } = await import("@googleapis/sheets");
|
|
883
|
+
const auth = new googleAuth.GoogleAuth({ scopes: ["https://www.googleapis.com/auth/spreadsheets"] });
|
|
884
|
+
const sheets = googleSheets("v4");
|
|
885
|
+
const match = url.match(/\/d\/([^/]+)/);
|
|
886
|
+
if (!match) throw new Error(`Invalid Google Sheets URL: ${url}`);
|
|
887
|
+
const spreadsheetId = match[1];
|
|
888
|
+
const headers = Object.keys(rows[0]);
|
|
889
|
+
const values = [headers, ...rows.map((row) => headers.map((header) => row[header]))];
|
|
890
|
+
const getColumnLetter = (col) => {
|
|
891
|
+
let letter = "";
|
|
892
|
+
while (col > 0) {
|
|
893
|
+
col--;
|
|
894
|
+
letter = String.fromCharCode(65 + col % 26) + letter;
|
|
895
|
+
col = Math.floor(col / 26);
|
|
896
|
+
}
|
|
897
|
+
return letter;
|
|
898
|
+
};
|
|
899
|
+
const numRows = values.length;
|
|
900
|
+
const numCols = headers.length;
|
|
901
|
+
const endColumn = getColumnLetter(numCols);
|
|
902
|
+
let range;
|
|
903
|
+
const gid = Number(new URL(url).searchParams.get("gid"));
|
|
904
|
+
if (gid) {
|
|
905
|
+
const sheet = (await sheets.spreadsheets.get({
|
|
906
|
+
spreadsheetId,
|
|
907
|
+
auth
|
|
908
|
+
})).data.sheets?.find((sheet) => sheet.properties?.sheetId === gid);
|
|
909
|
+
if (!sheet || !sheet.properties?.title) throw new Error(`Sheet not found for gid: ${gid}`);
|
|
910
|
+
range = `${sheet.properties.title}!A1:${endColumn}${numRows}`;
|
|
911
|
+
} else {
|
|
912
|
+
const newSheetTitle = `Sheet${Date.now()}`;
|
|
913
|
+
await sheets.spreadsheets.batchUpdate({
|
|
914
|
+
spreadsheetId,
|
|
915
|
+
auth,
|
|
916
|
+
requestBody: { requests: [{ addSheet: { properties: { title: newSheetTitle } } }] }
|
|
917
|
+
});
|
|
918
|
+
range = `${newSheetTitle}!A1:${endColumn}${numRows}`;
|
|
919
|
+
}
|
|
920
|
+
logger.debug(`Writing CSV to Google Sheets URL: ${url} with ${values.length} rows`);
|
|
921
|
+
await sheets.spreadsheets.values.update({
|
|
922
|
+
spreadsheetId,
|
|
923
|
+
range,
|
|
924
|
+
valueInputOption: "USER_ENTERED",
|
|
925
|
+
auth,
|
|
926
|
+
requestBody: { values }
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region src/server/utils/evalTableUtils.ts
|
|
931
|
+
/**
|
|
932
|
+
* Error thrown when a comparison eval is not found.
|
|
933
|
+
*/
|
|
934
|
+
var ComparisonEvalNotFoundError = class extends Error {
|
|
935
|
+
constructor(evalId) {
|
|
936
|
+
super(`Comparison eval not found: ${evalId}`);
|
|
937
|
+
this.name = "ComparisonEvalNotFoundError";
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
/**
|
|
941
|
+
*
|
|
942
|
+
*
|
|
943
|
+
*
|
|
944
|
+
* Keep this in it's current order, as it is used to map the columns in the CSV, so it needs to be static.
|
|
945
|
+
*
|
|
946
|
+
*
|
|
947
|
+
* The keys are the names of the columns in the metadata object, and the values are the names of the columns in the CSV.
|
|
948
|
+
*
|
|
949
|
+
* This is imported by enterprise so it doesn't need to be copied.
|
|
950
|
+
*
|
|
951
|
+
*/
|
|
952
|
+
const REDTEAM_METADATA_KEYS_TO_CSV_COLUMN_NAMES = {
|
|
953
|
+
messages: "Messages",
|
|
954
|
+
redteamHistory: "RedteamHistory",
|
|
955
|
+
redteamTreeHistory: "RedteamTreeHistory",
|
|
956
|
+
pluginId: "pluginId",
|
|
957
|
+
strategyId: "strategyId",
|
|
958
|
+
sessionId: "sessionId",
|
|
959
|
+
sessionIds: "sessionIds"
|
|
960
|
+
};
|
|
961
|
+
const REDTEAM_METADATA_COLUMNS = Object.values(REDTEAM_METADATA_KEYS_TO_CSV_COLUMN_NAMES);
|
|
962
|
+
/**
|
|
963
|
+
* Get the status string for an output
|
|
964
|
+
*/
|
|
965
|
+
function getOutputStatus(output) {
|
|
966
|
+
if (output.pass) return "PASS";
|
|
967
|
+
return output.failureReason === ResultFailureReason.ASSERT ? "FAIL" : "ERROR";
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Format named scores for CSV output.
|
|
971
|
+
* Returns empty string if no named scores, otherwise JSON string.
|
|
972
|
+
*/
|
|
973
|
+
function formatNamedScores(namedScores) {
|
|
974
|
+
if (!namedScores || Object.keys(namedScores).length === 0) return "";
|
|
975
|
+
const rounded = {};
|
|
976
|
+
for (const [key, value] of Object.entries(namedScores)) if (typeof value === "number" && !Number.isNaN(value)) rounded[key] = Number(value.toFixed(2));
|
|
977
|
+
if (Object.keys(rounded).length === 0) return "";
|
|
978
|
+
return JSON.stringify(rounded);
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Build CSV headers for an evaluation table.
|
|
982
|
+
*
|
|
983
|
+
* @param vars - Variable names from the table head
|
|
984
|
+
* @param prompts - Prompt definitions from the table head
|
|
985
|
+
* @param options - Export options
|
|
986
|
+
* @returns Array of header strings
|
|
987
|
+
*/
|
|
988
|
+
function buildCsvHeaders(vars, prompts, options = {}) {
|
|
989
|
+
const headers = [
|
|
990
|
+
...options.hasDescriptions ? ["Description"] : [],
|
|
991
|
+
...vars,
|
|
992
|
+
...prompts.flatMap((prompt) => {
|
|
993
|
+
const provider = prompt.provider || "";
|
|
994
|
+
return [
|
|
995
|
+
provider ? `[${provider}] ${prompt.label}` : prompt.label,
|
|
996
|
+
"Status",
|
|
997
|
+
"Score",
|
|
998
|
+
"Named Scores",
|
|
999
|
+
"Grader Reason",
|
|
1000
|
+
"Comment"
|
|
1001
|
+
];
|
|
1002
|
+
})
|
|
1003
|
+
];
|
|
1004
|
+
if (options.isRedteam) headers.push(...REDTEAM_METADATA_COLUMNS);
|
|
1005
|
+
return headers;
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Convert a single table row to CSV row values.
|
|
1009
|
+
*
|
|
1010
|
+
* @param row - The table row to convert
|
|
1011
|
+
* @param options - Export options
|
|
1012
|
+
* @returns Array of values for the CSV row
|
|
1013
|
+
*/
|
|
1014
|
+
function tableRowToCsvValues(row, options = {}) {
|
|
1015
|
+
const rowValues = [
|
|
1016
|
+
...options.hasDescriptions ? [row.test.description || ""] : [],
|
|
1017
|
+
...row.vars,
|
|
1018
|
+
...row.outputs.flatMap((output) => {
|
|
1019
|
+
if (!output) return [
|
|
1020
|
+
"",
|
|
1021
|
+
"",
|
|
1022
|
+
"",
|
|
1023
|
+
"",
|
|
1024
|
+
"",
|
|
1025
|
+
""
|
|
1026
|
+
];
|
|
1027
|
+
const status = getOutputStatus(output);
|
|
1028
|
+
const score = output.score?.toFixed(2) ?? "";
|
|
1029
|
+
const namedScores = formatNamedScores(output.namedScores);
|
|
1030
|
+
return [
|
|
1031
|
+
output.text || "",
|
|
1032
|
+
status,
|
|
1033
|
+
score,
|
|
1034
|
+
namedScores,
|
|
1035
|
+
output.gradingResult?.reason || "",
|
|
1036
|
+
output.gradingResult?.comment || ""
|
|
1037
|
+
];
|
|
1038
|
+
})
|
|
1039
|
+
];
|
|
1040
|
+
if (options.isRedteam) {
|
|
1041
|
+
const redteamKeys = Object.keys(REDTEAM_METADATA_KEYS_TO_CSV_COLUMN_NAMES);
|
|
1042
|
+
const firstOutputMetadata = row.outputs[0]?.metadata;
|
|
1043
|
+
for (const key of redteamKeys) {
|
|
1044
|
+
let value = firstOutputMetadata?.[key];
|
|
1045
|
+
if (key === "strategyId" && (value === null || value === void 0)) value = "basic";
|
|
1046
|
+
if (value === null || value === void 0) rowValues.push("");
|
|
1047
|
+
else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") rowValues.push(value.toString());
|
|
1048
|
+
else rowValues.push(JSON.stringify(value));
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return rowValues;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Generates CSV data from evaluation table data.
|
|
1055
|
+
*
|
|
1056
|
+
* Column structure per prompt:
|
|
1057
|
+
* - Output: Pure LLM output text (no pass/fail prefix)
|
|
1058
|
+
* - Status: PASS | FAIL | ERROR
|
|
1059
|
+
* - Score: Numeric score (e.g., "1.00")
|
|
1060
|
+
* - Named Scores: JSON object with per-assertion scores (e.g., {"clarity": 0.90, "accuracy": 0.85})
|
|
1061
|
+
* - Grader Reason: Explanation from the grader
|
|
1062
|
+
* - Comment: Additional grader comment
|
|
1063
|
+
*
|
|
1064
|
+
* This function is the single source of truth for CSV generation,
|
|
1065
|
+
* used by both the WebUI export and CLI export.
|
|
1066
|
+
*
|
|
1067
|
+
* @param table - The evaluation table data
|
|
1068
|
+
* @param options - Export options
|
|
1069
|
+
* @returns CSV formatted string
|
|
1070
|
+
*/
|
|
1071
|
+
function evalTableToCsv(table, options = { isRedteam: false }) {
|
|
1072
|
+
const { isRedteam } = options;
|
|
1073
|
+
const hasDescriptions = table.body.some((row) => row.test.description);
|
|
1074
|
+
return stringify([buildCsvHeaders(table.head.vars, table.head.prompts, {
|
|
1075
|
+
hasDescriptions,
|
|
1076
|
+
isRedteam
|
|
1077
|
+
}), ...table.body.map((row) => tableRowToCsvValues(row, {
|
|
1078
|
+
hasDescriptions,
|
|
1079
|
+
isRedteam
|
|
1080
|
+
}))]);
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Generate JSON data from evaluation table
|
|
1084
|
+
* @param table Evaluation table data
|
|
1085
|
+
* @returns JSON object
|
|
1086
|
+
*/
|
|
1087
|
+
function evalTableToJson(table) {
|
|
1088
|
+
return table;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Merges comparison tables with the main table for side-by-side CSV export.
|
|
1092
|
+
*
|
|
1093
|
+
* @param mainEvalId - The ID of the main evaluation
|
|
1094
|
+
* @param mainTable - The main evaluation table
|
|
1095
|
+
* @param comparisonData - Array of comparison eval data (eval ID and table)
|
|
1096
|
+
* @returns Merged table with all prompts and outputs combined
|
|
1097
|
+
*/
|
|
1098
|
+
function mergeComparisonTables(mainEvalId, mainTable, comparisonData) {
|
|
1099
|
+
return {
|
|
1100
|
+
head: {
|
|
1101
|
+
prompts: [...mainTable.head.prompts.map((prompt) => ({
|
|
1102
|
+
...prompt,
|
|
1103
|
+
label: `[${mainEvalId}] ${prompt.label || ""}`
|
|
1104
|
+
})), ...comparisonData.flatMap(({ evalId, table }) => table.head.prompts.map((prompt) => ({
|
|
1105
|
+
...prompt,
|
|
1106
|
+
label: `[${evalId}] ${prompt.label || ""}`
|
|
1107
|
+
})))],
|
|
1108
|
+
vars: mainTable.head.vars
|
|
1109
|
+
},
|
|
1110
|
+
body: mainTable.body.map((row) => {
|
|
1111
|
+
const testIdx = row.testIdx;
|
|
1112
|
+
const matchingRows = comparisonData.map(({ table }) => table.body.find((compRow) => compRow.testIdx === testIdx)).filter((r) => r !== void 0);
|
|
1113
|
+
return {
|
|
1114
|
+
...row,
|
|
1115
|
+
outputs: [...row.outputs, ...matchingRows.flatMap((r) => r.outputs)]
|
|
1116
|
+
};
|
|
1117
|
+
})
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* High-level function to generate CSV from an evaluation.
|
|
1122
|
+
*
|
|
1123
|
+
* Used by WebUI for CSV downloads (with or without comparison evals).
|
|
1124
|
+
* For CLI exports, use `streamEvalCsv` which is more memory-efficient
|
|
1125
|
+
* for large datasets.
|
|
1126
|
+
*
|
|
1127
|
+
* Both functions use the same underlying formatting (`evalTableToCsv`,
|
|
1128
|
+
* `buildCsvHeaders`, `tableRowToCsvValues`) to ensure consistent output.
|
|
1129
|
+
*
|
|
1130
|
+
* @param eval_ - The evaluation to export
|
|
1131
|
+
* @param options - Export options including filters and comparison eval IDs
|
|
1132
|
+
* @returns CSV formatted string
|
|
1133
|
+
* @throws ComparisonEvalNotFoundError if a comparison eval ID is not found
|
|
1134
|
+
* @throws Error if comparison exports requested without findEvalById callback
|
|
1135
|
+
*/
|
|
1136
|
+
async function generateEvalCsv(eval_, options = {}) {
|
|
1137
|
+
const UNLIMITED_RESULTS = Number.MAX_SAFE_INTEGER;
|
|
1138
|
+
const mainTable = await eval_.getTablePage({
|
|
1139
|
+
offset: 0,
|
|
1140
|
+
limit: UNLIMITED_RESULTS,
|
|
1141
|
+
filterMode: options.filterMode,
|
|
1142
|
+
searchQuery: options.searchQuery,
|
|
1143
|
+
filters: options.filters
|
|
1144
|
+
});
|
|
1145
|
+
let finalTable = mainTable;
|
|
1146
|
+
if (options.comparisonEvalIds && options.comparisonEvalIds.length > 0) {
|
|
1147
|
+
if (!options.findEvalById) throw new Error("findEvalById callback is required for comparison exports. Pass Eval.findById when calling from server routes.");
|
|
1148
|
+
const indices = mainTable.body.map((row) => row.testIdx);
|
|
1149
|
+
const comparisonData = await Promise.all(options.comparisonEvalIds.map(async (comparisonEvalId) => {
|
|
1150
|
+
const comparisonEval = await options.findEvalById(comparisonEvalId);
|
|
1151
|
+
if (!comparisonEval) throw new ComparisonEvalNotFoundError(comparisonEvalId);
|
|
1152
|
+
const table = await comparisonEval.getTablePage({
|
|
1153
|
+
offset: 0,
|
|
1154
|
+
limit: indices.length,
|
|
1155
|
+
filterMode: "all",
|
|
1156
|
+
testIndices: indices,
|
|
1157
|
+
searchQuery: options.searchQuery,
|
|
1158
|
+
filters: options.filters
|
|
1159
|
+
});
|
|
1160
|
+
return {
|
|
1161
|
+
evalId: comparisonEval.id,
|
|
1162
|
+
table
|
|
1163
|
+
};
|
|
1164
|
+
}));
|
|
1165
|
+
finalTable = mergeComparisonTables(eval_.id, mainTable, comparisonData);
|
|
1166
|
+
}
|
|
1167
|
+
return evalTableToCsv(finalTable, { isRedteam: Boolean(eval_.config.redteam) });
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Stream CSV data from an evaluation in batches.
|
|
1171
|
+
*
|
|
1172
|
+
* This is more memory-efficient for large evaluations as it processes
|
|
1173
|
+
* results in batches rather than loading everything into memory.
|
|
1174
|
+
*
|
|
1175
|
+
* Used by the CLI export (`promptfoo eval -o output.csv`) to maintain
|
|
1176
|
+
* consistent CSV format with WebUI exports while handling large datasets.
|
|
1177
|
+
*
|
|
1178
|
+
* @param eval_ - The evaluation to export
|
|
1179
|
+
* @param options - Streaming options including the write callback
|
|
1180
|
+
*/
|
|
1181
|
+
async function streamEvalCsv(eval_, options) {
|
|
1182
|
+
const { isRedteam = false, write } = options;
|
|
1183
|
+
const varNames = eval_.vars;
|
|
1184
|
+
const prompts = eval_.prompts;
|
|
1185
|
+
const numPrompts = prompts.length;
|
|
1186
|
+
let headersWritten = false;
|
|
1187
|
+
let hasDescriptions = false;
|
|
1188
|
+
let firstBatchBuffer = null;
|
|
1189
|
+
for await (const batchResults of eval_.fetchResultsBatched()) {
|
|
1190
|
+
const rowsByTestIdx = /* @__PURE__ */ new Map();
|
|
1191
|
+
for (const result of batchResults) {
|
|
1192
|
+
if (!rowsByTestIdx.has(result.testIdx)) rowsByTestIdx.set(result.testIdx, {
|
|
1193
|
+
testIdx: result.testIdx,
|
|
1194
|
+
vars: varNames.map((varName) => {
|
|
1195
|
+
const value = result.testCase?.vars?.[varName];
|
|
1196
|
+
return value !== void 0 ? String(value) : "";
|
|
1197
|
+
}),
|
|
1198
|
+
outputs: new Array(numPrompts).fill(null),
|
|
1199
|
+
test: { description: result.testCase?.description }
|
|
1200
|
+
});
|
|
1201
|
+
const row = rowsByTestIdx.get(result.testIdx);
|
|
1202
|
+
row.outputs[result.promptIdx] = {
|
|
1203
|
+
text: result.response?.output ?? "",
|
|
1204
|
+
pass: result.success,
|
|
1205
|
+
score: result.score,
|
|
1206
|
+
namedScores: result.namedScores,
|
|
1207
|
+
failureReason: result.failureReason,
|
|
1208
|
+
gradingResult: result.gradingResult,
|
|
1209
|
+
metadata: result.metadata
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
const rows = Array.from(rowsByTestIdx.values());
|
|
1213
|
+
if (!headersWritten) {
|
|
1214
|
+
hasDescriptions = rows.some((r) => r.test.description);
|
|
1215
|
+
await write(stringify([buildCsvHeaders(varNames, prompts, {
|
|
1216
|
+
hasDescriptions,
|
|
1217
|
+
isRedteam
|
|
1218
|
+
})]));
|
|
1219
|
+
headersWritten = true;
|
|
1220
|
+
if (!hasDescriptions) {
|
|
1221
|
+
firstBatchBuffer = rows;
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (firstBatchBuffer !== null) {
|
|
1226
|
+
if (rows.some((r) => r.test.description) && !hasDescriptions) {}
|
|
1227
|
+
const bufferedCsvRows = firstBatchBuffer.map((row) => tableRowToCsvValues(row, {
|
|
1228
|
+
hasDescriptions,
|
|
1229
|
+
isRedteam
|
|
1230
|
+
}));
|
|
1231
|
+
if (bufferedCsvRows.length > 0) await write(stringify(bufferedCsvRows));
|
|
1232
|
+
firstBatchBuffer = null;
|
|
1233
|
+
}
|
|
1234
|
+
const csvRows = rows.map((row) => tableRowToCsvValues(row, {
|
|
1235
|
+
hasDescriptions,
|
|
1236
|
+
isRedteam
|
|
1237
|
+
}));
|
|
1238
|
+
if (csvRows.length > 0) await write(stringify(csvRows));
|
|
1239
|
+
}
|
|
1240
|
+
if (firstBatchBuffer !== null) {
|
|
1241
|
+
const bufferedCsvRows = firstBatchBuffer.map((row) => tableRowToCsvValues(row, {
|
|
1242
|
+
hasDescriptions,
|
|
1243
|
+
isRedteam
|
|
1244
|
+
}));
|
|
1245
|
+
if (bufferedCsvRows.length > 0) await write(stringify(bufferedCsvRows));
|
|
1246
|
+
}
|
|
1247
|
+
if (!headersWritten) await write(stringify([buildCsvHeaders(varNames, prompts, {
|
|
1248
|
+
hasDescriptions: false,
|
|
1249
|
+
isRedteam
|
|
1250
|
+
})]));
|
|
1251
|
+
}
|
|
1252
|
+
//#endregion
|
|
1253
|
+
//#region src/util/output.ts
|
|
1254
|
+
const outputToSimpleString = (output) => {
|
|
1255
|
+
const passFailText = output.pass ? "[PASS]" : output.failureReason === ResultFailureReason.ASSERT ? "[FAIL]" : "[ERROR]";
|
|
1256
|
+
const namedScoresText = Object.entries(output.namedScores).map(([name, value]) => `${name}: ${value?.toFixed(2)}`).join(", ");
|
|
1257
|
+
const scoreText = namedScoresText.length > 0 ? `(${output.score?.toFixed(2)}, ${namedScoresText})` : `(${output.score?.toFixed(2)})`;
|
|
1258
|
+
const gradingResultText = output.gradingResult ? `${output.pass ? "Pass" : "Fail"} Reason: ${output.gradingResult.reason}` : "";
|
|
1259
|
+
return dedent`
|
|
1260
|
+
${passFailText} ${scoreText}
|
|
1261
|
+
|
|
1262
|
+
${output.text}
|
|
1263
|
+
|
|
1264
|
+
${gradingResultText}
|
|
1265
|
+
`.trim();
|
|
1266
|
+
};
|
|
1267
|
+
function sanitizeConfigForOutput(config) {
|
|
1268
|
+
return sanitizeObject(config, {
|
|
1269
|
+
context: "output config",
|
|
1270
|
+
throwOnError: true,
|
|
1271
|
+
maxDepth: Number.POSITIVE_INFINITY
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
function createOutputMetadata(evalRecord) {
|
|
1275
|
+
let evaluationCreatedAt;
|
|
1276
|
+
if (evalRecord.createdAt) try {
|
|
1277
|
+
const date = new Date(evalRecord.createdAt);
|
|
1278
|
+
evaluationCreatedAt = Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
1279
|
+
} catch {
|
|
1280
|
+
evaluationCreatedAt = void 0;
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
promptfooVersion: VERSION,
|
|
1284
|
+
nodeVersion: process.version,
|
|
1285
|
+
platform: os$1.platform(),
|
|
1286
|
+
arch: os$1.arch(),
|
|
1287
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1288
|
+
evaluationCreatedAt,
|
|
1289
|
+
author: evalRecord.author
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* JSON writer with improved error handling for large datasets.
|
|
1294
|
+
* Provides helpful error messages when memory limits are exceeded.
|
|
1295
|
+
*/
|
|
1296
|
+
async function writeJsonOutputSafely(outputPath, evalRecord, shareableUrl) {
|
|
1297
|
+
const metadata = createOutputMetadata(evalRecord);
|
|
1298
|
+
try {
|
|
1299
|
+
const summary = await evalRecord.toEvaluateSummary();
|
|
1300
|
+
const redactedConfig = sanitizeConfigForOutput(evalRecord.config);
|
|
1301
|
+
const outputData = {
|
|
1302
|
+
evalId: evalRecord.id,
|
|
1303
|
+
results: summary,
|
|
1304
|
+
config: redactedConfig,
|
|
1305
|
+
shareableUrl,
|
|
1306
|
+
metadata
|
|
1307
|
+
};
|
|
1308
|
+
const jsonString = JSON.stringify(outputData, null, 2);
|
|
1309
|
+
await fsPromises.writeFile(outputPath, jsonString);
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
const msg = error?.message ?? "";
|
|
1312
|
+
const isStringLen = error instanceof RangeError && msg.includes("Invalid string length");
|
|
1313
|
+
const isHeapOOM = /heap out of memory|Array buffer allocation failed|ERR_STRING_TOO_LONG/i.test(msg);
|
|
1314
|
+
if (isStringLen || isHeapOOM) {
|
|
1315
|
+
const resultCount = await evalRecord.getResultsCount();
|
|
1316
|
+
logger.error(`Dataset too large for JSON export (${resultCount} results).`);
|
|
1317
|
+
throw new Error(`Dataset too large for JSON export. The evaluation has ${resultCount} results which exceeds memory limits. Consider using JSONL format instead: --output output.jsonl`);
|
|
1318
|
+
} else throw error;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
async function writeOutput(outputPath, evalRecord, shareableUrl) {
|
|
1322
|
+
if (outputPath.match(/^https:\/\/docs\.google\.com\/spreadsheets\//)) {
|
|
1323
|
+
const table = await evalRecord.getTable();
|
|
1324
|
+
invariant(table, "Table is required");
|
|
1325
|
+
const rows = table.body.map((row) => {
|
|
1326
|
+
const csvRow = {};
|
|
1327
|
+
table.head.vars.forEach((varName, index) => {
|
|
1328
|
+
csvRow[varName] = row.vars[index];
|
|
1329
|
+
});
|
|
1330
|
+
table.head.prompts.forEach((prompt, index) => {
|
|
1331
|
+
csvRow[`[${prompt.provider}] ${prompt.label}`] = outputToSimpleString(row.outputs[index]);
|
|
1332
|
+
});
|
|
1333
|
+
return csvRow;
|
|
1334
|
+
});
|
|
1335
|
+
logger.info(`Writing ${rows.length} rows to Google Sheets...`);
|
|
1336
|
+
await writeCsvToGoogleSheet(rows, outputPath);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const { data: outputExtension } = OutputFileExtension.safeParse(path$1.extname(outputPath).slice(1).toLowerCase());
|
|
1340
|
+
invariant(outputExtension, `Unsupported output file format ${outputExtension}. Please use one of: ${OutputFileExtension.options.join(", ")}.`);
|
|
1341
|
+
const outputDir = path$1.dirname(outputPath);
|
|
1342
|
+
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
1343
|
+
const metadata = createOutputMetadata(evalRecord);
|
|
1344
|
+
if (outputExtension === "csv") {
|
|
1345
|
+
const fileHandle = await fsPromises.open(outputPath, "w");
|
|
1346
|
+
try {
|
|
1347
|
+
await streamEvalCsv(evalRecord, {
|
|
1348
|
+
isRedteam: Boolean(evalRecord.config.redteam),
|
|
1349
|
+
write: async (data) => {
|
|
1350
|
+
await fileHandle.write(data);
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
} finally {
|
|
1354
|
+
await fileHandle.close();
|
|
1355
|
+
}
|
|
1356
|
+
} else if (outputExtension === "json") await writeJsonOutputSafely(outputPath, evalRecord, shareableUrl);
|
|
1357
|
+
else if (outputExtension === "yaml" || outputExtension === "yml" || outputExtension === "txt") {
|
|
1358
|
+
const summary = await evalRecord.toEvaluateSummary();
|
|
1359
|
+
const redactedConfig = sanitizeConfigForOutput(evalRecord.config);
|
|
1360
|
+
await fsPromises.writeFile(outputPath, yaml.dump({
|
|
1361
|
+
evalId: evalRecord.id,
|
|
1362
|
+
results: summary,
|
|
1363
|
+
config: redactedConfig,
|
|
1364
|
+
shareableUrl,
|
|
1365
|
+
metadata
|
|
1366
|
+
}));
|
|
1367
|
+
} else if (outputExtension === "html") {
|
|
1368
|
+
const table = await evalRecord.getTable();
|
|
1369
|
+
invariant(table, "Table is required");
|
|
1370
|
+
const summary = await evalRecord.toEvaluateSummary();
|
|
1371
|
+
const redactedConfig = sanitizeConfigForOutput(evalRecord.config);
|
|
1372
|
+
const template = await fsPromises.readFile(path$1.join(getDirectory(), "tableOutput.html"), "utf-8");
|
|
1373
|
+
const htmlTable = [[...table.head.vars, ...table.head.prompts.map((prompt) => `[${prompt.provider}] ${prompt.label}`)], ...table.body.map((row) => [...row.vars, ...row.outputs.map(outputToSimpleString)])];
|
|
1374
|
+
const htmlOutput = getNunjucksEngine().renderString(template, {
|
|
1375
|
+
config: redactedConfig,
|
|
1376
|
+
table: htmlTable,
|
|
1377
|
+
results: summary
|
|
1378
|
+
});
|
|
1379
|
+
await fsPromises.writeFile(outputPath, htmlOutput);
|
|
1380
|
+
} else if (outputExtension === "jsonl") {
|
|
1381
|
+
await fsPromises.writeFile(outputPath, "");
|
|
1382
|
+
for await (const batchResults of evalRecord.fetchResultsBatched()) {
|
|
1383
|
+
const text = batchResults.map((result) => JSON.stringify(result)).join(os$1.EOL) + os$1.EOL;
|
|
1384
|
+
await fsPromises.appendFile(outputPath, text);
|
|
1385
|
+
}
|
|
1386
|
+
} else if (outputExtension === "xml") {
|
|
1387
|
+
const summary = await evalRecord.toEvaluateSummary();
|
|
1388
|
+
const redactedConfig = sanitizeConfigForOutput(evalRecord.config);
|
|
1389
|
+
const sanitizeForXml = (obj) => {
|
|
1390
|
+
if (obj === null || obj === void 0) return "";
|
|
1391
|
+
if (typeof obj === "boolean" || typeof obj === "number") return String(obj);
|
|
1392
|
+
if (typeof obj === "string") return obj;
|
|
1393
|
+
if (Array.isArray(obj)) return obj.map(sanitizeForXml);
|
|
1394
|
+
if (typeof obj === "object") {
|
|
1395
|
+
const sanitized = {};
|
|
1396
|
+
for (const [key, value] of Object.entries(obj)) sanitized[key] = sanitizeForXml(value);
|
|
1397
|
+
return sanitized;
|
|
1398
|
+
}
|
|
1399
|
+
return String(obj);
|
|
1400
|
+
};
|
|
1401
|
+
const xmlData = new XMLBuilder({
|
|
1402
|
+
ignoreAttributes: false,
|
|
1403
|
+
format: true,
|
|
1404
|
+
indentBy: " "
|
|
1405
|
+
}).build({ promptfoo: {
|
|
1406
|
+
evalId: evalRecord.id,
|
|
1407
|
+
results: sanitizeForXml(summary),
|
|
1408
|
+
config: sanitizeForXml(redactedConfig),
|
|
1409
|
+
shareableUrl: shareableUrl || ""
|
|
1410
|
+
} });
|
|
1411
|
+
await fsPromises.writeFile(outputPath, xmlData);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
async function writeMultipleOutputs(outputPaths, evalRecord, shareableUrl) {
|
|
1415
|
+
await Promise.all(outputPaths.map((outputPath) => writeOutput(outputPath, evalRecord, shareableUrl)));
|
|
1416
|
+
}
|
|
1417
|
+
//#endregion
|
|
1418
|
+
//#region src/util/runtime.ts
|
|
1419
|
+
function printBorder() {
|
|
1420
|
+
const border = "=".repeat(TERMINAL_MAX_WIDTH);
|
|
1421
|
+
logger.info(border);
|
|
1422
|
+
}
|
|
1423
|
+
//#endregion
|
|
1424
|
+
export { readFilters as A, getResolvedRelativePath as C, maybeLoadResponseFormatFromExternalFile as D, maybeLoadFromExternalFileWithVars as E, extractVariablesFromTemplates as F, getNunjucksEngine as I, loadFunction as L, renderEnvOnlyInObject as M, renderVarsInObject as N, maybeLoadToolsFromExternalFile as O, extractVariablesFromTemplate as P, parseFileUrl as R, getNunjucksEngineForFilePath as S, maybeLoadFromExternalFile as T, getProviderDescription as _, evalTableToJson as a, isOpenAiProvider as b, fetchCsvFromGoogleSheet as c, extractRuntimeVars as d, filterRuntimeVars as f, doesProviderRefMatch as g, checkProviderApiKeys as h, ComparisonEvalNotFoundError as i, readOutput as j, parsePathOrGlob as k, setupEnv as l, resultIsForTestCase as m, writeMultipleOutputs as n, generateEvalCsv as o, getTestCaseDeduplicationKey as p, writeOutput as r, mergeComparisonTables as s, printBorder as t, deduplicateTestCases as u, isAnthropicProvider as v, maybeLoadConfigFromExternalFile as w, isProviderAllowed as x, isGoogleProvider as y };
|
|
1425
|
+
|
|
1426
|
+
//# sourceMappingURL=util-ZzmqNPlg.js.map
|