@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,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration constants for Revenium Anthropic middleware
|
|
3
|
+
* Centralizes all magic numbers and default values
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Default configuration values
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_CONFIG = {
|
|
9
|
+
/** Default Revenium API base URL */
|
|
10
|
+
REVENIUM_BASE_URL: 'https://api.revenium.io/meter',
|
|
11
|
+
/** Default API timeout in milliseconds */
|
|
12
|
+
API_TIMEOUT: 5000,
|
|
13
|
+
/** Default maximum retries for failed API calls */
|
|
14
|
+
MAX_RETRIES: 3,
|
|
15
|
+
/** Default fail silent behavior */
|
|
16
|
+
FAIL_SILENT: true,
|
|
17
|
+
/** Minimum allowed API timeout */
|
|
18
|
+
MIN_API_TIMEOUT: 1000,
|
|
19
|
+
/** Maximum allowed API timeout */
|
|
20
|
+
MAX_API_TIMEOUT: 60000,
|
|
21
|
+
/** Maximum allowed retry attempts */
|
|
22
|
+
MAX_RETRY_ATTEMPTS: 10,
|
|
23
|
+
/** Warning threshold for low API timeout */
|
|
24
|
+
LOW_TIMEOUT_WARNING_THRESHOLD: 3000,
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Circuit breaker configuration constants
|
|
28
|
+
*/
|
|
29
|
+
export const CIRCUIT_BREAKER_CONFIG = {
|
|
30
|
+
/** Default number of failures before opening circuit */
|
|
31
|
+
FAILURE_THRESHOLD: 5,
|
|
32
|
+
/** Default recovery timeout in milliseconds (30 seconds) */
|
|
33
|
+
RECOVERY_TIMEOUT: 30000,
|
|
34
|
+
/** Default number of successful calls needed to close circuit */
|
|
35
|
+
SUCCESS_THRESHOLD: 3,
|
|
36
|
+
/** Default time window for counting failures (1 minute) */
|
|
37
|
+
TIME_WINDOW: 60000,
|
|
38
|
+
/** Maximum failure history size to prevent memory leaks */
|
|
39
|
+
MAX_FAILURE_HISTORY_SIZE: 1000,
|
|
40
|
+
/** Periodic cleanup interval (5 minutes) */
|
|
41
|
+
CLEANUP_INTERVAL: 300000,
|
|
42
|
+
/** Cleanup trigger threshold for failures array size */
|
|
43
|
+
CLEANUP_SIZE_THRESHOLD: 100,
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Retry configuration constants
|
|
47
|
+
*/
|
|
48
|
+
export const RETRY_CONFIG = {
|
|
49
|
+
/** Base delay for exponential backoff in milliseconds */
|
|
50
|
+
BASE_DELAY: 1000,
|
|
51
|
+
/** Maximum delay for exponential backoff */
|
|
52
|
+
MAX_DELAY: 5000,
|
|
53
|
+
/** Jitter factor for randomizing delays */
|
|
54
|
+
JITTER_FACTOR: 0.1,
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Validation constants
|
|
58
|
+
*/
|
|
59
|
+
export const VALIDATION_CONFIG = {
|
|
60
|
+
/** Minimum API key length */
|
|
61
|
+
MIN_API_KEY_LENGTH: 20,
|
|
62
|
+
/** Required API key prefix for Revenium */
|
|
63
|
+
REVENIUM_API_KEY_PREFIX: 'hak_',
|
|
64
|
+
/** Required API key prefix for Anthropic */
|
|
65
|
+
ANTHROPIC_API_KEY_PREFIX: 'sk-ant-',
|
|
66
|
+
/** Maximum tokens warning threshold */
|
|
67
|
+
HIGH_MAX_TOKENS_THRESHOLD: 4096,
|
|
68
|
+
/** Temperature range */
|
|
69
|
+
MIN_TEMPERATURE: 0,
|
|
70
|
+
MAX_TEMPERATURE: 1,
|
|
71
|
+
/** Response quality score range */
|
|
72
|
+
MIN_QUALITY_SCORE: 0,
|
|
73
|
+
MAX_QUALITY_SCORE: 1,
|
|
74
|
+
/** API timeout constraints */
|
|
75
|
+
MIN_API_TIMEOUT: 1000,
|
|
76
|
+
MAX_API_TIMEOUT: 60000,
|
|
77
|
+
LOW_TIMEOUT_WARNING_THRESHOLD: 3000,
|
|
78
|
+
/** Retry constraints */
|
|
79
|
+
MAX_RETRY_ATTEMPTS: 10,
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Logging constants
|
|
83
|
+
*/
|
|
84
|
+
export const LOGGING_CONFIG = {
|
|
85
|
+
/** Middleware name for log prefixes */
|
|
86
|
+
MIDDLEWARE_NAME: 'Revenium',
|
|
87
|
+
/** User agent string for API requests */
|
|
88
|
+
USER_AGENT: 'revenium-middleware-anthropic-node/1.0.0',
|
|
89
|
+
/** Debug environment variable name */
|
|
90
|
+
DEBUG_ENV_VAR: 'REVENIUM_DEBUG',
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Environment variable names
|
|
94
|
+
*/
|
|
95
|
+
export const ENV_VARS = {
|
|
96
|
+
/** Revenium API key */
|
|
97
|
+
REVENIUM_API_KEY: 'REVENIUM_METERING_API_KEY',
|
|
98
|
+
/** Revenium base URL */
|
|
99
|
+
REVENIUM_BASE_URL: 'REVENIUM_METERING_BASE_URL',
|
|
100
|
+
/** Anthropic API key */
|
|
101
|
+
ANTHROPIC_API_KEY: 'ANTHROPIC_API_KEY',
|
|
102
|
+
/** Debug mode */
|
|
103
|
+
DEBUG: 'REVENIUM_DEBUG',
|
|
104
|
+
/** Log level */
|
|
105
|
+
LOG_LEVEL: 'REVENIUM_LOG_LEVEL',
|
|
106
|
+
/** API timeout */
|
|
107
|
+
API_TIMEOUT: 'REVENIUM_API_TIMEOUT',
|
|
108
|
+
/** Fail silent mode */
|
|
109
|
+
FAIL_SILENT: 'REVENIUM_FAIL_SILENT',
|
|
110
|
+
/** Maximum retries */
|
|
111
|
+
MAX_RETRIES: 'REVENIUM_MAX_RETRIES',
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* API endpoints
|
|
115
|
+
*/
|
|
116
|
+
export const API_ENDPOINTS = {
|
|
117
|
+
/** Revenium AI completions endpoint */
|
|
118
|
+
AI_COMPLETIONS: '/v2/ai/completions',
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Anthropic model patterns
|
|
122
|
+
*/
|
|
123
|
+
export const ANTHROPIC_PATTERNS = {
|
|
124
|
+
/** Pattern to identify Claude models */
|
|
125
|
+
CLAUDE_MODEL_PATTERN: /claude/i,
|
|
126
|
+
/** Known Anthropic stop reasons */
|
|
127
|
+
STOP_REASONS: {
|
|
128
|
+
END_TURN: 'end_turn',
|
|
129
|
+
MAX_TOKENS: 'max_tokens',
|
|
130
|
+
STOP_SEQUENCE: 'stop_sequence',
|
|
131
|
+
TOOL_USE: 'tool_use',
|
|
132
|
+
},
|
|
133
|
+
/** Revenium stop reason mappings */
|
|
134
|
+
REVENIUM_STOP_REASON_MAP: {
|
|
135
|
+
'end_turn': 'END',
|
|
136
|
+
'max_tokens': 'TOKEN_LIMIT',
|
|
137
|
+
'stop_sequence': 'END_SEQUENCE',
|
|
138
|
+
'tool_use': 'END',
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modern Revenium Anthropic Middleware
|
|
3
|
+
* Clean architecture without backward compatibility constraints
|
|
4
|
+
*/
|
|
5
|
+
// Import type augmentations to extend Anthropic types with usageMetadata
|
|
6
|
+
import "./types/anthropic-augmentation.js";
|
|
7
|
+
import { initializeConfig, getLogger, setConfig, getConfig, getConfigStatus, } from "./config.js";
|
|
8
|
+
import { patchAnthropic, unpatchAnthropic, isAnthropicPatched, } from "./wrapper.js";
|
|
9
|
+
import { trackUsageAsync } from "./tracking.js";
|
|
10
|
+
import { getCircuitBreakerStats, resetCircuitBreaker, } from "./utils/circuit-breaker.js";
|
|
11
|
+
// Export configuration functions
|
|
12
|
+
export { setConfig, setLogger, getLogger, getConfig, getConfigStatus, validateCurrentConfig, } from "./config.js";
|
|
13
|
+
// Export wrapper control functions
|
|
14
|
+
export { patchAnthropic, unpatchAnthropic, isAnthropicPatched, } from "./wrapper.js";
|
|
15
|
+
// Export tracking functions
|
|
16
|
+
export { sendReveniumMetrics, trackUsageAsync, extractUsageFromStream, } from "./tracking.js";
|
|
17
|
+
// Export utility functions
|
|
18
|
+
export { getCircuitBreakerStats, resetCircuitBreaker, canExecuteRequest, } from "./utils/circuit-breaker.js";
|
|
19
|
+
export { validateReveniumConfig, validateAnthropicMessageParams, validateUsageMetadata, } from "./utils/validation.js";
|
|
20
|
+
/**
|
|
21
|
+
* Initialize the Revenium middleware with configuration from environment variables
|
|
22
|
+
* This function can be called explicitly for better error handling and control
|
|
23
|
+
*
|
|
24
|
+
*
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
// Global logger
|
|
28
|
+
const logger = getLogger();
|
|
29
|
+
export function initialize() {
|
|
30
|
+
const initialized = initializeConfig();
|
|
31
|
+
if (!initialized) {
|
|
32
|
+
throw new Error("Failed to initialize Revenium middleware: missing required environment variables. " +
|
|
33
|
+
"Set REVENIUM_METERING_API_KEY or call configure() with manual configuration.");
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
patchAnthropic();
|
|
37
|
+
logger.info("Revenium Anthropic middleware initialized successfully");
|
|
38
|
+
logger.debug("Environment variables found and configuration loaded");
|
|
39
|
+
}
|
|
40
|
+
catch (patchError) {
|
|
41
|
+
const errorMessage = patchError instanceof Error ? patchError.message : String(patchError);
|
|
42
|
+
throw new Error(`Failed to patch Anthropic SDK: ${errorMessage}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Auto-initialization with graceful fallback
|
|
47
|
+
* Attempts to initialize automatically but doesn't throw on failure
|
|
48
|
+
*/
|
|
49
|
+
function autoInitialize() {
|
|
50
|
+
try {
|
|
51
|
+
initialize();
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// Log debug message but don't throw - allow manual configuration later
|
|
55
|
+
logger.debug("Auto-initialization failed, manual configuration required", {
|
|
56
|
+
error: error instanceof Error ? error.message : String(error),
|
|
57
|
+
suggestion: "Call initialize() or configure() explicitly for detailed error information",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Perform auto-initialization with graceful fallback
|
|
62
|
+
autoInitialize();
|
|
63
|
+
/**
|
|
64
|
+
* Manual initialization with custom configuration
|
|
65
|
+
*/
|
|
66
|
+
export function configure(config) {
|
|
67
|
+
setConfig(config);
|
|
68
|
+
patchAnthropic();
|
|
69
|
+
getLogger().info("Revenium Anthropic middleware configured successfully");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Check if the middleware is properly initialized
|
|
73
|
+
*/
|
|
74
|
+
export function isInitialized() {
|
|
75
|
+
return isAnthropicPatched() && !!getConfig();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get comprehensive middleware status
|
|
79
|
+
*/
|
|
80
|
+
export function getStatus() {
|
|
81
|
+
const configStatus = getConfigStatus();
|
|
82
|
+
const circuitBreakerStats = getCircuitBreakerStats();
|
|
83
|
+
let anthropicVersion;
|
|
84
|
+
try {
|
|
85
|
+
// Try to get Anthropic version - use dynamic require for compatibility
|
|
86
|
+
const anthropicPackage = globalThis.require?.("@anthropic-ai/sdk/package.json");
|
|
87
|
+
anthropicVersion = anthropicPackage?.version;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Ignore if we can't get the version
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
initialized: isInitialized(),
|
|
94
|
+
patched: isAnthropicPatched(),
|
|
95
|
+
hasConfig: configStatus.hasConfig,
|
|
96
|
+
anthropicVersion,
|
|
97
|
+
circuitBreakerState: circuitBreakerStats.state,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Manually track an Anthropic API call (for cases where auto-patching isn't used)
|
|
102
|
+
*/
|
|
103
|
+
export function trackAnthropicCall(trackingData) {
|
|
104
|
+
return trackUsageAsync(trackingData);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Reset middleware to initial state (useful for testing)
|
|
108
|
+
*/
|
|
109
|
+
export function reset() {
|
|
110
|
+
try {
|
|
111
|
+
unpatchAnthropic();
|
|
112
|
+
resetCircuitBreaker();
|
|
113
|
+
logger.debug("Middleware reset completed");
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
logger.error("Error during middleware reset", {
|
|
117
|
+
error: error instanceof Error ? error.message : String(error),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracking implementation for Anthropic middleware with resilience patterns
|
|
3
|
+
*/
|
|
4
|
+
import fetch from "node-fetch";
|
|
5
|
+
import { getConfig, getLogger } from "./config.js";
|
|
6
|
+
import { executeWithCircuitBreaker } from "./utils/circuit-breaker.js";
|
|
7
|
+
import { withRetry, ReveniumApiError, createErrorContext, handleError, } from "./utils/error-handling.js";
|
|
8
|
+
import { DEFAULT_CONFIG, API_ENDPOINTS, LOGGING_CONFIG, ANTHROPIC_PATTERNS, } from "./constants.js";
|
|
9
|
+
// Global logger
|
|
10
|
+
const logger = getLogger();
|
|
11
|
+
/**
|
|
12
|
+
* Send tracking data to Revenium API with resilience patterns
|
|
13
|
+
*/
|
|
14
|
+
export async function sendReveniumMetrics(data) {
|
|
15
|
+
const config = getConfig();
|
|
16
|
+
if (!config)
|
|
17
|
+
throw new Error("Revenium configuration not available");
|
|
18
|
+
const requestId = data.requestId;
|
|
19
|
+
logger.debug("Preparing to send metrics to Revenium", {
|
|
20
|
+
requestId,
|
|
21
|
+
model: data.model,
|
|
22
|
+
inputTokens: data.inputTokens,
|
|
23
|
+
outputTokens: data.outputTokens,
|
|
24
|
+
duration: data.duration,
|
|
25
|
+
isStreamed: data.isStreamed,
|
|
26
|
+
});
|
|
27
|
+
// Build payload using exact structure from working implementations
|
|
28
|
+
const payload = buildReveniumPayload(data);
|
|
29
|
+
// Create request options
|
|
30
|
+
const requestOptions = {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
"x-api-key": config.reveniumApiKey,
|
|
35
|
+
"User-Agent": LOGGING_CONFIG.USER_AGENT,
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify(payload),
|
|
38
|
+
timeout: config.apiTimeout || DEFAULT_CONFIG.API_TIMEOUT,
|
|
39
|
+
};
|
|
40
|
+
// Add abort signal for timeout
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timeoutId = setTimeout(() => controller.abort(), requestOptions.timeout);
|
|
43
|
+
requestOptions.signal = controller.signal;
|
|
44
|
+
// Handle abort signal errors to prevent unhandled error events
|
|
45
|
+
controller.signal.addEventListener("abort", () => {
|
|
46
|
+
// Silently handle abort - this is expected behavior for timeouts
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
// Execute with circuit breaker and retry logic
|
|
50
|
+
await executeWithCircuitBreaker(async () => {
|
|
51
|
+
return withRetry(async () => {
|
|
52
|
+
logger.debug("Sending request to Revenium API", {
|
|
53
|
+
requestId,
|
|
54
|
+
url: `${config.reveniumBaseUrl}${API_ENDPOINTS.AI_COMPLETIONS}`,
|
|
55
|
+
payloadSize: requestOptions.body.length,
|
|
56
|
+
});
|
|
57
|
+
let response;
|
|
58
|
+
try {
|
|
59
|
+
response = await fetch(`${config.reveniumBaseUrl}${API_ENDPOINTS.AI_COMPLETIONS}`, requestOptions);
|
|
60
|
+
}
|
|
61
|
+
catch (fetchError) {
|
|
62
|
+
// Handle AbortError and other fetch errors
|
|
63
|
+
if (fetchError instanceof Error && fetchError.name === "AbortError") {
|
|
64
|
+
throw new Error(`Request timeout after ${requestOptions.timeout}ms`);
|
|
65
|
+
}
|
|
66
|
+
throw fetchError;
|
|
67
|
+
}
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
const responseText = await response
|
|
70
|
+
.text()
|
|
71
|
+
.catch(() => "Unable to read response");
|
|
72
|
+
const errorContext = createErrorContext()
|
|
73
|
+
.withRequestId(requestId)
|
|
74
|
+
.withModel(data.model)
|
|
75
|
+
.withStatus(response.status)
|
|
76
|
+
.with("responseBody", responseText)
|
|
77
|
+
.build();
|
|
78
|
+
throw new ReveniumApiError(`Revenium API error: ${response.status} ${response.statusText}`, response.status, responseText, errorContext);
|
|
79
|
+
}
|
|
80
|
+
logger.debug("Successfully sent metrics to Revenium", {
|
|
81
|
+
requestId,
|
|
82
|
+
status: response.status,
|
|
83
|
+
duration: data.duration,
|
|
84
|
+
});
|
|
85
|
+
return response;
|
|
86
|
+
}, config.maxRetries || DEFAULT_CONFIG.MAX_RETRIES);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
const errorContext = createErrorContext()
|
|
91
|
+
.withRequestId(requestId)
|
|
92
|
+
.withModel(data.model)
|
|
93
|
+
.withDuration(data.duration)
|
|
94
|
+
.build();
|
|
95
|
+
handleError(error, logger, errorContext);
|
|
96
|
+
// Always fail silently for tracking errors to prevent breaking user's application
|
|
97
|
+
// Tracking errors should never break the user's main application flow
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
clearTimeout(timeoutId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build Revenium payload from tracking data
|
|
105
|
+
*/
|
|
106
|
+
function buildReveniumPayload(data) {
|
|
107
|
+
const now = new Date().toISOString();
|
|
108
|
+
const requestTime = data.requestTime.toISOString();
|
|
109
|
+
const completionStartTime = data.responseTime.toISOString();
|
|
110
|
+
return {
|
|
111
|
+
stopReason: getStopReason(data.stopReason),
|
|
112
|
+
costType: "AI",
|
|
113
|
+
isStreamed: data.isStreamed,
|
|
114
|
+
taskType: data.metadata?.taskType,
|
|
115
|
+
agent: data.metadata?.agent,
|
|
116
|
+
operationType: "CHAT",
|
|
117
|
+
inputTokenCount: data.inputTokens,
|
|
118
|
+
outputTokenCount: data.outputTokens,
|
|
119
|
+
reasoningTokenCount: 0, // Anthropic doesn't currently have reasoning tokens
|
|
120
|
+
cacheCreationTokenCount: data.cacheCreationTokens || 0,
|
|
121
|
+
cacheReadTokenCount: data.cacheReadTokens || 0,
|
|
122
|
+
totalTokenCount: data.inputTokens + data.outputTokens,
|
|
123
|
+
organizationId: data.metadata?.organizationId,
|
|
124
|
+
productId: data.metadata?.productId,
|
|
125
|
+
subscriber: data.metadata?.subscriber, // Pass through nested subscriber object directly
|
|
126
|
+
subscriptionId: data.metadata?.subscriptionId,
|
|
127
|
+
model: data.model,
|
|
128
|
+
transactionId: data.requestId,
|
|
129
|
+
responseTime: now,
|
|
130
|
+
requestDuration: Math.round(data.duration),
|
|
131
|
+
provider: "Anthropic",
|
|
132
|
+
requestTime: requestTime,
|
|
133
|
+
completionStartTime: completionStartTime,
|
|
134
|
+
timeToFirstToken: data.timeToFirstToken || 0,
|
|
135
|
+
traceId: data.metadata?.traceId,
|
|
136
|
+
middlewareSource: "nodejs",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Normalize stop reason for Revenium API
|
|
141
|
+
*/
|
|
142
|
+
function getStopReason(stopReason) {
|
|
143
|
+
if (!stopReason)
|
|
144
|
+
return "END";
|
|
145
|
+
// Use predefined mapping from constants
|
|
146
|
+
return (ANTHROPIC_PATTERNS.REVENIUM_STOP_REASON_MAP[stopReason] || "END");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Fire-and-forget async tracking wrapper
|
|
150
|
+
* Ensures tracking never blocks the main application flow
|
|
151
|
+
*/
|
|
152
|
+
export function trackUsageAsync(trackingData) {
|
|
153
|
+
const config = getConfig();
|
|
154
|
+
if (!config)
|
|
155
|
+
return logger.warn("Revenium configuration not available - skipping tracking", {
|
|
156
|
+
requestId: trackingData.requestId,
|
|
157
|
+
});
|
|
158
|
+
// Run tracking in background without awaiting
|
|
159
|
+
sendReveniumMetrics(trackingData)
|
|
160
|
+
.then(() => {
|
|
161
|
+
logger.debug("Revenium tracking completed successfully", {
|
|
162
|
+
requestId: trackingData.requestId,
|
|
163
|
+
model: trackingData.model,
|
|
164
|
+
totalTokens: trackingData.inputTokens + trackingData.outputTokens,
|
|
165
|
+
});
|
|
166
|
+
})
|
|
167
|
+
.catch((error) => {
|
|
168
|
+
const errorContext = createErrorContext()
|
|
169
|
+
.withRequestId(trackingData.requestId)
|
|
170
|
+
.withModel(trackingData.model)
|
|
171
|
+
.build();
|
|
172
|
+
logger.warn("Revenium tracking failed", {
|
|
173
|
+
error: error instanceof Error ? error.message : String(error),
|
|
174
|
+
requestId: trackingData.requestId,
|
|
175
|
+
context: errorContext,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Extract usage data from Anthropic response
|
|
181
|
+
*/
|
|
182
|
+
export function extractUsageFromResponse(response) {
|
|
183
|
+
const usage = response.usage || {};
|
|
184
|
+
return {
|
|
185
|
+
inputTokens: usage.input_tokens || 0,
|
|
186
|
+
outputTokens: usage.output_tokens || 0,
|
|
187
|
+
cacheCreationTokens: usage.cache_creation_input_tokens,
|
|
188
|
+
cacheReadTokens: usage.cache_read_input_tokens,
|
|
189
|
+
stopReason: response.stop_reason,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Extract usage data from streaming chunks
|
|
194
|
+
*/
|
|
195
|
+
export function extractUsageFromStream(chunks) {
|
|
196
|
+
let inputTokens = 0;
|
|
197
|
+
let outputTokens = 0;
|
|
198
|
+
let cacheCreationTokens;
|
|
199
|
+
let cacheReadTokens;
|
|
200
|
+
let stopReason;
|
|
201
|
+
for (const chunk of chunks) {
|
|
202
|
+
let usage = null;
|
|
203
|
+
// According to Anthropic docs, usage data appears in:
|
|
204
|
+
// 1. message_start event: chunk.message.usage (initial token count)
|
|
205
|
+
// 2. message_delta event: chunk.usage (final token count)
|
|
206
|
+
// 3. Direct usage field on chunk
|
|
207
|
+
if (chunk?.type === "message_start" && chunk?.message?.usage) {
|
|
208
|
+
usage = chunk?.message?.usage;
|
|
209
|
+
}
|
|
210
|
+
else if (chunk?.usage) {
|
|
211
|
+
usage = chunk?.usage;
|
|
212
|
+
}
|
|
213
|
+
else if (chunk?.delta?.usage) {
|
|
214
|
+
usage = chunk?.delta?.usage;
|
|
215
|
+
}
|
|
216
|
+
//Verify usage with optional chaining
|
|
217
|
+
// Use the highest token counts found (message_delta should have final counts)
|
|
218
|
+
if (usage?.input_tokens) {
|
|
219
|
+
inputTokens = Math.max(inputTokens, usage?.input_tokens);
|
|
220
|
+
}
|
|
221
|
+
if (usage?.output_tokens) {
|
|
222
|
+
outputTokens = Math.max(outputTokens, usage?.output_tokens);
|
|
223
|
+
}
|
|
224
|
+
if (usage?.cache_creation_input_tokens) {
|
|
225
|
+
cacheCreationTokens = usage?.cache_creation_input_tokens;
|
|
226
|
+
}
|
|
227
|
+
if (usage?.cache_read_input_tokens) {
|
|
228
|
+
cacheReadTokens = usage?.cache_read_input_tokens;
|
|
229
|
+
}
|
|
230
|
+
// Extract stop reason from delta
|
|
231
|
+
if (chunk?.delta?.stop_reason) {
|
|
232
|
+
stopReason = chunk?.delta?.stop_reason;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
inputTokens,
|
|
237
|
+
outputTokens,
|
|
238
|
+
cacheCreationTokens,
|
|
239
|
+
cacheReadTokens,
|
|
240
|
+
stopReason,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=tracking.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript module augmentation for Anthropic SDK
|
|
3
|
+
*
|
|
4
|
+
* This file extends Anthropic's existing types to include the usageMetadata field
|
|
5
|
+
* through TypeScript's declaration merging feature. This allows developers to
|
|
6
|
+
* use usageMetadata directly in Anthropic API calls without type casting or
|
|
7
|
+
* TypeScript errors.
|
|
8
|
+
*
|
|
9
|
+
* ## What is Module Augmentation?
|
|
10
|
+
*
|
|
11
|
+
* Module augmentation is a TypeScript feature that allows you to extend existing
|
|
12
|
+
* interfaces from external libraries. When you import this middleware, TypeScript
|
|
13
|
+
* automatically recognizes the usageMetadata field as a valid parameter.
|
|
14
|
+
*
|
|
15
|
+
* ## Benefits:
|
|
16
|
+
* - **Type Safety**: Full IntelliSense support for usageMetadata
|
|
17
|
+
* - **No Type Casting**: Use usageMetadata directly without `as any`
|
|
18
|
+
* - **Automatic Validation**: TypeScript validates the structure at compile time
|
|
19
|
+
* - **Better Developer Experience**: Auto-completion and error detection
|
|
20
|
+
*
|
|
21
|
+
* ## Usage Examples:
|
|
22
|
+
*
|
|
23
|
+
* ### Basic Usage:
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import 'revenium-middleware-anthropic-node';
|
|
26
|
+
* import Anthropic from '@anthropic-ai/sdk';
|
|
27
|
+
*
|
|
28
|
+
* const anthropic = new Anthropic();
|
|
29
|
+
*
|
|
30
|
+
* const response = await anthropic.messages.create({
|
|
31
|
+
* model: 'claude-3-5-sonnet-latest',
|
|
32
|
+
* max_tokens: 1024,
|
|
33
|
+
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
34
|
+
* usageMetadata: { // TypeScript recognizes this natively
|
|
35
|
+
* subscriber: { id: 'user-123', email: 'user@example.com' },
|
|
36
|
+
* organizationId: 'my-company',
|
|
37
|
+
* taskType: 'customer-support',
|
|
38
|
+
* traceId: 'session-abc-123'
|
|
39
|
+
* }
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* ### Streaming Usage:
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const stream = await anthropic.messages.stream({
|
|
46
|
+
* model: 'claude-3-5-sonnet-latest',
|
|
47
|
+
* max_tokens: 1024,
|
|
48
|
+
* messages: [{ role: 'user', content: 'Generate a report' }],
|
|
49
|
+
* usageMetadata: {
|
|
50
|
+
* taskType: 'content-generation',
|
|
51
|
+
* productId: 'report-generator',
|
|
52
|
+
* responseQualityScore: 0.95
|
|
53
|
+
* }
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* ### Advanced Usage with All Fields:
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const response = await anthropic.messages.create({
|
|
60
|
+
* model: 'claude-3-5-sonnet-latest',
|
|
61
|
+
* max_tokens: 2048,
|
|
62
|
+
* messages: [{ role: 'user', content: 'Complex analysis task' }],
|
|
63
|
+
* usageMetadata: {
|
|
64
|
+
* subscriber: {
|
|
65
|
+
* id: 'user-456',
|
|
66
|
+
* email: 'analyst@company.com',
|
|
67
|
+
* credential: { name: 'api-key', value: 'sk-...' }
|
|
68
|
+
* },
|
|
69
|
+
* traceId: 'analysis-session-789',
|
|
70
|
+
* taskId: 'task-001',
|
|
71
|
+
* taskType: 'data-analysis',
|
|
72
|
+
* organizationId: 'enterprise-client',
|
|
73
|
+
* subscriptionId: 'premium-plan',
|
|
74
|
+
* productId: 'analytics-suite',
|
|
75
|
+
* agent: 'data-analyst-bot',
|
|
76
|
+
* responseQualityScore: 0.98,
|
|
77
|
+
* customField: 'custom-value' // Extensible with custom fields
|
|
78
|
+
* }
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @public
|
|
83
|
+
* @since 1.1.0
|
|
84
|
+
*/
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=anthropic-augmentation.js.map
|