@revenium/anthropic 1.0.0

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.
@@ -0,0 +1,374 @@
1
+ "use strict";
2
+ /**
3
+ * Anthropic SDK wrapper with type safety and structured error handling
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.patchAnthropic = patchAnthropic;
10
+ exports.unpatchAnthropic = unpatchAnthropic;
11
+ exports.isAnthropicPatched = isAnthropicPatched;
12
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
13
+ const config_1 = require("./config");
14
+ const tracking_1 = require("./tracking");
15
+ const validation_1 = require("./utils/validation");
16
+ const error_handling_1 = require("./utils/error-handling");
17
+ const crypto_1 = require("crypto");
18
+ // Global logger
19
+ const logger = (0, config_1.getLogger)();
20
+ /**
21
+ * Global patching context
22
+ */
23
+ const patchingContext = {
24
+ originalMethods: {},
25
+ isPatched: false,
26
+ patchedInstances: new WeakSet()
27
+ };
28
+ /**
29
+ * Get the Messages prototype using sophisticated prototype access
30
+ * Uses multiple fallback strategies to access the Anthropic Messages class prototype
31
+ */
32
+ function getMessagesPrototype() {
33
+ try {
34
+ // Method 1: Try to access through the constructor's prototype chain
35
+ // Look for the Messages constructor in the Anthropic module
36
+ // The Anthropic SDK typically exposes internal classes through the constructor
37
+ if (sdk_1.default?.Messages)
38
+ return sdk_1.default?.Messages?.prototype;
39
+ // Method 2: Try to access through the constructor's static properties
40
+ const anthropicConstructor = sdk_1.default;
41
+ if (anthropicConstructor?._Messages)
42
+ return anthropicConstructor?._Messages?.prototype;
43
+ // Method 3: Create a minimal instance with the real API key if available
44
+ // Fallback approach when direct prototype access methods fail
45
+ const apiKey = process.env.ANTHROPIC_API_KEY;
46
+ if (!apiKey) {
47
+ throw new error_handling_1.AnthropicPatchingError('Unable to access Anthropic Messages prototype: No API key available and direct prototype access failed');
48
+ }
49
+ const minimalInstance = new sdk_1.default({ apiKey });
50
+ const messagesPrototype = Object.getPrototypeOf(minimalInstance.messages);
51
+ // Clean up the minimal instance immediately
52
+ // Note: The instance will be garbage collected, but the prototype reference remains
53
+ return messagesPrototype;
54
+ }
55
+ catch (error) {
56
+ // If all methods fail, throw a descriptive error
57
+ throw new error_handling_1.AnthropicPatchingError(`Unable to access Anthropic Messages prototype: ${error instanceof Error ? error.message : String(error)}`);
58
+ }
59
+ }
60
+ /**
61
+ * Patch Anthropic SDK by modifying prototype methods
62
+ */
63
+ function patchAnthropic() {
64
+ if (patchingContext.isPatched)
65
+ return;
66
+ try {
67
+ // Access the Messages class prototype using sophisticated prototype access
68
+ const messagesPrototype = getMessagesPrototype();
69
+ if (!messagesPrototype)
70
+ throw new error_handling_1.AnthropicPatchingError('Unable to access Anthropic Messages prototype');
71
+ // Store original methods
72
+ patchingContext.originalMethods.create = messagesPrototype?.create;
73
+ patchingContext.originalMethods.stream = messagesPrototype?.stream;
74
+ if (!patchingContext.originalMethods?.create) {
75
+ throw new error_handling_1.AnthropicPatchingError('Unable to find original create method');
76
+ }
77
+ // Patch the create method
78
+ const patchedCreateFunction = function (params, options) {
79
+ return patchedCreateMethod.call(this, params, options);
80
+ };
81
+ messagesPrototype.create = patchedCreateFunction;
82
+ // Patch the stream method if it exists
83
+ if (patchingContext.originalMethods?.stream) {
84
+ messagesPrototype.stream = function (params, options) {
85
+ return patchedStreamMethod.call(this, params, options);
86
+ };
87
+ }
88
+ patchingContext.isPatched = true;
89
+ logger.info('Anthropic SDK patched successfully');
90
+ }
91
+ catch (error) {
92
+ const errorContext = (0, error_handling_1.createErrorContext)()
93
+ .with('patchingAttempt', true)
94
+ .build();
95
+ (0, error_handling_1.handleError)(error, logger, errorContext);
96
+ if (error instanceof error_handling_1.AnthropicPatchingError)
97
+ throw error;
98
+ throw new error_handling_1.AnthropicPatchingError(`Failed to patch Anthropic SDK: ${error instanceof Error ? error.message : String(error)}`, errorContext);
99
+ }
100
+ }
101
+ /**
102
+ * Unpatch Anthropic SDK (restore original methods)
103
+ */
104
+ function unpatchAnthropic() {
105
+ if (!patchingContext.isPatched)
106
+ return;
107
+ try {
108
+ // Access the Messages class prototype using sophisticated prototype access
109
+ const messagesPrototype = getMessagesPrototype();
110
+ if (messagesPrototype && patchingContext.originalMethods.create) {
111
+ messagesPrototype.create = patchingContext.originalMethods.create;
112
+ }
113
+ if (messagesPrototype && patchingContext.originalMethods.stream) {
114
+ messagesPrototype.stream = patchingContext.originalMethods?.stream;
115
+ }
116
+ patchingContext.isPatched = false;
117
+ patchingContext.originalMethods = {};
118
+ logger.info('Anthropic SDK unpatched successfully');
119
+ }
120
+ catch (error) {
121
+ const errorContext = (0, error_handling_1.createErrorContext)()
122
+ .with('unpatchingAttempt', true)
123
+ .build();
124
+ (0, error_handling_1.handleError)(error, logger, errorContext);
125
+ throw new error_handling_1.AnthropicPatchingError(`Failed to unpatch Anthropic SDK: ${error instanceof Error ? error.message : String(error)}`, errorContext);
126
+ }
127
+ }
128
+ /**
129
+ * Check if Anthropic SDK is patched
130
+ */
131
+ function isAnthropicPatched() {
132
+ return patchingContext.isPatched;
133
+ }
134
+ /**
135
+ * Handle streaming response by collecting chunks and extracting usage data
136
+ */
137
+ async function handleStreamingResponse(stream, context) {
138
+ const { requestId, model, metadata, requestTime, startTime } = context;
139
+ // Create a new async generator that collects chunks and tracks usage
140
+ async function* trackingStream() {
141
+ const chunks = [];
142
+ let firstTokenTime;
143
+ try {
144
+ for await (const chunk of stream) {
145
+ // Track first token time
146
+ if (!firstTokenTime && chunk.type === 'content_block_delta') {
147
+ firstTokenTime = Date.now();
148
+ }
149
+ chunks.push(chunk);
150
+ yield chunk;
151
+ }
152
+ // After stream completes, extract usage and track
153
+ const endTime = Date.now();
154
+ const responseTime = new Date();
155
+ const duration = endTime - startTime;
156
+ logger.debug('Stream completed, extracting usage', {
157
+ requestId,
158
+ chunkCount: chunks.length,
159
+ duration
160
+ });
161
+ const usage = (0, tracking_1.extractUsageFromStream)(chunks);
162
+ // Create tracking data
163
+ const trackingData = {
164
+ requestId,
165
+ model,
166
+ inputTokens: usage.inputTokens,
167
+ outputTokens: usage.outputTokens,
168
+ cacheCreationTokens: usage.cacheCreationTokens,
169
+ cacheReadTokens: usage.cacheReadTokens,
170
+ duration,
171
+ isStreamed: true,
172
+ stopReason: usage.stopReason,
173
+ metadata,
174
+ requestTime,
175
+ responseTime
176
+ };
177
+ // Track usage asynchronously
178
+ (0, tracking_1.trackUsageAsync)(trackingData);
179
+ logger.debug('Anthropic streaming request completed successfully', {
180
+ requestId,
181
+ model,
182
+ inputTokens: usage.inputTokens,
183
+ outputTokens: usage.outputTokens,
184
+ duration
185
+ });
186
+ }
187
+ catch (error) {
188
+ logger.error('Error processing streaming response', {
189
+ requestId,
190
+ error: error instanceof Error ? error.message : String(error)
191
+ });
192
+ throw error;
193
+ }
194
+ }
195
+ return trackingStream();
196
+ }
197
+ /**
198
+ * Patched create method implementation
199
+ */
200
+ async function patchedCreateMethod(params, options) {
201
+ const requestId = (0, crypto_1.randomUUID)();
202
+ const startTime = Date.now();
203
+ const requestTime = new Date();
204
+ logger.debug('Intercepted Anthropic messages.create call', {
205
+ requestId,
206
+ model: params.model,
207
+ hasMetadata: !!params.usageMetadata,
208
+ isStreaming: !!params.stream
209
+ });
210
+ // Validate parameters
211
+ const validation = (0, validation_1.validateAnthropicMessageParams)(params);
212
+ if (!validation.isValid) {
213
+ logger.warn('Invalid Anthropic parameters detected', {
214
+ requestId,
215
+ errors: validation.errors,
216
+ warnings: validation.warnings
217
+ });
218
+ }
219
+ // Extract and validate metadata
220
+ const metadata = (0, validation_1.validateUsageMetadata)(params.usageMetadata || {});
221
+ // Remove usageMetadata from params before calling original method
222
+ const { usageMetadata, ...cleanParams } = params;
223
+ try {
224
+ // Call original method
225
+ const originalCreate = patchingContext.originalMethods.create;
226
+ if (!originalCreate)
227
+ throw new error_handling_1.RequestProcessingError('Original create method not available');
228
+ const response = await originalCreate.call(this, cleanParams, options);
229
+ // Check if this is a streaming response
230
+ const isStreaming = !!params.stream;
231
+ if (!isStreaming) {
232
+ const endTime = Date.now();
233
+ const duration = endTime - startTime;
234
+ const responseTime = new Date();
235
+ // Extract usage information
236
+ const usage = (0, tracking_1.extractUsageFromResponse)(response);
237
+ // Create tracking data
238
+ const trackingData = {
239
+ requestId,
240
+ model: params.model,
241
+ inputTokens: usage.inputTokens,
242
+ outputTokens: usage.outputTokens,
243
+ cacheCreationTokens: usage.cacheCreationTokens,
244
+ cacheReadTokens: usage.cacheReadTokens,
245
+ duration,
246
+ isStreamed: false,
247
+ stopReason: usage.stopReason,
248
+ metadata,
249
+ requestTime,
250
+ responseTime
251
+ };
252
+ // Track usage asynchronously
253
+ (0, tracking_1.trackUsageAsync)(trackingData);
254
+ logger.debug('Anthropic request completed successfully', {
255
+ requestId,
256
+ model: params.model,
257
+ inputTokens: usage.inputTokens,
258
+ outputTokens: usage.outputTokens,
259
+ duration
260
+ });
261
+ return response;
262
+ }
263
+ // Handle streaming response - need to collect chunks and extract usage
264
+ return handleStreamingResponse(response, {
265
+ requestId,
266
+ model: params.model,
267
+ metadata,
268
+ requestTime,
269
+ startTime
270
+ });
271
+ }
272
+ catch (error) {
273
+ const endTime = Date.now();
274
+ const duration = endTime - startTime;
275
+ const errorContext = (0, error_handling_1.createErrorContext)()
276
+ .withRequestId(requestId)
277
+ .withModel(params.model)
278
+ .withDuration(duration)
279
+ .build();
280
+ (0, error_handling_1.handleError)(error, logger, errorContext);
281
+ throw error;
282
+ }
283
+ }
284
+ /**
285
+ * Patched stream method implementation
286
+ */
287
+ async function* patchedStreamMethod(params, options) {
288
+ const requestId = (0, crypto_1.randomUUID)();
289
+ const startTime = Date.now();
290
+ const requestTime = new Date();
291
+ const responseTime = new Date();
292
+ const chunks = [];
293
+ let firstTokenTime;
294
+ logger.debug('Intercepted Anthropic messages.stream call', {
295
+ requestId,
296
+ model: params.model,
297
+ hasMetadata: !!params.usageMetadata
298
+ });
299
+ // Validate parameters
300
+ const validation = (0, validation_1.validateAnthropicMessageParams)(params);
301
+ if (!validation.isValid) {
302
+ logger.warn('Invalid Anthropic streaming parameters detected', {
303
+ requestId,
304
+ errors: validation.errors,
305
+ warnings: validation.warnings
306
+ });
307
+ }
308
+ // Extract and validate metadata
309
+ const metadata = (0, validation_1.validateUsageMetadata)(params.usageMetadata || {});
310
+ // Remove usageMetadata from params before calling original method
311
+ const { usageMetadata, ...cleanParams } = params;
312
+ try {
313
+ // Call original stream method
314
+ const originalStream = patchingContext.originalMethods?.stream;
315
+ if (!originalStream) {
316
+ throw new error_handling_1.StreamProcessingError('Original stream method not available');
317
+ }
318
+ const stream = originalStream.call(this, cleanParams, options);
319
+ for await (const chunk of stream) {
320
+ // Track first token time
321
+ if (!firstTokenTime && chunk.type === 'content_block_delta') {
322
+ firstTokenTime = Date.now();
323
+ }
324
+ chunks.push(chunk);
325
+ yield chunk;
326
+ }
327
+ const endTime = Date.now();
328
+ const duration = endTime - startTime;
329
+ const timeToFirstToken = firstTokenTime ? firstTokenTime - startTime : undefined;
330
+ // Extract usage information from all chunks
331
+ const usage = (0, tracking_1.extractUsageFromStream)(chunks);
332
+ // Create tracking data
333
+ const trackingData = {
334
+ requestId,
335
+ model: params.model,
336
+ inputTokens: usage.inputTokens,
337
+ outputTokens: usage.outputTokens,
338
+ cacheCreationTokens: usage.cacheCreationTokens,
339
+ cacheReadTokens: usage.cacheReadTokens,
340
+ duration,
341
+ isStreamed: true,
342
+ stopReason: usage.stopReason,
343
+ metadata,
344
+ requestTime,
345
+ responseTime,
346
+ timeToFirstToken
347
+ };
348
+ // Track usage asynchronously
349
+ (0, tracking_1.trackUsageAsync)(trackingData);
350
+ logger.debug('Anthropic streaming request completed successfully', {
351
+ requestId,
352
+ model: params.model,
353
+ inputTokens: usage.inputTokens,
354
+ outputTokens: usage.outputTokens,
355
+ duration,
356
+ timeToFirstToken,
357
+ chunkCount: chunks.length
358
+ });
359
+ }
360
+ catch (error) {
361
+ const endTime = Date.now();
362
+ const duration = endTime - startTime;
363
+ const errorContext = (0, error_handling_1.createErrorContext)()
364
+ .withRequestId(requestId)
365
+ .withModel(params.model)
366
+ .withDuration(duration)
367
+ .with('isStreaming', true)
368
+ .with('chunkCount', chunks.length)
369
+ .build();
370
+ (0, error_handling_1.handleError)(error, logger, errorContext);
371
+ throw error;
372
+ }
373
+ }
374
+ //# sourceMappingURL=wrapper.js.map
@@ -0,0 +1,197 @@
1
+ import { validateReveniumConfig } from './utils/validation.js';
2
+ import { DEFAULT_CONFIG, ENV_VARS, LOGGING_CONFIG } from './constants.js';
3
+ /**
4
+ * Simple console logger implementation for Anthropic middleware
5
+ */
6
+ class ConsoleLogger {
7
+ isDebugEnabled() {
8
+ return process.env[ENV_VARS.DEBUG] === 'true';
9
+ }
10
+ formatMessage(level, message, context) {
11
+ const timestamp = new Date().toISOString();
12
+ const prefix = `[${LOGGING_CONFIG.MIDDLEWARE_NAME}${level === 'DEBUG' ? ' Debug' : ''}]`;
13
+ const contextStr = context ? ` ${JSON.stringify(context)}` : '';
14
+ return `${timestamp} ${prefix} ${message}${contextStr}`;
15
+ }
16
+ debug(message, context) {
17
+ if (this.isDebugEnabled()) {
18
+ console.debug(this.formatMessage('DEBUG', message, context));
19
+ }
20
+ }
21
+ info(message, context) {
22
+ console.info(this.formatMessage('INFO', message, context));
23
+ }
24
+ warn(message, context) {
25
+ console.warn(this.formatMessage('WARN', message, context));
26
+ }
27
+ error(message, context) {
28
+ console.error(this.formatMessage('ERROR', message, context));
29
+ }
30
+ }
31
+ /**
32
+ * Default console logger implementation
33
+ */
34
+ export const defaultLogger = new ConsoleLogger();
35
+ /**
36
+ * Load configuration from environment variables
37
+ */
38
+ function loadConfigFromEnvironment() {
39
+ const env = {
40
+ reveniumApiKey: process.env[ENV_VARS.REVENIUM_API_KEY],
41
+ reveniumBaseUrl: process.env[ENV_VARS.REVENIUM_BASE_URL],
42
+ anthropicApiKey: process.env[ENV_VARS.ANTHROPIC_API_KEY],
43
+ debug: process.env[ENV_VARS.DEBUG] === 'true',
44
+ logLevel: process.env[ENV_VARS.LOG_LEVEL],
45
+ apiTimeout: process.env[ENV_VARS.API_TIMEOUT],
46
+ failSilent: process.env[ENV_VARS.FAIL_SILENT],
47
+ maxRetries: process.env[ENV_VARS.MAX_RETRIES]
48
+ };
49
+ return env;
50
+ }
51
+ /**
52
+ * Convert environment config to Revenium config
53
+ */
54
+ function createConfigFromEnvironment(env) {
55
+ if (!env.reveniumApiKey) {
56
+ return null;
57
+ }
58
+ const apiTimeout = env.apiTimeout ? parseInt(env.apiTimeout, 10) : undefined;
59
+ const failSilent = env.failSilent !== 'false'; // Default to true
60
+ const maxRetries = env.maxRetries ? parseInt(env.maxRetries, 10) : undefined;
61
+ return {
62
+ reveniumApiKey: env.reveniumApiKey,
63
+ reveniumBaseUrl: env.reveniumBaseUrl || DEFAULT_CONFIG.REVENIUM_BASE_URL,
64
+ anthropicApiKey: env.anthropicApiKey,
65
+ apiTimeout,
66
+ failSilent,
67
+ maxRetries
68
+ };
69
+ }
70
+ /**
71
+ * Validate Revenium configuration with enhanced error reporting
72
+ */
73
+ export function validateConfig(config) {
74
+ const validation = validateReveniumConfig(config);
75
+ if (!validation.isValid) {
76
+ // Log detailed validation errors
77
+ getLogger().error('Configuration validation failed', {
78
+ errors: validation.errors,
79
+ warnings: validation.warnings,
80
+ suggestions: validation.suggestions
81
+ });
82
+ // Create detailed error message
83
+ let errorMessage = 'Configuration validation failed:\n';
84
+ validation.errors.forEach((error, index) => {
85
+ errorMessage += ` ${index + 1}. ${error}\n`;
86
+ });
87
+ if (validation.suggestions && validation.suggestions.length > 0) {
88
+ errorMessage += '\nSuggestions:\n';
89
+ validation.suggestions.forEach((suggestion) => {
90
+ errorMessage += ` • ${suggestion}\n`;
91
+ });
92
+ }
93
+ throw new Error(errorMessage.trim());
94
+ }
95
+ // Log warnings if any
96
+ if (validation.warnings && validation.warnings.length > 0) {
97
+ getLogger().warn('Configuration warnings', {
98
+ warnings: validation.warnings
99
+ });
100
+ }
101
+ }
102
+ /**
103
+ * Global configuration instance
104
+ */
105
+ let globalConfig = null;
106
+ let globalLogger = defaultLogger;
107
+ /**
108
+ * Get the current global configuration
109
+ */
110
+ export function getConfig() {
111
+ return globalConfig;
112
+ }
113
+ /**
114
+ * Set the global configuration
115
+ */
116
+ export function setConfig(config) {
117
+ validateConfig(config);
118
+ globalConfig = config;
119
+ globalLogger.debug('Revenium configuration updated', {
120
+ baseUrl: config.reveniumBaseUrl,
121
+ hasApiKey: !!config.reveniumApiKey,
122
+ hasAnthropicKey: !!config.anthropicApiKey
123
+ });
124
+ }
125
+ /**
126
+ * Get the current logger
127
+ */
128
+ export function getLogger() {
129
+ return globalLogger;
130
+ }
131
+ /**
132
+ * Set a custom logger
133
+ */
134
+ export function setLogger(logger) {
135
+ globalLogger = logger;
136
+ globalLogger.debug('Custom logger set for Revenium middleware');
137
+ }
138
+ /**
139
+ * Initialize configuration from environment variables
140
+ */
141
+ export function initializeConfig() {
142
+ const env = loadConfigFromEnvironment();
143
+ const config = createConfigFromEnvironment(env);
144
+ if (config) {
145
+ try {
146
+ setConfig(config);
147
+ globalLogger.debug('Revenium middleware initialized from environment variables');
148
+ return true;
149
+ }
150
+ catch (error) {
151
+ globalLogger.error('Failed to initialize Revenium configuration', {
152
+ error: error instanceof Error ? error.message : String(error)
153
+ });
154
+ return false;
155
+ }
156
+ }
157
+ // Log what's missing for easier debugging
158
+ if (!env.reveniumApiKey) {
159
+ globalLogger.warn(`Revenium middleware not initialized. Missing ${ENV_VARS.REVENIUM_API_KEY} environment variable`);
160
+ globalLogger.info(`Set ${ENV_VARS.REVENIUM_API_KEY} to enable automatic tracking`);
161
+ }
162
+ return false;
163
+ }
164
+ /**
165
+ * Get configuration status
166
+ */
167
+ export function getConfigStatus() {
168
+ if (!globalConfig) {
169
+ return {
170
+ hasConfig: false,
171
+ hasApiKey: false,
172
+ hasAnthropicKey: false,
173
+ baseUrl: ''
174
+ };
175
+ }
176
+ return {
177
+ hasConfig: true,
178
+ hasApiKey: !!globalConfig.reveniumApiKey,
179
+ hasAnthropicKey: !!globalConfig.anthropicApiKey,
180
+ baseUrl: globalConfig.reveniumBaseUrl
181
+ };
182
+ }
183
+ /**
184
+ * Validate current configuration
185
+ */
186
+ export function validateCurrentConfig() {
187
+ if (!globalConfig)
188
+ return false;
189
+ try {
190
+ const validation = validateReveniumConfig(globalConfig);
191
+ return validation.isValid;
192
+ }
193
+ catch {
194
+ return false;
195
+ }
196
+ }
197
+ //# sourceMappingURL=config.js.map