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.
- package/LICENSE +21 -0
- package/README.md +775 -0
- package/dist/browser-ui.d.ts +43 -0
- package/dist/browser-ui.d.ts.map +1 -0
- package/dist/browser-ui.js +246 -0
- package/dist/browser-ui.js.map +1 -0
- package/dist/capture/event.d.ts +33 -0
- package/dist/capture/event.d.ts.map +1 -0
- package/dist/capture/event.js +2 -0
- package/dist/capture/event.js.map +1 -0
- package/dist/capture/index.d.ts +4 -0
- package/dist/capture/index.d.ts.map +1 -0
- package/dist/capture/index.js +4 -0
- package/dist/capture/index.js.map +1 -0
- package/dist/capture/recorder.d.ts +24 -0
- package/dist/capture/recorder.d.ts.map +1 -0
- package/dist/capture/recorder.js +46 -0
- package/dist/capture/recorder.js.map +1 -0
- package/dist/capture/replay.d.ts +20 -0
- package/dist/capture/replay.d.ts.map +1 -0
- package/dist/capture/replay.js +47 -0
- package/dist/capture/replay.js.map +1 -0
- package/dist/ci/api-client.d.ts +38 -0
- package/dist/ci/api-client.d.ts.map +1 -0
- package/dist/ci/api-client.js +96 -0
- package/dist/ci/api-client.js.map +1 -0
- package/dist/ci/benchmark.d.ts +33 -0
- package/dist/ci/benchmark.d.ts.map +1 -0
- package/dist/ci/benchmark.js +213 -0
- package/dist/ci/benchmark.js.map +1 -0
- package/dist/ci/ed-runner.d.ts +48 -0
- package/dist/ci/ed-runner.d.ts.map +1 -0
- package/dist/ci/ed-runner.js +260 -0
- package/dist/ci/ed-runner.js.map +1 -0
- package/dist/ci/executor.d.ts +13 -0
- package/dist/ci/executor.d.ts.map +1 -0
- package/dist/ci/executor.js +542 -0
- package/dist/ci/executor.js.map +1 -0
- package/dist/ci/git-info.d.ts +17 -0
- package/dist/ci/git-info.d.ts.map +1 -0
- package/dist/ci/git-info.js +102 -0
- package/dist/ci/git-info.js.map +1 -0
- package/dist/ci/index.d.ts +6 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +4 -0
- package/dist/ci/index.js.map +1 -0
- package/dist/ci/measurement.d.ts +9 -0
- package/dist/ci/measurement.d.ts.map +1 -0
- package/dist/ci/measurement.js +15 -0
- package/dist/ci/measurement.js.map +1 -0
- package/dist/ci/replay.d.ts +31 -0
- package/dist/ci/replay.d.ts.map +1 -0
- package/dist/ci/replay.js +96 -0
- package/dist/ci/replay.js.map +1 -0
- package/dist/ci/reporters/default.d.ts +8 -0
- package/dist/ci/reporters/default.d.ts.map +1 -0
- package/dist/ci/reporters/default.js +46 -0
- package/dist/ci/reporters/default.js.map +1 -0
- package/dist/ci/reporters/index.d.ts +8 -0
- package/dist/ci/reporters/index.d.ts.map +1 -0
- package/dist/ci/reporters/index.js +14 -0
- package/dist/ci/reporters/index.js.map +1 -0
- package/dist/ci/reporters/json.d.ts +8 -0
- package/dist/ci/reporters/json.d.ts.map +1 -0
- package/dist/ci/reporters/json.js +14 -0
- package/dist/ci/reporters/json.js.map +1 -0
- package/dist/ci/reporters/junit.d.ts +8 -0
- package/dist/ci/reporters/junit.d.ts.map +1 -0
- package/dist/ci/reporters/junit.js +48 -0
- package/dist/ci/reporters/junit.js.map +1 -0
- package/dist/ci/runner.d.ts +3 -0
- package/dist/ci/runner.d.ts.map +1 -0
- package/dist/ci/runner.js +187 -0
- package/dist/ci/runner.js.map +1 -0
- package/dist/ci/test-discovery.d.ts +5 -0
- package/dist/ci/test-discovery.d.ts.map +1 -0
- package/dist/ci/test-discovery.js +11 -0
- package/dist/ci/test-discovery.js.map +1 -0
- package/dist/ci/test-loader.d.ts +19 -0
- package/dist/ci/test-loader.d.ts.map +1 -0
- package/dist/ci/test-loader.js +149 -0
- package/dist/ci/test-loader.js.map +1 -0
- package/dist/ci/test-registry.d.ts +42 -0
- package/dist/ci/test-registry.d.ts.map +1 -0
- package/dist/ci/test-registry.js +18 -0
- package/dist/ci/test-registry.js.map +1 -0
- package/dist/ci/trace-schema.d.ts +30 -0
- package/dist/ci/trace-schema.d.ts.map +1 -0
- package/dist/ci/trace-schema.js +66 -0
- package/dist/ci/trace-schema.js.map +1 -0
- package/dist/ci/trace-writer.d.ts +16 -0
- package/dist/ci/trace-writer.d.ts.map +1 -0
- package/dist/ci/trace-writer.js +108 -0
- package/dist/ci/trace-writer.js.map +1 -0
- package/dist/ci/types.d.ts +108 -0
- package/dist/ci/types.d.ts.map +1 -0
- package/dist/ci/types.js +3 -0
- package/dist/ci/types.js.map +1 -0
- package/dist/ci/upload-client.d.ts +74 -0
- package/dist/ci/upload-client.d.ts.map +1 -0
- package/dist/ci/upload-client.js +195 -0
- package/dist/ci/upload-client.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +716 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/agent-state.d.ts +47 -0
- package/dist/core/agent-state.d.ts.map +1 -0
- package/dist/core/agent-state.js +137 -0
- package/dist/core/agent-state.js.map +1 -0
- package/dist/core/judge-utils.d.ts +22 -0
- package/dist/core/judge-utils.d.ts.map +1 -0
- package/dist/core/judge-utils.js +211 -0
- package/dist/core/judge-utils.js.map +1 -0
- package/dist/core/registry.d.ts +28 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +52 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/dashboard-server.d.ts +65 -0
- package/dist/dashboard-server.d.ts.map +1 -0
- package/dist/dashboard-server.js +3940 -0
- package/dist/dashboard-server.js.map +1 -0
- package/dist/execution/tool-runner.d.ts +26 -0
- package/dist/execution/tool-runner.d.ts.map +1 -0
- package/dist/execution/tool-runner.js +316 -0
- package/dist/execution/tool-runner.js.map +1 -0
- package/dist/html/dashboard.html +2218 -0
- package/dist/http.d.ts +14 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +13 -0
- package/dist/http.js.map +1 -0
- package/dist/index.cjs +8102 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptors/ai-interceptor.d.ts +26 -0
- package/dist/interceptors/ai-interceptor.d.ts.map +1 -0
- package/dist/interceptors/ai-interceptor.js +756 -0
- package/dist/interceptors/ai-interceptor.js.map +1 -0
- package/dist/interceptors/db-auto.d.ts +8 -0
- package/dist/interceptors/db-auto.d.ts.map +1 -0
- package/dist/interceptors/db-auto.js +217 -0
- package/dist/interceptors/db-auto.js.map +1 -0
- package/dist/interceptors/db.d.ts +23 -0
- package/dist/interceptors/db.d.ts.map +1 -0
- package/dist/interceptors/db.js +137 -0
- package/dist/interceptors/db.js.map +1 -0
- package/dist/interceptors/http.d.ts +28 -0
- package/dist/interceptors/http.d.ts.map +1 -0
- package/dist/interceptors/http.js +356 -0
- package/dist/interceptors/http.js.map +1 -0
- package/dist/interceptors/side-effects.d.ts +7 -0
- package/dist/interceptors/side-effects.d.ts.map +1 -0
- package/dist/interceptors/side-effects.js +72 -0
- package/dist/interceptors/side-effects.js.map +1 -0
- package/dist/interceptors/telemetry-push.d.ts +142 -0
- package/dist/interceptors/telemetry-push.d.ts.map +1 -0
- package/dist/interceptors/telemetry-push.js +463 -0
- package/dist/interceptors/telemetry-push.js.map +1 -0
- package/dist/interceptors/tool.d.ts +2 -0
- package/dist/interceptors/tool.d.ts.map +1 -0
- package/dist/interceptors/tool.js +274 -0
- package/dist/interceptors/tool.js.map +1 -0
- package/dist/interceptors/workflow-ai.d.ts +5 -0
- package/dist/interceptors/workflow-ai.d.ts.map +1 -0
- package/dist/interceptors/workflow-ai.js +382 -0
- package/dist/interceptors/workflow-ai.js.map +1 -0
- package/dist/internals/conditional-recorder.d.ts +21 -0
- package/dist/internals/conditional-recorder.d.ts.map +1 -0
- package/dist/internals/conditional-recorder.js +54 -0
- package/dist/internals/conditional-recorder.js.map +1 -0
- package/dist/internals/mock-resolver.d.ts +146 -0
- package/dist/internals/mock-resolver.d.ts.map +1 -0
- package/dist/internals/mock-resolver.js +427 -0
- package/dist/internals/mock-resolver.js.map +1 -0
- package/dist/matchers/index.d.ts +96 -0
- package/dist/matchers/index.d.ts.map +1 -0
- package/dist/matchers/index.js +668 -0
- package/dist/matchers/index.js.map +1 -0
- package/dist/observability.d.ts +82 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +471 -0
- package/dist/observability.js.map +1 -0
- package/dist/portal-executor.d.ts +30 -0
- package/dist/portal-executor.d.ts.map +1 -0
- package/dist/portal-executor.js +324 -0
- package/dist/portal-executor.js.map +1 -0
- package/dist/portal-server.d.ts +3 -0
- package/dist/portal-server.d.ts.map +1 -0
- package/dist/portal-server.js +279 -0
- package/dist/portal-server.js.map +1 -0
- package/dist/proxy/llm-capture.d.ts +14 -0
- package/dist/proxy/llm-capture.d.ts.map +1 -0
- package/dist/proxy/llm-capture.js +264 -0
- package/dist/proxy/llm-capture.js.map +1 -0
- package/dist/reporter.d.ts +3 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +72 -0
- package/dist/reporter.js.map +1 -0
- package/dist/runWorkflowSubprocess.d.ts +14 -0
- package/dist/runWorkflowSubprocess.d.ts.map +1 -0
- package/dist/runWorkflowSubprocess.js +66 -0
- package/dist/runWorkflowSubprocess.js.map +1 -0
- package/dist/runner.d.ts +16 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +138 -0
- package/dist/runner.js.map +1 -0
- package/dist/socket-connector.d.ts +22 -0
- package/dist/socket-connector.d.ts.map +1 -0
- package/dist/socket-connector.js +104 -0
- package/dist/socket-connector.js.map +1 -0
- package/dist/telemetry-batcher.d.ts +56 -0
- package/dist/telemetry-batcher.d.ts.map +1 -0
- package/dist/telemetry-batcher.js +143 -0
- package/dist/telemetry-batcher.js.map +1 -0
- package/dist/test-setup.d.ts +12 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +13 -0
- package/dist/test-setup.js.map +1 -0
- package/dist/tool-registry.d.ts +31 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +73 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/tool-runner-worker.d.ts +2 -0
- package/dist/tool-runner-worker.d.ts.map +1 -0
- package/dist/tool-runner-worker.js +215 -0
- package/dist/tool-runner-worker.js.map +1 -0
- package/dist/trace-adapter/context.d.ts +72 -0
- package/dist/trace-adapter/context.d.ts.map +1 -0
- package/dist/trace-adapter/context.js +80 -0
- package/dist/trace-adapter/context.js.map +1 -0
- package/dist/tracing.d.ts +2 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/tracing.js +59 -0
- package/dist/tracing.js.map +1 -0
- package/dist/trigger-executor.d.ts +12 -0
- package/dist/trigger-executor.d.ts.map +1 -0
- package/dist/trigger-executor.js +130 -0
- package/dist/trigger-executor.js.map +1 -0
- package/dist/types/portal.d.ts +76 -0
- package/dist/types/portal.d.ts.map +1 -0
- package/dist/types/portal.js +2 -0
- package/dist/types/portal.js.map +1 -0
- package/dist/utils/debug.d.ts +3 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/debug.js +8 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/license-error.d.ts +23 -0
- package/dist/utils/license-error.d.ts.map +1 -0
- package/dist/utils/license-error.js +42 -0
- package/dist/utils/license-error.js.map +1 -0
- package/dist/utils/redact.d.ts +7 -0
- package/dist/utils/redact.d.ts.map +1 -0
- package/dist/utils/redact.js +26 -0
- package/dist/utils/redact.js.map +1 -0
- package/dist/workflow-runner-worker.d.ts +2 -0
- package/dist/workflow-runner-worker.d.ts.map +1 -0
- package/dist/workflow-runner-worker.js +329 -0
- package/dist/workflow-runner-worker.js.map +1 -0
- package/dist/workflow-runner.d.ts +14 -0
- package/dist/workflow-runner.d.ts.map +1 -0
- package/dist/workflow-runner.js +34 -0
- package/dist/workflow-runner.js.map +1 -0
- package/docs/agent-coding-instructions.md +138 -0
- package/docs/agent-integration-guide.md +564 -0
- package/docs/agents.md +140 -0
- package/docs/dashboard.md +394 -0
- package/docs/deno.md +69 -0
- package/docs/instrumentation.md +424 -0
- package/docs/langfuse-trace-structure.md +145 -0
- package/docs/matchers.md +173 -0
- package/docs/observability_contract.md +192 -0
- package/docs/observability_mode.md +195 -0
- package/docs/quickstart.md +621 -0
- package/docs/security-compliance.md +566 -0
- package/docs/test-writing-guidelines.md +444 -0
- package/docs/tools.md +165 -0
- package/docs/workflow-modes.md +253 -0
- package/package.json +76 -0
- package/src/browser-ui.ts +281 -0
- package/src/capture/event.ts +30 -0
- package/src/capture/index.ts +3 -0
- package/src/capture/recorder.ts +62 -0
- package/src/capture/replay.ts +55 -0
- package/src/ci/api-client.ts +136 -0
- package/src/ci/benchmark.ts +257 -0
- package/src/ci/ed-runner.ts +351 -0
- package/src/ci/executor.ts +671 -0
- package/src/ci/git-info.ts +127 -0
- package/src/ci/index.ts +5 -0
- package/src/ci/measurement.ts +25 -0
- package/src/ci/replay.ts +127 -0
- package/src/ci/reporters/default.ts +50 -0
- package/src/ci/reporters/index.ts +21 -0
- package/src/ci/reporters/json.ts +18 -0
- package/src/ci/reporters/junit.ts +61 -0
- package/src/ci/runner.ts +208 -0
- package/src/ci/test-discovery.ts +16 -0
- package/src/ci/test-loader.ts +187 -0
- package/src/ci/test-registry.ts +62 -0
- package/src/ci/trace-schema.ts +96 -0
- package/src/ci/trace-writer.ts +107 -0
- package/src/ci/types.ts +115 -0
- package/src/ci/upload-client.ts +300 -0
- package/src/cli.ts +811 -0
- package/src/core/agent-state.ts +162 -0
- package/src/core/judge-utils.ts +232 -0
- package/src/core/registry.ts +92 -0
- package/src/dashboard-server.ts +2047 -0
- package/src/execution/tool-runner.ts +352 -0
- package/src/html/dashboard.html +2218 -0
- package/src/http.ts +13 -0
- package/src/index.ts +138 -0
- package/src/interceptors/ai-interceptor.ts +798 -0
- package/src/interceptors/db-auto.ts +243 -0
- package/src/interceptors/db.ts +156 -0
- package/src/interceptors/http.ts +393 -0
- package/src/interceptors/side-effects.ts +83 -0
- package/src/interceptors/telemetry-push.ts +537 -0
- package/src/interceptors/tool.ts +287 -0
- package/src/interceptors/workflow-ai.ts +419 -0
- package/src/internals/conditional-recorder.ts +63 -0
- package/src/internals/mock-resolver.ts +492 -0
- package/src/matchers/index.ts +824 -0
- package/src/observability.ts +501 -0
- package/src/portal-executor.ts +355 -0
- package/src/portal-server.ts +304 -0
- package/src/proxy/llm-capture.ts +301 -0
- package/src/reporter.ts +81 -0
- package/src/runWorkflowSubprocess.ts +74 -0
- package/src/runner.ts +178 -0
- package/src/socket-connector.ts +117 -0
- package/src/telemetry-batcher.ts +191 -0
- package/src/test-setup.ts +16 -0
- package/src/tool-registry.ts +94 -0
- package/src/tool-runner-worker.ts +244 -0
- package/src/trace-adapter/context.ts +156 -0
- package/src/tracing.ts +62 -0
- package/src/trigger-executor.ts +171 -0
- package/src/types/agent.d.ts +63 -0
- package/src/types/expect.d.ts +81 -0
- package/src/types/modules.d.ts +2 -0
- package/src/types/portal.ts +69 -0
- package/src/utils/debug.ts +8 -0
- package/src/utils/license-error.ts +43 -0
- package/src/utils/redact.ts +25 -0
- package/src/workflow-runner-worker.ts +386 -0
- 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
|