@use-lattice/litmus 0.121.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/LICENSE +19 -0
  2. package/dist/src/accounts-Bt1oJb1Z.cjs +219 -0
  3. package/dist/src/accounts-DjOU8Rm3.js +178 -0
  4. package/dist/src/agentic-utils-D03IiXQc.js +153 -0
  5. package/dist/src/agentic-utils-Dh7xaMQM.cjs +180 -0
  6. package/dist/src/agents-C6BIMlZa.js +231 -0
  7. package/dist/src/agents-DvIpNX1L.cjs +666 -0
  8. package/dist/src/agents-ZP0RP9vV.cjs +231 -0
  9. package/dist/src/agents-maJXdjbR.js +665 -0
  10. package/dist/src/aimlapi-BTbQjG2E.cjs +30 -0
  11. package/dist/src/aimlapi-CwMxqfXP.js +30 -0
  12. package/dist/src/audio-BBUdvsde.cjs +97 -0
  13. package/dist/src/audio-D5DPZ7I-.js +97 -0
  14. package/dist/src/base-BEysXrkq.cjs +222 -0
  15. package/dist/src/base-C451JQfq.js +193 -0
  16. package/dist/src/blobs-BY8MDmpo.js +230 -0
  17. package/dist/src/blobs-BgcNn97m.cjs +256 -0
  18. package/dist/src/cache-BBE_lsTA.cjs +4 -0
  19. package/dist/src/cache-BkrqU5Ba.js +237 -0
  20. package/dist/src/cache-DsCxFlsZ.cjs +297 -0
  21. package/dist/src/chat-CPJWDP6a.cjs +289 -0
  22. package/dist/src/chat-CXX3xzkk.cjs +811 -0
  23. package/dist/src/chat-CcDgZFJ4.js +787 -0
  24. package/dist/src/chat-Dz5ZeGO2.js +289 -0
  25. package/dist/src/chatkit-Dw0mKkML.cjs +1158 -0
  26. package/dist/src/chatkit-swAIVuea.js +1157 -0
  27. package/dist/src/chunk-DEq-mXcV.js +15 -0
  28. package/dist/src/claude-agent-sdk-BXZJtOg6.js +379 -0
  29. package/dist/src/claude-agent-sdk-CkfyjDoG.cjs +383 -0
  30. package/dist/src/cloudflare-ai-BzpJcqUH.js +161 -0
  31. package/dist/src/cloudflare-ai-Cmy_R1y2.cjs +161 -0
  32. package/dist/src/cloudflare-gateway-B9tVQKok.cjs +272 -0
  33. package/dist/src/cloudflare-gateway-DrD3ew3H.js +272 -0
  34. package/dist/src/codex-sdk-Dezj9Nwm.js +1056 -0
  35. package/dist/src/codex-sdk-Dl9D4k5B.cjs +1060 -0
  36. package/dist/src/cometapi-C-9YvCHC.js +54 -0
  37. package/dist/src/cometapi-DHgDKoO2.cjs +54 -0
  38. package/dist/src/completion-B8Ctyxpr.js +120 -0
  39. package/dist/src/completion-Cxrt08sj.cjs +131 -0
  40. package/dist/src/createHash-BwgE13yv.cjs +27 -0
  41. package/dist/src/createHash-DmPQkvBh.js +15 -0
  42. package/dist/src/docker-BiqcTwLv.js +80 -0
  43. package/dist/src/docker-C7tEJnP-.cjs +80 -0
  44. package/dist/src/esm-C62Zofr1.cjs +409 -0
  45. package/dist/src/esm-DMVc93eh.js +379 -0
  46. package/dist/src/evalResult-C3NJPQOo.cjs +301 -0
  47. package/dist/src/evalResult-C7JJAPBb.js +295 -0
  48. package/dist/src/evalResult-DoVTZZWI.cjs +2 -0
  49. package/dist/src/extractor-DnMD3fwt.cjs +391 -0
  50. package/dist/src/extractor-DtlL28vL.js +374 -0
  51. package/dist/src/fetch-BTxakTSg.cjs +1133 -0
  52. package/dist/src/fetch-DQckpUFz.js +928 -0
  53. package/dist/src/fileExtensions-DnqA1y9x.js +85 -0
  54. package/dist/src/fileExtensions-bYh77CN8.cjs +114 -0
  55. package/dist/src/genaiTracer-CyZrmaK0.cjs +268 -0
  56. package/dist/src/genaiTracer-D3fD9dNV.js +256 -0
  57. package/dist/src/graders-BNscxFrU.js +13644 -0
  58. package/dist/src/graders-D2oE9Msq.js +2 -0
  59. package/dist/src/graders-c0Ez_w-9.cjs +2 -0
  60. package/dist/src/graders-d0F2M3e9.cjs +14056 -0
  61. package/dist/src/image-0ZhE0VlR.cjs +280 -0
  62. package/dist/src/image-CWE1pdNv.js +257 -0
  63. package/dist/src/image-D9ZK6hwL.js +163 -0
  64. package/dist/src/image-DKZgZITg.cjs +163 -0
  65. package/dist/src/index.cjs +11366 -0
  66. package/dist/src/index.d.cts +19640 -0
  67. package/dist/src/index.d.ts +19641 -0
  68. package/dist/src/index.js +11306 -0
  69. package/dist/src/invariant-Ddh24eXh.js +25 -0
  70. package/dist/src/invariant-kfQ8Bu82.cjs +30 -0
  71. package/dist/src/knowledgeBase-BgPyGFUd.cjs +122 -0
  72. package/dist/src/knowledgeBase-DyHilYaP.js +122 -0
  73. package/dist/src/litellm-CyMeneHS.js +135 -0
  74. package/dist/src/litellm-DWDF73yF.cjs +135 -0
  75. package/dist/src/logger-C40ZGil9.js +717 -0
  76. package/dist/src/logger-DyfK9PBt.cjs +917 -0
  77. package/dist/src/luma-ray-BAU9X_ep.cjs +315 -0
  78. package/dist/src/luma-ray-nwVseBbv.js +313 -0
  79. package/dist/src/messages-B5ADWTTv.js +245 -0
  80. package/dist/src/messages-BCnZfqrS.cjs +257 -0
  81. package/dist/src/meteor-DLZZ3osF.cjs +134 -0
  82. package/dist/src/meteor-DUiCJRC-.js +134 -0
  83. package/dist/src/modelslab-00cveB8L.cjs +163 -0
  84. package/dist/src/modelslab-D9sCU_L7.js +163 -0
  85. package/dist/src/nova-reel-CTapvqYH.js +276 -0
  86. package/dist/src/nova-reel-DlWuuroF.cjs +278 -0
  87. package/dist/src/nova-sonic-5UPWfeMv.cjs +363 -0
  88. package/dist/src/nova-sonic-BhSwQNym.js +363 -0
  89. package/dist/src/openai-BWrJK9d8.cjs +52 -0
  90. package/dist/src/openai-DumO8WQn.js +47 -0
  91. package/dist/src/openclaw-B8brrjC_.cjs +577 -0
  92. package/dist/src/openclaw-Bkayww9q.js +571 -0
  93. package/dist/src/opencode-sdk-7xjoDNiM.cjs +562 -0
  94. package/dist/src/opencode-sdk-SGwAPxht.js +558 -0
  95. package/dist/src/otlpReceiver-CoAHfAN9.cjs +15 -0
  96. package/dist/src/otlpReceiver-oO3EQwI9.js +14 -0
  97. package/dist/src/providerRegistry-4yjhaEM8.js +45 -0
  98. package/dist/src/providerRegistry-DhV4rJIc.cjs +50 -0
  99. package/dist/src/providers-B5RJVG-7.cjs +33609 -0
  100. package/dist/src/providers-BdmZCLzV.js +33262 -0
  101. package/dist/src/providers-CxtRxn8e.js +2 -0
  102. package/dist/src/providers-DnQLNbx1.cjs +3 -0
  103. package/dist/src/pythonUtils-BD0druiM.cjs +275 -0
  104. package/dist/src/pythonUtils-IBhn5YGR.js +249 -0
  105. package/dist/src/quiverai-BDOwZBsM.cjs +213 -0
  106. package/dist/src/quiverai-D3JTF5lD.js +213 -0
  107. package/dist/src/responses-B2LCDCXZ.js +667 -0
  108. package/dist/src/responses-BvNm4Xv9.cjs +685 -0
  109. package/dist/src/rubyUtils-B0NwnfpY.cjs +245 -0
  110. package/dist/src/rubyUtils-BroxzZ7c.cjs +2 -0
  111. package/dist/src/rubyUtils-hqVw5UvJ.js +222 -0
  112. package/dist/src/sagemaker-Cno2V-Sx.js +689 -0
  113. package/dist/src/sagemaker-fV_KUgs5.cjs +691 -0
  114. package/dist/src/server-BOuAXb06.cjs +238 -0
  115. package/dist/src/server-CtI-EWzm.cjs +2 -0
  116. package/dist/src/server-Cy3DZymt.js +189 -0
  117. package/dist/src/slack-CP8xBePa.js +135 -0
  118. package/dist/src/slack-DSQ1yXVb.cjs +135 -0
  119. package/dist/src/store-BwDDaBjb.cjs +246 -0
  120. package/dist/src/store-DcbLC593.cjs +2 -0
  121. package/dist/src/store-IGpqMIkv.js +240 -0
  122. package/dist/src/tables-3Q2cL7So.cjs +373 -0
  123. package/dist/src/tables-Bi2fjr4W.js +288 -0
  124. package/dist/src/telemetry-Bg2WqF79.js +161 -0
  125. package/dist/src/telemetry-D0x6u5kX.cjs +166 -0
  126. package/dist/src/telemetry-DXNimrI0.cjs +2 -0
  127. package/dist/src/text-B_UCRPp2.js +22 -0
  128. package/dist/src/text-CW1cyrwj.cjs +33 -0
  129. package/dist/src/tokenUsageUtils-NYT-WKS6.js +138 -0
  130. package/dist/src/tokenUsageUtils-bVa1ga6f.cjs +173 -0
  131. package/dist/src/transcription-Cl_W16Pr.js +122 -0
  132. package/dist/src/transcription-yt1EecY8.cjs +124 -0
  133. package/dist/src/transform-BCtGrl_W.cjs +228 -0
  134. package/dist/src/transform-Bv6gG2MJ.cjs +1688 -0
  135. package/dist/src/transform-CY1wbpRy.js +1507 -0
  136. package/dist/src/transform-DU8rUL9P.cjs +2 -0
  137. package/dist/src/transform-yWaShiKr.js +216 -0
  138. package/dist/src/transformersAvailability-BGkzavwb.js +35 -0
  139. package/dist/src/transformersAvailability-DKoRtQLy.cjs +35 -0
  140. package/dist/src/types-5aqHpBwE.cjs +3769 -0
  141. package/dist/src/types-Bn6D9c4U.js +3300 -0
  142. package/dist/src/util-BkKlTkI2.js +293 -0
  143. package/dist/src/util-CTh0bfOm.cjs +1119 -0
  144. package/dist/src/util-D17oBwo7.cjs +328 -0
  145. package/dist/src/util-DsS_-v4p.js +613 -0
  146. package/dist/src/util-DuntT1Ga.js +951 -0
  147. package/dist/src/util-aWjdCYMI.cjs +667 -0
  148. package/dist/src/utils-CisQwpjA.js +94 -0
  149. package/dist/src/utils-yWamDvmz.cjs +123 -0
  150. package/dist/tsconfig.tsbuildinfo +1 -0
  151. package/drizzle/0000_lush_hellion.sql +36 -0
  152. package/drizzle/0001_wide_calypso.sql +3 -0
  153. package/drizzle/0002_tidy_juggernaut.sql +1 -0
  154. package/drizzle/0003_lively_naoko.sql +8 -0
  155. package/drizzle/0004_minor_peter_quill.sql +19 -0
  156. package/drizzle/0005_silky_millenium_guard.sql +2 -0
  157. package/drizzle/0006_harsh_caretaker.sql +42 -0
  158. package/drizzle/0007_cloudy_wong.sql +1 -0
  159. package/drizzle/0008_broad_boomer.sql +2 -0
  160. package/drizzle/0009_strong_marten_broadcloak.sql +19 -0
  161. package/drizzle/0010_needy_bishop.sql +11 -0
  162. package/drizzle/0011_moaning_millenium_guard.sql +1 -0
  163. package/drizzle/0012_late_marten_broadcloak.sql +2 -0
  164. package/drizzle/0013_previous_dormammu.sql +9 -0
  165. package/drizzle/0014_lazy_captain_universe.sql +2 -0
  166. package/drizzle/0015_zippy_wallop.sql +29 -0
  167. package/drizzle/0016_jazzy_zemo.sql +2 -0
  168. package/drizzle/0017_reflective_praxagora.sql +4 -0
  169. package/drizzle/0018_fat_vanisher.sql +22 -0
  170. package/drizzle/0019_new_clint_barton.sql +8 -0
  171. package/drizzle/0020_skinny_maverick.sql +1 -0
  172. package/drizzle/0021_mysterious_madelyne_pryor.sql +13 -0
  173. package/drizzle/0022_sleepy_ultimo.sql +25 -0
  174. package/drizzle/0023_wooden_mandrill.sql +2 -0
  175. package/drizzle/AGENTS.md +68 -0
  176. package/drizzle/CLAUDE.md +1 -0
  177. package/drizzle/meta/0000_snapshot.json +221 -0
  178. package/drizzle/meta/0001_snapshot.json +214 -0
  179. package/drizzle/meta/0002_snapshot.json +221 -0
  180. package/drizzle/meta/0005_snapshot.json +369 -0
  181. package/drizzle/meta/0006_snapshot.json +638 -0
  182. package/drizzle/meta/0007_snapshot.json +640 -0
  183. package/drizzle/meta/0008_snapshot.json +649 -0
  184. package/drizzle/meta/0009_snapshot.json +554 -0
  185. package/drizzle/meta/0010_snapshot.json +619 -0
  186. package/drizzle/meta/0011_snapshot.json +627 -0
  187. package/drizzle/meta/0012_snapshot.json +639 -0
  188. package/drizzle/meta/0013_snapshot.json +717 -0
  189. package/drizzle/meta/0014_snapshot.json +717 -0
  190. package/drizzle/meta/0015_snapshot.json +897 -0
  191. package/drizzle/meta/0016_snapshot.json +1031 -0
  192. package/drizzle/meta/0018_snapshot.json +1210 -0
  193. package/drizzle/meta/0019_snapshot.json +1165 -0
  194. package/drizzle/meta/0020_snapshot.json +1232 -0
  195. package/drizzle/meta/0021_snapshot.json +1311 -0
  196. package/drizzle/meta/0022_snapshot.json +1481 -0
  197. package/drizzle/meta/0023_snapshot.json +1496 -0
  198. package/drizzle/meta/_journal.json +174 -0
  199. package/package.json +240 -0
