@revenium/anthropic 1.1.0 → 1.1.1

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.
@@ -6,6 +6,8 @@ import { getLogger, getConfig } from "./config.js";
6
6
  import { trackUsageAsync, extractUsageFromResponse, extractUsageFromStream, } from "./tracking.js";
7
7
  import { validateAnthropicMessageParams, validateUsageMetadata, } from "./utils/validation.js";
8
8
  import { AnthropicPatchingError, RequestProcessingError, StreamProcessingError, createErrorContext, handleError, } from "./utils/error-handling.js";
9
+ import { shouldCapturePrompts } from "./utils/prompt-extraction.js";
10
+ import { detectVisionContent } from "./utils/trace-fields.js";
9
11
  import { randomUUID } from "crypto";
10
12
  // Global logger
11
13
  const logger = getLogger();
@@ -128,6 +130,65 @@ export function unpatchAnthropic() {
128
130
  export function isAnthropicPatched() {
129
131
  return patchingContext.isPatched;
130
132
  }
133
+ /**
134
+ * Reconstruct a response object from streaming chunks for prompt capture
135
+ */
136
+ function reconstructResponseFromChunks(chunks, model) {
137
+ const contentBlocks = [];
138
+ let stopReason;
139
+ let stopSequence;
140
+ const usage = {};
141
+ for (const chunk of chunks) {
142
+ if (chunk.type === "content_block_start" && chunk.content_block) {
143
+ contentBlocks.push({ ...chunk.content_block });
144
+ }
145
+ else if (chunk.type === "content_block_delta" && chunk.delta) {
146
+ const lastBlock = contentBlocks[contentBlocks.length - 1];
147
+ if (lastBlock && chunk.delta.type === "text_delta") {
148
+ if (lastBlock.type === "text") {
149
+ lastBlock.text = (lastBlock.text || "") + (chunk.delta.text || "");
150
+ }
151
+ }
152
+ else if (lastBlock && chunk.delta.type === "input_json_delta") {
153
+ if (lastBlock.type === "tool_use") {
154
+ lastBlock.input = lastBlock.input || "";
155
+ lastBlock.input +=
156
+ chunk.delta.partial_json || "";
157
+ }
158
+ }
159
+ }
160
+ else if (chunk.type === "message_delta" && chunk.delta) {
161
+ const delta = chunk.delta;
162
+ if (delta.stop_reason) {
163
+ stopReason = delta.stop_reason;
164
+ }
165
+ if (delta.stop_sequence) {
166
+ stopSequence = delta.stop_sequence;
167
+ }
168
+ }
169
+ else if (chunk.type === "message_start" && chunk.message?.usage) {
170
+ Object.assign(usage, chunk.message.usage);
171
+ }
172
+ else if (chunk.usage) {
173
+ Object.assign(usage, chunk.usage);
174
+ }
175
+ }
176
+ return {
177
+ id: `reconstructed-${Date.now()}`,
178
+ type: "message",
179
+ role: "assistant",
180
+ content: contentBlocks,
181
+ model,
182
+ stop_reason: stopReason || "end_turn",
183
+ stop_sequence: stopSequence,
184
+ usage: {
185
+ input_tokens: usage.input_tokens || 0,
186
+ output_tokens: usage.output_tokens || 0,
187
+ cache_creation_input_tokens: usage.cache_creation_input_tokens,
188
+ cache_read_input_tokens: usage.cache_read_input_tokens,
189
+ },
190
+ };
191
+ }
131
192
  /**
132
193
  * Handle streaming response by collecting chunks and extracting usage data
133
194
  */
@@ -160,6 +221,10 @@ async function handleStreamingResponse(stream, context) {
160
221
  timeToFirstToken,
161
222
  });
162
223
  const usage = extractUsageFromStream(chunks);
224
+ let reconstructedResponse = undefined;
225
+ if (shouldCapturePrompts(metadata)) {
226
+ reconstructedResponse = reconstructResponseFromChunks(chunks, model);
227
+ }
163
228
  // Create tracking data
