elasticdash-test 0.1.18-alpha-21 → 0.1.18-alpha-23
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/package.json
CHANGED
|
@@ -14,6 +14,55 @@ function isAIWrapperActive(): boolean {
|
|
|
14
14
|
return ((globalThis as Record<string, unknown>)[AI_WRAPPER_KEY] as number ?? 0) > 0
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* When inside a wrapAI call, the ai-interceptor captures the actual HTTP
|
|
19
|
+
* request payload and stashes it here so wrapAI can attach it to its event.
|
|
20
|
+
*/
|
|
21
|
+
const LLM_REQUEST_KEY = '__elasticdash_last_llm_request__'
|
|
22
|
+
export interface CapturedLLMRequest {
|
|
23
|
+
url: string
|
|
24
|
+
provider: string
|
|
25
|
+
model: string
|
|
26
|
+
messages?: unknown[]
|
|
27
|
+
body: Record<string, unknown>
|
|
28
|
+
promptSnippet?: string
|
|
29
|
+
usage?: UsageInfo
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function consumeCapturedLLMRequest(): CapturedLLMRequest | undefined {
|
|
33
|
+
const g = globalThis as Record<string, unknown>
|
|
34
|
+
const req = g[LLM_REQUEST_KEY] as CapturedLLMRequest | undefined
|
|
35
|
+
if (req) g[LLM_REQUEST_KEY] = undefined
|
|
36
|
+
return req
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function extractPromptSnippet(body: Record<string, unknown>): string | undefined {
|
|
40
|
+
let messages: unknown[] | undefined
|
|
41
|
+
if (Array.isArray(body.messages)) messages = body.messages
|
|
42
|
+
else if (Array.isArray(body.contents)) messages = body.contents
|
|
43
|
+
|
|
44
|
+
if (!messages || messages.length === 0) return undefined
|
|
45
|
+
|
|
46
|
+
// Find the last user message
|
|
47
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
48
|
+
const msg = messages[i] as Record<string, unknown> | undefined
|
|
49
|
+
if (!msg) continue
|
|
50
|
+
if (msg.role === 'user') {
|
|
51
|
+
let content = msg.content
|
|
52
|
+
if (Array.isArray(content)) {
|
|
53
|
+
content = content
|
|
54
|
+
.map((b: unknown) => (b && typeof b === 'object' ? String((b as Record<string, unknown>).text ?? '') : String(b)))
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join('')
|
|
57
|
+
}
|
|
58
|
+
if (typeof content === 'string') {
|
|
59
|
+
return content.slice(0, 100)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
17
66
|
type UsageInfo = { inputTokens?: number; outputTokens?: number; totalTokens?: number }
|
|
18
67
|
|
|
19
68
|
function extractUsage(provider: string, body: Record<string, unknown>): UsageInfo | undefined {
|
|
@@ -397,9 +446,45 @@ export function installAIInterceptor(): void {
|
|
|
397
446
|
|
|
398
447
|
const provider = detectProvider(url)
|
|
399
448
|
|
|
400
|
-
// Skip recording when inside a wrapAI call to avoid duplicate events
|
|
449
|
+
// Skip recording when inside a wrapAI call to avoid duplicate events,
|
|
450
|
+
// but capture the actual HTTP request body and response usage so wrapAI
|
|
451
|
+
// can attach them to its event.
|
|
401
452
|
if (provider && isAIWrapperActive()) {
|
|
402
|
-
|
|
453
|
+
let capturedReq: Record<string, unknown> | undefined
|
|
454
|
+
let capturedModel = 'unknown'
|
|
455
|
+
let capturedMessages: unknown[] | undefined
|
|
456
|
+
let capturedSnippet: string | undefined
|
|
457
|
+
try {
|
|
458
|
+
const rawBody = init?.body
|
|
459
|
+
if (rawBody && typeof rawBody === 'string') {
|
|
460
|
+
capturedReq = JSON.parse(rawBody) as Record<string, unknown>
|
|
461
|
+
capturedModel = extractModel(provider, capturedReq, url)
|
|
462
|
+
capturedMessages = Array.isArray(capturedReq.messages) ? capturedReq.messages : Array.isArray(capturedReq.contents) ? capturedReq.contents : undefined
|
|
463
|
+
capturedSnippet = extractPromptSnippet(capturedReq)
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// Ignore parse errors
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const response = await originalFetch!(input, init)
|
|
470
|
+
|
|
471
|
+
// Extract usage from the response (clone to avoid consuming the body)
|
|
472
|
+
if (capturedReq) {
|
|
473
|
+
const captured: CapturedLLMRequest = {
|
|
474
|
+
url, provider, model: capturedModel, messages: capturedMessages,
|
|
475
|
+
body: capturedReq, promptSnippet: capturedSnippet,
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const cloned = response.clone()
|
|
479
|
+
const responseBody = await cloned.json() as Record<string, unknown>
|
|
480
|
+
captured.usage = extractUsage(provider, responseBody)
|
|
481
|
+
} catch {
|
|
482
|
+
// Streaming or non-JSON response — usage not available from response
|
|
483
|
+
}
|
|
484
|
+
;(globalThis as Record<string, unknown>)[LLM_REQUEST_KEY] = captured
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return response
|
|
403
488
|
}
|
|
404
489
|
|
|
405
490
|
const traceAtCall = getCurrentTrace()
|
|
@@ -2,6 +2,7 @@ import { getCaptureContext } from '../capture/recorder.js'
|
|
|
2
2
|
import { rawDateNow } from './side-effects.js'
|
|
3
3
|
import { getHttpRunContext, getHttpFrozenEvent, getHttpPromptMock, getHttpUserPromptMock, getHttpAIMock, pushTelemetryEvent, tryAutoInitHttpContext, getObservabilityContext } from './telemetry-push.js'
|
|
4
4
|
import { resolveAIMock, resolvePromptMock, resolveUserPromptMock } from '../internals/mock-resolver.js'
|
|
5
|
+
import { consumeCapturedLLMRequest } from './ai-interceptor.js'
|
|
5
6
|
import type { WorkflowEvent } from '../capture/event.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -57,6 +58,23 @@ function extractUsage(output: unknown): UsageInfo | undefined {
|
|
|
57
58
|
return undefined
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* After callFn returns, consume any captured LLM request from ai-interceptor
|
|
63
|
+
* and merge it into the event input so the recorded event contains the actual
|
|
64
|
+
* HTTP payload sent to the LLM (messages, model, parameters).
|
|
65
|
+
* Also returns captured usage when the app's output doesn't include it.
|
|
66
|
+
*/
|
|
67
|
+
function enrichFromLLMCapture(input: unknown, appUsage: UsageInfo | undefined): { input: unknown; usage: UsageInfo | undefined } {
|
|
68
|
+
const captured = consumeCapturedLLMRequest()
|
|
69
|
+
if (!captured) return { input, usage: appUsage }
|
|
70
|
+
const enrichedInput = (input && typeof input === 'object')
|
|
71
|
+
? { ...(input as Record<string, unknown>), llmRequest: captured.body, promptSnippet: captured.promptSnippet }
|
|
72
|
+
: { originalInput: input, llmRequest: captured.body, promptSnippet: captured.promptSnippet }
|
|
73
|
+
// Prefer app-level usage if available, fall back to HTTP-level usage
|
|
74
|
+
const usage = appUsage ?? captured.usage
|
|
75
|
+
return { input: enrichedInput, usage }
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
function isReadableStream(v: unknown): v is ReadableStream<Uint8Array> {
|
|
61
79
|
return (
|
|
62
80
|
typeof v === 'object' &&
|
|
@@ -127,10 +145,8 @@ function wrapAsyncIterable<T>(
|
|
|
127
145
|
export function wrapAI<Args extends unknown[], R>(
|
|
128
146
|
modelName: string,
|
|
129
147
|
callFn: (...args: Args) => Promise<R>,
|
|
130
|
-
|
|
148
|
+
_options?: { model?: string; provider?: string },
|
|
131
149
|
): (...args: Args) => Promise<R> {
|
|
132
|
-
const actualModel = options?.model
|
|
133
|
-
const actualProvider = options?.provider
|
|
134
150
|
return async (...args: Args): Promise<R> => {
|
|
135
151
|
await tryAutoInitHttpContext()
|
|
136
152
|
const ctx = getCaptureContext()
|
|
@@ -146,22 +162,19 @@ export function wrapAI<Args extends unknown[], R>(
|
|
|
146
162
|
// Observability-only mode: record and push, no mocks/replay
|
|
147
163
|
if (!ctx && !httpCtx && obsCtx) {
|
|
148
164
|
const id = obsCtx.nextId()
|
|
149
|
-
const
|
|
150
|
-
// Attach actual model/provider so reruns use the correct API model name
|
|
151
|
-
const input = (actualModel || actualProvider)
|
|
152
|
-
? { ...(rawInput && typeof rawInput === 'object' ? rawInput as Record<string, unknown> : { prompt: rawInput }), ...(actualModel ? { model: actualModel } : {}), ...(actualProvider ? { provider: actualProvider } : {}) }
|
|
153
|
-
: rawInput
|
|
165
|
+
const input = args.length === 1 ? args[0] : args
|
|
154
166
|
try {
|
|
155
167
|
const output = await callFn(...args)
|
|
168
|
+
const enriched = enrichFromLLMCapture(input, extractUsage(output))
|
|
156
169
|
|
|
157
170
|
if (isReadableStream(output)) {
|
|
158
171
|
const [streamForCaller, streamForRecorder] = output.tee()
|
|
159
172
|
bufferReadableStream(streamForRecorder).then((rawText) => {
|
|
160
173
|
const durationMs = rawDateNow() - start
|
|
161
|
-
pushTelemetryEvent({ id, type: 'ai', name: modelName, input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs })
|
|
174
|
+
pushTelemetryEvent({ id, type: 'ai', name: modelName, input: enriched.input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs, ...(enriched.usage ? { usage: enriched.usage } : {}) })
|
|
162
175
|
}).catch(() => {
|
|
163
176
|
const durationMs = rawDateNow() - start
|
|
164
|
-
pushTelemetryEvent({ id, type: 'ai', name: modelName, input, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs })
|
|
177
|
+
pushTelemetryEvent({ id, type: 'ai', name: modelName, input: enriched.input, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs, ...(enriched.usage ? { usage: enriched.usage } : {}) })
|
|
165
178
|
})
|
|
166
179
|
return streamForCaller as unknown as R
|
|
167
180
|
}
|
|
@@ -170,23 +183,23 @@ export function wrapAI<Args extends unknown[], R>(
|
|
|
170
183
|
return wrapAsyncIterable(output, (chunks) => {
|
|
171
184
|
const durationMs = rawDateNow() - start
|
|
172
185
|
const rawText = chunks.map((c) => (typeof c === 'string' ? c : JSON.stringify(c))).join('')
|
|
173
|
-
pushTelemetryEvent({ id, type: 'ai', name: modelName, input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs })
|
|
186
|
+
pushTelemetryEvent({ id, type: 'ai', name: modelName, input: enriched.input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs, ...(enriched.usage ? { usage: enriched.usage } : {}) })
|
|
174
187
|
}) as unknown as R
|
|
175
188
|
}
|
|
176
189
|
|
|
177
190
|
const durationMs = rawDateNow() - start
|
|
178
|
-
const usage = extractUsage(output)
|
|
179
191
|
const event: WorkflowEvent = {
|
|
180
|
-
id, type: 'ai', name: modelName, input, output,
|
|
192
|
+
id, type: 'ai', name: modelName, input: enriched.input, output,
|
|
181
193
|
timestamp: start, durationMs,
|
|
182
|
-
...(usage ? { usage } : {}),
|
|
194
|
+
...(enriched.usage ? { usage: enriched.usage } : {}),
|
|
183
195
|
}
|
|
184
196
|
pushTelemetryEvent(event)
|
|
185
197
|
return output
|
|
186
198
|
} catch (e) {
|
|
199
|
+
const enriched = enrichFromLLMCapture(input, undefined)
|
|
187
200
|
const durationMs = rawDateNow() - start
|
|
188
201
|
pushTelemetryEvent({
|
|
189
|
-
id, type: 'ai', name: modelName, input,
|
|
202
|
+
id, type: 'ai', name: modelName, input: enriched.input,
|
|
190
203
|
output: { error: String(e) }, timestamp: start, durationMs,
|
|
191
204
|
})
|
|
192
205
|
throw e
|
|
@@ -314,15 +327,16 @@ export function wrapAI<Args extends unknown[], R>(
|
|
|
314
327
|
|
|
315
328
|
try {
|
|
316
329
|
const output = await callFn(...httpEffectiveArgs)
|
|
330
|
+
const enriched = enrichFromLLMCapture(input, extractUsage(output))
|
|
317
331
|
|
|
318
332
|
if (isReadableStream(output)) {
|
|
319
333
|
const [streamForCaller, streamForRecorder] = output.tee()
|
|
320
334
|
bufferReadableStream(streamForRecorder).then((rawText) => {
|
|
321
335
|
const durationMs = rawDateNow() - start
|
|
322
|
-
pushTelemetryEvent({ id, type: 'ai', name: modelName, input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs })
|
|
336
|
+
pushTelemetryEvent({ id, type: 'ai', name: modelName, input: enriched.input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs, ...(enriched.usage ? { usage: enriched.usage } : {}) })
|
|
323
337
|
}).catch(() => {
|
|
324
338
|
const durationMs = rawDateNow() - start
|
|
325
|
-
pushTelemetryEvent({ id, type: 'ai', name: modelName, input, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs })
|
|
339
|
+
pushTelemetryEvent({ id, type: 'ai', name: modelName, input: enriched.input, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs, ...(enriched.usage ? { usage: enriched.usage } : {}) })
|
|
326
340
|
})
|
|
327
341
|
return streamForCaller as unknown as R
|
|
328
342
|
}
|
|
@@ -331,23 +345,23 @@ export function wrapAI<Args extends unknown[], R>(
|
|
|
331
345
|
return wrapAsyncIterable(output, (chunks) => {
|
|
332
346
|
const durationMs = rawDateNow() - start
|
|
333
347
|
const rawText = chunks.map((c) => (typeof c === 'string' ? c : JSON.stringify(c))).join('')
|
|
334
|
-
pushTelemetryEvent({ id, type: 'ai', name: modelName, input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs })
|
|
348
|
+
pushTelemetryEvent({ id, type: 'ai', name: modelName, input: enriched.input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs, ...(enriched.usage ? { usage: enriched.usage } : {}) })
|
|
335
349
|
}) as unknown as R
|
|
336
350
|
}
|
|
337
351
|
|
|
338
352
|
const durationMs = rawDateNow() - start
|
|
339
|
-
const usage = extractUsage(output)
|
|
340
353
|
const event: WorkflowEvent = {
|
|
341
|
-
id, type: 'ai', name: modelName, input, output,
|
|
354
|
+
id, type: 'ai', name: modelName, input: enriched.input, output,
|
|
342
355
|
timestamp: start, durationMs,
|
|
343
|
-
...(usage ? { usage } : {}),
|
|
356
|
+
...(enriched.usage ? { usage: enriched.usage } : {}),
|
|
344
357
|
}
|
|
345
358
|
pushTelemetryEvent(event)
|
|
346
359
|
return output
|
|
347
360
|
} catch (e) {
|
|
361
|
+
const enriched = enrichFromLLMCapture(input, undefined)
|
|
348
362
|
const durationMs = rawDateNow() - start
|
|
349
363
|
pushTelemetryEvent({
|
|
350
|
-
id, type: 'ai', name: modelName, input,
|
|
364
|
+
id, type: 'ai', name: modelName, input: enriched.input,
|
|
351
365
|
output: { error: String(e) }, timestamp: start, durationMs,
|
|
352
366
|
})
|
|
353
367
|
throw e
|