@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.
- package/LICENSE +21 -0
- package/README.md +807 -0
- package/dist/cjs/config.js +208 -0
- package/dist/cjs/constants.js +144 -0
- package/dist/cjs/index.js +148 -0
- package/dist/cjs/tracking.js +252 -0
- package/dist/cjs/types/anthropic-augmentation.js +87 -0
- package/dist/cjs/types.js +6 -0
- package/dist/cjs/utils/circuit-breaker.js +232 -0
- package/dist/cjs/utils/error-handling.js +233 -0
- package/dist/cjs/utils/validation.js +307 -0
- package/dist/cjs/wrapper.js +374 -0
- package/dist/esm/config.js +197 -0
- package/dist/esm/constants.js +141 -0
- package/dist/esm/index.js +121 -0
- package/dist/esm/tracking.js +243 -0
- package/dist/esm/types/anthropic-augmentation.js +86 -0
- package/dist/esm/types.js +5 -0
- package/dist/esm/utils/circuit-breaker.js +223 -0
- package/dist/esm/utils/error-handling.js +216 -0
- package/dist/esm/utils/validation.js +296 -0
- package/dist/esm/wrapper.js +366 -0
- package/dist/types/config.d.ts +43 -0
- package/dist/types/constants.d.ts +141 -0
- package/dist/types/index.d.ts +54 -0
- package/dist/types/tracking.d.ts +42 -0
- package/dist/types/types/anthropic-augmentation.d.ts +182 -0
- package/dist/types/types.d.ts +647 -0
- package/dist/types/utils/circuit-breaker.d.ts +110 -0
- package/dist/types/utils/error-handling.d.ts +108 -0
- package/dist/types/utils/validation.d.ts +57 -0
- package/dist/types/wrapper.d.ts +16 -0
- package/package.json +74 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for Anthropic middleware
|
|
3
|
+
* Provides type-safe validation with detailed error reporting
|
|
4
|
+
*/
|
|
5
|
+
import { VALIDATION_CONFIG, ANTHROPIC_PATTERNS } from '../constants.js';
|
|
6
|
+
/**
|
|
7
|
+
* Type guard for checking if a value is a non-null object
|
|
8
|
+
*/
|
|
9
|
+
export function isObject(value) {
|
|
10
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Type guard for checking if a value is a string
|
|
14
|
+
*/
|
|
15
|
+
export function isString(value) {
|
|
16
|
+
return typeof value === 'string';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Type guard for checking if a value is a number
|
|
20
|
+
*/
|
|
21
|
+
export function isNumber(value) {
|
|
22
|
+
return typeof value === 'number' && !isNaN(value);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Type guard for checking if a value is a boolean
|
|
26
|
+
*/
|
|
27
|
+
export function isBoolean(value) {
|
|
28
|
+
return typeof value === 'boolean';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Validate and extract string from unknown value
|
|
32
|
+
*/
|
|
33
|
+
export function validateString(value, defaultValue = '') {
|
|
34
|
+
return isString(value) ? value : defaultValue;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate and extract number from unknown value
|
|
38
|
+
*/
|
|
39
|
+
export function validateNumber(value, defaultValue = 0) {
|
|
40
|
+
return isNumber(value) ? value : defaultValue;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate usage metadata object with Anthropic-specific considerations
|
|
44
|
+
*/
|
|
45
|
+
export function validateUsageMetadata(metadata) {
|
|
46
|
+
if (!isObject(metadata)) {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
const validated = {};
|
|
50
|
+
// Validate string fields
|
|
51
|
+
const stringFields = [
|
|
52
|
+
'traceId', 'taskId', 'taskType', 'subscriberEmail', 'subscriberId',
|
|
53
|
+
'subscriberCredentialName', 'subscriberCredential', 'organizationId',
|
|
54
|
+
'subscriptionId', 'productId', 'agent'
|
|
55
|
+
];
|
|
56
|
+
for (const field of stringFields) {
|
|
57
|
+
const value = metadata[field];
|
|
58
|
+
if (isString(value) && value.trim().length > 0) {
|
|
59
|
+
validated[field] = value.trim();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Validate nested subscriber object
|
|
63
|
+
const subscriberData = metadata.subscriber;
|
|
64
|
+
if (isObject(subscriberData)) {
|
|
65
|
+
const subscriber = {};
|
|
66
|
+
// Validate subscriber.id
|
|
67
|
+
if (isString(subscriberData?.id) && subscriberData?.id?.trim()?.length > 0) {
|
|
68
|
+
subscriber.id = subscriberData?.id?.trim();
|
|
69
|
+
}
|
|
70
|
+
// Validate subscriber.email
|
|
71
|
+
if (isString(subscriberData?.email) && subscriberData?.email?.trim()?.length > 0) {
|
|
72
|
+
subscriber.email = subscriberData?.email?.trim();
|
|
73
|
+
}
|
|
74
|
+
// Validate subscriber.credential object
|
|
75
|
+
const credentialData = subscriberData?.credential;
|
|
76
|
+
if (isObject(credentialData)) {
|
|
77
|
+
const credential = {};
|
|
78
|
+
if (isString(credentialData?.name) && credentialData?.name?.trim()?.length > 0) {
|
|
79
|
+
credential.name = credentialData?.name?.trim();
|
|
80
|
+
}
|
|
81
|
+
if (isString(credentialData?.value) && credentialData?.value?.trim()?.length > 0) {
|
|
82
|
+
credential.value = credentialData?.value?.trim();
|
|
83
|
+
}
|
|
84
|
+
// Only include credential if it has at least name or value
|
|
85
|
+
if (credential?.name || credential?.value) {
|
|
86
|
+
subscriber.credential = credential;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Only include subscriber if it has at least one field
|
|
90
|
+
if (subscriber?.id || subscriber?.email || subscriber?.credential) {
|
|
91
|
+
validated.subscriber = subscriber;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Validate number fields
|
|
95
|
+
const responseQualityScore = metadata.responseQualityScore;
|
|
96
|
+
if (isNumber(responseQualityScore) && responseQualityScore >= 0 && responseQualityScore <= 1) {
|
|
97
|
+
validated.responseQualityScore = responseQualityScore;
|
|
98
|
+
}
|
|
99
|
+
return validated;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Comprehensive Revenium configuration validation for Anthropic
|
|
103
|
+
*/
|
|
104
|
+
export function validateReveniumConfig(config) {
|
|
105
|
+
const errors = [];
|
|
106
|
+
const warnings = [];
|
|
107
|
+
const suggestions = [];
|
|
108
|
+
if (!isObject(config)) {
|
|
109
|
+
return {
|
|
110
|
+
isValid: false,
|
|
111
|
+
errors: ['Configuration must be an object'],
|
|
112
|
+
warnings: [],
|
|
113
|
+
suggestions: ['Ensure you are passing a valid configuration object with required fields']
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const cfg = config;
|
|
117
|
+
// Validate required Revenium API key
|
|
118
|
+
if (!isString(cfg?.reveniumApiKey)) {
|
|
119
|
+
errors.push('reveniumApiKey is required and must be a string');
|
|
120
|
+
suggestions.push('Set REVENIUM_METERING_API_KEY environment variable or provide reveniumApiKey in config');
|
|
121
|
+
}
|
|
122
|
+
else if (!cfg?.reveniumApiKey?.trim()) {
|
|
123
|
+
errors.push('reveniumApiKey cannot be empty');
|
|
124
|
+
}
|
|
125
|
+
else if (!cfg?.reveniumApiKey?.startsWith(VALIDATION_CONFIG.REVENIUM_API_KEY_PREFIX)) {
|
|
126
|
+
errors.push(`reveniumApiKey must start with "${VALIDATION_CONFIG.REVENIUM_API_KEY_PREFIX}"`);
|
|
127
|
+
suggestions.push('Obtain a valid Revenium API key from your Revenium dashboard');
|
|
128
|
+
}
|
|
129
|
+
else if (cfg?.reveniumApiKey?.length < VALIDATION_CONFIG.MIN_API_KEY_LENGTH) {
|
|
130
|
+
warnings.push('reveniumApiKey appears to be too short - verify it is correct');
|
|
131
|
+
}
|
|
132
|
+
// Validate Revenium base URL
|
|
133
|
+
if (!isString(cfg?.reveniumBaseUrl)) {
|
|
134
|
+
errors.push('reveniumBaseUrl is required and must be a string');
|
|
135
|
+
}
|
|
136
|
+
else if (!cfg?.reveniumBaseUrl?.trim()) {
|
|
137
|
+
errors.push('reveniumBaseUrl cannot be empty');
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
try {
|
|
141
|
+
const url = new URL(cfg?.reveniumBaseUrl);
|
|
142
|
+
if (!url.protocol.startsWith('http')) {
|
|
143
|
+
errors.push('reveniumBaseUrl must use HTTP or HTTPS protocol');
|
|
144
|
+
}
|
|
145
|
+
// Check for localhost/development hostnames (IPv4, IPv6, and named)
|
|
146
|
+
const localhostHostnames = ['localhost', '127.0.0.1', '::1', '[::1]'];
|
|
147
|
+
if (localhostHostnames.includes(url.hostname)) {
|
|
148
|
+
warnings.push('Using localhost for Revenium API - ensure this is intended for development');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
errors.push('reveniumBaseUrl must be a valid URL');
|
|
153
|
+
suggestions.push('Use format: https://api.revenium.io/meter');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Validate optional Anthropic API key
|
|
157
|
+
if (!isString(cfg?.anthropicApiKey)) {
|
|
158
|
+
errors.push('anthropicApiKey must be a string if provided');
|
|
159
|
+
}
|
|
160
|
+
else if (cfg?.anthropicApiKey?.trim()?.length === 0) {
|
|
161
|
+
warnings.push('anthropicApiKey is empty - API calls may fail');
|
|
162
|
+
}
|
|
163
|
+
else if (!cfg?.anthropicApiKey?.startsWith(VALIDATION_CONFIG.ANTHROPIC_API_KEY_PREFIX)) {
|
|
164
|
+
warnings.push(`anthropicApiKey does not start with "${VALIDATION_CONFIG.ANTHROPIC_API_KEY_PREFIX}" - verify it is correct`);
|
|
165
|
+
}
|
|
166
|
+
// Validate optional timeout using constants
|
|
167
|
+
if (cfg?.apiTimeout !== undefined && !isNumber(cfg?.apiTimeout)) {
|
|
168
|
+
errors.push('apiTimeout must be a number if provided');
|
|
169
|
+
}
|
|
170
|
+
else if (cfg?.apiTimeout !== undefined && cfg?.apiTimeout < VALIDATION_CONFIG.MIN_API_TIMEOUT) {
|
|
171
|
+
errors.push(`apiTimeout must be at least ${VALIDATION_CONFIG.MIN_API_TIMEOUT}ms`);
|
|
172
|
+
}
|
|
173
|
+
else if (cfg?.apiTimeout !== undefined && cfg?.apiTimeout > VALIDATION_CONFIG.MAX_API_TIMEOUT) {
|
|
174
|
+
errors.push(`apiTimeout must not exceed ${VALIDATION_CONFIG.MAX_API_TIMEOUT}ms`);
|
|
175
|
+
}
|
|
176
|
+
else if (cfg?.apiTimeout !== undefined && cfg?.apiTimeout < VALIDATION_CONFIG.LOW_TIMEOUT_WARNING_THRESHOLD) {
|
|
177
|
+
warnings.push('apiTimeout is very low - may cause timeouts for slow networks');
|
|
178
|
+
}
|
|
179
|
+
// Validate optional failSilent
|
|
180
|
+
if (cfg?.failSilent && !isBoolean(cfg?.failSilent)) {
|
|
181
|
+
errors.push('failSilent must be a boolean if provided');
|
|
182
|
+
}
|
|
183
|
+
// Validate optional maxRetries using constants
|
|
184
|
+
if (cfg?.maxRetries !== undefined && !isNumber(cfg?.maxRetries)) {
|
|
185
|
+
errors.push('maxRetries must be a number if provided');
|
|
186
|
+
}
|
|
187
|
+
else if (cfg?.maxRetries !== undefined && cfg?.maxRetries < 0) {
|
|
188
|
+
errors.push('maxRetries cannot be negative');
|
|
189
|
+
}
|
|
190
|
+
else if (cfg?.maxRetries !== undefined && cfg?.maxRetries > VALIDATION_CONFIG.MAX_RETRY_ATTEMPTS) {
|
|
191
|
+
errors.push(`maxRetries should not exceed ${VALIDATION_CONFIG.MAX_RETRY_ATTEMPTS}`);
|
|
192
|
+
}
|
|
193
|
+
else if (cfg?.maxRetries !== undefined && cfg?.maxRetries === 0) {
|
|
194
|
+
warnings.push('maxRetries is 0 - no retry attempts will be made');
|
|
195
|
+
}
|
|
196
|
+
if (errors.length > 0) {
|
|
197
|
+
return {
|
|
198
|
+
isValid: false,
|
|
199
|
+
errors,
|
|
200
|
+
warnings,
|
|
201
|
+
suggestions
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// Build validated config
|
|
205
|
+
const validatedConfig = {
|
|
206
|
+
reveniumApiKey: cfg?.reveniumApiKey,
|
|
207
|
+
reveniumBaseUrl: cfg?.reveniumBaseUrl,
|
|
208
|
+
anthropicApiKey: isString(cfg?.anthropicApiKey) ? cfg?.anthropicApiKey : undefined,
|
|
209
|
+
apiTimeout: isNumber(cfg?.apiTimeout) ? cfg?.apiTimeout : undefined,
|
|
210
|
+
failSilent: isBoolean(cfg?.failSilent) ? cfg?.failSilent : undefined,
|
|
211
|
+
maxRetries: isNumber(cfg?.maxRetries) ? cfg?.maxRetries : undefined
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
isValid: true,
|
|
215
|
+
errors: [],
|
|
216
|
+
warnings,
|
|
217
|
+
config: validatedConfig,
|
|
218
|
+
suggestions: suggestions.length > 0 ? suggestions : undefined
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Validate Anthropic message creation parameters
|
|
223
|
+
*/
|
|
224
|
+
export function validateAnthropicMessageParams(params) {
|
|
225
|
+
const errors = [];
|
|
226
|
+
const warnings = [];
|
|
227
|
+
if (!isObject(params)) {
|
|
228
|
+
return {
|
|
229
|
+
isValid: false,
|
|
230
|
+
errors: ['Message parameters must be an object'],
|
|
231
|
+
warnings: []
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const data = params;
|
|
235
|
+
// Validate required model field
|
|
236
|
+
if (!isString(data?.model)) {
|
|
237
|
+
errors.push('model field is required and must be a string');
|
|
238
|
+
}
|
|
239
|
+
else if (data?.model?.trim()?.length === 0) {
|
|
240
|
+
errors.push('model field cannot be empty');
|
|
241
|
+
}
|
|
242
|
+
else if (!ANTHROPIC_PATTERNS.CLAUDE_MODEL_PATTERN.test(data?.model)) {
|
|
243
|
+
warnings.push('Model name does not contain "claude" - verify it is a valid Anthropic model');
|
|
244
|
+
}
|
|
245
|
+
// Validate required messages array
|
|
246
|
+
if (!Array.isArray(data?.messages)) {
|
|
247
|
+
errors.push('messages field is required and must be an array');
|
|
248
|
+
}
|
|
249
|
+
else if (data?.messages?.length === 0) {
|
|
250
|
+
errors.push('messages array cannot be empty');
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
// Validate message structure
|
|
254
|
+
data?.messages?.forEach((message, index) => {
|
|
255
|
+
if (!isObject(message)) {
|
|
256
|
+
errors.push(`Message at index ${index} must be an object`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const msg = message;
|
|
260
|
+
if (!isString(msg.role)) {
|
|
261
|
+
errors.push(`Message at index ${index} must have a role field`);
|
|
262
|
+
}
|
|
263
|
+
else if (!['user', 'assistant', 'system'].includes(msg.role)) {
|
|
264
|
+
warnings.push(`Message at index ${index} has unusual role: ${msg.role}`);
|
|
265
|
+
}
|
|
266
|
+
if (msg?.content && !isString(msg?.content) && !Array.isArray(msg?.content)) {
|
|
267
|
+
warnings.push(`Message at index ${index} content should be a string or array`);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// Validate optional parameters
|
|
272
|
+
if (!isNumber(data?.max_tokens)) {
|
|
273
|
+
warnings.push('max_tokens should be a number');
|
|
274
|
+
}
|
|
275
|
+
else if (data?.max_tokens <= 0) {
|
|
276
|
+
warnings.push('max_tokens should be positive');
|
|
277
|
+
}
|
|
278
|
+
else if (data?.max_tokens > VALIDATION_CONFIG.HIGH_MAX_TOKENS_THRESHOLD) {
|
|
279
|
+
warnings.push('max_tokens is very high - verify this is intended');
|
|
280
|
+
}
|
|
281
|
+
if (!isNumber(data?.temperature)) {
|
|
282
|
+
warnings.push('temperature should be a number');
|
|
283
|
+
}
|
|
284
|
+
else if (data?.temperature < VALIDATION_CONFIG.MIN_TEMPERATURE || data?.temperature > VALIDATION_CONFIG.MAX_TEMPERATURE) {
|
|
285
|
+
warnings.push(`temperature should be between ${VALIDATION_CONFIG.MIN_TEMPERATURE} and ${VALIDATION_CONFIG.MAX_TEMPERATURE} for Anthropic models`);
|
|
286
|
+
}
|
|
287
|
+
if (data?.stream && !isBoolean(data?.stream)) {
|
|
288
|
+
warnings.push('stream should be a boolean');
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
isValid: errors.length === 0,
|
|
292
|
+
errors,
|
|
293
|
+
warnings
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic SDK wrapper with type safety and structured error handling
|
|
3
|
+
*/
|
|
4
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
5
|
+
import { getLogger } from './config.js';
|
|
6
|
+
import { trackUsageAsync, extractUsageFromResponse, extractUsageFromStream } from './tracking.js';
|
|
7
|
+
import { validateAnthropicMessageParams, validateUsageMetadata } from './utils/validation.js';
|
|
8
|
+
import { AnthropicPatchingError, RequestProcessingError, StreamProcessingError, createErrorContext, handleError } from './utils/error-handling.js';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
// Global logger
|
|
11
|
+
const logger = getLogger();
|
|
12
|
+
/**
|
|
13
|
+
* Global patching context
|
|
14
|
+
*/
|
|
15
|
+
const patchingContext = {
|
|
16
|
+
originalMethods: {},
|
|
17
|
+
isPatched: false,
|
|
18
|
+
patchedInstances: new WeakSet()
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Get the Messages prototype using sophisticated prototype access
|
|
22
|
+
* Uses multiple fallback strategies to access the Anthropic Messages class prototype
|
|
23
|
+
*/
|
|
24
|
+
function getMessagesPrototype() {
|
|
25
|
+
try {
|
|
26
|
+
// Method 1: Try to access through the constructor's prototype chain
|
|
27
|
+
// Look for the Messages constructor in the Anthropic module
|
|
28
|
+
// The Anthropic SDK typically exposes internal classes through the constructor
|
|
29
|
+
if (Anthropic?.Messages)
|
|
30
|
+
return Anthropic?.Messages?.prototype;
|
|
31
|
+
// Method 2: Try to access through the constructor's static properties
|
|
32
|
+
const anthropicConstructor = Anthropic;
|
|
33
|
+
if (anthropicConstructor?._Messages)
|
|
34
|
+
return anthropicConstructor?._Messages?.prototype;
|
|
35
|
+
// Method 3: Create a minimal instance with the real API key if available
|
|
36
|
+
// Fallback approach when direct prototype access methods fail
|
|
37
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
38
|
+
if (!apiKey) {
|
|
39
|
+
throw new AnthropicPatchingError('Unable to access Anthropic Messages prototype: No API key available and direct prototype access failed');
|
|
40
|
+
}
|
|
41
|
+
const minimalInstance = new Anthropic({ apiKey });
|
|
42
|
+
const messagesPrototype = Object.getPrototypeOf(minimalInstance.messages);
|
|
43
|
+
// Clean up the minimal instance immediately
|
|
44
|
+
// Note: The instance will be garbage collected, but the prototype reference remains
|
|
45
|
+
return messagesPrototype;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
// If all methods fail, throw a descriptive error
|
|
49
|
+
throw new AnthropicPatchingError(`Unable to access Anthropic Messages prototype: ${error instanceof Error ? error.message : String(error)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Patch Anthropic SDK by modifying prototype methods
|
|
54
|
+
*/
|
|
55
|
+
export function patchAnthropic() {
|
|
56
|
+
if (patchingContext.isPatched)
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
// Access the Messages class prototype using sophisticated prototype access
|
|
60
|
+
const messagesPrototype = getMessagesPrototype();
|
|
61
|
+
if (!messagesPrototype)
|
|
62
|
+
throw new AnthropicPatchingError('Unable to access Anthropic Messages prototype');
|
|
63
|
+
// Store original methods
|
|
64
|
+
patchingContext.originalMethods.create = messagesPrototype?.create;
|
|
65
|
+
patchingContext.originalMethods.stream = messagesPrototype?.stream;
|
|
66
|
+
if (!patchingContext.originalMethods?.create) {
|
|
67
|
+
throw new AnthropicPatchingError('Unable to find original create method');
|
|
68
|
+
}
|
|
69
|
+
// Patch the create method
|
|
70
|
+
const patchedCreateFunction = function (params, options) {
|
|
71
|
+
return patchedCreateMethod.call(this, params, options);
|
|
72
|
+
};
|
|
73
|
+
messagesPrototype.create = patchedCreateFunction;
|
|
74
|
+
// Patch the stream method if it exists
|
|
75
|
+
if (patchingContext.originalMethods?.stream) {
|
|
76
|
+
messagesPrototype.stream = function (params, options) {
|
|
77
|
+
return patchedStreamMethod.call(this, params, options);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
patchingContext.isPatched = true;
|
|
81
|
+
logger.info('Anthropic SDK patched successfully');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const errorContext = createErrorContext()
|
|
85
|
+
.with('patchingAttempt', true)
|
|
86
|
+
.build();
|
|
87
|
+
handleError(error, logger, errorContext);
|
|
88
|
+
if (error instanceof AnthropicPatchingError)
|
|
89
|
+
throw error;
|
|
90
|
+
throw new AnthropicPatchingError(`Failed to patch Anthropic SDK: ${error instanceof Error ? error.message : String(error)}`, errorContext);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Unpatch Anthropic SDK (restore original methods)
|
|
95
|
+
*/
|
|
96
|
+
export function unpatchAnthropic() {
|
|
97
|
+
if (!patchingContext.isPatched)
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
// Access the Messages class prototype using sophisticated prototype access
|
|
101
|
+
const messagesPrototype = getMessagesPrototype();
|
|
102
|
+
if (messagesPrototype && patchingContext.originalMethods.create) {
|
|
103
|
+
messagesPrototype.create = patchingContext.originalMethods.create;
|
|
104
|
+
}
|
|
105
|
+
if (messagesPrototype && patchingContext.originalMethods.stream) {
|
|
106
|
+
messagesPrototype.stream = patchingContext.originalMethods?.stream;
|
|
107
|
+
}
|
|
108
|
+
patchingContext.isPatched = false;
|
|
109
|
+
patchingContext.originalMethods = {};
|
|
110
|
+
logger.info('Anthropic SDK unpatched successfully');
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const errorContext = createErrorContext()
|
|
114
|
+
.with('unpatchingAttempt', true)
|
|
115
|
+
.build();
|
|
116
|
+
handleError(error, logger, errorContext);
|
|
117
|
+
throw new AnthropicPatchingError(`Failed to unpatch Anthropic SDK: ${error instanceof Error ? error.message : String(error)}`, errorContext);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Check if Anthropic SDK is patched
|
|
122
|
+
*/
|
|
123
|
+
export function isAnthropicPatched() {
|
|
124
|
+
return patchingContext.isPatched;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Handle streaming response by collecting chunks and extracting usage data
|
|
128
|
+
*/
|
|
129
|
+
async function handleStreamingResponse(stream, context) {
|
|
130
|
+
const { requestId, model, metadata, requestTime, startTime } = context;
|
|
131
|
+
// Create a new async generator that collects chunks and tracks usage
|
|
132
|
+
async function* trackingStream() {
|
|
133
|
+
const chunks = [];
|
|
134
|
+
let firstTokenTime;
|
|
135
|
+
try {
|
|
136
|
+
for await (const chunk of stream) {
|
|
137
|
+
// Track first token time
|
|
138
|
+
if (!firstTokenTime && chunk.type === 'content_block_delta') {
|
|
139
|
+
firstTokenTime = Date.now();
|
|
140
|
+
}
|
|
141
|
+
chunks.push(chunk);
|
|
142
|
+
yield chunk;
|
|
143
|
+
}
|
|
144
|
+
// After stream completes, extract usage and track
|
|
145
|
+
const endTime = Date.now();
|
|
146
|
+
const responseTime = new Date();
|
|
147
|
+
const duration = endTime - startTime;
|
|
148
|
+
logger.debug('Stream completed, extracting usage', {
|
|
149
|
+
requestId,
|
|
150
|
+
chunkCount: chunks.length,
|
|
151
|
+
duration
|
|
152
|
+
});
|
|
153
|
+
const usage = extractUsageFromStream(chunks);
|
|
154
|
+
// Create tracking data
|
|
155
|
+
const trackingData = {
|
|
156
|
+
requestId,
|
|
157
|
+
model,
|
|
158
|
+
inputTokens: usage.inputTokens,
|
|
159
|
+
outputTokens: usage.outputTokens,
|
|
160
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
161
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
162
|
+
duration,
|
|
163
|
+
isStreamed: true,
|
|
164
|
+
stopReason: usage.stopReason,
|
|
165
|
+
metadata,
|
|
166
|
+
requestTime,
|
|
167
|
+
responseTime
|
|
168
|
+
};
|
|
169
|
+
// Track usage asynchronously
|
|
170
|
+
trackUsageAsync(trackingData);
|
|
171
|
+
logger.debug('Anthropic streaming request completed successfully', {
|
|
172
|
+
requestId,
|
|
173
|
+
model,
|
|
174
|
+
inputTokens: usage.inputTokens,
|
|
175
|
+
outputTokens: usage.outputTokens,
|
|
176
|
+
duration
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
logger.error('Error processing streaming response', {
|
|
181
|
+
requestId,
|
|
182
|
+
error: error instanceof Error ? error.message : String(error)
|
|
183
|
+
});
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return trackingStream();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Patched create method implementation
|
|
191
|
+
*/
|
|
192
|
+
async function patchedCreateMethod(params, options) {
|
|
193
|
+
const requestId = randomUUID();
|
|
194
|
+
const startTime = Date.now();
|
|
195
|
+
const requestTime = new Date();
|
|
196
|
+
logger.debug('Intercepted Anthropic messages.create call', {
|
|
197
|
+
requestId,
|
|
198
|
+
model: params.model,
|
|
199
|
+
hasMetadata: !!params.usageMetadata,
|
|
200
|
+
isStreaming: !!params.stream
|
|
201
|
+
});
|
|
202
|
+
// Validate parameters
|
|
203
|
+
const validation = validateAnthropicMessageParams(params);
|
|
204
|
+
if (!validation.isValid) {
|
|
205
|
+
logger.warn('Invalid Anthropic parameters detected', {
|
|
206
|
+
requestId,
|
|
207
|
+
errors: validation.errors,
|
|
208
|
+
warnings: validation.warnings
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// Extract and validate metadata
|
|
212
|
+
const metadata = validateUsageMetadata(params.usageMetadata || {});
|
|
213
|
+
// Remove usageMetadata from params before calling original method
|
|
214
|
+
const { usageMetadata, ...cleanParams } = params;
|
|
215
|
+
try {
|
|
216
|
+
// Call original method
|
|
217
|
+
const originalCreate = patchingContext.originalMethods.create;
|
|
218
|
+
if (!originalCreate)
|
|
219
|
+
throw new RequestProcessingError('Original create method not available');
|
|
220
|
+
const response = await originalCreate.call(this, cleanParams, options);
|
|
221
|
+
// Check if this is a streaming response
|
|
222
|
+
const isStreaming = !!params.stream;
|
|
223
|
+
if (!isStreaming) {
|
|
224
|
+
const endTime = Date.now();
|
|
225
|
+
const duration = endTime - startTime;
|
|
226
|
+
const responseTime = new Date();
|
|
227
|
+
// Extract usage information
|
|
228
|
+
const usage = extractUsageFromResponse(response);
|
|
229
|
+
// Create tracking data
|
|
230
|
+
const trackingData = {
|
|
231
|
+
requestId,
|
|
232
|
+
model: params.model,
|
|
233
|
+
inputTokens: usage.inputTokens,
|
|
234
|
+
outputTokens: usage.outputTokens,
|
|
235
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
236
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
237
|
+
duration,
|
|
238
|
+
isStreamed: false,
|
|
239
|
+
stopReason: usage.stopReason,
|
|
240
|
+
metadata,
|
|
241
|
+
requestTime,
|
|
242
|
+
responseTime
|
|
243
|
+
};
|
|
244
|
+
// Track usage asynchronously
|
|
245
|
+
trackUsageAsync(trackingData);
|
|
246
|
+
logger.debug('Anthropic request completed successfully', {
|
|
247
|
+
requestId,
|
|
248
|
+
model: params.model,
|
|
249
|
+
inputTokens: usage.inputTokens,
|
|
250
|
+
outputTokens: usage.outputTokens,
|
|
251
|
+
duration
|
|
252
|
+
});
|
|
253
|
+
return response;
|
|
254
|
+
}
|
|
255
|
+
// Handle streaming response - need to collect chunks and extract usage
|
|
256
|
+
return handleStreamingResponse(response, {
|
|
257
|
+
requestId,
|
|
258
|
+
model: params.model,
|
|
259
|
+
metadata,
|
|
260
|
+
requestTime,
|
|
261
|
+
startTime
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
const endTime = Date.now();
|
|
266
|
+
const duration = endTime - startTime;
|
|
267
|
+
const errorContext = createErrorContext()
|
|
268
|
+
.withRequestId(requestId)
|
|
269
|
+
.withModel(params.model)
|
|
270
|
+
.withDuration(duration)
|
|
271
|
+
.build();
|
|
272
|
+
handleError(error, logger, errorContext);
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Patched stream method implementation
|
|
278
|
+
*/
|
|
279
|
+
async function* patchedStreamMethod(params, options) {
|
|
280
|
+
const requestId = randomUUID();
|
|
281
|
+
const startTime = Date.now();
|
|
282
|
+
const requestTime = new Date();
|
|
283
|
+
const responseTime = new Date();
|
|
284
|
+
const chunks = [];
|
|
285
|
+
let firstTokenTime;
|
|
286
|
+
logger.debug('Intercepted Anthropic messages.stream call', {
|
|
287
|
+
requestId,
|
|
288
|
+
model: params.model,
|
|
289
|
+
hasMetadata: !!params.usageMetadata
|
|
290
|
+
});
|
|
291
|
+
// Validate parameters
|
|
292
|
+
const validation = validateAnthropicMessageParams(params);
|
|
293
|
+
if (!validation.isValid) {
|
|
294
|
+
logger.warn('Invalid Anthropic streaming parameters detected', {
|
|
295
|
+
requestId,
|
|
296
|
+
errors: validation.errors,
|
|
297
|
+
warnings: validation.warnings
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
// Extract and validate metadata
|
|
301
|
+
const metadata = validateUsageMetadata(params.usageMetadata || {});
|
|
302
|
+
// Remove usageMetadata from params before calling original method
|
|
303
|
+
const { usageMetadata, ...cleanParams } = params;
|
|
304
|
+
try {
|
|
305
|
+
// Call original stream method
|
|
306
|
+
const originalStream = patchingContext.originalMethods?.stream;
|
|
307
|
+
if (!originalStream) {
|
|
308
|
+
throw new StreamProcessingError('Original stream method not available');
|
|
309
|
+
}
|
|
310
|
+
const stream = originalStream.call(this, cleanParams, options);
|
|
311
|
+
for await (const chunk of stream) {
|
|
312
|
+
// Track first token time
|
|
313
|
+
if (!firstTokenTime && chunk.type === 'content_block_delta') {
|
|
314
|
+
firstTokenTime = Date.now();
|
|
315
|
+
}
|
|
316
|
+
chunks.push(chunk);
|
|
317
|
+
yield chunk;
|
|
318
|
+
}
|
|
319
|
+
const endTime = Date.now();
|
|
320
|
+
const duration = endTime - startTime;
|
|
321
|
+
const timeToFirstToken = firstTokenTime ? firstTokenTime - startTime : undefined;
|
|
322
|
+
// Extract usage information from all chunks
|
|
323
|
+
const usage = extractUsageFromStream(chunks);
|
|
324
|
+
// Create tracking data
|
|
325
|
+
const trackingData = {
|
|
326
|
+
requestId,
|
|
327
|
+
model: params.model,
|
|
328
|
+
inputTokens: usage.inputTokens,
|
|
329
|
+
outputTokens: usage.outputTokens,
|
|
330
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
331
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
332
|
+
duration,
|
|
333
|
+
isStreamed: true,
|
|
334
|
+
stopReason: usage.stopReason,
|
|
335
|
+
metadata,
|
|
336
|
+
requestTime,
|
|
337
|
+
responseTime,
|
|
338
|
+
timeToFirstToken
|
|
339
|
+
};
|
|
340
|
+
// Track usage asynchronously
|
|
341
|
+
trackUsageAsync(trackingData);
|
|
342
|
+
logger.debug('Anthropic streaming request completed successfully', {
|
|
343
|
+
requestId,
|
|
344
|
+
model: params.model,
|
|
345
|
+
inputTokens: usage.inputTokens,
|
|
346
|
+
outputTokens: usage.outputTokens,
|
|
347
|
+
duration,
|
|
348
|
+
timeToFirstToken,
|
|
349
|
+
chunkCount: chunks.length
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
const endTime = Date.now();
|
|
354
|
+
const duration = endTime - startTime;
|
|
355
|
+
const errorContext = createErrorContext()
|
|
356
|
+
.withRequestId(requestId)
|
|
357
|
+
.withModel(params.model)
|
|
358
|
+
.withDuration(duration)
|
|
359
|
+
.with('isStreaming', true)
|
|
360
|
+
.with('chunkCount', chunks.length)
|
|
361
|
+
.build();
|
|
362
|
+
handleError(error, logger, errorContext);
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
//# sourceMappingURL=wrapper.js.map
|