elasticdash-sdk 0.2.0

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 (349) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +775 -0
  3. package/dist/browser-ui.d.ts +43 -0
  4. package/dist/browser-ui.d.ts.map +1 -0
  5. package/dist/browser-ui.js +246 -0
  6. package/dist/browser-ui.js.map +1 -0
  7. package/dist/capture/event.d.ts +33 -0
  8. package/dist/capture/event.d.ts.map +1 -0
  9. package/dist/capture/event.js +2 -0
  10. package/dist/capture/event.js.map +1 -0
  11. package/dist/capture/index.d.ts +4 -0
  12. package/dist/capture/index.d.ts.map +1 -0
  13. package/dist/capture/index.js +4 -0
  14. package/dist/capture/index.js.map +1 -0
  15. package/dist/capture/recorder.d.ts +24 -0
  16. package/dist/capture/recorder.d.ts.map +1 -0
  17. package/dist/capture/recorder.js +46 -0
  18. package/dist/capture/recorder.js.map +1 -0
  19. package/dist/capture/replay.d.ts +20 -0
  20. package/dist/capture/replay.d.ts.map +1 -0
  21. package/dist/capture/replay.js +47 -0
  22. package/dist/capture/replay.js.map +1 -0
  23. package/dist/ci/api-client.d.ts +38 -0
  24. package/dist/ci/api-client.d.ts.map +1 -0
  25. package/dist/ci/api-client.js +96 -0
  26. package/dist/ci/api-client.js.map +1 -0
  27. package/dist/ci/benchmark.d.ts +33 -0
  28. package/dist/ci/benchmark.d.ts.map +1 -0
  29. package/dist/ci/benchmark.js +213 -0
  30. package/dist/ci/benchmark.js.map +1 -0
  31. package/dist/ci/ed-runner.d.ts +48 -0
  32. package/dist/ci/ed-runner.d.ts.map +1 -0
  33. package/dist/ci/ed-runner.js +260 -0
  34. package/dist/ci/ed-runner.js.map +1 -0
  35. package/dist/ci/executor.d.ts +13 -0
  36. package/dist/ci/executor.d.ts.map +1 -0
  37. package/dist/ci/executor.js +542 -0
  38. package/dist/ci/executor.js.map +1 -0
  39. package/dist/ci/git-info.d.ts +17 -0
  40. package/dist/ci/git-info.d.ts.map +1 -0
  41. package/dist/ci/git-info.js +102 -0
  42. package/dist/ci/git-info.js.map +1 -0
  43. package/dist/ci/index.d.ts +6 -0
  44. package/dist/ci/index.d.ts.map +1 -0
  45. package/dist/ci/index.js +4 -0
  46. package/dist/ci/index.js.map +1 -0
  47. package/dist/ci/measurement.d.ts +9 -0
  48. package/dist/ci/measurement.d.ts.map +1 -0
  49. package/dist/ci/measurement.js +15 -0
  50. package/dist/ci/measurement.js.map +1 -0
  51. package/dist/ci/replay.d.ts +31 -0
  52. package/dist/ci/replay.d.ts.map +1 -0
  53. package/dist/ci/replay.js +96 -0
  54. package/dist/ci/replay.js.map +1 -0
  55. package/dist/ci/reporters/default.d.ts +8 -0
  56. package/dist/ci/reporters/default.d.ts.map +1 -0
  57. package/dist/ci/reporters/default.js +46 -0
  58. package/dist/ci/reporters/default.js.map +1 -0
  59. package/dist/ci/reporters/index.d.ts +8 -0
  60. package/dist/ci/reporters/index.d.ts.map +1 -0
  61. package/dist/ci/reporters/index.js +14 -0
  62. package/dist/ci/reporters/index.js.map +1 -0
  63. package/dist/ci/reporters/json.d.ts +8 -0
  64. package/dist/ci/reporters/json.d.ts.map +1 -0
  65. package/dist/ci/reporters/json.js +14 -0
  66. package/dist/ci/reporters/json.js.map +1 -0
  67. package/dist/ci/reporters/junit.d.ts +8 -0
  68. package/dist/ci/reporters/junit.d.ts.map +1 -0
  69. package/dist/ci/reporters/junit.js +48 -0
  70. package/dist/ci/reporters/junit.js.map +1 -0
  71. package/dist/ci/runner.d.ts +3 -0
  72. package/dist/ci/runner.d.ts.map +1 -0
  73. package/dist/ci/runner.js +187 -0
  74. package/dist/ci/runner.js.map +1 -0
  75. package/dist/ci/test-discovery.d.ts +5 -0
  76. package/dist/ci/test-discovery.d.ts.map +1 -0
  77. package/dist/ci/test-discovery.js +11 -0
  78. package/dist/ci/test-discovery.js.map +1 -0
  79. package/dist/ci/test-loader.d.ts +19 -0
  80. package/dist/ci/test-loader.d.ts.map +1 -0
  81. package/dist/ci/test-loader.js +149 -0
  82. package/dist/ci/test-loader.js.map +1 -0
  83. package/dist/ci/test-registry.d.ts +42 -0
  84. package/dist/ci/test-registry.d.ts.map +1 -0
  85. package/dist/ci/test-registry.js +18 -0
  86. package/dist/ci/test-registry.js.map +1 -0
  87. package/dist/ci/trace-schema.d.ts +30 -0
  88. package/dist/ci/trace-schema.d.ts.map +1 -0
  89. package/dist/ci/trace-schema.js +66 -0
  90. package/dist/ci/trace-schema.js.map +1 -0
  91. package/dist/ci/trace-writer.d.ts +16 -0
  92. package/dist/ci/trace-writer.d.ts.map +1 -0
  93. package/dist/ci/trace-writer.js +108 -0
  94. package/dist/ci/trace-writer.js.map +1 -0
  95. package/dist/ci/types.d.ts +108 -0
  96. package/dist/ci/types.d.ts.map +1 -0
  97. package/dist/ci/types.js +3 -0
  98. package/dist/ci/types.js.map +1 -0
  99. package/dist/ci/upload-client.d.ts +74 -0
  100. package/dist/ci/upload-client.d.ts.map +1 -0
  101. package/dist/ci/upload-client.js +195 -0
  102. package/dist/ci/upload-client.js.map +1 -0
  103. package/dist/cli.d.ts +3 -0
  104. package/dist/cli.d.ts.map +1 -0
  105. package/dist/cli.js +716 -0
  106. package/dist/cli.js.map +1 -0
  107. package/dist/core/agent-state.d.ts +47 -0
  108. package/dist/core/agent-state.d.ts.map +1 -0
  109. package/dist/core/agent-state.js +137 -0
  110. package/dist/core/agent-state.js.map +1 -0
  111. package/dist/core/judge-utils.d.ts +22 -0
  112. package/dist/core/judge-utils.d.ts.map +1 -0
  113. package/dist/core/judge-utils.js +211 -0
  114. package/dist/core/judge-utils.js.map +1 -0
  115. package/dist/core/registry.d.ts +28 -0
  116. package/dist/core/registry.d.ts.map +1 -0
  117. package/dist/core/registry.js +52 -0
  118. package/dist/core/registry.js.map +1 -0
  119. package/dist/dashboard-server.d.ts +65 -0
  120. package/dist/dashboard-server.d.ts.map +1 -0
  121. package/dist/dashboard-server.js +3940 -0
  122. package/dist/dashboard-server.js.map +1 -0
  123. package/dist/execution/tool-runner.d.ts +26 -0
  124. package/dist/execution/tool-runner.d.ts.map +1 -0
  125. package/dist/execution/tool-runner.js +316 -0
  126. package/dist/execution/tool-runner.js.map +1 -0
  127. package/dist/html/dashboard.html +2218 -0
  128. package/dist/http.d.ts +14 -0
  129. package/dist/http.d.ts.map +1 -0
  130. package/dist/http.js +13 -0
  131. package/dist/http.js.map +1 -0
  132. package/dist/index.cjs +8102 -0
  133. package/dist/index.d.ts +61 -0
  134. package/dist/index.d.ts.map +1 -0
  135. package/dist/index.js +67 -0
  136. package/dist/index.js.map +1 -0
  137. package/dist/interceptors/ai-interceptor.d.ts +26 -0
  138. package/dist/interceptors/ai-interceptor.d.ts.map +1 -0
  139. package/dist/interceptors/ai-interceptor.js +756 -0
  140. package/dist/interceptors/ai-interceptor.js.map +1 -0
  141. package/dist/interceptors/db-auto.d.ts +8 -0
  142. package/dist/interceptors/db-auto.d.ts.map +1 -0
  143. package/dist/interceptors/db-auto.js +217 -0
  144. package/dist/interceptors/db-auto.js.map +1 -0
  145. package/dist/interceptors/db.d.ts +23 -0
  146. package/dist/interceptors/db.d.ts.map +1 -0
  147. package/dist/interceptors/db.js +137 -0
  148. package/dist/interceptors/db.js.map +1 -0
  149. package/dist/interceptors/http.d.ts +28 -0
  150. package/dist/interceptors/http.d.ts.map +1 -0
  151. package/dist/interceptors/http.js +356 -0
  152. package/dist/interceptors/http.js.map +1 -0
  153. package/dist/interceptors/side-effects.d.ts +7 -0
  154. package/dist/interceptors/side-effects.d.ts.map +1 -0
  155. package/dist/interceptors/side-effects.js +72 -0
  156. package/dist/interceptors/side-effects.js.map +1 -0
  157. package/dist/interceptors/telemetry-push.d.ts +142 -0
  158. package/dist/interceptors/telemetry-push.d.ts.map +1 -0
  159. package/dist/interceptors/telemetry-push.js +463 -0
  160. package/dist/interceptors/telemetry-push.js.map +1 -0
  161. package/dist/interceptors/tool.d.ts +2 -0
  162. package/dist/interceptors/tool.d.ts.map +1 -0
  163. package/dist/interceptors/tool.js +274 -0
  164. package/dist/interceptors/tool.js.map +1 -0
  165. package/dist/interceptors/workflow-ai.d.ts +5 -0
  166. package/dist/interceptors/workflow-ai.d.ts.map +1 -0
  167. package/dist/interceptors/workflow-ai.js +382 -0
  168. package/dist/interceptors/workflow-ai.js.map +1 -0
  169. package/dist/internals/conditional-recorder.d.ts +21 -0
  170. package/dist/internals/conditional-recorder.d.ts.map +1 -0
  171. package/dist/internals/conditional-recorder.js +54 -0
  172. package/dist/internals/conditional-recorder.js.map +1 -0
  173. package/dist/internals/mock-resolver.d.ts +146 -0
  174. package/dist/internals/mock-resolver.d.ts.map +1 -0
  175. package/dist/internals/mock-resolver.js +427 -0
  176. package/dist/internals/mock-resolver.js.map +1 -0
  177. package/dist/matchers/index.d.ts +96 -0
  178. package/dist/matchers/index.d.ts.map +1 -0
  179. package/dist/matchers/index.js +668 -0
  180. package/dist/matchers/index.js.map +1 -0
  181. package/dist/observability.d.ts +82 -0
  182. package/dist/observability.d.ts.map +1 -0
  183. package/dist/observability.js +471 -0
  184. package/dist/observability.js.map +1 -0
  185. package/dist/portal-executor.d.ts +30 -0
  186. package/dist/portal-executor.d.ts.map +1 -0
  187. package/dist/portal-executor.js +324 -0
  188. package/dist/portal-executor.js.map +1 -0
  189. package/dist/portal-server.d.ts +3 -0
  190. package/dist/portal-server.d.ts.map +1 -0
  191. package/dist/portal-server.js +279 -0
  192. package/dist/portal-server.js.map +1 -0
  193. package/dist/proxy/llm-capture.d.ts +14 -0
  194. package/dist/proxy/llm-capture.d.ts.map +1 -0
  195. package/dist/proxy/llm-capture.js +264 -0
  196. package/dist/proxy/llm-capture.js.map +1 -0
  197. package/dist/reporter.d.ts +3 -0
  198. package/dist/reporter.d.ts.map +1 -0
  199. package/dist/reporter.js +72 -0
  200. package/dist/reporter.js.map +1 -0
  201. package/dist/runWorkflowSubprocess.d.ts +14 -0
  202. package/dist/runWorkflowSubprocess.d.ts.map +1 -0
  203. package/dist/runWorkflowSubprocess.js +66 -0
  204. package/dist/runWorkflowSubprocess.js.map +1 -0
  205. package/dist/runner.d.ts +16 -0
  206. package/dist/runner.d.ts.map +1 -0
  207. package/dist/runner.js +138 -0
  208. package/dist/runner.js.map +1 -0
  209. package/dist/socket-connector.d.ts +22 -0
  210. package/dist/socket-connector.d.ts.map +1 -0
  211. package/dist/socket-connector.js +104 -0
  212. package/dist/socket-connector.js.map +1 -0
  213. package/dist/telemetry-batcher.d.ts +56 -0
  214. package/dist/telemetry-batcher.d.ts.map +1 -0
  215. package/dist/telemetry-batcher.js +143 -0
  216. package/dist/telemetry-batcher.js.map +1 -0
  217. package/dist/test-setup.d.ts +12 -0
  218. package/dist/test-setup.d.ts.map +1 -0
  219. package/dist/test-setup.js +13 -0
  220. package/dist/test-setup.js.map +1 -0
  221. package/dist/tool-registry.d.ts +31 -0
  222. package/dist/tool-registry.d.ts.map +1 -0
  223. package/dist/tool-registry.js +73 -0
  224. package/dist/tool-registry.js.map +1 -0
  225. package/dist/tool-runner-worker.d.ts +2 -0
  226. package/dist/tool-runner-worker.d.ts.map +1 -0
  227. package/dist/tool-runner-worker.js +215 -0
  228. package/dist/tool-runner-worker.js.map +1 -0
  229. package/dist/trace-adapter/context.d.ts +72 -0
  230. package/dist/trace-adapter/context.d.ts.map +1 -0
  231. package/dist/trace-adapter/context.js +80 -0
  232. package/dist/trace-adapter/context.js.map +1 -0
  233. package/dist/tracing.d.ts +2 -0
  234. package/dist/tracing.d.ts.map +1 -0
  235. package/dist/tracing.js +59 -0
  236. package/dist/tracing.js.map +1 -0
  237. package/dist/trigger-executor.d.ts +12 -0
  238. package/dist/trigger-executor.d.ts.map +1 -0
  239. package/dist/trigger-executor.js +130 -0
  240. package/dist/trigger-executor.js.map +1 -0
  241. package/dist/types/portal.d.ts +76 -0
  242. package/dist/types/portal.d.ts.map +1 -0
  243. package/dist/types/portal.js +2 -0
  244. package/dist/types/portal.js.map +1 -0
  245. package/dist/utils/debug.d.ts +3 -0
  246. package/dist/utils/debug.d.ts.map +1 -0
  247. package/dist/utils/debug.js +8 -0
  248. package/dist/utils/debug.js.map +1 -0
  249. package/dist/utils/license-error.d.ts +23 -0
  250. package/dist/utils/license-error.d.ts.map +1 -0
  251. package/dist/utils/license-error.js +42 -0
  252. package/dist/utils/license-error.js.map +1 -0
  253. package/dist/utils/redact.d.ts +7 -0
  254. package/dist/utils/redact.d.ts.map +1 -0
  255. package/dist/utils/redact.js +26 -0
  256. package/dist/utils/redact.js.map +1 -0
  257. package/dist/workflow-runner-worker.d.ts +2 -0
  258. package/dist/workflow-runner-worker.d.ts.map +1 -0
  259. package/dist/workflow-runner-worker.js +329 -0
  260. package/dist/workflow-runner-worker.js.map +1 -0
  261. package/dist/workflow-runner.d.ts +14 -0
  262. package/dist/workflow-runner.d.ts.map +1 -0
  263. package/dist/workflow-runner.js +34 -0
  264. package/dist/workflow-runner.js.map +1 -0
  265. package/docs/agent-coding-instructions.md +138 -0
  266. package/docs/agent-integration-guide.md +564 -0
  267. package/docs/agents.md +140 -0
  268. package/docs/dashboard.md +394 -0
  269. package/docs/deno.md +69 -0
  270. package/docs/instrumentation.md +424 -0
  271. package/docs/langfuse-trace-structure.md +145 -0
  272. package/docs/matchers.md +173 -0
  273. package/docs/observability_contract.md +192 -0
  274. package/docs/observability_mode.md +195 -0
  275. package/docs/quickstart.md +621 -0
  276. package/docs/security-compliance.md +566 -0
  277. package/docs/test-writing-guidelines.md +444 -0
  278. package/docs/tools.md +165 -0
  279. package/docs/workflow-modes.md +253 -0
  280. package/package.json +76 -0
  281. package/src/browser-ui.ts +281 -0
  282. package/src/capture/event.ts +30 -0
  283. package/src/capture/index.ts +3 -0
  284. package/src/capture/recorder.ts +62 -0
  285. package/src/capture/replay.ts +55 -0
  286. package/src/ci/api-client.ts +136 -0
  287. package/src/ci/benchmark.ts +257 -0
  288. package/src/ci/ed-runner.ts +351 -0
  289. package/src/ci/executor.ts +671 -0
  290. package/src/ci/git-info.ts +127 -0
  291. package/src/ci/index.ts +5 -0
  292. package/src/ci/measurement.ts +25 -0
  293. package/src/ci/replay.ts +127 -0
  294. package/src/ci/reporters/default.ts +50 -0
  295. package/src/ci/reporters/index.ts +21 -0
  296. package/src/ci/reporters/json.ts +18 -0
  297. package/src/ci/reporters/junit.ts +61 -0
  298. package/src/ci/runner.ts +208 -0
  299. package/src/ci/test-discovery.ts +16 -0
  300. package/src/ci/test-loader.ts +187 -0
  301. package/src/ci/test-registry.ts +62 -0
  302. package/src/ci/trace-schema.ts +96 -0
  303. package/src/ci/trace-writer.ts +107 -0
  304. package/src/ci/types.ts +115 -0
  305. package/src/ci/upload-client.ts +300 -0
  306. package/src/cli.ts +811 -0
  307. package/src/core/agent-state.ts +162 -0
  308. package/src/core/judge-utils.ts +232 -0
  309. package/src/core/registry.ts +92 -0
  310. package/src/dashboard-server.ts +2047 -0
  311. package/src/execution/tool-runner.ts +352 -0
  312. package/src/html/dashboard.html +2218 -0
  313. package/src/http.ts +13 -0
  314. package/src/index.ts +138 -0
  315. package/src/interceptors/ai-interceptor.ts +798 -0
  316. package/src/interceptors/db-auto.ts +243 -0
  317. package/src/interceptors/db.ts +156 -0
  318. package/src/interceptors/http.ts +393 -0
  319. package/src/interceptors/side-effects.ts +83 -0
  320. package/src/interceptors/telemetry-push.ts +537 -0
  321. package/src/interceptors/tool.ts +287 -0
  322. package/src/interceptors/workflow-ai.ts +419 -0
  323. package/src/internals/conditional-recorder.ts +63 -0
  324. package/src/internals/mock-resolver.ts +492 -0
  325. package/src/matchers/index.ts +824 -0
  326. package/src/observability.ts +501 -0
  327. package/src/portal-executor.ts +355 -0
  328. package/src/portal-server.ts +304 -0
  329. package/src/proxy/llm-capture.ts +301 -0
  330. package/src/reporter.ts +81 -0
  331. package/src/runWorkflowSubprocess.ts +74 -0
  332. package/src/runner.ts +178 -0
  333. package/src/socket-connector.ts +117 -0
  334. package/src/telemetry-batcher.ts +191 -0
  335. package/src/test-setup.ts +16 -0
  336. package/src/tool-registry.ts +94 -0
  337. package/src/tool-runner-worker.ts +244 -0
  338. package/src/trace-adapter/context.ts +156 -0
  339. package/src/tracing.ts +62 -0
  340. package/src/trigger-executor.ts +171 -0
  341. package/src/types/agent.d.ts +63 -0
  342. package/src/types/expect.d.ts +81 -0
  343. package/src/types/modules.d.ts +2 -0
  344. package/src/types/portal.ts +69 -0
  345. package/src/utils/debug.ts +8 -0
  346. package/src/utils/license-error.ts +43 -0
  347. package/src/utils/redact.ts +25 -0
  348. package/src/workflow-runner-worker.ts +386 -0
  349. package/src/workflow-runner.ts +58 -0
