@warpmetrics/warp 0.0.7 → 0.0.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Warpmetrics
3
+ Copyright (c) 2026 Warpmetrics
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -105,15 +105,6 @@ ref(response) // 'wm_call_01jkx3ndef8mn2q7kpvhc4e9ws'
105
105
  ref('wm_run_01jkx3ndek0gh4r5tmqp9a3bcv') // pass-through
106
106
  ```
107
107
 
108
- ### `cost(target)`
109
-
110
- Get the estimated cost in USD for any target. Aggregates across all nested calls for runs and groups.
111
-
112
- ```js
113
- cost(response) // 0.0012
114
- cost(r) // 0.0036 (sum of all calls in the run)
115
- ```
116
-
117
108
  ### `flush()`
118
109
 
119
110
  Manually flush pending events. Events are auto-flushed on an interval and on process exit, but you can force it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warpmetrics/warp",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Measure your agents, not your LLM calls.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -18,6 +18,7 @@
18
18
  "test": "vitest run",
19
19
  "test:watch": "vitest",
20
20
  "test:coverage": "vitest run --coverage",
21
+ "preversion": "vitest run --coverage",
21
22
  "release:patch": "npm version patch && git push origin main --tags",
22
23
  "release:minor": "npm version minor && git push origin main --tags"
23
24
  },
@@ -9,9 +9,3 @@ export const groupRegistry = new Map();
9
9
 
10
10
  /** @type {WeakMap<object, string>} LLM response object → call id */
11
11
  export const responseRegistry = new WeakMap();
12
-
13
- /** @type {WeakMap<object, number>} LLM response object → cost in USD */
14
- export const costRegistry = new WeakMap();
15
-
16
- /** @type {Map<string, number>} call id → cost in USD (for aggregation) */
17
- export const costByCallId = new Map();
@@ -21,6 +21,7 @@ const queue = {
21
21
  calls: [],
22
22
  links: [],
23
23
  outcomes: [],
24
+ acts: [],
24
25
  };
25
26
 
26
27
  let flushTimeout = null;
@@ -44,6 +45,7 @@ export function clearQueue() {
44
45
  queue.calls.length = 0;
45
46
  queue.links.length = 0;
46
47
  queue.outcomes.length = 0;
48
+ queue.acts.length = 0;
47
49
  }
48
50
 
49
51
  // ---------------------------------------------------------------------------
@@ -56,7 +58,7 @@ function enqueue(type, event) {
56
58
  queue[type].push(event);
57
59
 
58
60
  const total = queue.runs.length + queue.groups.length + queue.calls.length
59
- + queue.links.length + queue.outcomes.length;
61
+ + queue.links.length + queue.outcomes.length + queue.acts.length;
60
62
 
61
63
  if (total >= config.maxBatchSize) {
62
64
  flush();
@@ -81,10 +83,11 @@ export async function flush() {
81
83
  calls: queue.calls.splice(0),
82
84
  links: queue.links.splice(0),
83
85
  outcomes: queue.outcomes.splice(0),
86
+ acts: queue.acts.splice(0),
84
87
  };
85
88
 
86
89
  const total = batch.runs.length + batch.groups.length + batch.calls.length
87
- + batch.links.length + batch.outcomes.length;
90
+ + batch.links.length + batch.outcomes.length + batch.acts.length;
88
91
 
89
92
  if (total === 0) return;
90
93
 
@@ -100,10 +103,17 @@ export async function flush() {
100
103
  `[warpmetrics] Flushing ${total} events`
101
104
  + ` (runs=${batch.runs.length} groups=${batch.groups.length}`
102
105
  + ` calls=${batch.calls.length} links=${batch.links.length}`
103
- + ` outcomes=${batch.outcomes.length})`
106
+ + ` outcomes=${batch.outcomes.length} acts=${batch.acts.length})`
104
107
  );
105
108
  }
106
109
 
110
+ const raw = JSON.stringify(batch);
111
+ const body = JSON.stringify({ d: Buffer.from(raw, 'utf-8').toString('base64') });
112
+
113
+ if (config.debug) {
114
+ console.log(`[warpmetrics] Payload size: ${(raw.length / 1024).toFixed(1)}KB → ${(body.length / 1024).toFixed(1)}KB (encoded)`);
115
+ }
116
+
107
117
  try {
108
118
  const res = await fetch(`${config.baseUrl}/v1/events`, {
109
119
  method: 'POST',
@@ -112,7 +122,7 @@ export async function flush() {
112
122
  'Authorization': `Bearer ${config.apiKey}`,
113
123
  'X-SDK-Version': SDK_VERSION,
114
124
  },
115
- body: JSON.stringify(batch),
125
+ body,
116
126
  });
117
127
 
118
128
  if (!res.ok) {
@@ -122,7 +132,8 @@ export async function flush() {
122
132
 
123
133
  if (config.debug) {
124
134
  const result = await res.json();
125
- console.log(`[warpmetrics] Flush OK — received=${result.received} processed=${result.processed}`);
135
+ const d = result.data || result;
136
+ console.log(`[warpmetrics] Flush OK — received=${d.received} processed=${d.processed}`);
126
137
  }
127
138
  } catch (err) {
128
139
  if (config.debug) {
@@ -134,6 +145,7 @@ export async function flush() {
134
145
  queue.calls.unshift(...batch.calls);
135
146
  queue.links.unshift(...batch.links);
136
147
  queue.outcomes.unshift(...batch.outcomes);
148
+ queue.acts.unshift(...batch.acts);
137
149
  }
138
150
  }
139
151
 
@@ -185,6 +197,15 @@ export function logOutcome(data) {
185
197
  });
186
198
  }
187
199
 
200
+ export function logAct(data) {
201
+ enqueue('acts', {
202
+ targetId: data.targetId,
203
+ name: data.name,
204
+ metadata: data.metadata,
205
+ timestamp: new Date().toISOString(),
206
+ });
207
+ }
208
+
188
209
  // ---------------------------------------------------------------------------
189
210
  // Auto-flush on process exit (Node.js only)
190
211
  // ---------------------------------------------------------------------------
package/src/core/utils.js CHANGED
@@ -13,46 +13,3 @@ export function generateId(prefix) {
13
13
  return `wm_${prefix}_${ulid().toLowerCase()}`;
14
14
  }
15
15
 
16
- // ---------------------------------------------------------------------------
17
- // Pricing (per 1 M tokens, USD)
18
- // Best-effort — returns 0 for unknown models.
19
- // ---------------------------------------------------------------------------
20
-
21
- const PRICING = {
22
- // OpenAI
23
- 'gpt-4o': { prompt: 2.50, completion: 10.00 },
24
- 'gpt-4o-2024-11-20': { prompt: 2.50, completion: 10.00 },
25
- 'gpt-4o-mini': { prompt: 0.15, completion: 0.60 },
26
- 'gpt-4o-mini-2024-07-18': { prompt: 0.15, completion: 0.60 },
27
- 'gpt-4-turbo': { prompt: 10.00, completion: 30.00 },
28
- 'gpt-4-turbo-preview': { prompt: 10.00, completion: 30.00 },
29
- 'gpt-4': { prompt: 30.00, completion: 60.00 },
30
- 'gpt-3.5-turbo': { prompt: 0.50, completion: 1.50 },
31
- 'o1': { prompt: 15.00, completion: 60.00 },
32
- 'o1-mini': { prompt: 3.00, completion: 12.00 },
33
- 'o3-mini': { prompt: 1.10, completion: 4.40 },
34
-
35
- // Anthropic
36
- 'claude-sonnet-4-5-20250514': { prompt: 3.00, completion: 15.00 },
37
- 'claude-opus-4-6': { prompt: 15.00, completion: 75.00 },
38
- 'claude-3-5-sonnet-20241022': { prompt: 3.00, completion: 15.00 },
39
- 'claude-3-5-sonnet-latest': { prompt: 3.00, completion: 15.00 },
40
- 'claude-3-5-haiku-20241022': { prompt: 0.80, completion: 4.00 },
41
- 'claude-3-5-haiku-latest': { prompt: 0.80, completion: 4.00 },
42
- 'claude-3-opus-20240229': { prompt: 15.00, completion: 75.00 },
43
- 'claude-3-haiku-20240307': { prompt: 0.25, completion: 1.25 },
44
- };
45
-
46
- /**
47
- * Estimate cost from model name and token counts.
48
- * Returns 0 for unknown models.
49
- * @param {string} model
50
- * @param {{ prompt: number, completion: number }} tokens
51
- * @returns {number} cost in USD
52
- */
53
- export function calculateCost(model, tokens) {
54
- const p = PRICING[model];
55
- if (!p) return 0;
56
- return (tokens.prompt / 1_000_000) * p.prompt
57
- + (tokens.completion / 1_000_000) * p.completion;
58
- }
package/src/core/warp.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // Warpmetrics SDK — warp()
2
2
  // Wraps an LLM client so every API call is automatically tracked.
3
3
 
4
- import { generateId, calculateCost } from './utils.js';
5
- import { responseRegistry, costRegistry, costByCallId } from './registry.js';
4
+ import { generateId } from './utils.js';
5
+ import { responseRegistry } from './registry.js';
6
6
  import { logCall, setConfig, getConfig } from './transport.js';
7
7
  import * as openai from '../providers/openai.js';
8
8
  import * as anthropic from '../providers/anthropic.js';
@@ -39,21 +39,18 @@ function createInterceptor(originalFn, context, provider) {
39
39
  }
40
40
 
41
41
  const ext = provider.extract(result);
42
- const cost = calculateCost(model, ext.tokens);
43
42
 
44
43
  logCall({
45
44
  id: callId, provider: provider.name, model, messages,
46
45
  response: ext.response,
47
46
  tools: tools ? tools.map(t => t.function?.name || t.name).filter(Boolean) : null,
48
47
  toolCalls: ext.toolCalls,
49
- tokens: ext.tokens, cost, latency,
48
+ tokens: ext.tokens, latency,
50
49
  timestamp: new Date().toISOString(),
51
50
  status: 'success',
52
51
  });
53
52
 
54
53
  responseRegistry.set(result, callId);
55
- costRegistry.set(result, cost);
56
- costByCallId.set(callId, cost);
57
54
 
58
55
  return result;
59
56
  } catch (error) {
@@ -90,21 +87,17 @@ function wrapStream(stream, ctx) {
90
87
  ? ctx.provider.normalizeUsage(usage)
91
88
  : { prompt: 0, completion: 0, total: 0 };
92
89
 
93
- const cost = calculateCost(ctx.model, tokens);
94
-
95
90
  logCall({
96
91
  id: ctx.callId, provider: ctx.provider.name, model: ctx.model, messages: ctx.messages,
97
92
  response: content,
98
93
  tools: ctx.tools ? ctx.tools.map(t => t.function?.name || t.name).filter(Boolean) : null,
99
- tokens, cost,
94
+ tokens,
100
95
  latency: Date.now() - ctx.start,
101
96
  timestamp: new Date().toISOString(),
102
97
  status: 'success',
103
98
  });
104
99
 
105
100
  responseRegistry.set(wrapped, ctx.callId);
106
- costRegistry.set(wrapped, cost);
107
- costByCallId.set(ctx.callId, cost);
108
101
  },
109
102
  };
110
103
  return wrapped;
@@ -140,6 +133,14 @@ export function warp(client, options) {
140
133
  });
141
134
  }
142
135
 
136
+ const finalCfg = getConfig();
137
+ if (finalCfg.debug) {
138
+ const masked = finalCfg.apiKey
139
+ ? finalCfg.apiKey.slice(0, 10) + '...' + finalCfg.apiKey.slice(-4)
140
+ : '(none)';
141
+ console.log(`[warpmetrics] Config: baseUrl=${finalCfg.baseUrl} apiKey=${masked} enabled=${finalCfg.enabled} flushInterval=${finalCfg.flushInterval} maxBatchSize=${finalCfg.maxBatchSize}`);
142
+ }
143
+
143
144
  const provider = findProvider(client);
144
145
 
145
146
  if (!provider) {
package/src/index.d.ts CHANGED
@@ -42,6 +42,7 @@ export interface OutcomeOptions {
42
42
  metadata?: Record<string, any>;
43
43
  }
44
44
 
45
+
45
46
  /**
46
47
  * Wrap an LLM client to automatically track every API call.
47
48
  * Pass options on the first call to configure the SDK; env vars are used as defaults.
@@ -64,11 +65,15 @@ export function outcome(
64
65
  options?: OutcomeOptions,
65
66
  ): void;
66
67
 
68
+ /** Record an action taken on a tracked target (e.g. acting on feedback). */
69
+ export function act(
70
+ target: Run | Group | object | string,
71
+ name: string,
72
+ metadata?: Record<string, any>,
73
+ ): void;
74
+
67
75
  /** Resolve any trackable target to its string ID. */
68
76
  export function ref(target: Run | Group | object | string): string | undefined;
69
77
 
70
- /** Get the cost in USD for any tracked target. */
71
- export function cost(target: Run | Group | object | string): number;
72
-
73
78
  /** Manually flush pending events to the API. */
74
79
  export function flush(): Promise<void>;
package/src/index.js CHANGED
@@ -6,14 +6,13 @@
6
6
  // group(label, options?) — create a group
7
7
  // add(target, ...items) — add groups / calls to a run or group
8
8
  // outcome(target, name, options?) — record a result
9
+ // act(target, name, options?) — record an action taken on a result
9
10
  // ref(target) — get tracking ID
10
- // cost(target) — get cost in USD
11
-
12
11
  export { warp } from './core/warp.js';
13
12
  export { run } from './trace/run.js';
14
13
  export { group } from './trace/group.js';
15
14
  export { add } from './trace/add.js';
16
15
  export { outcome } from './trace/outcome.js';
16
+ export { act } from './trace/act.js';
17
17
  export { ref } from './trace/ref.js';
18
- export { cost } from './trace/cost.js';
19
18
  export { flush } from './core/transport.js';
@@ -9,11 +9,13 @@ export function detect(client) {
9
9
  export function extract(result) {
10
10
  const input = result?.usage?.input_tokens || 0;
11
11
  const output = result?.usage?.output_tokens || 0;
12
+ const cacheWrite = result?.usage?.cache_creation_input_tokens || 0;
13
+ const cacheRead = result?.usage?.cache_read_input_tokens || 0;
12
14
  return {
13
15
  response: Array.isArray(result?.content)
14
16
  ? result.content.filter(c => c.type === 'text').map(c => c.text).join('')
15
17
  : '',
16
- tokens: { prompt: input, completion: output, total: input + output },
18
+ tokens: { prompt: input, completion: output, total: input + output, cacheWrite, cacheRead },
17
19
  toolCalls: null,
18
20
  };
19
21
  }
@@ -28,7 +30,9 @@ export function extractStreamDelta(chunk) {
28
30
  export function normalizeUsage(usage) {
29
31
  const prompt = usage?.input_tokens || 0;
30
32
  const completion = usage?.output_tokens || 0;
31
- return { prompt, completion, total: prompt + completion };
33
+ const cacheWrite = usage?.cache_creation_input_tokens || 0;
34
+ const cacheRead = usage?.cache_read_input_tokens || 0;
35
+ return { prompt, completion, total: prompt + completion, cacheWrite, cacheRead };
32
36
  }
33
37
 
34
38
  export function proxy(client, intercept) {
@@ -16,12 +16,17 @@ function isResponsesAPI(result) {
16
16
  }
17
17
 
18
18
  function extractChatCompletions(result) {
19
+ const prompt = result?.usage?.prompt_tokens || 0;
20
+ const completion = result?.usage?.completion_tokens || 0;
21
+ const cachedInput = result?.usage?.prompt_tokens_details?.cached_tokens || 0;
22
+
19
23
  return {
20
24
  response: result?.choices?.[0]?.message?.content || '',
21
25
  tokens: {
22
- prompt: result?.usage?.prompt_tokens || 0,
23
- completion: result?.usage?.completion_tokens || 0,
24
- total: result?.usage?.total_tokens || 0,
26
+ prompt,
27
+ completion,
28
+ total: result?.usage?.total_tokens || 0,
29
+ cachedInput,
25
30
  },
26
31
  toolCalls: result?.choices?.[0]?.message?.tool_calls?.map(tc => ({
27
32
  id: tc.id,
@@ -40,12 +45,13 @@ function extractResponses(result) {
40
45
 
41
46
  const input = result?.usage?.input_tokens || 0;
42
47
  const output = result?.usage?.output_tokens || 0;
48
+ const cachedInput = result?.usage?.input_tokens_details?.cached_tokens || 0;
43
49
 
44
50
  const fnCalls = (result?.output || []).filter(item => item.type === 'function_call');
45
51
 
46
52
  return {
47
53
  response: text,
48
- tokens: { prompt: input, completion: output, total: input + output },
54
+ tokens: { prompt: input, completion: output, total: input + output, cachedInput },
49
55
  toolCalls: fnCalls.length > 0
50
56
  ? fnCalls.map(fc => ({ id: fc.id, name: fc.name, arguments: fc.arguments }))
51
57
  : null,
@@ -79,7 +85,9 @@ export function extractStreamDelta(chunk) {
79
85
  export function normalizeUsage(usage) {
80
86
  const prompt = usage?.prompt_tokens || usage?.input_tokens || 0;
81
87
  const completion = usage?.completion_tokens || usage?.output_tokens || 0;
82
- return { prompt, completion, total: prompt + completion };
88
+ const cachedInput = usage?.prompt_tokens_details?.cached_tokens
89
+ || usage?.input_tokens_details?.cached_tokens || 0;
90
+ return { prompt, completion, total: prompt + completion, cachedInput };
83
91
  }
84
92
 
85
93
  // ---------------------------------------------------------------------------
@@ -0,0 +1,26 @@
1
+ // Warpmetrics SDK — act()
2
+
3
+ import { ref as getRef } from './ref.js';
4
+ import { logAct, getConfig } from '../core/transport.js';
5
+
6
+ /**
7
+ * Record an action taken on a tracked target (e.g. acting on feedback).
8
+ *
9
+ * @param {object | string} target — Run, Group, LLM response, or ref string
10
+ * @param {string} name — action name ("improve-section", "refine-prompt")
11
+ * @param {Record<string, any>} [metadata] — arbitrary extra data
12
+ */
13
+ export function act(target, name, metadata) {
14
+ const targetId = getRef(target);
15
+
16
+ if (!targetId) {
17
+ if (getConfig().debug) console.warn('[warpmetrics] act() — target not tracked.');
18
+ return;
19
+ }
20
+
21
+ logAct({
22
+ targetId,
23
+ name,
24
+ metadata: metadata || null,
25
+ });
26
+ }
package/src/trace/cost.js DELETED
@@ -1,50 +0,0 @@
1
- // Warpmetrics SDK — cost()
2
-
3
- import { ref as getRef } from './ref.js';
4
- import { runRegistry, groupRegistry, costRegistry, costByCallId } from '../core/registry.js';
5
-
6
- /**
7
- * Get the cost in USD for any tracked target.
8
- *
9
- * - Response object → cost of that single call
10
- * - Run / Group → sum of all nested calls
11
- * - Ref string → lookup in registries
12
- *
13
- * @param {object | string} target
14
- * @returns {number} cost in USD
15
- */
16
- export function cost(target) {
17
- // Response object — direct lookup
18
- if (typeof target === 'object' && costRegistry.has(target)) {
19
- return costRegistry.get(target);
20
- }
21
-
22
- const id = getRef(target);
23
- if (!id) return 0;
24
-
25
- // Single call by ref string
26
- if (id.startsWith('wm_call_')) {
27
- return costByCallId.get(id) || 0;
28
- }
29
-
30
- // Run or Group — aggregate
31
- const container = runRegistry.get(id) || groupRegistry.get(id);
32
- if (container) return sumCosts(container);
33
-
34
- return 0;
35
- }
36
-
37
- function sumCosts(container) {
38
- let total = 0;
39
-
40
- for (const callId of container.calls) {
41
- total += costByCallId.get(callId) || 0;
42
- }
43
-
44
- for (const groupId of container.groups) {
45
- const g = groupRegistry.get(groupId);
46
- if (g) total += sumCosts(g);
47
- }
48
-
49
- return total;
50
- }