@vantinelai/node-sdk 0.4.5
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/README.md +189 -0
- package/dist/client.d.ts +105 -0
- package/dist/client.js +431 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +28 -0
- package/dist/integrations/anthropic.d.ts +64 -0
- package/dist/integrations/anthropic.js +138 -0
- package/dist/integrations/index.d.ts +5 -0
- package/dist/integrations/index.js +11 -0
- package/dist/integrations/openai-agents.d.ts +70 -0
- package/dist/integrations/openai-agents.js +108 -0
- package/dist/monitor.d.ts +41 -0
- package/dist/monitor.js +308 -0
- package/dist/security.d.ts +17 -0
- package/dist/security.js +82 -0
- package/package.json +49 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.MODEL_PRICING = exports.VantinelClient = void 0;
|
|
40
|
+
exports.estimateCostFromTokens = estimateCostFromTokens;
|
|
41
|
+
exports.estimateCostFromText = estimateCostFromText;
|
|
42
|
+
exports.estimateTokens = estimateTokens;
|
|
43
|
+
const axios_1 = __importDefault(require("axios"));
|
|
44
|
+
const security_1 = require("./security");
|
|
45
|
+
/**
|
|
46
|
+
* Rough token estimation: ~4 characters per token.
|
|
47
|
+
*/
|
|
48
|
+
function estimateTokens(text) {
|
|
49
|
+
return Math.ceil(text.length / 4);
|
|
50
|
+
}
|
|
51
|
+
const DEFAULT_COST_PER_1K_TOKENS = 0.01;
|
|
52
|
+
/**
|
|
53
|
+
* Model pricing per 1k tokens (input, output, cache_read) in USD.
|
|
54
|
+
*/
|
|
55
|
+
const MODEL_PRICING = {
|
|
56
|
+
// OpenAI 2026 Models
|
|
57
|
+
'gpt-5.2': { input: 0.00175, output: 0.014 },
|
|
58
|
+
'gpt-5.2-pro': { input: 0.021, output: 0.168 },
|
|
59
|
+
'gpt-5-mini': { input: 0.00025, output: 0.002 },
|
|
60
|
+
// OpenAI Legacy
|
|
61
|
+
'gpt-4o': { input: 0.0025, output: 0.010 },
|
|
62
|
+
'gpt-4o-2024-05-13': { input: 0.005, output: 0.015 },
|
|
63
|
+
'gpt-4o-2024-08-06': { input: 0.0025, output: 0.010 },
|
|
64
|
+
'gpt-4o-2024-11-20': { input: 0.0025, output: 0.010 },
|
|
65
|
+
'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
|
|
66
|
+
'o1': { input: 0.015, output: 0.060 },
|
|
67
|
+
'o3-mini': { input: 0.0011, output: 0.0044 },
|
|
68
|
+
'gpt-4-turbo': { input: 0.01, output: 0.03 },
|
|
69
|
+
// Anthropic 2026 Models
|
|
70
|
+
'claude-4.6-opus': { input: 0.005, output: 0.025 },
|
|
71
|
+
'claude-4.6-sonnet': { input: 0.003, output: 0.015 },
|
|
72
|
+
'claude-4.5-opus': { input: 0.005, output: 0.025 },
|
|
73
|
+
'claude-4.5-sonnet': { input: 0.003, output: 0.015 },
|
|
74
|
+
'claude-4.5-haiku': { input: 0.001, output: 0.005 },
|
|
75
|
+
// Anthropic Legacy
|
|
76
|
+
'claude-3-5-sonnet-20241022': { input: 0.003, output: 0.015 },
|
|
77
|
+
'claude-3-5-haiku-20241022': { input: 0.0008, output: 0.004 },
|
|
78
|
+
'claude-3-opus': { input: 0.015, output: 0.075 },
|
|
79
|
+
// Google Models 2026
|
|
80
|
+
'gemini-3.1-pro': { input: 0.002, output: 0.012 },
|
|
81
|
+
'gemini-3.0-pro': { input: 0.002, output: 0.012 },
|
|
82
|
+
'gemini-3-flash': { input: 0.0005, output: 0.003 },
|
|
83
|
+
'gemini-2.5-flash': { input: 0.000075, output: 0.0003 },
|
|
84
|
+
'gemini-2.0-flash': { input: 0.0001, output: 0.0004 },
|
|
85
|
+
'gemini-1.5-pro': { input: 0.00125, output: 0.005 },
|
|
86
|
+
};
|
|
87
|
+
exports.MODEL_PRICING = MODEL_PRICING;
|
|
88
|
+
function estimateCostFromText(text) {
|
|
89
|
+
const tokens = estimateTokens(text);
|
|
90
|
+
return (tokens / 1000) * DEFAULT_COST_PER_1K_TOKENS;
|
|
91
|
+
}
|
|
92
|
+
function estimateCostFromTokens(model, inputTokens, outputTokens, cachedTokens = 0) {
|
|
93
|
+
const pricing = MODEL_PRICING[model];
|
|
94
|
+
if (pricing) {
|
|
95
|
+
const regularInputTokens = Math.max(0, inputTokens - cachedTokens);
|
|
96
|
+
const cacheReadPrice = pricing.cache_read !== undefined ? pricing.cache_read : pricing.input * 0.5;
|
|
97
|
+
return (regularInputTokens / 1000) * pricing.input +
|
|
98
|
+
(cachedTokens / 1000) * cacheReadPrice +
|
|
99
|
+
(outputTokens / 1000) * pricing.output;
|
|
100
|
+
}
|
|
101
|
+
return ((inputTokens + outputTokens) / 1000) * DEFAULT_COST_PER_1K_TOKENS;
|
|
102
|
+
}
|
|
103
|
+
class VantinelClient {
|
|
104
|
+
constructor(config) {
|
|
105
|
+
this.config = config;
|
|
106
|
+
this.batchQueue = [];
|
|
107
|
+
this.flushTimer = null;
|
|
108
|
+
this.globalMetadata = {};
|
|
109
|
+
const validatedUrl = (0, security_1.validateCollectorUrl)(config.collectorUrl || 'http://localhost:8000');
|
|
110
|
+
this.client = axios_1.default.create({
|
|
111
|
+
baseURL: validatedUrl,
|
|
112
|
+
timeout: 2000,
|
|
113
|
+
});
|
|
114
|
+
const interval = config.flushInterval ?? 0;
|
|
115
|
+
if (interval > 0) {
|
|
116
|
+
this.flushTimer = setInterval(() => {
|
|
117
|
+
this.flush().catch((err) => {
|
|
118
|
+
console.warn('[Vantinel] Background flush failed:', err.message);
|
|
119
|
+
});
|
|
120
|
+
}, interval);
|
|
121
|
+
// Prevent the timer from keeping the Node.js process alive
|
|
122
|
+
if (this.flushTimer.unref) {
|
|
123
|
+
this.flushTimer.unref();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
setGlobalMetadata(metadata) {
|
|
128
|
+
this.globalMetadata = { ...this.globalMetadata, ...metadata };
|
|
129
|
+
}
|
|
130
|
+
mergeGlobalMetadata(event) {
|
|
131
|
+
if (Object.keys(this.globalMetadata).length === 0)
|
|
132
|
+
return event;
|
|
133
|
+
return {
|
|
134
|
+
...event,
|
|
135
|
+
metadata: { ...this.globalMetadata, ...(event.metadata ?? {}) },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async sendWithRetry(events) {
|
|
139
|
+
const maxRetries = this.config.retry?.maxRetries ?? 0;
|
|
140
|
+
const backoffMs = this.config.retry?.backoffMs ?? 100;
|
|
141
|
+
const payload = events.length === 1 ? events[0] : events;
|
|
142
|
+
const body = JSON.stringify(payload);
|
|
143
|
+
const timestamp = Date.now();
|
|
144
|
+
const nonce = (0, security_1.generateNonce)();
|
|
145
|
+
const signature = (0, security_1.hmacSign)(this.config.apiKey || '', timestamp, body, nonce);
|
|
146
|
+
const headers = {
|
|
147
|
+
'Content-Type': 'application/json',
|
|
148
|
+
'X-Vantinel-API-Key': this.config.apiKey || '',
|
|
149
|
+
'X-Vantinel-Signature': signature,
|
|
150
|
+
'X-Vantinel-Timestamp': String(timestamp),
|
|
151
|
+
'X-Vantinel-Nonce': nonce,
|
|
152
|
+
'X-Vantinel-Client': this.config.clientId || '',
|
|
153
|
+
};
|
|
154
|
+
let lastError = null;
|
|
155
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
156
|
+
if (attempt > 0) {
|
|
157
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs * attempt));
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const response = await this.client.post('/v1/events', body, { headers });
|
|
161
|
+
if (Array.isArray(payload)) {
|
|
162
|
+
// For batch sends, use the server's aggregate decision if available
|
|
163
|
+
const data = response.data;
|
|
164
|
+
if (data && typeof data.decision === 'string') {
|
|
165
|
+
return data;
|
|
166
|
+
}
|
|
167
|
+
// If server returns an array of decisions, use the most restrictive one
|
|
168
|
+
if (Array.isArray(data)) {
|
|
169
|
+
const dominated = data.find((d) => d.decision === 'block');
|
|
170
|
+
if (dominated)
|
|
171
|
+
return dominated;
|
|
172
|
+
const approval = data.find((d) => d.decision === 'require_approval');
|
|
173
|
+
if (approval)
|
|
174
|
+
return approval;
|
|
175
|
+
}
|
|
176
|
+
return { decision: 'allow' };
|
|
177
|
+
}
|
|
178
|
+
return response.data;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const status = error.response?.status;
|
|
182
|
+
// Retry on network errors or 5xx
|
|
183
|
+
if (!status || status >= 500) {
|
|
184
|
+
lastError = error;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
// Non-retryable error (4xx etc.)
|
|
188
|
+
console.warn('[Vantinel] Non-retryable error from collector:', error.message);
|
|
189
|
+
if (this.config.failMode === 'closed') {
|
|
190
|
+
return { decision: 'block', message: 'Vantinel Gateway returned non-retryable error and failMode is closed.' };
|
|
191
|
+
}
|
|
192
|
+
return { decision: 'allow' };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
console.warn('[Vantinel] Failed to contact collector after retries:', lastError?.message);
|
|
196
|
+
if (this.config.failMode === 'closed') {
|
|
197
|
+
return { decision: 'block', message: 'Vantinel Gateway is unreachable and failMode is closed.' };
|
|
198
|
+
}
|
|
199
|
+
return { decision: 'allow' };
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Send an event to the collector.
|
|
203
|
+
* Cost is only included if explicitly set on the event.
|
|
204
|
+
*/
|
|
205
|
+
async sendEvent(event) {
|
|
206
|
+
const enriched = this.mergeGlobalMetadata(event);
|
|
207
|
+
if (this.config.dryRun) {
|
|
208
|
+
console.log('[Vantinel DryRun] Event:', enriched);
|
|
209
|
+
return { decision: 'allow' };
|
|
210
|
+
}
|
|
211
|
+
const batchSize = this.config.batchSize ?? 1;
|
|
212
|
+
if (batchSize <= 1) {
|
|
213
|
+
// No batching — send immediately
|
|
214
|
+
return this.sendWithRetry([enriched]);
|
|
215
|
+
}
|
|
216
|
+
// Batching mode — buffer and flush when full
|
|
217
|
+
return new Promise((resolve) => {
|
|
218
|
+
this.batchQueue.push(enriched);
|
|
219
|
+
if (this.batchQueue.length >= batchSize) {
|
|
220
|
+
const batch = this.batchQueue.splice(0, batchSize);
|
|
221
|
+
this.sendWithRetry(batch)
|
|
222
|
+
.then((decision) => resolve(decision))
|
|
223
|
+
.catch(() => resolve(this.config.failMode === 'closed' ? { decision: 'block', message: 'Failed to send batch and failMode is closed.' } : { decision: 'allow' }));
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// Resolve immediately with allow; batch will be sent when full or on flush()
|
|
227
|
+
resolve({ decision: 'allow' });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Wrap a tool function — automatically measures latency and tracks it.
|
|
233
|
+
* This is the recommended way to use Vantinel.
|
|
234
|
+
*
|
|
235
|
+
* Cost is only reported if you explicitly provide it (e.g., from your LLM provider's usage data).
|
|
236
|
+
* Latency is always automatically measured.
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* const result = await client.wrap('search_db', '{"query":"test"}', async () => {
|
|
240
|
+
* return await searchDatabase('test');
|
|
241
|
+
* });
|
|
242
|
+
*/
|
|
243
|
+
async wrap(toolName, toolArgs, fn, options) {
|
|
244
|
+
const { createHash } = await Promise.resolve().then(() => __importStar(require('crypto')));
|
|
245
|
+
const hash = createHash('sha256').update(`${toolName}:${toolArgs}`).digest('hex').slice(0, 32);
|
|
246
|
+
const sessionId = options?.sessionId || `session_${Date.now()}`;
|
|
247
|
+
// Pre-check with the gateway
|
|
248
|
+
const preEvent = {
|
|
249
|
+
event_type: 'tool_call',
|
|
250
|
+
session_id: sessionId,
|
|
251
|
+
agent_id: this.config.agentId,
|
|
252
|
+
tool_name: toolName,
|
|
253
|
+
tool_args_hash: hash,
|
|
254
|
+
timestamp: Date.now(),
|
|
255
|
+
estimated_cost: options?.estimatedCost,
|
|
256
|
+
trace_id: options?.traceId,
|
|
257
|
+
};
|
|
258
|
+
const decision = await this.sendEvent(preEvent);
|
|
259
|
+
if (decision.decision === 'block') {
|
|
260
|
+
throw new Error(`[Vantinel] Tool call blocked: ${toolName} — ${decision.message || 'Policy violation'}`);
|
|
261
|
+
}
|
|
262
|
+
// Execute and time
|
|
263
|
+
const startTime = process.hrtime.bigint();
|
|
264
|
+
try {
|
|
265
|
+
const result = await fn();
|
|
266
|
+
const latencyMs = Number(process.hrtime.bigint() - startTime) / 1000000;
|
|
267
|
+
// Send completion with latency
|
|
268
|
+
const completionEvent = {
|
|
269
|
+
...preEvent,
|
|
270
|
+
latency_ms: latencyMs,
|
|
271
|
+
timestamp: Date.now(),
|
|
272
|
+
};
|
|
273
|
+
await this.sendEvent(completionEvent).catch(() => { });
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
const latencyMs = Number(process.hrtime.bigint() - startTime) / 1000000;
|
|
278
|
+
// Report error with latency
|
|
279
|
+
const errorEvent = {
|
|
280
|
+
...preEvent,
|
|
281
|
+
event_type: 'tool_error',
|
|
282
|
+
latency_ms: latencyMs,
|
|
283
|
+
timestamp: Date.now(),
|
|
284
|
+
metadata: {
|
|
285
|
+
error_message: err instanceof Error ? err.message : String(err),
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
await this.sendEvent(errorEvent).catch(() => { });
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Auto-instrument an OpenAI client.
|
|
294
|
+
* This monkey-patches `chat.completions.create` to automatically intercept calls,
|
|
295
|
+
* measure latency, extract exact token usage from the response, and calculate true cost.
|
|
296
|
+
*
|
|
297
|
+
* @param openaiClient - The instantiated OpenAI client (e.g., `new OpenAI()`)
|
|
298
|
+
* @param options - Optional configuration for the intercepted calls
|
|
299
|
+
* @returns The patched OpenAI client
|
|
300
|
+
*/
|
|
301
|
+
wrapOpenAI(openaiClient, options) {
|
|
302
|
+
if (!openaiClient?.chat?.completions?.create) {
|
|
303
|
+
console.warn('[Vantinel] Provided client does not look like an OpenAI client. wrapOpenAI failed.');
|
|
304
|
+
return openaiClient;
|
|
305
|
+
}
|
|
306
|
+
const originalCreate = openaiClient.chat.completions.create.bind(openaiClient.chat.completions);
|
|
307
|
+
openaiClient.chat.completions.create = async (body, reqOptions) => {
|
|
308
|
+
const toolName = 'openai_chat';
|
|
309
|
+
const argsText = typeof body === 'string' ? body : JSON.stringify(body);
|
|
310
|
+
const sessionId = options?.sessionId || `session_${Date.now()}`;
|
|
311
|
+
const { createHash } = await Promise.resolve().then(() => __importStar(require('crypto')));
|
|
312
|
+
const hash = createHash('sha256').update(`${toolName}:${argsText}`).digest('hex').slice(0, 32);
|
|
313
|
+
const isStream = body?.stream === true || reqOptions?.stream === true;
|
|
314
|
+
// Clone to avoid mutating the caller's object
|
|
315
|
+
const modifiedBody = isStream && !body.stream_options
|
|
316
|
+
? { ...body, stream_options: { include_usage: true } }
|
|
317
|
+
: body;
|
|
318
|
+
const preEvent = {
|
|
319
|
+
event_type: 'tool_call',
|
|
320
|
+
session_id: sessionId,
|
|
321
|
+
agent_id: this.config.agentId,
|
|
322
|
+
tool_name: toolName,
|
|
323
|
+
tool_args_hash: hash,
|
|
324
|
+
timestamp: Date.now(),
|
|
325
|
+
trace_id: options?.traceId,
|
|
326
|
+
};
|
|
327
|
+
const decision = await this.sendEvent(preEvent);
|
|
328
|
+
if (decision.decision === 'block') {
|
|
329
|
+
throw new Error(`[Vantinel] Tool call blocked: ${toolName} — ${decision.message || 'Policy violation'}`);
|
|
330
|
+
}
|
|
331
|
+
const startTime = process.hrtime.bigint();
|
|
332
|
+
try {
|
|
333
|
+
const response = await originalCreate(modifiedBody, reqOptions);
|
|
334
|
+
if (isStream) {
|
|
335
|
+
const self = this;
|
|
336
|
+
async function* wrapper() {
|
|
337
|
+
let finalUsage = null;
|
|
338
|
+
let finalModel = body.model;
|
|
339
|
+
try {
|
|
340
|
+
for await (const chunk of response) {
|
|
341
|
+
if (chunk.usage)
|
|
342
|
+
finalUsage = chunk.usage;
|
|
343
|
+
if (chunk.model)
|
|
344
|
+
finalModel = chunk.model;
|
|
345
|
+
yield chunk;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
const latencyMs = Number(process.hrtime.bigint() - startTime) / 1000000;
|
|
350
|
+
let estimatedCost = undefined;
|
|
351
|
+
if (finalUsage && finalModel) {
|
|
352
|
+
const cachedTokens = finalUsage.prompt_tokens_details?.cached_tokens || 0;
|
|
353
|
+
estimatedCost = estimateCostFromTokens(finalModel, finalUsage.prompt_tokens || 0, finalUsage.completion_tokens || 0, cachedTokens);
|
|
354
|
+
}
|
|
355
|
+
const completionEvent = {
|
|
356
|
+
...preEvent,
|
|
357
|
+
latency_ms: latencyMs,
|
|
358
|
+
estimated_cost: estimatedCost,
|
|
359
|
+
timestamp: Date.now(),
|
|
360
|
+
};
|
|
361
|
+
self.sendEvent(completionEvent).catch(() => { });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return wrapper();
|
|
365
|
+
}
|
|
366
|
+
const latencyMs = Number(process.hrtime.bigint() - startTime) / 1000000;
|
|
367
|
+
// Calculate exact cost from the actual usage reported by OpenAI!
|
|
368
|
+
let estimatedCost = undefined;
|
|
369
|
+
if (response.usage && response.model) {
|
|
370
|
+
const cachedTokens = response.usage.prompt_tokens_details?.cached_tokens || 0;
|
|
371
|
+
estimatedCost = estimateCostFromTokens(response.model, response.usage.prompt_tokens || 0, response.usage.completion_tokens || 0, cachedTokens);
|
|
372
|
+
}
|
|
373
|
+
const completionEvent = {
|
|
374
|
+
...preEvent,
|
|
375
|
+
latency_ms: latencyMs,
|
|
376
|
+
estimated_cost: estimatedCost,
|
|
377
|
+
timestamp: Date.now(),
|
|
378
|
+
};
|
|
379
|
+
await this.sendEvent(completionEvent).catch(() => { });
|
|
380
|
+
return response;
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
const latencyMs = Number(process.hrtime.bigint() - startTime) / 1000000;
|
|
384
|
+
const errorEvent = {
|
|
385
|
+
...preEvent,
|
|
386
|
+
event_type: 'tool_error',
|
|
387
|
+
latency_ms: latencyMs,
|
|
388
|
+
timestamp: Date.now(),
|
|
389
|
+
metadata: {
|
|
390
|
+
error_message: err instanceof Error ? err.message : String(err),
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
await this.sendEvent(errorEvent).catch(() => { });
|
|
394
|
+
throw err;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
return openaiClient;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Gracefully shut down the client: flush pending events and clear timers.
|
|
401
|
+
*/
|
|
402
|
+
async destroy() {
|
|
403
|
+
if (this.flushTimer) {
|
|
404
|
+
clearInterval(this.flushTimer);
|
|
405
|
+
this.flushTimer = null;
|
|
406
|
+
}
|
|
407
|
+
await this.flush();
|
|
408
|
+
}
|
|
409
|
+
async flush() {
|
|
410
|
+
if (this.batchQueue.length === 0)
|
|
411
|
+
return;
|
|
412
|
+
const batch = this.batchQueue.splice(0, this.batchQueue.length);
|
|
413
|
+
try {
|
|
414
|
+
await this.sendWithRetry(batch);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// Errors are already logged inside sendWithRetry
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async ping() {
|
|
421
|
+
const start = Date.now();
|
|
422
|
+
try {
|
|
423
|
+
await this.client.get('/health');
|
|
424
|
+
return { ok: true, latencyMs: Date.now() - start };
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
return { ok: false, latencyMs: Date.now() - start };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
exports.VantinelClient = VantinelClient;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from './client';
|
|
2
|
+
export * from './monitor';
|
|
3
|
+
export { validateCollectorUrl, redactApiKey } from './security';
|
|
4
|
+
export { patchOpenAIAgents, VantinelTracingProcessor } from './integrations/openai-agents';
|
|
5
|
+
export { wrapAnthropic } from './integrations/anthropic';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.wrapAnthropic = exports.VantinelTracingProcessor = exports.patchOpenAIAgents = exports.redactApiKey = exports.validateCollectorUrl = void 0;
|
|
18
|
+
__exportStar(require("./client"), exports);
|
|
19
|
+
__exportStar(require("./monitor"), exports);
|
|
20
|
+
var security_1 = require("./security");
|
|
21
|
+
Object.defineProperty(exports, "validateCollectorUrl", { enumerable: true, get: function () { return security_1.validateCollectorUrl; } });
|
|
22
|
+
Object.defineProperty(exports, "redactApiKey", { enumerable: true, get: function () { return security_1.redactApiKey; } });
|
|
23
|
+
// Framework integrations
|
|
24
|
+
var openai_agents_1 = require("./integrations/openai-agents");
|
|
25
|
+
Object.defineProperty(exports, "patchOpenAIAgents", { enumerable: true, get: function () { return openai_agents_1.patchOpenAIAgents; } });
|
|
26
|
+
Object.defineProperty(exports, "VantinelTracingProcessor", { enumerable: true, get: function () { return openai_agents_1.VantinelTracingProcessor; } });
|
|
27
|
+
var anthropic_1 = require("./integrations/anthropic");
|
|
28
|
+
Object.defineProperty(exports, "wrapAnthropic", { enumerable: true, get: function () { return anthropic_1.wrapAnthropic; } });
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic SDK integration for Vantinel.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import Anthropic from '@anthropic-ai/sdk';
|
|
6
|
+
* import { VantinelClient } from '@vantinelai/node-sdk';
|
|
7
|
+
* import { wrapAnthropic } from '@vantinelai/node-sdk/integrations/anthropic';
|
|
8
|
+
*
|
|
9
|
+
* const client = new VantinelClient({ apiKey: '...', clientId: '...' });
|
|
10
|
+
* const anthropic = wrapAnthropic(client, new Anthropic());
|
|
11
|
+
*
|
|
12
|
+
* const response = await anthropic.messages.create({
|
|
13
|
+
* model: 'claude-sonnet-4-6',
|
|
14
|
+
* max_tokens: 1024,
|
|
15
|
+
* messages: [{ role: 'user', content: 'Hello' }],
|
|
16
|
+
* });
|
|
17
|
+
*/
|
|
18
|
+
import type { VantinelClient } from '../client';
|
|
19
|
+
interface AnthropicUsage {
|
|
20
|
+
input_tokens?: number;
|
|
21
|
+
output_tokens?: number;
|
|
22
|
+
cache_creation_input_tokens?: number;
|
|
23
|
+
cache_read_input_tokens?: number;
|
|
24
|
+
}
|
|
25
|
+
interface AnthropicMessage {
|
|
26
|
+
role: string;
|
|
27
|
+
content: unknown;
|
|
28
|
+
}
|
|
29
|
+
interface AnthropicCreateParams {
|
|
30
|
+
model: string;
|
|
31
|
+
messages: AnthropicMessage[];
|
|
32
|
+
stream?: boolean;
|
|
33
|
+
max_tokens?: number;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
interface AnthropicResponse {
|
|
37
|
+
usage?: AnthropicUsage;
|
|
38
|
+
stop_reason?: string;
|
|
39
|
+
content?: Array<{
|
|
40
|
+
type: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
}>;
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
interface AnthropicClient {
|
|
46
|
+
messages: {
|
|
47
|
+
create(params: AnthropicCreateParams): Promise<AnthropicResponse>;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
};
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Wrap an Anthropic client to auto-monitor all messages.create() calls.
|
|
54
|
+
*
|
|
55
|
+
* @param vantinelClient - VantinelClient instance
|
|
56
|
+
* @param anthropicClient - anthropic.Anthropic() instance
|
|
57
|
+
* @param options - Optional session/agent configuration
|
|
58
|
+
* @returns The patched Anthropic client
|
|
59
|
+
*/
|
|
60
|
+
export declare function wrapAnthropic(vantinelClient: VantinelClient, anthropicClient: AnthropicClient, options?: {
|
|
61
|
+
sessionId?: string;
|
|
62
|
+
agentId?: string;
|
|
63
|
+
}): AnthropicClient;
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Anthropic SDK integration for Vantinel.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import Anthropic from '@anthropic-ai/sdk';
|
|
7
|
+
* import { VantinelClient } from '@vantinelai/node-sdk';
|
|
8
|
+
* import { wrapAnthropic } from '@vantinelai/node-sdk/integrations/anthropic';
|
|
9
|
+
*
|
|
10
|
+
* const client = new VantinelClient({ apiKey: '...', clientId: '...' });
|
|
11
|
+
* const anthropic = wrapAnthropic(client, new Anthropic());
|
|
12
|
+
*
|
|
13
|
+
* const response = await anthropic.messages.create({
|
|
14
|
+
* model: 'claude-sonnet-4-6',
|
|
15
|
+
* max_tokens: 1024,
|
|
16
|
+
* messages: [{ role: 'user', content: 'Hello' }],
|
|
17
|
+
* });
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.wrapAnthropic = wrapAnthropic;
|
|
54
|
+
const crypto = __importStar(require("crypto"));
|
|
55
|
+
const client_1 = require("../client");
|
|
56
|
+
function buildArgsHash(model, messages) {
|
|
57
|
+
return crypto
|
|
58
|
+
.createHash('sha256')
|
|
59
|
+
.update(JSON.stringify({ model, messages_count: messages.length, first_msg: messages[0]?.content }))
|
|
60
|
+
.digest('hex')
|
|
61
|
+
.slice(0, 32);
|
|
62
|
+
}
|
|
63
|
+
function extractCost(model, usage) {
|
|
64
|
+
if (!usage)
|
|
65
|
+
return undefined;
|
|
66
|
+
const inputTokens = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
67
|
+
const outputTokens = usage.output_tokens ?? 0;
|
|
68
|
+
const cachedTokens = usage.cache_read_input_tokens ?? 0;
|
|
69
|
+
return (0, client_1.estimateCostFromTokens)(model, inputTokens + cachedTokens, outputTokens, cachedTokens);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Wrap an Anthropic client to auto-monitor all messages.create() calls.
|
|
73
|
+
*
|
|
74
|
+
* @param vantinelClient - VantinelClient instance
|
|
75
|
+
* @param anthropicClient - anthropic.Anthropic() instance
|
|
76
|
+
* @param options - Optional session/agent configuration
|
|
77
|
+
* @returns The patched Anthropic client
|
|
78
|
+
*/
|
|
79
|
+
function wrapAnthropic(vantinelClient, anthropicClient, options) {
|
|
80
|
+
const originalCreate = anthropicClient.messages.create.bind(anthropicClient.messages);
|
|
81
|
+
const sessionId = options?.sessionId ?? `anthropic_${Date.now()}`;
|
|
82
|
+
anthropicClient.messages.create = async (params) => {
|
|
83
|
+
const { model, messages } = params;
|
|
84
|
+
const toolName = `anthropic_messages_${model}`;
|
|
85
|
+
const argsHash = buildArgsHash(model, messages);
|
|
86
|
+
// Pre-call: send event to get decision
|
|
87
|
+
let decision;
|
|
88
|
+
try {
|
|
89
|
+
const resp = await vantinelClient.sendEvent({
|
|
90
|
+
event_type: 'tool_call',
|
|
91
|
+
tool_name: toolName,
|
|
92
|
+
tool_args_hash: argsHash,
|
|
93
|
+
session_id: sessionId,
|
|
94
|
+
agent_id: options?.agentId,
|
|
95
|
+
timestamp: Date.now(),
|
|
96
|
+
metadata: { model, messages_count: messages.length, framework: 'anthropic' },
|
|
97
|
+
});
|
|
98
|
+
decision = resp?.decision;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Fail open: don't block the call if Vantinel is unavailable
|
|
102
|
+
}
|
|
103
|
+
if (decision === 'block') {
|
|
104
|
+
throw new Error(`Vantinel blocked Anthropic call: ${toolName}`);
|
|
105
|
+
}
|
|
106
|
+
const startTime = Date.now();
|
|
107
|
+
const result = await originalCreate(params);
|
|
108
|
+
const latencyMs = Date.now() - startTime;
|
|
109
|
+
const usage = result.usage;
|
|
110
|
+
const cost = extractCost(model, usage);
|
|
111
|
+
const toolUses = result.content
|
|
112
|
+
?.filter((b) => b.type === 'tool_use')
|
|
113
|
+
.map((b) => b.name)
|
|
114
|
+
.filter(Boolean) ?? [];
|
|
115
|
+
// Post-call telemetry (fire-and-forget)
|
|
116
|
+
void vantinelClient.sendEvent({
|
|
117
|
+
event_type: 'tool_result',
|
|
118
|
+
tool_name: toolName,
|
|
119
|
+
tool_args_hash: argsHash,
|
|
120
|
+
session_id: sessionId,
|
|
121
|
+
agent_id: options?.agentId,
|
|
122
|
+
timestamp: Date.now(),
|
|
123
|
+
estimated_cost: cost,
|
|
124
|
+
latency_ms: latencyMs,
|
|
125
|
+
metadata: {
|
|
126
|
+
model,
|
|
127
|
+
messages_count: messages.length,
|
|
128
|
+
framework: 'anthropic',
|
|
129
|
+
stop_reason: result.stop_reason,
|
|
130
|
+
tool_uses: toolUses,
|
|
131
|
+
input_tokens: usage?.input_tokens,
|
|
132
|
+
output_tokens: usage?.output_tokens,
|
|
133
|
+
},
|
|
134
|
+
}).catch(() => { });
|
|
135
|
+
return result;
|
|
136
|
+
};
|
|
137
|
+
return anthropicClient;
|
|
138
|
+
}
|