@@ -0,0 +1,756 @@
1
+ import { getCurrentTrace } from '../trace-adapter/context.js';
2
+ import { getCaptureContext } from '../capture/recorder.js';
3
+ import { rawDateNow } from './side-effects.js';
4
+ import { getObservabilityContext, getHttpRunContext, getHttpFrozenEvent, pushTelemetryEvent } from './telemetry-push.js';
5
+ /**
6
+ * Check the global flag set by wrapAI to avoid double-recording.
7
+ * Reads the global directly instead of importing from workflow-ai.ts
8
+ * to avoid circular dependency (telemetry-push → ai-interceptor → workflow-ai → telemetry-push).
9
+ */
10
+ const AI_WRAPPER_KEY = '__elasticdash_ai_wrapper_depth__';
11
+ function isAIWrapperActive() {
12
+ return (globalThis[AI_WRAPPER_KEY] ?? 0) > 0;
13
+ }
14
+ /**
15
+ * When inside a wrapAI call, the ai-interceptor captures the actual HTTP
16
+ * request payload and stashes it here so wrapAI can attach it to its event.
17
+ */
18
+ const LLM_REQUEST_KEY = '__elasticdash_last_llm_request__';
19
+ export function consumeCapturedLLMRequest() {
20
+ const g = globalThis;
21
+ const req = g[LLM_REQUEST_KEY];
22
+ if (req)
23
+ g[LLM_REQUEST_KEY] = undefined;
24
+ return req;
25
+ }
26
+ function extractPromptSnippet(body) {
27
+ let messages;
28
+ if (Array.isArray(body.messages))
29
+ messages = body.messages;
30
+ else if (Array.isArray(body.contents))
31
+ messages = body.contents;
32
+ if (!messages || messages.length === 0)
33
+ return undefined;
34
+ // Find the last user message
35
+ for (let i = messages.length - 1; i >= 0; i--) {
36
+ const msg = messages[i];
37
+ if (!msg)
38
+ continue;
39
+ if (msg.role === 'user') {
40
+ let content = msg.content;
41
+ if (Array.isArray(content)) {
42
+ content = content
43
+ .map((b) => (b && typeof b === 'object' ? String(b.text ?? '') : String(b)))
44
+ .filter(Boolean)
45
+ .join('');
46
+ }
47
+ if (typeof content === 'string') {
48
+ return content.slice(0, 100);
49
+ }
50
+ }
51
+ }
52
+ return undefined;
53
+ }
54
+ function extractUsage(provider, body) {
55
+ if (provider === 'openai' || provider === 'grok' || provider === 'kimi') {
56
+ const u = body.usage;
57
+ if (!u)
58
+ return undefined;
59
+ return { inputTokens: u.prompt_tokens, outputTokens: u.completion_tokens, totalTokens: u.total_tokens };
60
+ }
61
+ if (provider === 'anthropic') {
62
+ const u = body.usage;
63
+ if (!u)
64
+ return undefined;
65
+ return { inputTokens: u.input_tokens, outputTokens: u.output_tokens, totalTokens: (u.input_tokens ?? 0) + (u.output_tokens ?? 0) };
66
+ }
67
+ if (provider === 'gemini') {
68
+ const u = body.usageMetadata;
69
+ if (!u)
70
+ return undefined;
71
+ return { inputTokens: u.promptTokenCount, outputTokens: u.candidatesTokenCount, totalTokens: u.totalTokenCount };
72
+ }
73
+ return undefined;
74
+ }
75
+ function extractAssistantMessage(provider, body) {
76
+ if (provider === 'openai' || provider === 'grok' || provider === 'kimi') {
77
+ const choices = body.choices;
78
+ if (Array.isArray(choices) && choices.length > 0) {
79
+ const msg = choices[0].message;
80
+ if (msg && typeof msg === 'object')
81
+ return msg;
82
+ }
83
+ }
84
+ if (provider === 'anthropic') {
85
+ const content = body.content;
86
+ if (Array.isArray(content)) {
87
+ return { role: 'assistant', content };
88
+ }
89
+ }
90
+ if (provider === 'gemini') {
91
+ const candidates = body.candidates;
92
+ if (Array.isArray(candidates) && candidates.length > 0) {
93
+ const content = candidates[0].content;
94
+ if (content && typeof content === 'object')
95
+ return content;
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ /** URL patterns for known AI providers */
101
+ const AI_PATTERNS = {
102
+ openai: /https?:\/\/api\.openai\.com\/v1\/((chat\/)?completions|embeddings)/,
103
+ anthropic: /https?:\/\/api\.anthropic\.com\/v1\/messages/,
104
+ gemini: /https?:\/\/generativelanguage\.googleapis\.com\/.*\/models\/[^\/:]+:(generateContent|streamGenerateContent)/,
105
+ grok: /https?:\/\/api\.x\.ai\/v1\/(chat\/)?completions/,
106
+ kimi: /https?:\/\/api\.moonshot\.ai\/v1\/(chat\/)?completions/,
107
+ };
108
+ /** Detect which provider (if any) a URL belongs to */
109
+ function detectProvider(url) {
110
+ for (const [provider, pattern] of Object.entries(AI_PATTERNS)) {
111
+ if (pattern.test(url))
112
+ return provider;
113
+ }
114
+ return null;
115
+ }
116
+ /** Extract model name from request body or URL (for Gemini) */
117
+ function extractModel(provider, body, url) {
118
+ if (provider === 'gemini') {
119
+ // URL shape: .../models/gemini-1.5-pro:generateContent
120
+ const match = /\/models\/([^/:]+):/.exec(url);
121
+ return match ? match[1] : 'unknown';
122
+ }
123
+ return typeof body.model === 'string' ? body.model : 'unknown';
124
+ }
125
+ /** Extract prompt text from request body */
126
+ function extractPrompt(provider, body) {
127
+ if (provider === 'openai' || provider === 'anthropic' || provider === 'grok' || provider === 'kimi') {
128
+ let systemPrefix = '';
129
+ // Anthropic supports a top-level `system` parameter
130
+ if (provider === 'anthropic') {
131
+ if (typeof body.system === 'string') {
132
+ systemPrefix = `system: ${body.system}\n`;
133
+ }
134
+ else if (Array.isArray(body.system)) {
135
+ systemPrefix = body.system
136
+ .map((b) => {
137
+ if (b && typeof b === 'object') {
138
+ return String(b.text ?? '');
139
+ }
140
+ return String(b);
141
+ })
142
+ .filter(Boolean)
143
+ .map((t) => `system: ${t}`)
144
+ .join('\n') + '\n';
145
+ }
146
+ }
147
+ const messages = body.messages;
148
+ if (Array.isArray(messages)) {
149
+ const msgText = messages
150
+ .map((m) => {
151
+ if (m && typeof m === 'object') {
152
+ const msg = m;
153
+ // Anthropic content can be a string or an array of content blocks
154
+ let content = msg.content;
155
+ if (Array.isArray(content)) {
156
+ content = content
157
+ .map((b) => {
158
+ if (b && typeof b === 'object') {
159
+ return String(b.text ?? '');
160
+ }
161
+ return String(b);
162
+ })
163
+ .filter(Boolean)
164
+ .join('');
165
+ }
166
+ return `${msg.role}: ${content}`;
167
+ }
168
+ return String(m);
169
+ })
170
+ .join('\n');
171
+ return systemPrefix + msgText;
172
+ }
173
+ // Legacy completions API (OpenAI)
174
+ if (typeof body.prompt === 'string')
175
+ return body.prompt;
176
+ if (typeof body.input === 'string')
177
+ return body.input;
178
+ if (Array.isArray(body.input))
179
+ return body.input.map((v) => String(v)).join('\n');
180
+ return '';
181
+ }
182
+ if (provider === 'gemini') {
183
+ const contents = body.contents;
184
+ if (Array.isArray(contents)) {
185
+ return contents
186
+ .flatMap((c) => {
187
+ if (c && typeof c === 'object') {
188
+ const parts = c.parts;
189
+ if (Array.isArray(parts)) {
190
+ return parts.map((p) => {
191
+ if (p && typeof p === 'object') {
192
+ return String(p.text ?? '');
193
+ }
194
+ return '';
195
+ });
196
+ }
197
+ }
198
+ return [];
199
+ })
200
+ .join('\n');
201
+ }
202
+ }
203
+ return '';
204
+ }
205
+ /** Extract completion text from response body */
206
+ function extractCompletion(provider, responseBody) {
207
+ // Handle buffered streaming format
208
+ if (responseBody.streamed === true && typeof responseBody.completion === 'string') {
209
+ return responseBody.completion;
210
+ }
211
+ if (provider === 'openai' || provider === 'grok' || provider === 'kimi') {
212
+ const choices = responseBody.choices;
213
+ if (Array.isArray(choices) && choices.length > 0) {
214
+ const first = choices[0];
215
+ if (first.message && typeof first.message === 'object') {
216
+ return String(first.message.content ?? '');
217
+ }
218
+ if (typeof first.text === 'string')
219
+ return first.text;
220
+ }
221
+ // Embedding response: data[].embedding
222
+ const data = responseBody.data;
223
+ if (Array.isArray(data) && data.length > 0) {
224
+ const first = data[0];
225
+ if (Array.isArray(first?.embedding)) {
226
+ return `[${data.length} embedding(s), ${first.embedding.length} dimensions]`;
227
+ }
228
+ }
229
+ }
230
+ if (provider === 'anthropic') {
231
+ const content = responseBody.content;
232
+ if (Array.isArray(content)) {
233
+ return content
234
+ .map((block) => {
235
+ if (block && typeof block === 'object') {
236
+ const b = block;
237
+ if (b.type === 'text' && typeof b.text === 'string')
238
+ return b.text;
239
+ }
240
+ return '';
241
+ })
242
+ .filter(Boolean)
243
+ .join('');
244
+ }
245
+ }
246
+ if (provider === 'gemini') {
247
+ const candidates = responseBody.candidates;
248
+ if (Array.isArray(candidates) && candidates.length > 0) {
249
+ const first = candidates[0];
250
+ if (first.content && typeof first.content === 'object') {
251
+ const parts = first.content.parts;
252
+ if (Array.isArray(parts) && parts.length > 0) {
253
+ return String(parts[0].text ?? '');
254
+ }
255
+ }
256
+ }
257
+ // Gemini embedding response: embeddings[].values
258
+ const embeddings = responseBody.embeddings;
259
+ if (Array.isArray(embeddings) && embeddings.length > 0) {
260
+ const first = embeddings[0];
261
+ if (Array.isArray(first?.values)) {
262
+ return `[${embeddings.length} embedding(s), ${first.values.length} dimensions]`;
263
+ }
264
+ }
265
+ }
266
+ return '';
267
+ }
268
+ /** Buffer a streaming SSE/NDJSON response to extract the completion text */
269
+ async function bufferSSEStream(provider, stream) {
270
+ const decoder = new TextDecoder();
271
+ const reader = stream.getReader();
272
+ let raw = '';
273
+ try {
274
+ for (;;) {
275
+ const { done, value } = await reader.read();
276
+ if (done)
277
+ break;
278
+ raw += decoder.decode(value, { stream: true });
279
+ }
280
+ }
281
+ finally {
282
+ reader.releaseLock();
283
+ }
284
+ const lines = raw.split('\n');
285
+ let completion = '';
286
+ if (provider === 'gemini') {
287
+ // NDJSON: lines may be wrapped in `[` / `]` / `,`
288
+ for (const line of lines) {
289
+ const trimmed = line.trim().replace(/^[,\[]/, '').replace(/[,\]]$/, '');
290
+ if (!trimmed)
291
+ continue;
292
+ try {
293
+ const obj = JSON.parse(trimmed);
294
+ const candidates = obj.candidates;
295
+ if (Array.isArray(candidates) && candidates.length > 0) {
296
+ const first = candidates[0];
297
+ if (first.content && typeof first.content === 'object') {
298
+ const parts = first.content.parts;
299
+ if (Array.isArray(parts) && parts.length > 0) {
300
+ completion += String(parts[0].text ?? '');
301
+ }
302
+ }
303
+ }
304
+ }
305
+ catch {
306
+ // skip unparseable lines
307
+ }
308
+ }
309
+ }
310
+ else if (provider === 'anthropic') {
311
+ // Anthropic SSE format: event: <type>\ndata: <json>
312
+ for (const line of lines) {
313
+ if (!line.startsWith('data: '))
314
+ continue;
315
+ const data = line.slice(6).trim();
316
+ try {
317
+ const obj = JSON.parse(data);
318
+ if (obj.type === 'content_block_delta') {
319
+ const delta = obj.delta;
320
+ if (delta && delta.type === 'text_delta' && typeof delta.text === 'string') {
321
+ completion += delta.text;
322
+ }
323
+ }
324
+ }
325
+ catch {
326
+ // skip unparseable lines
327
+ }
328
+ }
329
+ }
330
+ else {
331
+ // OpenAI / Grok / Kimi SSE format
332
+ for (const line of lines) {
333
+ if (!line.startsWith('data: '))
334
+ continue;
335
+ const data = line.slice(6).trim();
336
+ if (data === '[DONE]')
337
+ continue;
338
+ try {
339
+ const obj = JSON.parse(data);
340
+ const choices = obj.choices;
341
+ if (Array.isArray(choices) && choices.length > 0) {
342
+ const first = choices[0];
343
+ if (first.delta && typeof first.delta === 'object') {
344
+ completion += String(first.delta.content ?? '');
345
+ }
346
+ }
347
+ }
348
+ catch {
349
+ // skip unparseable lines
350
+ }
351
+ }
352
+ }
353
+ return completion;
354
+ }
355
+ /** Extract usage from buffered raw SSE text */
356
+ function extractStreamUsage(provider, rawSSE) {
357
+ const lines = rawSSE.split('\n');
358
+ // Walk backwards to find usage in the final events
359
+ for (let i = lines.length - 1; i >= 0; i--) {
360
+ const line = lines[i];
361
+ if (!line.startsWith('data: '))
362
+ continue;
363
+ const data = line.slice(6).trim();
364
+ if (data === '[DONE]')
365
+ continue;
366
+ try {
367
+ const obj = JSON.parse(data);
368
+ // OpenAI / Grok / Kimi: usage in the final chunk
369
+ const usage = extractUsage(provider, obj);
370
+ if (usage)
371
+ return usage;
372
+ // Anthropic: usage in message_delta event
373
+ if (obj.type === 'message_delta') {
374
+ const u = obj.usage;
375
+ if (u)
376
+ return extractUsage('anthropic', { usage: u });
377
+ }
378
+ // Anthropic: usage in message_start event
379
+ if (obj.type === 'message_start') {
380
+ const msg = obj.message;
381
+ if (msg?.usage)
382
+ return extractUsage('anthropic', { usage: msg.usage });
383
+ }
384
+ }
385
+ catch { /* skip */ }
386
+ }
387
+ return undefined;
388
+ }
389
+ /** Build a minimal non-streaming JSON response body from a completion string (for replay) */
390
+ function synthesizeCompletionJSON(provider, completion) {
391
+ if (provider === 'gemini') {
392
+ return {
393
+ candidates: [{ content: { parts: [{ text: completion }], role: 'model' }, finishReason: 'STOP' }],
394
+ };
395
+ }
396
+ if (provider === 'anthropic') {
397
+ return {
398
+ id: 'replay',
399
+ type: 'message',
400
+ role: 'assistant',
401
+ content: [{ type: 'text', text: completion }],
402
+ stop_reason: 'end_turn',
403
+ stop_sequence: null,
404
+ };
405
+ }
406
+ // OpenAI / Grok / Kimi format
407
+ return {
408
+ id: 'replay',
409
+ object: 'chat.completion',
410
+ choices: [{ index: 0, message: { role: 'assistant', content: completion }, finish_reason: 'stop' }],
411
+ };
412
+ }
413
+ /** Build a minimal SSE/NDJSON ReadableStream from a completion string (for replay) */
414
+ function synthesizeSSEStream(provider, completion) {
415
+ const encoder = new TextEncoder();
416
+ return new ReadableStream({
417
+ start(ctrl) {
418
+ if (provider === 'gemini') {
419
+ const chunk = `[{"candidates":[{"content":{"parts":[{"text":${JSON.stringify(completion)}}],"role":"model"},"finishReason":"STOP"}]}]\n`;
420
+ ctrl.enqueue(encoder.encode(chunk));
421
+ }
422
+ else if (provider === 'anthropic') {
423
+ const msgStart = `event: message_start\ndata: ${JSON.stringify({ type: 'message_start', message: { id: 'replay', type: 'message', role: 'assistant', content: [], stop_reason: null, stop_sequence: null } })}\n\n`;
424
+ const blockStart = `event: content_block_start\ndata: ${JSON.stringify({ type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } })}\n\n`;
425
+ const delta = `event: content_block_delta\ndata: ${JSON.stringify({ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: completion } })}\n\n`;
426
+ const blockStop = `event: content_block_stop\ndata: ${JSON.stringify({ type: 'content_block_stop', index: 0 })}\n\n`;
427
+ const msgDelta = `event: message_delta\ndata: ${JSON.stringify({ type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null } })}\n\n`;
428
+ const msgStop = `event: message_stop\ndata: ${JSON.stringify({ type: 'message_stop' })}\n\n`;
429
+ ctrl.enqueue(encoder.encode(msgStart));
430
+ ctrl.enqueue(encoder.encode(blockStart));
431
+ ctrl.enqueue(encoder.encode(delta));
432
+ ctrl.enqueue(encoder.encode(blockStop));
433
+ ctrl.enqueue(encoder.encode(msgDelta));
434
+ ctrl.enqueue(encoder.encode(msgStop));
435
+ }
436
+ else {
437
+ const frame1 = `data: ${JSON.stringify({ id: 'replay', choices: [{ delta: { content: completion }, index: 0, finish_reason: null }] })}\n\n`;
438
+ const frame2 = 'data: [DONE]\n\n';
439
+ ctrl.enqueue(encoder.encode(frame1));
440
+ ctrl.enqueue(encoder.encode(frame2));
441
+ }
442
+ ctrl.close();
443
+ },
444
+ });
445
+ }
446
+ // Keep a reference to the original fetch so we can restore it
447
+ let originalFetch = null;
448
+ /**
449
+ * Install the AI fetch interceptor. Wraps globalThis.fetch to automatically
450
+ * record LLM steps into the active trace for OpenAI, Gemini, and Grok calls.
451
+ */
452
+ export function installAIInterceptor() {
453
+ if (originalFetch)
454
+ return; // already installed
455
+ originalFetch = globalThis.fetch;
456
+ globalThis.fetch = async function patchedFetch(input, init) {
457
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
458
+ const provider = detectProvider(url);
459
+ // Skip recording when inside a wrapAI call to avoid duplicate events,
460
+ // but capture the actual HTTP request body and response usage so wrapAI
461
+ // can attach them to its event.
462
+ if (provider && isAIWrapperActive()) {
463
+ let capturedReq;
464
+ let capturedModel = 'unknown';
465
+ let capturedMessages;
466
+ let capturedSnippet;
467
+ try {
468
+ const rawBody = init?.body;
469
+ if (rawBody && typeof rawBody === 'string') {
470
+ capturedReq = JSON.parse(rawBody);
471
+ capturedModel = extractModel(provider, capturedReq, url);
472
+ capturedMessages = Array.isArray(capturedReq.messages) ? capturedReq.messages : Array.isArray(capturedReq.contents) ? capturedReq.contents : undefined;
473
+ capturedSnippet = extractPromptSnippet(capturedReq);
474
+ }
475
+ }
476
+ catch {
477
+ // Ignore parse errors
478
+ }
479
+ const response = await originalFetch(input, init);
480
+ // Extract usage from the response (clone to avoid consuming the body)
481
+ if (capturedReq) {
482
+ const captured = {
483
+ url, provider, model: capturedModel, messages: capturedMessages,
484
+ body: capturedReq, promptSnippet: capturedSnippet,
485
+ };
486
+ const isStreaming = capturedReq.stream === true;
487
+ try {
488
+ const cloned = response.clone();
489
+ if (!isStreaming) {
490
+ // Non-streaming: parse JSON response for usage
491
+ const responseBody = await cloned.json();
492
+ captured.usage = extractUsage(provider, responseBody);
493
+ }
494
+ else if (cloned.body) {
495
+ // Streaming: read the raw SSE text to extract usage from final events
496
+ try {
497
+ const decoder = new TextDecoder();
498
+ const reader = cloned.body.getReader();
499
+ let rawSSE = '';
500
+ for (;;) {
501
+ const { done, value } = await reader.read();
502
+ if (done)
503
+ break;
504
+ rawSSE += decoder.decode(value, { stream: true });
505
+ }
506
+ reader.releaseLock();
507
+ captured.usage = extractStreamUsage(provider, rawSSE);
508
+ }
509
+ catch { /* stream read failed */ }
510
+ }
511
+ }
512
+ catch {
513
+ // Response body not available — usage won't be captured
514
+ }
515
+ ;
516
+ globalThis[LLM_REQUEST_KEY] = captured;
517
+ }
518
+ return response;
519
+ }
520
+ const traceAtCall = getCurrentTrace();
521
+ const obsCtx = getObservabilityContext();
522
+ const httpCtx = getHttpRunContext();
523
+ // No match or no active context: pass through unchanged
524
+ if (!provider || (!traceAtCall && !obsCtx && !httpCtx)) {
525
+ return originalFetch(input, init);
526
+ }
527
+ // Parse request body to extract model and prompt
528
+ let model = 'unknown';
529
+ let prompt = '';
530
+ let isStreaming = false;
531
+ let messages;
532
+ try {
533
+ const rawBody = init?.body;
534
+ if (rawBody && typeof rawBody === 'string') {
535
+ const body = JSON.parse(rawBody);
536
+ model = extractModel(provider, body, url);
537
+ prompt = extractPrompt(provider, body);
538
+ isStreaming = body.stream === true;
539
+ // Capture full messages array for rich display in the dashboard
540
+ if (Array.isArray(body.messages))
541
+ messages = body.messages;
542
+ else if (Array.isArray(body.contents))
543
+ messages = body.contents; // Gemini
544
+ }
545
+ }
546
+ catch {
547
+ // Ignore parse errors — still pass through
548
+ }
549
+ const ctx = getCaptureContext();
550
+ // Observability-only mode: no trace handle, no capture context — record via pushTelemetryEvent
551
+ // Skip when inside a wrapAI call to avoid duplicate events (wrapAI records its own)
552
+ if (!traceAtCall && !ctx && obsCtx && !isAIWrapperActive()) {
553
+ const id = obsCtx.nextId();
554
+ const start = rawDateNow();
555
+ const eventInput = { url, provider, model, prompt, messages };
556
+ const response = await originalFetch(input, init);
557
+ if (isStreaming && response.body) {
558
+ const [streamForCaller, streamForRecorder] = response.body.tee();
559
+ bufferSSEStream(provider, streamForRecorder).then((completion) => {
560
+ const durationMs = rawDateNow() - start;
561
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: { streamed: true, completion }, timestamp: start, durationMs });
562
+ }).catch(() => {
563
+ const durationMs = rawDateNow() - start;
564
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs });
565
+ });
566
+ return new Response(streamForCaller, { status: response.status, statusText: response.statusText, headers: response.headers });
567
+ }
568
+ try {
569
+ const cloned = response.clone();
570
+ const responseBody = await cloned.json();
571
+ const completion = extractCompletion(provider, responseBody);
572
+ const usage = extractUsage(provider, responseBody);
573
+ const durationMs = rawDateNow() - start;
574
+ const event = {
575
+ id, type: 'ai', name: model, input: eventInput, output: { completion },
576
+ timestamp: start, durationMs,
577
+ ...(usage ? { usage } : {}),
578
+ };
579
+ pushTelemetryEvent(event);
580
+ }
581
+ catch {
582
+ const durationMs = rawDateNow() - start;
583
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, timestamp: start, durationMs });
584
+ }
585
+ return response;
586
+ }
587
+ // HTTP mode (no capture context): replay frozen AI events or execute live + push telemetry
588
+ // Skip when inside a wrapAI call to avoid duplicate events (wrapAI records its own)
589
+ if (!ctx && httpCtx && !isAIWrapperActive()) {
590
+ const id = httpCtx.nextId();
591
+ const eventInput = { url, provider, model, prompt, messages };
592
+ // Replay frozen step
593
+ const frozen = getHttpFrozenEvent(id);
594
+ if (frozen && frozen.type === 'ai') {
595
+ pushTelemetryEvent(frozen);
596
+ const frozenOutput = frozen.output;
597
+ const completion = frozenOutput ? extractCompletion(provider, frozenOutput) : '(replayed)';
598
+ if (isStreaming) {
599
+ return new Response(synthesizeSSEStream(provider, completion), {
600
+ status: 200,
601
+ headers: { 'Content-Type': provider === 'gemini' ? 'application/json' : 'text/event-stream' },
602
+ });
603
+ }
604
+ const body = frozenOutput?.streamed === true
605
+ ? synthesizeCompletionJSON(provider, completion)
606
+ : (frozenOutput ?? synthesizeCompletionJSON(provider, completion));
607
+ return new Response(JSON.stringify(body), {
608
+ status: 200,
609
+ headers: { 'Content-Type': 'application/json' },
610
+ });
611
+ }
612
+ // Not frozen → execute live, push telemetry
613
+ const start = rawDateNow();
614
+ const response = await originalFetch(input, init);
615
+ if (isStreaming && response.body) {
616
+ const [streamForCaller, streamForRecorder] = response.body.tee();
617
+ bufferSSEStream(provider, streamForRecorder).then((completion) => {
618
+ const durationMs = rawDateNow() - start;
619
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: { streamed: true, completion }, timestamp: start, durationMs });
620
+ }).catch(() => {
621
+ const durationMs = rawDateNow() - start;
622
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs });
623
+ });
624
+ return new Response(streamForCaller, { status: response.status, statusText: response.statusText, headers: response.headers });
625
+ }
626
+ try {
627
+ const cloned = response.clone();
628
+ const responseBody = await cloned.json();
629
+ const completion = extractCompletion(provider, responseBody);
630
+ const usage = extractUsage(provider, responseBody);
631
+ const durationMs = rawDateNow() - start;
632
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: { completion }, timestamp: start, durationMs, ...(usage ? { usage } : {}) });
633
+ }
634
+ catch {
635
+ const durationMs = rawDateNow() - start;
636
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, timestamp: start, durationMs });
637
+ }
638
+ return response;
639
+ }
640
+ if (ctx && traceAtCall) {
641
+ const { recorder, replay } = ctx;
642
+ const id = recorder.nextId();
643
+ const start = rawDateNow();
644
+ // Replay mode: return the historical response without making a real call
645
+ if (replay.shouldReplay(id)) {
646
+ const historicalEvent = replay.getRecordedEvent(id);
647
+ const historicalInput = historicalEvent?.input;
648
+ const historicalUrl = typeof historicalInput?.url === 'string' ? historicalInput.url : undefined;
649
+ const historicalProvider = typeof historicalInput?.provider === 'string' ? historicalInput.provider : undefined;
650
+ const isReplayMatch = !!historicalEvent
651
+ && historicalEvent.type === 'ai'
652
+ && historicalProvider === provider
653
+ && historicalUrl === url;
654
+ if (isReplayMatch && historicalEvent) {
655
+ recorder.record(historicalEvent);
656
+ const historicalOutput = historicalEvent.output;
657
+ const completion = historicalOutput ? extractCompletion(provider, historicalOutput) : '(replayed)';
658
+ traceAtCall.recordLLMStep({ model, provider, prompt, completion, workflowEventId: id });
659
+ if (isStreaming) {
660
+ // Current caller expects a streaming response — always synthesize SSE
661
+ return new Response(synthesizeSSEStream(provider, completion), {
662
+ status: 200,
663
+ headers: { 'Content-Type': provider === 'gemini' ? 'application/json' : 'text/event-stream' },
664
+ });
665
+ }
666
+ if (historicalOutput?.streamed === true) {
667
+ // Original was streamed but caller now expects JSON — synthesize a completion response
668
+ return new Response(JSON.stringify(synthesizeCompletionJSON(provider, completion)), {
669
+ status: 200,
670
+ headers: { 'Content-Type': 'application/json' },
671
+ });
672
+ }
673
+ return new Response(historicalOutput != null ? JSON.stringify(historicalOutput) : null, { status: 200, headers: { 'Content-Type': 'application/json' } });
674
+ }
675
+ // No historical event found — fall through to fresh execution
676
+ }
677
+ // Fresh execution: make the real call and record to both systems
678
+ const response = await originalFetch(input, init);
679
+ const durationMs = rawDateNow() - start;
680
+ if (isStreaming) {
681
+ if (response.body) {
682
+ const [streamForCaller, streamForRecorder] = response.body.tee();
683
+ recorder.trackAsync(bufferSSEStream(provider, streamForRecorder).then((completion) => {
684
+ const durationMs = rawDateNow() - start;
685
+ traceAtCall.recordLLMStep({ model, provider, prompt, completion, workflowEventId: id, durationMs });
686
+ recorder.record({ id, type: 'ai', name: model, input: { url, provider, model, prompt, messages }, output: { streamed: true, completion }, timestamp: start, durationMs });
687
+ }).catch(() => {
688
+ const durationMs = rawDateNow() - start;
689
+ traceAtCall.recordLLMStep({ model, provider, prompt, completion: '(streamed-error)', workflowEventId: id, durationMs });
690
+ recorder.record({ id, type: 'ai', name: model, input: { url, provider, model, prompt, messages }, output: null, timestamp: start, durationMs });
691
+ }));
692
+ return new Response(streamForCaller, {
693
+ status: response.status,
694
+ statusText: response.statusText,
695
+ headers: response.headers,
696
+ });
697
+ }
698
+ else {
699
+ traceAtCall.recordLLMStep({ model, provider, prompt, completion: '(streamed)', workflowEventId: id, durationMs });
700
+ recorder.record({ id, type: 'ai', name: model, input: { url, provider, model, prompt, messages }, output: null, timestamp: start, durationMs });
701
+ }
702
+ }
703
+ else {
704
+ try {
705
+ const cloned = response.clone();
706
+ const responseBody = await cloned.json();
707
+ const completion = extractCompletion(provider, responseBody);
708
+ const usage = extractUsage(provider, responseBody);
709
+ const assistantMessage = extractAssistantMessage(provider, responseBody);
710
+ traceAtCall.recordLLMStep({ model, provider, prompt, completion, workflowEventId: id, durationMs });
711
+ recorder.record({ id, type: 'ai', name: model, input: { url, provider, model, prompt, messages }, output: assistantMessage ?? responseBody, timestamp: start, durationMs, usage });
712
+ }
713
+ catch {
714
+ traceAtCall.recordLLMStep({ model, provider, prompt, completion: '', workflowEventId: id, durationMs });
715
+ recorder.record({ id, type: 'ai', name: model, input: { url, provider, model, prompt, messages }, output: null, timestamp: start, durationMs });
716
+ }
717
+ }
718
+ return response;
719
+ }
720
+ // No capture context — original behaviour (trace handle only, outside of a workflow run)
721
+ if (!traceAtCall)
722
+ return originalFetch(input, init);
723
+ const response = await originalFetch(input, init);
724
+ if (isStreaming && response.body) {
725
+ const [streamForCaller, streamForRecorder] = response.body.tee();
726
+ bufferSSEStream(provider, streamForRecorder).then((completion) => {
727
+ traceAtCall.recordLLMStep({ model, provider, prompt, completion });
728
+ }).catch(() => {
729
+ traceAtCall.recordLLMStep({ model, provider, prompt, completion: '(streamed-error)' });
730
+ });
731
+ return new Response(streamForCaller, { status: response.status, statusText: response.statusText, headers: response.headers });
732
+ }
733
+ else if (!isStreaming) {
734
+ try {
735
+ const cloned = response.clone();
736
+ const responseBody = await cloned.json();
737
+ const completion = extractCompletion(provider, responseBody);
738
+ traceAtCall.recordLLMStep({ model, provider, prompt, completion });
739
+ }
740
+ catch {
741
+ traceAtCall.recordLLMStep({ model, provider, prompt, completion: '' });
742
+ }
743
+ }
744
+ return response;
745
+ };
746
+ }
747
+ /**
748
+ * Uninstall the AI fetch interceptor, restoring globalThis.fetch to its original value.
749
+ */
750
+ export function uninstallAIInterceptor() {
751
+ if (originalFetch) {
752
+ globalThis.fetch = originalFetch;
753
+ originalFetch = null;
754
+ }
755
+ }
756
+ //# sourceMappingURL=ai-interceptor.js.map