@revenium/anthropic 1.1.1 → 1.1.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.1.4] - 2026-03-03
6
+
7
+ ### Fixed
8
+
9
+ - Use canonical model name from API response for pricing resolution
10
+ - Use resolved model in streaming error context
11
+
12
+ ## [1.1.3] - 2026-02-19
13
+
14
+ ### Changed
15
+
16
+ - Version bump (1.1.2 blocked by npm registry)
17
+
18
+ ## [1.1.2] - 2026-02-18
19
+
20
+ ### Changed
21
+
22
+ - Version bump for npm publish with updated changelog
23
+
24
+ ## [1.1.1] - 2026-02-18
25
+
26
+ ### Added
27
+
28
+ - Tool metering support with meter_tool event tracking
29
+ - Output fields feature for tool metering documentation
30
+ - Fetch timeout configuration for API calls
31
+
32
+ ### Fixed
33
+
34
+ - Removed dead response.text() call in sendToolEvent
35
+ - Tests updated to use global fetch mock
36
+ - Used native fetch in tool-tracker module
37
+
5
38
  ## [1.1.0] - 2026-01-20
6
39
 
7
40
  ### Added
@@ -20,6 +53,7 @@ All notable changes to this project will be documented in this file.
20
53
 
21
54
  - Added terminal cost/metrics summary output after each API call
22
55
  - Added distributed tracing support with 10 visualization fields
56
+ - Added CI/CD workflows for automated testing and integration
23
57
 
24
58
  ### Fixed
25
59
 
@@ -128,6 +162,10 @@ All notable changes to this project will be documented in this file.
128
162
  - Configurable retry logic
129
163
  - Debug logging support
130
164
 
165
+ [1.1.3]: https://github.com/revenium/revenium-middleware-anthropic-node/releases/tag/v1.1.3
166
+ [1.1.2]: https://github.com/revenium/revenium-middleware-anthropic-node/releases/tag/v1.1.2
167
+ [1.1.1]: https://github.com/revenium/revenium-middleware-anthropic-node/releases/tag/v1.1.1
168
+ [1.1.0]: https://github.com/revenium/revenium-middleware-anthropic-node/releases/tag/v1.1.0
131
169
  [1.0.9]: https://github.com/revenium/revenium-middleware-anthropic-node/releases/tag/v1.0.9
132
170
  [1.0.8]: https://github.com/revenium/revenium-middleware-anthropic-node/releases/tag/v1.0.8
133
171
  [1.0.7]: https://github.com/revenium/revenium-middleware-anthropic-node/releases/tag/v1.0.7
package/README.md CHANGED
@@ -295,6 +295,54 @@ Add business context to track usage by organization, user, task type, or custom
295
295
 
