@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,233 @@
1
+ "use strict";
2
+ /**
3
+ * Error handling utilities for Anthropic middleware with structured errors and context building
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ErrorContext = exports.StreamProcessingError = exports.ValidationError = exports.ReveniumApiError = exports.RequestProcessingError = exports.AnthropicPatchingError = exports.ConfigurationError = exports.ReveniumError = void 0;
7
+ exports.getErrorMessage = getErrorMessage;
8
+ exports.getErrorStack = getErrorStack;
9
+ exports.createErrorContext = createErrorContext;
10
+ exports.handleError = handleError;
11
+ exports.withErrorHandling = withErrorHandling;
12
+ exports.withRetry = withRetry;
13
+ const constants_1 = require("../constants");
14
+ /**
15
+ * Base error class for Revenium middleware errors
16
+ */
17
+ class ReveniumError extends Error {
18
+ constructor(message, code, context) {
19
+ super(message);
20
+ this.name = 'ReveniumError';
21
+ this.code = code;
22
+ this.context = context;
23
+ // Maintain proper stack trace for where our error was thrown
24
+ if (Error.captureStackTrace) {
25
+ Error.captureStackTrace(this, ReveniumError);
26
+ }
27
+ }
28
+ }
29
+ exports.ReveniumError = ReveniumError;
30
+ /**
31
+ * Configuration-related errors
32
+ */
33
+ class ConfigurationError extends ReveniumError {
34
+ constructor(message, context) {
35
+ super(message, 'CONFIGURATION_ERROR', context);
36
+ this.name = 'ConfigurationError';
37
+ }
38
+ }
39
+ exports.ConfigurationError = ConfigurationError;
40
+ /**
41
+ * Anthropic SDK patching errors
42
+ */
43
+ class AnthropicPatchingError extends ReveniumError {
44
+ constructor(message, context) {
45
+ super(message, 'ANTHROPIC_PATCHING_ERROR', context);
46
+ this.name = 'AnthropicPatchingError';
47
+ }
48
+ }
49
+ exports.AnthropicPatchingError = AnthropicPatchingError;
50
+ /**
51
+ * Request processing errors
52
+ */
53
+ class RequestProcessingError extends ReveniumError {
54
+ constructor(message, context) {
55
+ super(message, 'REQUEST_PROCESSING_ERROR', context);
56
+ this.name = 'RequestProcessingError';
57
+ }
58
+ }
59
+ exports.RequestProcessingError = RequestProcessingError;
60
+ /**
61
+ * Revenium API communication errors
62
+ */
63
+ class ReveniumApiError extends ReveniumError {
64
+ constructor(message, statusCode, responseBody, context) {
65
+ super(message, 'REVENIUM_API_ERROR', context);
66
+ this.name = 'ReveniumApiError';
67
+ this.statusCode = statusCode;
68
+ this.responseBody = responseBody;
69
+ }
70
+ }
71
+ exports.ReveniumApiError = ReveniumApiError;
72
+ /**
73
+ * Validation errors
74
+ */
75
+ class ValidationError extends ReveniumError {
76
+ constructor(message, validationErrors, context) {
77
+ super(message, 'VALIDATION_ERROR', context);
78
+ this.name = 'ValidationError';
79
+ this.validationErrors = validationErrors;
80
+ }
81
+ }
82
+ exports.ValidationError = ValidationError;
83
+ /**
84
+ * Stream processing errors
85
+ */
86
+ class StreamProcessingError extends ReveniumError {
87
+ constructor(message, context) {
88
+ super(message, 'STREAM_PROCESSING_ERROR', context);
89
+ this.name = 'StreamProcessingError';
90
+ }
91
+ }
92
+ exports.StreamProcessingError = StreamProcessingError;
93
+ /**
94
+ * Error context builder for consistent error reporting
95
+ */
96
+ class ErrorContext {
97
+ constructor() {
98
+ this.context = {};
99
+ }
100
+ /**
101
+ * Add request ID to error context
102
+ */
103
+ withRequestId(requestId) {
104
+ this.context.requestId = requestId;
105
+ return this;
106
+ }
107
+ /**
108
+ * Add model information to error context
109
+ */
110
+ withModel(model) {
111
+ this.context.model = model;
112
+ return this;
113
+ }
114
+ /**
115
+ * Add duration to error context
116
+ */
117
+ withDuration(duration) {
118
+ this.context.duration = duration;
119
+ return this;
120
+ }
121
+ /**
122
+ * Add HTTP status to error context
123
+ */
124
+ withStatus(status) {
125
+ this.context.status = status;
126
+ return this;
127
+ }
128
+ /**
129
+ * Add custom field to error context
130
+ */
131
+ with(key, value) {
132
+ this.context[key] = value;
133
+ return this;
134
+ }
135
+ /**
136
+ * Build the context object
137
+ */
138
+ build() {
139
+ return { ...this.context };
140
+ }
141
+ }
142
+ exports.ErrorContext = ErrorContext;
143
+ /**
144
+ * Safe error message extraction
145
+ */
146
+ function getErrorMessage(error) {
147
+ if (error instanceof Error)
148
+ return error.message;
149
+ if (typeof error === 'string')
150
+ return error;
151
+ if (error && typeof error === 'object' && 'message' in error) {
152
+ const message = error.message;
153
+ if (typeof message === 'string')
154
+ return message;
155
+ }
156
+ return 'Unknown error occurred';
157
+ }
158
+ /**
159
+ * Safe error stack extraction
160
+ */
161
+ function getErrorStack(error) {
162
+ if (error instanceof Error && error.stack)
163
+ return error.stack;
164
+ return;
165
+ }
166
+ /**
167
+ * Create error context builder
168
+ */
169
+ function createErrorContext() {
170
+ return new ErrorContext();
171
+ }
172
+ /**
173
+ * Handle and log errors consistently
174
+ */
175
+ function handleError(error, logger, context) {
176
+ const message = getErrorMessage(error);
177
+ const stack = getErrorStack(error);
178
+ const logContext = {
179
+ ...context,
180
+ error: message,
181
+ stack: stack
182
+ };
183
+ if (error instanceof ReveniumError) {
184
+ logContext.errorCode = error.code;
185
+ logContext.errorContext = error.context;
186
+ if (error instanceof ReveniumApiError) {
187
+ logContext.statusCode = error.statusCode;
188
+ logContext.responseBody = error.responseBody;
189
+ }
190
+ if (error instanceof ValidationError) {
191
+ logContext.validationErrors = error.validationErrors;
192
+ }
193
+ }
194
+ logger.error('Error occurred', logContext);
195
+ }
196
+ /**
197
+ * Wrap async function with error handling
198
+ */
199
+ function withErrorHandling(fn, errorHandler) {
200
+ return async (...args) => {
201
+ try {
202
+ return await fn(...args);
203
+ }
204
+ catch (error) {
205
+ errorHandler(error, ...args);
206
+ return undefined;
207
+ }
208
+ };
209
+ }
210
+ /**
211
+ * Retry function with exponential backoff
212
+ */
213
+ async function withRetry(fn, maxRetries = constants_1.RETRY_CONFIG.BASE_DELAY / 1000, // Convert to attempts for backward compatibility
214
+ baseDelay = constants_1.RETRY_CONFIG.BASE_DELAY) {
215
+ let lastError;
216
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
217
+ try {
218
+ return await fn();
219
+ }
220
+ catch (error) {
221
+ lastError = error;
222
+ if (attempt === maxRetries) {
223
+ break;
224
+ }
225
+ // Exponential backoff with jitter using constants
226
+ const delay = Math.min(baseDelay * Math.pow(2, attempt), constants_1.RETRY_CONFIG.MAX_DELAY);
227
+ const jitter = Math.random() * constants_1.RETRY_CONFIG.JITTER_FACTOR * delay;
228
+ await new Promise(resolve => setTimeout(resolve, delay + jitter));
229
+ }
230
+ }
231
+ throw lastError;
232
+ }
233
+ //# sourceMappingURL=error-handling.js.map
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ /**
3
+ * Validation utilities for Anthropic middleware
4
+ * Provides type-safe validation with detailed error reporting
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.isObject = isObject;
8
+ exports.isString = isString;
9
+ exports.isNumber = isNumber;
10
+ exports.isBoolean = isBoolean;
11
+ exports.validateString = validateString;
12
+ exports.validateNumber = validateNumber;
13
+ exports.validateUsageMetadata = validateUsageMetadata;
14
+ exports.validateReveniumConfig = validateReveniumConfig;
15
+ exports.validateAnthropicMessageParams = validateAnthropicMessageParams;
16
+ const constants_1 = require("../constants");
17
+ /**
18
+ * Type guard for checking if a value is a non-null object
19
+ */
20
+ function isObject(value) {
21
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
22
+ }
23
+ /**
24
+ * Type guard for checking if a value is a string
25
+ */
26
+ function isString(value) {
27
+ return typeof value === 'string';
28
+ }
29
+ /**
30
+ * Type guard for checking if a value is a number
31
+ */
32
+ function isNumber(value) {
33
+ return typeof value === 'number' && !isNaN(value);
34
+ }
35
+ /**
36
+ * Type guard for checking if a value is a boolean
37
+ */
38
+ function isBoolean(value) {
39
+ return typeof value === 'boolean';
40
+ }
41
+ /**
42
+ * Validate and extract string from unknown value
43
+ */
44
+ function validateString(value, defaultValue = '') {
45
+ return isString(value) ? value : defaultValue;
46
+ }
47
+ /**
48
+ * Validate and extract number from unknown value
49
+ */
50
+ function validateNumber(value, defaultValue = 0) {
51
+ return isNumber(value) ? value : defaultValue;
52
+ }
53
+ /**
54
+ * Validate usage metadata object with Anthropic-specific considerations
55
+ */
56
+ function validateUsageMetadata(metadata) {
57
+ if (!isObject(metadata)) {
58
+ return {};
59
+ }
60
+ const validated = {};
61
+ // Validate string fields
62
+ const stringFields = [
63
+ 'traceId', 'taskId', 'taskType', 'subscriberEmail', 'subscriberId',
64
+ 'subscriberCredentialName', 'subscriberCredential', 'organizationId',
65
+ 'subscriptionId', 'productId', 'agent'
66
+ ];
67
+ for (const field of stringFields) {
68
+ const value = metadata[field];
69
+ if (isString(value) && value.trim().length > 0) {
70
+ validated[field] = value.trim();
71
+ }
72
+ }
73
+ // Validate nested subscriber object
74
+ const subscriberData = metadata.subscriber;
75
+ if (isObject(subscriberData)) {
76
+ const subscriber = {};
77
+ // Validate subscriber.id
78
+ if (isString(subscriberData?.id) && subscriberData?.id?.trim()?.length > 0) {
79
+ subscriber.id = subscriberData?.id?.trim();
80
+ }
81
+ // Validate subscriber.email
82
+ if (isString(subscriberData?.email) && subscriberData?.email?.trim()?.length > 0) {
83
+ subscriber.email = subscriberData?.email?.trim();
84
+ }
85
+ // Validate subscriber.credential object
86
+ const credentialData = subscriberData?.credential;
87
+ if (isObject(credentialData)) {
88
+ const credential = {};
89
+ if (isString(credentialData?.name) && credentialData?.name?.trim()?.length > 0) {
90
+ credential.name = credentialData?.name?.trim();
91
+ }
92
+ if (isString(credentialData?.value) && credentialData?.value?.trim()?.length > 0) {
93
+ credential.value = credentialData?.value?.trim();
94
+ }
95
+ // Only include credential if it has at least name or value
96
+ if (credential?.name || credential?.value) {
97
+ subscriber.credential = credential;
98
+ }
99
+ }
100
+ // Only include subscriber if it has at least one field
101
+ if (subscriber?.id || subscriber?.email || subscriber?.credential) {
102
+ validated.subscriber = subscriber;
103
+ }
104
+ }
105
+ // Validate number fields
106
+ const responseQualityScore = metadata.responseQualityScore;
107
+ if (isNumber(responseQualityScore) && responseQualityScore >= 0 && responseQualityScore <= 1) {
108
+ validated.responseQualityScore = responseQualityScore;
109
+ }
110
+ return validated;
111
+ }
112
+ /**
113
+ * Comprehensive Revenium configuration validation for Anthropic
114
+ */
115
+ function validateReveniumConfig(config) {
116
+ const errors = [];
117
+ const warnings = [];
118
+ const suggestions = [];
119
+ if (!isObject(config)) {
120
+ return {
121
+ isValid: false,
122
+ errors: ['Configuration must be an object'],
123
+ warnings: [],
124
+ suggestions: ['Ensure you are passing a valid configuration object with required fields']
125
+ };
126
+ }
127
+ const cfg = config;
128
+ // Validate required Revenium API key
129
+ if (!isString(cfg?.reveniumApiKey)) {
130
+ errors.push('reveniumApiKey is required and must be a string');
131
+ suggestions.push('Set REVENIUM_METERING_API_KEY environment variable or provide reveniumApiKey in config');
132
+ }
133
+ else if (!cfg?.reveniumApiKey?.trim()) {
134
+ errors.push('reveniumApiKey cannot be empty');
135
+ }
136
+ else if (!cfg?.reveniumApiKey?.startsWith(constants_1.VALIDATION_CONFIG.REVENIUM_API_KEY_PREFIX)) {
137
+ errors.push(`reveniumApiKey must start with "${constants_1.VALIDATION_CONFIG.REVENIUM_API_KEY_PREFIX}"`);
138
+ suggestions.push('Obtain a valid Revenium API key from your Revenium dashboard');
139
+ }
140
+ else if (cfg?.reveniumApiKey?.length < constants_1.VALIDATION_CONFIG.MIN_API_KEY_LENGTH) {
141
+ warnings.push('reveniumApiKey appears to be too short - verify it is correct');
142
+ }
143
+ // Validate Revenium base URL
144
+ if (!isString(cfg?.reveniumBaseUrl)) {
145
+ errors.push('reveniumBaseUrl is required and must be a string');
146
+ }
147
+ else if (!cfg?.reveniumBaseUrl?.trim()) {
148
+ errors.push('reveniumBaseUrl cannot be empty');
149
+ }
150
+ else {
151
+ try {
152
+ const url = new URL(cfg?.reveniumBaseUrl);
153
+ if (!url.protocol.startsWith('http')) {
154
+ errors.push('reveniumBaseUrl must use HTTP or HTTPS protocol');
155
+ }
156
+ // Check for localhost/development hostnames (IPv4, IPv6, and named)
157
+ const localhostHostnames = ['localhost', '127.0.0.1', '::1', '[::1]'];
158
+ if (localhostHostnames.includes(url.hostname)) {
159
+ warnings.push('Using localhost for Revenium API - ensure this is intended for development');
160
+ }
161
+ }
162
+ catch {
163
+ errors.push('reveniumBaseUrl must be a valid URL');
164
+ suggestions.push('Use format: https://api.revenium.io/meter');
165
+ }
166
+ }
167
+ // Validate optional Anthropic API key
168
+ if (!isString(cfg?.anthropicApiKey)) {
169
+ errors.push('anthropicApiKey must be a string if provided');
170
+ }
171
+ else if (cfg?.anthropicApiKey?.trim()?.length === 0) {
172
+ warnings.push('anthropicApiKey is empty - API calls may fail');
173
+ }
174
+ else if (!cfg?.anthropicApiKey?.startsWith(constants_1.VALIDATION_CONFIG.ANTHROPIC_API_KEY_PREFIX)) {
175
+ warnings.push(`anthropicApiKey does not start with "${constants_1.VALIDATION_CONFIG.ANTHROPIC_API_KEY_PREFIX}" - verify it is correct`);
176
+ }
177
+ // Validate optional timeout using constants
178
+ if (cfg?.apiTimeout !== undefined && !isNumber(cfg?.apiTimeout)) {
179
+ errors.push('apiTimeout must be a number if provided');
180
+ }
181
+ else if (cfg?.apiTimeout !== undefined && cfg?.apiTimeout < constants_1.VALIDATION_CONFIG.MIN_API_TIMEOUT) {
182
+ errors.push(`apiTimeout must be at least ${constants_1.VALIDATION_CONFIG.MIN_API_TIMEOUT}ms`);
183
+ }
184
+ else if (cfg?.apiTimeout !== undefined && cfg?.apiTimeout > constants_1.VALIDATION_CONFIG.MAX_API_TIMEOUT) {
185
+ errors.push(`apiTimeout must not exceed ${constants_1.VALIDATION_CONFIG.MAX_API_TIMEOUT}ms`);
186
+ }
187
+ else if (cfg?.apiTimeout !== undefined && cfg?.apiTimeout < constants_1.VALIDATION_CONFIG.LOW_TIMEOUT_WARNING_THRESHOLD) {
188
+ warnings.push('apiTimeout is very low - may cause timeouts for slow networks');
189
+ }
190
+ // Validate optional failSilent
191
+ if (cfg?.failSilent && !isBoolean(cfg?.failSilent)) {
192
+ errors.push('failSilent must be a boolean if provided');
193
+ }
194
+ // Validate optional maxRetries using constants
195
+ if (cfg?.maxRetries !== undefined && !isNumber(cfg?.maxRetries)) {
196
+ errors.push('maxRetries must be a number if provided');
197
+ }
198
+ else if (cfg?.maxRetries !== undefined && cfg?.maxRetries < 0) {
199
+ errors.push('maxRetries cannot be negative');
200
+ }
201
+ else if (cfg?.maxRetries !== undefined && cfg?.maxRetries > constants_1.VALIDATION_CONFIG.MAX_RETRY_ATTEMPTS) {
202
+ errors.push(`maxRetries should not exceed ${constants_1.VALIDATION_CONFIG.MAX_RETRY_ATTEMPTS}`);
203
+ }
204
+ else if (cfg?.maxRetries !== undefined && cfg?.maxRetries === 0) {
205
+ warnings.push('maxRetries is 0 - no retry attempts will be made');
206
+ }
207
+ if (errors.length > 0) {
208
+ return {
209
+ isValid: false,
210
+ errors,
211
+ warnings,
212
+ suggestions
213
+ };
214
+ }
215
+ // Build validated config
216
+ const validatedConfig = {
217
+ reveniumApiKey: cfg?.reveniumApiKey,
218
+ reveniumBaseUrl: cfg?.reveniumBaseUrl,
219
+ anthropicApiKey: isString(cfg?.anthropicApiKey) ? cfg?.anthropicApiKey : undefined,
220
+ apiTimeout: isNumber(cfg?.apiTimeout) ? cfg?.apiTimeout : undefined,
221
+ failSilent: isBoolean(cfg?.failSilent) ? cfg?.failSilent : undefined,
222
+ maxRetries: isNumber(cfg?.maxRetries) ? cfg?.maxRetries : undefined
223
+ };
224
+ return {
225
+ isValid: true,
226
+ errors: [],
227
+ warnings,
228
+ config: validatedConfig,
229
+ suggestions: suggestions.length > 0 ? suggestions : undefined
230
+ };
231
+ }
232
+ /**
233
+ * Validate Anthropic message creation parameters
234
+ */
235
+ function validateAnthropicMessageParams(params) {
236
+ const errors = [];
237
+ const warnings = [];
238
+ if (!isObject(params)) {
239
+ return {
240
+ isValid: false,
241
+ errors: ['Message parameters must be an object'],
242
+ warnings: []
243
+ };
244
+ }
245
+ const data = params;
246
+ // Validate required model field
247
+ if (!isString(data?.model)) {
248
+ errors.push('model field is required and must be a string');
249
+ }
250
+ else if (data?.model?.trim()?.length === 0) {
251
+ errors.push('model field cannot be empty');
252
+ }
253
+ else if (!constants_1.ANTHROPIC_PATTERNS.CLAUDE_MODEL_PATTERN.test(data?.model)) {
254
+ warnings.push('Model name does not contain "claude" - verify it is a valid Anthropic model');
255
+ }
256
+ // Validate required messages array
257
+ if (!Array.isArray(data?.messages)) {
258
+ errors.push('messages field is required and must be an array');
259
+ }
260
+ else if (data?.messages?.length === 0) {
261
+ errors.push('messages array cannot be empty');
262
+ }
263
+ else {
264
+ // Validate message structure
265
+ data?.messages?.forEach((message, index) => {
266
+ if (!isObject(message)) {
267
+ errors.push(`Message at index ${index} must be an object`);
268
+ return;
269
+ }
270
+ const msg = message;
271
+ if (!isString(msg.role)) {
272
+ errors.push(`Message at index ${index} must have a role field`);
273
+ }
274
+ else if (!['user', 'assistant', 'system'].includes(msg.role)) {
275
+ warnings.push(`Message at index ${index} has unusual role: ${msg.role}`);
276
+ }
277
+ if (msg?.content && !isString(msg?.content) && !Array.isArray(msg?.content)) {
278
+ warnings.push(`Message at index ${index} content should be a string or array`);
279
+ }
280
+ });
281
+ }
282
+ // Validate optional parameters
283
+ if (!isNumber(data?.max_tokens)) {
284
+ warnings.push('max_tokens should be a number');
285
+ }
286
+ else if (data?.max_tokens <= 0) {
287
+ warnings.push('max_tokens should be positive');
288
+ }
289
+ else if (data?.max_tokens > constants_1.VALIDATION_CONFIG.HIGH_MAX_TOKENS_THRESHOLD) {
290
+ warnings.push('max_tokens is very high - verify this is intended');
291
+ }
292
+ if (!isNumber(data?.temperature)) {
293
+ warnings.push('temperature should be a number');
294
+ }
295
+ else if (data?.temperature < constants_1.VALIDATION_CONFIG.MIN_TEMPERATURE || data?.temperature > constants_1.VALIDATION_CONFIG.MAX_TEMPERATURE) {
296
+ warnings.push(`temperature should be between ${constants_1.VALIDATION_CONFIG.MIN_TEMPERATURE} and ${constants_1.VALIDATION_CONFIG.MAX_TEMPERATURE} for Anthropic models`);
297
+ }
298
+ if (data?.stream && !isBoolean(data?.stream)) {
299
+ warnings.push('stream should be a boolean');
300
+ }
301
+ return {
302
+ isValid: errors.length === 0,
303
+ errors,
304
+ warnings
305
+ };
306
+ }
307
+ //# sourceMappingURL=validation.js.map