@sentrial/sdk 0.4.0 → 0.4.2
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/dist/index.cjs +1345 -641
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +343 -142
- package/dist/index.d.ts +343 -142
- package/dist/index.js +1341 -640
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
package/dist/index.cjs
CHANGED
|
@@ -15,12 +15,13 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
15
|
}
|
|
16
16
|
return to;
|
|
17
17
|
};
|
|
18
|
-
var __toCommonJS = (
|
|
18
|
+
var __toCommonJS = (mod2) => __copyProps(__defProp({}, "__esModule", { value: true }), mod2);
|
|
19
19
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ApiError: () => ApiError,
|
|
24
|
+
EventBatcher: () => EventBatcher,
|
|
24
25
|
EventType: () => EventType,
|
|
25
26
|
Experiment: () => Experiment,
|
|
26
27
|
ExperimentRunTracker: () => ExperimentRunTracker,
|
|
@@ -40,6 +41,7 @@ __export(index_exports, {
|
|
|
40
41
|
clearSessionContext: () => clearSessionContext,
|
|
41
42
|
configure: () => configure,
|
|
42
43
|
configureVercel: () => configureVercel,
|
|
44
|
+
createContextVar: () => createContextVar,
|
|
43
45
|
getCurrentInteraction: () => getCurrentInteraction,
|
|
44
46
|
getCurrentSessionId: () => getCurrentSessionId,
|
|
45
47
|
getExperimentContext: () => getExperimentContext,
|
|
@@ -62,6 +64,7 @@ __export(index_exports, {
|
|
|
62
64
|
withTool: () => withTool,
|
|
63
65
|
wrapAISDK: () => wrapAISDK,
|
|
64
66
|
wrapAnthropic: () => wrapAnthropic,
|
|
67
|
+
wrapClaudeAgent: () => wrapClaudeAgent,
|
|
65
68
|
wrapGoogle: () => wrapGoogle,
|
|
66
69
|
wrapLLM: () => wrapLLM,
|
|
67
70
|
wrapOpenAI: () => wrapOpenAI
|
|
@@ -138,7 +141,22 @@ var ValidationError = class extends SentrialError {
|
|
|
138
141
|
};
|
|
139
142
|
|
|
140
143
|
// src/redact.ts
|
|
141
|
-
var
|
|
144
|
+
var _createHash;
|
|
145
|
+
try {
|
|
146
|
+
const mod = eval("require")("crypto");
|
|
147
|
+
if (mod?.createHash) {
|
|
148
|
+
_createHash = mod.createHash;
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
function getCreateHash() {
|
|
153
|
+
if (!_createHash) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
'Sentrial PII hash mode requires Node.js crypto module. Use mode "label" or "remove" in browser/edge environments.'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return _createHash;
|
|
159
|
+
}
|
|
142
160
|
var DEFAULT_FIELDS = [
|
|
143
161
|
"userInput",
|
|
144
162
|
"assistantOutput",
|
|
@@ -165,7 +183,7 @@ var BUILTIN_PATTERNS = {
|
|
|
165
183
|
ipAddresses: { pattern: IP_ADDRESS_PATTERN, label: "IP_ADDRESS" }
|
|
166
184
|
};
|
|
167
185
|
function hashValue(value) {
|
|
168
|
-
return (
|
|
186
|
+
return getCreateHash()("sha256").update(value).digest("hex").slice(0, 6);
|
|
169
187
|
}
|
|
170
188
|
function replaceMatch(match, label, mode) {
|
|
171
189
|
switch (mode) {
|
|
@@ -227,6 +245,59 @@ function redactPayload(payload, config) {
|
|
|
227
245
|
return result;
|
|
228
246
|
}
|
|
229
247
|
|
|
248
|
+
// src/async-context.ts
|
|
249
|
+
var _AsyncLocalStorage = null;
|
|
250
|
+
try {
|
|
251
|
+
const mod = eval("require")("node:async_hooks");
|
|
252
|
+
if (mod?.AsyncLocalStorage) {
|
|
253
|
+
_AsyncLocalStorage = mod.AsyncLocalStorage;
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
var NodeContextVar = class {
|
|
258
|
+
_storage;
|
|
259
|
+
_defaultValue;
|
|
260
|
+
constructor(defaultValue) {
|
|
261
|
+
this._storage = new _AsyncLocalStorage();
|
|
262
|
+
this._defaultValue = defaultValue;
|
|
263
|
+
}
|
|
264
|
+
get() {
|
|
265
|
+
const store = this._storage.getStore();
|
|
266
|
+
return store !== void 0 ? store : this._defaultValue;
|
|
267
|
+
}
|
|
268
|
+
set(value) {
|
|
269
|
+
const previous = this.get();
|
|
270
|
+
this._storage.enterWith(value);
|
|
271
|
+
return { _previous: previous };
|
|
272
|
+
}
|
|
273
|
+
reset(token) {
|
|
274
|
+
this._storage.enterWith(token._previous);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
var SimpleContextVar = class {
|
|
278
|
+
_value;
|
|
279
|
+
constructor(defaultValue) {
|
|
280
|
+
this._value = defaultValue;
|
|
281
|
+
}
|
|
282
|
+
get() {
|
|
283
|
+
return this._value;
|
|
284
|
+
}
|
|
285
|
+
set(value) {
|
|
286
|
+
const previous = this._value;
|
|
287
|
+
this._value = value;
|
|
288
|
+
return { _previous: previous };
|
|
289
|
+
}
|
|
290
|
+
reset(token) {
|
|
291
|
+
this._value = token._previous;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
function createContextVar(defaultValue) {
|
|
295
|
+
if (_AsyncLocalStorage) {
|
|
296
|
+
return new NodeContextVar(defaultValue);
|
|
297
|
+
}
|
|
298
|
+
return new SimpleContextVar(defaultValue);
|
|
299
|
+
}
|
|
300
|
+
|
|
230
301
|
// src/cost.ts
|
|
231
302
|
var OPENAI_PRICING = {
|
|
232
303
|
"gpt-5.2": { input: 5, output: 15 },
|
|
@@ -278,7 +349,8 @@ var GOOGLE_PRICING = {
|
|
|
278
349
|
"gemini-1.0-pro": { input: 0.5, output: 1.5 }
|
|
279
350
|
};
|
|
280
351
|
function findModelKey(model, pricing) {
|
|
281
|
-
|
|
352
|
+
const keys = Object.keys(pricing).sort((a, b) => b.length - a.length);
|
|
353
|
+
for (const key of keys) {
|
|
282
354
|
if (model.startsWith(key)) {
|
|
283
355
|
return key;
|
|
284
356
|
}
|
|
@@ -306,179 +378,783 @@ function calculateGoogleCost(params) {
|
|
|
306
378
|
return calculateCost(inputTokens, outputTokens, GOOGLE_PRICING[modelKey]);
|
|
307
379
|
}
|
|
308
380
|
|
|
309
|
-
// src/
|
|
310
|
-
var
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
// src/client.ts
|
|
319
|
-
var DEFAULT_API_URL = "https://api.sentrial.com";
|
|
320
|
-
var MAX_RETRIES = 3;
|
|
321
|
-
var INITIAL_BACKOFF_MS = 500;
|
|
322
|
-
var MAX_BACKOFF_MS = 8e3;
|
|
323
|
-
var BACKOFF_MULTIPLIER = 2;
|
|
324
|
-
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
325
|
-
var REQUEST_TIMEOUT_MS = 1e4;
|
|
326
|
-
var SentrialClient = class {
|
|
327
|
-
apiUrl;
|
|
328
|
-
apiKey;
|
|
329
|
-
failSilently;
|
|
330
|
-
piiConfig;
|
|
331
|
-
piiConfigNeedsHydration = false;
|
|
332
|
-
piiHydrationPromise;
|
|
333
|
-
currentState = {};
|
|
334
|
-
constructor(config = {}) {
|
|
335
|
-
this.apiUrl = (config.apiUrl ?? (typeof process !== "undefined" ? process.env?.SENTRIAL_API_URL : void 0) ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
336
|
-
this.apiKey = config.apiKey ?? (typeof process !== "undefined" ? process.env?.SENTRIAL_API_KEY : void 0);
|
|
337
|
-
this.failSilently = config.failSilently ?? true;
|
|
338
|
-
if (config.pii === true) {
|
|
339
|
-
this.piiConfig = { enabled: true };
|
|
340
|
-
this.piiConfigNeedsHydration = true;
|
|
341
|
-
} else if (config.pii && typeof config.pii === "object") {
|
|
342
|
-
this.piiConfig = config.pii;
|
|
343
|
-
this.piiConfigNeedsHydration = false;
|
|
344
|
-
}
|
|
381
|
+
// src/wrappers.ts
|
|
382
|
+
var _currentSessionId = createContextVar(null);
|
|
383
|
+
var _currentClient = createContextVar(null);
|
|
384
|
+
var _defaultClient = null;
|
|
385
|
+
function setSessionContext(sessionId, client) {
|
|
386
|
+
_currentSessionId.set(sessionId);
|
|
387
|
+
if (client) {
|
|
388
|
+
_currentClient.set(client);
|
|
345
389
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
if (response.ok) {
|
|
377
|
-
const data = await response.json();
|
|
378
|
-
if (data.config) {
|
|
379
|
-
this.piiConfig = {
|
|
380
|
-
enabled: data.config.enabled,
|
|
381
|
-
mode: data.config.mode,
|
|
382
|
-
fields: data.config.fields,
|
|
383
|
-
builtinPatterns: data.config.builtinPatterns,
|
|
384
|
-
customPatterns: (data.config.customPatterns || []).map(
|
|
385
|
-
(cp) => ({
|
|
386
|
-
pattern: new RegExp(cp.pattern, "g"),
|
|
387
|
-
label: cp.label
|
|
388
|
-
})
|
|
389
|
-
),
|
|
390
|
-
enhancedDetection: data.config.enhancedDetection
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
} catch {
|
|
395
|
-
}
|
|
396
|
-
this.piiConfigNeedsHydration = false;
|
|
397
|
-
})();
|
|
398
|
-
await this.piiHydrationPromise;
|
|
390
|
+
}
|
|
391
|
+
function clearSessionContext() {
|
|
392
|
+
_currentSessionId.set(null);
|
|
393
|
+
_currentClient.set(null);
|
|
394
|
+
}
|
|
395
|
+
function getSessionContext() {
|
|
396
|
+
return _currentSessionId.get();
|
|
397
|
+
}
|
|
398
|
+
function setDefaultClient(client) {
|
|
399
|
+
_defaultClient = client;
|
|
400
|
+
}
|
|
401
|
+
function _setSessionContextWithTokens(sessionId, client) {
|
|
402
|
+
const _sessionToken = _currentSessionId.set(sessionId);
|
|
403
|
+
const _clientToken = client ? _currentClient.set(client) : _currentClient.set(_currentClient.get());
|
|
404
|
+
return { _sessionToken, _clientToken };
|
|
405
|
+
}
|
|
406
|
+
function _restoreSessionContext(tokens) {
|
|
407
|
+
_currentSessionId.reset(tokens._sessionToken);
|
|
408
|
+
_currentClient.reset(tokens._clientToken);
|
|
409
|
+
}
|
|
410
|
+
function getTrackingClient() {
|
|
411
|
+
return _currentClient.get() ?? _defaultClient;
|
|
412
|
+
}
|
|
413
|
+
function wrapOpenAI(client, options = {}) {
|
|
414
|
+
const { trackWithoutSession = false } = options;
|
|
415
|
+
const chat = client.chat;
|
|
416
|
+
if (!chat?.completions?.create) {
|
|
417
|
+
console.warn("Sentrial: OpenAI client does not have chat.completions.create");
|
|
418
|
+
return client;
|
|
399
419
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (
|
|
408
|
-
|
|
420
|
+
const originalCreate = chat.completions.create.bind(chat.completions);
|
|
421
|
+
chat.completions.create = async function(...args) {
|
|
422
|
+
const startTime = Date.now();
|
|
423
|
+
const params = args[0] ?? {};
|
|
424
|
+
const messages = params.messages ?? [];
|
|
425
|
+
const model = params.model ?? "unknown";
|
|
426
|
+
const isStreaming = params.stream === true;
|
|
427
|
+
if (isStreaming && !params.stream_options?.include_usage) {
|
|
428
|
+
args[0] = { ...params, stream_options: { ...params.stream_options, include_usage: true } };
|
|
409
429
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const headers = {
|
|
415
|
-
"Content-Type": "application/json"
|
|
416
|
-
};
|
|
417
|
-
if (this.apiKey) {
|
|
418
|
-
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
419
|
-
}
|
|
420
|
-
const finalBody = this.piiConfig && body && typeof body === "object" ? redactPayload(body, this.piiConfig) : body;
|
|
421
|
-
const controller = new AbortController();
|
|
422
|
-
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
423
|
-
let response;
|
|
424
|
-
try {
|
|
425
|
-
response = await fetch(url, {
|
|
426
|
-
method,
|
|
427
|
-
headers,
|
|
428
|
-
body: finalBody ? JSON.stringify(finalBody) : void 0,
|
|
429
|
-
signal: controller.signal
|
|
430
|
-
});
|
|
431
|
-
} finally {
|
|
432
|
-
clearTimeout(timeoutId);
|
|
433
|
-
}
|
|
434
|
-
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < MAX_RETRIES) {
|
|
435
|
-
await this.sleep(backoff);
|
|
436
|
-
backoff = Math.min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
if (!response.ok) {
|
|
440
|
-
const errorBody = await response.text();
|
|
441
|
-
let errorData = {};
|
|
442
|
-
try {
|
|
443
|
-
errorData = JSON.parse(errorBody);
|
|
444
|
-
} catch {
|
|
445
|
-
}
|
|
446
|
-
const error = new ApiError(
|
|
447
|
-
errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
448
|
-
response.status,
|
|
449
|
-
errorData.error?.code
|
|
450
|
-
);
|
|
451
|
-
if (this.failSilently) {
|
|
452
|
-
console.warn(`Sentrial: Request failed (${method} ${url}):`, error.message);
|
|
453
|
-
return null;
|
|
454
|
-
}
|
|
455
|
-
throw error;
|
|
456
|
-
}
|
|
457
|
-
return await response.json();
|
|
458
|
-
} catch (error) {
|
|
459
|
-
if (error instanceof ApiError) {
|
|
460
|
-
throw error;
|
|
461
|
-
}
|
|
462
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
463
|
-
if (attempt < MAX_RETRIES) {
|
|
464
|
-
await this.sleep(backoff);
|
|
465
|
-
backoff = Math.min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
430
|
+
try {
|
|
431
|
+
const response = await originalCreate(...args);
|
|
432
|
+
if (isStreaming) {
|
|
433
|
+
return wrapOpenAIStream(response, { startTime, messages, model, trackWithoutSession });
|
|
468
434
|
}
|
|
435
|
+
const durationMs = Date.now() - startTime;
|
|
436
|
+
const promptTokens = response.usage?.prompt_tokens ?? 0;
|
|
437
|
+
const completionTokens = response.usage?.completion_tokens ?? 0;
|
|
438
|
+
const totalTokens = response.usage?.total_tokens ?? 0;
|
|
439
|
+
let outputContent = "";
|
|
440
|
+
if (response.choices?.[0]?.message?.content) {
|
|
441
|
+
outputContent = response.choices[0].message.content;
|
|
442
|
+
}
|
|
443
|
+
const cost = calculateOpenAICost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
|
|
444
|
+
trackLLMCall({
|
|
445
|
+
provider: "openai",
|
|
446
|
+
model,
|
|
447
|
+
messages,
|
|
448
|
+
output: outputContent,
|
|
449
|
+
promptTokens,
|
|
450
|
+
completionTokens,
|
|
451
|
+
totalTokens,
|
|
452
|
+
cost,
|
|
453
|
+
durationMs,
|
|
454
|
+
trackWithoutSession
|
|
455
|
+
});
|
|
456
|
+
return response;
|
|
457
|
+
} catch (error) {
|
|
458
|
+
const durationMs = Date.now() - startTime;
|
|
459
|
+
trackLLMError({
|
|
460
|
+
provider: "openai",
|
|
461
|
+
model,
|
|
462
|
+
messages,
|
|
463
|
+
error,
|
|
464
|
+
durationMs,
|
|
465
|
+
trackWithoutSession
|
|
466
|
+
});
|
|
467
|
+
throw error;
|
|
469
468
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
469
|
+
};
|
|
470
|
+
return client;
|
|
471
|
+
}
|
|
472
|
+
function wrapAnthropic(client, options = {}) {
|
|
473
|
+
const { trackWithoutSession = false } = options;
|
|
474
|
+
const messages = client.messages;
|
|
475
|
+
if (!messages?.create) {
|
|
476
|
+
console.warn("Sentrial: Anthropic client does not have messages.create");
|
|
477
|
+
return client;
|
|
479
478
|
}
|
|
480
|
-
|
|
481
|
-
|
|
479
|
+
const originalCreate = messages.create.bind(messages);
|
|
480
|
+
messages.create = async function(...args) {
|
|
481
|
+
const startTime = Date.now();
|
|
482
|
+
const params = args[0] ?? {};
|
|
483
|
+
const inputMessages = params.messages ?? [];
|
|
484
|
+
const model = params.model ?? "unknown";
|
|
485
|
+
const system = params.system ?? "";
|
|
486
|
+
const isStreaming = params.stream === true;
|
|
487
|
+
try {
|
|
488
|
+
const response = await originalCreate(...args);
|
|
489
|
+
if (isStreaming) {
|
|
490
|
+
return wrapAnthropicStream(response, {
|
|
491
|
+
startTime,
|
|
492
|
+
messages: inputMessages,
|
|
493
|
+
model,
|
|
494
|
+
system,
|
|
495
|
+
trackWithoutSession
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
const durationMs = Date.now() - startTime;
|
|
499
|
+
const promptTokens = response.usage?.input_tokens ?? 0;
|
|
500
|
+
const completionTokens = response.usage?.output_tokens ?? 0;
|
|
501
|
+
const totalTokens = promptTokens + completionTokens;
|
|
502
|
+
let outputContent = "";
|
|
503
|
+
if (response.content) {
|
|
504
|
+
for (const block of response.content) {
|
|
505
|
+
if (block.type === "text") {
|
|
506
|
+
outputContent += block.text;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const cost = calculateAnthropicCost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
|
|
511
|
+
const fullMessages = system ? [{ role: "system", content: system }, ...inputMessages] : inputMessages;
|
|
512
|
+
trackLLMCall({
|
|
513
|
+
provider: "anthropic",
|
|
514
|
+
model,
|
|
515
|
+
messages: fullMessages,
|
|
516
|
+
output: outputContent,
|
|
517
|
+
promptTokens,
|
|
518
|
+
completionTokens,
|
|
519
|
+
totalTokens,
|
|
520
|
+
cost,
|
|
521
|
+
durationMs,
|
|
522
|
+
trackWithoutSession
|
|
523
|
+
});
|
|
524
|
+
return response;
|
|
525
|
+
} catch (error) {
|
|
526
|
+
const durationMs = Date.now() - startTime;
|
|
527
|
+
trackLLMError({
|
|
528
|
+
provider: "anthropic",
|
|
529
|
+
model,
|
|
530
|
+
messages: inputMessages,
|
|
531
|
+
error,
|
|
532
|
+
durationMs,
|
|
533
|
+
trackWithoutSession
|
|
534
|
+
});
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
return client;
|
|
539
|
+
}
|
|
540
|
+
function wrapGoogle(model, options = {}) {
|
|
541
|
+
const { trackWithoutSession = false } = options;
|
|
542
|
+
const originalGenerate = model.generateContent;
|
|
543
|
+
if (!originalGenerate) {
|
|
544
|
+
console.warn("Sentrial: Google model does not have generateContent");
|
|
545
|
+
return model;
|
|
546
|
+
}
|
|
547
|
+
model.generateContent = async function(...args) {
|
|
548
|
+
const startTime = Date.now();
|
|
549
|
+
const contents = args[0];
|
|
550
|
+
const modelName = model.model ?? "gemini-unknown";
|
|
551
|
+
const messages = googleContentsToMessages(contents);
|
|
552
|
+
try {
|
|
553
|
+
const response = await originalGenerate.apply(model, args);
|
|
554
|
+
const durationMs = Date.now() - startTime;
|
|
555
|
+
let promptTokens = 0;
|
|
556
|
+
let completionTokens = 0;
|
|
557
|
+
const usageMeta = response.response?.usageMetadata ?? response.usageMetadata;
|
|
558
|
+
if (usageMeta) {
|
|
559
|
+
promptTokens = usageMeta.promptTokenCount ?? 0;
|
|
560
|
+
completionTokens = usageMeta.candidatesTokenCount ?? 0;
|
|
561
|
+
}
|
|
562
|
+
const totalTokens = promptTokens + completionTokens;
|
|
563
|
+
let outputContent = "";
|
|
564
|
+
try {
|
|
565
|
+
outputContent = response.response?.text?.() ?? response.text?.() ?? "";
|
|
566
|
+
} catch {
|
|
567
|
+
}
|
|
568
|
+
const cost = calculateGoogleCost({ model: modelName, inputTokens: promptTokens, outputTokens: completionTokens });
|
|
569
|
+
trackLLMCall({
|
|
570
|
+
provider: "google",
|
|
571
|
+
model: modelName,
|
|
572
|
+
messages,
|
|
573
|
+
output: outputContent,
|
|
574
|
+
promptTokens,
|
|
575
|
+
completionTokens,
|
|
576
|
+
totalTokens,
|
|
577
|
+
cost,
|
|
578
|
+
durationMs,
|
|
579
|
+
trackWithoutSession
|
|
580
|
+
});
|
|
581
|
+
return response;
|
|
582
|
+
} catch (error) {
|
|
583
|
+
const durationMs = Date.now() - startTime;
|
|
584
|
+
trackLLMError({
|
|
585
|
+
provider: "google",
|
|
586
|
+
model: modelName,
|
|
587
|
+
messages,
|
|
588
|
+
error,
|
|
589
|
+
durationMs,
|
|
590
|
+
trackWithoutSession
|
|
591
|
+
});
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
return model;
|
|
596
|
+
}
|
|
597
|
+
function googleContentsToMessages(contents) {
|
|
598
|
+
if (typeof contents === "string") {
|
|
599
|
+
return [{ role: "user", content: contents }];
|
|
600
|
+
}
|
|
601
|
+
if (Array.isArray(contents)) {
|
|
602
|
+
return contents.map((item) => {
|
|
603
|
+
if (typeof item === "string") {
|
|
604
|
+
return { role: "user", content: item };
|
|
605
|
+
}
|
|
606
|
+
if (item && typeof item === "object") {
|
|
607
|
+
return { role: item.role ?? "user", content: String(item.content ?? item) };
|
|
608
|
+
}
|
|
609
|
+
return { role: "user", content: String(item) };
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
return [{ role: "user", content: String(contents) }];
|
|
613
|
+
}
|
|
614
|
+
function wrapLLM(client, provider) {
|
|
615
|
+
if (provider === "openai" || client.chat?.completions?.create) {
|
|
616
|
+
return wrapOpenAI(client);
|
|
617
|
+
}
|
|
618
|
+
if (provider === "anthropic" || client.messages?.create) {
|
|
619
|
+
return wrapAnthropic(client);
|
|
620
|
+
}
|
|
621
|
+
if (provider === "google" || client.generateContent) {
|
|
622
|
+
return wrapGoogle(client);
|
|
623
|
+
}
|
|
624
|
+
console.warn("Sentrial: Unknown LLM client type. No auto-tracking applied.");
|
|
625
|
+
return client;
|
|
626
|
+
}
|
|
627
|
+
function wrapOpenAIStream(stream, ctx) {
|
|
628
|
+
let fullContent = "";
|
|
629
|
+
let usage = null;
|
|
630
|
+
let tracked = false;
|
|
631
|
+
const originalIterator = stream[Symbol.asyncIterator]?.bind(stream);
|
|
632
|
+
if (!originalIterator) return stream;
|
|
633
|
+
const trackResult = () => {
|
|
634
|
+
if (tracked) return;
|
|
635
|
+
tracked = true;
|
|
636
|
+
const durationMs = Date.now() - ctx.startTime;
|
|
637
|
+
const promptTokens = usage?.prompt_tokens ?? 0;
|
|
638
|
+
const completionTokens = usage?.completion_tokens ?? 0;
|
|
639
|
+
const totalTokens = usage?.total_tokens ?? promptTokens + completionTokens;
|
|
640
|
+
const cost = calculateOpenAICost({ model: ctx.model, inputTokens: promptTokens, outputTokens: completionTokens });
|
|
641
|
+
trackLLMCall({
|
|
642
|
+
provider: "openai",
|
|
643
|
+
model: ctx.model,
|
|
644
|
+
messages: ctx.messages,
|
|
645
|
+
output: fullContent,
|
|
646
|
+
promptTokens,
|
|
647
|
+
completionTokens,
|
|
648
|
+
totalTokens,
|
|
649
|
+
cost,
|
|
650
|
+
durationMs,
|
|
651
|
+
trackWithoutSession: ctx.trackWithoutSession
|
|
652
|
+
});
|
|
653
|
+
};
|
|
654
|
+
return new Proxy(stream, {
|
|
655
|
+
get(target, prop, receiver) {
|
|
656
|
+
if (prop === Symbol.asyncIterator) {
|
|
657
|
+
return function() {
|
|
658
|
+
const iter = originalIterator();
|
|
659
|
+
return {
|
|
660
|
+
async next() {
|
|
661
|
+
const result = await iter.next();
|
|
662
|
+
if (!result.done) {
|
|
663
|
+
const chunk = result.value;
|
|
664
|
+
const delta = chunk.choices?.[0]?.delta?.content;
|
|
665
|
+
if (delta) fullContent += delta;
|
|
666
|
+
if (chunk.usage) usage = chunk.usage;
|
|
667
|
+
} else {
|
|
668
|
+
trackResult();
|
|
669
|
+
}
|
|
670
|
+
return result;
|
|
671
|
+
},
|
|
672
|
+
async return(value) {
|
|
673
|
+
trackResult();
|
|
674
|
+
return iter.return?.(value) ?? { done: true, value: void 0 };
|
|
675
|
+
},
|
|
676
|
+
async throw(error) {
|
|
677
|
+
return iter.throw?.(error) ?? { done: true, value: void 0 };
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
return Reflect.get(target, prop, receiver);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
function wrapAnthropicStream(stream, ctx) {
|
|
687
|
+
let fullContent = "";
|
|
688
|
+
let inputTokens = 0;
|
|
689
|
+
let outputTokens = 0;
|
|
690
|
+
let tracked = false;
|
|
691
|
+
const originalIterator = stream[Symbol.asyncIterator]?.bind(stream);
|
|
692
|
+
if (!originalIterator) return stream;
|
|
693
|
+
const trackResult = () => {
|
|
694
|
+
if (tracked) return;
|
|
695
|
+
tracked = true;
|
|
696
|
+
const durationMs = Date.now() - ctx.startTime;
|
|
697
|
+
const totalTokens = inputTokens + outputTokens;
|
|
698
|
+
const cost = calculateAnthropicCost({ model: ctx.model, inputTokens, outputTokens });
|
|
699
|
+
const fullMessages = ctx.system ? [{ role: "system", content: ctx.system }, ...ctx.messages] : ctx.messages;
|
|
700
|
+
trackLLMCall({
|
|
701
|
+
provider: "anthropic",
|
|
702
|
+
model: ctx.model,
|
|
703
|
+
messages: fullMessages,
|
|
704
|
+
output: fullContent,
|
|
705
|
+
promptTokens: inputTokens,
|
|
706
|
+
completionTokens: outputTokens,
|
|
707
|
+
totalTokens,
|
|
708
|
+
cost,
|
|
709
|
+
durationMs,
|
|
710
|
+
trackWithoutSession: ctx.trackWithoutSession
|
|
711
|
+
});
|
|
712
|
+
};
|
|
713
|
+
return new Proxy(stream, {
|
|
714
|
+
get(target, prop, receiver) {
|
|
715
|
+
if (prop === Symbol.asyncIterator) {
|
|
716
|
+
return function() {
|
|
717
|
+
const iter = originalIterator();
|
|
718
|
+
return {
|
|
719
|
+
async next() {
|
|
720
|
+
const result = await iter.next();
|
|
721
|
+
if (!result.done) {
|
|
722
|
+
const event = result.value;
|
|
723
|
+
if (event.type === "content_block_delta" && event.delta?.text) {
|
|
724
|
+
fullContent += event.delta.text;
|
|
725
|
+
}
|
|
726
|
+
if (event.type === "message_start" && event.message?.usage) {
|
|
727
|
+
inputTokens = event.message.usage.input_tokens ?? 0;
|
|
728
|
+
}
|
|
729
|
+
if (event.type === "message_delta" && event.usage) {
|
|
730
|
+
outputTokens = event.usage.output_tokens ?? 0;
|
|
731
|
+
}
|
|
732
|
+
} else {
|
|
733
|
+
trackResult();
|
|
734
|
+
}
|
|
735
|
+
return result;
|
|
736
|
+
},
|
|
737
|
+
async return(value) {
|
|
738
|
+
trackResult();
|
|
739
|
+
return iter.return?.(value) ?? { done: true, value: void 0 };
|
|
740
|
+
},
|
|
741
|
+
async throw(error) {
|
|
742
|
+
return iter.throw?.(error) ?? { done: true, value: void 0 };
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
return Reflect.get(target, prop, receiver);
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
function trackLLMCall(params) {
|
|
752
|
+
const client = getTrackingClient();
|
|
753
|
+
if (!client) return;
|
|
754
|
+
const sessionId = _currentSessionId.get();
|
|
755
|
+
if (!sessionId && !params.trackWithoutSession) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (sessionId) {
|
|
759
|
+
client.trackToolCall({
|
|
760
|
+
sessionId,
|
|
761
|
+
toolName: `llm:${params.provider}:${params.model}`,
|
|
762
|
+
toolInput: {
|
|
763
|
+
messages: params.messages,
|
|
764
|
+
model: params.model,
|
|
765
|
+
provider: params.provider
|
|
766
|
+
},
|
|
767
|
+
toolOutput: {
|
|
768
|
+
content: params.output,
|
|
769
|
+
tokens: {
|
|
770
|
+
prompt: params.promptTokens,
|
|
771
|
+
completion: params.completionTokens,
|
|
772
|
+
total: params.totalTokens
|
|
773
|
+
},
|
|
774
|
+
cost_usd: params.cost
|
|
775
|
+
},
|
|
776
|
+
reasoning: `LLM call to ${params.provider} ${params.model}`,
|
|
777
|
+
estimatedCost: params.cost,
|
|
778
|
+
tokenCount: params.totalTokens,
|
|
779
|
+
metadata: {
|
|
780
|
+
provider: params.provider,
|
|
781
|
+
model: params.model,
|
|
782
|
+
duration_ms: params.durationMs,
|
|
783
|
+
prompt_tokens: params.promptTokens,
|
|
784
|
+
completion_tokens: params.completionTokens
|
|
785
|
+
}
|
|
786
|
+
}).catch((err) => {
|
|
787
|
+
console.warn("Sentrial: Failed to track LLM call:", err.message);
|
|
788
|
+
});
|
|
789
|
+
} else if (params.trackWithoutSession) {
|
|
790
|
+
client.createSession({
|
|
791
|
+
name: `LLM: ${params.provider}/${params.model}`,
|
|
792
|
+
agentName: `${params.provider}-wrapper`,
|
|
793
|
+
userId: "anonymous"
|
|
794
|
+
}).then((sid) => {
|
|
795
|
+
if (!sid) return;
|
|
796
|
+
return client.trackToolCall({
|
|
797
|
+
sessionId: sid,
|
|
798
|
+
toolName: `llm:${params.provider}:${params.model}`,
|
|
799
|
+
toolInput: {
|
|
800
|
+
messages: params.messages,
|
|
801
|
+
model: params.model,
|
|
802
|
+
provider: params.provider
|
|
803
|
+
},
|
|
804
|
+
toolOutput: {
|
|
805
|
+
content: params.output,
|
|
806
|
+
tokens: {
|
|
807
|
+
prompt: params.promptTokens,
|
|
808
|
+
completion: params.completionTokens,
|
|
809
|
+
total: params.totalTokens
|
|
810
|
+
},
|
|
811
|
+
cost_usd: params.cost
|
|
812
|
+
},
|
|
813
|
+
estimatedCost: params.cost,
|
|
814
|
+
tokenCount: params.totalTokens,
|
|
815
|
+
metadata: {
|
|
816
|
+
provider: params.provider,
|
|
817
|
+
model: params.model,
|
|
818
|
+
duration_ms: params.durationMs
|
|
819
|
+
}
|
|
820
|
+
}).then(() => {
|
|
821
|
+
return client.completeSession({
|
|
822
|
+
sessionId: sid,
|
|
823
|
+
success: true,
|
|
824
|
+
estimatedCost: params.cost,
|
|
825
|
+
promptTokens: params.promptTokens,
|
|
826
|
+
completionTokens: params.completionTokens,
|
|
827
|
+
totalTokens: params.totalTokens,
|
|
828
|
+
durationMs: params.durationMs
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
}).catch((err) => {
|
|
832
|
+
console.warn("Sentrial: Failed to track standalone LLM call:", err.message);
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function trackLLMError(params) {
|
|
837
|
+
const client = getTrackingClient();
|
|
838
|
+
if (!client) return;
|
|
839
|
+
const sessionId = _currentSessionId.get();
|
|
840
|
+
if (!sessionId && !params.trackWithoutSession) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (!sessionId) return;
|
|
844
|
+
client.trackError({
|
|
845
|
+
sessionId,
|
|
846
|
+
errorMessage: params.error.message,
|
|
847
|
+
errorType: params.error.name,
|
|
848
|
+
toolName: `llm:${params.provider}:${params.model}`,
|
|
849
|
+
metadata: {
|
|
850
|
+
provider: params.provider,
|
|
851
|
+
model: params.model,
|
|
852
|
+
duration_ms: params.durationMs
|
|
853
|
+
}
|
|
854
|
+
}).catch((err) => {
|
|
855
|
+
console.warn("Sentrial: Failed to track LLM error:", err.message);
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/batcher.ts
|
|
860
|
+
var EventBatcher = class {
|
|
861
|
+
queue = [];
|
|
862
|
+
flushIntervalMs;
|
|
863
|
+
flushThreshold;
|
|
864
|
+
maxQueueSize;
|
|
865
|
+
timer = null;
|
|
866
|
+
sendFn;
|
|
867
|
+
flushing = false;
|
|
868
|
+
shutdownCalled = false;
|
|
869
|
+
exitHandler;
|
|
870
|
+
constructor(sendFn, config = {}) {
|
|
871
|
+
this.sendFn = sendFn;
|
|
872
|
+
this.flushIntervalMs = config.flushIntervalMs ?? 1e3;
|
|
873
|
+
this.flushThreshold = config.flushThreshold ?? 10;
|
|
874
|
+
this.maxQueueSize = config.maxQueueSize ?? 1e3;
|
|
875
|
+
this.timer = setInterval(() => {
|
|
876
|
+
void this.flush();
|
|
877
|
+
}, this.flushIntervalMs);
|
|
878
|
+
if (this.timer && typeof this.timer === "object" && "unref" in this.timer) {
|
|
879
|
+
this.timer.unref();
|
|
880
|
+
}
|
|
881
|
+
this.exitHandler = () => {
|
|
882
|
+
void this.shutdown();
|
|
883
|
+
};
|
|
884
|
+
if (typeof process !== "undefined" && process.on) {
|
|
885
|
+
process.on("beforeExit", this.exitHandler);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Enqueue an event for batched delivery.
|
|
890
|
+
*
|
|
891
|
+
* If the queue hits `flushThreshold`, an automatic flush is triggered.
|
|
892
|
+
* If the queue is full (`maxQueueSize`), the oldest event is dropped.
|
|
893
|
+
*/
|
|
894
|
+
enqueue(method, url, body) {
|
|
895
|
+
if (this.shutdownCalled) return;
|
|
896
|
+
if (this.queue.length >= this.maxQueueSize) {
|
|
897
|
+
this.queue.shift();
|
|
898
|
+
if (typeof console !== "undefined") {
|
|
899
|
+
console.warn(
|
|
900
|
+
`Sentrial: Event queue full (${this.maxQueueSize}), dropping oldest event`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
this.queue.push({ method, url, body });
|
|
905
|
+
if (this.queue.length >= this.flushThreshold) {
|
|
906
|
+
void this.flush();
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Flush all queued events to the API.
|
|
911
|
+
*
|
|
912
|
+
* Drains the queue and fires all requests in parallel. Safe to call
|
|
913
|
+
* concurrently — only one flush runs at a time.
|
|
914
|
+
*/
|
|
915
|
+
async flush() {
|
|
916
|
+
if (this.flushing || this.queue.length === 0) return;
|
|
917
|
+
this.flushing = true;
|
|
918
|
+
const batch = this.queue.splice(0, this.queue.length);
|
|
919
|
+
try {
|
|
920
|
+
await Promise.all(
|
|
921
|
+
batch.map(
|
|
922
|
+
(event) => this.sendFn(event.method, event.url, event.body).catch((err) => {
|
|
923
|
+
if (typeof console !== "undefined") {
|
|
924
|
+
console.warn("Sentrial: Batched event failed:", err);
|
|
925
|
+
}
|
|
926
|
+
})
|
|
927
|
+
)
|
|
928
|
+
);
|
|
929
|
+
} finally {
|
|
930
|
+
this.flushing = false;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Stop the batcher: clear the timer, flush remaining events, remove exit handler.
|
|
935
|
+
*/
|
|
936
|
+
async shutdown() {
|
|
937
|
+
if (this.shutdownCalled) return;
|
|
938
|
+
this.shutdownCalled = true;
|
|
939
|
+
if (this.timer !== null) {
|
|
940
|
+
clearInterval(this.timer);
|
|
941
|
+
this.timer = null;
|
|
942
|
+
}
|
|
943
|
+
if (typeof process !== "undefined" && process.removeListener) {
|
|
944
|
+
process.removeListener("beforeExit", this.exitHandler);
|
|
945
|
+
}
|
|
946
|
+
this.flushing = false;
|
|
947
|
+
await this.flush();
|
|
948
|
+
}
|
|
949
|
+
/** Number of events currently queued. */
|
|
950
|
+
get size() {
|
|
951
|
+
return this.queue.length;
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
// src/types.ts
|
|
956
|
+
var EventType = /* @__PURE__ */ ((EventType2) => {
|
|
957
|
+
EventType2["TOOL_CALL"] = "tool_call";
|
|
958
|
+
EventType2["LLM_DECISION"] = "llm_decision";
|
|
959
|
+
EventType2["STATE_CHANGE"] = "state_change";
|
|
960
|
+
EventType2["ERROR"] = "error";
|
|
961
|
+
return EventType2;
|
|
962
|
+
})(EventType || {});
|
|
963
|
+
|
|
964
|
+
// src/client.ts
|
|
965
|
+
var DEFAULT_API_URL = "https://api.sentrial.com";
|
|
966
|
+
var MAX_RETRIES = 3;
|
|
967
|
+
var INITIAL_BACKOFF_MS = 500;
|
|
968
|
+
var MAX_BACKOFF_MS = 8e3;
|
|
969
|
+
var BACKOFF_MULTIPLIER = 2;
|
|
970
|
+
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
971
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
972
|
+
var SentrialClient = class {
|
|
973
|
+
apiUrl;
|
|
974
|
+
apiKey;
|
|
975
|
+
failSilently;
|
|
976
|
+
piiConfig;
|
|
977
|
+
piiConfigNeedsHydration = false;
|
|
978
|
+
piiHydrationPromise;
|
|
979
|
+
_stateVar = createContextVar({});
|
|
980
|
+
batcher;
|
|
981
|
+
/** Per-session cost/token accumulator — populated by trackToolCall/trackDecision */
|
|
982
|
+
sessionAccumulators = /* @__PURE__ */ new Map();
|
|
983
|
+
get currentState() {
|
|
984
|
+
return this._stateVar.get();
|
|
985
|
+
}
|
|
986
|
+
set currentState(value) {
|
|
987
|
+
this._stateVar.set(value);
|
|
988
|
+
}
|
|
989
|
+
constructor(config = {}) {
|
|
990
|
+
this.apiUrl = (config.apiUrl ?? (typeof process !== "undefined" ? process.env?.SENTRIAL_API_URL : void 0) ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
991
|
+
this.apiKey = config.apiKey ?? (typeof process !== "undefined" ? process.env?.SENTRIAL_API_KEY : void 0);
|
|
992
|
+
this.failSilently = config.failSilently ?? true;
|
|
993
|
+
if (config.pii === true) {
|
|
994
|
+
this.piiConfig = { enabled: true };
|
|
995
|
+
this.piiConfigNeedsHydration = true;
|
|
996
|
+
} else if (config.pii && typeof config.pii === "object") {
|
|
997
|
+
this.piiConfig = config.pii;
|
|
998
|
+
this.piiConfigNeedsHydration = false;
|
|
999
|
+
}
|
|
1000
|
+
if (config.batching?.enabled) {
|
|
1001
|
+
this.batcher = new EventBatcher(
|
|
1002
|
+
(method, url, body) => this.safeRequest(method, url, body),
|
|
1003
|
+
config.batching
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Fetch the organization's PII config from the server.
|
|
1009
|
+
*
|
|
1010
|
+
* Called lazily on the first request when `pii: true` was passed to the constructor.
|
|
1011
|
+
* Uses a single shared promise so concurrent requests don't trigger duplicate fetches.
|
|
1012
|
+
*/
|
|
1013
|
+
async hydratePiiConfig() {
|
|
1014
|
+
if (!this.piiConfigNeedsHydration) return;
|
|
1015
|
+
if (this.piiHydrationPromise) {
|
|
1016
|
+
await this.piiHydrationPromise;
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
this.piiHydrationPromise = (async () => {
|
|
1020
|
+
try {
|
|
1021
|
+
const headers = {};
|
|
1022
|
+
if (this.apiKey) {
|
|
1023
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1024
|
+
}
|
|
1025
|
+
const controller = new AbortController();
|
|
1026
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
1027
|
+
let response;
|
|
1028
|
+
try {
|
|
1029
|
+
response = await fetch(`${this.apiUrl}/api/sdk/pii-config`, {
|
|
1030
|
+
method: "GET",
|
|
1031
|
+
headers,
|
|
1032
|
+
signal: controller.signal
|
|
1033
|
+
});
|
|
1034
|
+
} finally {
|
|
1035
|
+
clearTimeout(timeoutId);
|
|
1036
|
+
}
|
|
1037
|
+
if (response.ok) {
|
|
1038
|
+
const data = await response.json();
|
|
1039
|
+
if (data.config) {
|
|
1040
|
+
this.piiConfig = {
|
|
1041
|
+
enabled: data.config.enabled,
|
|
1042
|
+
mode: data.config.mode,
|
|
1043
|
+
fields: data.config.fields,
|
|
1044
|
+
builtinPatterns: data.config.builtinPatterns,
|
|
1045
|
+
customPatterns: (data.config.customPatterns || []).map(
|
|
1046
|
+
(cp) => ({
|
|
1047
|
+
pattern: new RegExp(cp.pattern, "g"),
|
|
1048
|
+
label: cp.label
|
|
1049
|
+
})
|
|
1050
|
+
),
|
|
1051
|
+
enhancedDetection: data.config.enhancedDetection
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
} catch {
|
|
1056
|
+
}
|
|
1057
|
+
this.piiConfigNeedsHydration = false;
|
|
1058
|
+
})();
|
|
1059
|
+
await this.piiHydrationPromise;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Make an HTTP request with retry logic and exponential backoff.
|
|
1063
|
+
*
|
|
1064
|
+
* Retries on transient failures (network errors, timeouts, 429/5xx).
|
|
1065
|
+
* Up to MAX_RETRIES attempts with exponential backoff.
|
|
1066
|
+
*/
|
|
1067
|
+
async safeRequest(method, url, body) {
|
|
1068
|
+
if (this.piiConfigNeedsHydration) {
|
|
1069
|
+
await this.hydratePiiConfig();
|
|
1070
|
+
}
|
|
1071
|
+
let lastError;
|
|
1072
|
+
let backoff = INITIAL_BACKOFF_MS;
|
|
1073
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
1074
|
+
try {
|
|
1075
|
+
const headers = {
|
|
1076
|
+
"Content-Type": "application/json"
|
|
1077
|
+
};
|
|
1078
|
+
if (this.apiKey) {
|
|
1079
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1080
|
+
}
|
|
1081
|
+
const finalBody = this.piiConfig && body && typeof body === "object" ? redactPayload(body, this.piiConfig) : body;
|
|
1082
|
+
const controller = new AbortController();
|
|
1083
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
1084
|
+
let response;
|
|
1085
|
+
try {
|
|
1086
|
+
response = await fetch(url, {
|
|
1087
|
+
method,
|
|
1088
|
+
headers,
|
|
1089
|
+
body: finalBody ? JSON.stringify(finalBody) : void 0,
|
|
1090
|
+
signal: controller.signal
|
|
1091
|
+
});
|
|
1092
|
+
} finally {
|
|
1093
|
+
clearTimeout(timeoutId);
|
|
1094
|
+
}
|
|
1095
|
+
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < MAX_RETRIES) {
|
|
1096
|
+
await this.sleep(backoff);
|
|
1097
|
+
backoff = Math.min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
if (!response.ok) {
|
|
1101
|
+
const errorBody = await response.text();
|
|
1102
|
+
let errorData = {};
|
|
1103
|
+
try {
|
|
1104
|
+
errorData = JSON.parse(errorBody);
|
|
1105
|
+
} catch {
|
|
1106
|
+
}
|
|
1107
|
+
const error = new ApiError(
|
|
1108
|
+
errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
1109
|
+
response.status,
|
|
1110
|
+
errorData.error?.code
|
|
1111
|
+
);
|
|
1112
|
+
if (this.failSilently) {
|
|
1113
|
+
console.warn(`Sentrial: Request failed (${method} ${url}):`, error.message);
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
throw error;
|
|
1117
|
+
}
|
|
1118
|
+
return await response.json();
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
if (error instanceof ApiError) {
|
|
1121
|
+
throw error;
|
|
1122
|
+
}
|
|
1123
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1124
|
+
if (attempt < MAX_RETRIES) {
|
|
1125
|
+
await this.sleep(backoff);
|
|
1126
|
+
backoff = Math.min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const networkError = new NetworkError(
|
|
1132
|
+
lastError?.message ?? "Unknown network error",
|
|
1133
|
+
lastError
|
|
1134
|
+
);
|
|
1135
|
+
if (this.failSilently) {
|
|
1136
|
+
console.warn(`Sentrial: Request failed after ${MAX_RETRIES + 1} attempts (${method} ${url}):`, networkError.message);
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
throw networkError;
|
|
1140
|
+
}
|
|
1141
|
+
sleep(ms) {
|
|
1142
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1143
|
+
}
|
|
1144
|
+
accumulate(sessionId, cost, tokenCount, toolOutput) {
|
|
1145
|
+
let acc = this.sessionAccumulators.get(sessionId);
|
|
1146
|
+
if (!acc) {
|
|
1147
|
+
acc = { cost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1148
|
+
this.sessionAccumulators.set(sessionId, acc);
|
|
1149
|
+
}
|
|
1150
|
+
if (cost != null) acc.cost += cost;
|
|
1151
|
+
if (tokenCount != null) acc.totalTokens += tokenCount;
|
|
1152
|
+
const rawTokens = toolOutput?.tokens;
|
|
1153
|
+
if (rawTokens && typeof rawTokens === "object" && !Array.isArray(rawTokens)) {
|
|
1154
|
+
const tokens = rawTokens;
|
|
1155
|
+
if (typeof tokens.prompt === "number") acc.promptTokens += tokens.prompt;
|
|
1156
|
+
if (typeof tokens.completion === "number") acc.completionTokens += tokens.completion;
|
|
1157
|
+
}
|
|
482
1158
|
}
|
|
483
1159
|
/**
|
|
484
1160
|
* Create a new session
|
|
@@ -514,6 +1190,7 @@ var SentrialClient = class {
|
|
|
514
1190
|
* @returns Event data
|
|
515
1191
|
*/
|
|
516
1192
|
async trackToolCall(params) {
|
|
1193
|
+
this.accumulate(params.sessionId, params.estimatedCost, params.tokenCount, params.toolOutput);
|
|
517
1194
|
const stateBefore = { ...this.currentState };
|
|
518
1195
|
this.currentState[`${params.toolName}_result`] = params.toolOutput;
|
|
519
1196
|
const payload = {
|
|
@@ -532,6 +1209,10 @@ var SentrialClient = class {
|
|
|
532
1209
|
if (params.traceId !== void 0) payload.traceId = params.traceId;
|
|
533
1210
|
if (params.spanId !== void 0) payload.spanId = params.spanId;
|
|
534
1211
|
if (params.metadata !== void 0) payload.metadata = params.metadata;
|
|
1212
|
+
if (this.batcher) {
|
|
1213
|
+
this.batcher.enqueue("POST", `${this.apiUrl}/api/sdk/events`, payload);
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
535
1216
|
return this.safeRequest("POST", `${this.apiUrl}/api/sdk/events`, payload);
|
|
536
1217
|
}
|
|
537
1218
|
/**
|
|
@@ -541,6 +1222,7 @@ var SentrialClient = class {
|
|
|
541
1222
|
* @returns Event data
|
|
542
1223
|
*/
|
|
543
1224
|
async trackDecision(params) {
|
|
1225
|
+
this.accumulate(params.sessionId, params.estimatedCost, params.tokenCount);
|
|
544
1226
|
const stateBefore = { ...this.currentState };
|
|
545
1227
|
const payload = {
|
|
546
1228
|
sessionId: params.sessionId,
|
|
@@ -556,6 +1238,10 @@ var SentrialClient = class {
|
|
|
556
1238
|
if (params.traceId !== void 0) payload.traceId = params.traceId;
|
|
557
1239
|
if (params.spanId !== void 0) payload.spanId = params.spanId;
|
|
558
1240
|
if (params.metadata !== void 0) payload.metadata = params.metadata;
|
|
1241
|
+
if (this.batcher) {
|
|
1242
|
+
this.batcher.enqueue("POST", `${this.apiUrl}/api/sdk/events`, payload);
|
|
1243
|
+
return null;
|
|
1244
|
+
}
|
|
559
1245
|
return this.safeRequest("POST", `${this.apiUrl}/api/sdk/events`, payload);
|
|
560
1246
|
}
|
|
561
1247
|
/**
|
|
@@ -582,6 +1268,10 @@ var SentrialClient = class {
|
|
|
582
1268
|
if (params.traceId !== void 0) payload.traceId = params.traceId;
|
|
583
1269
|
if (params.spanId !== void 0) payload.spanId = params.spanId;
|
|
584
1270
|
if (params.metadata !== void 0) payload.metadata = params.metadata;
|
|
1271
|
+
if (this.batcher) {
|
|
1272
|
+
this.batcher.enqueue("POST", `${this.apiUrl}/api/sdk/events`, payload);
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
585
1275
|
return this.safeRequest("POST", `${this.apiUrl}/api/sdk/events`, payload);
|
|
586
1276
|
}
|
|
587
1277
|
/**
|
|
@@ -627,6 +1317,10 @@ var SentrialClient = class {
|
|
|
627
1317
|
if (params.metadata) {
|
|
628
1318
|
payload.metadata = params.metadata;
|
|
629
1319
|
}
|
|
1320
|
+
if (this.batcher) {
|
|
1321
|
+
this.batcher.enqueue("POST", `${this.apiUrl}/api/sdk/events`, payload);
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
630
1324
|
return this.safeRequest("POST", `${this.apiUrl}/api/sdk/events`, payload);
|
|
631
1325
|
}
|
|
632
1326
|
/**
|
|
@@ -656,6 +1350,17 @@ var SentrialClient = class {
|
|
|
656
1350
|
* ```
|
|
657
1351
|
*/
|
|
658
1352
|
async completeSession(params) {
|
|
1353
|
+
if (this.batcher) {
|
|
1354
|
+
await this.batcher.flush();
|
|
1355
|
+
}
|
|
1356
|
+
const acc = this.sessionAccumulators.get(params.sessionId);
|
|
1357
|
+
if (acc) {
|
|
1358
|
+
if (params.estimatedCost === void 0 && acc.cost > 0) params = { ...params, estimatedCost: acc.cost };
|
|
1359
|
+
if (params.promptTokens === void 0 && acc.promptTokens > 0) params = { ...params, promptTokens: acc.promptTokens };
|
|
1360
|
+
if (params.completionTokens === void 0 && acc.completionTokens > 0) params = { ...params, completionTokens: acc.completionTokens };
|
|
1361
|
+
if (params.totalTokens === void 0 && acc.totalTokens > 0) params = { ...params, totalTokens: acc.totalTokens };
|
|
1362
|
+
this.sessionAccumulators.delete(params.sessionId);
|
|
1363
|
+
}
|
|
659
1364
|
const payload = {
|
|
660
1365
|
status: params.success !== false ? "completed" : "failed",
|
|
661
1366
|
success: params.success ?? true
|
|
@@ -676,6 +1381,27 @@ var SentrialClient = class {
|
|
|
676
1381
|
payload
|
|
677
1382
|
);
|
|
678
1383
|
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Flush any queued events immediately.
|
|
1386
|
+
*
|
|
1387
|
+
* No-op if batching is not enabled.
|
|
1388
|
+
*/
|
|
1389
|
+
async flush() {
|
|
1390
|
+
if (this.batcher) {
|
|
1391
|
+
await this.batcher.flush();
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Shut down the event batcher, flushing remaining events.
|
|
1396
|
+
*
|
|
1397
|
+
* Call this before your process exits for a clean shutdown.
|
|
1398
|
+
* No-op if batching is not enabled.
|
|
1399
|
+
*/
|
|
1400
|
+
async shutdown() {
|
|
1401
|
+
if (this.batcher) {
|
|
1402
|
+
await this.batcher.shutdown();
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
679
1405
|
/**
|
|
680
1406
|
* Begin tracking an interaction (simplified API)
|
|
681
1407
|
*
|
|
@@ -712,13 +1438,18 @@ var SentrialClient = class {
|
|
|
712
1438
|
if (params.input) {
|
|
713
1439
|
this.currentState.input = params.input;
|
|
714
1440
|
}
|
|
1441
|
+
let sessionTokens;
|
|
1442
|
+
if (sessionId) {
|
|
1443
|
+
sessionTokens = _setSessionContextWithTokens(sessionId, this);
|
|
1444
|
+
}
|
|
715
1445
|
return new Interaction({
|
|
716
1446
|
client: this,
|
|
717
1447
|
sessionId,
|
|
718
1448
|
eventId,
|
|
719
1449
|
userId: params.userId,
|
|
720
1450
|
event: params.event,
|
|
721
|
-
userInput: params.input
|
|
1451
|
+
userInput: params.input,
|
|
1452
|
+
sessionTokens
|
|
722
1453
|
});
|
|
723
1454
|
}
|
|
724
1455
|
// Cost calculation static methods for convenience
|
|
@@ -735,12 +1466,15 @@ var Interaction = class {
|
|
|
735
1466
|
userId;
|
|
736
1467
|
/** Event name for this interaction */
|
|
737
1468
|
event;
|
|
1469
|
+
startTime = Date.now();
|
|
738
1470
|
finished = false;
|
|
739
1471
|
success = true;
|
|
740
1472
|
failureReason;
|
|
741
1473
|
output;
|
|
742
1474
|
userInput;
|
|
743
1475
|
degraded;
|
|
1476
|
+
/** Context tokens for restoring previous session context on finish() */
|
|
1477
|
+
sessionTokens;
|
|
744
1478
|
constructor(config) {
|
|
745
1479
|
this.client = config.client;
|
|
746
1480
|
this.sessionId = config.sessionId;
|
|
@@ -749,6 +1483,7 @@ var Interaction = class {
|
|
|
749
1483
|
this.event = config.event;
|
|
750
1484
|
this.userInput = config.userInput;
|
|
751
1485
|
this.degraded = config.sessionId === null;
|
|
1486
|
+
this.sessionTokens = config.sessionTokens;
|
|
752
1487
|
}
|
|
753
1488
|
/**
|
|
754
1489
|
* Set the output for this interaction
|
|
@@ -784,18 +1519,24 @@ var Interaction = class {
|
|
|
784
1519
|
}
|
|
785
1520
|
this.finished = true;
|
|
786
1521
|
const finalOutput = params.output ?? this.output;
|
|
787
|
-
|
|
1522
|
+
const result = await this.client.completeSession({
|
|
788
1523
|
sessionId: this.sessionId,
|
|
789
1524
|
success: params.success ?? this.success,
|
|
790
1525
|
failureReason: params.failureReason ?? this.failureReason,
|
|
791
1526
|
estimatedCost: params.estimatedCost,
|
|
792
1527
|
customMetrics: params.customMetrics,
|
|
1528
|
+
durationMs: params.durationMs ?? Date.now() - this.startTime,
|
|
793
1529
|
promptTokens: params.promptTokens,
|
|
794
1530
|
completionTokens: params.completionTokens,
|
|
795
1531
|
totalTokens: params.totalTokens,
|
|
796
1532
|
userInput: this.userInput,
|
|
797
1533
|
assistantOutput: finalOutput
|
|
798
1534
|
});
|
|
1535
|
+
if (this.sessionTokens) {
|
|
1536
|
+
_restoreSessionContext(this.sessionTokens);
|
|
1537
|
+
this.sessionTokens = void 0;
|
|
1538
|
+
}
|
|
1539
|
+
return result;
|
|
799
1540
|
}
|
|
800
1541
|
/**
|
|
801
1542
|
* Track a tool call within this interaction
|
|
@@ -855,16 +1596,24 @@ function configure(config) {
|
|
|
855
1596
|
function begin(params) {
|
|
856
1597
|
return getClient().begin(params);
|
|
857
1598
|
}
|
|
1599
|
+
async function flush() {
|
|
1600
|
+
if (defaultClient) await defaultClient.flush();
|
|
1601
|
+
}
|
|
1602
|
+
async function shutdown() {
|
|
1603
|
+
if (defaultClient) await defaultClient.shutdown();
|
|
1604
|
+
}
|
|
858
1605
|
var sentrial = {
|
|
859
1606
|
configure,
|
|
860
|
-
begin
|
|
1607
|
+
begin,
|
|
1608
|
+
flush,
|
|
1609
|
+
shutdown
|
|
861
1610
|
};
|
|
862
1611
|
|
|
863
1612
|
// src/vercel.ts
|
|
864
|
-
var
|
|
1613
|
+
var _defaultClient2 = null;
|
|
865
1614
|
var _globalConfig = {};
|
|
866
1615
|
function configureVercel(config) {
|
|
867
|
-
|
|
1616
|
+
_defaultClient2 = new SentrialClient({
|
|
868
1617
|
apiKey: config.apiKey,
|
|
869
1618
|
apiUrl: config.apiUrl,
|
|
870
1619
|
failSilently: config.failSilently ?? true
|
|
@@ -876,10 +1625,10 @@ function configureVercel(config) {
|
|
|
876
1625
|
};
|
|
877
1626
|
}
|
|
878
1627
|
function getClient2() {
|
|
879
|
-
if (!
|
|
880
|
-
|
|
1628
|
+
if (!_defaultClient2) {
|
|
1629
|
+
_defaultClient2 = new SentrialClient();
|
|
881
1630
|
}
|
|
882
|
-
return
|
|
1631
|
+
return _defaultClient2;
|
|
883
1632
|
}
|
|
884
1633
|
function extractModelInfo(model) {
|
|
885
1634
|
const modelId = model.modelId || model.id || "unknown";
|
|
@@ -888,7 +1637,7 @@ function extractModelInfo(model) {
|
|
|
888
1637
|
}
|
|
889
1638
|
function guessProvider(modelId) {
|
|
890
1639
|
const id = modelId.toLowerCase();
|
|
891
|
-
if (id.includes("gpt") || id.
|
|
1640
|
+
if (id.includes("gpt") || id.startsWith("o1") || id.startsWith("o3") || id.startsWith("o4") || id.startsWith("chatgpt")) return "openai";
|
|
892
1641
|
if (id.includes("claude")) return "anthropic";
|
|
893
1642
|
if (id.includes("gemini")) return "google";
|
|
894
1643
|
if (id.includes("mistral") || id.includes("mixtral") || id.includes("codestral") || id.includes("pixtral")) return "mistral";
|
|
@@ -914,7 +1663,7 @@ function calculateCostForCall(provider, modelId, promptTokens, completionTokens)
|
|
|
914
1663
|
case "mistral":
|
|
915
1664
|
return promptTokens / 1e6 * 2 + completionTokens / 1e6 * 6;
|
|
916
1665
|
default:
|
|
917
|
-
return
|
|
1666
|
+
return 0;
|
|
918
1667
|
}
|
|
919
1668
|
}
|
|
920
1669
|
function extractInput(params) {
|
|
@@ -1047,15 +1796,14 @@ function wrapGenerateText(originalFn, client, config) {
|
|
|
1047
1796
|
const result = await originalFn(wrappedParams);
|
|
1048
1797
|
const durationMs = Date.now() - startTime;
|
|
1049
1798
|
const resolvedModelId = result.response?.modelId || modelId;
|
|
1050
|
-
const promptTokens = result.usage?.promptTokens
|
|
1051
|
-
const completionTokens = result.usage?.completionTokens
|
|
1052
|
-
const totalTokens = result.usage?.totalTokens
|
|
1799
|
+
const promptTokens = result.usage?.promptTokens ?? 0;
|
|
1800
|
+
const completionTokens = result.usage?.completionTokens ?? 0;
|
|
1801
|
+
const totalTokens = result.usage?.totalTokens ?? promptTokens + completionTokens;
|
|
1053
1802
|
const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
|
|
1054
1803
|
const steps = result.steps;
|
|
1055
1804
|
if (steps && steps.length >= 1) {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
await client.trackEvent({
|
|
1805
|
+
const stepPromises = steps.map(
|
|
1806
|
+
(step, i) => client.trackEvent({
|
|
1059
1807
|
sessionId,
|
|
1060
1808
|
eventType: "llm_call",
|
|
1061
1809
|
eventData: {
|
|
@@ -1063,14 +1811,16 @@ function wrapGenerateText(originalFn, client, config) {
|
|
|
1063
1811
|
provider,
|
|
1064
1812
|
step: i + 1,
|
|
1065
1813
|
total_steps: steps.length,
|
|
1066
|
-
prompt_tokens: step.usage?.promptTokens
|
|
1067
|
-
completion_tokens: step.usage?.completionTokens
|
|
1068
|
-
total_tokens: step.usage?.totalTokens
|
|
1814
|
+
prompt_tokens: step.usage?.promptTokens ?? 0,
|
|
1815
|
+
completion_tokens: step.usage?.completionTokens ?? 0,
|
|
1816
|
+
total_tokens: step.usage?.totalTokens ?? 0,
|
|
1069
1817
|
finish_reason: step.finishReason,
|
|
1070
1818
|
tool_calls: step.toolCalls?.map((tc) => tc.toolName)
|
|
1071
1819
|
}
|
|
1072
|
-
})
|
|
1073
|
-
|
|
1820
|
+
}).catch(() => {
|
|
1821
|
+
})
|
|
1822
|
+
);
|
|
1823
|
+
await Promise.all(stepPromises);
|
|
1074
1824
|
} else {
|
|
1075
1825
|
await client.trackEvent({
|
|
1076
1826
|
sessionId,
|
|
@@ -1148,8 +1898,10 @@ function wrapStreamText(originalFn, client, config) {
|
|
|
1148
1898
|
tools: params.tools ? wrapToolsAsync(params.tools, sessionPromise, client) : void 0
|
|
1149
1899
|
};
|
|
1150
1900
|
const result = originalFn(wrappedParams);
|
|
1901
|
+
const originalTextStream = result.textStream;
|
|
1902
|
+
let fullText = "";
|
|
1151
1903
|
let tracked = false;
|
|
1152
|
-
async function trackCompletion(
|
|
1904
|
+
async function trackCompletion(text, error) {
|
|
1153
1905
|
if (tracked) return;
|
|
1154
1906
|
tracked = true;
|
|
1155
1907
|
const durationMs = Date.now() - startTime;
|
|
@@ -1160,80 +1912,118 @@ function wrapStreamText(originalFn, client, config) {
|
|
|
1160
1912
|
sessionId: sid,
|
|
1161
1913
|
errorType: error.name || "Error",
|
|
1162
1914
|
errorMessage: error.message || "Unknown error"
|
|
1915
|
+
}).catch(() => {
|
|
1163
1916
|
});
|
|
1164
1917
|
await client.completeSession({
|
|
1165
1918
|
sessionId: sid,
|
|
1166
1919
|
success: false,
|
|
1167
1920
|
failureReason: error.message || "Unknown error",
|
|
1168
1921
|
durationMs
|
|
1922
|
+
}).catch(() => {
|
|
1169
1923
|
});
|
|
1170
1924
|
return;
|
|
1171
1925
|
}
|
|
1926
|
+
let resolvedModelId = modelId;
|
|
1927
|
+
try {
|
|
1928
|
+
const resp = result.response ? await result.response : void 0;
|
|
1929
|
+
if (resp?.modelId) resolvedModelId = resp.modelId;
|
|
1930
|
+
} catch {
|
|
1931
|
+
}
|
|
1172
1932
|
let usage;
|
|
1173
1933
|
try {
|
|
1174
1934
|
usage = result.usage ? await result.usage : void 0;
|
|
1175
1935
|
} catch {
|
|
1176
1936
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
trackCompletion(textProp).catch(() => {
|
|
1206
|
-
});
|
|
1207
|
-
} else if (textProp != null && typeof textProp.then === "function") {
|
|
1208
|
-
const originalTextPromise = textProp;
|
|
1209
|
-
result.text = originalTextPromise.then((text) => {
|
|
1210
|
-
trackCompletion(text).catch(() => {
|
|
1937
|
+
let steps;
|
|
1938
|
+
try {
|
|
1939
|
+
steps = result.steps ? await result.steps : void 0;
|
|
1940
|
+
} catch {
|
|
1941
|
+
}
|
|
1942
|
+
if (steps && steps.length >= 1) {
|
|
1943
|
+
let totalPrompt = 0, totalCompletion = 0;
|
|
1944
|
+
const stepPromises = steps.map((step, i) => {
|
|
1945
|
+
const sp = step.usage?.promptTokens ?? 0;
|
|
1946
|
+
const sc = step.usage?.completionTokens ?? 0;
|
|
1947
|
+
totalPrompt += sp;
|
|
1948
|
+
totalCompletion += sc;
|
|
1949
|
+
return client.trackEvent({
|
|
1950
|
+
sessionId: sid,
|
|
1951
|
+
eventType: "llm_call",
|
|
1952
|
+
eventData: {
|
|
1953
|
+
model: resolvedModelId,
|
|
1954
|
+
provider,
|
|
1955
|
+
step: i + 1,
|
|
1956
|
+
total_steps: steps.length,
|
|
1957
|
+
prompt_tokens: sp,
|
|
1958
|
+
completion_tokens: sc,
|
|
1959
|
+
total_tokens: step.usage?.totalTokens ?? 0,
|
|
1960
|
+
finish_reason: step.finishReason,
|
|
1961
|
+
tool_calls: step.toolCalls?.map((tc) => tc.toolName)
|
|
1962
|
+
}
|
|
1963
|
+
}).catch(() => {
|
|
1964
|
+
});
|
|
1211
1965
|
});
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1966
|
+
await Promise.all(stepPromises);
|
|
1967
|
+
const promptTokens = usage?.promptTokens ?? totalPrompt;
|
|
1968
|
+
const completionTokens = usage?.completionTokens ?? totalCompletion;
|
|
1969
|
+
const totalTokens = usage?.totalTokens ?? promptTokens + completionTokens;
|
|
1970
|
+
const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
|
|
1971
|
+
await client.completeSession({
|
|
1972
|
+
sessionId: sid,
|
|
1973
|
+
success: true,
|
|
1974
|
+
output: text,
|
|
1975
|
+
durationMs,
|
|
1976
|
+
estimatedCost: cost,
|
|
1977
|
+
promptTokens,
|
|
1978
|
+
completionTokens,
|
|
1979
|
+
totalTokens
|
|
1980
|
+
}).catch(() => {
|
|
1215
1981
|
});
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1982
|
+
} else {
|
|
1983
|
+
const promptTokens = usage?.promptTokens ?? 0;
|
|
1984
|
+
const completionTokens = usage?.completionTokens ?? 0;
|
|
1985
|
+
const totalTokens = usage?.totalTokens ?? promptTokens + completionTokens;
|
|
1986
|
+
const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
|
|
1987
|
+
await client.trackEvent({
|
|
1988
|
+
sessionId: sid,
|
|
1989
|
+
eventType: "llm_call",
|
|
1990
|
+
eventData: {
|
|
1991
|
+
model: resolvedModelId,
|
|
1992
|
+
provider,
|
|
1993
|
+
prompt_tokens: promptTokens,
|
|
1994
|
+
completion_tokens: completionTokens,
|
|
1995
|
+
total_tokens: totalTokens
|
|
1226
1996
|
}
|
|
1227
|
-
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1997
|
+
}).catch(() => {
|
|
1998
|
+
});
|
|
1999
|
+
await client.completeSession({
|
|
2000
|
+
sessionId: sid,
|
|
2001
|
+
success: true,
|
|
2002
|
+
output: text,
|
|
2003
|
+
durationMs,
|
|
2004
|
+
estimatedCost: cost,
|
|
2005
|
+
promptTokens,
|
|
2006
|
+
completionTokens,
|
|
2007
|
+
totalTokens
|
|
2008
|
+
}).catch(() => {
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
1236
2011
|
}
|
|
2012
|
+
result.textStream = (async function* () {
|
|
2013
|
+
try {
|
|
2014
|
+
for await (const chunk of originalTextStream) {
|
|
2015
|
+
fullText += chunk;
|
|
2016
|
+
yield chunk;
|
|
2017
|
+
}
|
|
2018
|
+
await trackCompletion(fullText);
|
|
2019
|
+
} catch (error) {
|
|
2020
|
+
await trackCompletion(
|
|
2021
|
+
fullText,
|
|
2022
|
+
error instanceof Error ? error : new Error(String(error))
|
|
2023
|
+
);
|
|
2024
|
+
throw error;
|
|
2025
|
+
}
|
|
2026
|
+
})();
|
|
1237
2027
|
return result;
|
|
1238
2028
|
};
|
|
1239
2029
|
}
|
|
@@ -1261,10 +2051,22 @@ function wrapGenerateObject(originalFn, client, config) {
|
|
|
1261
2051
|
const result = await originalFn(params);
|
|
1262
2052
|
const durationMs = Date.now() - startTime;
|
|
1263
2053
|
const resolvedModelId = result.response?.modelId || modelId;
|
|
1264
|
-
const promptTokens = result.usage?.promptTokens
|
|
1265
|
-
const completionTokens = result.usage?.completionTokens
|
|
1266
|
-
const totalTokens = result.usage?.totalTokens
|
|
2054
|
+
const promptTokens = result.usage?.promptTokens ?? 0;
|
|
2055
|
+
const completionTokens = result.usage?.completionTokens ?? 0;
|
|
2056
|
+
const totalTokens = result.usage?.totalTokens ?? promptTokens + completionTokens;
|
|
1267
2057
|
const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
|
|
2058
|
+
await client.trackEvent({
|
|
2059
|
+
sessionId,
|
|
2060
|
+
eventType: "llm_call",
|
|
2061
|
+
eventData: {
|
|
2062
|
+
model: resolvedModelId,
|
|
2063
|
+
provider,
|
|
2064
|
+
prompt_tokens: promptTokens,
|
|
2065
|
+
completion_tokens: completionTokens,
|
|
2066
|
+
total_tokens: totalTokens
|
|
2067
|
+
}
|
|
2068
|
+
}).catch(() => {
|
|
2069
|
+
});
|
|
1268
2070
|
await client.completeSession({
|
|
1269
2071
|
sessionId,
|
|
1270
2072
|
success: true,
|
|
@@ -1293,393 +2095,279 @@ function wrapGenerateObject(originalFn, client, config) {
|
|
|
1293
2095
|
}
|
|
1294
2096
|
};
|
|
1295
2097
|
}
|
|
1296
|
-
function wrapStreamObject(originalFn, client, config) {
|
|
1297
|
-
return (params) => {
|
|
1298
|
-
const startTime = Date.now();
|
|
1299
|
-
const { modelId, provider } = extractModelInfo(params.model);
|
|
1300
|
-
const input = extractInput(params);
|
|
1301
|
-
const sessionPromise = (async () => {
|
|
1302
|
-
try {
|
|
1303
|
-
const id = await client.createSession({
|
|
1304
|
-
name: `streamObject: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
|
|
1305
|
-
agentName: config.defaultAgent ?? "vercel-ai-sdk",
|
|
1306
|
-
userId: config.userId ?? "anonymous",
|
|
1307
|
-
convoId: config.convoId,
|
|
1308
|
-
metadata: {
|
|
1309
|
-
model: modelId,
|
|
1310
|
-
provider,
|
|
1311
|
-
function: "streamObject"
|
|
1312
|
-
}
|
|
1313
|
-
});
|
|
1314
|
-
if (id) {
|
|
1315
|
-
client.setInput(id, input).catch(() => {
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
return id;
|
|
1319
|
-
} catch {
|
|
1320
|
-
return null;
|
|
1321
|
-
}
|
|
1322
|
-
})();
|
|
1323
|
-
const result = originalFn(params);
|
|
1324
|
-
if (result.object) {
|
|
1325
|
-
const originalObjectPromise = result.object;
|
|
1326
|
-
result.object = originalObjectPromise.then(async (obj) => {
|
|
1327
|
-
const durationMs = Date.now() - startTime;
|
|
1328
|
-
const sid = await sessionPromise;
|
|
1329
|
-
if (sid) {
|
|
1330
|
-
let usage;
|
|
1331
|
-
try {
|
|
1332
|
-
usage = result.usage ? await result.usage : void 0;
|
|
1333
|
-
} catch {
|
|
1334
|
-
}
|
|
1335
|
-
const promptTokens = usage?.promptTokens || 0;
|
|
1336
|
-
const completionTokens = usage?.completionTokens || 0;
|
|
1337
|
-
const totalTokens = usage?.totalTokens || promptTokens + completionTokens;
|
|
1338
|
-
const cost = calculateCostForCall(provider, modelId, promptTokens, completionTokens);
|
|
1339
|
-
await client.completeSession({
|
|
1340
|
-
sessionId: sid,
|
|
1341
|
-
success: true,
|
|
1342
|
-
output: JSON.stringify(obj),
|
|
1343
|
-
durationMs,
|
|
1344
|
-
estimatedCost: cost,
|
|
1345
|
-
promptTokens,
|
|
1346
|
-
completionTokens,
|
|
1347
|
-
totalTokens
|
|
1348
|
-
});
|
|
1349
|
-
}
|
|
1350
|
-
return obj;
|
|
1351
|
-
}).catch(async (error) => {
|
|
1352
|
-
const durationMs = Date.now() - startTime;
|
|
1353
|
-
const sid = await sessionPromise;
|
|
1354
|
-
if (sid) {
|
|
1355
|
-
await client.trackError({
|
|
1356
|
-
sessionId: sid,
|
|
1357
|
-
errorType: error instanceof Error ? error.name : "Error",
|
|
1358
|
-
errorMessage: error instanceof Error ? error.message : "Unknown error"
|
|
1359
|
-
});
|
|
1360
|
-
await client.completeSession({
|
|
1361
|
-
sessionId: sid,
|
|
1362
|
-
success: false,
|
|
1363
|
-
failureReason: error instanceof Error ? error.message : "Unknown error",
|
|
1364
|
-
durationMs
|
|
1365
|
-
});
|
|
1366
|
-
}
|
|
1367
|
-
throw error;
|
|
1368
|
-
});
|
|
1369
|
-
}
|
|
1370
|
-
return result;
|
|
1371
|
-
};
|
|
1372
|
-
}
|
|
1373
|
-
function wrapAISDK(ai, options) {
|
|
1374
|
-
const client = options?.client ?? getClient2();
|
|
1375
|
-
const config = {
|
|
1376
|
-
defaultAgent: options?.defaultAgent ?? _globalConfig.defaultAgent,
|
|
1377
|
-
userId: options?.userId ?? _globalConfig.userId,
|
|
1378
|
-
convoId: options?.convoId ?? _globalConfig.convoId
|
|
1379
|
-
};
|
|
1380
|
-
return {
|
|
1381
|
-
generateText: ai.generateText ? wrapGenerateText(ai.generateText, client, config) : wrapGenerateText(
|
|
1382
|
-
() => Promise.reject(new Error("generateText not available")),
|
|
1383
|
-
client,
|
|
1384
|
-
config
|
|
1385
|
-
),
|
|
1386
|
-
streamText: ai.streamText ? wrapStreamText(ai.streamText, client, config) : wrapStreamText(() => ({ textStream: (async function* () {
|
|
1387
|
-
})() }), client, config),
|
|
1388
|
-
generateObject: ai.generateObject ? wrapGenerateObject(ai.generateObject, client, config) : wrapGenerateObject(
|
|
1389
|
-
() => Promise.reject(new Error("generateObject not available")),
|
|
1390
|
-
client,
|
|
1391
|
-
config
|
|
1392
|
-
),
|
|
1393
|
-
streamObject: ai.streamObject ? wrapStreamObject(ai.streamObject, client, config) : wrapStreamObject(() => ({}), client, config)
|
|
1394
|
-
};
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// src/wrappers.ts
|
|
1398
|
-
var _currentSessionId = null;
|
|
1399
|
-
var _currentClient = null;
|
|
1400
|
-
var _defaultClient2 = null;
|
|
1401
|
-
function setSessionContext(sessionId, client) {
|
|
1402
|
-
_currentSessionId = sessionId;
|
|
1403
|
-
if (client) {
|
|
1404
|
-
_currentClient = client;
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
function clearSessionContext() {
|
|
1408
|
-
_currentSessionId = null;
|
|
1409
|
-
_currentClient = null;
|
|
1410
|
-
}
|
|
1411
|
-
function getSessionContext() {
|
|
1412
|
-
return _currentSessionId;
|
|
1413
|
-
}
|
|
1414
|
-
function setDefaultClient(client) {
|
|
1415
|
-
_defaultClient2 = client;
|
|
1416
|
-
}
|
|
1417
|
-
function getTrackingClient() {
|
|
1418
|
-
return _currentClient ?? _defaultClient2;
|
|
1419
|
-
}
|
|
1420
|
-
function wrapOpenAI(client, options = {}) {
|
|
1421
|
-
const { trackWithoutSession = false } = options;
|
|
1422
|
-
const chat = client.chat;
|
|
1423
|
-
if (!chat?.completions?.create) {
|
|
1424
|
-
console.warn("Sentrial: OpenAI client does not have chat.completions.create");
|
|
1425
|
-
return client;
|
|
1426
|
-
}
|
|
1427
|
-
const originalCreate = chat.completions.create.bind(chat.completions);
|
|
1428
|
-
chat.completions.create = async function(...args) {
|
|
1429
|
-
const startTime = Date.now();
|
|
1430
|
-
const params = args[0] ?? {};
|
|
1431
|
-
const messages = params.messages ?? [];
|
|
1432
|
-
const model = params.model ?? "unknown";
|
|
1433
|
-
try {
|
|
1434
|
-
const response = await originalCreate(...args);
|
|
1435
|
-
const durationMs = Date.now() - startTime;
|
|
1436
|
-
const promptTokens = response.usage?.prompt_tokens ?? 0;
|
|
1437
|
-
const completionTokens = response.usage?.completion_tokens ?? 0;
|
|
1438
|
-
const totalTokens = response.usage?.total_tokens ?? 0;
|
|
1439
|
-
let outputContent = "";
|
|
1440
|
-
if (response.choices?.[0]?.message?.content) {
|
|
1441
|
-
outputContent = response.choices[0].message.content;
|
|
1442
|
-
}
|
|
1443
|
-
const cost = calculateOpenAICost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
|
|
1444
|
-
trackLLMCall({
|
|
1445
|
-
provider: "openai",
|
|
1446
|
-
model,
|
|
1447
|
-
messages,
|
|
1448
|
-
output: outputContent,
|
|
1449
|
-
promptTokens,
|
|
1450
|
-
completionTokens,
|
|
1451
|
-
totalTokens,
|
|
1452
|
-
cost,
|
|
1453
|
-
durationMs,
|
|
1454
|
-
trackWithoutSession
|
|
1455
|
-
});
|
|
1456
|
-
return response;
|
|
1457
|
-
} catch (error) {
|
|
1458
|
-
const durationMs = Date.now() - startTime;
|
|
1459
|
-
trackLLMError({
|
|
1460
|
-
provider: "openai",
|
|
1461
|
-
model,
|
|
1462
|
-
messages,
|
|
1463
|
-
error,
|
|
1464
|
-
durationMs,
|
|
1465
|
-
trackWithoutSession
|
|
1466
|
-
});
|
|
1467
|
-
throw error;
|
|
1468
|
-
}
|
|
1469
|
-
};
|
|
1470
|
-
return client;
|
|
1471
|
-
}
|
|
1472
|
-
function wrapAnthropic(client, options = {}) {
|
|
1473
|
-
const { trackWithoutSession = false } = options;
|
|
1474
|
-
const messages = client.messages;
|
|
1475
|
-
if (!messages?.create) {
|
|
1476
|
-
console.warn("Sentrial: Anthropic client does not have messages.create");
|
|
1477
|
-
return client;
|
|
1478
|
-
}
|
|
1479
|
-
const originalCreate = messages.create.bind(messages);
|
|
1480
|
-
messages.create = async function(...args) {
|
|
1481
|
-
const startTime = Date.now();
|
|
1482
|
-
const params = args[0] ?? {};
|
|
1483
|
-
const inputMessages = params.messages ?? [];
|
|
1484
|
-
const model = params.model ?? "unknown";
|
|
1485
|
-
const system = params.system ?? "";
|
|
1486
|
-
try {
|
|
1487
|
-
const response = await originalCreate(...args);
|
|
1488
|
-
const durationMs = Date.now() - startTime;
|
|
1489
|
-
const promptTokens = response.usage?.input_tokens ?? 0;
|
|
1490
|
-
const completionTokens = response.usage?.output_tokens ?? 0;
|
|
1491
|
-
const totalTokens = promptTokens + completionTokens;
|
|
1492
|
-
let outputContent = "";
|
|
1493
|
-
if (response.content) {
|
|
1494
|
-
for (const block of response.content) {
|
|
1495
|
-
if (block.type === "text") {
|
|
1496
|
-
outputContent += block.text;
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
const cost = calculateAnthropicCost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
|
|
1501
|
-
const fullMessages = system ? [{ role: "system", content: system }, ...inputMessages] : inputMessages;
|
|
1502
|
-
trackLLMCall({
|
|
1503
|
-
provider: "anthropic",
|
|
1504
|
-
model,
|
|
1505
|
-
messages: fullMessages,
|
|
1506
|
-
output: outputContent,
|
|
1507
|
-
promptTokens,
|
|
1508
|
-
completionTokens,
|
|
1509
|
-
totalTokens,
|
|
1510
|
-
cost,
|
|
1511
|
-
durationMs,
|
|
1512
|
-
trackWithoutSession
|
|
1513
|
-
});
|
|
1514
|
-
return response;
|
|
1515
|
-
} catch (error) {
|
|
1516
|
-
const durationMs = Date.now() - startTime;
|
|
1517
|
-
trackLLMError({
|
|
1518
|
-
provider: "anthropic",
|
|
1519
|
-
model,
|
|
1520
|
-
messages: inputMessages,
|
|
1521
|
-
error,
|
|
1522
|
-
durationMs,
|
|
1523
|
-
trackWithoutSession
|
|
1524
|
-
});
|
|
1525
|
-
throw error;
|
|
1526
|
-
}
|
|
1527
|
-
};
|
|
1528
|
-
return client;
|
|
1529
|
-
}
|
|
1530
|
-
function wrapGoogle(model, options = {}) {
|
|
1531
|
-
const { trackWithoutSession = false } = options;
|
|
1532
|
-
const originalGenerate = model.generateContent;
|
|
1533
|
-
if (!originalGenerate) {
|
|
1534
|
-
console.warn("Sentrial: Google model does not have generateContent");
|
|
1535
|
-
return model;
|
|
1536
|
-
}
|
|
1537
|
-
model.generateContent = async function(...args) {
|
|
2098
|
+
function wrapStreamObject(originalFn, client, config) {
|
|
2099
|
+
return (params) => {
|
|
1538
2100
|
const startTime = Date.now();
|
|
1539
|
-
const
|
|
1540
|
-
const
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
|
|
2101
|
+
const { modelId, provider } = extractModelInfo(params.model);
|
|
2102
|
+
const input = extractInput(params);
|
|
2103
|
+
const sessionPromise = (async () => {
|
|
2104
|
+
try {
|
|
2105
|
+
const id = await client.createSession({
|
|
2106
|
+
name: `streamObject: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
|
|
2107
|
+
agentName: config.defaultAgent ?? "vercel-ai-sdk",
|
|
2108
|
+
userId: config.userId ?? "anonymous",
|
|
2109
|
+
convoId: config.convoId,
|
|
2110
|
+
metadata: {
|
|
2111
|
+
model: modelId,
|
|
2112
|
+
provider,
|
|
2113
|
+
function: "streamObject"
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
if (id) {
|
|
2117
|
+
client.setInput(id, input).catch(() => {
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
return id;
|
|
2121
|
+
} catch {
|
|
2122
|
+
return null;
|
|
2123
|
+
}
|
|
2124
|
+
})();
|
|
2125
|
+
const result = originalFn(params);
|
|
2126
|
+
async function completeStreamObject(obj, error) {
|
|
1544
2127
|
const durationMs = Date.now() - startTime;
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
if (
|
|
1548
|
-
|
|
1549
|
-
|
|
2128
|
+
const sid = await sessionPromise;
|
|
2129
|
+
if (!sid) return;
|
|
2130
|
+
if (error) {
|
|
2131
|
+
await client.trackError({
|
|
2132
|
+
sessionId: sid,
|
|
2133
|
+
errorType: error.name || "Error",
|
|
2134
|
+
errorMessage: error.message || "Unknown error"
|
|
2135
|
+
}).catch(() => {
|
|
2136
|
+
});
|
|
2137
|
+
await client.completeSession({
|
|
2138
|
+
sessionId: sid,
|
|
2139
|
+
success: false,
|
|
2140
|
+
failureReason: error.message || "Unknown error",
|
|
2141
|
+
durationMs
|
|
2142
|
+
}).catch(() => {
|
|
2143
|
+
});
|
|
2144
|
+
return;
|
|
1550
2145
|
}
|
|
1551
|
-
|
|
1552
|
-
let outputContent = "";
|
|
2146
|
+
let usage;
|
|
1553
2147
|
try {
|
|
1554
|
-
|
|
2148
|
+
usage = result.usage ? await result.usage : void 0;
|
|
1555
2149
|
} catch {
|
|
1556
2150
|
}
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
2151
|
+
const promptTokens = usage?.promptTokens ?? 0;
|
|
2152
|
+
const completionTokens = usage?.completionTokens ?? 0;
|
|
2153
|
+
const totalTokens = usage?.totalTokens ?? promptTokens + completionTokens;
|
|
2154
|
+
const cost = calculateCostForCall(provider, modelId, promptTokens, completionTokens);
|
|
2155
|
+
await client.trackEvent({
|
|
2156
|
+
sessionId: sid,
|
|
2157
|
+
eventType: "llm_call",
|
|
2158
|
+
eventData: {
|
|
2159
|
+
model: modelId,
|
|
2160
|
+
provider,
|
|
2161
|
+
prompt_tokens: promptTokens,
|
|
2162
|
+
completion_tokens: completionTokens,
|
|
2163
|
+
total_tokens: totalTokens
|
|
2164
|
+
}
|
|
2165
|
+
}).catch(() => {
|
|
2166
|
+
});
|
|
2167
|
+
await client.completeSession({
|
|
2168
|
+
sessionId: sid,
|
|
2169
|
+
success: true,
|
|
2170
|
+
output: JSON.stringify(obj),
|
|
2171
|
+
durationMs,
|
|
2172
|
+
estimatedCost: cost,
|
|
1563
2173
|
promptTokens,
|
|
1564
2174
|
completionTokens,
|
|
1565
|
-
totalTokens
|
|
1566
|
-
|
|
1567
|
-
durationMs,
|
|
1568
|
-
trackWithoutSession
|
|
2175
|
+
totalTokens
|
|
2176
|
+
}).catch(() => {
|
|
1569
2177
|
});
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
error
|
|
1578
|
-
|
|
1579
|
-
|
|
2178
|
+
}
|
|
2179
|
+
if (result.object) {
|
|
2180
|
+
const originalObjectPromise = result.object;
|
|
2181
|
+
result.object = originalObjectPromise.then(async (obj) => {
|
|
2182
|
+
await completeStreamObject(obj);
|
|
2183
|
+
return obj;
|
|
2184
|
+
}).catch(async (error) => {
|
|
2185
|
+
await completeStreamObject(void 0, error instanceof Error ? error : new Error(String(error)));
|
|
2186
|
+
throw error;
|
|
2187
|
+
});
|
|
2188
|
+
} else if (result.usage) {
|
|
2189
|
+
result.usage.then(async () => {
|
|
2190
|
+
await completeStreamObject(void 0);
|
|
2191
|
+
}).catch(async (error) => {
|
|
2192
|
+
await completeStreamObject(void 0, error instanceof Error ? error : new Error(String(error)));
|
|
1580
2193
|
});
|
|
1581
|
-
throw error;
|
|
1582
2194
|
}
|
|
2195
|
+
return result;
|
|
1583
2196
|
};
|
|
1584
|
-
return model;
|
|
1585
|
-
}
|
|
1586
|
-
function googleContentsToMessages(contents) {
|
|
1587
|
-
if (typeof contents === "string") {
|
|
1588
|
-
return [{ role: "user", content: contents }];
|
|
1589
|
-
}
|
|
1590
|
-
if (Array.isArray(contents)) {
|
|
1591
|
-
return contents.map((item) => {
|
|
1592
|
-
if (typeof item === "string") {
|
|
1593
|
-
return { role: "user", content: item };
|
|
1594
|
-
}
|
|
1595
|
-
if (item && typeof item === "object") {
|
|
1596
|
-
return { role: item.role ?? "user", content: String(item.content ?? item) };
|
|
1597
|
-
}
|
|
1598
|
-
return { role: "user", content: String(item) };
|
|
1599
|
-
});
|
|
1600
|
-
}
|
|
1601
|
-
return [{ role: "user", content: String(contents) }];
|
|
1602
2197
|
}
|
|
1603
|
-
function
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
2198
|
+
function wrapAISDK(ai, options) {
|
|
2199
|
+
const client = options?.client ?? getClient2();
|
|
2200
|
+
const config = {
|
|
2201
|
+
defaultAgent: options?.defaultAgent ?? _globalConfig.defaultAgent,
|
|
2202
|
+
userId: options?.userId ?? _globalConfig.userId,
|
|
2203
|
+
convoId: options?.convoId ?? _globalConfig.convoId
|
|
2204
|
+
};
|
|
2205
|
+
return {
|
|
2206
|
+
generateText: ai.generateText ? wrapGenerateText(ai.generateText, client, config) : wrapGenerateText(
|
|
2207
|
+
() => Promise.reject(new Error("generateText not available")),
|
|
2208
|
+
client,
|
|
2209
|
+
config
|
|
2210
|
+
),
|
|
2211
|
+
streamText: ai.streamText ? wrapStreamText(ai.streamText, client, config) : wrapStreamText(() => ({ textStream: (async function* () {
|
|
2212
|
+
})() }), client, config),
|
|
2213
|
+
generateObject: ai.generateObject ? wrapGenerateObject(ai.generateObject, client, config) : wrapGenerateObject(
|
|
2214
|
+
() => Promise.reject(new Error("generateObject not available")),
|
|
2215
|
+
client,
|
|
2216
|
+
config
|
|
2217
|
+
),
|
|
2218
|
+
streamObject: ai.streamObject ? wrapStreamObject(ai.streamObject, client, config) : wrapStreamObject(() => ({}), client, config)
|
|
2219
|
+
};
|
|
1615
2220
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
content: params.output,
|
|
1634
|
-
tokens: {
|
|
1635
|
-
prompt: params.promptTokens,
|
|
1636
|
-
completion: params.completionTokens,
|
|
1637
|
-
total: params.totalTokens
|
|
1638
|
-
},
|
|
1639
|
-
cost_usd: params.cost
|
|
1640
|
-
},
|
|
1641
|
-
reasoning: `LLM call to ${params.provider} ${params.model}`,
|
|
1642
|
-
estimatedCost: params.cost,
|
|
1643
|
-
tokenCount: params.totalTokens,
|
|
1644
|
-
metadata: {
|
|
1645
|
-
provider: params.provider,
|
|
1646
|
-
model: params.model,
|
|
1647
|
-
duration_ms: params.durationMs,
|
|
1648
|
-
prompt_tokens: params.promptTokens,
|
|
1649
|
-
completion_tokens: params.completionTokens
|
|
1650
|
-
}
|
|
1651
|
-
}).catch((err) => {
|
|
1652
|
-
console.warn("Sentrial: Failed to track LLM call:", err.message);
|
|
2221
|
+
|
|
2222
|
+
// src/claude-code.ts
|
|
2223
|
+
function wrapClaudeAgent(queryFn, wrapOptions) {
|
|
2224
|
+
const {
|
|
2225
|
+
client,
|
|
2226
|
+
defaultAgent = "claude-agent",
|
|
2227
|
+
userId = "anonymous",
|
|
2228
|
+
convoId,
|
|
2229
|
+
extraMetadata
|
|
2230
|
+
} = wrapOptions;
|
|
2231
|
+
return function wrappedQuery(params) {
|
|
2232
|
+
const { prompt, options = {} } = params;
|
|
2233
|
+
const startTime = Date.now();
|
|
2234
|
+
let sessionId = null;
|
|
2235
|
+
let resolveSessionReady;
|
|
2236
|
+
const sessionReady = new Promise((resolve) => {
|
|
2237
|
+
resolveSessionReady = resolve;
|
|
1653
2238
|
});
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
2239
|
+
const sessionName = typeof prompt === "string" ? `${defaultAgent}: ${prompt.slice(0, 100)}` : `${defaultAgent} session`;
|
|
2240
|
+
const pendingToolCalls = [];
|
|
2241
|
+
const sentrialToolHook = {
|
|
2242
|
+
hooks: [
|
|
2243
|
+
async (input, toolUseID, _opts) => {
|
|
2244
|
+
await sessionReady;
|
|
2245
|
+
if (!sessionId) return;
|
|
2246
|
+
const toolOutput = input?.tool_response && typeof input.tool_response === "object" ? input.tool_response : { response: input?.tool_response ?? null };
|
|
2247
|
+
const p = client.trackToolCall({
|
|
2248
|
+
sessionId,
|
|
2249
|
+
toolName: input?.tool_name ?? "unknown",
|
|
2250
|
+
toolInput: input?.tool_input ?? {},
|
|
2251
|
+
toolOutput,
|
|
2252
|
+
metadata: { tool_use_id: toolUseID }
|
|
2253
|
+
}).catch(() => {
|
|
2254
|
+
});
|
|
2255
|
+
pendingToolCalls.push(p);
|
|
2256
|
+
}
|
|
2257
|
+
]
|
|
2258
|
+
};
|
|
2259
|
+
const sentrialToolFailureHook = {
|
|
2260
|
+
hooks: [
|
|
2261
|
+
async (input, toolUseID, _opts) => {
|
|
2262
|
+
await sessionReady;
|
|
2263
|
+
if (!sessionId) return;
|
|
2264
|
+
const p = client.trackToolCall({
|
|
2265
|
+
sessionId,
|
|
2266
|
+
toolName: input?.tool_name ?? "unknown",
|
|
2267
|
+
toolInput: input?.tool_input ?? {},
|
|
2268
|
+
toolOutput: {},
|
|
2269
|
+
toolError: { message: input?.error ?? "unknown error" },
|
|
2270
|
+
metadata: { tool_use_id: toolUseID }
|
|
2271
|
+
}).catch(() => {
|
|
2272
|
+
});
|
|
2273
|
+
pendingToolCalls.push(p);
|
|
2274
|
+
}
|
|
2275
|
+
]
|
|
2276
|
+
};
|
|
2277
|
+
const mergedHooks = {
|
|
2278
|
+
...options.hooks ?? {}
|
|
2279
|
+
};
|
|
2280
|
+
const existingPostToolUse = mergedHooks.PostToolUse ?? [];
|
|
2281
|
+
mergedHooks.PostToolUse = [...existingPostToolUse, sentrialToolHook];
|
|
2282
|
+
const existingPostToolUseFailure = mergedHooks.PostToolUseFailure ?? [];
|
|
2283
|
+
mergedHooks.PostToolUseFailure = [...existingPostToolUseFailure, sentrialToolFailureHook];
|
|
2284
|
+
const mergedOptions = {
|
|
2285
|
+
...options,
|
|
2286
|
+
hooks: mergedHooks
|
|
2287
|
+
};
|
|
2288
|
+
const generator = queryFn({ prompt, options: mergedOptions });
|
|
2289
|
+
return (async function* () {
|
|
2290
|
+
try {
|
|
2291
|
+
for await (const message of generator) {
|
|
2292
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
2293
|
+
const metadata = {
|
|
2294
|
+
model: message.model,
|
|
2295
|
+
tools: message.tools,
|
|
2296
|
+
cwd: message.cwd,
|
|
2297
|
+
mcp_servers: message.mcp_servers,
|
|
2298
|
+
sdk_session_id: message.session_id,
|
|
2299
|
+
...extraMetadata ?? {}
|
|
2300
|
+
};
|
|
2301
|
+
try {
|
|
2302
|
+
sessionId = await client.createSession({
|
|
2303
|
+
name: sessionName,
|
|
2304
|
+
agentName: defaultAgent,
|
|
2305
|
+
userId,
|
|
2306
|
+
convoId,
|
|
2307
|
+
metadata
|
|
2308
|
+
});
|
|
2309
|
+
} catch {
|
|
2310
|
+
sessionId = null;
|
|
2311
|
+
}
|
|
2312
|
+
resolveSessionReady();
|
|
2313
|
+
}
|
|
2314
|
+
if (message.type === "result" && sessionId) {
|
|
2315
|
+
const isError = !!message.is_error;
|
|
2316
|
+
const inputTokens = message.usage?.input_tokens ?? 0;
|
|
2317
|
+
const outputTokens = message.usage?.output_tokens ?? 0;
|
|
2318
|
+
let failureReason;
|
|
2319
|
+
if (isError) {
|
|
2320
|
+
if (message.errors && message.errors.length > 0) {
|
|
2321
|
+
failureReason = message.errors.join("; ");
|
|
2322
|
+
} else {
|
|
2323
|
+
failureReason = message.subtype;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
await Promise.allSettled(pendingToolCalls);
|
|
2327
|
+
try {
|
|
2328
|
+
await client.completeSession({
|
|
2329
|
+
sessionId,
|
|
2330
|
+
success: !isError,
|
|
2331
|
+
failureReason,
|
|
2332
|
+
estimatedCost: message.total_cost_usd,
|
|
2333
|
+
promptTokens: inputTokens,
|
|
2334
|
+
completionTokens: outputTokens,
|
|
2335
|
+
totalTokens: inputTokens + outputTokens,
|
|
2336
|
+
durationMs: message.duration_ms ?? Date.now() - startTime,
|
|
2337
|
+
userInput: typeof prompt === "string" ? prompt : void 0,
|
|
2338
|
+
output: message.result,
|
|
2339
|
+
customMetrics: {
|
|
2340
|
+
num_turns: message.num_turns ?? 0,
|
|
2341
|
+
duration_api_ms: message.duration_api_ms ?? 0
|
|
2342
|
+
}
|
|
2343
|
+
});
|
|
2344
|
+
} catch {
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
yield message;
|
|
2348
|
+
}
|
|
2349
|
+
} catch (error) {
|
|
2350
|
+
if (sessionId) {
|
|
2351
|
+
await Promise.allSettled(pendingToolCalls);
|
|
2352
|
+
try {
|
|
2353
|
+
await client.completeSession({
|
|
2354
|
+
sessionId,
|
|
2355
|
+
success: false,
|
|
2356
|
+
failureReason: error instanceof Error ? error.message : String(error),
|
|
2357
|
+
durationMs: Date.now() - startTime
|
|
2358
|
+
});
|
|
2359
|
+
} catch {
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
throw error;
|
|
1673
2363
|
}
|
|
1674
|
-
})
|
|
1675
|
-
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
2364
|
+
})();
|
|
2365
|
+
};
|
|
1678
2366
|
}
|
|
1679
2367
|
|
|
1680
2368
|
// src/decorators.ts
|
|
1681
2369
|
var _defaultClient3 = null;
|
|
1682
|
-
var _currentInteraction = null;
|
|
2370
|
+
var _currentInteraction = createContextVar(null);
|
|
1683
2371
|
function getClient3() {
|
|
1684
2372
|
if (!_defaultClient3) {
|
|
1685
2373
|
try {
|
|
@@ -1699,7 +2387,7 @@ function getCurrentSessionId() {
|
|
|
1699
2387
|
return getSessionContext();
|
|
1700
2388
|
}
|
|
1701
2389
|
function getCurrentInteraction() {
|
|
1702
|
-
return _currentInteraction;
|
|
2390
|
+
return _currentInteraction.get();
|
|
1703
2391
|
}
|
|
1704
2392
|
function withTool(name, fn) {
|
|
1705
2393
|
const isAsync = fn.constructor.name === "AsyncFunction";
|
|
@@ -1800,10 +2488,11 @@ function withSession(agentName, fn, options = {}) {
|
|
|
1800
2488
|
input: userInput
|
|
1801
2489
|
});
|
|
1802
2490
|
const sessionId = interaction.getSessionId();
|
|
2491
|
+
let sessionTokens;
|
|
1803
2492
|
if (sessionId) {
|
|
1804
|
-
|
|
2493
|
+
sessionTokens = _setSessionContextWithTokens(sessionId, client);
|
|
1805
2494
|
}
|
|
1806
|
-
|
|
2495
|
+
const interactionToken = _currentInteraction.set(interaction);
|
|
1807
2496
|
try {
|
|
1808
2497
|
const result = await fn(...args);
|
|
1809
2498
|
let output;
|
|
@@ -1828,8 +2517,10 @@ function withSession(agentName, fn, options = {}) {
|
|
|
1828
2517
|
});
|
|
1829
2518
|
throw error;
|
|
1830
2519
|
} finally {
|
|
1831
|
-
|
|
1832
|
-
|
|
2520
|
+
if (sessionTokens) {
|
|
2521
|
+
_restoreSessionContext(sessionTokens);
|
|
2522
|
+
}
|
|
2523
|
+
_currentInteraction.reset(interactionToken);
|
|
1833
2524
|
}
|
|
1834
2525
|
};
|
|
1835
2526
|
}
|
|
@@ -1915,10 +2606,11 @@ function TrackSession(agentName, options) {
|
|
|
1915
2606
|
input: userInput
|
|
1916
2607
|
});
|
|
1917
2608
|
const sessionId = interaction.getSessionId();
|
|
2609
|
+
let sessionTokens;
|
|
1918
2610
|
if (sessionId) {
|
|
1919
|
-
|
|
2611
|
+
sessionTokens = _setSessionContextWithTokens(sessionId, client);
|
|
1920
2612
|
}
|
|
1921
|
-
|
|
2613
|
+
const interactionToken = _currentInteraction.set(interaction);
|
|
1922
2614
|
try {
|
|
1923
2615
|
const result = await originalMethod.apply(this, args);
|
|
1924
2616
|
let output;
|
|
@@ -1943,8 +2635,10 @@ function TrackSession(agentName, options) {
|
|
|
1943
2635
|
});
|
|
1944
2636
|
throw error;
|
|
1945
2637
|
} finally {
|
|
1946
|
-
|
|
1947
|
-
|
|
2638
|
+
if (sessionTokens) {
|
|
2639
|
+
_restoreSessionContext(sessionTokens);
|
|
2640
|
+
}
|
|
2641
|
+
_currentInteraction.reset(interactionToken);
|
|
1948
2642
|
}
|
|
1949
2643
|
};
|
|
1950
2644
|
return descriptor;
|
|
@@ -1957,6 +2651,8 @@ var SessionContext = class {
|
|
|
1957
2651
|
client;
|
|
1958
2652
|
interaction = null;
|
|
1959
2653
|
output;
|
|
2654
|
+
sessionTokens;
|
|
2655
|
+
interactionToken;
|
|
1960
2656
|
constructor(options) {
|
|
1961
2657
|
this.userId = options.userId;
|
|
1962
2658
|
this.agent = options.agent;
|
|
@@ -1975,9 +2671,9 @@ var SessionContext = class {
|
|
|
1975
2671
|
});
|
|
1976
2672
|
const sessionId = this.interaction.getSessionId();
|
|
1977
2673
|
if (sessionId) {
|
|
1978
|
-
|
|
2674
|
+
this.sessionTokens = _setSessionContextWithTokens(sessionId, this.client);
|
|
1979
2675
|
}
|
|
1980
|
-
|
|
2676
|
+
this.interactionToken = _currentInteraction.set(this.interaction);
|
|
1981
2677
|
return this;
|
|
1982
2678
|
}
|
|
1983
2679
|
/**
|
|
@@ -1997,8 +2693,12 @@ var SessionContext = class {
|
|
|
1997
2693
|
failureReason: options?.error
|
|
1998
2694
|
});
|
|
1999
2695
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2696
|
+
if (this.sessionTokens) {
|
|
2697
|
+
_restoreSessionContext(this.sessionTokens);
|
|
2698
|
+
}
|
|
2699
|
+
if (this.interactionToken) {
|
|
2700
|
+
_currentInteraction.reset(this.interactionToken);
|
|
2701
|
+
}
|
|
2002
2702
|
}
|
|
2003
2703
|
/**
|
|
2004
2704
|
* Get the session ID
|
|
@@ -2052,30 +2752,31 @@ function serializeOutput(value) {
|
|
|
2052
2752
|
}
|
|
2053
2753
|
|
|
2054
2754
|
// src/context.ts
|
|
2055
|
-
var _experimentContext = null;
|
|
2755
|
+
var _experimentContext = createContextVar(null);
|
|
2056
2756
|
function getSystemPrompt(defaultPrompt) {
|
|
2057
|
-
|
|
2058
|
-
|
|
2757
|
+
const ctx = _experimentContext.get();
|
|
2758
|
+
if (ctx?.systemPrompt) {
|
|
2759
|
+
return ctx.systemPrompt;
|
|
2059
2760
|
}
|
|
2060
2761
|
return defaultPrompt ?? "";
|
|
2061
2762
|
}
|
|
2062
2763
|
function getExperimentContext() {
|
|
2063
|
-
return _experimentContext;
|
|
2764
|
+
return _experimentContext.get();
|
|
2064
2765
|
}
|
|
2065
2766
|
function isExperimentMode() {
|
|
2066
|
-
return _experimentContext !== null;
|
|
2767
|
+
return _experimentContext.get() !== null;
|
|
2067
2768
|
}
|
|
2068
2769
|
function getVariantName() {
|
|
2069
|
-
return _experimentContext?.variantName ?? null;
|
|
2770
|
+
return _experimentContext.get()?.variantName ?? null;
|
|
2070
2771
|
}
|
|
2071
2772
|
function getExperimentId() {
|
|
2072
|
-
return _experimentContext?.experimentId ?? null;
|
|
2773
|
+
return _experimentContext.get()?.experimentId ?? null;
|
|
2073
2774
|
}
|
|
2074
2775
|
function setExperimentContext(context) {
|
|
2075
|
-
_experimentContext
|
|
2776
|
+
_experimentContext.set(context);
|
|
2076
2777
|
}
|
|
2077
2778
|
function clearExperimentContext() {
|
|
2078
|
-
_experimentContext
|
|
2779
|
+
_experimentContext.set(null);
|
|
2079
2780
|
}
|
|
2080
2781
|
|
|
2081
2782
|
// src/experiment.ts
|
|
@@ -2409,6 +3110,7 @@ var Experiment = class {
|
|
|
2409
3110
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2410
3111
|
0 && (module.exports = {
|
|
2411
3112
|
ApiError,
|
|
3113
|
+
EventBatcher,
|
|
2412
3114
|
EventType,
|
|
2413
3115
|
Experiment,
|
|
2414
3116
|
ExperimentRunTracker,
|
|
@@ -2428,6 +3130,7 @@ var Experiment = class {
|
|
|
2428
3130
|
clearSessionContext,
|
|
2429
3131
|
configure,
|
|
2430
3132
|
configureVercel,
|
|
3133
|
+
createContextVar,
|
|
2431
3134
|
getCurrentInteraction,
|
|
2432
3135
|
getCurrentSessionId,
|
|
2433
3136
|
getExperimentContext,
|
|
@@ -2450,6 +3153,7 @@ var Experiment = class {
|
|
|
2450
3153
|
withTool,
|
|
2451
3154
|
wrapAISDK,
|
|
2452
3155
|
wrapAnthropic,
|
|
3156
|
+
wrapClaudeAgent,
|
|
2453
3157
|
wrapGoogle,
|
|
2454
3158
|
wrapLLM,
|
|
2455
3159
|
wrapOpenAI
|