@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,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