164
229
  const trackingData = {
165
230
  requestId,
@@ -176,6 +241,8 @@ async function handleStreamingResponse(stream, context) {
176
241
  responseTime,
177
242
  timeToFirstToken,
178
243
  requestBody: requestBody,
244
+ response: reconstructedResponse,
245
+ hasVisionContent: detectVisionContent(requestBody),
179
246
  };
180
247
  // Track usage asynchronously
181
248
  trackUsageAsync(trackingData);
@@ -237,6 +304,8 @@ async function patchedCreateMethod(params, options) {
237
304
  const responseTime = new Date();
238
305
  // Extract usage information
239
306
  const usage = extractUsageFromResponse(response);
307
+ // Detect vision content
308
+ const hasVisionContent = detectVisionContent(params);
240
309
  // Create tracking data
241
310
  const trackingData = {
242
311
  requestId,
@@ -251,7 +320,9 @@ async function patchedCreateMethod(params, options) {
251
320
  metadata,
252
321
  requestTime,
253
322
  responseTime,
323
+ hasVisionContent,
254
324
  requestBody: params,
325
+ response,
255
326
  };
256
327
  // Track usage asynchronously
257
328
  trackUsageAsync(trackingData);
@@ -336,6 +407,12 @@ async function* patchedStreamMethod(params, options) {
336
407
  : undefined;
337
408
  // Extract usage information from all chunks
338
409
  const usage = extractUsageFromStream(chunks);
410
+ // Detect vision content
411
+ const hasVisionContent = detectVisionContent(params);
412
+ let reconstructedResponse = undefined;
413
+ if (shouldCapturePrompts(metadata)) {
414
+ reconstructedResponse = reconstructResponseFromChunks(chunks, params.model);
415
+ }
339
416
  // Create tracking data
340
417
  const trackingData = {
341
418
  requestId,
@@ -351,7 +428,9 @@ async function* patchedStreamMethod(params, options) {
351
428
  requestTime,
352
429
  responseTime,
353
430
  timeToFirstToken,
431
+ hasVisionContent,
354
432
  requestBody: params,
433
+ response: reconstructedResponse,
355
434
  };
356
435
  // Track usage asynchronously
357
436
  trackUsageAsync(trackingData);
@@ -47,7 +47,7 @@ export interface ReveniumConfig {
47
47
  printSummary?: boolean | SummaryFormat;
48
48
  /** Revenium team ID for fetching cost metrics from the API. If not provided, the summary will still be printed but without cost information. */
49
49
  teamId?: string;
50
- /** Whether to capture prompts and responses for analysis (default: false) */
50
+ /** Whether to capture and send prompts to Revenium API (default: false) */
51
51
  capturePrompts?: boolean;
52
52
  }
53
53
  /**
@@ -60,17 +60,27 @@ export interface UsageMetadata {
60
60
  traceId?: string;
61
61
  /** Classification of AI operation (e.g., 'customer-support', 'content-generation', 'code-review') */
62
62
  taskType?: string;
63
- /** Customer organization identifier for multi-tenant applications */
63
+ /** Customer organization name for multi-tenant applications (used for lookup/auto-creation) */
64
+ organizationName?: string;
65
+ /**
66
+ * @deprecated Use organizationName instead. This field will be removed in a future version.
67
+ * Customer organization identifier for multi-tenant applications
68
+ */
64
69
  organizationId?: string;
65
70
  /** Reference to billing plan or subscription tier */
66
71
  subscriptionId?: string;
67
- /** Product or feature identifier that is using AI services */
72
+ /** Product or feature name that is using AI services (used for lookup/auto-creation) */
73
+ productName?: string;
74
+ /**
75
+ * @deprecated Use productName instead. This field will be removed in a future version.
76
+ * Product or feature identifier that is using AI services
77
+ */
68
78
  productId?: string;
69
79
  /** Agent or bot identifier for automated systems */
70
80
  agent?: string;
71
81
  /** Quality score of AI response (0.0-1.0) for performance tracking */
72
82
  responseQualityScore?: number;
73
- /** Whether to capture prompts and responses for this request (overrides global config) */
83
+ /** Per-call override for prompt capture (overrides environment variable and config) */
74
84
  capturePrompts?: boolean;
75
85
  /** Allow additional custom fields for extensibility */
76
86
  [key: string]: unknown;
@@ -133,8 +143,12 @@ export interface TrackingData {
133
143
  responseTime: Date;
134
144
  /** Time to first token in milliseconds (for streaming responses) */
135
145
  timeToFirstToken?: number;
146
+ /** Whether the request contains vision/image content */
147
+ hasVisionContent?: boolean;
136
148
  /** Request body containing Anthropic message parameters */
137
149
  requestBody?: AnthropicMessageParams;
150
+ /** Response data from Anthropic API */
151
+ response?: AnthropicResponse;
138
152
  }
139
153
  /**
140
154
  * Internal payload structure for Revenium API
@@ -165,10 +179,10 @@ export interface ReveniumPayload {
165
179
  cacheReadTokenCount: number;
166
180
  /** Total token count (sum of all token types) */
167
181
  totalTokenCount: number;
168
- /** Organization identifier for multi-tenant tracking */
169
- organizationId?: string;
170
- /** Product identifier */
171
- productId?: string;
182
+ /** Organization name for multi-tenant tracking (used for lookup/auto-creation) */
183
+ organizationName?: string;
184
+ /** Product name (used for lookup/auto-creation) */
185
+ productName?: string;
172
186
  /** Subscriber information */
173
187
  subscriber?: Subscriber;
174
188
  /** Subscription identifier */
@@ -201,9 +215,14 @@ export interface ReveniumPayload {
201
215
  parentTransactionId?: string;
202
216
  transactionName?: string;
203
217
  region?: string;
218
+ hasVisionContent?: boolean;
204
219
  credentialAlias?: string;
205
220
  traceType?: string;
206
221
  traceName?: string;
222
+ systemPrompt?: string;
223
+ inputMessages?: string;
224
+ outputResponse?: string;
225
+ promptsTruncated?: boolean;
207
226
  }
208
227
  /**
209
228
  * Anthropic content block types for message validation
@@ -4,6 +4,7 @@ export declare function getCredentialAlias(): string | null;
4
4
  export declare function getTraceType(): string | null;
5
5
  export declare function getTraceName(): string | null;
6
6
  export declare function detectOperationSubtype(requestBody?: any): string | null;
7
+ export declare function detectVisionContent(params?: any): boolean;
7
8
  export declare function getParentTransactionId(): string | null;
8
9
  export declare function getTransactionName(): string | null;
9
10
  export declare function getRetryNumber(): number;
@@ -2,7 +2,7 @@
2
2
  * Validation utilities for Anthropic middleware
3
3
  * Provides type-safe validation with detailed error reporting
4
4
  */
5
- import { ReveniumConfig, UsageMetadata } from '../types';
5
+ import { ReveniumConfig, UsageMetadata } from "../types";
6
6
  /**
7
7
  * Type guard for checking if a value is a non-null object
8
8
  */
@@ -17,8 +17,8 @@ const streamingWithMetadata = async () => {
17
17
  try {
18
18
  const metadata: UsageMetadata = {
19
19
  subscriber: { id: "user-123", email: "user@example.com" },
20
- organizationId: "my-org",
21
- productId: "my-product",
20
+ organizationName: "my-org",
21
+ productName: "my-product",
22
22
  taskType: "streaming-demo",
23
23
  traceId: `session_${Date.now()}`,
24
24
  };
@@ -101,7 +101,7 @@ const manualTracking = async () => {
101
101
  responseTime: now,
102
102
  metadata: {
103
103
  subscriber: { id: "manual-user" },
104
- organizationId: "manual-org",
104
+ organizationName: "manual-org",
105
105
  taskType: "manual-tracking",
106
106
  },
107
107
  };
@@ -111,7 +111,7 @@ const manualTracking = async () => {
111
111
  console.log(
112
112
  `Tracked: ${
113
113
  trackingData.inputTokens + trackingData.outputTokens
114
- } tokens\n`
114
+ } tokens\n`,
115
115
  );
116
116
 
117
117
  const status = getStatus();
@@ -27,11 +27,11 @@ async function main() {
27
27
  },
28
28
 
29
29
  // Organization & billing
30
- organizationId: 'my-customers-name',
30
+ organizationName: 'my-customers-name',
31
31
  subscriptionId: 'plan-enterprise-2024',
32
32
 
33
33
  // Product & task tracking
34
- productId: 'my-product',
34
+ productName: 'my-product',
35
35
  taskType: 'doc-summary',
36
36
  agent: 'customer-support',
37
37
 
@@ -15,9 +15,9 @@ async function main() {
15
15
  },
16
16
  traceId: "trace-123",
17
17
  taskType: "task-123",
18
- organizationId: "org-123",
18
+ organizationName: "AcmeCorp",
19
19
  subscriptionId: "sub-123",
20
- productId: "prod-123",
20
+ productName: "ai-assistant",
21
21
  agent: "agent-123",
22
22
  responseQualityScore: 0.95,
23
23
  };
@@ -48,7 +48,7 @@ async function main() {
48
48
  : "Non-text response";
49
49
  console.log("RESPONSE: \n", textResponse);
50
50
  console.log(
51
- `Tokens: ${response.usage?.input_tokens} input + ${response.usage?.output_tokens} output\n`
51
+ `Tokens: ${response.usage?.input_tokens} input + ${response.usage?.output_tokens} output\n`,
52
52
  );
53
53
  } catch (error) {
54
54
  console.error("Error: ", error);
@@ -0,0 +1,105 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { configure } from "../src/index";
3
+
4
+ async function main() {
5
+ console.log("=== Anthropic Prompt Capture Example ===\n");
6
+
7
+ if (!process.env.ANTHROPIC_API_KEY) {
8
+ console.error("Error: ANTHROPIC_API_KEY environment variable is required");
9
+ console.error("Please set it before running this example:");
10
+ console.error(" export ANTHROPIC_API_KEY=your_api_key_here");
11
+ process.exit(1);
12
+ }
13
+
14
+ const config = {
15
+ reveniumApiKey: process.env.REVENIUM_METERING_API_KEY || "hak_test_key",
16
+ reveniumBaseUrl:
17
+ process.env.REVENIUM_METERING_BASE_URL || "https://api.revenium.ai",
18
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY,
19
+ capturePrompts: true,
20
+ };
21
+
22
+ configure(config);
23
+
24
+ const anthropic = new Anthropic({
25
+ apiKey: config.anthropicApiKey,
26
+ });
27
+
28
+ console.log("Example 1: Prompt capture enabled via config");
29
+ console.log("Making request with prompt capture enabled...\n");
30
+
31
+ try {
32
+ const response = await anthropic.messages.create({
33
+ model: "claude-3-5-sonnet-20241022",
34
+ max_tokens: 100,
35
+ system: "You are a helpful assistant that provides concise answers.",
36
+ messages: [
37
+ {
38
+ role: "user",
39
+ content: "What is the capital of France?",
40
+ },
41
+ ],
42
+ usageMetadata: {
43
+ organizationName: "org-prompt-capture-demo",
44
+ productName: "prod-anthropic-prompt-capture",
45
+ },
46
+ });
47
+
48
+ console.log(
49
+ "Response:",
50
+ response.content[0].type === "text" ? response.content[0].text : "",
51
+ );
52
+ console.log("\nPrompts captured and sent to Revenium API!");
53
+ } catch (error) {
54
+ console.error(
55
+ "Error:",
56
+ error instanceof Error ? error.message : String(error),
57
+ );
58
+ }
59
+
60
+ console.log("\n" + "=".repeat(50) + "\n");
61
+
62
+ console.log("Example 2: Per-call override to disable prompt capture");
63
+ console.log("Making request with prompt capture disabled via metadata...\n");
64
+
65
+ try {
66
+ const response2 = await anthropic.messages.create({
67
+ model: "claude-3-5-sonnet-20241022",
68
+ max_tokens: 100,
69
+ system: "You are a helpful assistant.",
70
+ messages: [
71
+ {
72
+ role: "user",
73
+ content: "What is 2+2?",
74
+ },
75
+ ],
76
+ usageMetadata: {
77
+ organizationName: "org-prompt-capture-demo",
78
+ productName: "prod-anthropic-prompt-capture",
79
+ capturePrompts: false,
80
+ },
81
+ });
82
+
83
+ console.log(
84
+ "Response:",
85
+ response2.content[0].type === "text" ? response2.content[0].text : "",
86
+ );
87
+ console.log("\nPrompts NOT captured (overridden via metadata)!");
88
+ } catch (error) {
89
+ console.error(
90
+ "Error:",
91
+ error instanceof Error ? error.message : String(error),
92
+ );
93
+ }
94
+
95
+ console.log("\n" + "=".repeat(50) + "\n");
96
+ console.log("Examples completed!");
97
+ console.log(
98
+ "\nNote: Set REVENIUM_CAPTURE_PROMPTS=true in .env to enable globally",
99
+ );
100
+ console.log(
101
+ "Or use capturePrompts: true in config or metadata for per-call control",
102
+ );
103
+ }
104
+
105
+ main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenium/anthropic",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
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",