elasticdash-test 0.1.18-alpha-24 → 0.1.18-alpha-26

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elasticdash-test",
3
- "version": "0.1.18-alpha-24",
3
+ "version": "0.1.18-alpha-26",
4
4
  "description": "AI-native test runner for ElasticDash workflow testing",
5
5
  "type": "module",
6
6
  "bin": {
@@ -364,6 +364,35 @@ async function bufferSSEStream(
364
364
  return completion
365
365
  }
366
366
 
367
+ /** Extract usage from buffered raw SSE text */
368
+ function extractStreamUsage(provider: string, rawSSE: string): UsageInfo | undefined {
369
+ const lines = rawSSE.split('\n')
370
+ // Walk backwards to find usage in the final events
371
+ for (let i = lines.length - 1; i >= 0; i--) {
372
+ const line = lines[i]
373
+ if (!line.startsWith('data: ')) continue
374
+ const data = line.slice(6).trim()
375
+ if (data === '[DONE]') continue
376
+ try {
377
+ const obj = JSON.parse(data) as Record<string, unknown>
378
+ // OpenAI / Grok / Kimi: usage in the final chunk
379
+ const usage = extractUsage(provider, obj)
380
+ if (usage) return usage
381
+ // Anthropic: usage in message_delta event
382
+ if (obj.type === 'message_delta') {
383
+ const u = (obj as Record<string, unknown>).usage as Record<string, number> | undefined
384
+ if (u) return extractUsage('anthropic', { usage: u })
385
+ }
386
+ // Anthropic: usage in message_start event
387
+ if (obj.type === 'message_start') {
388
+ const msg = (obj as Record<string, unknown>).message as Record<string, unknown> | undefined
389
+ if (msg?.usage) return extractUsage('anthropic', { usage: msg.usage })
390
+ }
391
+ } catch { /* skip */ }
392
+ }
393
+ return undefined
394
+ }
395
+
367
396
  /** Build a minimal non-streaming JSON response body from a completion string (for replay) */
368
397
  function synthesizeCompletionJSON(
369
398
  provider: string,
@@ -474,12 +503,30 @@ export function installAIInterceptor(): void {
474
503
  url, provider, model: capturedModel, messages: capturedMessages,
475
504
  body: capturedReq, promptSnippet: capturedSnippet,
476
505
  }
506
+ const isStreaming = capturedReq.stream === true
477
507
  try {
478
508
  const cloned = response.clone()
479
- const responseBody = await cloned.json() as Record<string, unknown>
480
- captured.usage = extractUsage(provider, responseBody)
509
+ if (!isStreaming) {
510
+ // Non-streaming: parse JSON response for usage
511
+ const responseBody = await cloned.json() as Record<string, unknown>
512
+ captured.usage = extractUsage(provider, responseBody)
513
+ } else if (cloned.body) {
514
+ // Streaming: read the raw SSE text to extract usage from final events
515
+ try {
516
+ const decoder = new TextDecoder()
517
+ const reader = cloned.body.getReader()
518
+ let rawSSE = ''
519
+ for (;;) {
520
+ const { done, value } = await reader.read()
521
+ if (done) break
522
+ rawSSE += decoder.decode(value, { stream: true })
523
+ }
524
+ reader.releaseLock()
525
+ captured.usage = extractStreamUsage(provider, rawSSE)
526
+ } catch { /* stream read failed */ }
527
+ }
481
528
  } catch {
482
- // Streaming or non-JSON response — usage not available from response
529
+ // Response body not available — usage won't be captured
483
530
  }
484
531
  ;(globalThis as Record<string, unknown>)[LLM_REQUEST_KEY] = captured
485
532
  }
@@ -7,6 +7,7 @@ import { rawDateNow } from './side-effects.js'
7
7
  // Skip them here to avoid duplicate observations.
8
8
  const AI_URL_PATTERNS = [
9
9
  /https?:\/\/api\.openai\.com\/v1\/((chat\/)?completions|embeddings)/,
10
+ /https?:\/\/api\.anthropic\.com\/v1\/messages/,
10
11
  /https?:\/\/generativelanguage\.googleapis\.com\/.*\/models\/[^/:]+:(generateContent|streamGenerateContent)/,
11
12
  /https?:\/\/api\.x\.ai\/v1\/(chat\/)?completions/,
12
13
  /https?:\/\/api\.moonshot\.ai\/v1\/(chat\/)?completions/,
@@ -45,6 +45,14 @@ function extractUsage(output: unknown): UsageInfo | undefined {
45
45
  totalTokens: u.total_tokens,
46
46
  }
47
47
  }
48
+ // Vercel AI SDK: { inputTokens, outputTokens }
49
+ if (u.inputTokens != null || u.outputTokens != null) {
50
+ return {
51
+ inputTokens: u.inputTokens,
52
+ outputTokens: u.outputTokens,
53
+ totalTokens: (u.inputTokens ?? 0) + (u.outputTokens ?? 0),
54
+ }
55
+ }
48
56
  }
49
57
  // Gemini SDK: usageMetadata
50
58
  if (o.usageMetadata && typeof o.usageMetadata === 'object') {
@@ -55,6 +63,14 @@ function extractUsage(output: unknown): UsageInfo | undefined {
55
63
  totalTokens: u.totalTokenCount,
56
64
  }
57
65
  }
66
+ // Flat token fields (e.g. { tokens: N } or { outputTokens: N, inputTokens: N })
67
+ if (typeof o.tokens === 'number' || typeof o.outputTokens === 'number') {
68
+ return {
69
+ inputTokens: typeof o.inputTokens === 'number' ? o.inputTokens : undefined,
70
+ outputTokens: typeof o.outputTokens === 'number' ? o.outputTokens : (typeof o.tokens === 'number' ? o.tokens : undefined),
71
+ totalTokens: (typeof o.inputTokens === 'number' ? o.inputTokens : 0) + (typeof o.outputTokens === 'number' ? o.outputTokens : (typeof o.tokens === 'number' ? o.tokens : 0)),
72
+ }
73
+ }
58
74
  return undefined
59
75
  }
60
76