@revenium/anthropic 1.0.8 → 1.0.9
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/CHANGELOG.md +34 -1
- package/README.md +156 -49
- package/dist/cjs/config.js +55 -7
- package/dist/cjs/constants.js +39 -24
- package/dist/cjs/tracking.js +82 -20
- package/dist/cjs/types/anthropic-augmentation.js +0 -62
- package/dist/cjs/utils/summary-printer.js +189 -0
- package/dist/cjs/utils/trace-fields.js +117 -0
- package/dist/cjs/utils/validation.js +55 -23
- package/dist/cjs/wrapper.js +49 -41
- package/dist/esm/config.js +55 -7
- package/dist/esm/constants.js +38 -23
- package/dist/esm/tracking.js +82 -20
- package/dist/esm/types/anthropic-augmentation.js +0 -62
- package/dist/esm/utils/summary-printer.js +186 -0
- package/dist/esm/utils/trace-fields.js +106 -0
- package/dist/esm/utils/validation.js +56 -24
- package/dist/esm/wrapper.js +55 -47
- package/dist/types/config.d.ts +1 -0
- package/dist/types/constants.d.ts +15 -0
- package/dist/types/types/anthropic-augmentation.d.ts +0 -92
- package/dist/types/types.d.ts +28 -196
- package/dist/types/utils/summary-printer.d.ts +3 -0
- package/dist/types/utils/trace-fields.d.ts +10 -0
- package/examples/advanced.ts +128 -0
- package/examples/basic.ts +132 -0
- package/examples/getting_started.ts +6 -6
- package/examples/metadata.ts +58 -0
- package/package.json +4 -6
- package/examples/advanced-features.ts +0 -469
- package/examples/basic-usage.ts +0 -314
package/dist/esm/tracking.js
CHANGED
|
@@ -6,8 +6,21 @@ import { getConfig, getLogger } from "./config.js";
|
|
|
6
6
|
import { executeWithCircuitBreaker } from "./utils/circuit-breaker.js";
|
|
7
7
|
import { withRetry, ReveniumApiError, createErrorContext, handleError, } from "./utils/error-handling.js";
|
|
8
8
|
import { DEFAULT_CONFIG, API_ENDPOINTS, LOGGING_CONFIG, ANTHROPIC_PATTERNS, } from "./constants.js";
|
|
9
|
+
import { getEnvironment, getRegion, getCredentialAlias, getTraceType, getTraceName, detectOperationSubtype, getParentTransactionId, getTransactionName, getRetryNumber, } from "./utils/trace-fields.js";
|
|
10
|
+
import { printUsageSummary } from "./utils/summary-printer.js";
|
|
9
11
|
// Global logger
|
|
10
12
|
const logger = getLogger();
|
|
13
|
+
if (typeof process !== "undefined") {
|
|
14
|
+
process.on("uncaughtException", (error) => {
|
|
15
|
+
if (error.name === "AbortError") {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (error.stack && error.stack.includes("tracking.ts")) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
11
24
|
/**
|
|
12
25
|
* Send tracking data to Revenium API with resilience patterns
|
|
13
26
|
*/
|
|
@@ -25,7 +38,8 @@ export async function sendReveniumMetrics(data) {
|
|
|
25
38
|
isStreamed: data.isStreamed,
|
|
26
39
|
});
|
|
27
40
|
// Build payload using exact structure from working implementations
|
|
28
|
-
|
|
41
|
+
// Built early so we can print summary even if Revenium tracking fails
|
|
42
|
+
const payload = await buildReveniumPayload(data);
|
|
29
43
|
// Create request options
|
|
30
44
|
const requestOptions = {
|
|
31
45
|
method: "POST",
|
|
@@ -41,10 +55,6 @@ export async function sendReveniumMetrics(data) {
|
|
|
41
55
|
const controller = new AbortController();
|
|
42
56
|
const timeoutId = setTimeout(() => controller.abort(), requestOptions.timeout);
|
|
43
57
|
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
58
|
try {
|
|
49
59
|
// Execute with circuit breaker and retry logic
|
|
50
60
|
await executeWithCircuitBreaker(async () => {
|
|
@@ -57,9 +67,12 @@ export async function sendReveniumMetrics(data) {
|
|
|
57
67
|
let response;
|
|
58
68
|
try {
|
|
59
69
|
response = await fetch(`${config.reveniumBaseUrl}${API_ENDPOINTS.AI_COMPLETIONS}`, requestOptions);
|
|
70
|
+
if (response.body) {
|
|
71
|
+
response.body.once("error", () => { });
|
|
72
|
+
response.body.resume();
|
|
73
|
+
}
|
|
60
74
|
}
|
|
61
75
|
catch (fetchError) {
|
|
62
|
-
// Handle AbortError and other fetch errors
|
|
63
76
|
if (fetchError instanceof Error && fetchError.name === "AbortError") {
|
|
64
77
|
throw new Error(`Request timeout after ${requestOptions.timeout}ms`);
|
|
65
78
|
}
|
|
@@ -82,6 +95,17 @@ export async function sendReveniumMetrics(data) {
|
|
|
82
95
|
status: response.status,
|
|
83
96
|
duration: data.duration,
|
|
84
97
|
});
|
|
98
|
+
try {
|
|
99
|
+
await response.text();
|
|
100
|
+
}
|
|
101
|
+
catch (bodyError) {
|
|
102
|
+
logger.debug("Error reading response body (non-critical)", {
|
|
103
|
+
requestId,
|
|
104
|
+
error: bodyError instanceof Error
|
|
105
|
+
? bodyError.message
|
|
106
|
+
: String(bodyError),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
85
109
|
return response;
|
|
86
110
|
}, config.maxRetries ?? DEFAULT_CONFIG.MAX_RETRIES);
|
|
87
111
|
});
|
|
@@ -102,19 +126,28 @@ export async function sendReveniumMetrics(data) {
|
|
|
102
126
|
}
|
|
103
127
|
finally {
|
|
104
128
|
clearTimeout(timeoutId);
|
|
129
|
+
// Print usage summary regardless of whether Revenium tracking succeeded or failed
|
|
130
|
+
// This ensures users see the summary "after each Anthropic API request" as documented
|
|
131
|
+
printUsageSummary(payload);
|
|
105
132
|
}
|
|
106
133
|
}
|
|
107
134
|
/**
|
|
108
135
|
* Build Revenium payload from tracking data
|
|
109
136
|
*/
|
|
110
|
-
function buildReveniumPayload(data) {
|
|
137
|
+
async function buildReveniumPayload(data) {
|
|
111
138
|
const now = new Date().toISOString();
|
|
112
139
|
const requestTime = data.requestTime.toISOString();
|
|
113
140
|
const completionStartTime = data.responseTime.toISOString();
|
|
114
141
|
// Standard known fields
|
|
115
142
|
const knownFields = new Set([
|
|
116
|
-
|
|
117
|
-
|
|
143
|
+
"taskType",
|
|
144
|
+
"agent",
|
|
145
|
+
"organizationId",
|
|
146
|
+
"productId",
|
|
147
|
+
"subscriber",
|
|
148
|
+
"subscriptionId",
|
|
149
|
+
"traceId",
|
|
150
|
+
"responseQualityScore",
|
|
118
151
|
]);
|
|
119
152
|
// Extract custom fields (anything NOT in the standard 8 fields)
|
|
120
153
|
const customFields = {};
|
|
@@ -125,23 +158,26 @@ function buildReveniumPayload(data) {
|
|
|
125
158
|
}
|
|
126
159
|
}
|
|
127
160
|
}
|
|
161
|
+
const environment = getEnvironment();
|
|
162
|
+
const region = await getRegion();
|
|
163
|
+
const credentialAlias = getCredentialAlias();
|
|
164
|
+
const traceType = getTraceType();
|
|
165
|
+
const traceName = getTraceName();
|
|
166
|
+
const parentTransactionId = getParentTransactionId();
|
|
167
|
+
const transactionName = getTransactionName();
|
|
168
|
+
const retryNumber = getRetryNumber();
|
|
169
|
+
const operationSubtype = detectOperationSubtype(data.requestBody);
|
|
128
170
|
return {
|
|
129
171
|
stopReason: getStopReason(data.stopReason),
|
|
130
172
|
costType: "AI",
|
|
131
173
|
isStreamed: data.isStreamed,
|
|
132
|
-
taskType: data.metadata?.taskType,
|
|
133
|
-
agent: data.metadata?.agent,
|
|
134
174
|
operationType: "CHAT",
|
|
135
175
|
inputTokenCount: data.inputTokens,
|
|
136
176
|
outputTokenCount: data.outputTokens,
|
|
137
|
-
reasoningTokenCount: 0,
|
|
177
|
+
reasoningTokenCount: 0,
|
|
138
178
|
cacheCreationTokenCount: data.cacheCreationTokens || 0,
|
|
139
179
|
cacheReadTokenCount: data.cacheReadTokens || 0,
|
|
140
180
|
totalTokenCount: data.inputTokens + data.outputTokens,
|
|
141
|
-
organizationId: data.metadata?.organizationId,
|
|
142
|
-
productId: data.metadata?.productId,
|
|
143
|
-
subscriber: data.metadata?.subscriber, // Pass through nested subscriber object directly
|
|
144
|
-
subscriptionId: data.metadata?.subscriptionId,
|
|
145
181
|
model: data.model,
|
|
146
182
|
transactionId: data.requestId,
|
|
147
183
|
responseTime: now,
|
|
@@ -150,10 +186,31 @@ function buildReveniumPayload(data) {
|
|
|
150
186
|
requestTime: requestTime,
|
|
151
187
|
completionStartTime: completionStartTime,
|
|
152
188
|
timeToFirstToken: data.timeToFirstToken || 0,
|
|
153
|
-
traceId: data.metadata?.traceId,
|
|
154
|
-
responseQualityScore: data.metadata?.responseQualityScore, // Fixed: Now sending to Revenium
|
|
155
189
|
middlewareSource: "nodejs",
|
|
156
|
-
...
|
|
190
|
+
...(data.metadata?.taskType && { taskType: data.metadata.taskType }),
|
|
191
|
+
...(data.metadata?.agent && { agent: data.metadata.agent }),
|
|
192
|
+
...(data.metadata?.organizationId && {
|
|
193
|
+
organizationId: data.metadata.organizationId,
|
|
194
|
+
}),
|
|
195
|
+
...(data.metadata?.productId && { productId: data.metadata.productId }),
|
|
196
|
+
...(data.metadata?.subscriber && { subscriber: data.metadata.subscriber }),
|
|
197
|
+
...(data.metadata?.subscriptionId && {
|
|
198
|
+
subscriptionId: data.metadata.subscriptionId,
|
|
199
|
+
}),
|
|
200
|
+
...(data.metadata?.traceId && { traceId: data.metadata.traceId }),
|
|
201
|
+
...(data.metadata?.responseQualityScore !== undefined && {
|
|
202
|
+
responseQualityScore: data.metadata.responseQualityScore,
|
|
203
|
+
}),
|
|
204
|
+
...(environment && { environment }),
|
|
205
|
+
...(region && { region }),
|
|
206
|
+
...(credentialAlias && { credentialAlias }),
|
|
207
|
+
...(traceType && { traceType }),
|
|
208
|
+
...(traceName && { traceName }),
|
|
209
|
+
...(parentTransactionId && { parentTransactionId }),
|
|
210
|
+
...(transactionName && { transactionName }),
|
|
211
|
+
...(retryNumber !== undefined && { retryNumber }),
|
|
212
|
+
...(operationSubtype && { operationSubtype }),
|
|
213
|
+
...customFields,
|
|
157
214
|
};
|
|
158
215
|
}
|
|
159
216
|
/**
|
|
@@ -175,7 +232,6 @@ export function trackUsageAsync(trackingData) {
|
|
|
175
232
|
return logger.warn("Revenium configuration not available - skipping tracking", {
|
|
176
233
|
requestId: trackingData.requestId,
|
|
177
234
|
});
|
|
178
|
-
// Run tracking in background without awaiting
|
|
179
235
|
sendReveniumMetrics(trackingData)
|
|
180
236
|
.then(() => {
|
|
181
237
|
logger.debug("Revenium tracking completed successfully", {
|
|
@@ -185,6 +241,12 @@ export function trackUsageAsync(trackingData) {
|
|
|
185
241
|
});
|
|
186
242
|
})
|
|
187
243
|
.catch((error) => {
|
|
244
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
245
|
+
logger.debug("Metrics request aborted (process exiting)", {
|
|
246
|
+
requestId: trackingData.requestId,
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
188
250
|
const errorContext = createErrorContext()
|
|
189
251
|
.withRequestId(trackingData.requestId)
|
|
190
252
|
.withModel(trackingData.model)
|
|
@@ -18,68 +18,6 @@
|
|
|
18
18
|
* - **Automatic Validation**: TypeScript validates the structure at compile time
|
|
19
19
|
* - **Better Developer Experience**: Auto-completion and error detection
|
|
20
20
|
*
|
|
21
|
-
* ## Usage Examples:
|
|
22
|
-
*
|
|
23
|
-
* ### Basic Usage:
|
|
24
|
-
* ```typescript
|
|
25
|
-
* import '@revenium/anthropic';
|
|
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
|
-
* taskType: 'data-analysis',
|
|
71
|
-
* organizationId: 'enterprise-client',
|
|
72
|
-
* subscriptionId: 'premium-plan',
|
|
73
|
-
* productId: 'analytics-suite',
|
|
74
|
-
* agent: 'data-analyst-bot',
|
|
75
|
-
* responseQualityScore: 0.98,
|
|
76
|
-
* customField: 'custom-value' // Extensible with custom fields
|
|
77
|
-
* }
|
|
78
|
-
* });
|
|
79
|
-
* ```
|
|
80
|
-
*
|
|
81
|
-
* @public
|
|
82
|
-
* @since 1.1.0
|
|
83
21
|
*/
|
|
84
22
|
export {};
|
|
85
23
|
//# sourceMappingURL=anthropic-augmentation.js.map
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { getConfig, getLogger } from "../config.js";
|
|
2
|
+
import { DEFAULT_CONFIG, SUMMARY_PRINTER_CONFIG } from "../constants.js";
|
|
3
|
+
const logger = getLogger();
|
|
4
|
+
function delayWithUnref(ms) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const timer = setTimeout(resolve, ms);
|
|
7
|
+
if (typeof timer.unref === "function") {
|
|
8
|
+
timer.unref();
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
async function fetchCompletionMetrics(transactionId, maxRetries = SUMMARY_PRINTER_CONFIG.MAX_RETRIES, retryDelay = SUMMARY_PRINTER_CONFIG.RETRY_DELAY) {
|
|
13
|
+
const config = getConfig();
|
|
14
|
+
if (!config) {
|
|
15
|
+
logger.debug("No config available for summary printing");
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (!config.teamId) {
|
|
19
|
+
logger.debug("Team ID not configured, skipping cost retrieval for summary");
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const baseUrl = (config.reveniumBaseUrl || DEFAULT_CONFIG.REVENIUM_BASE_URL).replace(/\/+$/, "");
|
|
23
|
+
const url = `${baseUrl}/profitstream/v2/api/sources/metrics/ai/completions`;
|
|
24
|
+
const teamId = config.teamId.trim();
|
|
25
|
+
const urlWithParams = `${url}?teamId=${encodeURIComponent(teamId)}&transactionId=${encodeURIComponent(transactionId)}`;
|
|
26
|
+
logger.debug("Fetching completion metrics", { url: urlWithParams });
|
|
27
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
28
|
+
// Create an AbortController with timeout to prevent hung requests from keeping Node process alive
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timeoutId = setTimeout(() => controller.abort(), SUMMARY_PRINTER_CONFIG.FETCH_TIMEOUT);
|
|
31
|
+
// Unref the timer so it doesn't keep the process alive
|
|
32
|
+
if (typeof timeoutId.unref === "function") {
|
|
33
|
+
timeoutId.unref();
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch(urlWithParams, {
|
|
37
|
+
method: "GET",
|
|
38
|
+
headers: {
|
|
39
|
+
Accept: "application/json",
|
|
40
|
+
"x-api-key": config.reveniumApiKey,
|
|
41
|
+
},
|
|
42
|
+
signal: controller.signal,
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
try {
|
|
46
|
+
await response.text();
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
logger.debug(`Completions metrics API returned ${response.status}`, {
|
|
50
|
+
attempt: attempt + 1,
|
|
51
|
+
});
|
|
52
|
+
if (attempt < maxRetries - 1) {
|
|
53
|
+
await delayWithUnref(retryDelay);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const data = (await response.json());
|
|
59
|
+
const completions = data._embedded?.aICompletionMetricResourceList;
|
|
60
|
+
if (completions && completions.length > 0) {
|
|
61
|
+
return completions[0];
|
|
62
|
+
}
|
|
63
|
+
if (attempt < maxRetries - 1) {
|
|
64
|
+
logger.debug(`Waiting for metrics to aggregate (attempt ${attempt + 1}/${maxRetries})...`);
|
|
65
|
+
await delayWithUnref(retryDelay);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
logger.debug("Failed to fetch completion metrics", {
|
|
70
|
+
error: error instanceof Error ? error.message : String(error),
|
|
71
|
+
attempt: attempt + 1,
|
|
72
|
+
});
|
|
73
|
+
if (attempt < maxRetries - 1) {
|
|
74
|
+
await delayWithUnref(retryDelay);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
clearTimeout(timeoutId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function isSummaryFormat(value) {
|
|
84
|
+
return value === "human" || value === "json";
|
|
85
|
+
}
|
|
86
|
+
function formatAndPrintJsonSummary(payload, metrics) {
|
|
87
|
+
const config = getConfig();
|
|
88
|
+
const summary = {
|
|
89
|
+
model: payload.model,
|
|
90
|
+
provider: payload.provider,
|
|
91
|
+
durationSeconds: payload.requestDuration / 1000,
|
|
92
|
+
inputTokenCount: payload.inputTokenCount,
|
|
93
|
+
outputTokenCount: payload.outputTokenCount,
|
|
94
|
+
totalTokenCount: payload.totalTokenCount,
|
|
95
|
+
cost: typeof metrics?.totalCost === "number" ? metrics.totalCost : null,
|
|
96
|
+
};
|
|
97
|
+
if (summary.cost === null) {
|
|
98
|
+
summary.costStatus = config?.teamId ? "pending" : "unavailable";
|
|
99
|
+
}
|
|
100
|
+
if (payload.traceId) {
|
|
101
|
+
summary.traceId = payload.traceId;
|
|
102
|
+
}
|
|
103
|
+
console.log(JSON.stringify(summary));
|
|
104
|
+
}
|
|
105
|
+
function formatAndPrintHumanSummary(payload, metrics) {
|
|
106
|
+
console.log("\n" + "=".repeat(60));
|
|
107
|
+
console.log("📊 REVENIUM USAGE SUMMARY");
|
|
108
|
+
console.log("=".repeat(60));
|
|
109
|
+
console.log(`🤖 Model: ${payload.model}`);
|
|
110
|
+
console.log(`🏢 Provider: ${payload.provider}`);
|
|
111
|
+
console.log(`⏱️ Duration: ${(payload.requestDuration / 1000).toFixed(2)}s`);
|
|
112
|
+
console.log("\n💬 Token Usage:");
|
|
113
|
+
console.log(` 📥 Input Tokens: ${(payload.inputTokenCount ?? 0).toLocaleString()}`);
|
|
114
|
+
console.log(` 📤 Output Tokens: ${(payload.outputTokenCount ?? 0).toLocaleString()}`);
|
|
115
|
+
console.log(` 📊 Total Tokens: ${(payload.totalTokenCount ?? 0).toLocaleString()}`);
|
|
116
|
+
if (typeof metrics?.totalCost === "number") {
|
|
117
|
+
console.log(`\n💰 Cost: $${metrics.totalCost.toFixed(6)}`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const config = getConfig();
|
|
121
|
+
if (!config?.teamId) {
|
|
122
|
+
console.log(`\n💰 Cost: Set REVENIUM_TEAM_ID environment variable to see pricing`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
console.log(`\n💰 Cost: (pending aggregation)`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (payload.traceId) {
|
|
129
|
+
console.log(`\n🔖 Trace ID: ${payload.traceId}`);
|
|
130
|
+
}
|
|
131
|
+
console.log("=".repeat(60) + "\n");
|
|
132
|
+
}
|
|
133
|
+
function formatAndPrintSummary(payload, metrics, format) {
|
|
134
|
+
if (format === "json") {
|
|
135
|
+
formatAndPrintJsonSummary(payload, metrics);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
formatAndPrintHumanSummary(payload, metrics);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function safeFormatAndPrintSummary(payload, metrics, format) {
|
|
142
|
+
try {
|
|
143
|
+
formatAndPrintSummary(payload, metrics, format);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
logger.debug("Failed to format and print summary", {
|
|
147
|
+
error: error instanceof Error ? error.message : String(error),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function getSummaryFormat(value) {
|
|
152
|
+
if (!value)
|
|
153
|
+
return null;
|
|
154
|
+
if (value === true)
|
|
155
|
+
return "human";
|
|
156
|
+
if (isSummaryFormat(value)) {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
export function printUsageSummary(payload) {
|
|
162
|
+
const config = getConfig();
|
|
163
|
+
const format = getSummaryFormat(config?.printSummary);
|
|
164
|
+
if (!format) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (config?.teamId && payload.transactionId) {
|
|
168
|
+
fetchCompletionMetrics(payload.transactionId)
|
|
169
|
+
.then((metrics) => {
|
|
170
|
+
safeFormatAndPrintSummary(payload, metrics, format);
|
|
171
|
+
})
|
|
172
|
+
.catch((error) => {
|
|
173
|
+
logger.debug("Failed to print usage summary with metrics", {
|
|
174
|
+
error: error instanceof Error ? error.message : String(error),
|
|
175
|
+
});
|
|
176
|
+
safeFormatAndPrintSummary(payload, null, format);
|
|
177
|
+
})
|
|
178
|
+
.catch(() => {
|
|
179
|
+
// Final safety catch to prevent unhandled rejections
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
safeFormatAndPrintSummary(payload, null, format);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=summary-printer.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { getLogger } from "../config.js";
|
|
2
|
+
const logger = getLogger();
|
|
3
|
+
let cachedRegion = null;
|
|
4
|
+
let regionCached = false;
|
|
5
|
+
export function getEnvironment() {
|
|
6
|
+
const env = process.env.REVENIUM_ENVIRONMENT ||
|
|
7
|
+
process.env.NODE_ENV ||
|
|
8
|
+
process.env.DEPLOYMENT_ENV ||
|
|
9
|
+
null;
|
|
10
|
+
if (env && env.length > 255) {
|
|
11
|
+
logger.warn(`environment exceeds max length of 255 characters. Truncating.`);
|
|
12
|
+
return env.substring(0, 255).trim();
|
|
13
|
+
}
|
|
14
|
+
return env ? env.trim() : null;
|
|
15
|
+
}
|
|
16
|
+
export async function getRegion() {
|
|
17
|
+
if (regionCached) {
|
|
18
|
+
return cachedRegion;
|
|
19
|
+
}
|
|
20
|
+
const envRegion = process.env.AWS_REGION ||
|
|
21
|
+
process.env.AZURE_REGION ||
|
|
22
|
+
process.env.GCP_REGION ||
|
|
23
|
+
process.env.REVENIUM_REGION;
|
|
24
|
+
if (envRegion) {
|
|
25
|
+
cachedRegion = envRegion.trim();
|
|
26
|
+
regionCached = true;
|
|
27
|
+
return cachedRegion;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timeoutId = setTimeout(() => controller.abort(), 1000);
|
|
32
|
+
const response = await fetch("http://169.254.169.254/latest/meta-data/placement/region", {
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
clearTimeout(timeoutId);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
cachedRegion = null;
|
|
38
|
+
regionCached = true;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
cachedRegion = text.trim();
|
|
43
|
+
regionCached = true;
|
|
44
|
+
return cachedRegion;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
cachedRegion = null;
|
|
48
|
+
regionCached = true;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function getCredentialAlias() {
|
|
53
|
+
const alias = process.env.REVENIUM_CREDENTIAL_ALIAS || null;
|
|
54
|
+
if (alias && alias.length > 255) {
|
|
55
|
+
logger.warn(`credentialAlias exceeds max length of 255 characters. Truncating.`);
|
|
56
|
+
return alias.substring(0, 255).trim();
|
|
57
|
+
}
|
|
58
|
+
return alias ? alias.trim() : null;
|
|
59
|
+
}
|
|
60
|
+
export function getTraceType() {
|
|
61
|
+
const traceType = process.env.REVENIUM_TRACE_TYPE;
|
|
62
|
+
if (!traceType) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(traceType)) {
|
|
66
|
+
logger.warn(`Invalid trace_type format: ${traceType}. Must be alphanumeric with hyphens/underscores only.`);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (traceType.length > 128) {
|
|
70
|
+
logger.warn(`trace_type exceeds max length of 128 characters: ${traceType}. Truncating.`);
|
|
71
|
+
return traceType.substring(0, 128);
|
|
72
|
+
}
|
|
73
|
+
return traceType;
|
|
74
|
+
}
|
|
75
|
+
export function getTraceName() {
|
|
76
|
+
const traceName = process.env.REVENIUM_TRACE_NAME;
|
|
77
|
+
if (!traceName) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (traceName.length > 256) {
|
|
81
|
+
logger.warn(`trace_name exceeds max length of 256 characters. Truncating.`);
|
|
82
|
+
return traceName.substring(0, 256);
|
|
83
|
+
}
|
|
84
|
+
return traceName;
|
|
85
|
+
}
|
|
86
|
+
export function detectOperationSubtype(requestBody) {
|
|
87
|
+
if (requestBody && requestBody.tools && requestBody.tools.length > 0) {
|
|
88
|
+
return "function_call";
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
export function getParentTransactionId() {
|
|
93
|
+
return process.env.REVENIUM_PARENT_TRANSACTION_ID || null;
|
|
94
|
+
}
|
|
95
|
+
export function getTransactionName() {
|
|
96
|
+
return process.env.REVENIUM_TRANSACTION_NAME || null;
|
|
97
|
+
}
|
|
98
|
+
export function getRetryNumber() {
|
|
99
|
+
const retryNum = process.env.REVENIUM_RETRY_NUMBER;
|
|
100
|
+
if (retryNum) {
|
|
101
|
+
const parsed = parseInt(retryNum, 10);
|
|
102
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
103
|
+
}
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=trace-fields.js.map
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Validation utilities for Anthropic middleware
|
|
3
3
|
* Provides type-safe validation with detailed error reporting
|
|
4
4
|
*/
|
|
5
|
-
import { VALIDATION_CONFIG, ANTHROPIC_PATTERNS } from '../constants.js';
|
|
5
|
+
import { VALIDATION_CONFIG, ANTHROPIC_PATTERNS, DEFAULT_CONFIG } from '../constants.js';
|
|
6
6
|
/**
|
|
7
7
|
* Type guard for checking if a value is a non-null object
|
|
8
8
|
*/
|
|
@@ -128,29 +128,31 @@ export function validateReveniumConfig(config) {
|
|
|
128
128
|
else if (cfg?.reveniumApiKey?.length < VALIDATION_CONFIG.MIN_API_KEY_LENGTH) {
|
|
129
129
|
warnings.push('reveniumApiKey appears to be too short - verify it is correct');
|
|
130
130
|
}
|
|
131
|
-
// Validate Revenium base URL
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
131
|
+
// Validate Revenium base URL (optional - defaults to https://api.revenium.ai)
|
|
132
|
+
if (cfg?.reveniumBaseUrl !== undefined) {
|
|
133
|
+
if (!isString(cfg?.reveniumBaseUrl)) {
|
|
134
|
+
errors.push('reveniumBaseUrl must be a string if provided');
|
|
135
|
+
}
|
|
136
|
+
else if (!cfg?.reveniumBaseUrl?.trim()) {
|
|
137
|
+
errors.push('reveniumBaseUrl cannot be empty if provided');
|
|
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
|
+
}
|
|
143
150
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
warnings.push('Using localhost for Revenium API - ensure this is intended for development');
|
|
151
|
+
catch {
|
|
152
|
+
errors.push('reveniumBaseUrl must be a valid URL');
|
|
153
|
+
suggestions.push('Use format: https://api.revenium.ai');
|
|
148
154
|
}
|
|
149
155
|
}
|
|
150
|
-
catch {
|
|
151
|
-
errors.push('reveniumBaseUrl must be a valid URL');
|
|
152
|
-
suggestions.push('Use format: https://api.revenium.ai');
|
|
153
|
-
}
|
|
154
156
|
}
|
|
155
157
|
// Validate optional Anthropic API key
|
|
156
158
|
if (cfg?.anthropicApiKey !== undefined && !isString(cfg?.anthropicApiKey)) {
|
|
@@ -192,6 +194,23 @@ export function validateReveniumConfig(config) {
|
|
|
192
194
|
else if (cfg?.maxRetries !== undefined && cfg?.maxRetries === 0) {
|
|
193
195
|
warnings.push('maxRetries is 0 - no retry attempts will be made');
|
|
194
196
|
}
|
|
197
|
+
// Validate optional printSummary
|
|
198
|
+
if (cfg?.printSummary !== undefined) {
|
|
199
|
+
const validPrintSummaryValues = [true, false, 'human', 'json'];
|
|
200
|
+
if (!validPrintSummaryValues.includes(cfg?.printSummary)) {
|
|
201
|
+
errors.push("printSummary must be a boolean or one of: 'human', 'json'");
|
|
202
|
+
suggestions.push("Use printSummary: true, 'human', or 'json' to enable summary output");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Validate optional teamId
|
|
206
|
+
if (cfg?.teamId !== undefined) {
|
|
207
|
+
if (!isString(cfg?.teamId)) {
|
|
208
|
+
errors.push('teamId must be a string if provided');
|
|
209
|
+
}
|
|
210
|
+
else if (cfg?.teamId?.trim()?.length === 0) {
|
|
211
|
+
errors.push('teamId cannot be empty if provided');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
195
214
|
if (errors.length > 0) {
|
|
196
215
|
return {
|
|
197
216
|
isValid: false,
|
|
@@ -200,14 +219,27 @@ export function validateReveniumConfig(config) {
|
|
|
200
219
|
suggestions
|
|
201
220
|
};
|
|
202
221
|
}
|
|
203
|
-
//
|
|
222
|
+
// Determine validated printSummary value
|
|
223
|
+
let validatedPrintSummary;
|
|
224
|
+
if (cfg?.printSummary === true || cfg?.printSummary === 'human') {
|
|
225
|
+
validatedPrintSummary = 'human';
|
|
226
|
+
}
|
|
227
|
+
else if (cfg?.printSummary === 'json') {
|
|
228
|
+
validatedPrintSummary = 'json';
|
|
229
|
+
}
|
|
230
|
+
else if (cfg?.printSummary === false) {
|
|
231
|
+
validatedPrintSummary = false;
|
|
232
|
+
}
|
|
233
|
+
// Build validated config (apply default for reveniumBaseUrl if not provided)
|
|
204
234
|
const validatedConfig = {
|
|
205
235
|
reveniumApiKey: cfg?.reveniumApiKey,
|
|
206
|
-
reveniumBaseUrl: cfg?.reveniumBaseUrl,
|
|
236
|
+
reveniumBaseUrl: isString(cfg?.reveniumBaseUrl) ? cfg?.reveniumBaseUrl : DEFAULT_CONFIG.REVENIUM_BASE_URL,
|
|
207
237
|
anthropicApiKey: isString(cfg?.anthropicApiKey) ? cfg?.anthropicApiKey : undefined,
|
|
208
238
|
apiTimeout: isNumber(cfg?.apiTimeout) ? cfg?.apiTimeout : undefined,
|
|
209
239
|
failSilent: isBoolean(cfg?.failSilent) ? cfg?.failSilent : undefined,
|
|
210
|
-
maxRetries: isNumber(cfg?.maxRetries) ? cfg?.maxRetries : undefined
|
|
240
|
+
maxRetries: isNumber(cfg?.maxRetries) ? cfg?.maxRetries : undefined,
|
|
241
|
+
printSummary: validatedPrintSummary,
|
|
242
|
+
teamId: isString(cfg?.teamId) ? cfg?.teamId?.trim() : undefined
|
|
211
243
|
};
|
|
212
244
|
return {
|
|
213
245
|
isValid: true,
|