296
296
  - [examples/advanced-features.ts](https://github.com/revenium/revenium-middleware-anthropic-node/blob/HEAD/examples/advanced-features.ts) - Working examples
297
297
 
298
+ ## Tool Metering
299
+
300
+ Track execution of custom tools and external API calls with automatic timing, error handling, and metadata collection.
301
+
302
+ ### Quick Example
303
+
304
+ ```typescript
305
+ import { meterTool, setToolContext } from '@revenium/anthropic';
306
+
307
+ setToolContext({
308
+ agent: 'my-agent',
309
+ traceId: 'session-123'
310
+ });
311
+
312
+ const result = await meterTool('weather-api', async () => {
313
+ return await fetch('https://api.example.com/weather');
314
+ }, {
315
+ operation: 'get_forecast',
316
+ outputFields: ['temperature', 'humidity']
317
+ });
318
+ ```
319
+
320
+ ### Functions
321
+
322
+ **meterTool(toolId, fn, metadata?)**
323
+
324
+ Wraps a function with automatic metering. Captures duration, success/failure, and errors. Returns function result unchanged.
325
+
326
+ **reportToolCall(toolId, report)**
327
+
328
+ Manually report a tool call that was already executed. Useful when wrapping is not possible.
329
+
330
+ **Context Management**
331
+
332
+ - `setToolContext(ctx)` - Set context for all subsequent tool calls
333
+ - `getToolContext()` - Get current context
334
+ - `clearToolContext()` - Clear context
335
+ - `runWithToolContext(ctx, fn)` - Run function with scoped context
336
+
337
+ ### Metadata Options
338
+
339
+ | Field | Description |
340
+ |-------|-------------|
341
+ | `operation` | Tool operation name (e.g., "search", "scrape") |
342
+ | `outputFields` | Array of field names to auto-extract from result |
343
+ | `usageMetadata` | Custom metrics (e.g., tokens, results count) |
344
+ | `agent`, `traceId`, etc. | Context fields (inherited from setToolContext) |
345
+
298
346
  ## Metadata Fields
299
347
 
300
348
  The middleware automatically sends the following fields to Revenium's `/meter/v2/ai/completions` endpoint:
package/dist/cjs/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Clean architecture without backward compatibility constraints
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.validateUsageMetadata = exports.validateAnthropicMessageParams = exports.validateReveniumConfig = exports.canExecuteRequest = exports.resetCircuitBreaker = exports.getCircuitBreakerStats = exports.extractUsageFromStream = exports.trackUsageAsync = exports.sendReveniumMetrics = exports.isAnthropicPatched = exports.unpatchAnthropic = exports.patchAnthropic = exports.validateCurrentConfig = exports.getConfigStatus = exports.getConfig = exports.getLogger = exports.setLogger = exports.setConfig = void 0;
7
+ exports.runWithToolContext = exports.clearToolContext = exports.getToolContext = exports.setToolContext = exports.reportToolCall = exports.meterTool = exports.validateUsageMetadata = exports.validateAnthropicMessageParams = exports.validateReveniumConfig = exports.canExecuteRequest = exports.resetCircuitBreaker = exports.getCircuitBreakerStats = exports.extractUsageFromStream = exports.trackUsageAsync = exports.sendReveniumMetrics = exports.isAnthropicPatched = exports.unpatchAnthropic = exports.patchAnthropic = exports.validateCurrentConfig = exports.getConfigStatus = exports.getConfig = exports.getLogger = exports.setLogger = exports.setConfig = void 0;
8
8
  exports.initialize = initialize;
9
9
  exports.configure = configure;
10
10
  exports.isInitialized = isInitialized;
@@ -44,6 +44,14 @@ var validation_1 = require("./utils/validation");
44
44
  Object.defineProperty(exports, "validateReveniumConfig", { enumerable: true, get: function () { return validation_1.validateReveniumConfig; } });
45
45
  Object.defineProperty(exports, "validateAnthropicMessageParams", { enumerable: true, get: function () { return validation_1.validateAnthropicMessageParams; } });
46
46
  Object.defineProperty(exports, "validateUsageMetadata", { enumerable: true, get: function () { return validation_1.validateUsageMetadata; } });
47
+ var tool_tracker_1 = require("./tool-tracker");
48
+ Object.defineProperty(exports, "meterTool", { enumerable: true, get: function () { return tool_tracker_1.meterTool; } });
49
+ Object.defineProperty(exports, "reportToolCall", { enumerable: true, get: function () { return tool_tracker_1.reportToolCall; } });
50
+ var tool_context_1 = require("./tool-context");
51
+ Object.defineProperty(exports, "setToolContext", { enumerable: true, get: function () { return tool_context_1.setToolContext; } });
52
+ Object.defineProperty(exports, "getToolContext", { enumerable: true, get: function () { return tool_context_1.getToolContext; } });
53
+ Object.defineProperty(exports, "clearToolContext", { enumerable: true, get: function () { return tool_context_1.clearToolContext; } });
54
+ Object.defineProperty(exports, "runWithToolContext", { enumerable: true, get: function () { return tool_context_1.runWithToolContext; } });
47
55
  /**
48
56
  * Initialize the Revenium middleware with configuration from environment variables
49
57
  * This function can be called explicitly for better error handling and control
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setToolContext = setToolContext;
4
+ exports.getToolContext = getToolContext;
5
+ exports.clearToolContext = clearToolContext;
6
+ exports.runWithToolContext = runWithToolContext;
7
+ const async_hooks_1 = require("async_hooks");
8
+ const contextStorage = new async_hooks_1.AsyncLocalStorage();
9
+ function setToolContext(ctx) {
10
+ const current = contextStorage.getStore() ?? {};
11
+ contextStorage.enterWith({ ...current, ...ctx });
12
+ }
13
+ function getToolContext() {
14
+ return contextStorage.getStore() ?? {};
15
+ }
16
+ function clearToolContext() {
17
+ contextStorage.enterWith({});
18
+ }
19
+ function runWithToolContext(ctx, fn) {
20
+ const merged = { ...getToolContext(), ...ctx };
21
+ return contextStorage.run(merged, fn);
22
+ }
23
+ //# sourceMappingURL=tool-context.js.map
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.meterTool = meterTool;
4
+ exports.reportToolCall = reportToolCall;
5
+ const crypto_1 = require("crypto");
6
+ const tool_context_1 = require("./tool-context");
7
+ const config_1 = require("./config");
8
+ const constants_1 = require("./constants");
9
+ const MIDDLEWARE_SOURCE = "revenium-anthropic-node";
10
+ const TOOL_EVENTS_ENDPOINT = "/tool/events";
11
+ function isPromise(value) {
12
+ return value !== null && typeof value === "object" && typeof value.then === "function";
13
+ }
14
+ function extractOutputFields(result, fields) {
15
+ if (typeof result !== "object" || result === null) {
16
+ return {};
17
+ }
18
+ const extracted = {};
19
+ for (const field of fields) {
20
+ if (field in result) {
21
+ extracted[field] = result[field];
22
+ }
23
+ }
24
+ return extracted;
25
+ }
26
+ function buildToolEventPayload(toolId, durationMs, success, metadata, errorMessage) {
27
+ const context = (0, tool_context_1.getToolContext)();
28
+ const transactionId = metadata?.transactionId ?? context.transactionId ?? (0, crypto_1.randomUUID)();
29
+ return {
30
+ transactionId,
31
+ toolId,
32
+ operation: metadata?.operation,
33
+ durationMs,
34
+ success,
35
+ timestamp: new Date().toISOString(),
36
+ errorMessage,
37
+ usageMetadata: metadata?.usageMetadata,
38
+ agent: metadata?.agent ?? context.agent,
39
+ organizationName: metadata?.organizationName ?? context.organizationName,
40
+ productName: metadata?.productName ?? context.productName,
41
+ subscriberCredential: metadata?.subscriberCredential ?? context.subscriberCredential,
42
+ workflowId: metadata?.workflowId ?? context.workflowId,
43
+ traceId: metadata?.traceId ?? context.traceId,
44
+ middlewareSource: MIDDLEWARE_SOURCE,
45
+ };
46
+ }
47
+ async function sendToolEvent(payload) {
48
+ const config = (0, config_1.getConfig)();
49
+ const logger = (0, config_1.getLogger)();
50
+ if (!config) {
51
+ logger.warn("Revenium configuration not found, skipping tool event tracking");
52
+ return;
53
+ }
54
+ const url = `${config.reveniumBaseUrl || constants_1.DEFAULT_CONFIG.REVENIUM_BASE_URL}${TOOL_EVENTS_ENDPOINT}`;
55
+ logger.debug("Sending tool event to Revenium", {
56
+ url,
57
+ toolId: payload.toolId,
58
+ transactionId: payload.transactionId,
59
+ operation: payload.operation,
60
+ durationMs: payload.durationMs,
61
+ success: payload.success,
62
+ });
63
+ const controller = new AbortController();
64
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
65
+ const response = await fetch(url, {
66
+ method: "POST",
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ "Accept": "application/json",
70
+ "x-api-key": config.reveniumApiKey,
71
+ },
72
+ body: JSON.stringify(payload),
73
+ signal: controller.signal,
74
+ }).finally(() => clearTimeout(timeoutId));
75
+ logger.debug("Tool event response", {
76
+ status: response.status,
77
+ statusText: response.statusText,
78
+ transactionId: payload.transactionId,
79
+ toolId: payload.toolId,
80
+ });
81
+ if (!response.ok) {
82
+ const responseText = await response.text();
83
+ logger.error("Tool event API error", {
84
+ status: response.status,
85
+ statusText: response.statusText,
86
+ body: responseText,
87
+ transactionId: payload.transactionId,
88
+ toolId: payload.toolId,
89
+ });
90
+ throw new Error(`Revenium tool event API error: ${response.status} ${response.statusText}`);
91
+ }
92
+ logger.debug("Tool event sent successfully", {
93
+ transactionId: payload.transactionId,
94
+ toolId: payload.toolId,
95
+ });
96
+ }
97
+ function dispatchToolEvent(payload) {
98
+ const logger = (0, config_1.getLogger)();
99
+ sendToolEvent(payload)
100
+ .then(() => {
101
+ logger.debug("Tool event sent successfully", {
102
+ transactionId: payload.transactionId,
103
+ toolId: payload.toolId,
104
+ });
105
+ })
106
+ .catch((error) => {
107
+ logger.warn("Failed to send tool event", {
108
+ transactionId: payload.transactionId,
109
+ toolId: payload.toolId,
110
+ error: error instanceof Error ? error.message : String(error),
111
+ });
112
+ });
113
+ }
114
+ function meterTool(toolId, fn, metadata) {
115
+ const startTime = performance.now();
116
+ const handleSuccess = (result) => {
117
+ const durationMs = Math.round(performance.now() - startTime);
118
+ let finalMetadata = metadata;
119
+ if (metadata?.outputFields && metadata.outputFields.length > 0) {
120
+ const extracted = extractOutputFields(result, metadata.outputFields);
121
+ finalMetadata = {
122
+ ...metadata,
123
+ usageMetadata: {
124
+ ...metadata.usageMetadata,
125
+ ...extracted,
126
+ },
127
+ };
128
+ }
129
+ const payload = buildToolEventPayload(toolId, durationMs, true, finalMetadata);
130
+ dispatchToolEvent(payload);
131
+ return result;
132
+ };
133
+ const handleError = (error) => {
134
+ const durationMs = Math.round(performance.now() - startTime);
135
+ const errorMessage = error instanceof Error ? error.message : String(error);
136
+ const payload = buildToolEventPayload(toolId, durationMs, false, metadata, errorMessage);
137
+ dispatchToolEvent(payload);
138
+ throw error;
139
+ };
140
+ try {
141
+ const result = fn();
142
+ if (isPromise(result)) {
143
+ return result.then(handleSuccess, handleError);
144
+ }
145
+ return Promise.resolve(handleSuccess(result));
146
+ }
147
+ catch (error) {
148
+ return Promise.reject(handleError(error));
149
+ }
150
+ }
151
+ function reportToolCall(toolId, report) {
152
+ const context = (0, tool_context_1.getToolContext)();
153
+ const transactionId = report.transactionId ?? context.transactionId ?? (0, crypto_1.randomUUID)();
154
+ const payload = {
155
+ transactionId,
156
+ toolId,
157
+ operation: report.operation,
158
+ durationMs: report.durationMs,
159
+ success: report.success,
160
+ timestamp: report.timestamp ?? new Date().toISOString(),
161
+ errorMessage: report.errorMessage,
162
+ usageMetadata: report.usageMetadata,
163
+ agent: report.agent ?? context.agent,
164
+ organizationName: report.organizationName ?? context.organizationName,
165
+ productName: report.productName ?? context.productName,
166
+ subscriberCredential: report.subscriberCredential ?? context.subscriberCredential,
167
+ workflowId: report.workflowId ?? context.workflowId,
168
+ traceId: report.traceId ?? context.traceId,
169
+ middlewareSource: MIDDLEWARE_SOURCE,
170
+ };
171
+ dispatchToolEvent(payload);
172
+ }
173
+ //# sourceMappingURL=tool-tracker.js.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=tool-metering.js.map
package/dist/cjs/types.js CHANGED
@@ -2,5 +2,20 @@
2
2
  /**
3
3
  * Type definitions for Anthropic middleware
4
4
  */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
17
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
18
+ };
5
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
+ __exportStar(require("./types/tool-metering"), exports);
6
21
  //# sourceMappingURL=types.js.map
