@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,252 @@
1
+ "use strict";
2
+ /**
3
+ * Tracking implementation for Anthropic middleware with resilience patterns
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.sendReveniumMetrics = sendReveniumMetrics;
10
+ exports.trackUsageAsync = trackUsageAsync;
11
+ exports.extractUsageFromResponse = extractUsageFromResponse;
12
+ exports.extractUsageFromStream = extractUsageFromStream;
13
+ const node_fetch_1 = __importDefault(require("node-fetch"));
14
+ const config_1 = require("./config");
15
+ const circuit_breaker_1 = require("./utils/circuit-breaker");
16
+ const error_handling_1 = require("./utils/error-handling");
17
+ const constants_1 = require("./constants");
18
+ // Global logger
19
+ const logger = (0, config_1.getLogger)();
20
+ /**
21
+ * Send tracking data to Revenium API with resilience patterns
22
+ */
23
+ async function sendReveniumMetrics(data) {
24
+ const config = (0, config_1.getConfig)();
25
+ if (!config)
26
+ throw new Error("Revenium configuration not available");
27
+ const requestId = data.requestId;
28
+ logger.debug("Preparing to send metrics to Revenium", {
29
+ requestId,
30
+ model: data.model,
31
+ inputTokens: data.inputTokens,
32
+ outputTokens: data.outputTokens,
33
+ duration: data.duration,
34
+ isStreamed: data.isStreamed,
35
+ });
36
+ // Build payload using exact structure from working implementations
37
+ const payload = buildReveniumPayload(data);
38
+ // Create request options
39
+ const requestOptions = {
40
+ method: "POST",
41
+ headers: {
42
+ "Content-Type": "application/json",
43
+ "x-api-key": config.reveniumApiKey,
44
+ "User-Agent": constants_1.LOGGING_CONFIG.USER_AGENT,
45
+ },
46
+ body: JSON.stringify(payload),
47
+ timeout: config.apiTimeout || constants_1.DEFAULT_CONFIG.API_TIMEOUT,
48
+ };
49
+ // Add abort signal for timeout
50
+ const controller = new AbortController();
51
+ const timeoutId = setTimeout(() => controller.abort(), requestOptions.timeout);
52
+ requestOptions.signal = controller.signal;
53
+ // Handle abort signal errors to prevent unhandled error events
54
+ controller.signal.addEventListener("abort", () => {
55
+ // Silently handle abort - this is expected behavior for timeouts
56
+ });
57
+ try {
58
+ // Execute with circuit breaker and retry logic
59
+ await (0, circuit_breaker_1.executeWithCircuitBreaker)(async () => {
60
+ return (0, error_handling_1.withRetry)(async () => {
61
+ logger.debug("Sending request to Revenium API", {
62
+ requestId,
63
+ url: `${config.reveniumBaseUrl}${constants_1.API_ENDPOINTS.AI_COMPLETIONS}`,
64
+ payloadSize: requestOptions.body.length,
65
+ });
66
+ let response;
67
+ try {
68
+ response = await (0, node_fetch_1.default)(`${config.reveniumBaseUrl}${constants_1.API_ENDPOINTS.AI_COMPLETIONS}`, requestOptions);
69
+ }
70
+ catch (fetchError) {
71
+ // Handle AbortError and other fetch errors
72
+ if (fetchError instanceof Error && fetchError.name === "AbortError") {
73
+ throw new Error(`Request timeout after ${requestOptions.timeout}ms`);
74
+ }
75
+ throw fetchError;
76
+ }
77
+ if (!response.ok) {
78
+ const responseText = await response
79
+ .text()
80
+ .catch(() => "Unable to read response");
81
+ const errorContext = (0, error_handling_1.createErrorContext)()
82
+ .withRequestId(requestId)
83
+ .withModel(data.model)
84
+ .withStatus(response.status)
85
+ .with("responseBody", responseText)
86
+ .build();
87
+ throw new error_handling_1.ReveniumApiError(`Revenium API error: ${response.status} ${response.statusText}`, response.status, responseText, errorContext);
88
+ }
89
+ logger.debug("Successfully sent metrics to Revenium", {
90
+ requestId,
91
+ status: response.status,
92
+ duration: data.duration,
93
+ });
94
+ return response;
95
+ }, config.maxRetries || constants_1.DEFAULT_CONFIG.MAX_RETRIES);
96
+ });
97
+ }
98
+ catch (error) {
99
+ const errorContext = (0, error_handling_1.createErrorContext)()
100
+ .withRequestId(requestId)
101
+ .withModel(data.model)
102
+ .withDuration(data.duration)
103
+ .build();
104
+ (0, error_handling_1.handleError)(error, logger, errorContext);
105
+ // Always fail silently for tracking errors to prevent breaking user's application
106
+ // Tracking errors should never break the user's main application flow
107
+ }
108
+ finally {
109
+ clearTimeout(timeoutId);
110
+ }
111
+ }
112
+ /**
113
+ * Build Revenium payload from tracking data
114
+ */
115
+ function buildReveniumPayload(data) {
116
+ const now = new Date().toISOString();
117
+ const requestTime = data.requestTime.toISOString();
118
+ const completionStartTime = data.responseTime.toISOString();
119
+ return {
120
+ stopReason: getStopReason(data.stopReason),
121
+ costType: "AI",
122
+ isStreamed: data.isStreamed,
123
+ taskType: data.metadata?.taskType,
124
+ agent: data.metadata?.agent,
125
+ operationType: "CHAT",
126
+ inputTokenCount: data.inputTokens,
127
+ outputTokenCount: data.outputTokens,
128
+ reasoningTokenCount: 0, // Anthropic doesn't currently have reasoning tokens
129
+ cacheCreationTokenCount: data.cacheCreationTokens || 0,
130
+ cacheReadTokenCount: data.cacheReadTokens || 0,
131
+ totalTokenCount: data.inputTokens + data.outputTokens,
132
+ organizationId: data.metadata?.organizationId,
133
+ productId: data.metadata?.productId,
134
+ subscriber: data.metadata?.subscriber, // Pass through nested subscriber object directly
135
+ subscriptionId: data.metadata?.subscriptionId,
136
+ model: data.model,
137
+ transactionId: data.requestId,
138
+ responseTime: now,
139
+ requestDuration: Math.round(data.duration),
140
+ provider: "Anthropic",
141
+ requestTime: requestTime,
142
+ completionStartTime: completionStartTime,
143
+ timeToFirstToken: data.timeToFirstToken || 0,
144
+ traceId: data.metadata?.traceId,
145
+ middlewareSource: "nodejs",
146
+ };
147
+ }
148
+ /**
149
+ * Normalize stop reason for Revenium API
150
+ */
151
+ function getStopReason(stopReason) {
152
+ if (!stopReason)
153
+ return "END";
154
+ // Use predefined mapping from constants
155
+ return (constants_1.ANTHROPIC_PATTERNS.REVENIUM_STOP_REASON_MAP[stopReason] || "END");
156
+ }
157
+ /**
158
+ * Fire-and-forget async tracking wrapper
159
+ * Ensures tracking never blocks the main application flow
160
+ */
161
+ function trackUsageAsync(trackingData) {
162
+ const config = (0, config_1.getConfig)();
163
+ if (!config)
164
+ return logger.warn("Revenium configuration not available - skipping tracking", {
165
+ requestId: trackingData.requestId,
166
+ });
167
+ // Run tracking in background without awaiting
168
+ sendReveniumMetrics(trackingData)
169
+ .then(() => {
170
+ logger.debug("Revenium tracking completed successfully", {
171
+ requestId: trackingData.requestId,
172
+ model: trackingData.model,
173
+ totalTokens: trackingData.inputTokens + trackingData.outputTokens,
174
+ });
175
+ })
176
+ .catch((error) => {
177
+ const errorContext = (0, error_handling_1.createErrorContext)()
178
+ .withRequestId(trackingData.requestId)
179
+ .withModel(trackingData.model)
180
+ .build();
181
+ logger.warn("Revenium tracking failed", {
182
+ error: error instanceof Error ? error.message : String(error),
183
+ requestId: trackingData.requestId,
184
+ context: errorContext,
185
+ });
186
+ });
187
+ }
188
+ /**
189
+ * Extract usage data from Anthropic response
190
+ */
191
+ function extractUsageFromResponse(response) {
192
+ const usage = response.usage || {};
193
+ return {
194
+ inputTokens: usage.input_tokens || 0,
195
+ outputTokens: usage.output_tokens || 0,
196
+ cacheCreationTokens: usage.cache_creation_input_tokens,
197
+ cacheReadTokens: usage.cache_read_input_tokens,
198
+ stopReason: response.stop_reason,
199
+ };
200
+ }
201
+ /**
202
+ * Extract usage data from streaming chunks
203
+ */
204
+ function extractUsageFromStream(chunks) {
205
+ let inputTokens = 0;
206
+ let outputTokens = 0;
207
+ let cacheCreationTokens;
208
+ let cacheReadTokens;
209
+ let stopReason;
210
+ for (const chunk of chunks) {
211
+ let usage = null;
212
+ // According to Anthropic docs, usage data appears in:
213
+ // 1. message_start event: chunk.message.usage (initial token count)
214
+ // 2. message_delta event: chunk.usage (final token count)
215
+ // 3. Direct usage field on chunk
216
+ if (chunk?.type === "message_start" && chunk?.message?.usage) {
217
+ usage = chunk?.message?.usage;
218
+ }
219
+ else if (chunk?.usage) {
220
+ usage = chunk?.usage;
221
+ }
222
+ else if (chunk?.delta?.usage) {
223
+ usage = chunk?.delta?.usage;
224
+ }
225
+ //Verify usage with optional chaining
226
+ // Use the highest token counts found (message_delta should have final counts)
227
+ if (usage?.input_tokens) {
228
+ inputTokens = Math.max(inputTokens, usage?.input_tokens);
229
+ }
230
+ if (usage?.output_tokens) {
231
+ outputTokens = Math.max(outputTokens, usage?.output_tokens);
232
+ }
233
+ if (usage?.cache_creation_input_tokens) {
234
+ cacheCreationTokens = usage?.cache_creation_input_tokens;
235
+ }
236
+ if (usage?.cache_read_input_tokens) {
237
+ cacheReadTokens = usage?.cache_read_input_tokens;
238
+ }
239
+ // Extract stop reason from delta
240
+ if (chunk?.delta?.stop_reason) {
241
+ stopReason = chunk?.delta?.stop_reason;
242
+ }
243
+ }
244
+ return {
245
+ inputTokens,
246
+ outputTokens,
247
+ cacheCreationTokens,
248
+ cacheReadTokens,
249
+ stopReason,
250
+ };
251
+ }
252
+ //# sourceMappingURL=tracking.js.map
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ /**
3
+ * TypeScript module augmentation for Anthropic SDK
4
+ *
5
+ * This file extends Anthropic's existing types to include the usageMetadata field
6
+ * through TypeScript's declaration merging feature. This allows developers to
7
+ * use usageMetadata directly in Anthropic API calls without type casting or
8
+ * TypeScript errors.
9
+ *
10
+ * ## What is Module Augmentation?
11
+ *
12
+ * Module augmentation is a TypeScript feature that allows you to extend existing
13
+ * interfaces from external libraries. When you import this middleware, TypeScript
14
+ * automatically recognizes the usageMetadata field as a valid parameter.
15
+ *
16
+ * ## Benefits:
17
+ * - **Type Safety**: Full IntelliSense support for usageMetadata
18
+ * - **No Type Casting**: Use usageMetadata directly without `as any`
19
+ * - **Automatic Validation**: TypeScript validates the structure at compile time
20
+ * - **Better Developer Experience**: Auto-completion and error detection
21
+ *
22
+ * ## Usage Examples:
23
+ *
24
+ * ### Basic Usage:
25
+ * ```typescript
26
+ * import 'revenium-middleware-anthropic-node';
27
+ * import Anthropic from '@anthropic-ai/sdk';
28
+ *
29
+ * const anthropic = new Anthropic();
30
+ *
31
+ * const response = await anthropic.messages.create({
32
+ * model: 'claude-3-5-sonnet-latest',
33
+ * max_tokens: 1024,
34
+ * messages: [{ role: 'user', content: 'Hello!' }],
35
+ * usageMetadata: { // TypeScript recognizes this natively
36
+ * subscriber: { id: 'user-123', email: 'user@example.com' },
37
+ * organizationId: 'my-company',
38
+ * taskType: 'customer-support',
39
+ * traceId: 'session-abc-123'
40
+ * }
41
+ * });
42
+ * ```
43
+ *
44
+ * ### Streaming Usage:
45
+ * ```typescript
46
+ * const stream = await anthropic.messages.stream({
47
+ * model: 'claude-3-5-sonnet-latest',
48
+ * max_tokens: 1024,
49
+ * messages: [{ role: 'user', content: 'Generate a report' }],
50
+ * usageMetadata: {
51
+ * taskType: 'content-generation',
52
+ * productId: 'report-generator',
53
+ * responseQualityScore: 0.95
54
+ * }
55
+ * });
56
+ * ```
57
+ *
58
+ * ### Advanced Usage with All Fields:
59
+ * ```typescript
60
+ * const response = await anthropic.messages.create({
61
+ * model: 'claude-3-5-sonnet-latest',
62
+ * max_tokens: 2048,
63
+ * messages: [{ role: 'user', content: 'Complex analysis task' }],
64
+ * usageMetadata: {
65
+ * subscriber: {
66
+ * id: 'user-456',
67
+ * email: 'analyst@company.com',
68
+ * credential: { name: 'api-key', value: 'sk-...' }
69
+ * },
70
+ * traceId: 'analysis-session-789',
71
+ * taskId: 'task-001',
72
+ * taskType: 'data-analysis',
73
+ * organizationId: 'enterprise-client',
74
+ * subscriptionId: 'premium-plan',
75
+ * productId: 'analytics-suite',
76
+ * agent: 'data-analyst-bot',
77
+ * responseQualityScore: 0.98,
78
+ * customField: 'custom-value' // Extensible with custom fields
79
+ * }
80
+ * });
81
+ * ```
82
+ *
83
+ * @public
84
+ * @since 1.1.0
85
+ */
86
+ Object.defineProperty(exports, "__esModule", { value: true });
87
+ //# sourceMappingURL=anthropic-augmentation.js.map
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * Type definitions for Anthropic middleware
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ /**
3
+ * Circuit breaker pattern implementation for handling repeated failures
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CircuitBreaker = exports.DEFAULT_CIRCUIT_CONFIG = exports.CircuitState = void 0;
7
+ exports.getCircuitBreaker = getCircuitBreaker;
8
+ exports.resetCircuitBreaker = resetCircuitBreaker;
9
+ exports.canExecuteRequest = canExecuteRequest;
10
+ exports.executeWithCircuitBreaker = executeWithCircuitBreaker;
11
+ exports.getCircuitBreakerStats = getCircuitBreakerStats;
12
+ const constants_1 = require("../constants");
13
+ /**
14
+ * Circuit breaker states
15
+ */
16
+ var CircuitState;
17
+ (function (CircuitState) {
18
+ CircuitState["CLOSED"] = "CLOSED";
19
+ CircuitState["OPEN"] = "OPEN";
20
+ CircuitState["HALF_OPEN"] = "HALF_OPEN"; // Testing if service recovered
21
+ })(CircuitState || (exports.CircuitState = CircuitState = {}));
22
+ /**
23
+ * Default circuit breaker configuration
24
+ */
25
+ exports.DEFAULT_CIRCUIT_CONFIG = {
26
+ failureThreshold: constants_1.CIRCUIT_BREAKER_CONFIG.FAILURE_THRESHOLD,
27
+ recoveryTimeout: constants_1.CIRCUIT_BREAKER_CONFIG.RECOVERY_TIMEOUT,
28
+ successThreshold: constants_1.CIRCUIT_BREAKER_CONFIG.SUCCESS_THRESHOLD,
29
+ timeWindow: constants_1.CIRCUIT_BREAKER_CONFIG.TIME_WINDOW
30
+ };
31
+ /**
32
+ * Circuit breaker implementation
33
+ */
34
+ class CircuitBreaker {
35
+ constructor(config = exports.DEFAULT_CIRCUIT_CONFIG) {
36
+ this.config = config;
37
+ this.state = CircuitState.CLOSED;
38
+ this.failureCount = 0;
39
+ this.successCount = 0;
40
+ this.lastFailureTime = 0;
41
+ this.failures = []; // Timestamps of failures
42
+ this.lastCleanupTime = 0;
43
+ this.maxFailureHistorySize = constants_1.CIRCUIT_BREAKER_CONFIG.MAX_FAILURE_HISTORY_SIZE;
44
+ this.lastCleanupTime = Date.now();
45
+ }
46
+ /**
47
+ * Execute a function with circuit breaker protection
48
+ */
49
+ async execute(fn) {
50
+ if (this.state === CircuitState.OPEN) {
51
+ if (this.shouldAttemptRecovery()) {
52
+ this.state = CircuitState.HALF_OPEN;
53
+ this.successCount = 0;
54
+ }
55
+ else {
56
+ throw new Error('Circuit breaker is OPEN - failing fast');
57
+ }
58
+ }
59
+ try {
60
+ const result = await fn();
61
+ this.onSuccess();
62
+ return result;
63
+ }
64
+ catch (error) {
65
+ this.onFailure();
66
+ throw error;
67
+ }
68
+ }
69
+ /**
70
+ * Check if circuit breaker allows execution
71
+ */
72
+ canExecute() {
73
+ if (this.state === CircuitState.CLOSED || this.state === CircuitState.HALF_OPEN)
74
+ return true;
75
+ if (this.state === CircuitState.OPEN && this.shouldAttemptRecovery())
76
+ return true;
77
+ return false;
78
+ }
79
+ /**
80
+ * Get current circuit breaker state
81
+ */
82
+ getState() {
83
+ return this.state;
84
+ }
85
+ /**
86
+ * Get circuit breaker statistics
87
+ */
88
+ getStats() {
89
+ const now = Date.now();
90
+ const recentFailures = this.failures.filter(timestamp => now - timestamp < this.config.timeWindow).length;
91
+ let timeUntilRecovery;
92
+ if (this.state === CircuitState.OPEN) {
93
+ const timeSinceLastFailure = now - this.lastFailureTime;
94
+ timeUntilRecovery = Math.max(0, this.config.recoveryTimeout - timeSinceLastFailure);
95
+ }
96
+ return {
97
+ state: this.state,
98
+ failureCount: this.failureCount,
99
+ successCount: this.successCount,
100
+ recentFailures,
101
+ timeUntilRecovery
102
+ };
103
+ }
104
+ /**
105
+ * Reset circuit breaker to closed state
106
+ */
107
+ reset() {
108
+ this.state = CircuitState.CLOSED;
109
+ this.failureCount = 0;
110
+ this.successCount = 0;
111
+ this.lastFailureTime = 0;
112
+ this.lastCleanupTime = Date.now();
113
+ this.failures = [];
114
+ }
115
+ /**
116
+ * Handle successful execution
117
+ */
118
+ onSuccess() {
119
+ if (this.state === CircuitState.HALF_OPEN) {
120
+ this.successCount++;
121
+ if (this.successCount >= this.config.successThreshold) {
122
+ this.state = CircuitState.CLOSED;
123
+ this.failureCount = 0;
124
+ this.failures = [];
125
+ }
126
+ }
127
+ else if (this.state === CircuitState.CLOSED) {
128
+ // Reset failure count on success in closed state
129
+ this.failureCount = 0;
130
+ this.performPeriodicCleanup();
131
+ }
132
+ }
133
+ /**
134
+ * Handle failed execution
135
+ */
136
+ onFailure() {
137
+ const now = Date.now();
138
+ this.failureCount++;
139
+ this.lastFailureTime = now;
140
+ // Prevent unbounded growth of failures array
141
+ if (this.failures.length >= this.maxFailureHistorySize) {
142
+ this.failures = this.failures.slice(-Math.floor(this.maxFailureHistorySize / 2));
143
+ }
144
+ this.failures.push(now);
145
+ this.cleanupOldFailures();
146
+ if (this.state === CircuitState.HALF_OPEN) {
147
+ // Go back to open state on any failure in half-open
148
+ this.state = CircuitState.OPEN;
149
+ this.successCount = 0;
150
+ }
151
+ else if (this.state === CircuitState.CLOSED) {
152
+ // Check if we should open the circuit
153
+ const recentFailures = this.failures.filter(timestamp => now - timestamp < this.config.timeWindow).length;
154
+ if (recentFailures >= this.config.failureThreshold) {
155
+ this.state = CircuitState.OPEN;
156
+ }
157
+ }
158
+ }
159
+ /**
160
+ * Check if we should attempt recovery from open state
161
+ */
162
+ shouldAttemptRecovery() {
163
+ const now = Date.now();
164
+ return now - this.lastFailureTime >= this.config.recoveryTimeout;
165
+ }
166
+ /**
167
+ * Remove old failure timestamps outside the time window
168
+ */
169
+ cleanupOldFailures() {
170
+ const now = Date.now();
171
+ this.failures = this.failures.filter(timestamp => now - timestamp < this.config.timeWindow);
172
+ }
173
+ /**
174
+ * Perform periodic cleanup to prevent memory leaks
175
+ * Only runs cleanup if enough time has passed since last cleanup
176
+ */
177
+ performPeriodicCleanup() {
178
+ const now = Date.now();
179
+ const timeSinceLastCleanup = now - this.lastCleanupTime;
180
+ // Use constants for cleanup thresholds
181
+ if (timeSinceLastCleanup > constants_1.CIRCUIT_BREAKER_CONFIG.CLEANUP_INTERVAL ||
182
+ this.failures.length > constants_1.CIRCUIT_BREAKER_CONFIG.CLEANUP_SIZE_THRESHOLD) {
183
+ this.cleanupOldFailures();
184
+ this.lastCleanupTime = now;
185
+ }
186
+ }
187
+ }
188
+ exports.CircuitBreaker = CircuitBreaker;
189
+ /**
190
+ * Global circuit breaker instance for Revenium API calls
191
+ */
192
+ let globalCircuitBreaker = null;
193
+ /**
194
+ * Get or create the global circuit breaker instance
195
+ */
196
+ function getCircuitBreaker(config) {
197
+ if (!globalCircuitBreaker) {
198
+ const finalConfig = config ? { ...exports.DEFAULT_CIRCUIT_CONFIG, ...config } : exports.DEFAULT_CIRCUIT_CONFIG;
199
+ globalCircuitBreaker = new CircuitBreaker(finalConfig);
200
+ }
201
+ return globalCircuitBreaker;
202
+ }
203
+ /**
204
+ * Reset the global circuit breaker
205
+ */
206
+ function resetCircuitBreaker() {
207
+ if (globalCircuitBreaker) {
208
+ globalCircuitBreaker.reset();
209
+ }
210
+ }
211
+ /**
212
+ * Check if the global circuit breaker allows execution
213
+ */
214
+ function canExecuteRequest() {
215
+ const circuitBreaker = getCircuitBreaker();
216
+ return circuitBreaker.canExecute();
217
+ }
218
+ /**
219
+ * Execute a function with global circuit breaker protection
220
+ */
221
+ async function executeWithCircuitBreaker(fn) {
222
+ const circuitBreaker = getCircuitBreaker();
223
+ return circuitBreaker.execute(fn);
224
+ }
225
+ /**
226
+ * Get global circuit breaker statistics
227
+ */
228
+ function getCircuitBreakerStats() {
229
+ const circuitBreaker = getCircuitBreaker();
230
+ return circuitBreaker.getStats();
231
+ }
232
+ //# sourceMappingURL=circuit-breaker.js.map