@@ -0,0 +1,1158 @@
1
+ const require_logger = require("./logger-DyfK9PBt.cjs");
2
+ const require_openai = require("./openai-BWrJK9d8.cjs");
3
+ const require_providerRegistry = require("./providerRegistry-DhV4rJIc.cjs");
4
+ let http = require("http");
5
+ http = require_logger.__toESM(http);
6
+ let playwright = require("playwright");
7
+ //#region src/providers/openai/chatkit-pool.ts
8
+ /**
9
+ * ChatKit Browser Pool
10
+ *
11
+ * Manages a pool of browser contexts for concurrent ChatKit evaluations.
12
+ * This significantly reduces resource usage compared to spawning separate
13
+ * browsers for each test.
14
+ *
15
+ * Architecture:
16
+ * - Single browser process (shared across all tests)
17
+ * - Multiple browser contexts (isolated like incognito windows)
18
+ * - Shared HTTP server with per-workflow template routing
19
+ * - Pages are workflow-specific (different workflows get different pages)
20
+ */
21
+ const CHATKIT_READY_TIMEOUT_MS$1 = 6e4;
22
+ const PAGE_REFRESH_TIMEOUT_MS = 6e4;
23
+ const PAGE_ACQUIRE_TIMEOUT_MS = 12e4;
24
+ const IDLE_SHUTDOWN_DELAY_MS = 5e3;
25
+ /**
26
+ * Singleton browser pool for ChatKit evaluations.
27
+ * Supports high concurrency by reusing browser contexts.
28
+ * Each workflow gets its own isolated pages via template routing.
29
+ */
30
+ var ChatKitBrowserPool = class ChatKitBrowserPool {
31
+ static instance = null;
32
+ static cleanupRegistered = false;
33
+ browser = null;
34
+ server = null;
35
+ serverPort = 0;
36
+ pages = [];
37
+ waitQueue = [];
38
+ config;
39
+ templates = /* @__PURE__ */ new Map();
40
+ initialized = false;
41
+ initPromise = null;
42
+ idleTimer = null;
43
+ constructor(config) {
44
+ this.config = config;
45
+ }
46
+ /**
47
+ * Register process exit handlers to clean up browser resources
48
+ */
49
+ static registerCleanupHandlers() {
50
+ if (ChatKitBrowserPool.cleanupRegistered) return;
51
+ ChatKitBrowserPool.cleanupRegistered = true;
52
+ const cleanup = () => {
53
+ if (ChatKitBrowserPool.instance) {
54
+ ChatKitBrowserPool.instance.shutdown().catch(() => {});
55
+ ChatKitBrowserPool.instance = null;
56
+ }
57
+ };
58
+ process.on("beforeExit", () => {
59
+ if (ChatKitBrowserPool.instance) {
60
+ ChatKitBrowserPool.instance.shutdown().catch(() => {});
61
+ ChatKitBrowserPool.instance = null;
62
+ }
63
+ });
64
+ process.on("exit", cleanup);
65
+ }
66
+ /**
67
+ * Get the singleton pool instance
68
+ */
69
+ static getInstance(config) {
70
+ if (!ChatKitBrowserPool.instance) {
71
+ ChatKitBrowserPool.instance = new ChatKitBrowserPool({
72
+ maxConcurrency: config?.maxConcurrency ?? 4,
73
+ headless: config?.headless ?? true,
74
+ serverPort: config?.serverPort ?? 0
75
+ });
76
+ ChatKitBrowserPool.registerCleanupHandlers();
77
+ const instance = ChatKitBrowserPool.instance;
78
+ require_providerRegistry.providerRegistry.register({ async shutdown() {
79
+ if (instance) {
80
+ await instance.shutdown();
81
+ ChatKitBrowserPool.instance = null;
82
+ }
83
+ } });
84
+ } else if (config) {
85
+ const existing = ChatKitBrowserPool.instance.config;
86
+ if (config.maxConcurrency !== void 0 && config.maxConcurrency !== existing.maxConcurrency || config.headless !== void 0 && config.headless !== existing.headless) require_logger.logger.warn("[ChatKitPool] Pool already exists with different config, ignoring new config", {
87
+ existing: {
88
+ maxConcurrency: existing.maxConcurrency,
89
+ headless: existing.headless
90
+ },
91
+ requested: {
92
+ maxConcurrency: config.maxConcurrency,
93
+ headless: config.headless
94
+ }
95
+ });
96
+ }
97
+ return ChatKitBrowserPool.instance;
98
+ }
99
+ /**
100
+ * Reset the singleton (for testing)
101
+ */
102
+ static resetInstance() {
103
+ if (ChatKitBrowserPool.instance) {
104
+ ChatKitBrowserPool.instance.shutdown().catch((err) => {
105
+ require_logger.logger.debug("[ChatKitPool] Error during shutdown:", { error: String(err) });
106
+ });
107
+ ChatKitBrowserPool.instance = null;
108
+ }
109
+ }
110
+ /**
111
+ * Generate a template key from workflow configuration.
112
+ * This ensures different workflows get isolated pages.
113
+ */
114
+ static generateTemplateKey(workflowId, version, userId) {
115
+ return `${workflowId}:${version || "default"}:${userId || "default"}`;
116
+ }
117
+ /**
118
+ * Register a template for a workflow configuration
119
+ */
120
+ setTemplate(templateKey, html) {
121
+ if (this.templates.get(templateKey) !== html) {
122
+ this.templates.set(templateKey, html);
123
+ require_logger.logger.debug("[ChatKitPool] Registered template", { templateKey });
124
+ for (const page of this.pages) if (page.templateKey === templateKey) page.ready = false;
125
+ }
126
+ }
127
+ /**
128
+ * Initialize the pool - launches browser and creates server
129
+ */
130
+ async initialize() {
131
+ if (this.initialized) return;
132
+ if (this.initPromise != null) return this.initPromise;
133
+ this.initPromise = this.doInitialize();
134
+ await this.initPromise;
135
+ this.initPromise = null;
136
+ }
137
+ async doInitialize() {
138
+ require_logger.logger.debug("[ChatKitPool] Initializing browser pool", { maxConcurrency: this.config.maxConcurrency });
139
+ this.server = http.createServer((req, res) => {
140
+ const pathParts = new URL(req.url || "/", `http://localhost`).pathname.split("/").filter(Boolean);
141
+ if (pathParts[0] === "template" && pathParts[1]) {
142
+ const templateKey = decodeURIComponent(pathParts[1]);
143
+ const template = this.templates.get(templateKey);
144
+ if (template) {
145
+ res.writeHead(200, { "Content-Type": "text/html" });
146
+ res.end(template);
147
+ return;
148
+ }
149
+ }
150
+ res.writeHead(404, { "Content-Type": "text/plain" });
151
+ res.end("Template not found");
152
+ });
153
+ await new Promise((resolve, reject) => {
154
+ this.server.once("error", (err) => {
155
+ reject(/* @__PURE__ */ new Error(`Failed to start ChatKit pool server: ${err.message}`));
156
+ });
157
+ this.server.listen(this.config.serverPort, () => {
158
+ const address = this.server.address();
159
+ this.serverPort = typeof address === "object" ? address?.port || 0 : 0;
160
+ require_logger.logger.debug("[ChatKitPool] Server started", { port: this.serverPort });
161
+ resolve();
162
+ });
163
+ });
164
+ try {
165
+ this.browser = await playwright.chromium.launch({ headless: this.config.headless });
166
+ } catch (error) {
167
+ if ((error instanceof Error ? error.message : String(error)).includes("Executable doesn't exist")) throw new Error("Playwright browser not installed. Run: npx playwright install chromium");
168
+ throw error;
169
+ }
170
+ this.initialized = true;
171
+ require_logger.logger.debug("[ChatKitPool] Browser pool initialized");
172
+ }
173
+ /**
174
+ * Acquire a page from the pool for a specific template.
175
+ * Only returns pages configured for the requested template.
176
+ * Blocks if all pages are in use.
177
+ */
178
+ async acquirePage(templateKey) {
179
+ this.cancelIdleTimer();
180
+ await this.initialize();
181
+ if (!this.templates.has(templateKey)) throw new Error(`Template not registered: ${templateKey}. Call setTemplate first.`);
182
+ const available = this.pages.find((p) => !p.inUse && p.ready && p.templateKey === templateKey);
183
+ if (available) {
184
+ available.inUse = true;
185
+ require_logger.logger.debug("[ChatKitPool] Acquired existing page", {
186
+ templateKey,
187
+ poolSize: this.pages.length
188
+ });
189
+ return available;
190
+ }
191
+ const needsRefresh = this.pages.find((p) => !p.inUse && !p.ready && p.templateKey === templateKey);
192
+ if (needsRefresh) {
193
+ await this.refreshPooledPage(needsRefresh);
194
+ needsRefresh.inUse = true;
195
+ require_logger.logger.debug("[ChatKitPool] Acquired and refreshed page", {
196
+ templateKey,
197
+ poolSize: this.pages.length
198
+ });
199
+ return needsRefresh;
200
+ }
201
+ if (this.pages.length < this.config.maxConcurrency) {
202
+ const pooledPage = await this.createPooledPage(templateKey);
203
+ pooledPage.inUse = true;
204
+ this.pages.push(pooledPage);
205
+ require_logger.logger.debug("[ChatKitPool] Created new page", {
206
+ templateKey,
207
+ poolSize: this.pages.length
208
+ });
209
+ return pooledPage;
210
+ }
211
+ require_logger.logger.debug("[ChatKitPool] Waiting for available page", {
212
+ templateKey,
213
+ poolSize: this.pages.length,
214
+ waiting: this.waitQueue.length + 1
215
+ });
216
+ return new Promise((resolve, reject) => {
217
+ const timeoutId = setTimeout(() => {
218
+ const index = this.waitQueue.findIndex((w) => w.resolve === wrappedResolve);
219
+ if (index >= 0) this.waitQueue.splice(index, 1);
220
+ reject(/* @__PURE__ */ new Error(`Timeout waiting for available page after ${PAGE_ACQUIRE_TIMEOUT_MS}ms. Pool has ${this.pages.length} pages, ${this.pages.filter((p) => p.inUse).length} in use.`));
221
+ }, PAGE_ACQUIRE_TIMEOUT_MS);
222
+ const wrappedResolve = (page) => {
223
+ clearTimeout(timeoutId);
224
+ resolve(page);
225
+ };
226
+ this.waitQueue.push({
227
+ templateKey,
228
+ resolve: wrappedResolve
229
+ });
230
+ });
231
+ }
232
+ /**
233
+ * Release a page back to the pool
234
+ */
235
+ async releasePage(pooledPage) {
236
+ const originalTemplateKey = pooledPage.templateKey;
237
+ try {
238
+ await this.refreshPooledPage(pooledPage);
239
+ } catch (error) {
240
+ require_logger.logger.warn("[ChatKitPool] Failed to reset page, recreating", { error });
241
+ const index = this.pages.indexOf(pooledPage);
242
+ if (index >= 0) this.pages.splice(index, 1);
243
+ try {
244
+ await pooledPage.context.close();
245
+ } catch {}
246
+ try {
247
+ const newPage = await this.createPooledPage(originalTemplateKey);
248
+ this.pages.push(newPage);
249
+ pooledPage = newPage;
250
+ } catch (createError) {
251
+ require_logger.logger.warn("[ChatKitPool] Failed to create replacement page", { error: createError });
252
+ await this.tryServeWaiters();
253
+ this.scheduleIdleShutdown();
254
+ return;
255
+ }
256
+ }
257
+ const waiterIndex = this.waitQueue.findIndex((w) => w.templateKey === pooledPage.templateKey);
258
+ if (waiterIndex >= 0) {
259
+ const waiter = this.waitQueue.splice(waiterIndex, 1)[0];
260
+ pooledPage.inUse = true;
261
+ waiter.resolve(pooledPage);
262
+ this.cancelIdleTimer();
263
+ } else {
264
+ pooledPage.inUse = false;
265
+ await this.tryServeWaiters();
266
+ this.scheduleIdleShutdown();
267
+ }
268
+ }
269
+ /**
270
+ * Try to serve waiting requests by creating new pages if we have capacity
271
+ */
272
+ async tryServeWaiters() {
273
+ while (this.waitQueue.length > 0 && this.pages.length < this.config.maxConcurrency) {
274
+ const waiter = this.waitQueue.shift();
275
+ if (!waiter) break;
276
+ try {
277
+ const newPage = await this.createPooledPage(waiter.templateKey);
278
+ newPage.inUse = true;
279
+ this.pages.push(newPage);
280
+ waiter.resolve(newPage);
281
+ require_logger.logger.debug("[ChatKitPool] Created page for waiting request", {
282
+ templateKey: waiter.templateKey,
283
+ poolSize: this.pages.length,
284
+ remainingWaiters: this.waitQueue.length
285
+ });
286
+ } catch (error) {
287
+ require_logger.logger.warn("[ChatKitPool] Failed to create page for waiter", {
288
+ templateKey: waiter.templateKey,
289
+ error
290
+ });
291
+ this.waitQueue.unshift(waiter);
292
+ break;
293
+ }
294
+ }
295
+ }
296
+ /**
297
+ * Schedule automatic shutdown if pool remains idle
298
+ */
299
+ scheduleIdleShutdown() {
300
+ this.cancelIdleTimer();
301
+ if (this.pages.filter((p) => p.inUse).length === 0 && this.waitQueue.length === 0 && this.pages.length > 0) {
302
+ require_logger.logger.debug("[ChatKitPool] Pool idle, scheduling shutdown", { delay: IDLE_SHUTDOWN_DELAY_MS });
303
+ this.idleTimer = setTimeout(() => {
304
+ if (this.pages.filter((p) => p.inUse).length === 0 && this.waitQueue.length === 0) {
305
+ require_logger.logger.debug("[ChatKitPool] Auto-shutting down idle pool");
306
+ this.shutdown().catch((err) => {
307
+ require_logger.logger.debug("[ChatKitPool] Error during idle shutdown", { error: String(err) });
308
+ });
309
+ ChatKitBrowserPool.instance = null;
310
+ }
311
+ }, IDLE_SHUTDOWN_DELAY_MS);
312
+ if (this.idleTimer.unref) this.idleTimer.unref();
313
+ }
314
+ }
315
+ /**
316
+ * Cancel scheduled idle shutdown
317
+ */
318
+ cancelIdleTimer() {
319
+ if (this.idleTimer) {
320
+ clearTimeout(this.idleTimer);
321
+ this.idleTimer = null;
322
+ }
323
+ }
324
+ /**
325
+ * Create a new pooled page with ChatKit initialized for a specific template
326
+ */
327
+ async createPooledPage(templateKey) {
328
+ if (!this.browser) throw new Error("Browser not initialized");
329
+ const context = await this.browser.newContext({ viewport: {
330
+ width: 800,
331
+ height: 600
332
+ } });
333
+ context.setDefaultTimeout(12e4);
334
+ try {
335
+ const page = await context.newPage();
336
+ const templateUrl = `http://localhost:${this.serverPort}/template/${encodeURIComponent(templateKey)}`;
337
+ await page.goto(templateUrl, { waitUntil: "domcontentloaded" });
338
+ await page.waitForFunction(() => window.__state?.ready === true, { timeout: CHATKIT_READY_TIMEOUT_MS$1 });
339
+ return {
340
+ context,
341
+ page,
342
+ ready: true,
343
+ inUse: false,
344
+ templateKey
345
+ };
346
+ } catch (error) {
347
+ try {
348
+ await context.close();
349
+ } catch {}
350
+ throw error;
351
+ }
352
+ }
353
+ async refreshPooledPage(pooledPage) {
354
+ require_logger.logger.debug("[ChatKitPool] Refreshing page", { timeout: PAGE_REFRESH_TIMEOUT_MS });
355
+ await pooledPage.page.reload({ waitUntil: "domcontentloaded" });
356
+ await pooledPage.page.waitForFunction(() => window.__state?.ready === true, { timeout: PAGE_REFRESH_TIMEOUT_MS });
357
+ pooledPage.ready = true;
358
+ }
359
+ /**
360
+ * Get pool statistics
361
+ */
362
+ getStats() {
363
+ return {
364
+ total: this.pages.length,
365
+ inUse: this.pages.filter((p) => p.inUse).length,
366
+ waiting: this.waitQueue.length,
367
+ templates: this.templates.size
368
+ };
369
+ }
370
+ /**
371
+ * Shutdown the pool and release all resources
372
+ */
373
+ async shutdown() {
374
+ require_logger.logger.debug("[ChatKitPool] Shutting down");
375
+ this.cancelIdleTimer();
376
+ if (this.waitQueue.length > 0) {
377
+ require_logger.logger.debug("[ChatKitPool] Clearing pending waiters", { count: this.waitQueue.length });
378
+ this.waitQueue = [];
379
+ }
380
+ for (const pooledPage of this.pages) try {
381
+ await pooledPage.context.close();
382
+ } catch {}
383
+ this.pages = [];
384
+ if (this.browser) {
385
+ try {
386
+ await this.browser.close();
387
+ } catch {}
388
+ this.browser = null;
389
+ }
390
+ if (this.server) {
391
+ this.server.close();
392
+ this.server = null;
393
+ }
394
+ this.initialized = false;
395
+ this.templates.clear();
396
+ require_logger.logger.debug("[ChatKitPool] Shutdown complete");
397
+ }
398
+ };
399
+ //#endregion
400
+ //#region src/providers/openai/chatkit.ts
401
+ /**
402
+ * OpenAI ChatKit Provider
403
+ *
404
+ * Evaluates ChatKit workflows deployed via Agent Builder using Playwright
405
+ * to interact with the ChatKit web component.
406
+ *
407
+ * ChatKit workflows created in OpenAI's Agent Builder don't expose a direct
408
+ * REST API for sending messages. Instead, they require interaction through
409
+ * the ChatKit web component, which this provider automates using Playwright.
410
+ *
411
+ * Prerequisites:
412
+ * - Playwright installed: npm install playwright && npx playwright install chromium
413
+ * - OPENAI_API_KEY environment variable set
414
+ *
415
+ * Usage:
416
+ * providers:
417
+ * - id: openai:chatkit:wf_68ffb83dbfc88190a38103c2bb9f421003f913035dbdb131
418
+ * config:
419
+ * version: '3' # Optional: workflow version
420
+ * timeout: 120000 # Optional: response timeout in ms (default: 120000)
421
+ * headless: true # Optional: run browser headless (default: true)
422
+ *
423
+ * Performance Notes:
424
+ * - Each evaluation spawns a browser instance, so it's slower than REST APIs
425
+ * - For reliable results, use --max-concurrency 1 to avoid resource contention
426
+ * - First test may be slower due to browser launch and ChatKit initialization
427
+ *
428
+ * Troubleshooting:
429
+ * - "Playwright not found": Run `npx playwright install chromium`
430
+ * - Timeout errors: Increase timeout config or use --max-concurrency 1
431
+ * - Empty responses: The workflow may not generate text for some inputs
432
+ */
433
+ const DEFAULT_TIMEOUT_MS = 12e4;
434
+ const DEFAULT_MAX_APPROVALS = 5;
435
+ const DEFAULT_POOL_SIZE = 4;
436
+ const CHATKIT_READY_TIMEOUT_MS = 6e4;
437
+ const DOM_SETTLE_DELAY_MS = 2e3;
438
+ const APPROVAL_PROCESS_DELAY_MS = 500;
439
+ const APPROVAL_CLICK_DELAY_MS = 1e3;
440
+ const RESPONSE_EXTRACT_RETRY_DELAY_MS = 500;
441
+ const CONTENT_STABILIZATION_MS = 1e4;
442
+ const CONTENT_POLL_MS = 500;
443
+ const MIN_WORKFLOW_WAIT_MS = 6e4;
444
+ const SHORT_RESPONSE_THRESHOLD = 100;
445
+ /**
446
+ * Check if a URL is from OpenAI's CDN by parsing the hostname.
447
+ * This is more secure than substring matching which could be bypassed.
448
+ */
449
+ function isOpenAICdnUrl(url) {
450
+ try {
451
+ return new URL(url).hostname === "cdn.platform.openai.com";
452
+ } catch {
453
+ return false;
454
+ }
455
+ }
456
+ /**
457
+ * Validate workflowId format to prevent script injection
458
+ */
459
+ function validateWorkflowId(workflowId) {
460
+ if (!workflowId || !/^wf_[a-zA-Z0-9]+$/.test(workflowId)) throw new Error(`Invalid workflowId format: ${workflowId}. Expected format: wf_<alphanumeric>`);
461
+ }
462
+ /**
463
+ * Validate version format to prevent script injection
464
+ */
465
+ function validateVersion(version) {
466
+ if (!/^[a-zA-Z0-9._-]+$/.test(version)) throw new Error(`Invalid version format: ${version}. Only alphanumeric, dot, dash, and underscore allowed.`);
467
+ }
468
+ /**
469
+ * Validate userId format to prevent script injection
470
+ */
471
+ function validateUserId(userId) {
472
+ if (!/^[a-zA-Z0-9._@-]+$/.test(userId)) throw new Error(`Invalid userId format: ${userId}. Only alphanumeric, dot, dash, underscore, and @ allowed.`);
473
+ }
474
+ /**
475
+ * Clean up assistant response text by removing noise and artifacts.
476
+ * This includes Cloudflare scripts, approval UI text, user echo, and JSON classification prefixes.
477
+ */
478
+ function cleanAssistantResponse(text) {
479
+ if (!text) return "";
480
+ let cleaned = text.replace(/\(function\(\)\{.*?\}\)\(\);?/gs, "").trim();
481
+ cleaned = cleaned.replace(/\n?Approval required\n?Does this work for you\?\n?Approve\n?Reject$/gi, "").replace(/\n?Approval required[\s\n]+Does this work for you\?[\s\n]+Approve[\s\n]+Reject$/gi, "").trim();
482
+ if (/^You said:/i.test(cleaned)) cleaned = "";
483
+ else cleaned = cleaned.replace(/You said:[\s\S]*/gi, "").trim();
484
+ const jsonMatch = cleaned.match(/^(\{[^}]+\})\s+(.+)/s);
485
+ if (jsonMatch && jsonMatch[2].trim().length > 50) cleaned = jsonMatch[2].trim();
486
+ return cleaned;
487
+ }
488
+ /**
489
+ * Generate the HTML page that hosts the ChatKit component
490
+ */
491
+ function generateChatKitHTML(apiKey, workflowId, version, userId) {
492
+ validateWorkflowId(workflowId);
493
+ if (version) validateVersion(version);
494
+ if (!userId) throw new Error("userId is required for ChatKit HTML generation");
495
+ validateUserId(userId);
496
+ return `<!DOCTYPE html>
497
+ <html>
498
+ <head>
499
+ <meta charset="utf-8">
500
+ <title>ChatKit Eval</title>
501
+ </head>
502
+ <body>
503
+ <openai-chatkit id="chatkit"></openai-chatkit>
504
+
505
+ <script src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js"><\/script>
506
+
507
+ <script>
508
+ window.__state = { ready: false, responses: [], threadId: null, error: null, responding: false };
509
+
510
+ async function init() {
511
+ const chatkit = document.getElementById('chatkit');
512
+
513
+ // Wait for element to be ready
514
+ let attempts = 0;
515
+ while (typeof chatkit.setOptions !== 'function' && attempts < 100) {
516
+ await new Promise(r => setTimeout(r, 100));
517
+ attempts++;
518
+ }
519
+
520
+ if (typeof chatkit.setOptions !== 'function') {
521
+ window.__state.error = 'ChatKit component failed to initialize';
522
+ return;
523
+ }
524
+
525
+ let cachedSecret = null;
526
+
527
+ chatkit.setOptions({
528
+ api: {
529
+ getClientSecret: async (existing) => {
530
+ if (existing) return existing;
531
+ if (cachedSecret) return cachedSecret;
532
+
533
+ const res = await fetch('https://api.openai.com/v1/chatkit/sessions', {
534
+ method: 'POST',
535
+ headers: {
536
+ 'Authorization': 'Bearer ${apiKey}',
537
+ 'Content-Type': 'application/json',
538
+ 'OpenAI-Beta': 'chatkit_beta=v1'
539
+ },
540
+ body: JSON.stringify({
541
+ workflow: { id: '${workflowId}'${version ? `, version: '${version}'` : ""} },
542
+ user: '${userId}'
543
+ })
544
+ });
545
+
546
+ if (!res.ok) {
547
+ const text = await res.text();
548
+ throw new Error('Session failed: ' + res.status + ' ' + text);
549
+ }
550
+
551
+ const data = await res.json();
552
+ cachedSecret = data.client_secret;
553
+ return cachedSecret;
554
+ }
555
+ },
556
+ header: { enabled: false },
557
+ history: { enabled: false },
558
+ });
559
+
560
+ chatkit.addEventListener('chatkit.ready', () => {
561
+ window.__state.ready = true;
562
+ });
563
+
564
+ chatkit.addEventListener('chatkit.error', (e) => {
565
+ window.__state.error = e.detail.error?.message || 'Unknown error';
566
+ });
567
+
568
+ chatkit.addEventListener('chatkit.thread.change', (e) => {
569
+ window.__state.threadId = e.detail.threadId;
570
+ });
571
+
572
+ chatkit.addEventListener('chatkit.response.start', () => {
573
+ window.__state.responding = true;
574
+ });
575
+
576
+ chatkit.addEventListener('chatkit.response.end', () => {
577
+ window.__state.responding = false;
578
+ window.__state.responses.push({ timestamp: Date.now() });
579
+ });
580
+
581
+ window.__chatkit = chatkit;
582
+ }
583
+
584
+ init().catch(e => {
585
+ window.__state.error = e.message;
586
+ });
587
+ <\/script>
588
+ </body>
589
+ </html>`;
590
+ }
591
+ /**
592
+ * Extract assistant response text from the ChatKit iframe
593
+ * Uses retry logic since DOM may still be updating after response.end event
594
+ */
595
+ async function extractResponseFromFrame(page, maxRetries = 3) {
596
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
597
+ const frames = page.frames();
598
+ for (const frame of frames) {
599
+ const url = frame.url();
600
+ if (isOpenAICdnUrl(url)) try {
601
+ const result = await frame.evaluate(() => {
602
+ const isUserMessage = (el) => {
603
+ const className = el.className?.toString().toLowerCase() || "";
604
+ const role = el.getAttribute("data-role") || "";
605
+ const testId = el.getAttribute("data-testid") || "";
606
+ return className.includes("user") || role === "user" || testId.includes("user");
607
+ };
608
+ const isAssistantMessage = (el) => {
609
+ const className = el.className?.toString().toLowerCase() || "";
610
+ const role = el.getAttribute("data-role") || "";
611
+ const testId = el.getAttribute("data-testid") || "";
612
+ return className.includes("assistant") || role === "assistant" || testId.includes("assistant");
613
+ };
614
+ for (const sel of [
615
+ "[data-thread-item=\"assistant-message\"]",
616
+ "[data-testid=\"assistant-message\"]",
617
+ "[data-role=\"assistant\"]",
618
+ "[class*=\"assistant\"]:not([class*=\"user\"])"
619
+ ]) {
620
+ const els = document.querySelectorAll(sel);
621
+ if (els.length > 0) {
622
+ const text = els[els.length - 1].textContent?.trim() || "";
623
+ if (text.length > 0) return {
624
+ text,
625
+ source: sel,
626
+ isAssistant: true
627
+ };
628
+ }
629
+ }
630
+ const allMessages = document.querySelectorAll("[class*=\"message\"]");
631
+ const messages = [];
632
+ allMessages.forEach((msg) => {
633
+ const text = msg.textContent?.trim() || "";
634
+ if (text.length > 0) messages.push({
635
+ text,
636
+ isUser: isUserMessage(msg),
637
+ isAssistant: isAssistantMessage(msg)
638
+ });
639
+ });
640
+ for (let i = messages.length - 1; i >= 0; i--) if (!messages[i].isUser && messages[i].text.length > 0) return {
641
+ text: messages[i].text,
642
+ source: "last-non-user",
643
+ isAssistant: true
644
+ };
645
+ const markdown = document.querySelectorAll(".markdown, [class*=\"markdown\"]");
646
+ if (markdown.length > 0) for (let i = markdown.length - 1; i >= 0; i--) {
647
+ const el = markdown[i];
648
+ let parent = el.parentElement;
649
+ let inUserArea = false;
650
+ while (parent && parent !== document.body) {
651
+ if (isUserMessage(parent)) {
652
+ inUserArea = true;
653
+ break;
654
+ }
655
+ parent = parent.parentElement;
656
+ }
657
+ if (!inUserArea) {
658
+ const text = el.textContent?.trim() || "";
659
+ if (text.length > 0) return {
660
+ text,
661
+ source: "markdown",
662
+ isAssistant: true
663
+ };
664
+ }
665
+ }
666
+ const responseContainers = document.querySelectorAll("[class*=\"response\"], [class*=\"reply\"], [class*=\"answer\"]");
667
+ for (let i = responseContainers.length - 1; i >= 0; i--) {
668
+ const container = responseContainers[i];
669
+ if (!isUserMessage(container)) {
670
+ const text = container.textContent?.trim() || "";
671
+ if (text.length > 0) return {
672
+ text,
673
+ source: "response-container",
674
+ isAssistant: true
675
+ };
676
+ }
677
+ }
678
+ const divs = Array.from(document.querySelectorAll("div"));
679
+ const candidateDivs = [];
680
+ for (const div of divs) {
681
+ const text = div.textContent?.trim() || "";
682
+ if (text.length > 0 && text.length < 5e3 && !isUserMessage(div)) {
683
+ let parent = div.parentElement;
684
+ let inUserArea = false;
685
+ while (parent && parent !== document.body) {
686
+ if (isUserMessage(parent)) {
687
+ inUserArea = true;
688
+ break;
689
+ }
690
+ parent = parent.parentElement;
691
+ }
692
+ if (!inUserArea) candidateDivs.push({
693
+ text,
694
+ el: div
695
+ });
696
+ }
697
+ }
698
+ if (candidateDivs.length > 0) {
699
+ const leafDivs = candidateDivs.filter((d) => d.el.querySelectorAll("[class*=\"message\"]").length === 0);
700
+ if (leafDivs.length > 0) return {
701
+ text: leafDivs[leafDivs.length - 1].text,
702
+ source: "leaf-div"
703
+ };
704
+ return {
705
+ text: candidateDivs[candidateDivs.length - 1].text,
706
+ source: "fallback-div"
707
+ };
708
+ }
709
+ return {
710
+ text: document.body?.textContent?.trim() || "",
711
+ source: "body"
712
+ };
713
+ });
714
+ if (result.text && result.text.length > 0) {
715
+ const trimmed = result.text.trim();
716
+ if (trimmed === "ApproveReject" || trimmed === "Approve" || trimmed === "Reject") {
717
+ require_logger.logger.debug("[ChatKitProvider] Skipping approval button text", { text: trimmed });
718
+ continue;
719
+ }
720
+ const cleaned = cleanAssistantResponse(result.text);
721
+ if (cleaned.length > 0) {
722
+ require_logger.logger.debug("[ChatKitProvider] Extracted response", {
723
+ source: result.source,
724
+ length: cleaned.length,
725
+ preview: cleaned.substring(0, 100)
726
+ });
727
+ return cleaned;
728
+ }
729
+ require_logger.logger.debug("[ChatKitProvider] No assistant content found after cleaning", {
730
+ originalLength: result.text.length,
731
+ source: result.source
732
+ });
733
+ }
734
+ } catch (e) {
735
+ require_logger.logger.debug("[ChatKitProvider] Could not access frame", {
736
+ url,
737
+ error: e,
738
+ attempt
739
+ });
740
+ }
741
+ }
742
+ if (attempt < maxRetries - 1) await page.waitForTimeout(RESPONSE_EXTRACT_RETRY_DELAY_MS);
743
+ }
744
+ return "";
745
+ }
746
+ /**
747
+ * Get the current visible text content from the ChatKit iframe.
748
+ * Returns the text content or null if iframe not accessible.
749
+ */
750
+ async function getIframeContent(page) {
751
+ const frames = page.frames();
752
+ require_logger.logger.debug("[ChatKitProvider] Checking frames", {
753
+ frameCount: frames.length,
754
+ frameUrls: frames.map((f) => f.url())
755
+ });
756
+ for (const frame of frames) if (isOpenAICdnUrl(frame.url())) try {
757
+ return await frame.evaluate(() => {
758
+ return document.body?.innerText || "";
759
+ });
760
+ } catch {}
761
+ return null;
762
+ }
763
+ async function waitForContentStabilization(page, timeout, startTime) {
764
+ let lastContent = "";
765
+ let lastChangeTime = Date.now();
766
+ const pollStartTime = Date.now();
767
+ let capturedAssistantResponse = "";
768
+ require_logger.logger.debug("[ChatKitProvider] Starting content stabilization polling");
769
+ while (Date.now() - pollStartTime < timeout) {
770
+ const state = await page.evaluate(() => window.__state);
771
+ const pollElapsed = Date.now() - pollStartTime;
772
+ if (pollElapsed % 5e3 < CONTENT_POLL_MS) require_logger.logger.debug("[ChatKitProvider] Polling state", {
773
+ pollElapsedMs: pollElapsed,
774
+ responding: state.responding,
775
+ responseCount: state.responses?.length,
776
+ error: state.error,
777
+ threadId: state.threadId
778
+ });
779
+ const currentContent = await getIframeContent(page) || "";
780
+ if (currentContent !== lastContent) {
781
+ require_logger.logger.debug("[ChatKitProvider] Content changed", {
782
+ previousLength: lastContent.length,
783
+ newLength: currentContent.length,
784
+ preview: currentContent.substring(Math.max(0, currentContent.length - 200))
785
+ });
786
+ lastContent = currentContent;
787
+ lastChangeTime = Date.now();
788
+ }
789
+ const timeSinceStart = Date.now() - startTime;
790
+ const timeSinceLastChange = Date.now() - lastChangeTime;
791
+ const assistantMatch = currentContent.match(/The assistant said:\s*\n*([\s\S]*)/i);
792
+ const assistantResponse = assistantMatch ? assistantMatch[1].trim() : currentContent;
793
+ if (!assistantMatch && currentContent.length > 0) require_logger.logger.debug("[ChatKitProvider] Assistant pattern not found, using full content for length check", { contentLength: currentContent.length });
794
+ capturedAssistantResponse = assistantResponse;
795
+ const isShortResponse = assistantResponse.length < SHORT_RESPONSE_THRESHOLD;
796
+ const effectiveStabilizationMs = isShortResponse ? CONTENT_STABILIZATION_MS * 2 : CONTENT_STABILIZATION_MS;
797
+ const effectiveMinWaitMs = isShortResponse ? MIN_WORKFLOW_WAIT_MS * 2 : MIN_WORKFLOW_WAIT_MS;
798
+ if (!state.responding && timeSinceLastChange >= effectiveStabilizationMs && timeSinceStart >= effectiveMinWaitMs) {
799
+ require_logger.logger.debug("[ChatKitProvider] Content stabilized", {
800
+ timeSinceStart,
801
+ timeSinceLastChange,
802
+ contentLength: currentContent.length,
803
+ assistantResponseLength: assistantResponse.length,
804
+ isShortResponse,
805
+ responseCount: state.responses?.length
806
+ });
807
+ return {
808
+ assistantResponse: capturedAssistantResponse,
809
+ fullContent: currentContent
810
+ };
811
+ }
812
+ await page.waitForTimeout(CONTENT_POLL_MS);
813
+ }
814
+ require_logger.logger.debug("[ChatKitProvider] Content stabilization timeout reached");
815
+ return {
816
+ assistantResponse: capturedAssistantResponse,
817
+ fullContent: lastContent
818
+ };
819
+ }
820
+ /**
821
+ * Handle workflow approval steps by clicking approve/reject buttons.
822
+ * Returns true if an approval was handled, false if no approval found.
823
+ */
824
+ async function handleApproval(page, action) {
825
+ const frames = page.frames();
826
+ for (const frame of frames) if (isOpenAICdnUrl(frame.url())) try {
827
+ const buttonText = action === "auto-approve" ? "Approve" : "Reject";
828
+ const buttonSelectors = [
829
+ `button:has-text("${buttonText}")`,
830
+ `[role="button"]:has-text("${buttonText}")`,
831
+ `[data-testid="${buttonText.toLowerCase()}-button"]`
832
+ ];
833
+ for (const selector of buttonSelectors) {
834
+ const button = await frame.$(selector);
835
+ if (button) {
836
+ if (await button.isVisible()) {
837
+ require_logger.logger.debug("[ChatKitProvider] Found approval button, clicking", {
838
+ action,
839
+ selector
840
+ });
841
+ await button.click();
842
+ await page.waitForTimeout(APPROVAL_CLICK_DELAY_MS);
843
+ return true;
844
+ }
845
+ }
846
+ }
847
+ if (await frame.evaluate((btnText) => {
848
+ const approveBtn = Array.from(document.querySelectorAll("button, [role=\"button\"]")).find((b) => b.textContent?.toLowerCase().includes(btnText.toLowerCase()));
849
+ if (approveBtn && approveBtn instanceof HTMLElement) {
850
+ approveBtn.click();
851
+ return true;
852
+ }
853
+ return false;
854
+ }, buttonText)) {
855
+ require_logger.logger.debug("[ChatKitProvider] Clicked approval button via evaluate", { action });
856
+ await page.waitForTimeout(APPROVAL_CLICK_DELAY_MS);
857
+ return true;
858
+ }
859
+ } catch (e) {
860
+ require_logger.logger.debug("[ChatKitProvider] Error checking for approval buttons", { error: e });
861
+ }
862
+ return false;
863
+ }
864
+ /**
865
+ * Process approvals until none remain or max reached.
866
+ * Returns the number of approvals processed.
867
+ */
868
+ async function processApprovals(page, approvalHandling, maxApprovals, timeout) {
869
+ if (approvalHandling === "skip") return 0;
870
+ let approvalCount = 0;
871
+ while (approvalCount < maxApprovals) {
872
+ await page.waitForTimeout(APPROVAL_PROCESS_DELAY_MS);
873
+ if (!await handleApproval(page, approvalHandling)) break;
874
+ approvalCount++;
875
+ require_logger.logger.debug("[ChatKitProvider] Processed approval", {
876
+ count: approvalCount,
877
+ max: maxApprovals
878
+ });
879
+ try {
880
+ await page.waitForFunction((prevCount) => window.__state?.responses?.length > prevCount, approvalCount, { timeout: timeout / 2 });
881
+ await page.waitForTimeout(DOM_SETTLE_DELAY_MS);
882
+ } catch {
883
+ break;
884
+ }
885
+ }
886
+ return approvalCount;
887
+ }
888
+ var OpenAiChatKitProvider = class OpenAiChatKitProvider extends require_openai.OpenAiGenericProvider {
889
+ chatKitConfig;
890
+ browser = null;
891
+ context = null;
892
+ page = null;
893
+ server = null;
894
+ serverPort = 0;
895
+ initialized = false;
896
+ static defaultUserId = null;
897
+ static getDefaultUserId() {
898
+ if (!OpenAiChatKitProvider.defaultUserId) OpenAiChatKitProvider.defaultUserId = `promptfoo-eval-${Date.now()}`;
899
+ return OpenAiChatKitProvider.defaultUserId;
900
+ }
901
+ constructor(workflowId, options = {}) {
902
+ super(workflowId, options);
903
+ const envPoolSize = process.env.PROMPTFOO_MAX_CONCURRENCY ? parseInt(process.env.PROMPTFOO_MAX_CONCURRENCY, 10) : NaN;
904
+ const defaultPoolSize = Number.isNaN(envPoolSize) ? DEFAULT_POOL_SIZE : envPoolSize;
905
+ this.chatKitConfig = {
906
+ workflowId: options.config?.workflowId || workflowId,
907
+ version: options.config?.version,
908
+ userId: options.config?.userId || OpenAiChatKitProvider.getDefaultUserId(),
909
+ timeout: options.config?.timeout || DEFAULT_TIMEOUT_MS,
910
+ headless: options.config?.headless ?? true,
911
+ serverPort: options.config?.serverPort || 0,
912
+ usePool: options.config?.usePool ?? true,
913
+ poolSize: options.config?.poolSize ?? defaultPoolSize,
914
+ approvalHandling: options.config?.approvalHandling ?? "auto-approve",
915
+ maxApprovals: options.config?.maxApprovals ?? DEFAULT_MAX_APPROVALS,
916
+ stateful: options.config?.stateful ?? false
917
+ };
918
+ }
919
+ id() {
920
+ const version = this.chatKitConfig.version ? `:${this.chatKitConfig.version}` : "";
921
+ return `openai:chatkit:${this.chatKitConfig.workflowId}${version}`;
922
+ }
923
+ toString() {
924
+ return `[OpenAI ChatKit Provider ${this.chatKitConfig.workflowId}]`;
925
+ }
926
+ /**
927
+ * Initialize the browser and ChatKit page
928
+ */
929
+ async initialize() {
930
+ if (this.initialized) return;
931
+ const apiKey = this.getApiKey();
932
+ if (!apiKey) throw new Error("OpenAI API key is required for ChatKit provider");
933
+ const workflowId = this.chatKitConfig.workflowId;
934
+ if (!workflowId) throw new Error("ChatKit workflowId is required");
935
+ require_logger.logger.debug("[ChatKitProvider] Initializing", {
936
+ workflowId,
937
+ version: this.chatKitConfig.version
938
+ });
939
+ const html = generateChatKitHTML(apiKey, workflowId, this.chatKitConfig.version, this.chatKitConfig.userId);
940
+ this.server = http.createServer((_req, res) => {
941
+ res.writeHead(200, { "Content-Type": "text/html" });
942
+ res.end(html);
943
+ });
944
+ await new Promise((resolve, reject) => {
945
+ this.server.once("error", (err) => {
946
+ reject(/* @__PURE__ */ new Error(`Failed to start ChatKit server: ${err.message}`));
947
+ });
948
+ this.server.listen(this.chatKitConfig.serverPort, () => {
949
+ const address = this.server.address();
950
+ this.serverPort = typeof address === "object" ? address?.port || 0 : 0;
951
+ require_logger.logger.debug("[ChatKitProvider] Server started", { port: this.serverPort });
952
+ resolve();
953
+ });
954
+ });
955
+ try {
956
+ this.browser = await playwright.chromium.launch({ headless: this.chatKitConfig.headless });
957
+ } catch (launchError) {
958
+ const errorMessage = launchError instanceof Error ? launchError.message : String(launchError);
959
+ if (errorMessage.includes("Executable doesn't exist") || errorMessage.includes("browserType.launch")) throw new Error(`Playwright browser not installed. Run: npx playwright install chromium
960
+ Original error: ${errorMessage}`);
961
+ throw launchError;
962
+ }
963
+ this.context = await this.browser.newContext({ viewport: {
964
+ width: 800,
965
+ height: 600
966
+ } });
967
+ this.page = await this.context.newPage();
968
+ this.page.on("console", (msg) => {
969
+ const type = msg.type();
970
+ if (type === "error" || type === "warning") require_logger.logger.debug("[ChatKitProvider] Browser console", {
971
+ type,
972
+ text: msg.text()
973
+ });
974
+ });
975
+ await this.page.goto(`http://localhost:${this.serverPort}`, { waitUntil: "domcontentloaded" });
976
+ require_logger.logger.debug("[ChatKitProvider] Waiting for ChatKit ready");
977
+ await this.page.waitForFunction(() => window.__state?.ready === true, { timeout: CHATKIT_READY_TIMEOUT_MS });
978
+ this.initialized = true;
979
+ if (!this.chatKitConfig.usePool) require_providerRegistry.providerRegistry.register(this);
980
+ require_logger.logger.debug("[ChatKitProvider] Initialized successfully");
981
+ }
982
+ /**
983
+ * Shutdown method for providerRegistry cleanup
984
+ */
985
+ async shutdown() {
986
+ await this.cleanup();
987
+ }
988
+ /**
989
+ * Clean up browser resources
990
+ */
991
+ async cleanup() {
992
+ if (this.context) {
993
+ await this.context.close();
994
+ this.context = null;
995
+ this.page = null;
996
+ }
997
+ if (this.browser) {
998
+ await this.browser.close();
999
+ this.browser = null;
1000
+ }
1001
+ if (this.server) {
1002
+ this.server.close();
1003
+ this.server = null;
1004
+ }
1005
+ this.initialized = false;
1006
+ }
1007
+ /**
1008
+ * Call the ChatKit workflow with the given prompt
1009
+ */
1010
+ async callApi(prompt, _context, _callApiOptions) {
1011
+ const usePool = this.chatKitConfig.usePool && !this.chatKitConfig.stateful;
1012
+ require_logger.logger.debug("[ChatKitProvider] Starting call", {
1013
+ prompt: prompt.substring(0, 100),
1014
+ workflowId: this.chatKitConfig.workflowId,
1015
+ usePool,
1016
+ stateful: this.chatKitConfig.stateful
1017
+ });
1018
+ if (usePool) return this.callApiWithPool(prompt);
1019
+ const startTime = Date.now();
1020
+ try {
1021
+ await this.initialize();
1022
+ if (!this.page) throw new Error("Browser page not initialized");
1023
+ if (!this.chatKitConfig.stateful) {
1024
+ await this.page.reload({ waitUntil: "domcontentloaded" });
1025
+ await this.page.waitForFunction(() => window.__state?.ready === true, { timeout: CHATKIT_READY_TIMEOUT_MS });
1026
+ }
1027
+ const responseCount = await this.page.evaluate(() => window.__state?.responses?.length || 0);
1028
+ const isFollowUp = this.chatKitConfig.stateful && responseCount > 0;
1029
+ require_logger.logger.debug("[ChatKitProvider] Sending message", {
1030
+ stateful: this.chatKitConfig.stateful,
1031
+ isFollowUp,
1032
+ responseCount
1033
+ });
1034
+ await this.page.evaluate(({ text, newThread }) => {
1035
+ return window.__chatkit.sendUserMessage({
1036
+ text,
1037
+ newThread
1038
+ });
1039
+ }, {
1040
+ text: prompt,
1041
+ newThread: !isFollowUp
1042
+ });
1043
+ require_logger.logger.debug("[ChatKitProvider] Waiting for response");
1044
+ const expectedResponseCount = responseCount + 1;
1045
+ await this.page.waitForFunction((expected) => window.__state?.responses?.length >= expected, expectedResponseCount, { timeout: this.chatKitConfig.timeout });
1046
+ const stabilizationResult = await waitForContentStabilization(this.page, this.chatKitConfig.timeout ?? DEFAULT_TIMEOUT_MS, startTime);
1047
+ const approvalsHandled = await processApprovals(this.page, this.chatKitConfig.approvalHandling ?? "auto-approve", this.chatKitConfig.maxApprovals ?? DEFAULT_MAX_APPROVALS, this.chatKitConfig.timeout ?? DEFAULT_TIMEOUT_MS);
1048
+ if (approvalsHandled > 0) require_logger.logger.debug("[ChatKitProvider] Processed approvals", { count: approvalsHandled });
1049
+ let responseText = await extractResponseFromFrame(this.page);
1050
+ if (!responseText && stabilizationResult.assistantResponse) {
1051
+ require_logger.logger.debug("[ChatKitProvider] Using fallback content from stabilization", { fallbackLength: stabilizationResult.assistantResponse.length });
1052
+ responseText = cleanAssistantResponse(stabilizationResult.assistantResponse);
1053
+ }
1054
+ const threadId = await this.page.evaluate(() => window.__state.threadId);
1055
+ const finalResponseCount = await this.page.evaluate(() => window.__state?.responses?.length || 0);
1056
+ const latencyMs = Date.now() - startTime;
1057
+ require_logger.logger.debug("[ChatKitProvider] Response received", {
1058
+ threadId,
1059
+ textLength: responseText.length,
1060
+ turnNumber: finalResponseCount,
1061
+ latencyMs
1062
+ });
1063
+ return {
1064
+ output: responseText,
1065
+ cached: false,
1066
+ latencyMs,
1067
+ sessionId: threadId,
1068
+ tokenUsage: { numRequests: 1 },
1069
+ metadata: {
1070
+ workflowId: this.chatKitConfig.workflowId,
1071
+ version: this.chatKitConfig.version,
1072
+ stateful: this.chatKitConfig.stateful,
1073
+ turnNumber: finalResponseCount
1074
+ }
1075
+ };
1076
+ } catch (error) {
1077
+ const errorMessage = error instanceof Error ? error.message : String(error);
1078
+ require_logger.logger.error("[ChatKitProvider] Call failed", { error: errorMessage });
1079
+ if (this.page) try {
1080
+ const stateError = await this.page.evaluate(() => window.__state?.error);
1081
+ if (stateError) return { error: `ChatKit workflow error: ${stateError}` };
1082
+ } catch {}
1083
+ if (errorMessage.includes("Timeout") || errorMessage.includes("timeout")) return { error: `ChatKit response timeout after ${this.chatKitConfig.timeout}ms. Try increasing timeout in config or use --max-concurrency 1 for more reliable results.` };
1084
+ if (errorMessage.includes("API key")) return { error: "OpenAI API key is required. Set OPENAI_API_KEY environment variable." };
1085
+ if (errorMessage.includes("Playwright") || errorMessage.includes("browser")) return { error: `Browser error: ${errorMessage}. Ensure Playwright is installed: npx playwright install chromium` };
1086
+ return { error: `ChatKit provider error: ${errorMessage}` };
1087
+ }
1088
+ }
1089
+ /**
1090
+ * Pool-based callApi for better concurrency support.
1091
+ * Uses a shared browser with multiple contexts instead of separate browsers.
1092
+ */
1093
+ async callApiWithPool(prompt) {
1094
+ const apiKey = this.getApiKey();
1095
+ if (!apiKey) return { error: "OpenAI API key is required. Set OPENAI_API_KEY environment variable." };
1096
+ const workflowId = this.chatKitConfig.workflowId;
1097
+ if (!workflowId) return { error: "ChatKit workflowId is required" };
1098
+ const pool = ChatKitBrowserPool.getInstance({
1099
+ maxConcurrency: this.chatKitConfig.poolSize,
1100
+ headless: this.chatKitConfig.headless
1101
+ });
1102
+ const templateKey = ChatKitBrowserPool.generateTemplateKey(workflowId, this.chatKitConfig.version, this.chatKitConfig.userId);
1103
+ const html = generateChatKitHTML(apiKey, workflowId, this.chatKitConfig.version, this.chatKitConfig.userId);
1104
+ pool.setTemplate(templateKey, html);
1105
+ let pooledPage = null;
1106
+ const startTime = Date.now();
1107
+ try {
1108
+ pooledPage = await pool.acquirePage(templateKey);
1109
+ const page = pooledPage.page;
1110
+ require_logger.logger.debug("[ChatKitProvider] Acquired page from pool", { stats: pool.getStats() });
1111
+ await page.evaluate((text) => {
1112
+ return window.__chatkit.sendUserMessage({
1113
+ text,
1114
+ newThread: true
1115
+ });
1116
+ }, prompt);
1117
+ await page.waitForFunction(() => window.__state?.responses?.length > 0, { timeout: this.chatKitConfig.timeout });
1118
+ const stabilizationResult = await waitForContentStabilization(page, this.chatKitConfig.timeout ?? DEFAULT_TIMEOUT_MS, startTime);
1119
+ const approvalsHandled = await processApprovals(page, this.chatKitConfig.approvalHandling ?? "auto-approve", this.chatKitConfig.maxApprovals ?? DEFAULT_MAX_APPROVALS, this.chatKitConfig.timeout ?? DEFAULT_TIMEOUT_MS);
1120
+ if (approvalsHandled > 0) require_logger.logger.debug("[ChatKitProvider] Pool processed approvals", { count: approvalsHandled });
1121
+ let responseText = await extractResponseFromFrame(page);
1122
+ if (!responseText && stabilizationResult.assistantResponse) {
1123
+ require_logger.logger.debug("[ChatKitProvider] Pool using fallback content from stabilization", { fallbackLength: stabilizationResult.assistantResponse.length });
1124
+ responseText = cleanAssistantResponse(stabilizationResult.assistantResponse);
1125
+ }
1126
+ const threadId = await page.evaluate(() => window.__state.threadId);
1127
+ const latencyMs = Date.now() - startTime;
1128
+ require_logger.logger.debug("[ChatKitProvider] Pool response received", {
1129
+ threadId,
1130
+ textLength: responseText.length,
1131
+ latencyMs
1132
+ });
1133
+ return {
1134
+ output: responseText,
1135
+ cached: false,
1136
+ latencyMs,
1137
+ sessionId: threadId,
1138
+ tokenUsage: { numRequests: 1 },
1139
+ metadata: {
1140
+ workflowId: this.chatKitConfig.workflowId,
1141
+ version: this.chatKitConfig.version,
1142
+ poolMode: true
1143
+ }
1144
+ };
1145
+ } catch (error) {
1146
+ const errorMessage = error instanceof Error ? error.message : String(error);
1147
+ require_logger.logger.error("[ChatKitProvider] Pool call failed", { error: errorMessage });
1148
+ if (errorMessage.includes("Timeout") || errorMessage.includes("timeout")) return { error: `ChatKit response timeout after ${this.chatKitConfig.timeout}ms. Try increasing timeout or reducing concurrency.` };
1149
+ return { error: `ChatKit provider error: ${errorMessage}` };
1150
+ } finally {
1151
+ if (pooledPage) await pool.releasePage(pooledPage);
1152
+ }
1153
+ }
1154
+ };
1155
+ //#endregion
1156
+ exports.OpenAiChatKitProvider = OpenAiChatKitProvider;
1157
+
1158
+ //# sourceMappingURL=chatkit-Dw0mKkML.cjs.map