@@ -201,27 +201,29 @@ function reconstructResponseFromChunks(chunks, model) {
201
201
  * Handle streaming response by collecting chunks and extracting usage data
202
202
  */
203
203
  async function handleStreamingResponse(stream, context) {
204
- const { requestId, model, metadata, requestTime, startTime, requestBody } = context;
205
- // Create a new async generator that collects chunks and tracks usage
204
+ const { requestId, requestModel, metadata, requestTime, startTime, requestBody } = context;
206
205
  async function* trackingStream() {
207
206
  const chunks = [];
208
207
  let firstTokenTime;
208
+ let resolvedModel;
209
209
  try {
210
210
  for await (const chunk of stream) {
211
- // Track first token time
212
211
  if (!firstTokenTime && chunk.type === "content_block_delta") {
213
212
  firstTokenTime = Date.now();
214
213
  }
214
+ if (!resolvedModel && chunk.type === "message_start" && chunk.message?.model) {
215
+ resolvedModel = chunk.message.model;
216
+ }
215
217
  chunks.push(chunk);
216
218
  yield chunk;
217
219
  }
218
- // After stream completes, extract usage and track
219
220
  const endTime = Date.now();
220
221
  const responseTime = new Date();
221
222
  const duration = endTime - startTime;
222
223
  const timeToFirstToken = firstTokenTime
223
224
  ? firstTokenTime - startTime
224
225
  : undefined;
226
+ const model = resolvedModel ?? requestModel;
225
227
  logger.debug("Stream completed, extracting usage", {
226
228
  requestId,
227
229
  chunkCount: chunks.length,
@@ -233,7 +235,6 @@ async function handleStreamingResponse(stream, context) {
233
235
  if ((0, prompt_extraction_1.shouldCapturePrompts)(metadata)) {
234
236
  reconstructedResponse = reconstructResponseFromChunks(chunks, model);
235
237
  }
236
- // Create tracking data
237
238
  const trackingData = {
238
239
  requestId,
239
240
  model,
@@ -317,7 +318,7 @@ async function patchedCreateMethod(params, options) {
317
318
  // Create tracking data
318
319
  const trackingData = {
319
320
  requestId,
320
- model: params.model,
321
+ model: response.model ?? params.model,
321
322
  inputTokens: usage.inputTokens,
322
323
  outputTokens: usage.outputTokens,
323
324
  cacheCreationTokens: usage.cacheCreationTokens,
@@ -336,7 +337,7 @@ async function patchedCreateMethod(params, options) {
336
337
  (0, tracking_1.trackUsageAsync)(trackingData);
337
338
  logger.debug("Anthropic request completed successfully", {
338
339
  requestId,
339
- model: params.model,
340
+ model: response.model ?? params.model,
340
341
  inputTokens: usage.inputTokens,
341
342
  outputTokens: usage.outputTokens,
342
343
  duration,
@@ -346,7 +347,7 @@ async function patchedCreateMethod(params, options) {
346
347
  // Handle streaming response - need to collect chunks and extract usage
347
348
  return handleStreamingResponse(response, {
348
349
  requestId,
349
- model: params.model,
350
+ requestModel: params.model,
350
351
  metadata,
351
352
  requestTime,
352
353
  startTime,
@@ -375,12 +376,12 @@ async function* patchedStreamMethod(params, options) {
375
376
  const responseTime = new Date();
376
377
  const chunks = [];
377
378
  let firstTokenTime;
379
+ let resolvedModel;
378
380
  logger.debug("Intercepted Anthropic messages.stream call", {
379
381
  requestId,
380
382
  model: params.model,
381
383
  hasMetadata: !!params.usageMetadata,
382
384
  });
383
- // Validate parameters
384
385
  const validation = (0, validation_1.validateAnthropicMessageParams)(params);
385
386
  if (!validation.isValid) {
386
387
  logger.warn("Invalid Anthropic streaming parameters detected", {
@@ -389,22 +390,21 @@ async function* patchedStreamMethod(params, options) {
389
390
  warnings: validation.warnings,
390
391
  });
391
392
  }
392
- // Extract and validate metadata
393
393
  const metadata = (0, validation_1.validateUsageMetadata)(params.usageMetadata || {});
394
- // Remove usageMetadata from params before calling original method
395
394
  const { usageMetadata, ...cleanParams } = params;
396
395
  try {
397
- // Call original stream method
398
396
  const originalStream = patchingContext.originalMethods?.stream;
399
397
  if (!originalStream) {
400
398
  throw new error_handling_1.StreamProcessingError("Original stream method not available");
401
399
  }
402
400
  const stream = originalStream.call(this, cleanParams, options);
403
401
  for await (const chunk of stream) {
404
- // Track first token time
405
402
  if (!firstTokenTime && chunk.type === "content_block_delta") {
406
403
  firstTokenTime = Date.now();
407
404
  }
405
+ if (!resolvedModel && chunk.type === "message_start" && chunk.message?.model) {
406
+ resolvedModel = chunk.message.model;
407
+ }
408
408
  chunks.push(chunk);
409
409
  yield chunk;
410
410
  }
@@ -413,18 +413,16 @@ async function* patchedStreamMethod(params, options) {
413
413
  const timeToFirstToken = firstTokenTime
414
414
  ? firstTokenTime - startTime
415
415
  : undefined;
416
- // Extract usage information from all chunks
416
+ const model = resolvedModel ?? params.model;
417
417
  const usage = (0, tracking_1.extractUsageFromStream)(chunks);
418
- // Detect vision content
419
418
  const hasVisionContent = (0, trace_fields_1.detectVisionContent)(params);
420
419
  let reconstructedResponse = undefined;
421
420
  if ((0, prompt_extraction_1.shouldCapturePrompts)(metadata)) {
422
- reconstructedResponse = reconstructResponseFromChunks(chunks, params.model);
421
+ reconstructedResponse = reconstructResponseFromChunks(chunks, model);
423
422
  }
424
- // Create tracking data
425
423
  const trackingData = {
426
424
  requestId,
427
- model: params.model,
425
+ model,
428
426
  inputTokens: usage.inputTokens,
429
427
  outputTokens: usage.outputTokens,
430
428
  cacheCreationTokens: usage.cacheCreationTokens,
@@ -440,11 +438,10 @@ async function* patchedStreamMethod(params, options) {
440
438
  requestBody: params,
441
439
  response: reconstructedResponse,
442
440
  };
443
- // Track usage asynchronously
444
441
  (0, tracking_1.trackUsageAsync)(trackingData);
445
442
  logger.debug("Anthropic streaming request completed successfully", {
446
443
  requestId,
447
- model: params.model,
444
+ model,
448
445
  inputTokens: usage.inputTokens,
449
446
  outputTokens: usage.outputTokens,
450
447
  duration,
@@ -457,7 +454,7 @@ async function* patchedStreamMethod(params, options) {
457
454
  const duration = endTime - startTime;
458
455
  const errorContext = (0, error_handling_1.createErrorContext)()
459
456
  .withRequestId(requestId)
460
- .withModel(params.model)
457
+ .withModel(resolvedModel ?? params.model)
461
458
  .withDuration(duration)
462
459
  .with("isStreaming", true)
463
460
  .with("chunkCount", chunks.length)
package/dist/esm/index.js CHANGED
@@ -17,6 +17,8 @@ export { sendReveniumMetrics, trackUsageAsync, extractUsageFromStream, } from ".
17
17
  // Export utility functions
18
18
  export { getCircuitBreakerStats, resetCircuitBreaker, canExecuteRequest, } from "./utils/circuit-breaker.js";
19
19
  export { validateReveniumConfig, validateAnthropicMessageParams, validateUsageMetadata, } from "./utils/validation.js";
20
+ export { meterTool, reportToolCall, } from "./tool-tracker.js";
21
+ export { setToolContext, getToolContext, clearToolContext, runWithToolContext, } from "./tool-context.js";
20
22
  /**
21
23
  * Initialize the Revenium middleware with configuration from environment variables
22
24
  * This function can be called explicitly for better error handling and control
@@ -0,0 +1,17 @@
1
+ import { AsyncLocalStorage } from "async_hooks";
2
+ const contextStorage = new AsyncLocalStorage();
3
+ export function setToolContext(ctx) {
4
+ const current = contextStorage.getStore() ?? {};
5
+ contextStorage.enterWith({ ...current, ...ctx });
6
+ }
7
+ export function getToolContext() {
8
+ return contextStorage.getStore() ?? {};
9
+ }
10
+ export function clearToolContext() {
11
+ contextStorage.enterWith({});
12
+ }
13
+ export function runWithToolContext(ctx, fn) {
14
+ const merged = { ...getToolContext(), ...ctx };
15
+ return contextStorage.run(merged, fn);
16
+ }
17
+ //# sourceMappingURL=tool-context.js.map
@@ -0,0 +1,169 @@
1
+ import { randomUUID } from "crypto";
2
+ import { getToolContext } from "./tool-context.js";
3
+ import { getConfig, getLogger } from "./config.js";
4
+ import { DEFAULT_CONFIG } from "./constants.js";
5
+ const MIDDLEWARE_SOURCE = "revenium-anthropic-node";
6
+ const TOOL_EVENTS_ENDPOINT = "/tool/events";
7
+ function isPromise(value) {
8
+ return value !== null && typeof value === "object" && typeof value.then === "function";
9
+ }
10
+ function extractOutputFields(result, fields) {
11
+ if (typeof result !== "object" || result === null) {
12
+ return {};
13
+ }
14
+ const extracted = {};
15
+ for (const field of fields) {
16
+ if (field in result) {
17
+ extracted[field] = result[field];
18
+ }
19
+ }
20
+ return extracted;
21
+ }
22
+ function buildToolEventPayload(toolId, durationMs, success, metadata, errorMessage) {
23
+ const context = getToolContext();
24
+ const transactionId = metadata?.transactionId ?? context.transactionId ?? randomUUID();
25
+ return {
26
+ transactionId,
27
+ toolId,
28
+ operation: metadata?.operation,
29
+ durationMs,
30
+ success,
31
+ timestamp: new Date().toISOString(),
32
+ errorMessage,
33
+ usageMetadata: metadata?.usageMetadata,
34
+ agent: metadata?.agent ?? context.agent,
35
+ organizationName: metadata?.organizationName ?? context.organizationName,
36
+ productName: metadata?.productName ?? context.productName,
37
+ subscriberCredential: metadata?.subscriberCredential ?? context.subscriberCredential,
38
+ workflowId: metadata?.workflowId ?? context.workflowId,
39
+ traceId: metadata?.traceId ?? context.traceId,
40
+ middlewareSource: MIDDLEWARE_SOURCE,
41
+ };
42
+ }
43
+ async function sendToolEvent(payload) {
44
+ const config = getConfig();
45
+ const logger = getLogger();
46
+ if (!config) {
47
+ logger.warn("Revenium configuration not found, skipping tool event tracking");
48
+ return;
49
+ }
50
+ const url = `${config.reveniumBaseUrl || DEFAULT_CONFIG.REVENIUM_BASE_URL}${TOOL_EVENTS_ENDPOINT}`;
51
+ logger.debug("Sending tool event to Revenium", {
52
+ url,
53
+ toolId: payload.toolId,
54
+ transactionId: payload.transactionId,
55
+ operation: payload.operation,
56
+ durationMs: payload.durationMs,
57
+ success: payload.success,
58
+ });
59
+ const controller = new AbortController();
60
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
61
+ const response = await fetch(url, {
62
+ method: "POST",
63
+ headers: {
64
+ "Content-Type": "application/json",
65
+ "Accept": "application/json",
66
+ "x-api-key": config.reveniumApiKey,
67
+ },
68
+ body: JSON.stringify(payload),
69
+ signal: controller.signal,
70
+ }).finally(() => clearTimeout(timeoutId));
71
+ logger.debug("Tool event response", {
72
+ status: response.status,
73
+ statusText: response.statusText,
74
+ transactionId: payload.transactionId,
75
+ toolId: payload.toolId,
76
+ });
77
+ if (!response.ok) {
78
+ const responseText = await response.text();
79
+ logger.error("Tool event API error", {
80
+ status: response.status,
81
+ statusText: response.statusText,
82
+ body: responseText,
83
+ transactionId: payload.transactionId,
84
+ toolId: payload.toolId,
85
+ });
86
+ throw new Error(`Revenium tool event API error: ${response.status} ${response.statusText}`);
87
+ }
88
+ logger.debug("Tool event sent successfully", {
89
+ transactionId: payload.transactionId,
90
+ toolId: payload.toolId,
91
+ });
92
+ }
93
+ function dispatchToolEvent(payload) {
94
+ const logger = getLogger();
95
+ sendToolEvent(payload)
96
+ .then(() => {
97
+ logger.debug("Tool event sent successfully", {
98
+ transactionId: payload.transactionId,
99
+ toolId: payload.toolId,
100
+ });
101
+ })
102
+ .catch((error) => {
103
+ logger.warn("Failed to send tool event", {
104
+ transactionId: payload.transactionId,
105
+ toolId: payload.toolId,
106
+ error: error instanceof Error ? error.message : String(error),
107
+ });
108
+ });
109
+ }
110
+ export function meterTool(toolId, fn, metadata) {
111
+ const startTime = performance.now();
112
+ const handleSuccess = (result) => {
113
+ const durationMs = Math.round(performance.now() - startTime);
114
+ let finalMetadata = metadata;
115
+ if (metadata?.outputFields && metadata.outputFields.length > 0) {
116
+ const extracted = extractOutputFields(result, metadata.outputFields);
117
+ finalMetadata = {
118
+ ...metadata,
119
+ usageMetadata: {
120
+ ...metadata.usageMetadata,
121
+ ...extracted,
122
+ },
123
+ };
124
+ }
125
+ const payload = buildToolEventPayload(toolId, durationMs, true, finalMetadata);
126
+ dispatchToolEvent(payload);
127
+ return result;
128
+ };
129
+ const handleError = (error) => {
130
+ const durationMs = Math.round(performance.now() - startTime);
131
+ const errorMessage = error instanceof Error ? error.message : String(error);
132
+ const payload = buildToolEventPayload(toolId, durationMs, false, metadata, errorMessage);
133
+ dispatchToolEvent(payload);
134
+ throw error;
135
+ };
136
+ try {
137
+ const result = fn();
138
+ if (isPromise(result)) {
139
+ return result.then(handleSuccess, handleError);
140
+ }
141
+ return Promise.resolve(handleSuccess(result));
142
+ }
143
+ catch (error) {
144
+ return Promise.reject(handleError(error));
145
+ }
146
+ }
147
+ export function reportToolCall(toolId, report) {
148
+ const context = getToolContext();
149
+ const transactionId = report.transactionId ?? context.transactionId ?? randomUUID();
150
+ const payload = {
151
+ transactionId,
152
+ toolId,
153
+ operation: report.operation,
154
+ durationMs: report.durationMs,
155
+ success: report.success,
156
+ timestamp: report.timestamp ?? new Date().toISOString(),
157
+ errorMessage: report.errorMessage,
158
+ usageMetadata: report.usageMetadata,
159
+ agent: report.agent ?? context.agent,
160
+ organizationName: report.organizationName ?? context.organizationName,
161
+ productName: report.productName ?? context.productName,
162
+ subscriberCredential: report.subscriberCredential ?? context.subscriberCredential,
163
+ workflowId: report.workflowId ?? context.workflowId,
164
+ traceId: report.traceId ?? context.traceId,
165
+ middlewareSource: MIDDLEWARE_SOURCE,
166
+ };
167
+ dispatchToolEvent(payload);
168
+ }
169
+ //# sourceMappingURL=tool-tracker.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tool-metering.js.map
package/dist/esm/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Type definitions for Anthropic middleware
3
3
  */
4
- export {};
4
+ export * from "./types/tool-metering.js";
5
5
  //# sourceMappingURL=types.js.map
@@ -193,27 +193,29 @@ function reconstructResponseFromChunks(chunks, model) {
193
193
  * Handle streaming response by collecting chunks and extracting usage data
194
194
  */
195
195
  async function handleStreamingResponse(stream, context) {
196
- const { requestId, model, metadata, requestTime, startTime, requestBody } = context;
197
- // Create a new async generator that collects chunks and tracks usage
196
+ const { requestId, requestModel, metadata, requestTime, startTime, requestBody } = context;
198
197
  async function* trackingStream() {
199
198
  const chunks = [];
200
199
  let firstTokenTime;
200
+ let resolvedModel;
201
201
  try {
202
202
  for await (const chunk of stream) {
203
- // Track first token time
204
203
  if (!firstTokenTime && chunk.type === "content_block_delta") {
205
204
  firstTokenTime = Date.now();
206
205
  }
206
+ if (!resolvedModel && chunk.type === "message_start" && chunk.message?.model) {
207
+ resolvedModel = chunk.message.model;
208
+ }
207
209
  chunks.push(chunk);
208
210
  yield chunk;
209
211
  }
210
- // After stream completes, extract usage and track
211
212
  const endTime = Date.now();
212
213
  const responseTime = new Date();
213
214
  const duration = endTime - startTime;
214
215
  const timeToFirstToken = firstTokenTime
215
216
  ? firstTokenTime - startTime
216
217
  : undefined;
218
+ const model = resolvedModel ?? requestModel;
217
219
  logger.debug("Stream completed, extracting usage", {
218
220
  requestId,
219
221
  chunkCount: chunks.length,
@@ -225,7 +227,6 @@ async function handleStreamingResponse(stream, context) {
225
227
  if (shouldCapturePrompts(metadata)) {
226
228
  reconstructedResponse = reconstructResponseFromChunks(chunks, model);
227
229
  }
228
- // Create tracking data
229
230
  const trackingData = {
230
231
  requestId,
231
232
  model,
@@ -309,7 +310,7 @@ async function patchedCreateMethod(params, options) {
309
310
  // Create tracking data
310
311
  const trackingData = {
311
312
  requestId,
312
- model: params.model,
313
+ model: response.model ?? params.model,
313
314
  inputTokens: usage.inputTokens,
314
315
  outputTokens: usage.outputTokens,
315
316
  cacheCreationTokens: usage.cacheCreationTokens,
@@ -328,7 +329,7 @@ async function patchedCreateMethod(params, options) {
328
329
  trackUsageAsync(trackingData);
329
330
  logger.debug("Anthropic request completed successfully", {
330
331
  requestId,
331
- model: params.model,
332
+ model: response.model ?? params.model,
332
333
  inputTokens: usage.inputTokens,
333
334
  outputTokens: usage.outputTokens,
334
335
  duration,
@@ -338,7 +339,7 @@ async function patchedCreateMethod(params, options) {
338
339
  // Handle streaming response - need to collect chunks and extract usage
339
340
  return handleStreamingResponse(response, {
340
341
  requestId,
341
- model: params.model,
342
+ requestModel: params.model,
342
343
  metadata,
343
344
  requestTime,
344
345
  startTime,
@@ -367,12 +368,12 @@ async function* patchedStreamMethod(params, options) {
367
368
  const responseTime = new Date();
368
369
  const chunks = [];
369
370
  let firstTokenTime;
371
+ let resolvedModel;
370
372
  logger.debug("Intercepted Anthropic messages.stream call", {
371
373
  requestId,
372
374
  model: params.model,
373
375
  hasMetadata: !!params.usageMetadata,
374
376
  });
375
- // Validate parameters
376
377
  const validation = validateAnthropicMessageParams(params);
377
378
  if (!validation.isValid) {
378
379
  logger.warn("Invalid Anthropic streaming parameters detected", {
@@ -381,22 +382,21 @@ async function* patchedStreamMethod(params, options) {
381
382
  warnings: validation.warnings,
382
383
  });
383
384
  }
384
- // Extract and validate metadata
385
385
  const metadata = validateUsageMetadata(params.usageMetadata || {});
386
- // Remove usageMetadata from params before calling original method
387
386
  const { usageMetadata, ...cleanParams } = params;
388
387
  try {
389
- // Call original stream method
390
388
  const originalStream = patchingContext.originalMethods?.stream;
391
389
  if (!originalStream) {
392
390
  throw new StreamProcessingError("Original stream method not available");
393
391
  }
394
392
  const stream = originalStream.call(this, cleanParams, options);
395
393
  for await (const chunk of stream) {
396
- // Track first token time
397
394
  if (!firstTokenTime && chunk.type === "content_block_delta") {
398
395
  firstTokenTime = Date.now();
399
396
  }
397
+ if (!resolvedModel && chunk.type === "message_start" && chunk.message?.model) {
398
+ resolvedModel = chunk.message.model;
399
+ }
400
400
  chunks.push(chunk);
401
401
  yield chunk;
402
402
  }
@@ -405,18 +405,16 @@ async function* patchedStreamMethod(params, options) {
405
405
  const timeToFirstToken = firstTokenTime
406
406
  ? firstTokenTime - startTime
407
407
  : undefined;
408
- // Extract usage information from all chunks
408
+ const model = resolvedModel ?? params.model;
409
409
  const usage = extractUsageFromStream(chunks);
410
- // Detect vision content
411
410
  const hasVisionContent = detectVisionContent(params);
412
411
  let reconstructedResponse = undefined;
413
412
  if (shouldCapturePrompts(metadata)) {
414
- reconstructedResponse = reconstructResponseFromChunks(chunks, params.model);
413
+ reconstructedResponse = reconstructResponseFromChunks(chunks, model);
415
414
  }
416
- // Create tracking data
417
415
  const trackingData = {
418
416
  requestId,
419
- model: params.model,
417
+ model,
420
418
  inputTokens: usage.inputTokens,
421
419
  outputTokens: usage.outputTokens,
422
420
  cacheCreationTokens: usage.cacheCreationTokens,
@@ -432,11 +430,10 @@ async function* patchedStreamMethod(params, options) {
432
430
  requestBody: params,
433
431
  response: reconstructedResponse,
434
432
  };
435
- // Track usage asynchronously
436
433
  trackUsageAsync(trackingData);
437
434
  logger.debug("Anthropic streaming request completed successfully", {
438
435
  requestId,
439
- model: params.model,
436
+ model,
440
437
  inputTokens: usage.inputTokens,
441
438
  outputTokens: usage.outputTokens,
442
439
  duration,
@@ -449,7 +446,7 @@ async function* patchedStreamMethod(params, options) {
449
446
  const duration = endTime - startTime;
450
447
  const errorContext = createErrorContext()
451
448
  .withRequestId(requestId)
452
- .withModel(params.model)
449
+ .withModel(resolvedModel ?? params.model)
453
450
  .withDuration(duration)
454
451
  .with("isStreaming", true)
455
452
  .with("chunkCount", chunks.length)
@@ -23,13 +23,23 @@ TrackingData,
23
23
  /** Configuration validation result with detailed feedback */
24
24
  ConfigValidationResult,
25
25
  /** Request validation result for API call validation */
26
- RequestValidationResult, } from "./types";
26
+ RequestValidationResult,
27
+ /** Tool context for tool metering */
28
+ ToolContext,
29
+ /** Tool metadata for tool metering */
30
+ ToolMetadata,
31
+ /** Tool event payload structure */
32
+ ToolEventPayload,
33
+ /** Tool call report structure */
34
+ ToolCallReport, } from "./types";
27
35
  import type { MiddlewareStatus } from "./types";
28
36
  export { setConfig, setLogger, getLogger, getConfig, getConfigStatus, validateCurrentConfig, } from "./config";
29
37
  export { patchAnthropic, unpatchAnthropic, isAnthropicPatched, } from "./wrapper";
30
38
  export { sendReveniumMetrics, trackUsageAsync, extractUsageFromStream, } from "./tracking";
31
39
  export { getCircuitBreakerStats, resetCircuitBreaker, canExecuteRequest, } from "./utils/circuit-breaker";
32
40
  export { validateReveniumConfig, validateAnthropicMessageParams, validateUsageMetadata, } from "./utils/validation";
41
+ export { meterTool, reportToolCall, } from "./tool-tracker";
42
+ export { setToolContext, getToolContext, clearToolContext, runWithToolContext, } from "./tool-context";
33
43
  export declare function initialize(): void;
34
44
  /**
35
45
  * Manual initialization with custom configuration
@@ -0,0 +1,6 @@
1
+ import { ToolContext } from "./types/tool-metering";
2
+ export declare function setToolContext(ctx: ToolContext): void;
3
+ export declare function getToolContext(): ToolContext;
4
+ export declare function clearToolContext(): void;
5
+ export declare function runWithToolContext<T>(ctx: ToolContext, fn: () => T | Promise<T>): T | Promise<T>;
6
+ //# sourceMappingURL=tool-context.d.ts.map
@@ -0,0 +1,4 @@
1
+ import { ToolMetadata, ToolCallReport } from "./types/tool-metering";
2
+ export declare function meterTool<T>(toolId: string, fn: () => T | Promise<T>, metadata?: ToolMetadata): Promise<T>;
3
+ export declare function reportToolCall(toolId: string, report: ToolCallReport): void;
4
+ //# sourceMappingURL=tool-tracker.d.ts.map
@@ -0,0 +1,47 @@
1
+ export interface ToolContext {
2
+ agent?: string;
3
+ organizationName?: string;
4
+ productName?: string;
5
+ subscriberCredential?: string;
6
+ workflowId?: string;
7
+ traceId?: string;
8
+ transactionId?: string;
9
+ }
10
+ export interface ToolMetadata extends ToolContext {
11
+ operation?: string;
12
+ outputFields?: string[];
13
+ usageMetadata?: Record<string, unknown>;
14
+ }
15
+ export interface ToolEventPayload {
16
+ transactionId: string;
17
+ toolId: string;
18
+ operation?: string;
19
+ durationMs: number;
20
+ success: boolean;
21
+ timestamp: string;
22
+ errorMessage?: string;
23
+ usageMetadata?: Record<string, unknown>;
24
+ agent?: string;
25
+ organizationName?: string;
26
+ productName?: string;
27
+ subscriberCredential?: string;
28
+ workflowId?: string;
29
+ traceId?: string;
30
+ middlewareSource: string;
31
+ }
32
+ export interface ToolCallReport {
33
+ operation?: string;
34
+ durationMs: number;
35
+ success: boolean;
36
+ errorMessage?: string;
37
+ usageMetadata?: Record<string, unknown>;
38
+ agent?: string;
39
+ organizationName?: string;
40
+ productName?: string;
41
+ subscriberCredential?: string;
42
+ workflowId?: string;
43
+ traceId?: string;
44
+ transactionId?: string;
45
+ timestamp?: string;
46
+ }
47
+ //# sourceMappingURL=tool-metering.d.ts.map
@@ -506,4 +506,5 @@ export interface EnvironmentConfig {
506
506
  * - 'json': JSON formatted output for automation/parsing
507
507
  */
508
508
  export type SummaryFormat = "human" | "json";
509
+ export * from "./types/tool-metering";
509
510
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenium/anthropic",
3
- "version": "1.1.1",
3
+ "version": "1.1.4",
4
4
  "description": "Transparent TypeScript middleware for automatic Revenium usage tracking with Anthropic Claude AI",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -24,9 +24,17 @@
24
24
  "build:types": "tsc -p tsconfig.types.json",
25
25
  "fix-esm": "node scripts/fix-esm-imports.js",
26
26
  "dev": "tsc --watch",
27
- "example": "npm run build && tsx examples/basic.ts",
27
+ "test": "jest",
28
+ "test:unit": "jest --testPathPatterns=tests/unit",
29
+ "test:integration": "jest --testPathPatterns=tests/integration",
30
+ "test:performance": "jest --testPathPatterns=tests/performance",
31
+ "test:watch": "jest --watch",
32
+ "test:coverage": "jest --coverage",
33
+ "test:e2e": "npm run build && tsx tests/test-trace-fields.ts",
34
+ "example": "npm run build && tsx examples/getting_started.ts",
28
35
  "example:basic": "npm run build && tsx examples/basic.ts",
29
36
  "example:advanced": "npm run build && tsx examples/advanced.ts",
37
+ "example:metadata": "npm run build && tsx examples/metadata.ts",
30
38
  "clean": "rimraf dist",
31
39
  "prepublishOnly": "npm run clean && npm run build"
32
40
  },
@@ -59,10 +67,13 @@
59
67
  },
60
68
  "devDependencies": {
61
69
  "@anthropic-ai/sdk": "^0.55.1",
70
+ "@types/jest": "^30.0.0",
62
71
  "@types/node": "^20.0.0",
63
72
  "@types/node-fetch": "^2.6.12",
64
73
  "dotenv": "^16.5.0",
74
+ "jest": "^30.2.0",
65
75
  "rimraf": "^6.0.1",
76
+ "ts-jest": "^29.4.6",
66
77
  "ts-node": "^10.9.2",
67
78
  "tsx": "^4.20.3",
68
79
  "typescript": "^5.8.3"